Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move participant validation out of registration forms into the Participant BAO #30698

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
295 changes: 295 additions & 0 deletions CRM/Event/BAO/Participant.php
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,20 @@ public static function create(&$params) {
return $participant;
}

/**
* @internal
* Do not call this from outside core code. It is expected to change multiple times in the course of refactoring
* participant validation out of the QuickForm layer. Eventually, there will be an API action for validation.
*/
public static function getAvailableSpaces(int $eventId, bool $includeWaitlist = TRUE): int {
jensschuppe marked this conversation as resolved.
Show resolved Hide resolved
$availableSpaces = self::eventFull(
$eventId,
TRUE,
$includeWaitlist
);
return is_numeric($availableSpaces) ? (int) $availableSpaces : 0;
}

/**
* Check whether the event is full for participation and return as.
* per requirements.
Expand Down Expand Up @@ -832,6 +846,287 @@ public static function deleteParticipant($id) {
return $participant;
}

/**
* Retrieves existing participants for a given event and contact.
*
* @param int $contactId
* The contact ID of participants to find.
* @param int $eventId
* The event ID of participants to find.
* @param bool $onlyCounted
* Whether to only consider registrations with a status with "is_counted".
* @param bool $includeOnWaitlist
* Whether to consider registrations with status "On waitlist" when restricing to "is_counted".
* @param array $excludeStatus
* A list of registration status to not consider (e.g. for ignoring cancelled registrations).
* @param array $filterRoleIds
* A list of participant role IDs to filter for. Registrations with other roles will not be considered.
* @param bool $includeTest
* Whether to include test participants.
*
* @return array<int,array{id:int,"status_id:name":string}>
* An array of participants (a subset of attributes) matching the given criteria, keyed by ID.
* @throws \CRM_Core_Exception
* @throws \Civi\API\Exception\UnauthorizedException
*
* @internal
* Do not call this from outside core code. It is expected to change multiple times in the course of refactoring
* participant validation out of the QuickForm layer. Eventually, there will be an API action for validation.
*/
public static function getExistingParticipants(
int $contactId,
int $eventId,
bool $onlyCounted = TRUE,
bool $includeOnWaitlist = TRUE,
array $excludeStatus = ['Cancelled'],
array $filterRoleIds = [],
bool $includeTest = FALSE
) {
$query = \Civi\Api4\Participant::get(FALSE)
->addSelect('id', 'status_id:name')
->addWhere('contact_id', '=', $contactId)
->addWhere('event_id', '=', $eventId);

if ($onlyCounted) {
$query
->addJoin('ParticipantStatusType AS participant_status_type', 'LEFT');
jensschuppe marked this conversation as resolved.
Show resolved Hide resolved
$clauses = [
['participant_status_type.is_counted', '=', TRUE],
];
if ($includeOnWaitlist) {
$clauses = ['participant_status_type.name', '=', 'On waitlist'];
}
$query->addClause('OR', $clauses);
}

if ([] !== $excludeStatus) {
$query->addWhere('status_id:name', 'NOT IN', $excludeStatus);
}

if ([] !== $filterRoleIds) {
$query->addWhere('role_id', 'IN', $filterRoleIds);
}

if (!$includeTest) {
$query->addWhere('is_test', '=', FALSE);
}

$result = $query->execute();
return $result->getArrayCopy();
}

/**
* @param int $contactId
* @param int $eventId
* @param string $context
* Either "admin" or "public" depending on the context of the validation/form.
* @param bool $isAdditional
* Whether to validate for an additional participant.
*
* @return array<int|string,string>
* A list of validation errors, possibly keyed by attribute name the error corresponds to.
* @throws \CRM_Core_Exception
*
* @internal
* Do not call this from outside core code. It is expected to change multiple times in the course of refactoring
* participant validation out of the QuickForm layer. Eventually, there will be an API action for validation.
*/
public static function validateExistingRegistration(
int $contactId,
int $eventId,
string $context = 'public',
bool $isAdditional = FALSE
): array {
$event = \Civi\Api4\Event::get(FALSE)
->addSelect(
'default_role_id',
'allow_same_participant_emails'
)
->addWhere('id', '=', $eventId)
->execute()
->single();
$excludeStatus = 'admin' === $context ? ['Cancelled'] : [];
$participantRoleIds = 'public' === $context ? [$participantRoleId ?? $event['default_role_id']] : [];
$existingParticipants = CRM_Event_BAO_Participant::getExistingParticipants(
$contactId,
$eventId,
TRUE,
TRUE,
$excludeStatus,
$participantRoleIds
);
$errors = [];

if (count($existingParticipants) > 0) {
if ($isAdditional) {
$errors[] = ts("It looks like this participant is already registered for this event. If you want to change your registration, or you feel that you've received this message in error, please contact the site administrator.");
}
elseif (!(bool) $event['allow_same_participant_emails']) {
if ('admin' === $context) {
$errors['event_id'] = ts('This contact has already been assigned to this event.');
}
else {
if ('On waitlist' === reset($existingParticipants)['status_id:name']) {
$errors[] = ts("It looks like you are already waitlisted for this event. If you want to change your registration, or you feel that you've received this message in error, please contact the site administrator.");
}
else {
$errors[] = ts("It looks like you are already registered for this event. If you want to change your registration, or you feel that you've received this message in error, please contact the site administrator.");
}
}
}
}

return $errors;
}

