Skip to content

Commit

Permalink
implement waiting list feature (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
fritzmg authored Jun 7, 2024
1 parent 99f238c commit 8d9941b
Show file tree
Hide file tree
Showing 24 changed files with 251 additions and 9 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ After installation you will have the possibility to enable registration for indi
- **End of registraton**: You can define an optional date after which registration will not be possible anymore.
- **End of cancellation**: You cand efine an optional date after which cancellation will not be possible anymore.
- **Require confirmation**: When enabled, only confirmed registrations count towards the total number of registrations.
- **Enable waiting list**: Keeps the registration open after the maximum amount of participants is reached. All registrations will be put on a waiting list and will automatically be advanced, if prior registrations are cancelled.
- **Advancement from waiting list notification**: This notification will be sent when a participant is advanced from the waiting list.


### Modules
Expand Down Expand Up @@ -63,6 +65,7 @@ The following template variables are available in event templates as well as the
- `$this->canRegister`: Whether registration is possible for this event.
- `$this->registrationForm`: Contains the registration form.
- `$this->isFull`: Whether the maximum amount of registrations have been reached for this event.
- `$this->isWaitingList`: Whether the maximum amount of registrations have been reached for this event and waiting list is enabled.
- `$this->registrationCount`: The current registration count for this event.
- `$this->reg_min`: The minimum amount of registrations for this event.
- `$this->reg_max`: The maximum amount of registrations for this event.
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"symfony/dependency-injection": "^5.4 || ^6.4",
"symfony/http-foundation": "^5.4 || ^6.4",
"symfony/http-kernel": "^5.4 || ^6.4",
"symfony/lock": "^5.4 || ^6.4",
"symfony/routing": "^5.4 || ^6.4",
"symfony/security-core": "^5.4 || ^6.4",
"symfony/translation": "^5.4 || ^6.4",
Expand Down
3 changes: 2 additions & 1 deletion contao/config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
$coreForm[$type][] = 'reg_cancel_url';
}

$tokensContent = ['event_*', 'reg_*', 'reg_count', 'reg_confirm_url', 'reg_confirm_url', 'admin_email'];
$tokensContent = ['event_*', 'reg_*', 'reg_count', 'reg_confirm_url', 'reg_cancel_url', 'admin_email'];
$tokensAddress = ['admin_email', 'reg_*', 'event_*'];

$GLOBALS['NOTIFICATION_CENTER']['NOTIFICATION_TYPE']['event_registration'][NotificationTypes::CONFIRM] = [
Expand All @@ -42,3 +42,4 @@
];

$GLOBALS['NOTIFICATION_CENTER']['NOTIFICATION_TYPE']['event_registration'][NotificationTypes::CANCEL] = &$GLOBALS['NOTIFICATION_CENTER']['NOTIFICATION_TYPE']['event_registration'][NotificationTypes::CONFIRM];
$GLOBALS['NOTIFICATION_CENTER']['NOTIFICATION_TYPE']['event_registration'][NotificationTypes::WAITING_LIST_ADVANCEMENT] = &$GLOBALS['NOTIFICATION_CENTER']['NOTIFICATION_TYPE']['event_registration'][NotificationTypes::CONFIRM];
19 changes: 18 additions & 1 deletion contao/dca/tl_calendar_events.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,25 @@
'sql' => ['type' => 'boolean', 'default' => false],
];

$GLOBALS['TL_DCA']['tl_calendar_events']['fields']['reg_enableWaitingList'] = [
'inputType' => 'checkbox',
'exclude' => true,
'eval' => ['tl_class' => 'clr w50', 'submitOnChange' => true],
'sql' => ['type' => 'boolean', 'default' => false],
];

