diff --git a/physionet-django/notification/utility.py b/physionet-django/notification/utility.py index 9e03c6d90a..96443031fb 100644 --- a/physionet-django/notification/utility.py +++ b/physionet-django/notification/utility.py @@ -1025,3 +1025,19 @@ def notify_event_participant_application(request, user, registered_user, event): body = loader.render_to_string('events/email/event_registration.html', context) # Not resend the email if there was an integrity error send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [user.email], fail_silently=False) + + +def notify_users_of_training_expiry(user, training, expiry): + """ + Send the training expiry email. + """ + + subject = f"{settings.SITE_NAME} Training Validation Notification" + context = { + 'name': user.get_full_name(), + 'SITE_NAME': settings.SITE_NAME, + 'training': training, + 'expiry': expiry + } + body = loader.render_to_string('training/email/training_notification.html', context) + send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [user, ], fail_silently=False) diff --git a/physionet-django/training/serializers.py b/physionet-django/training/serializers.py new file mode 100644 index 0000000000..61e10cd0c7 --- /dev/null +++ b/physionet-django/training/serializers.py @@ -0,0 +1,164 @@ +import datetime +from rest_framework import serializers +from django.db import transaction +from django.utils import timezone + +from training.models import OnPlatformTraining, Module, Quiz, QuizChoice, ContentBlock +from user.models import Training, TrainingType +from notification.utility import notify_users_of_training_expiry + + +NUMBER_OF_DAYS_SET_TO_EXPIRE = 30 + + +class QuizChoiceSerializer(serializers.ModelSerializer): + + class Meta: + model = QuizChoice + fields = "__all__" + read_only_fields = ['id', 'quiz'] + + +class QuizSerializer(serializers.ModelSerializer): + choices = QuizChoiceSerializer(many=True) + + class Meta: + model = Quiz + fields = "__all__" + read_only_fields = ['id', 'module'] + + +class ContentBlockSerializer(serializers.ModelSerializer): + + class Meta: + model = ContentBlock + fields = "__all__" + read_only_fields = ['id', 'module'] + + +class ModuleSerializer(serializers.ModelSerializer): + quizzes = QuizSerializer(many=True) + contents = ContentBlockSerializer(many=True) + + class Meta: + model = Module + fields = "__all__" + read_only_fields = ['id', 'training'] + + +class OnPlatformTrainingSerializer(serializers.ModelSerializer): + modules = ModuleSerializer(many=True) + + class Meta: + model = OnPlatformTraining + fields = "__all__" + read_only_fields = ['id', 'training_type'] + + +class TrainingTypeSerializer(serializers.ModelSerializer): + op_trainings = OnPlatformTrainingSerializer() + + class Meta: + model = TrainingType + fields = "__all__" + read_only_fields = ['id'] + + def update_training_for_major_version_change(self, instance): + """ + If it is a major version change, it sets all former user trainings + to a reduced date, and informs them all. + """ + + trainings = Training.objects.filter( + training_type=instance, + process_datetime__gte=timezone.now() - instance.valid_duration) + _ = trainings.update( + process_datetime=( + timezone.now() - (instance.valid_duration - timezone.timedelta( + days=NUMBER_OF_DAYS_SET_TO_EXPIRE)))) + + for training in trainings: + notify_users_of_training_expiry( + training.user, instance.name, NUMBER_OF_DAYS_SET_TO_EXPIRE) + + def update(self, instance, validated_data): + + with transaction.atomic(): + op_training = validated_data.pop('op_trainings') + modules = op_training.pop('modules') + + op_training['training_type'] = instance + + op_training_instance = OnPlatformTraining.objects.create(**op_training) + + for module in modules: + quizzes = module.pop('quizzes') + contents = module.pop('contents') + + module['training'] = op_training_instance + module_instance = Module.objects.create(**module) + + choice_bulk = [] + for quiz in quizzes: + choices = quiz.pop('choices') + + quiz['module'] = module_instance + q = Quiz(**quiz) + q.save() + + for choice in choices: + choice['quiz'] = q + choice_bulk.append(QuizChoice(**choice)) + + QuizChoice.objects.bulk_create(choice_bulk) + + content_bulk = [] + for content in contents: + content['module'] = module_instance + content_bulk.append(ContentBlock(**content)) + ContentBlock.objects.bulk_create(content_bulk) + + if op_training.get("version"): + if str(op_training.get("version")).endswith("0"): + self.update_training_for_major_version_change(instance) + + return op_training_instance + + def create(self, validated_data): + + with transaction.atomic(): + op_training = validated_data.pop('op_trainings') + modules = op_training.pop('modules') + + op_training['training_type'] = instance = TrainingType.objects.create(**validated_data) + + op_training_instance = OnPlatformTraining.objects.create(**op_training) + + for module in modules: + quizzes = module.pop('quizzes') + contents = module.pop('contents') + + module['training'] = op_training_instance + module_instance = Module.objects.create(**module) + + choice_bulk = [] + for quiz in quizzes: + choices = quiz.pop('choices') + + quiz['module'] = module_instance + q = Quiz(**quiz) + q.save() + + for choice in choices: + choice['quiz'] = q + choice_bulk.append(QuizChoice(**choice)) + + QuizChoice.objects.bulk_create(choice_bulk) + + content_bulk = [] + for content in contents: + content['module'] = module_instance + content_bulk.append(ContentBlock(**content)) + ContentBlock.objects.bulk_create(content_bulk) + + return instance diff --git a/physionet-django/training/templates/training/email/training_notification.html b/physionet-django/training/templates/training/email/training_notification.html new file mode 100644 index 0000000000..c01f26a47a --- /dev/null +++ b/physionet-django/training/templates/training/email/training_notification.html @@ -0,0 +1,9 @@ +{% load i18n %}{% autoescape off %}{% filter wordwrap:70 %} +Dear {{ name }}, + +Your training {{ training }} on {{ domain }} will be expiring in {{ expiry }} days. To retain the access it provides, kindly login to your account to retake it. + + +Regards +The {{ SITE_NAME }} Team +{% endfilter %}{% endautoescape %}