diff --git a/.env.example b/.env.example index ff738bf0f4..fcbeae72ac 100644 --- a/.env.example +++ b/.env.example @@ -184,6 +184,12 @@ DEFAULT_NUMBER_OF_APPLICATIONS_TO_REMIND = 5 # minimum number of word needed for research_summary field for Credentialing Model. MIN_WORDS_RESEARCH_SUMMARY_CREDENTIALING = 20 +# boolean to control, whether the system should auto reject the event registration application after the event has ended +ENABLE_EVENT_REGISTRATION_AUTO_REJECTION = false + +# maximum number of applications to reject per event every time the management command is run +DEFAULT_NUMBER_OF_APPLICATIONS_TO_REJECT_PER_EVENT = 5 + # CITISOAPService API # This is the WebServices username and password to access the CITI SOAP Service to obtain users training report details # The account can be created at https://webservices.citiprogram.org/login/CreateAccount.aspx diff --git a/deploy/production/etc/cron.d/physionet b/deploy/production/etc/cron.d/physionet index 46e50b93d3..ff06638479 100644 --- a/deploy/production/etc/cron.d/physionet +++ b/deploy/production/etc/cron.d/physionet @@ -19,3 +19,6 @@ # auto remind users of pending credentialing applications in case the references don't respond(sent before the auto reject) 0 */1 * * * www-data env DJANGO_SETTINGS_MODULE=physionet.settings.production /physionet/python-env/physionet/bin/python3 /physionet/physionet-build/physionet-django/manage.py remind_reference_identity_check + +# auto reject pending registration applications for events after the event end date +0 */1 * * * www-data env DJANGO_SETTINGS_MODULE=physionet.settings.production /physionet/python-env/physionet/bin/python3 /physionet/physionet-build/physionet-django/manage.py reject_past_event_applications diff --git a/deploy/staging/etc/cron.d/physionet b/deploy/staging/etc/cron.d/physionet index aac18c44b5..8933801090 100644 --- a/deploy/staging/etc/cron.d/physionet +++ b/deploy/staging/etc/cron.d/physionet @@ -19,3 +19,6 @@ # auto remind users of pending credentialing applications in case the references don't respond(sent before the auto reject) 0 */1 * * * www-data env DJANGO_SETTINGS_MODULE=physionet.settings.staging /physionet/python-env/physionet/bin/python3 /physionet/physionet-build/physionet-django/manage.py remind_reference_identity_check + +# auto reject pending registration applications for events after the event end date +0 */1 * * * www-data env DJANGO_SETTINGS_MODULE=physionet.settings.production /physionet/python-env/physionet/bin/python3 /physionet/physionet-build/physionet-django/manage.py reject_past_event_applications diff --git a/physionet-django/events/management/__init__.py b/physionet-django/events/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/physionet-django/events/management/commands/__init__.py b/physionet-django/events/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/physionet-django/events/management/commands/reject_past_event_applications.py b/physionet-django/events/management/commands/reject_past_event_applications.py new file mode 100644 index 0000000000..a306b8e96b --- /dev/null +++ b/physionet-django/events/management/commands/reject_past_event_applications.py @@ -0,0 +1,54 @@ +import logging + +from django.core.management.base import BaseCommand +from django.conf import settings +from django.db import transaction +from django.http import HttpRequest +from django.utils import timezone + +from events.models import Event, EventApplication +import notification.utility as notification + +LOGGER = logging.getLogger(__name__) + +AUTO_REJECTION_REASON = 'Event has ended.' + + +class Command(BaseCommand): + def add_arguments(self, parser): + parser.add_argument("-n", "--number", type=int, help="Number of applications to be rejected") + + def handle(self, *args, **options): + """ + Auto reject pending registration applications for events that have ended + """ + if not settings.ENABLE_EVENT_REGISTRATION_AUTO_REJECTION: + LOGGER.info('Auto rejection of event applications is disabled.') + return + + # creating an instance of HttpRequest to be used in the notification utility + request = HttpRequest() + + total_applications_per_event_to_reject = (options['number'] + or settings.DEFAULT_NUMBER_OF_APPLICATIONS_TO_REJECT_PER_EVENT) + past_events = Event.objects.filter( + end_date__lt=timezone.now(), + applications__status=EventApplication.EventApplicationStatus.WAITLISTED) + + LOGGER.info(f'{past_events.count()} events selected for auto rejection of waitlisted applications.') + + for event in past_events: + applications = event.applications.filter(status=EventApplication.EventApplicationStatus.WAITLISTED)[ + :total_applications_per_event_to_reject + ] + for application in applications: + with transaction.atomic(): + application.reject(comment_to_applicant=AUTO_REJECTION_REASON) + notification.notify_participant_event_decision( + request=request, + user=application.user, + event=application.event, + decision=EventApplication.EventApplicationStatus.NOT_APPROVED.label, + comment_to_applicant=AUTO_REJECTION_REASON + ) + LOGGER.info(f'Application {application.id} for event {event.id} rejected.') diff --git a/physionet-django/events/management/commands/test_reject_past_event_applications.py b/physionet-django/events/management/commands/test_reject_past_event_applications.py new file mode 100644 index 0000000000..4170c81130 --- /dev/null +++ b/physionet-django/events/management/commands/test_reject_past_event_applications.py @@ -0,0 +1,50 @@ +import logging +from io import StringIO + +from django.conf import settings +from django.core.management import call_command +from django.utils import timezone + +from events.models import Event, EventApplication +from events.tests_views import TestMixin + +LOGGER = logging.getLogger(__name__) + + +class TestRejectPendingCredentialingApplications(TestMixin): + + def test_rejection(self): + + # get dict of applications to be rejected per event + event_application_dict = {} + past_events = Event.objects.filter( + end_date__lt=timezone.now(), + applications__status=EventApplication.EventApplicationStatus.WAITLISTED) + for event in past_events: + event_application_dict[event] = event.applications.filter( + status=EventApplication.EventApplicationStatus.WAITLISTED + )[:settings.DEFAULT_NUMBER_OF_APPLICATIONS_TO_REJECT_PER_EVENT] + + LOGGER.info(f'Found {len(past_events)} events with waitlisted applications to be rejected.') + + # call the management command to auto reject applications + out = StringIO() + call_command('reject_past_event_applications', + number=settings.DEFAULT_NUMBER_OF_APPLICATIONS_TO_REJECT_PER_EVENT, stdout=out) + + if not settings.ENABLE_EVENT_REGISTRATION_AUTO_REJECTION: + # check if the applications status is unchanged + LOGGER.info('Auto rejection of event applications is disabled.') + LOGGER.info('Checking if the applications status is unchanged.') + for event, applications in event_application_dict.items(): + for application in applications: + application.refresh_from_db() + self.assertEqual(application.status, EventApplication.EventApplicationStatus.WAITLISTED) + return + + # check if the applications are rejected + for event, applications in event_application_dict.items(): + for application in applications: + application.refresh_from_db() + self.assertEqual(application.status, EventApplication.EventApplicationStatus.NOT_APPROVED) + LOGGER.info(f'Application {application.id} for event {event.id} auto rejection confirmed.') diff --git a/physionet-django/physionet/settings/base.py b/physionet-django/physionet/settings/base.py index bf5a4192ad..40fe3a1181 100644 --- a/physionet-django/physionet/settings/base.py +++ b/physionet-django/physionet/settings/base.py @@ -217,6 +217,11 @@ DEFAULT_NUMBER_OF_APPLICATIONS_TO_REJECT = config('DEFAULT_NUMBER_OF_APPLICATIONS_TO_REJECT', default=5, cast=int) DEFAULT_NUMBER_OF_APPLICATIONS_TO_REMIND = config('DEFAULT_NUMBER_OF_APPLICATIONS_TO_REMIND', default=5, cast=int) +ENABLE_EVENT_REGISTRATION_AUTO_REJECTION = config('ENABLE_EVENT_REGISTRATION_AUTO_REJECTION', default=False, cast=bool) + +DEFAULT_NUMBER_OF_APPLICATIONS_TO_REJECT_PER_EVENT = config('DEFAULT_NUMBER_OF_APPLICATIONS_TO_REJECT_PER_EVENT', + default=5, cast=int) + GCP_DELEGATION_EMAIL = config('GCP_DELEGATION_EMAIL', default=False) GCP_BUCKET_PREFIX = 'testing-delete.'