From 2b39e31a319f9f20ddfab5fe505f8b88177929bf Mon Sep 17 00:00:00 2001 From: Amit Upreti Date: Tue, 28 Mar 2023 14:19:58 -0400 Subject: [PATCH 01/44] Create Template for Training APP --- physionet-django/physionet/settings/base.py | 1 + physionet-django/physionet/urls.py | 2 ++ physionet-django/training/__init__.py | 0 physionet-django/training/admin.py | 0 physionet-django/training/apps.py | 5 +++++ physionet-django/training/migrations/__init__.py | 0 physionet-django/training/models.py | 1 + physionet-django/training/urls.py | 3 +++ physionet-django/training/views.py | 0 physionet-django/user/enums.py | 1 + 10 files changed, 13 insertions(+) create mode 100644 physionet-django/training/__init__.py create mode 100644 physionet-django/training/admin.py create mode 100644 physionet-django/training/apps.py create mode 100644 physionet-django/training/migrations/__init__.py create mode 100644 physionet-django/training/models.py create mode 100644 physionet-django/training/urls.py create mode 100644 physionet-django/training/views.py diff --git a/physionet-django/physionet/settings/base.py b/physionet-django/physionet/settings/base.py index 1d68f307d5..c72f07fdf0 100644 --- a/physionet-django/physionet/settings/base.py +++ b/physionet-django/physionet/settings/base.py @@ -56,6 +56,7 @@ 'oauth2_provider', 'corsheaders', + 'training', 'user', 'project', 'console', diff --git a/physionet-django/physionet/urls.py b/physionet-django/physionet/urls.py index a08844797e..7b76aa0e1e 100644 --- a/physionet-django/physionet/urls.py +++ b/physionet-django/physionet/urls.py @@ -24,6 +24,8 @@ path('console/', include('console.urls')), # user app path('', include('user.urls')), + # training app + path('', include('training.urls')), # project app path('projects/', include('project.urls')), # events diff --git a/physionet-django/training/__init__.py b/physionet-django/training/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/physionet-django/training/admin.py b/physionet-django/training/admin.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/physionet-django/training/apps.py b/physionet-django/training/apps.py new file mode 100644 index 0000000000..a94e5537a6 --- /dev/null +++ b/physionet-django/training/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class TrainingConfig(AppConfig): + name = 'training' diff --git a/physionet-django/training/migrations/__init__.py b/physionet-django/training/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/physionet-django/training/models.py b/physionet-django/training/models.py new file mode 100644 index 0000000000..137941ffae --- /dev/null +++ b/physionet-django/training/models.py @@ -0,0 +1 @@ +from django.db import models diff --git a/physionet-django/training/urls.py b/physionet-django/training/urls.py new file mode 100644 index 0000000000..b7d63511ad --- /dev/null +++ b/physionet-django/training/urls.py @@ -0,0 +1,3 @@ +from django.urls import path +from training import views + diff --git a/physionet-django/training/views.py b/physionet-django/training/views.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/physionet-django/user/enums.py b/physionet-django/user/enums.py index b3d32278f2..004ebbc96e 100644 --- a/physionet-django/user/enums.py +++ b/physionet-django/user/enums.py @@ -17,6 +17,7 @@ def choices(cls): class RequiredField(IntEnum): DOCUMENT = 0 URL = 1 + PLATFORM = 2 @classmethod def choices(cls): From 743a13e9b5c97b5ae6d5bb840fb8e80a3e3d06d7 Mon Sep 17 00:00:00 2001 From: Amit Upreti Date: Tue, 28 Mar 2023 14:42:32 -0400 Subject: [PATCH 02/44] Create Model for Course,tracking user progress 1. Course The idea is that a Training Course will be defined with user.TrainingType. This is the ultimate(top) model for a training course. The Course content for TrainingType model is implemented by training app. On Training app, `Course` model can be created for each TrainingType. For different versions of the same training course, we can create as many `Course` models as we want as long as the version is different. A Course is divided into modules. Each module has a description and a list of contents and quizzes. Modules are like chapters in a book. Each module has a list of contents and quizzes. Contents are like paragraphs in a chapter. Quizzes are like questions in a chapter. for ordering the modules, contents and quizzes, we have used `order` field. This field is used to order the modules, contents and quizzes The ordering is unique for each instance of the parent model. It is expected that the order will start from 1 and will be incremented by 1 with no gaps. 2. Tracking User Progress during training When a user starts a training, a `CourseProgress` model should be created for that user and the version of course. This model tracks the progress of the user during the course. Similarly, when a user starts a module, a ModuleProgress model should be created for each module in the training. For quiz, content progress, we should create a instance of CompletedContent or CompletedQuiz model when the user completes a content or quiz. I dont think we need to track when someone started a content or quiz as they are expected to complete in few minutes. --- physionet-django/training/admin.py | 42 +++++++++ physionet-django/training/models.py | 140 ++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+) diff --git a/physionet-django/training/admin.py b/physionet-django/training/admin.py index e69de29bb2..0d2876f456 100644 --- a/physionet-django/training/admin.py +++ b/physionet-django/training/admin.py @@ -0,0 +1,42 @@ +from django.contrib import admin +from training import models + + +class ContentBlockInline(admin.StackedInline): + model = models.ContentBlock + extra = 1 + + +class QuizChoiceInline(admin.TabularInline): + model = models.QuizChoice + extra = 1 + + +class QuizInline(admin.StackedInline): + model = models.Quiz + inlines = [QuizChoiceInline, ] + extra = 1 + + +class ModuleInline(admin.StackedInline): + model = models.Module + inlines = [ContentBlockInline, QuizInline] + extra = 1 + + +@admin.register(models.Quiz) +class QuizAdmin(admin.ModelAdmin): + list_display = ('module', 'question', 'order') + inlines = [QuizChoiceInline, ] + + +@admin.register(models.Module) +class ModuleAdmin(admin.ModelAdmin): + list_display = ('name', 'course', 'order') + inlines = [ContentBlockInline, QuizInline] + + +@admin.register(models.Course) +class CourseAdmin(admin.ModelAdmin): + list_display = ('training_type', 'version') + inlines = [ModuleInline] diff --git a/physionet-django/training/models.py b/physionet-django/training/models.py index 137941ffae..c5a7f9b3c1 100644 --- a/physionet-django/training/models.py +++ b/physionet-django/training/models.py @@ -1 +1,141 @@ from django.db import models + +from project.modelcomponents.fields import SafeHTMLField + + +class Course(models.Model): + training_type = models.ForeignKey('user.TrainingType', + on_delete=models.CASCADE, related_name='courses') + version = models.FloatField(default=1.0) + + class Meta: + default_permissions = ('change',) + unique_together = ('training_type', 'version') + + def __str__(self): + return f'{self.training_type} v{self.version}' + + +class Module(models.Model): + name = models.CharField(max_length=100) + course = models.ForeignKey('training.Course', on_delete=models.CASCADE, related_name='modules') + order = models.PositiveIntegerField() + description = SafeHTMLField() + + class Meta: + unique_together = ('course', 'order') + + def __str__(self): + return self.name + + +class Quiz(models.Model): + question = SafeHTMLField() + module = models.ForeignKey('training.Module', + on_delete=models.CASCADE, related_name='quizzes') + order = models.PositiveIntegerField() + + +class ContentBlock(models.Model): + module = models.ForeignKey('training.Module', + on_delete=models.CASCADE, related_name='contents') + body = SafeHTMLField() + order = models.PositiveIntegerField() + + +class QuizChoice(models.Model): + quiz = models.ForeignKey('training.Quiz', + on_delete=models.CASCADE, related_name='choices') + body = models.TextField() + is_correct = models.BooleanField('Correct Choice?', default=False) + + +class CourseProgress(models.Model): + class Status(models.TextChoices): + IN_PROGRESS = 'IP', 'In Progress' + COMPLETED = 'C', 'Completed' + + user = models.ForeignKey('user.User', on_delete=models.CASCADE) + course = models.ForeignKey('training.Course', on_delete=models.CASCADE) + status = models.CharField(max_length=2, choices=Status.choices, default=Status.IN_PROGRESS) + started_at = models.DateTimeField(auto_now_add=True) + completed_at = models.DateTimeField(auto_now=True) + + class Meta: + unique_together = ('user', 'course') + + def __str__(self): + return f'{self.user.username} - {self.course}' + + def get_next_module(self): + if self.status == self.Status.COMPLETED: + return None + + next_module = self.module_progresses.filter(status=self.module_progresses.model.Status.IN_PROGRESS).first() + if next_module: + return next_module.module + + last_module = self.module_progresses.filter( + status=self.module_progresses.model.Status.COMPLETED).order_by('-last_completed_order').first() + if last_module: + return self.course.modules.filter(order__gt=last_module.module.order).order_by('order').first() + + return self.course.modules.first() + + +class ModuleProgress(models.Model): + class Status(models.TextChoices): + IN_PROGRESS = 'IP', 'In Progress' + COMPLETED = 'C', 'Completed' + + course_progress = models.ForeignKey('training.CourseProgress', on_delete=models.CASCADE, + related_name='module_progresses') + module = models.ForeignKey('training.Module', on_delete=models.CASCADE) + status = models.CharField(max_length=2, choices=Status.choices, default=Status.IN_PROGRESS) + last_completed_order = models.PositiveIntegerField(null=True, default=0) + started_at = models.DateTimeField(null=True, blank=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f'{self.course_progress.user.username} - {self.module}' + + def get_next_content_or_quiz(self): + if self.status == self.Status.COMPLETED: + return None + + next_content = self.module.contents.filter(order__gt=self.last_completed_order).order_by('order').first() + next_quiz = self.module.quizzes.filter(order__gt=self.last_completed_order).order_by('order').first() + + if next_content and next_quiz: + return next_content if next_content.order < next_quiz.order else next_quiz + elif next_content: + return next_content + elif next_quiz: + return next_quiz + else: + return None + + def update_last_completed_order(self, completed_content_or_quiz): + if completed_content_or_quiz.order > self.last_completed_order: + self.last_completed_order = completed_content_or_quiz.order + self.save() + + +class CompletedContent(models.Model): + module_progress = models.ForeignKey('training.ModuleProgress', on_delete=models.CASCADE, + related_name='completed_contents') + content = models.ForeignKey('training.ContentBlock', on_delete=models.CASCADE) + completed_at = models.DateTimeField(null=True, blank=True) + + def __str__(self): + return f'{self.module_progress.course_progress.user.username} - {self.content}' + + +class CompletedQuiz(models.Model): + module_progress = models.ForeignKey('training.ModuleProgress', on_delete=models.CASCADE, + related_name='completed_quizzes') + quiz = models.ForeignKey('training.Quiz', on_delete=models.CASCADE) + completed_at = models.DateTimeField(null=True, blank=True) + + def __str__(self): + return f'{self.module_progress.course_progress.user.username} - {self.quiz}' From 99bfa07f506a0b6f8dcf2aab9a17c324e2c2d01b Mon Sep 17 00:00:00 2001 From: Amit Upreti Date: Tue, 28 Mar 2023 14:49:28 -0400 Subject: [PATCH 03/44] Create Courses with json Added a serializer which will be used to allow admins to create a new training or update the existing training. To track the updates to training(manage versions), we are using semantic versioning. If the updated course has a major update eg: from 1.9 to 2.0, then all the users who did complete the previous versions will be sent an email asking them to complete the new version of Course within x days. --- physionet-django/notification/utility.py | 16 ++ physionet-django/training/serializers.py | 167 ++++++++++++++++++ .../email/training_expiry_notification.html | 9 + 3 files changed, 192 insertions(+) create mode 100644 physionet-django/training/serializers.py create mode 100644 physionet-django/training/templates/training/email/training_expiry_notification.html diff --git a/physionet-django/notification/utility.py b/physionet-django/notification/utility.py index 83697e2603..b69f40c2b9 100644 --- a/physionet-django/notification/utility.py +++ b/physionet-django/notification/utility.py @@ -1025,6 +1025,22 @@ def notify_event_participant_application(request, user, registered_user, event): 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 Expiry Notification" + context = { + 'name': user.get_full_name(), + 'SITE_NAME': settings.SITE_NAME, + 'training': training, + 'expiry': expiry + } + body = loader.render_to_string('training/email/training_expiry_notification.html', context) + send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [user, ], fail_silently=False) + + def notify_primary_email(associated_email): """ Inform a user of the primary email address linked to their account. diff --git a/physionet-django/training/serializers.py b/physionet-django/training/serializers.py new file mode 100644 index 0000000000..3169be745a --- /dev/null +++ b/physionet-django/training/serializers.py @@ -0,0 +1,167 @@ +import datetime +from rest_framework import serializers +from django.db import transaction +from django.utils import timezone + +from training.models import Course, 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', 'course'] + + +class CourseSerializer(serializers.ModelSerializer): + modules = ModuleSerializer(many=True) + + class Meta: + model = Course + fields = "__all__" + read_only_fields = ['id', 'training_type'] + + +class TrainingTypeSerializer(serializers.ModelSerializer): + courses = CourseSerializer() + + class Meta: + model = TrainingType + fields = "__all__" + read_only_fields = ['id'] + + def update_course_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(): + course = validated_data.pop('courses') + modules = course.pop('modules') + + course['training_type'] = instance + + course_instance = Course.objects.create(**course) + + for module in modules: + quizzes = module.pop('quizzes') + contents = module.pop('contents') + + module['course'] = course_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) + + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.save() + if course.get("version"): + if str(course.get("version")).endswith("0"): + self.update_course_for_major_version_change(instance) + + return course_instance + + def create(self, validated_data): + + with transaction.atomic(): + course = validated_data.pop('courses') + modules = course.pop('modules') + + course['training_type'] = instance = TrainingType.objects.create(**validated_data) + + course_instance = Course.objects.create(**course) + + for module in modules: + quizzes = module.pop('quizzes') + contents = module.pop('contents') + + module['course'] = course_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_expiry_notification.html b/physionet-django/training/templates/training/email/training_expiry_notification.html new file mode 100644 index 0000000000..c01f26a47a --- /dev/null +++ b/physionet-django/training/templates/training/email/training_expiry_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 %} From 674278b3696d2f48e709eb40956e307aeecbe90d Mon Sep 17 00:00:00 2001 From: Amit Upreti Date: Tue, 28 Mar 2023 14:50:42 -0400 Subject: [PATCH 04/44] Course training with json examples and fixtures update 1. Added example json files to create and update a Course training. 2. updated the fixtures to create permissions to create the Course training and an demo Course training. --- .../static/sample/create-course-schema.json | 32 ++ .../static/sample/example-course-create.json | 47 ++ .../static/sample/example-course-update.json | 47 ++ .../training/fixtures/demo-training.json | 435 ++++++++++++++++++ .../user/fixtures/demo-training-type.json | 18 + physionet-django/user/fixtures/demo-user.json | 15 + 6 files changed, 594 insertions(+) create mode 100644 physionet-django/static/sample/create-course-schema.json create mode 100644 physionet-django/static/sample/example-course-create.json create mode 100644 physionet-django/static/sample/example-course-update.json create mode 100644 physionet-django/training/fixtures/demo-training.json diff --git a/physionet-django/static/sample/create-course-schema.json b/physionet-django/static/sample/create-course-schema.json new file mode 100644 index 0000000000..740bf5fe0d --- /dev/null +++ b/physionet-django/static/sample/create-course-schema.json @@ -0,0 +1,32 @@ +{ + "name": "string", + "description": "string", + "valid_duration": "string", + "required_field": "integer", + "home_page": "string", + "courses": { + "version": "float", + "modules": [ + { + "contents": [ + { + "body": "string", + "order": "integer" + } + ], + "quizzes": [ + { + "question": "string", + "order": "integer", + "choices": [ + { + "body": "string", + "is_correct": "boolean" + } + ] + } + ] + } + ] + } +} diff --git a/physionet-django/static/sample/example-course-create.json b/physionet-django/static/sample/example-course-create.json new file mode 100644 index 0000000000..9f63e596c8 --- /dev/null +++ b/physionet-django/static/sample/example-course-create.json @@ -0,0 +1,47 @@ +{ + "name": "Course 1", + "description": "