/**
* @param array $values
* A list of values to validate, keyed by participant properties/field names.
* @param string $context
* The context for which to create error messages. Either "public" or "admin".
*
* @return array<int|string,string>
* A list of validation error messages, possibly keyed by affected participant properties/field names.
*
* @internal
* Do not call this from outside core code. It is expected to change multiple times in the course of refactoring
* participant validation out of the QuickForm layer. Eventually, there will be an API action for validation.
*/
public static function validateAvailableSpaces(
array $values,
string $context = 'public'
): array {
$errors = [];
$eventId = $values['event_id'];
$event = \Civi\Api4\Event::get(FALSE)
->addSelect('has_waitlist', 'max_participants')
->addWhere('id', '=', $eventId)
->execute()
->single();

$availableSpaces = CRM_Event_BAO_Participant::eventFull(
$eventId,
TRUE,
$event['has_waitlist']
);
$spacesAvailable = is_numeric($availableSpaces) ? (int) $availableSpaces : 0;

//check for availability of registrations.
if ($event['max_participants'] !== NULL
&& !empty($values['additional_participants'])
&& empty($values['bypass_payment']) &&
((int) $values['additional_participants']) >= $spacesAvailable
) {
$errors['additional_participants'] = ts(
'There is only enough space left on this event for %1 participant(s).',
[1 => $spacesAvailable]
);
}

return $errors;
}

/**
* @param int $eventId
*
* @return array<int|string,string>
* A list of validation error messages, possibly keyed by affected participant properties/field names.
* @throws \CRM_Core_Exception
*
* @internal
jensschuppe marked this conversation as resolved.
Show resolved Hide resolved
* Do not call this from outside core code. It is expected to change multiple times in the course of refactoring
* participant validation out of the QuickForm layer. Eventually, there will be an API action for validation.
*/
public static function validateEvent(int $eventId): array {
$event = \Civi\Api4\Event::get(FALSE)
->addWhere('id', '=', $eventId)
->execute()
->single();
$errors = [];

// is the event active (enabled)?
if (!$event['is_active']) {
// form is inactive, die a fatal death
$errors[] = ts('The event you requested is currently unavailable (contact the site administrator for assistance).');
}

// is online registration is enabled?
if (!$event['is_online_registration']) {
$errors[] = ts('Online registration is not currently available for this event (contact the site administrator for assistance).');
}

// is this an event template ?
if (!empty($event['is_template'])) {
$errors[] = ts('Event templates are not meant to be registered.');
}

$now = date('YmdHis');
$startDate = CRM_Utils_Date::processDate($event['registration_start_date'] ?? NULL);

if ($startDate && ($startDate >= $now)) {
$errors[] = ts(
'Registration for this event begins on %1',
[1 => CRM_Utils_Date::customFormat($event['registration_start_date'] ?? NULL)]
);
}

$regEndDate = CRM_Utils_Date::processDate($event['registration_end_date'] ?? NULL);
$eventEndDate = CRM_Utils_Date::processDate($event['event_end_date'] ?? NULL);
if (($regEndDate && ($regEndDate < $now)) || (empty($regEndDate) && !empty($eventEndDate) && ($eventEndDate < $now))) {
$endDate = CRM_Utils_Date::customFormat($event['registration_end_date'] ?? NULL);
if (empty($regEndDate)) {
$endDate = CRM_Utils_Date::customFormat($event['event_end_date'] ?? NULL);
}
$errors[] = ts('Registration for this event ended on %1', [1 => $endDate]);
}

return $errors;
}

/**
* @param array $values
* A list of values to validate, keyed by participant properties/field names.
* @param string $context
* The context for which to create error messages. Either "public" or "admin".
* @param bool $isAdditional
* Whether to validate an additional participant.
*
* @return array<int|string,string>
* A list of validation error messages, possibly keyed by affected participant properties/field names.
*
* @internal
* Do not call this from outside core code. It is expected to change multiple times in the course of refactoring
* participant validation out of the QuickForm layer. Eventually, there will be an API action for validation.
*/
public static function validateEventRegistration(
array $values,
string $context = 'public',
bool $isAdditional = FALSE
): array {
if (!isset($values['event_id'])) {
throw new CRM_Core_Exception('Event ID is required for validating event registrations.');
}
if (!isset($values['contact_id'])) {
// TODO: Support validating without a contact?
throw new CRM_Core_Exception('Contact ID is required for validating event registrations.');
}

$errors = [];

// Validate event (status, registration period, etc.).
$errors += self::validateEvent($values['event_id']);

// Validate existing participants for the registering contact.
$errors += self::validateExistingRegistration($values['contact_id'], $values['event_id'], $context, $isAdditional);

// Validate available spaces.
$errors += self::validateAvailableSpaces($values, $context);

// TODO: Validate against available price options.

return $errors;
}

