-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
create onplatform training with json
Added a serializer to allow admins to create a new training or update the existing training. To track the updates to training, we are using semantic versioning. If the updated training has a major update eg from 1.9 to 2.0, then all the users who did completed the previous versions will be sent an email asking them to complete the new version of training with x days.
- Loading branch information
1 parent
1a6b5bc
commit afdb6b2
Showing
9 changed files
with
359 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
74 changes: 74 additions & 0 deletions
74
physionet-django/console/templates/console/training_type/index.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
{% extends "console/base_console.html" %} | ||
|
||
{% load static %} | ||
|
||
{% block title %}Training Types{% endblock %} | ||
|
||
{% block content %} | ||
<div class="card mb-3"> | ||
<div class="card-header"> | ||
Training Types <span class="badge badge-pill badge-info">{{ training_types|length }}</span> | ||
</div> | ||
<div class="card-body"> | ||
<div><button type="button" class="btn btn-sm btn-danger" data-toggle="modal" data-target="#op-training">Create/Update On Platform Training</button></div> | ||
<div class="table-responsive"> | ||
<table class="table table-bordered"> | ||
<thead> | ||
<tr> | ||
<th>Name</th> | ||
<th>Valid Duration</th> | ||
<th>On Platform Training</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{% for training in training_types %} | ||
<tr> | ||
<td>{{ training.name|title }}</td> | ||
<td>{{ training.valid_duration }}</td> | ||
<td>{% if training.required_field == 2 %} True {% else %} False {% endif %}</td> | ||
</tr> | ||
{% endfor %} | ||
</tbody> | ||
</table> | ||
</div> | ||
|
||
<div class="modal fade" id="op-training" tabindex="-1"> | ||
<div class="modal-dialog"> | ||
<form action="{% url 'op_training' %}" method="POST" enctype="multipart/form-data" class=""> | ||
<div class="modal-content"> | ||
<div class="modal-body"> | ||
<p>Download Sample Json File by <a href="{% static 'sample/create.json' %}" download>Clicking Here</a></p> | ||
<div> | ||
{% csrf_token %} | ||
<div class="form-group"> | ||
<label>Training Type: </label> | ||
<select name="training_id" class="form-control" required> | ||
<option disabled>Choose...</option> | ||
<option value="-1">Create new training</option> | ||
{% for training in training_types %} | ||
<option value="{{ training.id }}">{{ training.name|title }}</option> | ||
{% endfor %} | ||
</select> | ||
</div> | ||
<div class="form-group"> | ||
<label>File: </label> | ||
<input type="file" name="json_file" id="json_file" required="True" class="form-control"> | ||
</div> | ||
{# <div class="form-group">#} | ||
{# <input type="submit" name="create" class="btn btn-primary">#} | ||
{# </div>#} | ||
|
||
</div> | ||
</div> | ||
<div class="modal-footer"> | ||
<input type="submit" name="create" class="btn btn-primary"> | ||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> | ||
</div> | ||
</div> | ||
</form> | ||
</div> | ||
</div> | ||
|
||
</div> | ||
</div> | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
{ | ||
"op_trainings": { | ||
"version": "float", | ||
"contents": [ | ||
{ | ||
"body": "string", | ||
"order": "integer" | ||
} | ||
], | ||
"quizzes": [ | ||
{ | ||
"question": "string", | ||
"order": "string", | ||
"choices": [ | ||
{ | ||
"body": "string", | ||
"is_correct": "boolean" | ||
} | ||
] | ||
} | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import datetime | ||
from rest_framework import serializers | ||
from django.db import transaction | ||
from django.utils import timezone | ||
|
||
from training.models import OnPlatformTraining, 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', 'training'] | ||
|
||
|
||
class ContentBlockSerializer(serializers.ModelSerializer): | ||
|
||
class Meta: | ||
model = ContentBlock | ||
fields = "__all__" | ||
read_only_fields = ['id', 'training'] | ||
|
||
|
||
class OnPlatformTrainingSerializer(serializers.ModelSerializer): | ||
quizzes = QuizSerializer(many=True) | ||
contents = ContentBlockSerializer(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') | ||
quizzes = op_training.pop('quizzes') | ||
contents = op_training.pop('contents') | ||
|
||
op_training['training_type'] = instance | ||
|
||
op_training_instance = OnPlatformTraining.objects.create(**op_training) | ||
|
||
quiz_bulk = [] | ||
choice_bulk = [] | ||
for quiz in quizzes: | ||
choices = quiz.pop('choices') | ||
|
||
quiz['training'] = op_training_instance | ||
q = Quiz(**quiz) | ||
q.save() | ||
quiz_bulk.append(q) | ||
|
||
for choice in choices: | ||
choice['quiz'] = q | ||
choice_bulk.append(QuizChoice(**choice)) | ||
|
||
# Quiz.objects.bulk_create(quiz_bulk) | ||
QuizChoice.objects.bulk_create(choice_bulk) | ||
|
||
content_bulk = [] | ||
for content in contents: | ||
content['training'] = op_training_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 op_training.get("version"): | ||
if str(op_training.get("version")).endswith("0"): | ||
self.update_training_for_major_version_change(instance) | ||
|
||
return instance | ||
|
||
def create(self, validated_data): | ||
|
||
with transaction.atomic(): | ||
op_training = validated_data.pop('op_trainings') | ||
quizzes = op_training.pop('quizzes') | ||
contents = op_training.pop('contents') | ||
|
||
op_training['training_type'] = instance = TrainingType.objects.create(**validated_data) | ||
|
||
op_training_instance = OnPlatformTraining.objects.create(**op_training) | ||
|
||
quiz_bulk = [] | ||
choice_bulk = [] | ||
for quiz in quizzes: | ||
choices = quiz.pop('choices') | ||
|
||
quiz['training'] = op_training_instance | ||
q = Quiz(**quiz) | ||
q.save() | ||
quiz_bulk.append(q) | ||
|
||
for choice in choices: | ||
choice['quiz'] = q | ||
choice_bulk.append(QuizChoice(**choice)) | ||
|
||
# Quiz.objects.bulk_create(quiz_bulk) | ||
QuizChoice.objects.bulk_create(choice_bulk) | ||
|
||
content_bulk = [] | ||
for content in contents: | ||
content['training'] = op_training_instance | ||
content_bulk.append(ContentBlock(**content)) | ||
ContentBlock.objects.bulk_create(content_bulk) | ||
|
||
return instance |
9 changes: 9 additions & 0 deletions
9
physionet-django/training/templates/training/email/training_notification.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
from django.urls import path | ||
from training import views | ||
|
||
|
||
urlpatterns = [ | ||
path('training/', views.on_platform_training, name='op_training'), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,53 @@ | ||
from django.shortcuts import render | ||
import json | ||
import operator | ||
from itertools import chain | ||
|
||
# write your views here | ||
from django.contrib import messages | ||
from django.contrib.auth.decorators import login_required, permission_required | ||
from django.db.models import Prefetch | ||
from django.shortcuts import get_object_or_404, redirect, render | ||
from django.utils import timezone | ||
from django.utils.crypto import get_random_string | ||
|
||
from rest_framework.parsers import JSONParser | ||
|
||
from user.models import Training, TrainingType, TrainingQuestion, RequiredField | ||
from user.enums import TrainingStatus | ||
|
||
from training.models import OnPlatformTraining, QuizChoice, ContentBlock | ||
from training.serializers import TrainingTypeSerializer | ||
|
||
|
||
@permission_required('physionet.change_onplatformtraining', raise_exception=True) | ||
def on_platform_training(request): | ||
|
||
if request.POST: | ||
|
||
if request.POST.get('training_id') != "-1": | ||
training_type = get_object_or_404(TrainingType, pk=request.POST.get('training_id')) | ||
else: | ||
training_type = None | ||
|
||
json_file = request.FILES.get("json_file", "") | ||
|
||
if not json_file.name.endswith('.json'): | ||
messages.error(request, 'File is not of JSON type') | ||
return redirect("create_training") | ||
file_data = JSONParser().parse(json_file.file) | ||
serializer = TrainingTypeSerializer(training_type, data=file_data, partial=True) | ||
if serializer.is_valid(raise_exception=False): | ||
serializer.save() | ||
messages.success(request, 'On platform training created successfully.') | ||
else: | ||
messages.error(request, serializer.errors) | ||
|
||
return redirect("op_training") | ||
|
||
training_types = TrainingType.objects.filter(required_field=RequiredField.PLATFORM) | ||
return render( | ||
request, | ||
'console/training_type/index.html', | ||
{ | ||
'training_types': training_types, | ||
'training_type_nav': True, | ||
}) |