Test content description", + "valid_duration": "1095 00:00:00", + "required_field": 2, + "home_page": "http://localhost:8000/training/", + "courses": { + "version": "1.0", + "modules": [ + { + "name": "Module 1", + "description": "

Module description", + "order": 0, + "contents": [ + { + "body": "

Hello This is a test

Test content1

", + "order": 0 + } + ], + "quizzes": [ + { + "question": "What is the correct answer(choice1)?", + "order": 1, + "choices": [ + { + "body": "I am a choice1", + "is_correct": true + }, + { + "body": "I am a choice2", + "is_correct": false + }, + { + "body": "I am a choice3", + "is_correct": false + }, + { + "body": "I am a choice4", + "is_correct": false + } + ] + } + ] + } + ] + } +} diff --git a/physionet-django/static/sample/example-course-update.json b/physionet-django/static/sample/example-course-update.json new file mode 100644 index 0000000000..eb1fa159d4 --- /dev/null +++ b/physionet-django/static/sample/example-course-update.json @@ -0,0 +1,47 @@ +{ + "name":"Course 1 Updated", + "description":"

Test content description Updated", + "valid_duration": "1095 00:00:00", + "required_field": 2, + "home_page": "http://localhost:8000/training/", + "courses": { + "version": "1.1", + "modules": [ + { + "name": "Module 1 Updated", + "description": "

