Send an email to user when a new role is assigned to the user

Created on 8 December 2023, 10 months ago
Updated 8 February 2024, 7 months ago

Problem/Motivation

There is a sample model for this on the documentation but I downloaded and tried the model and it did not quite work for me and is rather too complex for me to fit to my purpose.

What I want to achieve is to send an email to a user when a new role is assigned to the user. This task has been an age-long one which many people kept asking how to do with Rules and it has always been some patched up methods to achieve it with Rules, and so far with ECA also.

In my use case, we are using roles to manage subscriptions so I am using 'role_expire' module to send emails when the role is about to be removed (their subscription is about to expire). This is why I was not looking to use ECA to send email when the role is removed, but learning how to do that will also be handy for any other day. The sample on the documentation site tries to achieve that but like I said, after importing and trying the model it didn't fit in exactly and is too complex for me to adapt to my use.

Proposed resolution

After I found I couldn't use the sample on the documentation, I tried to set up a simple model to try this so I setup the model I have attached to this post.

The long and short of what we are trying to achieve is send an email to a user when his account is assigned a 'manager' role.

What I did in the model is this:

1) I used 'Update content entity' event instead of 'Presave content entity' event. This is because when I tried 'Presave content entity' and I add a new role, the user would not get an email when the account is saved but will get an email when it is saved again. When I tried 'Update content entity', email was sent when the role is added and saved the first time (and also when saved subsequently).

2) Then I tested the role of the user being updated. In the sample I uploaded I tested to see that the user has an 'authenticated' role (in my live site I tested for 'editor' or 'blogger' roles - so I added two parallel connectors to test OR condition).

3) I then tested that the user updating the account has 'administrator' role (to avoid emails firing and messages displaying when the user saves his own profile)

4) I then tested if the user has a 'manager' role and if the role is present an email will be fired and a message displayed.

5) I then sent the email and next displayed a message on the page.

We are using this right now but you can see it is a tacky solution.

The problem

The problem with this is that each time an admin saves the user account, the email is fired all over again and the message displayed.

How I tried to overcome the problem

I tried to start a parallel 'Presave content entity' event to test if 'manager' role existed before the account is updated so that the 'send email' and 'display a message to the user' actions will not be fired. But I couldn't get the two events running side by side and affecting the final actions.

I am using Drupal 9.5.11 soon to upgrade to Drupal 10.

💬 Support request
Status

Fixed

Version

2.0

Component

Documentation

Created by

