Problem/Motivation

From what I can tell, the current release of this module allows users to be registered for the same event instance multiple times. I'd like to make sure that any given email address can only register for an event one time.

Proposed resolution

recurring_events_registration\src\RegistrationCreationService.php:324

  /**
   * Has the email address registered for this event before.
   *
   * @param str $email
   *   The email address of the user.
   *
   * @return bool/int
   *   If the registration already exists, return the registration ID.  Otherwise, return FALSE.
   */
  public function hasUserRegisteredByEmail($email) {
    $existing_registration_id = FALSE;
    $current_registrations = $this->retrieveAllSeriesRegisteredParties();
    foreach ($current_registrations as $registration_id => $registration_record) {
      if (($this->eventSeries->id() == $registration_record->get("eventseries_id")->target_id)
        && ($this->eventInstance->id() == $registration_record->get("eventinstance_id")->target_id)
        && (strtolower($email) == strtolower($registration_record->get("email")->value))) {

        $existing_registration_id = $registration_id;
        break;
      }
    }
    return $existing_registration_id;
  }

recurring_events\modules\recurring_events_registration\src\Form\RegistrantForm.php:415

  $existing_registration_id = $this->creationService->hasUserRegisteredByEmail($form["email"]["widget"][0]["value"]["#value"]);
  if ($existing_registration_id) {
    $message = $this->t("You've already registered for this event.");
    $form_state->setRedirect('entity.registrant.add_form', ['eventinstance' => $event_instance_id]);
  }
  else {
    ...
  }

Remaining tasks

  • I don't love how expensive the hasUserRegisteredByEmail() by having to essentially load and scan through every registration record on the site.
  • Is there a better way to get the user's email address instead of $form["email"]["widget"][0]["value"]["#value"]?
Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

ryankavalsky created an issue. See original summary.

owenbush’s picture

I think the idea behind this is good. I am a little concerned however because some of the use-cases we have for the module distinctly need emails to not be unique per event. For example, Libraries where parents are registering on behalf of themselves and maybe one or more young children who do not have emails.

There is an idea floating around in another issue of allowing per-event restrictions on the number of registrants someone can create. Part of that discussion has been how to determine whether a person has registered before. If they are logged in you can check the author of the registrant, (or even in your use-case the email), but if they are not logged in you would have to rely on something else. There is a problem with being able to game the system when relying upon emails, because of the use of plus- and dot-addressing. So name@domain.com is the same email as name+foobar@domain.com. Also, firstlast@domain.com is the same account as first.last@domain.com. So if there are restrictions on emails, they are pretty easy to circumvent for people who know about these sorts of things.

That's not to say it is not worthwhile doing it, but that checking emails is not a fool-proof way of preventing multiple-registrations. The best way would be to use a Drupal account, but a lot of sites won't want to enforce a Drupal login to register.

Its food for thought for sure and I'd love to have your input on this.

Here is one of the other issues/threads about this: #3113980: Limit number of registrations allowed

owenbush’s picture

As for your questions about efficiency of your code, you could use an entity query to query properties of the registrant - and eventseries_id, eventinstance_id and email are all basefields so they should be queryable.

You can get a query for a registrant like so:

$query = \Drupal::entityTypeManager()->getStorage('registrant')->getQuery();
$query->condition('eventseries_id', 1)
      ->condition('email', 'name@domain.com')
      ->execute();

I've not tested that, but I think it would work.

ryankavalsky’s picture

That's a great point about the plus- and dot-addressing. I wasn't aware of that until now (my organization's email system doesn't support it), but like you said, it's easy to work with. While the entityQuery works for exact email address matches, it won't be able to compare a "clean" (i.e. no ".", no "+", and lowercase) already-registered email address to a "clean" trying-to-register email address, sort of like this:

$query = \Drupal::entityTypeManager()->getStorage('registrant')->getQuery();
$query->condition('eventseries_id', $series_id)
      ->condition('eventinstance_id', $instance_id)
      ->condition(CLEANTHIS('email'), CLEANTHIS('$email_address_to_register'))
      ->execute();