Test content description Updated", + "order": 0, + "contents": [ + { + "body": "

Hello This is a test

Test content1 updated

", + "order": 0 + } + ], + "quizzes": [ + { + "question": "What is the correct answer(choice2)?", + "order": 1, + "choices": [ + { + "body": "I am a choice1", + "is_correct": false + }, + { + "body": "I am a choice2", + "is_correct": true + }, + { + "body": "I am a choice3", + "is_correct": false + }, + { + "body": "I am a choice4", + "is_correct": false + } + ] + } + ] + } + ] + } +} diff --git a/physionet-django/training/fixtures/demo-training.json b/physionet-django/training/fixtures/demo-training.json new file mode 100644 index 0000000000..85233b4e10 --- /dev/null +++ b/physionet-django/training/fixtures/demo-training.json @@ -0,0 +1,435 @@ +[ + { + "model": "training.course", + "pk": 1, + "fields": { + "training_type": 2, + "version": 1.0 + } + }, + { + "model": "training.module", + "pk": 1, + "fields": { + "name": "The Americas", + "description": "Learn about the Americas, including North and South America, Asia and Africa.", + "course": 1, + "order": 1 + } + }, + { + "model": "training.contentblock", + "pk": 1, + "fields": { + "module": 1, + "body": "

The Americas are comprised of two continents: North America and South America. These continents are connected by a narrow strip of land called the Isthmus of Panama. Together, North and South America span from the Arctic Circle in the north to Cape Horn in the south.

