How do I schedule a Rule using Fields? - drupal

I'm using the Commerce and Rules modules, and I'd like to keep in touch with my clients after they buy things from my store.
I would like to add a follow_up_date Field to the Product entity, and use that to schedule Rules. I've set up the rules, I've used the Entity has field condition so that follow_up_date gets loaded and can be read from and edited, but it doesn't appear as a data selector for scheduling components.
I've tried using the Date and Integer Date modules, and with datestamps of various kinds and granularities, but no dice.
How can I schedule a Rule using data from a Field?

Depending on how the access the product, fx via an order, a Rules bug/flaw is probably what's stopping you, see the issue on d.o.
There is a work around to fix this by using hook_entity_property_info_alter. Something like this should do the trick for you:
/**
* Implements hook_entity_property_info_alter() on top of the Line Item module.
*/
function module_entity_property_info_alter(&$info) {
// Add custom discount fields to circumvent a rules bug.
$properties = array();
foreach ($info['commerce_product']['bundles'] as $bundle => $bundle_info) {
$bundle_info += array('properties' => array());
$properties += $bundle_info['properties'];
}
$info['commerce_product']['properties']['field_NAME'] = $properties['field_NAME'];
}

Related

Silverstripe has_one CMS dropdown filter before dropdown_field_threshold

I have a has_one in one of my models. But it exceeds the dropdown_field_threshold.
But in the getCMSFields method I have changed the query for that dropdown. But since it already exceeds the threshold before this method is called, the dropdown is converted to NumericDropdown or something like that.
Is there a way to alter the query before the threshold gets checked?
By the way, I know I can make a custom dropdown with my own query. But Silverstripe already handles a lot, so it would be better to just alter the query.
Edit: Example code
public function getCMSFields() {
$fields = parent::getCMSFields();
$questionnaire = $this->QuestionnaireSection()->Questionnaire();
$nextQuestionOptions = $questionnaire->Sections();
/** #var DropdownField $dropdownField */
$dropdownField = $fields->dataFieldByName("NextQuestionID");
$dropdownField->setSource($nextQuestionOptions->map()->toArray());
return $fields;
}
Unfortunately it is not possible to alter this query within the context of the class. An alternative may be to write your own scaffold function instead of calling parent::getCMSFields, but that is not really advised in this case.
If you have some way to obtain the current Questionnaire object globally, you could add an augmentDataQueryCreation function like below to QuestionnaireSection to add a where clause to the query that is being executed. Be aware however that this function is called in every case that QuestionnaireSection::get() is executed.
public function augmentDataQueryCreation(SQLSelect $query, DataQuery $dataQuery){
$baseTable = $this->baseTable();
$filter = 1; //Your global param here
$dataQuery->where("\"$baseTable\".\"QuestionnaireID\" = $filter");
}
An alternative to prevent the DropdownField from being replaced by a NumericField is to change the threshold of this switch by adding the following code to your mysite.yml. This does not solve your problem entirely, but is a good workaround.
SilverStripe\ORM\FieldType\DBForeignKey:
dropdown_field_threshold: 100000

Is it possible to have versioned many_many relations?