/**
* Checks duplicate participants.
*
Expand Down
5 changes: 2 additions & 3 deletions CRM/Event/Form/EventFormTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,10 @@ public function getParticipantValue(string $fieldName) {
* @throws \CRM_Core_Exception
*/
protected function getAvailableSpaces(): int {
$availableSpaces = CRM_Event_BAO_Participant::eventFull($this->getEventID(),
TRUE,
return CRM_Event_BAO_Participant::getAvailableSpaces(
$this->getEventID(),
$this->getEventValue('has_waitlist')
);
return is_numeric($availableSpaces) ? (int) $availableSpaces : 0;
}

/**
Expand Down
18 changes: 3 additions & 15 deletions CRM/Event/Form/Participant.php
Original file line number Diff line number Diff line change
Expand Up @@ -775,21 +775,9 @@ public static function formRule($values, $files, $self) {

$eventId = $values['event_id'] ?? NULL;

$event = new CRM_Event_DAO_Event();
$event->id = $eventId;
$event->find(TRUE);

if (!$event->allow_same_participant_emails && !empty($contactId) && !empty($eventId)) {
$cancelledStatusID = CRM_Core_PseudoConstant::getKey('CRM_Event_BAO_Participant', 'status_id', 'Cancelled');
$dupeCheck = new CRM_Event_BAO_Participant();
$dupeCheck->contact_id = $contactId;
$dupeCheck->event_id = $eventId;
$dupeCheck->whereAdd("status_id != {$cancelledStatusID} ");
$dupeCheck->find(TRUE);
if (!empty($dupeCheck->id)) {
$errorMsg['event_id'] = ts('This contact has already been assigned to this event.');
}
}
$errorMsg += CRM_Event_BAO_Participant::validateExistingRegistration($contactId, $eventId, 'admin');

// TODO: No check for available spaces?
}
return empty($errorMsg) ? TRUE : $errorMsg;
}
Expand Down
39 changes: 5 additions & 34 deletions CRM/Event/Form/Registration.php
Original file line number Diff line number Diff line change
Expand Up @@ -1646,40 +1646,11 @@ public function processFirstParticipant($participantID) {
*
*/
protected function checkValidEvent(): void {
// is the event active (enabled)?
if (!$this->_values['event']['is_active']) {
// form is inactive, die a fatal death
CRM_Core_Error::statusBounce(ts('The event you requested is currently unavailable (contact the site administrator for assistance).'));
}

// is online registration is enabled?
if (!$this->_values['event']['is_online_registration']) {
CRM_Core_Error::statusBounce(ts('Online registration is not currently available for this event (contact the site administrator for assistance).'), $this->getInfoPageUrl());
}

// is this an event template ?
if (!empty($this->_values['event']['is_template'])) {
CRM_Core_Error::statusBounce(ts('Event templates are not meant to be registered.'), $this->getInfoPageUrl());
}

$now = date('YmdHis');
$startDate = CRM_Utils_Date::processDate($this->_values['event']['registration_start_date'] ?? NULL);

if ($startDate && ($startDate >= $now)) {
CRM_Core_Error::statusBounce(ts('Registration for this event begins on %1',
[1 => CRM_Utils_Date::customFormat($this->_values['event']['registration_start_date'] ?? NULL)]),
$this->getInfoPageUrl(),
ts('Sorry'));
}

$regEndDate = CRM_Utils_Date::processDate($this->_values['event']['registration_end_date'] ?? NULL);
$eventEndDate = CRM_Utils_Date::processDate($this->_values['event']['event_end_date'] ?? NULL);
if (($regEndDate && ($regEndDate < $now)) || (empty($regEndDate) && !empty($eventEndDate) && ($eventEndDate < $now))) {
$endDate = CRM_Utils_Date::customFormat($this->_values['event']['registration_end_date'] ?? NULL);
if (empty($regEndDate)) {
$endDate = CRM_Utils_Date::customFormat($this->_values['event']['event_end_date'] ?? NULL);
}
CRM_Core_Error::statusBounce(ts('Registration for this event ended on %1', [1 => $endDate]), $this->getInfoPageUrl(), ts('Sorry'));
foreach (CRM_Event_BAO_Participant::validateEvent($this->getEventID()) as $error) {
CRM_Core_Error::statusBounce(
$error,
$this->_values['event']['is_active'] ? $this->getInfoPageUrl() : NULL
);
}
}

Expand Down
Loading