\n\n

North America is home to three large countries: Canada, the United States, and Mexico. It also includes several smaller countries in Central America and the Caribbean. The continent is known for its diverse landscapes, ranging from frozen tundras in the north to tropical rainforests in the south.

\n\n

South America is made up of twelve countries, including Brazil, Argentina, and Colombia. It is known for its stunning natural beauty, including the Amazon rainforest and the Andes mountain range. The continent also has a rich cultural heritage, with vibrant cities and ancient ruins.

", + "order": 1 + } + }, + { + "model": "training.contentblock", + "pk": 2, + "fields": { + "module": 1, + "body": "

(Copied content)The Americas are comprised of two continents: North America and South America. These continents are connected by a narrow strip of land called the Isthmus of Panama. Together, North and South America span from the Arctic Circle in the north to Cape Horn in the south.

\n\n

North America is home to three large countries: Canada, the United States, and Mexico. It also includes several smaller countries in Central America and the Caribbean. The continent is known for its diverse landscapes, ranging from frozen tundras in the north to tropical rainforests in the south.

\n\n

South America is made up of twelve countries, including Brazil, Argentina, and Colombia. It is known for its stunning natural beauty, including the Amazon rainforest and the Andes mountain range. The continent also has a rich cultural heritage, with vibrant cities and ancient ruins.