I already used versioning on DataObjects when they contain a lot of content, now I'm wondering if it's possible to apply versioning to a many_many relation?
Assuming I have the following:
class Page extends SiteTree
{
private static $many_many = array(
'Images' => 'Image'
);
}
Then the ORM will create a Page_Images table for me to store the relations. In order to have a versioned relation, more tables would be required (eg. Page_Images_Live).
Is there any way to tell the ORM to create versioned relations? When looking at the above example with a Page * – * Images relation, I don't want the Image class to be versioned, but rather the relation. Eg. something like this:
Version Stage:
---
PageA
Images ( ImageA, ImageB, ImageC )
Version Live:
---
PageA
Images ( ImageA, ImageC, ImageD, ImageE )
Is that even possible out of the box?
I've spent a lot of time looking into this and without fundamentally modifying ManyManyList (as it doesn't expose the necessary hooks through the extension system), there isn't many choices.
I am a dessert-first kind of person, how CAN we do it?
My only suggestion to accomplish this feat is essentially a many-to-many bridge object (ie. a separate entity joining Page and Image) via $has_many though it still requires quite a bit of modification.
This is partially discussed on the forum where a solution about subverting the actual relationship by storing the versioned items against the actual object rather than in a joining table. That would work but I think we can still do better than that.
I am personally leaning towards tying the version of the relationship to the Page itself and my partial solution below covers this. Read below the fold for more info trying this as an update to ManyManyList.
Something like this is a start:
class PageImageVersion extends DataObject
{
private static $db = array(
'Version' => 'Int'
);
private static $has_one = array(
'Page' => 'Page',
'Image' => 'Image'
);
}
This contains our 2-way relationship plus we have our version number stored. You will want to specify the getCMSFields function to add the right fields required allowing you to relate it to an existing image or upload a new one. I am avoiding covering this as it should be relatively straight forward compared to the actual version handling part.
Now, we have a has_many on Page like so:
private static $has_many = array(
'Images' => 'PageImageVersion'
);
In my tests, I also added an extension for Image adding the matching $has_many onto it as well like so:
class ImageExtension extends DataExtension
{
private static $has_many = array(
'Pages' => 'PageImageVersion'
);
}
Honestly, not sure if this is necessary beyond adding the Pages
function on the Image side of the relationship. As far as I can see, it won't really matter for this particular usecase.
Unfortunately, because of this way of versioning, we can't use the standard way of calling the Images, we will need to be a bit creative. Something like this:
public function getVersionedImages($Version = null)
{
if ($Version == null)
{
$Version = $this->Version;
}
else if ($Version < 0)
{
$Version = max($this->Version - $Version, 1);
}
return $this->Images()->filter(array('Version' => $Version));
}
When you call getVersionedImages(), it will return all images that have the Version set on it aligning with the version of the current page. Also supports getting previous versions via getVersionedImages(-1) for the last version or even gets images for a specific version of the page by passing any position number.
OK, so far so good. We now need to make sure that every page write we are getting a duplicate list of images for this new version of the page.
With an onAfterWrite function on Page, we can do this:
public function onAfterWrite()
{
$lastVersionImages = $this->getVersionedImages(-1);
foreach ($lastVersionImages as $image)
{
$duplicate = $image->duplicate(false);
$duplicate->Version = $this->Version;
$duplicate->write();
}
}
For those playing at home, this is where things get a bit iffy relating to how restoring previous versions of Page would affect this.
Because we would be editing this in GridField, we will need to do a few things. First is make sure our code can handle the Add New function.
My idea is an onAfterWrite on the PageImageVersion object:
public function onAfterWrite()
{
//Make sure the version is actually saved
if ($this->Version == 0)
{
$this->Version = $this->Page()->Version;
$this->write();
}
}
To get your versioned items displaying in GridField, you would have it set up similar to this:
$gridFieldConfig = GridFieldConfig_RecordEditor::create();
$gridField = new GridField("Images", "Images", $this->getVersionedImages(), $gridFieldConfig);
$fields->addFieldToTab("Root.Images", $gridField);
You might want to link to images directly from the GridField via GridFieldConfig_RelationEditor however this is when things get sour.
Time for the veggies...
One of the big difficulties is GridField, for both linking and unlinking these entities. Using the standard GridFieldDeleteAction will directly update the relationship without the right version.
You will need to extend GridFieldDeleteAction and override the handleAction to write your Page object (to trigger another version), duplicate every version of our versioned image object for the last version while making it skip the one you don't want in the new version.
I'll admit, this last bit is just guesswork by me. From my understanding and debugging, it should work but simply there is a lot of fiddling to get it right.
Your extension of GridFieldDeleteAction then needs to be added to your specific GridField.
This would essentially be your last step away from making this solution work. Once you have the adding, removing, duplicating, version updating part down, it really is a matter of just using getVersionedImages() to get the right images.
Conclusion
Avoid. I get why you want to do this but I really don't see a clean way of being able to handle this without a decent sized update to how many_many relationships are handled in Silverstripe.
But I really want it as a ManyManyList!
The changes I see required for ManyManyList are having a 3-way key (Foreign Key, Local Key, Version Key) and the various methods for adding/removing/fetching etc updated.
If there were hooks in the add and remove functions, you might be able to sneak in the functionality as an extension (via Silverstripe's extension system) and add the needed data to the extra fields that many_many relationships allow.
While I could get this happening by extending ManyManyList directly and then forcing ManyManyList to be replaced with my custom class via Object::useCustomClass, it would be even more of a messy solution.
It is simply too long/complex for me to give a full answer for a pure ManyManyList solution at this stage (though I may get back to this later and give it a shot).
Disclaimer: I am not a Silverstripe Core dev, there may be a neater solution to this entire thing but I simply can't see how.
You can define second relation with "_Live" suffix and update it when the page is published. Note: This solution stores only two versions (live and stage).
Bellow is my implementation which automatically detects whether many-many relation is versioned or not. It then handles publishing and data retrieval. All what is needed is to define one extra many-many relation with "_Live" suffix.
$page->Images() returns items according to the current stage (stage/live).
class Page extends SiteTree
{
private static $many_many = array(
'Images' => 'Image',
'Images_Live' => 'Image'
);
public function publish($fromStage, $toStage, $createNewVersion = false)
{
if ($toStage == 'Live')
{
$this->publishManyToManyComponents();
}
parent::publish($fromStage, $toStage, $createNewVersion);
}
protected function publishManyToManyComponents()
{
foreach (static::getVersionedManyManyComponentNames() as $component_name)
{
$this->publishManyToManyComponent($component_name);
}
}
protected function publishManyToManyComponent($component_name)
{
$stage = $this->getManyManyComponents($component_name);
$live = $this->getManyManyComponents("{$component_name}_Live");
$live_table = $live->getJoinTable();
$live_fk = $live->getForeignKey();
$live_lk = $live->getLocalKey();
$stage_table = $stage->getJoinTable();
$stage_fk = $live->getForeignKey();
$stage_lk = $live->getLocalKey();
// update or add items from stage to live
foreach ($stage as $item)
{
$live->add($item, $stage->getExtraData(null, $item->ID));
}
// delete remaining items from live table
DB::query("DELETE l FROM $live_table AS l LEFT JOIN $stage_table AS s ON l.$live_fk = s.$stage_fk AND l.$live_lk = s.$stage_lk WHERE s.ID IS NULL");
// update new items IDs in live table (IDs are incremental so the new records can only have higher IDs than items in ID => should not cause duplicate IDs)
DB::query("UPDATE $live_table AS l INNER JOIN $stage_table AS s ON l.$live_fk = s.$stage_fk AND l.$live_lk = s.$stage_lk SET l.ID = s.ID WHERE l.ID != s.ID;");
}
public function manyManyComponent($component_name)
{
if (Versioned::current_stage() == 'Live' && static::isVersionedManyManyComponent($component_name))
{
return parent::manyManyComponent("{$component_name}_Live");
}
else
{
return parent::manyManyComponent($component_name);
}
}
protected static function isVersionedManyManyComponent($component_name)
{
$many_many_components = (array) Config::inst()->get(static::class, 'many_many', Config::INHERITED);
return isset($many_many_components[$component_name]) && isset($many_many_components["{$component_name}_Live"]);
}
protected static function getVersionedManyManyComponentNames()
{
$many_many_components = (array) Config::inst()->get(static::class, 'many_many', Config::INHERITED);
foreach ($many_many_components as $component_name => $dummy)
{
$is_live = 0;
$stage_component_name = preg_replace('/_Live$/', '', $component_name, -1, $is_live);
if ($is_live > 0 && isset($many_many_components[$stage_component_name]))
{
yield $stage_component_name;
}
}
}
}

Hide cck field based on role

Im looking for a way to hide a cck field for every one except for one specific role.
I know that there is a module, Content Permission module, that takes good care of this. But I have taken over a very big site with many content types, with lots of related cck fields being defined. So installing Content Permission module is not a good idea because of the great amount of settings it would require.
It's a drupal 6 installation.
You may use hook_nodeapi in a custom module:
/**
* Implements hook_nodeapi().
*/
function yourmodule_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
switch ($op) {
case 'view':
if (! user_access('show restricted content')) {
unset ($node->content['field_restrictedcontent']);
}
break;
}
}
/**
* Implements hook_perm().
*/
function yourmodule_perm () {
return array(
'show restricted content',
);
}
Nevertheless, be aware that this is somewhat a hack: I think you should reconsider using Content Permission module for your site, and make the effort needed to configure it for your node types. It's a one time job and it may protect you from compatibilities issues with other modules in your site.
You need to use any of the permissions module and reconfigure each of those fields in question. With code, you have to check user roles for each of those fields!