After reading the other issue thread (#3113980: Limit number of registrations allowed), it sounds like limiting registrations to one per email address is still a good idea. I'm thinking we should make it an option when somebody creates/edits an event/series. Do you have suggestions/resources for how to add a new boolean unique_email_address field to that form? The code I've worked up adds the field, but doesn't store the field's value. I've created a new recurring_events_update_xxxx() to alter the schema, but my update didn't go through (sorry, I'm a newbie at _update_):

Failed: Drupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'eventseries_field_data.event_registration__unique_email_address' in 'field list':

See the attached patch file for all changes I've made against 8.x-1.0-beta2. You'll also see that I've added a $message_type so depending on the registration result, the message can show either as a success, warning, or error.

I have some time in my schedule at the moment, so I'm happy to help in any way.

ryankavalsky’s picture

StatusFileSize
new15.26 KB

I've re-rolled this patch to correct some file formatting issues I was having, since I use PowerShell as my terminal:

  • Need to use LF instead of CRLF for EOL markers
  • Need to use UTF-8 encoding instead of UCS-2 BE BOM
  • Need to use spaces instead of 4-space tabs

If anybody else uses PowerShell, here's a script that allows you to create good diff files in one line:

$filename = $args[0]
git diff | Out-File -encoding ASCII $filename
(Get-Content $filename -Raw).Replace("`r`n","`n") | Set-Content $filename -Force

Then, just run the following command:

creatediff [patch_file_name].patch

owenbush’s picture

Status: Active » Needs work

I'm not against this patch at all, that's not why I have not merged it yet. It just needs some work like adding the field to the schema appropriately to get it to create the field in the database, and there is a @todo in the middle of this code which needs to be updated to check if duplicate emails are not allowed, that is hardcoded to TRUE at this point. So there is work to be done before this is mergeable and I'll try and see if I can get to it soon.

owenbush’s picture

Status: Needs work » Needs review
StatusFileSize
new14.44 KB

Thanks for your hard work on the patch. The patch would not apply and there were quite a few changes needed to make this work, so I have gone ahead and recreated the patch. If you have time, please take a moment to test this out. You will need to run database updates if you already have an installation with registration configured.

ryankavalsky’s picture

Thanks @owenbush! I tried applying the patch against a fresh install of 2.0.0-beta1 and 2.0.0-beta2, but neither worked. Which release should I test the patch against?

> git apply --stat recurring_events-existing_registrations
-3183483-7.patch
 .../recurring_events_registration.install          |   98 ++++++++++++++++++++
 .../src/Entity/Registrant.php                      |    1
 .../src/Form/RegistrantForm.php                    |    8 ++
 .../Plugin/Field/FieldType/EventRegistration.php   |   13 ++-
 .../Field/FieldWidget/EventRegistrationWidget.php  |   29 +++++-
 .../src/RegistrationCreationService.php            |   51 ++++++++++
 6 files changed, 193 insertions(+), 7 deletions(-)

> git apply --check recurring_events-existing_registration
s-3183483-7.patch
error: patch failed: modules/recurring_events_registration/recurring_events_registration.install:220
error: modules/recurring_events_registration/recurring_events_registration.install: patch does not apply
error: patch failed: modules/recurring_events_registration/src/Entity/Registrant.php:262
error: modules/recurring_events_registration/src/Entity/Registrant.php: patch does not apply
owenbush’s picture

Hey Ryan,

I'll check the patch soon and verify why this may be failing.

owenbush’s picture

Sorry about the delay. I have figured out what is going on.

There is another issue: #3173249: Allow Registrants to be Versionable and Add Status which has this patch:

https://www.drupal.org/files/issues/2020-10-16/recurring_events-versiona...

Because that patch also touches upon some of the same things, you need to have that patch applied first. That patch has actually been merged but only into the dev version of the module - I have not cut a release yet.

And the patches only apply to the 2.0.0-beta2 and 8.x-1.0-beta2 versions of the module.

ryankavalsky’s picture

After applying the versionable registrants patch to a clean install of 2.0.0-beta1, I got the below error. Is there an extra comma on line 109?

Is currently:

$container->has('content_moderation.moderation_information') ? $container->get('content_moderation.moderation_information') : NULL,

Should be:

$container->has('content_moderation.moderation_information') ? $container->get('content_moderation.moderation_information') : NULL

Error:

The website encountered an unexpected error. Please try again later.
ParseError: syntax error, unexpected ')' in Composer\Autoload\includeFile() (line 109 of sites\drupal.dcbfm\modules\recurring_events\modules\recurring_events_registration\src\Form\RegistrantForm.php).

Composer\Autoload\includeFile('C:\WS\repos\drupal/sites/drupal.dcbfm.dd/modules/recurring_events/modules/recurring_events_registration/src\Form\RegistrantForm.php') (Line: 322)
Composer\Autoload\ClassLoader->loadClass('Drupal\recurring_events_registration\Form\RegistrantForm')
spl_autoload_call('Drupal\recurring_events_registration\Form\RegistrantForm')
class_exists('Drupal\recurring_events_registration\Form\RegistrantForm') (Line: 492)
Drupal\Core\Entity\EntityType->hasHandlerClass('form', 'add') (Line: 465)
Drupal\Core\Enti
ty\EntityType->getHandlerClass('form', 'add') (Line: 525)
Drupal\Core\Entity\EntityType->getFormClass('add') (Line: 222)
Drupal\Core\Entity\EntityTypeManager->getFormObject('registrant', 'add') (Line: 82)
Drupal\Core\Entity\HtmlEntityFormController->getFormObject(Object, 'registrant.add.default') (Line: 76)
Drupal\Core\Controller\FormController->getContentResult(Object, Object)
call_user_func_array(Array, Array) (Line: 123)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 573)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 124)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext(Array, Array) (Line: 97)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 151)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 68)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 57)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 52)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 708)
Drupal\Core\DrupalKernel->handle(Object) (Line: 21)
owenbush’s picture