", + "order": 2 + } + }, + { + "model": "training.quiz", + "pk": 1, + "fields": { + "module": 1, + "question": "Which two continents make up the Americas?(Answer : North America and South America)", + "order": 3 + } + }, + { + "model": "training.quizchoice", + "pk": 1, + "fields": { + "quiz": 1, + "body": "Europe and Asia", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 2, + "fields": { + "quiz": 1, + "body": "Africa and Australia", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 3, + "fields": { + "quiz": 1, + "body": "North America and South America", + "is_correct": true + } + }, + { + "model": "training.quizchoice", + "pk": 4, + "fields": { + "quiz": 1, + "body": "Antarctica and South America", + "is_correct": false + } + }, + { + "model": "training.quiz", + "pk": 2, + "fields": { + "module": 1, + "question": "What connects North America and South America?(Answer : The Isthmus of Panama)", + "order": 4 + } + }, + { + "model": "training.quizchoice", + "pk": 5, + "fields": { + "quiz": 2, + "body": "The Isthmus of Panama", + "is_correct": true + } + }, + { + "model": "training.quizchoice", + "pk": 6, + "fields": { + "quiz": 2, + "body": "The Strait of Gibraltar", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 7, + "fields": { + "quiz": 2, + "body": "The Suez Canal", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 8, + "fields": { + "quiz": 2, + "body": "The Bering Strait", + "is_correct": false + } + }, + { + "model": "training.quiz", + "pk": 3, + "fields": { + "module": 1, + "question": "Which is NOT a large country in North America?(Answer : Cuba)", + "order": 5 + } + }, + { + "model": "training.quizchoice", + "pk": 9, + "fields": { + "quiz": 3, + "body": "Canada", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 10, + "fields": { + "quiz": 3, + "body": "United States", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 11, + "fields": { + "quiz": 3, + "body": "Mexico", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 12, + "fields": { + "quiz": 3, + "body": "Cuba", + "is_correct": true + } + }, + { + "model": "training.contentblock", + "pk": 3, + "fields": { + "module": 1, + "body": "