$GLOBALS['TL_DCA']['tl_calendar_events']['fields']['reg_waitingListAdvancementNotification'] = [
'exclude' => true,
'inputType' => 'select',
'eval' => ['includeBlankOption' => true, 'chosen' => true, 'tl_class' => 'w50'],
'sql' => ['type' => 'integer', 'unsigned' => true, 'default' => 0],
'relation' => ['type' => 'hasOne', 'load' => 'lazy', 'table' => 'tl_nc_notification'],
];

$GLOBALS['TL_DCA']['tl_calendar_events']['palettes']['__selector__'][] = 'reg_enable';
$GLOBALS['TL_DCA']['tl_calendar_events']['subpalettes']['reg_enable'] = 'reg_form,reg_min,reg_max,reg_regEnd,reg_cancelEnd,reg_requireConfirm';
$GLOBALS['TL_DCA']['tl_calendar_events']['palettes']['__selector__'][] = 'reg_enableWaitingList';
$GLOBALS['TL_DCA']['tl_calendar_events']['subpalettes']['reg_enable'] = 'reg_form,reg_min,reg_max,reg_regEnd,reg_cancelEnd,reg_requireConfirm,reg_enableWaitingList';
$GLOBALS['TL_DCA']['tl_calendar_events']['subpalettes']['reg_enableWaitingList'] = 'reg_waitingListAdvancementNotification';

foreach ($GLOBALS['TL_DCA']['tl_calendar_events']['palettes'] as $name => $palette) {
if (!is_string($palette)) {
Expand Down
8 changes: 7 additions & 1 deletion contao/dca/tl_event_registration.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@
'eval' => ['tl_class' => 'w50'],
'sql' => ['type' => 'boolean', 'default' => false],
],
'waiting' => [
'inputType' => 'checkbox',
'exclude' => true,
'eval' => ['tl_class' => 'w50', 'disabled' => false],
'sql' => ['type' => 'boolean', 'default' => false],
],
'form_data' => [
'inputType' => 'textarea',
'eval' => ['readonly' => true],
Expand Down Expand Up @@ -120,6 +126,6 @@
],

'palettes' => [
'default' => '{reg_legend},form_data,amount,confirmed,cancelled',
'default' => '{reg_legend},form_data,amount,confirmed,cancelled,waiting',
],
];
1 change: 1 addition & 0 deletions contao/languages/de/tl_nc_notification.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@
$GLOBALS['TL_LANG']['tl_nc_notification']['type']['event_registration'] = 'Event Registrierung';
$GLOBALS['TL_LANG']['tl_nc_notification']['type'][NotificationTypes::CONFIRM] = ['Registrierung bestätigen', 'Diese Benachrichtigung wird gesendet, wenn eine Registrierung für ein Event bestätigt wurde.'];
$GLOBALS['TL_LANG']['tl_nc_notification']['type'][NotificationTypes::CANCEL] = ['Registrierung stornieren', 'Diese Benachrichtigung wird gesendet, wenn eine Registrierung für ein Event storniert wurde.'];
$GLOBALS['TL_LANG']['tl_nc_notification']['type'][NotificationTypes::WAITING_LIST_ADVANCEMENT] = ['Aufstieg von Warteliste', 'Diese Benachrichtigung wird gesendet, wenn eine Registrierung von der Warteliste auf die normale Liste kommt.'];
1 change: 1 addition & 0 deletions contao/languages/de/tokens.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@