Thanks for finding that issue. I have actually merged a fix for that into the dev release, but just in case you need a patch for it, I have attached a patch here too. That patch is an additional patch to the other in this issue

ryankavalsky’s picture

Status: Needs review » Reviewed & tested by the community

Works like a charm! Thanks for the patch and re-patch. I've now marked this as RTBC.

owenbush’s picture

Issue tags: +rc eligible
owenbush’s picture

Status: Reviewed & tested by the community » Fixed

Status: Fixed » Closed (fixed)

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

ryankavalsky’s picture

Hi again! Just one more change to the patch. If somebody is editing their existing registration, we don't have to check for a duplicate email.
So, instead of:

+    if ($unique_email_address && $existing_registration_id) {
+      // If a registration already exists for the email display an error.
+      $form_state->setErrorByName('email', $this->t("You've already registered for this event."));
+    }

It should be

+    if ($entity->isNew() && $unique_email_address && $existing_registration_id) {
+      // If a registration already exists for the email display an error.
+      $form_state->setErrorByName('email', $this->t("You've already registered for this event."));
+    }
owenbush’s picture

Status: Closed (fixed) » Needs work

You raise a good point about editing a registrant, however, I think the solution is the wrong one. We don't actually want to not check for duplicates when editing, we still do (for example if someone tries to change their email address to something else and that email has already registered).

What we likely want to do instead is to check to see whether any registrants (other than the currently edited one) already have that email address. This will stop the validation errors upon resaving an existing registrant, but also prevent the person changing their address to one already registered with another registrant.

ryankavalsky’s picture

