2

I have a node with a field collection called itinerary, and that field collection contains a user entity reference called traveler. The node can have unlimited itineraries and an itinerary can have unlimited travelers.

I'm trying to make a rule to react when travelers are added.

My first thought was

  1. Loop through the node_unchanged:itinerary field
  2. Loop through the travellers list within each itinerary, building a list of pre-save travellers.
  3. Loop through the node:itinerary field
  4. Loop through the travellers list within each itinerary, building a list of post-save travellers.
  5. Loop through the pre-save travellers, removing each one from the post-save traveler list, then take action on the list of new travelers that results.

Unfortunately the new traveler list is always empty, the list of pre-save and post-save travellers are always the same, and when I look at the ids (UUID, field_collection ID, and revision id) of the itineraries in node and node-unchanged, they are identical. So it seems as far as I can tell, that rules has no access to field collection state before the node was saved.

Is this the case? Is my only solution to populate a hidden traveler list at the end of each save so I can compare that list with what is being saved? It seems like there must be a better way.

This question is similar to How to retrieve data in a field collection within a Rules Condition?, but that question also doesn't have an answer, and I don't feel like the question is entirely clear.

Update: I was asked for a screenshot of my rules setup, but that is pretty long, so I'll include an export of the rule first. This includes some rules just for comparing the values and trying to understand what is happening. Here is the result of the show messages rules that show field collection ID info:

New Entry: UUID: afe4e63b-637f-402b-b730-5fcac4d5c809   Field collection item ID: 20    Revision ID: 471
Old Entry Itinerary ID: Field collection item UUID: afe4e63b-637f-402b-b730-5fcac4d5c809 Field collection item ID: 20 Revision ID: 471

And here is the rule export:

{ "rules_react_to_added_travellers" : {
    "LABEL" : "React to added Travellers",
    "PLUGIN" : "rule",
    "OWNER" : "rules",
    "TAGS" : [ "mission_approval" ],
    "REQUIRES" : [ "rules", "rules_data_transforms" ],
    "USES VARIABLES" : {
      "node" : { "label" : "Mission Request", "type" : "node" },
      "node_unchanged" : { "label" : "Original Mission Request", "type" : "node" }
    },
    "IF" : [
      { "node_is_of_type" : {
          "node" : [ "node" ],
          "type" : { "value" : { "mission_request" : "mission_request" } }
        }
      },
      { "node_is_of_type" : {
          "node" : [ "node-unchanged" ],
          "type" : { "value" : { "mission_request" : "mission_request" } }
        }
      }
    ],
    "DO" : [
      { "variable_add" : {
          "USING" : { "type" : "list\u003Cuser\u003E", "value" : [ "" ] },
          "PROVIDE" : { "variable_added" : { "traveller_list" : "Traveller List" } }
        }
      },
      { "variable_add" : {
          "USING" : { "type" : "list\u003Cuser\u003E", "value" : [ "" ] },
          "PROVIDE" : { "variable_added" : { "traveller_list_unchanged" : "Original Traveller List" } }
        }
      },
      { "variable_add" : {
          "USING" : { "type" : "list\u003Ctext\u003E" },
          "PROVIDE" : { "variable_added" : { "text_traveler_list" : "New Traveler List (Text)" } }
        }
      },
      { "variable_add" : {
          "USING" : { "type" : "list\u003Ctext\u003E" },
          "PROVIDE" : { "variable_added" : { "new_traveler_names" : "New Traveller Names" } }
        }
      },
      { "variable_add" : {
          "USING" : { "type" : "list\u003Ctext\u003E" },
          "PROVIDE" : { "variable_added" : { "old_traveler_names" : "Old Traveler Name" } }
        }
      },
      { "LOOP" : {
          "USING" : { "list" : [ "node:field-mr-fc-itineraries" ] },
          "ITEM" : { "current_itinerary" : "Current Itinerary" },
          "DO" : [
            { "drupal_message" : { "message" : "New Entry:  UUID: [current-itinerary:uuid]\tField collection item ID: [current-itinerary:item-id]\tRevision ID: [current-itinerary:revision-id]" } },
            { "LOOP" : {
                "USING" : { "list" : [ "current-itinerary:field-mr-fc-travelers" ] },
                "ITEM" : { "current_traveler" : "Current Traveler" },
                "DO" : [
                  { "list_add" : {
                      "list" : [ "traveller-list" ],
                      "item" : [ "current-traveler" ],
                      "unique" : "1"
                    }
                  },
                  { "list_add" : {
                      "list" : [ "new-traveler-names" ],
                      "item" : [ "current-traveler:name" ],
                      "unique" : "1"
                    }
                  }
                ]
              }
            }
          ]
        }
      },
      { "LOOP" : {
          "USING" : { "list" : [ "node-unchanged:field-mr-fc-itineraries" ] },
          "ITEM" : { "current_old_itinerary" : "Current Old Itinerary" },
          "DO" : [
            { "LOOP" : {
                "USING" : { "list" : [ "current-old-itinerary:field-mr-fc-travelers" ] },
                "ITEM" : { "current_old_traveler" : "Current Old Traveler" },
                "DO" : [
                  { "list_add" : {
                      "list" : [ "traveller-list-unchanged" ],
                      "item" : [ "current-old-traveler" ],
                      "unique" : "1"
                    }
                  },
                  { "list_add" : {
                      "list" : [ "old-traveler-names" ],
                      "item" : [ "current-old-traveler:name" ],
                      "unique" : "1"
                    }
                  }
                ]
              }
            },
            { "drupal_message" : { "message" : "Old Entry Itinerary ID: Field collection item UUID: [current-old-itinerary:uuid] Field collection item ID: [current-old-itinerary:item-id] Revision ID: [current-old-itinerary:revision-id]" } }
          ]
        }
      },
      { "LOOP" : {
          "USING" : { "list" : [ "traveller-list-unchanged" ] },
          "ITEM" : { "current_traveler" : "Current Traveler" },
          "DO" : [
            { "list_remove" : { "list" : [ "traveller-list" ], "item" : [ "current-traveler" ] } }
          ]
        }
      },
      { "LOOP" : {
          "USING" : { "list" : [ "traveller-list" ] },
          "ITEM" : { "list_item" : "Current list item" },
          "DO" : [
            { "list_add" : {
                "list" : [ "text-traveler-list" ],
                "item" : [ "list-item:name" ],
                "unique" : "1"
              }
            }
          ]
        }
      },
      { "implode" : {
          "USING" : { "input_list" : [ "text-traveler-list" ], "gluez" : ", " },
          "PROVIDE" : { "combined_text" : { "combined_text" : "Combined text" } }
        }
      },
      { "implode" : {
          "USING" : { "input_list" : [ "new-traveler-names" ], "gluez" : ", " },
          "PROVIDE" : { "combined_text" : { "new_names" : "New Names" } }
        }
      },
      { "implode" : {
          "USING" : { "input_list" : [ "old-traveler-names" ], "gluez" : ", " },
          "PROVIDE" : { "combined_text" : { "old_names" : "Old Names" } }
        }
      },
      { "drupal_message" : { "message" : "Calculated New Travellers: [combined-text:value]  Original Travelers: [old-names:value] New Travelers: [new-names:value]" } },
      { "component_rules_take_a_list_of_users_and_notify_their_area_leaders" : { "traveler_list" : [ "traveller-list" ], "node" : [ "node" ] } }
    ]
  }
}

This component is invoked by a rule triggered on Event "After updating existing content of type Mission Request". It runs two other rules that evaluate other thing about the state of the mission request, send out any necessary notifications, and then pass node and node_unchanged to this component.

That looks like this:

{ "rules_mission_request_is_updated" : {
    "LABEL" : "Mission Request is updated",
    "PLUGIN" : "reaction rule",
    "OWNER" : "rules",
    "TAGS" : [ "mission_approval" ],
    "REQUIRES" : [ "rules" ],
    "ON" : { "node_update--mission_request" : { "bundle" : "mission_request" } },
    "IF" : [
      { "node_is_published" : { "node" : [ "node" ] } },
      { "NOT data_is" : { "data" : [ "node:field-mr-approval-status" ], "value" : "3" } },
      { "NOT data_is_empty" : { "data" : [ "node:field-mr-approval-status" ] } }
    ],
    "DO" : [
      { "component_rules_get_added_budgets" : { "node" : [ "node" ], "node_unchanged" : [ "node_unchanged" ] } },
      { "component_rules_react_to_removed_budgets" : { "node" : [ "node" ], "node_unchanged" : [ "node_unchanged" ] } },
      { "component_rules_react_to_added_travellers" : { "node" : [ "node" ], "node_unchanged" : [ "node_unchanged" ] } }
    ]
  }
}
  • Can you provide a screenshot of your rule setup? – No Sssweat Oct 2 '17 at 6:21
  • Sure, but it is more complicated than just what I'm asking about. Let me try to add that. – UltraBob Oct 2 '17 at 6:23
  • I added an export of the rule. I can stitch together a screenshot if that is better. Note, this is a component that takes node and node_unchanged and is triggered After updating existing content of type Mission Request – UltraBob Oct 2 '17 at 6:36
  • 1
    Nice challenge, time to go find my thinking-cap ... PS to @NoSssweat : exported rules always cntain the full story, screenshots about rules often keep part of the details hidden ... – Pierre.Vriens Oct 2 '17 at 7:09
  • 1
    Hey @NoSssweat, wake up ... are you familiar with Rules Event "After updating an existing field collection item"? Refer to my answer for waaayyy more details ... Eureca! – Pierre.Vriens Oct 2 '17 at 21:33
2

Note: In the case where your field collection is a multi-value field and you need to compare the contents of all field collections, checking the contents of single field collection instances isn't useful. Effectively the real answer to your question is probably that for what you want to do, the only possible solution is keeping a field to check against, but if You want to check single field collection instances at a time, the "After updating an existing field collection item" trigger is what you need.

Processing field collections is always fun, and even more fun to do so with the Rules module. My favorite sample to refer to (for actually doing so) is contained in part 1 of my answer to "How to iterate over all field collection items in an eMail body using Rules?".

However to answer your question, I think you're using the wrong technique (sorry ...). What you should do to "react when travelers are added" (as in your question), is to rework (simplify) your Rules Actions and use a technique which is similar to what is explained in my answer to "How to check with Rules if a new element has been added to a list in a node?".

However, your question is about field collection items, which is a slightly different story ... as indicated also in comment #2 of the issue about "Field Collection does not work with Rules' 'node-unchanged' option", and which states:

... For example:

  1. Create a content type with a field collection that contains an integer field.
  2. Create an "After updating existing content" rule for that content type.
  3. Add a "Show a message on the site" action that prints the integer field attached to node:field-collection and node-unchanged:field-collection.

The printed value for node-unchanged is wrong. It shows the changed value.

This seems similar to "... the list of pre-save and post-save travellers are always the same ...", as in your question. So far the bad news, read on in comment nr 3 of that same issue, which is like so:

The reason that node-unchanged and node both show the same (new) field collection values is that the node stores a reference to the field collection. The field collection is a separate entity. Changes to it can be caught by the rules events for field collections. The "after updating an existing field collection item" event gives the correct unchanged and new values.

With this explanation in mind, have a look at this rule (in Rules export format):