Need advice to get my logic correct in d7

Goal: Is to save information of a node which gets updated. We need to gather the node id of the node which is updated and also the user names of people who have bookmarked it.
Implementation:
I have managed to get the both this detail using flags and rules module. I made a custom module which implemented the hook to get this info.
I am getting stuck here:
Now I need to save the user name and the node id. I am still deciding if I want to use fields or the db layer.
One username can have multiple node id saved.
Now the problem is I don't know for sure how many nodes will be enough. It depends on the user. It can be 5 can be 500 or even 5000 node ids that might need to be saved for one user.
So how do I make provision for this ?
So I am stuck with the logic. How should I use the db layer or the fields in custom content type to save this ? and how should I do it ?
Please advice. I am using d7.
custom module code
/*
* Implementation of the hook_rules_action_info()
*
*/
function customvishal_rules_action_info()
{
$actions = array(
'customvishal_action_userdetail' => array(
'label' =>t('Custom function to send notifications'),
'group'=>t('Cusotm Code for sending notifications'),
'parameter'=> array(
'account'=> array(
'type'=>'user',
'label'=>t('Going to get user list'),
),
// for the node
'productdetail'=> array(
'type'=>'node',
'label'=>t('Passding the node data'),
),
)
),
);
return $actions;
}
/*
* The action function for the rules exampled hello world
*
*/
function customvishal_action_userdetail($account,$productdetail)
{
drupal_set_message(t('This user #username! has flagged it',
array('#username' => $account->mail)));
drupal_set_message(t('This node #nid has got updated',
array('#nid' => $productdetail->nid)));
// The above takes care of the node and the user information later I will put
// it in a db or something like that.
// end of the function customvishal_action_userdetail
}
It really seems like you should be using hook_node_update() and hook_node_insert() to for access to nodes that have just been added or updated.
If you wanted access to the node data just before it was saved, then hook_node_presave() would be the one to use.
I don't think you need presave though because you mentioned you needed the node ID, and presave does not have that for new nodes yet.
Here's a way to process new and updated nodes. The first 2 functions just hook into the right place and route the node to the 3rd function.
<?php
// hook into node inserts
function customvishal_node_insert($node) {
if ($node->type == 'mynodetype') {
customvishal_handle_data($node);
}
}
// hook into node updates
function customvishal_node_update($node) {
if ($node->type == 'mynodetype') {
customvishal_handle_data($node);
}
}
// custom handler for the nodes
function customvishal_handle_data($node) {
// load a user object of the node's author
$author = user_load($node->uid);
// now do what we need to do with $node and $user data
}
Remember you need to clear the Drupal cache for new hooks in your module to work in D7.