$GLOBALS['TL_LANG']['NOTIFICATION_CENTER_TOKEN'][NotificationTypes::CONFIRM] = &$GLOBALS['TL_LANG']['NOTIFICATION_CENTER_TOKEN']['core_form'];
$GLOBALS['TL_LANG']['NOTIFICATION_CENTER_TOKEN'][NotificationTypes::CANCEL] = &$GLOBALS['TL_LANG']['NOTIFICATION_CENTER_TOKEN']['core_form'];
$GLOBALS['TL_LANG']['NOTIFICATION_CENTER_TOKEN'][NotificationTypes::WAITING_LIST_ADVANCEMENT] = &$GLOBALS['TL_LANG']['NOTIFICATION_CENTER_TOKEN']['core_form'];
2 changes: 2 additions & 0 deletions contao/languages/en/tl_calendar_events.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@
$GLOBALS['TL_LANG']['tl_calendar_events']['reg_regEnd'] = ['End of registration', 'Specify an optional date for the end of the registration.'];
$GLOBALS['TL_LANG']['tl_calendar_events']['reg_cancelEnd'] = ['End of cancellation', 'Specify an optional date for the end of being able to cancel.'];
$GLOBALS['TL_LANG']['tl_calendar_events']['reg_requireConfirm'] = ['Require confirmation', 'Only confirmed registrations are counted.'];
$GLOBALS['TL_LANG']['tl_calendar_events']['reg_enableWaitingList'] = ['Enable waiting list', 'Enables registrations past the maximum amount of participants.'];
$GLOBALS['TL_LANG']['tl_calendar_events']['reg_waitingListAdvancementNotification'] = ['Advancement from waiting list notification', 'This notification will be sent when a participant is moved from the waiting list to the regular list of active registrations.'];
$GLOBALS['TL_LANG']['tl_calendar_events']['registrations'] = 'Registrations';
1 change: 1 addition & 0 deletions contao/languages/en/tl_event_registration.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@
$GLOBALS['TL_LANG']['tl_event_registration']['amount'] = ['Amount', 'Amount of people for this registration.'];
$GLOBALS['TL_LANG']['tl_event_registration']['confirmed'] = ['Confirmed', 'Whether this registration has been confirmed.'];
$GLOBALS['TL_LANG']['tl_event_registration']['cancelled'] = ['Cancelled', 'Whether this registration has been cancelled.'];
$GLOBALS['TL_LANG']['tl_event_registration']['waiting'] = ['Waiting list', 'Whether this registration is on the waiting list.'];
$GLOBALS['TL_LANG']['tl_event_registration']['form_data'] = ['Form data', 'The form data for this registration.'];
1 change: 1 addition & 0 deletions contao/languages/en/tl_nc_notification.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@
$GLOBALS['TL_LANG']['tl_nc_notification']['type']['event_registration'] = 'Event registration';
$GLOBALS['TL_LANG']['tl_nc_notification']['type'][NotificationTypes::CONFIRM] = ['Confirm registration', 'This notification type can be sent when an event registration is confirmed.'];
$GLOBALS['TL_LANG']['tl_nc_notification']['type'][NotificationTypes::CANCEL] = ['Cancel registration', 'This notification type can be sent when an event registration is cancelled.'];
$GLOBALS['TL_LANG']['tl_nc_notification']['type'][NotificationTypes::WAITING_LIST_ADVANCEMENT] = ['Advancement from waiting list', 'This notification type can be sent when an event registration is cancelled.'];
1 change: 1 addition & 0 deletions contao/languages/en/tokens.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@

$GLOBALS['TL_LANG']['NOTIFICATION_CENTER_TOKEN'][NotificationTypes::CONFIRM] = &$GLOBALS['TL_LANG']['NOTIFICATION_CENTER_TOKEN']['core_form'];
$GLOBALS['TL_LANG']['NOTIFICATION_CENTER_TOKEN'][NotificationTypes::CANCEL] = &$GLOBALS['TL_LANG']['NOTIFICATION_CENTER_TOKEN']['core_form'];
$GLOBALS['TL_LANG']['NOTIFICATION_CENTER_TOKEN'][NotificationTypes::WAITING_LIST_ADVANCEMENT] = &$GLOBALS['TL_LANG']['NOTIFICATION_CENTER_TOKEN']['core_form'];
3 changes: 3 additions & 0 deletions contao/templates/mod_event_registration_form.html5
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
<?php if (!$this->canRegister): ?>
<p class="cannot-register"><?= $this->trans('cannot_register', [], 'im_contao_event_registration') ?></p>
<?php else: ?>
<?php if ($this->isWaitingList): ?>
<p class="waiting-list"><?= $this->trans('is_waiting_list', [], 'im_contao_event_registration') ?></p>
<?php endif; ?>
<?= $this->registrationForm ?>
<?php endif; ?>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Contao\Template;
use InspiredMinds\ContaoEventRegistration\EventRegistration;
use InspiredMinds\ContaoEventRegistration\Model\EventRegistrationModel;
use InspiredMinds\ContaoEventRegistration\WaitingListChecker;
use NotificationCenter\Model\Notification;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
Expand All @@ -42,6 +43,7 @@ public function __construct(
private readonly NodeManager $nodeManager,
private readonly TranslatorInterface $translator,
private readonly SimpleTokenParser $simpleTokenParser,
private readonly WaitingListChecker $waitingListChecker,
) {
}