The Americas, also known as America, are lands of the Western Hemisphere composed of numerous entities and regions variably defined by geography, politics, and culture1. The Americas are recognized in the English-speaking world to include two separate continents: North America and South America1.

\n\n

The Americas have more than 1.014 billion inhabitants and boast an area of over 16.43 million square miles2. The Americas comprise 35 countries, including some of the world’s largest countries as well as several dependent territories2.

\n", + "order": 6 + } + }, + { + "model": "training.quiz", + "pk": 4, + "fields": { + "module": 1, + "question": "Which of the following is NOT a country in the Americas? (Answer : India)", + "order": 7 + } + }, + { + "model": "training.quizchoice", + "pk": 13, + "fields": { + "quiz": 4, + "body": "Canada", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 14, + "fields": { + "quiz": 4, + "body": "Mexico", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 15, + "fields": { + "quiz": 4, + "body": "Brazil", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 16, + "fields": { + "quiz": 4, + "body": "India", + "is_correct": true + } + }, + { + "model": "training.contentblock", + "pk": 4, + "fields": { + "module": 1, + "body": "

The Americas are a group of countries in the Western Hemisphere. They are also known as America.

\n\n

There are two continents in the Americas: North America and South America. They are connected by the Isthmus of Panama.

", + "order": 8 + } + }, + { + "model": "training.contentblock", + "pk": 5, + "fields": { + "module": 1, + "body": "

North America is home to three large countries: Canada, the United States, and Mexico. It also includes several smaller countries in Central America and the Caribbean. The continent is known for its diverse landscapes, ranging from frozen tundras in the north to tropical rainforests in the south.

", + "order": 9 + } + }, + { + "model": "training.contentblock", + "pk": 6, + "fields": { + "module": 1, + "body": "

South America is made up of twelve countries, including Brazil, Argentina, and Colombia. It is known for its stunning natural beauty, including the Amazon rainforest and the Andes mountain range. The continent also has a rich cultural heritage, with vibrant cities and ancient ruins.

", + "order": 10 + } + }, + { + "model": "training.contentblock", + "pk": 7, + "fields": { + "module": 1, + "body": "

As our course on World 101: Introduction to Continents and Countries comes to an end, I would like to take this opportunity to thank each and every one of you for your participation and engagement throughout the course.

\n\n

It has been a pleasure to share this journey with you and I hope that the knowledge and insights gained during our time together will serve you well in your future endeavors.

\n\n

Thank you for making this course a success. I wish you all the best in your future studies and pursuits.