🇳🇬Nigeria chike Nigeria

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @chike
  • 🇩🇪Germany jurgenhaas Gottmadingen

    I don't see why the sample from the ECA Guide should be too complex. It is doing exactly what's described: it determines, if any of the roles was just added or removed.

    IN contrast, the uploaded model doesn't check for any changes, it just checks if the saved user has the roles, but it doesn't verify if that user had the role already before saving.

    Finding out such changes needs a few more steps and that's why the sample was provided, as it demonstrates a way on how that can be achieved. We can't just take out some steps to make it appear more simple, it needs to do all those verifications.

    While the sample model verifies for any role being either added or removed, when saving the user entity, it can be made simpler if only one or two roles should be checked, whether they got added. With that in mind, you may want to go back to the original sample and give it a try to modify that towards your needs.

  • 🇳🇬Nigeria chike Nigeria

    Thank you @jurgenhaas I gave the sample on the ECA guide a second shot but it doesn't work as expected.

    I imported it and edited one of my accounts with 3 roles, role1, role2 and role3. It displayed message 'Role target_id: role1 is new', 'Role target_id: role2 is new', and 'Role target_id: role3 is new'. It also displayed message 'Role target_id: role1 has been removed', 'Role target_id: role2 has been removed', 'Role target_id: role3 has been removed'. Meanwhile I just clicked Edit button on the account and saved without changing anything.

    I edited the account again and saw that it has removed one of the roles (I didn't unselect the role myself - the model removed it). I then saved the account again and it displayed message (by now only two roles were active), 'Role target_id: role1 is new', 'Role target_id: role2 is new' and also at the same time displayed message 'Role target_id: role1 has been removed', 'Role target_id: role2 has been removed'. And one of the roles got removed again remaining only one role.

    I edited the account again (now having only one role left) and it showed the message again 'Role target_id: role1 is new' and again at the same time showed 'Role target_id: role1 has been removed' but this time the last role was not removed. I kept editing but the last role remained.

    I edited the account and added 5 roles on it, it showed the message like before, 5 messages for the roles being new and 5 messages for the roles being removed but saved 4 roles instead of the 5 I selected. I edited the account and it removed one role keeping 3 this time. I edited again and it removed one role keeping 2. I edited again and it removed one keeping 1. I edited and the last one remained.

    IMO about 85% of the use case of this functionality will be testing if 'a particular role' is added or removed as against if 'any role' is added or removed.

    The sample is for someone to get started with and fashion to his/her use case but I am thinking it will help if a version is made to test for particular roles or particular number of 'named' roles. This will help new ECA starters to get going first before understanding ECA more deeply, especially as regards to this functionality which I think many will be interested in. Don't also forget that when you test 'any role' the message is generic and a new starter might need to set messages like, "You have been made a Manager", "You have subscribed to Deluxe plan", "Your access to our exclusive club has been revoked". Testing for 'any role', it will be difficult customizing these messages, maybe not for someone already familiar with ECA and the tools, but definitely for a new starter.

    So perhaps a sample would be placed on the ECA guide testing for a particular role or a particular number of named roles. I think this will help.

    Thank you.

  • 🇩🇪Germany jurgenhaas Gottmadingen

    I can't reproduce that behaviour you describe. The sample model works as expected. Please retry in a fresh Drupal installation and then only load that one sample model and give it another try. The description sounds as if there is something else intervening what's going on. If the problem still persists, please write up step by step how that behaviour can be reproduced, with Drupal version, how to install the new instance and so on, so that we can really go through that list of tasks to reproduce the problem.

  • 🇳🇬Nigeria chike Nigeria

    Maybe you should test with more roles than one custom role. Create like 2 or 3 custom roles and try adding them to a user.

    In our case the users already had a custom role assigned so the new role on which we want ECA to act is a second custom role.

    I tested now with a fresh install of Drupal 10.1.7 and when I added just one custom role everything seemed fine till I created the second custom role and tried assigning it to the user then the behaviour I described above happened again.

    Step by step to reproduce:

    1) Create a role 'manager.

    2) Create another role 'chief_editor'

    3) Edit an authenticated user and assign 'manager' role to him

    4) Edit same user and assign 'chief_editor' role to him

    5) Edit same user and change nothing, just save the page. One of the roles could be gone.

    6) Try assigning the standard 'content_editor' role also on the user. It might not assign. If it assigns, then edit the page again and do nothing, just save again and one role random would be gone.

  • 🇩🇪Germany jurgenhaas Gottmadingen

    Sounds like this model runs into the issue, where looping through an entity field's list of items modifies that list, which is often not the intention of the loop. In other words, when looping through the list of items from an entity field and dropping items from the list for the purpose of looping, actually removes that item from the field values as well. If the model only loops through, this has no impact, but if the entity gets saved afterwards, then the modification can be saved too, which sounds like what you're describing above.

    This has been discussed in detail in 💬 Change of token when running through a loop Fixed with the explanation of the behaviour and resolution in comment #7.

    In short: using Token: set value behaves like a reference to the original entity field and if data will be manipulated, not only the token value (e.g. dropping an item from a list) gets changed, but also the referenced field value. When using Entity: get field value instead, this creates a new data object in a token which is unlinked from its source.

    So, modifying the model accordingly should probably resolve this issue. If so, we should also update the sample in the ECA Guide. Waiting for your own tests on this whether this is the solution to this.

  • 🇳🇬Nigeria chike Nigeria

    It will be great to update the sample on the ECA guide but like I opined before, most real world application of this feature will be testing if a specific role is added or removed. The current sample model it too complex for a site builder like myself to quickly tame to his use when most likely (s)he doesn't want to test ANY role changes but wants to test specific role changes.

    I will suggest that in addition to updating the current model on the ECA guide that two additional models be made, one to test if A role is added and the other to test if A role is removed. It will help a great lot, from the site builder POV.

  • 🇩🇪Germany jurgenhaas Gottmadingen

    Action plugins should be generic, ECA models use them for specific tasks, then. So, for this issue we should stick to the real problem, which is about that model to work with all the roles and their changes to the user entity. As this seems to be broken, it needs to be fixed and then the reported problem in this issue will be resolved.

    When it comes to optimized tools with regard to changed entity references, which is what's proposed in #7, then this topic is being addressed in eca_content: Provide an addition and removal events for entity references Active and is very likely going to be implemented in ECA 2 at some point.

    As for fixing the example model, I've created an issue in the ECA Guide issue queue: https://gitlab.lakedrops.com/drupal/documentation/eca/-/issues/61

  • Status changed to Fixed 9 months ago
  • 🇩🇪Germany jurgenhaas Gottmadingen

    This is also related to 📌 Problem detected in documentation examples Active and will be fixed there. Marking this one fixed for that reaons.

  • 🇩🇪Germany jurgenhaas Gottmadingen

    The problem in the model has been solved and the ECA Guide got updated.

  • 🇳🇬Nigeria chike Nigeria

    @jurgenhaas thank you for following up with this.

    I think the issue is not solved yet. Let me explain with screenshots.

    I added 'chief_editor' role to a user and no message was displayed but this error was logged,

    Failed execution of Get the list of original roles (Activity_0111k7q) from ECA Detect user role changes (eca_lib_0010) for event Drupal\eca_content\Event\ContentEntityPreSave: The provided field roles does not exist as a property path on the user entity having ID 2..\n\n#0 C:\wamp64\www\d10play\web\modules\contrib\eca\src\Entity\Objects\EcaAction.php(99): Drupal\eca_content\Plugin\Action\GetFieldValue->access(Object(Drupal\user\Entity\User), NULL, true) #1 C:\wamp64\www\d10play\web\modules\contrib\eca\src\Processor.php(188): Drupal\eca\Entity\Objects\EcaAction->execute(Object(Drupal\eca\Entity\Objects\EcaAction), Object(Drupal\eca_content\Event\ContentEntityPreSave), Array) #2 C:\wamp64\www\d10play\web\modules\contrib\eca\src\Processor.php(190): Drupal\eca\Processor->executeSuccessors(Object(Drupal\eca\Entity\Eca), Object(Drupal\eca\Entity\Objects\EcaAction), Object(Drupal\eca_content\Event\ContentEntityPreSave), Array) #3 C:\wamp64\www\d10play\web\modules\contrib\eca\src\Processor.php(146): Drupal\eca\Processor->executeSuccessors(Object(Drupal\eca\Entity\Eca), Object(Drupal\eca\Entity\Objects\EcaEvent), Object(Drupal\eca_content\Event\ContentEntityPreSave), Array) #4 C:\wamp64\www\d10play\web\modules\contrib\eca\src\EventSubscriber\EcaBase.php(76): Drupal\eca\Processor->execute(Object(Drupal\eca_content\Event\ContentEntityPreSave), 'eca.content_ent...') #5 [internal function]: Drupal\eca\EventSubscriber\EcaBase->onEvent(Object(Drupal\eca_content\Event\ContentEntityPreSave), 'eca.content_ent...', Object(Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher)) #6 C:\wamp64\www\d10play\web\core\lib\Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher.php(111): call_user_func(Array, Object(Drupal\eca_content\Event\ContentEntityPreSave), 'eca.content_ent...', Object(Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher)) #7 C:\wamp64\www\d10play\web\modules\contrib\eca\src\Event\TriggerEvent.php(73): Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object(Drupal\eca_content\Event\ContentEntityPreSave), 'eca.content_ent...') #8 C:\wamp64\www\d10play\web\modules\contrib\eca\modules\content\src\HookHandler.php(199): Drupal\eca\Event\TriggerEvent->dispatchFromPlugin('content_entity:...', Object(Drupal\user\Entity\User), Object(Drupal\eca\Service\ContentEntityTypes)) #9 C:\wamp64\www\d10play\web\modules\contrib\eca\modules\content\eca_content.module(83): Drupal\eca_content\HookHandler->presave(Object(Drupal\user\Entity\User)) #10 [internal function]: eca_content_entity_presave(Object(Drupal\user\Entity\User)) #11 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\Extension\ModuleHandler.php(409): call_user_func_array(Object(Closure), Array) #12 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\Extension\ModuleHandler.php(388): Drupal\Core\Extension\ModuleHandler->Drupal\Core\Extension\{closure}(Object(Closure), 'eca_content') #13 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\Extension\ModuleHandler.php(416): Drupal\Core\Extension\ModuleHandler->invokeAllWith('entity_presave', Object(Closure)) #14 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\Entity\EntityStorageBase.php(217): Drupal\Core\Extension\ModuleHandler->invokeAll('entity_presave', Array) #15 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\Entity\ContentEntityStorageBase.php(900): Drupal\Core\Entity\EntityStorageBase->invokeHook('presave', Object(Drupal\user\Entity\User)) #16 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\Entity\EntityStorageBase.php(529): Drupal\Core\Entity\ContentEntityStorageBase->invokeHook('presave', Object(Drupal\user\Entity\User)) #17 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\Entity\ContentEntityStorageBase.php(753): Drupal\Core\Entity\EntityStorageBase->doPreSave(Object(Drupal\user\Entity\User)) #18 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\Entity\EntityStorageBase.php(483): Drupal\Core\Entity\ContentEntityStorageBase->doPreSave(Object(Drupal\user\Entity\User)) #19 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\Entity\Sql\SqlContentEntityStorage.php(806): Drupal\Core\Entity\EntityStorageBase->save(Object(Drupal\user\Entity\User)) #20 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\Entity\EntityBase.php(352): Drupal\Core\Entity\Sql\SqlContentEntityStorage->save(Object(Drupal\user\Entity\User)) #21 C:\wamp64\www\d10play\web\core\modules\user\src\ProfileForm.php(46): Drupal\Core\Entity\EntityBase->save() #22 [internal function]: Drupal\user\ProfileForm->save(Array, Object(Drupal\Core\Form\FormState)) #23 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\Form\FormSubmitter.php(129): call_user_func_array(Array, Array) #24 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\Form\FormSubmitter.php(67): Drupal\Core\Form\FormSubmitter->executeSubmitHandlers(Array, Object(Drupal\Core\Form\FormState)) #25 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\Form\FormBuilder.php(597): Drupal\Core\Form\FormSubmitter->doSubmitForm(Array, Object(Drupal\Core\Form\FormState)) #26 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\Form\FormBuilder.php(325): Drupal\Core\Form\FormBuilder->processForm('user_form', Array, Object(Drupal\Core\Form\FormState)) #27 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\Controller\FormController.php(73): Drupal\Core\Form\FormBuilder->buildForm(Object(Drupal\user\ProfileForm), Object(Drupal\Core\Form\FormState)) #28 [internal function]: Drupal\Core\Controller\FormController->getContentResult(Object(Symfony\Component\HttpFoundation\Request), Object(Drupal\Core\Routing\RouteMatch)) #29 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber.php(123): call_user_func_array(Array, Array) #30 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\Render\Renderer.php(627): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() #31 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber.php(124): Drupal\Core\Render\Renderer->executeInRenderContext(Object(Drupal\Core\Render\RenderContext), Object(Closure)) #32 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber.php(97): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext(Array, Array) #33 C:\wamp64\www\d10play\vendor\symfony\http-kernel\HttpKernel.php(181): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() #34 C:\wamp64\www\d10play\vendor\symfony\http-kernel\HttpKernel.php(76): Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object(Symfony\Component\HttpFoundation\Request), 1) #35 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\StackMiddleware\Session.php(58): Symfony\Component\HttpKernel\HttpKernel->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true) #36 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\StackMiddleware\KernelPreHandle.php(48): Drupal\Core\StackMiddleware\Session->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true) #37 C:\wamp64\www\d10play\web\core\modules\page_cache\src\StackMiddleware\PageCache.php(106): Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true) #38 C:\wamp64\www\d10play\web\core\modules\page_cache\src\StackMiddleware\PageCache.php(85): Drupal\page_cache\StackMiddleware\PageCache->pass(Object(Symfony\Component\HttpFoundation\Request), 1, true) #39 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\StackMiddleware\ReverseProxyMiddleware.php(48): Drupal\page_cache\StackMiddleware\PageCache->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true) #40 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\StackMiddleware\NegotiationMiddleware.php(51): Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true) #41 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\StackMiddleware\AjaxPageState.php(36): Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true) #42 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\StackMiddleware\StackedHttpKernel.php(51): Drupal\Core\StackMiddleware\AjaxPageState->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true) #43 C:\wamp64\www\d10play\web\core\lib\Drupal\Core\DrupalKernel.php(704): Drupal\Core\StackMiddleware\StackedHttpKernel->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true) #44 C:\wamp64\www\d10play\web\index.php(19): Drupal\Core\DrupalKernel->handle(Object(Symfony\Component\HttpFoundation\Request)) #45 {main}

    I did nothing on the page and saved the user again and this message was displayed,

    I removed the role and this message was displayed,

    I added the role back and again no message was displayed.

    I then added 'manager' role to the user and this message was displayed,

    I added 'content_editor' role and this message was displayed,

    I removed 'content_editor' role and this message was displayed,

    I removed 'manager' role and this message was displayed,

    I removed 'chief_editor' role and this message was displayed,

    I am using v2 of the sample model, Drupal 10.2.0 ECA 1.1.4 and BPMN.iO 1.1.3

    You know I kept asking for a model that can test if 'my_role' is added or removed instead of detecting ANY role changes and in my opinion that will be okay for a lot of people as this is exactly the functionality that folks asked for in the older days of Rules...to test if a particular role has been added or removed. I am thinking a way this can be achieved using existing Drupal events is by testing if 'my_role' does not exist during the 'Presave content entity' event and then test if it now exists in the 'Update content entity' event. This means it has just been added. Then to test when it is removed will be vice versa.

  • 🇩🇪Germany jurgenhaas Gottmadingen

    TBH, I have no idea why there should not be a property roles for a user entity as the error message indicates. This would be best addressed in a separate issue, if you can somehow make this reproducible so that we can have a look and find the source of the problem.

    You know I kept asking for a model that can test if 'my_role' is added or removed instead of detecting ANY role changes

    Yes, I know. And my answer to that was in #8: there is a feature request to provide a new event for something like that, see eca_content: Provide an addition and removal events for entity references Active .

    The behaviour you describe in your last comment is hard to reproduce. I've tested this in a fresh Drupal 10.2 installation with ECA 2.0.x (bug fixing should always be done with the latest dev release). I added 3 new roles, added and removed roles as I wished, and I always received the expected messages.

    If you have a reproducible way, please describe that in an order list - as comprehensive as possible - so that anybody can go through those steps to defintely see the same problem. I wasn't able so far with the information given.

  • Automatically closed - issue fixed for 2 weeks with no activity.

  • 🇦🇺Australia lambch

    I found this thread while trying to work through a similar issue.

    Going to leave this comment in the hope it helps others with similar problems (or it may even help the OP).

    In my case - when updating a user's roles - notifications would appear when expected, however the notification would only ever list the user's most senior role in the place of the [role] token.

    I tracked it down to basically being set by the order in which I drew the arrows coming out from the exclusive gateways. Draw the arrows in different orders and you may get strange results.

    So when looking at the exported yaml. Compare the incorrect:

      Gateway_13jfy1q:
        type: 0
        successors:
          -
            id: Activity_1fyvngi
            condition: ''
          -
            id: Gateway_17p519o
            condition: Flow_14743br
    

    To what it should be:

      Gateway_13jfy1q:
        type: 0
        successors:
          -
            id: Activity_1fyvngi
            condition: Flow_14743br
          -
            id: Gateway_17p519o
            condition: ''
    

    Swapping the order of the successors around made things work correctly.

    The 'compare two scalar values' check was being interpreted as truthy, however as the first successor condition to 'Drop first off the list of roles' was being looped until the last role remained - it was this last role what ended up being passed through to the next step as the token.

    Hope this helps

Production build 0.71.5 2024