Expand Down Expand Up @@ -111,5 +113,8 @@ private function processCancel(Template $template, ModuleModel $model, CalendarE
if (!empty($model->nc_notification) && null !== ($notification = Notification::findById((int) $model->nc_notification))) {
$notification->send($tokens);
}

// Process waiting lists
($this->waitingListChecker)($event);
}
}
29 changes: 29 additions & 0 deletions src/Cron/WaitingListsCronJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Contao Event Registration extension.
*
* (c) INSPIRED MINDS
*
* @license LGPL-3.0-or-later
*/

namespace InspiredMinds\ContaoEventRegistration\Cron;

use Contao\CoreBundle\DependencyInjection\Attribute\AsCronJob;
use InspiredMinds\ContaoEventRegistration\WaitingListChecker;

#[AsCronJob('hourly')]
class WaitingListsCronJob
{
public function __construct(private readonly WaitingListChecker $waitingListChecker)
{
}

public function __invoke(): void
{
($this->waitingListChecker)();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public function __invoke(DataContainer $dc): void
->removeField('reg_regEnd')
->removeField('reg_cancelEnd')
->removeField('reg_requireConfirm')
->removeField('reg_enableWaitingList')
->applyToSubPalette('reg_enable', 'tl_calendar_events')
;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Contao Event Registration extension.
*
* (c) INSPIRED MINDS
*
* @license LGPL-3.0-or-later
*/

namespace InspiredMinds\ContaoEventRegistration\EventListener\DataContainer\CalendarEvents;

use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
use Doctrine\DBAL\Connection;
use InspiredMinds\ContaoEventRegistration\NotificationTypes;

#[AsCallback('tl_calendar_events', 'fields.reg_waitingListAdvancementNotification.options')]
class WaitingListAdvancementNotificationOptionsCallbackListener
{
public function __construct(private readonly Connection $db)
{
}

public function __invoke(): array
{
return $this->db->fetchAllKeyValue('SELECT id, title FROM tl_nc_notification WHERE type = ? ORDER BY title', [NotificationTypes::WAITING_LIST_ADVANCEMENT]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@
use Contao\Image;
use Contao\StringUtil;
use InspiredMinds\ContaoEventRegistration\EventRegistration\LabelBuilder;
use Symfony\Contracts\Translation\TranslatorInterface;

/**
* @Callback(table="tl_event_registration", target="list.sorting.child_record")
*/
class ChildRecordCallbackListener
{
public function __construct(private readonly LabelBuilder $labelBuilder)
{
public function __construct(
private readonly LabelBuilder $labelBuilder,
private readonly TranslatorInterface $translator,
) {
}

public function __invoke(array $row): string
Expand All @@ -42,14 +45,25 @@ public function __invoke(array $row): string
}

$icon = 'visible_.svg';
$alt = '';
$attributes = ' style="float:left; margin-right:0.3em;"';

if ($row['cancelled']) {
$icon = 'unpublished.svg';
$alt = $this->translator->trans('tl_event_registration.cancelled.0', [], 'contao_tl_event_registration');
} elseif ($row['waiting']) {
$icon = 'news.svg';
$alt = $this->translator->trans('tl_event_registration.waiting.0', [], 'contao_tl_event_registration');
} elseif ($row['confirmed']) {
$icon = 'visible.svg';
$alt = $this->translator->trans('tl_event_registration.confirmed.0', [], 'contao_tl_event_registration');
}

if ($alt) {
$attributes .= ' title="'.$alt.'"';
}

$label = Image::getHtml($icon, '', ' style="float:left; margin-right:0.3em;"').' '.$label;
$label = Image::getHtml($icon, '', $attributes).' '.$label;

return '<div class="tl_content_left">'.$label.'</div>';
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Contao Event Registration extension.
*
* (c) INSPIRED MINDS
*
* @license LGPL-3.0-or-later
*/

namespace InspiredMinds\ContaoEventRegistration\EventListener\DataContainer\EventRegistration;

use Contao\CalendarEventsModel;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
use Contao\DataContainer;
use InspiredMinds\ContaoEventRegistration\WaitingListChecker;

#[AsCallback('tl_event_registration', 'config.onsubmit')]
class ConfigOnSubmitCallbackListener
{
public function __construct(private readonly WaitingListChecker $waitingListChecker)
{
}

public function __invoke(DataContainer $dc): void
{
if ($event = CalendarEventsModel::findById($dc->activeRecord?->pid)) {
($this->waitingListChecker)($event);
}
}
}
1 change: 1 addition & 0 deletions src/EventListener/EventRegistrationFormListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public function __invoke(array &$submittedData, array $formData, array|null $fil
$registration->form = (int) $form->id;
$registration->member = $member ? (int) $member->id : 0;
$registration->amount = max(1, (int) ($submittedData['amount'] ?? 1));
$registration->waiting = $this->eventRegistration->isWaitingList($event);
$registration->form_data = json_encode($submittedData, JSON_THROW_ON_ERROR);

$registration->save();
Expand Down
17 changes: 14 additions & 3 deletions src/EventRegistration.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ public function addTemplateData(Template $template, CalendarEventsModel $event):

$template->registrationCount = fn (): int => $this->getRegistrationCount($event);

$template->isWaitingList = fn (): bool => $this->isWaitingList($event);

$mainEvent = $this->getMainEvent($event);

$template->reg_min = $mainEvent->reg_min;
Expand All @@ -80,7 +82,12 @@ public function canRegister(CalendarEventsModel $event): bool
return false;
}

return !$this->isFull($event);
return $this->isWaitingList($event) || !$this->isFull($event);
}

public function isWaitingList(CalendarEventsModel $event): bool
{
return $this->getMainEvent($event)->reg_enableWaitingList && $this->isFull($event);
}

public function isFull(CalendarEventsModel $event): bool
Expand All @@ -92,12 +99,12 @@ public function isFull(CalendarEventsModel $event): bool
return false;
}

$count = $this->getRegistrationCount($event);
$count = $this->getRegistrationCount($event, false);

return $count >= (int) $event->reg_max;
}

public function getRegistrationCount(CalendarEventsModel $event): int
public function getRegistrationCount(CalendarEventsModel $event, bool $excludeWaitingList = false): int
{
$event = $this->getMainEvent($event);

Expand All @@ -107,6 +114,10 @@ public function getRegistrationCount(CalendarEventsModel $event): int
$query .= ' AND confirmed = 1';
}

if ($event->reg_enableWaitingList && $excludeWaitingList) {
$query .= ' AND waiting != 1';
}

$query .= ';';

return (int) $this->db->fetchOne($query, [(int) $event->id]);
Expand Down
2 changes: 2 additions & 0 deletions src/NotificationTypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ class NotificationTypes
public const CONFIRM = 'event_registration_confirm';

public const CANCEL = 'event_registration_cancel';

public const WAITING_LIST_ADVANCEMENT = 'event_registration_waiting_list_advancement';
}
Loading

0 comments on commit 8d9941b

Please sign in to comment.