diff --git a/physionet-django/physionet/settings/base.py b/physionet-django/physionet/settings/base.py index 5a32fb7e51..70c17670e9 100644 --- a/physionet-django/physionet/settings/base.py +++ b/physionet-django/physionet/settings/base.py @@ -52,6 +52,7 @@ 'background_task', 'rest_framework', + 'training', 'user', 'project', 'console', @@ -370,7 +371,7 @@ ['NumberedList', 'BulletedList'], ['InlineEquation', 'BlockEquation', 'CodeSnippet', 'Table'], ['Link', 'Unlink'], - ['RemoveFormat', 'Source'], + ['RemoveFormat', 'Source'] ], 'removeDialogTabs': 'link:advanced', 'disableNativeSpellChecker': False, 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..881a801987 --- /dev/null +++ b/physionet-django/training/admin.py @@ -0,0 +1,30 @@ +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 + + +@admin.register(models.Quiz) +class QuizAdmin(admin.ModelAdmin): + list_display = ('training', 'question', 'order') + inlines = [QuizChoiceInline, ] + + +@admin.register(models.OnPlatformTraining) +class OnPlatformTrainingAdmin(admin.ModelAdmin): + list_display = ('training', 'version') + inlines = [ContentBlockInline, QuizInline] 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/fixtures/demo-training.json b/physionet-django/training/fixtures/demo-training.json new file mode 100644 index 0000000000..dd1b9eacc5 --- /dev/null +++ b/physionet-django/training/fixtures/demo-training.json @@ -0,0 +1,73 @@ +[ + { + "model": "training.onplatformtraining", + "pk": 1, + "fields": { + "training": 2, + "version": 1.0 + } + }, + { + "model": "training.contentblock", + "pk": 1, + "fields": { + "training": 1, + "body": "Content Body 1", + "order": 1 + } + }, + { + "model": "training.contentblock", + "pk": 2, + "fields": { + "training": 1, + "body": "Content Body 2", + "order": 3 + } + }, + { + "model": "training.quiz", + "pk": 1, + "fields": { + "training": 1, + "question": "Which of this is not a country in America?", + "order": 2 + } + }, + { + "model": "training.quizchoice", + "pk": 1, + "fields": { + "quiz": 1, + "body": "Canada", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 2, + "fields": { + "quiz": 1, + "body": "Spain", + "is_correct": true + } + }, + { + "model": "training.quizchoice", + "pk": 3, + "fields": { + "quiz": 1, + "body": "Cuba", + "is_correct": false + } + }, + { + "model": "training.quizchoice", + "pk": 4, + "fields": { + "quiz": 1, + "body": "Mexico", + "is_correct": false + } + } +] \ No newline at end of file diff --git a/physionet-django/training/migrations/0001_initial.py b/physionet-django/training/migrations/0001_initial.py new file mode 100644 index 0000000000..fb1b59c24b --- /dev/null +++ b/physionet-django/training/migrations/0001_initial.py @@ -0,0 +1,67 @@ +# Generated by Django 4.1.5 on 2023-02-28 14:29 + +from django.db import migrations, models +import django.db.models.deletion +import project.modelcomponents.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('user', '0052_alter_trainingtype_required_field'), + ] + + operations = [ + migrations.CreateModel( + name='OnPlatformTraining', + fields=[ + ('id', models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.FloatField(default=1.0, unique=True)), + ('training', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='op_trainings', to='user.trainingtype')), + ], + options={ + 'default_permissions': ('change',), + }, + ), + migrations.CreateModel( + name='Quiz', + fields=[ + ('id', models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('question', project.modelcomponents.fields.SafeHTMLField()), + ('order', models.PositiveIntegerField()), + ('training', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='quizzes', to='training.onplatformtraining')), + ], + ), + migrations.CreateModel( + name='QuizChoice', + fields=[ + ('id', models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('body', models.TextField()), + ('is_correct', models.BooleanField(default=False, verbose_name='Correct Choice?')), + ('quiz', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='choices', to='training.quiz')), + ], + ), + migrations.CreateModel( + name='ContentBlock', + fields=[ + ('id', models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('body', project.modelcomponents.fields.SafeHTMLField()), + ('order', models.PositiveIntegerField()), + ('training', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='contents', to='training.onplatformtraining')), + ], + ), + ] 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..02ca594ca5 --- /dev/null +++ b/physionet-django/training/models.py @@ -0,0 +1,33 @@ +from django.db import models + +from project.modelcomponents.fields import SafeHTMLField + + +class OnPlatformTraining(models.Model): + training = models.ForeignKey('user.TrainingType', + on_delete=models.CASCADE, related_name='op_trainings') + version = models.FloatField(default=1.0, unique=True) + + class Meta: + default_permissions = ('change',) + + +class Quiz(models.Model): + question = SafeHTMLField() + training = models.ForeignKey('training.OnPlatformTraining', + on_delete=models.CASCADE, related_name='quizzes') + order = models.PositiveIntegerField() + + +class ContentBlock(models.Model): + training = models.ForeignKey('training.OnPlatformTraining', + 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) diff --git a/physionet-django/training/views.py b/physionet-django/training/views.py new file mode 100644 index 0000000000..f4fb6055dc --- /dev/null +++ b/physionet-django/training/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# write your views here 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): diff --git a/physionet-django/user/fixtures/demo-training-type.json b/physionet-django/user/fixtures/demo-training-type.json index 35b34ca87d..a1be555f27 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": "On Platform Training", + "description": "

This training teaches about name of places.


Lorem ipsum dolor sit amet. Et excepturi amet Et autem aut quisquam galisum eos totam esse ut omnis eveniet. Et eius assumenda non tempore sequiqui laboriosam est corporis dolores. Eos consequatur fugiatAut iste ad corrupti nostrum est quos autem est optio quod et iure nemo ea temporibus magni. Est placeat dicta 33 laborum nulla33 veritatis non nihil quod sit quia dolor.

Id internos repellatSed culpa cum natus internos aut enim consectetur et architecto quibusdam eum ipsam fugit et quia animi. Eos obcaecati exercitationem Ut repellat aut nemo quia ut quia perferendis.

", + "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 e2688ac689..870213af02 100644 --- a/physionet-django/user/fixtures/demo-user.json +++ b/physionet-django/user/fixtures/demo-user.json @@ -14362,6 +14362,11 @@ "can_review_training", "user", "trainingtype" + ], + [ + "change_onplatformtraining", + "training", + "onplatformtraining" ] ] } diff --git a/physionet-django/user/migrations/0052_alter_trainingtype_required_field.py b/physionet-django/user/migrations/0052_alter_trainingtype_required_field.py new file mode 100644 index 0000000000..ae96c835b8 --- /dev/null +++ b/physionet-django/user/migrations/0052_alter_trainingtype_required_field.py @@ -0,0 +1,21 @@ +# Generated by Django 4.1.5 on 2023-02-18 02:12 + +from django.db import migrations, models +import user.enums + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0051_credentialapplication_auto_rejection_reason'), + ] + + operations = [ + migrations.AlterField( + model_name='trainingtype', + name='required_field', + field=models.PositiveSmallIntegerField( + choices=[(0, 'DOCUMENT'), (1, 'URL'), (2, 'PLATFORM')], + default=user.enums.RequiredField['DOCUMENT']), + ), + ]