Removing [nid:n] in nodereference autocomplete

Using the autocomplete field for a cck nodereference always displays the node id as a cryptic bracketed extension:
Page Title [nid:23]
I understand that this ensures that selections are unique in case nodes have the same title, but obviously this is a nasty thing to expose to the user.
Has anyone had any success in removing these brackets, or adding a different unique identifier?
Ultimately, you need to change the output of nodereference_autocomplete() in nodereference.module.
To do this properly, you want a custom module to cleanly override the function.
This function is defined as a menu callback, thus,
/**
* Implementation of hook_menu_alter().
*/
function custom_module_menu_alter(&$items) {
$items['nodereference/autocomplete']['page callback'] = 'custom_module_new_nodereference_autocomplete';
}
Then, copy the nodereference_autocomplete function into your custom module, changing it's name to match your callback. Then change this one line:
$matches[$row['title'] ." [nid:$id]"] = '<div class="reference-autocomplete">'. $row['rendered'] . '</div>';
Dropping the nid reference.
$matches[$row['title']] = '<div class="reference-autocomplete">'. $row['rendered'] . '</div>';
I believe the identifier is purely cosmetic at this point, which means you could also change the text however you like. If it is not purely cosmetic, well, I haven't tested to see what will happen in the wrong conditions.
I always meant to identify how to do this. Thank you for motivating me with your question.
What Grayside has posted will work... as long as you don't have two nodes with the same title. In other words, if you want to do as Grayside has proposed, you need to be aware that the nid is not entirely unimportant. The nodereference_autocomplete_validate() function does two things. It checks to see if there is a node that matches, and if so, it passes the nid on, setting it to the $form_state array. If it can't find a node, it will set an error. If the nid is present, it will be used to get the node, which also is faster, the code is here:
preg_match('/^(?:\s*|(.*) )?\[\s*nid\s*:\s*(\d+)\s*\]$/', $value, $matches);
if (!empty($matches)) {
// Explicit [nid:n].
list(, $title, $nid) = $matches;
if (!empty($title) && ($n = node_load($nid)) && $title != $n->title) {
form_error($element[$field_key], t('%name: title mismatch. Please check your selection.', array('%name' => t($field['widget']['label']))));
}
}
This just checks to see if there is a nid and checks if that node matches with the title, if so the nid is passed on.
The 2nd option is a bit slower, but it is here errors can happen. If you follow the execution, you will see, that if will try to find a node based on title alone, and will take the first node that matches. The result of this, is that if you have two nodes with the same title, one of them will always be used. This might not be a problem for you, but the thing is, that you will never find out if this happens. Everything will work just fine and the user will think that he selected the node he wanted to. This might be the case, but he might as well have chosen the wrong node.
So in short, you can get rid of the nid in the autocomplete callback, but it has 2 drawbacks:
performance (little)
uncertainty in selecting the correct node.
So you have to think about it, before going this route. Especially, since you most likely wont be able to find the problem of the selection of the wrong nodes, should it happen. Another thing to be aware of, is that the nid showing up, also brings some valuable info to the users, a quick way to lookup the node, should they be in doubt if it is the one they want, if several nodes have similar titles.
I got Grayside's answer to work, but I had to use MENU alter, instead of the FORM alter he posted. No biggy!
function custommodule_menu_alter(&$items) {
$items['nodereference/autocomplete']['page callback'] = 'fp_tweaks_nodereference_autocomplete';
}
I've found an alternative solution is to change your widget type to select list and then use the chosen module to convert your list to an autocomplete field.
This handles nodes with the same title, and actually I think the UI is better than the one provided by the autocomplete widget.
To anyone coming across this (rather old) topic by way of a google search - for Drupal 7 please consider using entityreference module and "Entity Reference" field type if possible.
You can acheive a lot more in configuration with an "Entity Reference" field. It doesn't have this problem with the nid in square brackets.
Here is the full Drupal 7 version (References 7.x-2.1) of Grayside's answer. This goes in your custom module:
/**
* Implementation of hook_menu_alter().
*/
function custom_menu_alter(&$items) {
$items['node_reference/autocomplete/%/%/%']['page callback'] = 'custom_new_node_reference_autocomplete';
}
/**
* Implementation of Menu callback for the autocomplete results.
*/
function custom_new_node_reference_autocomplete($entity_type, $bundle, $field_name, $string = '') {
$field = field_info_field($field_name);
$instance = field_info_instance($entity_type, $field_name, $bundle);
$options = array(
'string' => $string,
'match' => $instance['widget']['settings']['autocomplete_match'],
'limit' => 10,
);
$references = node_reference_potential_references($field, $options);
$matches = array();
foreach ($references as $id => $row) {
// Markup is fine in autocompletion results (might happen when rendered
// through Views) but we want to remove hyperlinks.
$suggestion = preg_replace('/<a href="([^<]*)">([^<]*)<\/a>/', '$2', $row['rendered']);
// Add a class wrapper for a few required CSS overrides.
$matches[$row['title']] = '<div class="reference-autocomplete">' . $suggestion . '</div>'; // this is the line that was modified to remove the "[nid:XX]" disambiguator
}
drupal_json_output($matches);
}

Resources