{ "rules_verify_updates_to_field_collection_items" : {
    "LABEL" : "Verify updates to field collection items",
    "PLUGIN" : "reaction rule",
    "OWNER" : "rules",
    "REQUIRES" : [ "rules", "rules_conditional", "field_collection" ],
    "ON" : { "field_collection_item_update" : [] },
    "IF" : [
      { "entity_has_field" : { "entity" : [ "field-collection-item" ], "field" : "field_reviewers" } }
    ],
    "DO" : [
      { "LOOP" : {
          "USING" : { "list" : [ "field-collection-item-unchanged:field-reviewers" ] },
          "ITEM" : { "previous_reviewer" : "Previous reviewer" },
          "DO" : [
            { "CONDITIONAL" : [
                {
                  "IF" : { "NOT list_contains" : {
                      "list" : [ "field-collection-item:field-reviewers" ],
                      "item" : [ "previous-reviewer" ]
                    }
                  },
                  "DO" : [
                    { "drupal_message" : { "message" : "Reviewer [previous-reviewer:name] was removed from the list of reviewers in field collection item with id [field-collection-item:item-id]." } }
                  ]
                }
              ]
            }
          ]
        }
      },
      { "LOOP" : {
          "USING" : { "list" : [ "field-collection-item:field-reviewers" ] },
          "ITEM" : { "current_reviewer" : "Current reviewer" },
          "DO" : [
            { "CONDITIONAL" : [
                {
                  "IF" : { "NOT list_contains" : {
                      "list" : [ "field-collection-item-unchanged:field-reviewers" ],
                      "item" : [ "current-reviewer" ]
                    }
                  },
                  "DO" : [
                    { "drupal_message" : { "message" : "Reviewer [current-reviewer:name] was added to the list of reviewers in field collection item with id [field-collection-item:item-id]." } }
                  ]
                }
              ]
            }
          ]
        }
      }
    ]
  }
}

Some details about the above rule:

  • Rules Event: After updating an existing field collection item.
  • Rules Condition: Entity has field, for a field with machine name field_reviewers, which is a multi value entity reference field (to users). Think of those reviewers as equivalent to your travelers.
  • Rules Actions:
    • The 1st loop (related to field-collection-item-unchanged:field-reviewers), will discover all removed reviewers.
    • The 2nd loop (related to field-collection-item:field-reviewers), will discover all added reviewers.

Obviously, for this rule to work you'll also need the Conditional Rules module, though you could also rework the Rules Action to performing Rules Components (in which you add the appropriate Rules Conditions), so that you don't need this extra module (for more details on that, refer to the answer to "What's the alternative to using the Conditional Rules module?").

Here is a demo of this rule (= some Drupal messages shown after I added/removed some user references in 2 field collection items (with id = 5 and 6):

Reviewer Hillary.Trump was removed from the list of reviewers in field collection item with id 5.
Reviewer UltraBob was added to the list of reviewers in field collection item with id 5.
Reviewer Donald.Clinton was removed from the list of reviewers in field collection item with id 6.
Reviewer Dries.Buytaert was added to the list of reviewers in field collection item with id 6.
Reviewer Pierre.Vriens was added to the list of reviewers in field collection item with id 6.
  • The problem I have is that the list of travellers in my node-unchanged is identical to the one from my node. Both have the version that I am attempting to save. Maybe there is a shorter-winded way of comparing the two lists than I have, but that won't help if the lists are identical. – UltraBob Oct 2 '17 at 8:45
  • Sure I've added a description and export of the invoking rule. – UltraBob Oct 2 '17 at 9:00
  • Thanks Pierre, I’ll try your suggestions, but I’ve converted the two lists of travelers into strings and system messaged them out so I know the rule is getting data. – UltraBob Oct 2 '17 at 10:54
  • Thanks, I responded on that thread turning it into a feature request. In the case where your field collection is a multi-value field and you need to compare the contents of all fcs, checking the contents of single fc instances isn't useful. Effectively the real answer to my question is that for what I want to do, the only possible solution is keeping a field to check against, but if I want to check single fc instances at a time, the "After updating an existing field collection item" trigger is what I need. – UltraBob Oct 3 '17 at 2:20
  • 1
    Awesome! Thanks as always for you help! You were right, I should have gotten into rules a long time ago, I actually already replaced a custom module I has developed with rules, but there sure is a lot to learn when you first dive into drupal in earnest. – UltraBob Oct 3 '17 at 8:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.