Skip to content

Commit

Permalink
create onplatform training with json
Browse files Browse the repository at this point in the history
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
superryeti committed Mar 20, 2023
1 parent 1a6b5bc commit afdb6b2
Show file tree
Hide file tree
Showing 9 changed files with 359 additions and 8 deletions.
29 changes: 23 additions & 6 deletions physionet-django/console/templates/console/console_navbar.html
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,31 @@
</li>
{% endif %}

<!-- Training -->
{% if perms.user.can_review_training %}
<!-- training -->
<li class="nav-item {% if training_nav %}active{% endif %}" data-toggle="tooltip" data-placement="right">
<a id="nav_storage_requests" class="nav-link" href="{% url 'training_list' 'review' %}">
<i class="fa fa-fw fa-school"></i>
<span class="nav-link-text">Training check</span>
<li class="nav-item" data-toggle="tooltip" data-placement="right">
{% if training_type_nav or training_nav %}
<a id="nav_pages_dropdown" class="nav-link nav-link-collapse drop" data-toggle="collapse" href="#trainingComponent" data-parent="#sideAccordion" aria-expanded="true">
{% else %}
<a id="nav_pages_dropdown" class="nav-link nav-link-collapse drop collapsed" data-toggle="collapse" href="#trainingComponent" data-parent="#sideAccordion" aria-expanded="false">
{% endif %}
<i class="fa fa-fw fa-window-maximize"></i>
<span class="nav-link-text">Training</span>
</a>
</li>
<!-- submenu -->
{% if training_type_nav or training_nav %}
<ul class="sidenav-second-level collapse show" id="trainingComponent" style="">
{% else %}
<ul class="sidenav-second-level collapse" id="trainingComponent">
{% endif %}
<li class="nav-item {% if training_nav %}active{% endif %}">
<a id="nav_console_news" class="nav-link" href="{% url 'training_list' 'review' %}">Training Check</a>
</li>
<li class="nav-item {% if training_type_nav %}active{% endif %}">
<a id="nav_console_news" class="nav-link" href="{% url 'op_training' %}">OP Training</a>
</li>
</ul>
</li>
{% endif %}

<!-- events -->
Expand Down
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 %}
16 changes: 16 additions & 0 deletions physionet-django/notification/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -1022,3 +1022,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)
2 changes: 2 additions & 0 deletions physionet-django/physionet/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,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
Expand Down
23 changes: 23 additions & 0 deletions physionet-django/static/sample/create.json
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"
}
]
}
]
}
}
153 changes: 153 additions & 0 deletions physionet-django/training/serializers.py
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
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 %}
7 changes: 7 additions & 0 deletions physionet-django/training/urls.py
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'),
]
54 changes: 52 additions & 2 deletions physionet-django/training/views.py
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,
})

0 comments on commit afdb6b2

Please sign in to comment.