That's a great point. We can add a parameter to the hasUserRegisteredByEmail() function that will allow us to optionally exclude a registration ID when checking for duplicate email addresses. Then, when somebody is editing their existing registration, we'll just be sure to send over that existing registration ID as the ID to not be checked against.

RegistrationCreationService.php hasUserRegisteredByEmail:

  public function hasUserRegisteredByEmail($email, $registration_to_exclude = null) {
    // Look through this series' registrations.
    $existing_registration_id = FALSE;
    $current_series_registrations = $this->retrieveAllSeriesRegisteredParties();
    foreach ($current_series_registrations as $registration_id => $registration_record) {
      // Compare the event instance ID and email address.
      if (((!isset($registration_to_exclude)) || ($registration_id != $registration_to_exclude))
        && ($this->eventInstance->id() == $registration_record->get('eventinstance_id')->target_id)
        && ($this->cleanEmailAddress($email) == $this->cleanEmailAddress($registration_record->get('email')->value))) {
        // Remember the existing registration ID and stop looking.
        $existing_registration_id = $registration_id;
        break;
      }
    }
    return $existing_registration_id;
  }

RegistrantForm.php validateForm():

    $unique_email_address = $this->creationService->registrationUniqueEmailAddress();
    $email_address = $form_state->getValue('email');
    $registration_to_exclude = ($entity->isNew() ? null : $entity->id());
    $existing_registration_id = $this->creationService->hasUserRegisteredByEmail($email_address[0]['value'], $registration_to_exclude);
    if ($unique_email_address && $existing_registration_id) {
      // If a registration already exists for the email display an error.
      $form_state->setErrorByName('email', $this->t("You've already registered for this event."));
    }
owenbush’s picture

Status: Needs work » Needs review
StatusFileSize
new2.84 KB
new14.8 KB

Attached is a patch to deal with editing registrants.

ryankavalsky’s picture

Thanks for putting out that patch! It didn't apply cleanly for me, for either 2.0.0-beta1 or 2.0.0-beta2. I was using the beta1 and beta2 code pulled fresh from the project page.

2.0.0-beta1:

PS C:\WS\repos\2.0.0-beta1\recurring_events> git apply --check recurring_events-existing_registrations-3183483-20.patch
error: patch failed: modules/recurring_events_registration/recurring_events_registration.install:220
error: modules/recurring_events_registration/recurring_events_registration.install: patch does not apply
error: patch failed: modules/recurring_events_registration/src/Entity/Registrant.php:262
error: modules/recurring_events_registration/src/Entity/Registrant.php: patch does not apply
error: patch failed: modules/recurring_events_registration/src/Plugin/Field/FieldWidget/EventRegistrationWidget.php:277
error: modules/recurring_events_registration/src/Plugin/Field/FieldWidget/EventRegistrationWidget.php: patch does not apply

2.0.0-beta2:

PS C:\WS\repos\2.0.0-beta2\recurring_events> git apply --check recurring_events-existing_registrations-3183483-20.patch
error: patch failed: modules/recurring_events_registration/recurring_events_registration.install:220
error: modules/recurring_events_registration/recurring_events_registration.install: patch does not apply
error: patch failed: modules/recurring_events_registration/src/Entity/Registrant.php:262
error: modules/recurring_events_registration/src/Entity/Registrant.php: patch does not apply
owenbush’s picture

In all likelihood this does not apply to the latest releases because it was built against the latest dev versions of the module, which have had a lot of changes over the last few weeks.

I'm going to try cut a new beta release soon to capture all those changes.

  • owenbush committed d0e7ca1 on 2.0.x
    Issue #3183483 by owenbush, ryankavalsky: Check For Existing...

  • owenbush committed 2381632 on 8.x-1.x
    Issue #3183483 by owenbush, ryankavalsky: Check For Existing...
owenbush’s picture

Status: Needs review » Fixed

I am merging this now, so we can get this into the next beta, and ultimately release candidate, release. If there are issues we can address them with a new patch.

Status: Fixed » Closed (fixed)

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