\n", + "order": 11 + } + }, + { + "model": "training.module", + "pk": 2, + "fields": { + "course": 1, + "name": "Wondering about Europe?", + "description": "Europe is a continent located in the Northern Hemisphere. It is bordered by the Arctic Ocean to the north, the Atlantic Ocean to the west, and the Mediterranean Sea to the south. It is also connected to Asia by the Ural Mountains and the Caspian Sea.", + "order": 2 + } + }, + { + "model": "training.contentblock", + "pk": 8, + "fields": { + "module": 2, + "body": "

Europe is a continent located in the Northern Hemisphere. It is bordered by the Arctic Ocean to the north, the Atlantic Ocean to the west, and the Mediterranean Sea to the south. It is also connected to Asia by the Ural Mountains and the Caspian Sea.

\n\n

Europe is home to a diverse range of cultures, landscapes, and languages. It is also the second-smallest continent in the world, with a total area of 3.93 million square miles (10.2 million square kilometers).

\n", + "order": 1 + } + }, + { + "model": "training.contentblock", + "pk": 9, + "fields": { + "module": 2, + "body": "

Europe is made up of 50 countries. The largest by area is Russia, while the smallest is Monaco. The continent is also home to many important cities, including London, Paris, and Istanbul.

\n", + "order": 2 + } + }, + { + "model": "training.contentblock", + "pk": 10, + "fields": { + "module": 2, + "body": "

Europe is the second most populous continent in the world, with a population of 741 million. It is also home to many of the world’s most well-known historical sites, including Stonehenge, the Parthenon, and the Colosseum.

\n", + "order": 3 + } + }, + { + "model": "training.contentblock", + "pk": 11, + "fields": { + "module": 2, + "body": "

Europe is also known for its diverse landscapes. It is home to the highest mountain in the world, Mount Everest, as well as the lowest point on land, the Dead Sea. It is also home to the world’s largest ice sheet, the Greenland ice sheet.

\n", + "order": 4 + } + }, + { + "model": "training.contentblock", + "pk": 12, + "fields": { + "module": 2, + "body": "

The continent is also home to some of the world’s largest lakes, including Lake Superior and Lake Baikal. It is also home to the world’s largest river, the Amazon.

\n", + "order": 5 + } + }, + { + "model": "training.contentblock", + "pk": 13, + "fields": { + "module": 2, + "body": "

Europe is also home to many of the world’s most well-known cities. London, Paris, and Rome are some of the most famous cities in the world. They are also home to many of the world’s most famous landmarks, including the Eiffel Tower, the Colosseum, and the Parthenon.

\n", + "order": 6 + } + }, + { + "model": "training.contentblock", + "pk": 14, + "fields": { + "module": 2, + "body": "

Europe is also home to many of the world’s most famous museums. The Louvre in Paris is one of the most famous museums in the world. It is also home to many of the world’s most famous works of art, including the Mona Lisa and the Venus de Milo.

\n", + "order": 7 + } + }, + { + "model": "training.contentblock", + "pk": 15, + "fields": { + "module": 2, + "body": "

Europe is also home to many of the world’s most famous landmarks. The Eiffel Tower in Paris is one of the most famous landmarks in the world. It is also home to many of the world’s most famous works of art, including the Mona Lisa and the Venus de Milo.

\n", + "order": 8 + } + }, + { + "model": "training.quiz", + "pk": 5, + "fields": { + "module": 2, + "question": "Which of the following countries is NOT located in Europe? (Answer: Nepal)", + "order": 9 + } + }, + { + "model": "training.quizchoice", + "pk": 17, + "fields": { + "quiz": 5, + "body": "France", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 18, + "fields": { + "quiz": 5, + "body": "Nepal", + "is_correct": true + } + }, + { + "model": "training.quizchoice", + "pk": 19, + "fields": { + "quiz": 5, + "body": "Spain", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 20, + "fields": { + "quiz": 5, + "body": "Germany", + "is_correct": false + } + }, + { + "model": "training.quiz", + "pk": 6, + "fields": { + "module": 2, + "question": "Which of the following countries is NOT located in Africa?(Answer: Nepal)", + "order": 10 + } + }, + { + "model": "training.quizchoice", + "pk": 21, + "fields": { + "quiz": 6, + "body": "Egypt", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 22, + "fields": { + "quiz": 6, + "body": "Nepal", + "is_correct": true + } + }, + { + "model": "training.quizchoice", + "pk": 23, + "fields": { + "quiz": 6, + "body": "Zimbabwe", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 24, + "fields": { + "quiz": 6, + "body": "Cameroon", + "is_correct": false + } + } +] diff --git a/physionet-django/user/fixtures/demo-training-type.json b/physionet-django/user/fixtures/demo-training-type.json index 35b34ca87d..f91d28e02f 100644 --- a/physionet-django/user/fixtures/demo-training-type.json +++ b/physionet-django/user/fixtures/demo-training-type.json @@ -17,6 +17,24 @@ ] } }, + { + "model": "user.trainingtype", + "pk": 2, + "fields": { + "name": "World 101: Introduction to Continents and Countries", + "description": "

Join us for an exciting journey around the globe as we explore the continents and countries that make up our world. In this training, you’ll learn about the geography, culture, and history of different regions and gain a deeper understanding of our interconnected world.

\n\n

What You Will Learn:

\n\n
    \n\t
  • The names and locations of the seven continents
  • \n\t
  • Key countries and their capitals on each continent
  • \n\t
  • Basic geographical features and landmarks
  • \n\t
  • Cultural and historical highlights of different regions
  • \n
\n\n

Prerequisites:

\n\n
    \n\t
  • No prior knowledge is required
  • \n\t
  • An interest in geography and world cultures is recommended
  • \n
\n\n

Don’t miss this opportunity to expand your horizons and discover the fascinating world we live in. Our experienced instructors will guide you through this engaging training, providing insights and knowledge along the way. Sign up now to reserve your spot!

\n\n

Contact: For more information or to register for this training, please contact us at training@discoveringtheworld.com or call us at 555-1234.

\n", + "home_page": "/", + "valid_duration": "1095 00:00:00", + "required_field": 2, + "questions": [ + 1, + 2, + 3, + 4, + 5 + ] + } + }, { "model": "user.question", "pk": 1, diff --git a/physionet-django/user/fixtures/demo-user.json b/physionet-django/user/fixtures/demo-user.json index 39070d6642..fa1eb2cb39 100644 --- a/physionet-django/user/fixtures/demo-user.json +++ b/physionet-django/user/fixtures/demo-user.json @@ -14458,6 +14458,16 @@ "view_redirect", "redirects", "redirect" + ], + [ + "change_course", + "training", + "course" + ], + [ + "can_view_course_guidelines", + "training", + "course" ] ] } @@ -14570,6 +14580,11 @@ "can_view_admin_console", "user", "user" + ], + [ + "can_view_course_guidelines", + "training", + "course" ] ] } From 647a1e99602a351287b2f3bd0cf14b77adae5a8b Mon Sep 17 00:00:00 2001 From: Amit Upreti Date: Tue, 28 Mar 2023 15:07:25 -0400 Subject: [PATCH 05/44] Manage courses from admin console allows admins to manage(create, update or download an existing course) courses from the admin console We also added a Course Guidelines under `Guidelines` in the admin console which explains the steps to create a course --- .../templates/console/console_navbar.html | 74 ++++---- .../templates/console/guidelines_course.html | 161 ++++++++++++++++++ .../console/training_type/index.html | 71 ++++++++ physionet-django/console/urls.py | 7 + physionet-django/console/views.py | 20 ++- .../static/sample/create-course-schema.json | 54 +++--- .../static/sample/example-course-create.json | 84 ++++----- .../static/sample/example-course-update.json | 88 +++++----- physionet-django/training/models.py | 3 + physionet-django/training/serializers.py | 20 ++- physionet-django/training/urls.py | 4 + physionet-django/training/views.py | 71 ++++++++ 12 files changed, 495 insertions(+), 162 deletions(-) create mode 100644 physionet-django/console/templates/console/guidelines_course.html create mode 100644 physionet-django/console/templates/console/training_type/index.html diff --git a/physionet-django/console/templates/console/console_navbar.html b/physionet-django/console/templates/console/console_navbar.html index 4094e97e4f..c987620b2c 100644 --- a/physionet-django/console/templates/console/console_navbar.html +++ b/physionet-django/console/templates/console/console_navbar.html @@ -13,42 +13,37 @@