Skip to content

Commit

Permalink
implement trainings for user
Browse files Browse the repository at this point in the history
Allows users to take the test from settings.

The quiz.html file loads all the content and questions based on the order. All the data is hidden from user and gradually displayed as users go through the training

We have two level of verifications of answers. the first one is done on quiz.html where users go through the content and they will see the related questions and answer the questions. users can go to next section or skip the question only if they have answered the question correctly.
We also have another verification done on view. here if the users had correctly answered the questions a json string is generated and sent via POST, we will then check the database to verify that the data in json string(question,answer) matched with the database. This should help us stop someone from just sending a post request to complete the training.

Quick note:
1. Users are not able to resume the test if they refresh the browser
2. Users can take the same test multiple times
3. Only multiple choice questions is supported yet.
  • Loading branch information
superryeti committed Mar 20, 2023
1 parent afdb6b2 commit 3705fc9
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 0 deletions.
14 changes: 14 additions & 0 deletions physionet-django/static/custom/css/quiz.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.eachQuiz{
display: none
}

.quizContainer {
max-width: 100%;
position: relative;
margin: auto;
display: block;
}

.alert {
display: none
}
99 changes: 99 additions & 0 deletions physionet-django/static/custom/js/quiz.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
var slideIndex = 1;
sessionStorage.setItem("slidePosition", 0);

showSlide(slideIndex);

function changeSlide(n) {
showSlide(slideIndex += n);
}

$('.next').on('click', function () {
// move to next slide but keep track of start of partaining training
let slidePosition = parseInt(sessionStorage.getItem("slidePosition"));
sessionStorage.setItem("slidePosition", slidePosition - 1)
changeSlide(1);
})

$('.previous').on('click', function () {
// move to next slide but keep track of start of partaining training
let slidePosition = parseInt(sessionStorage.getItem("slidePosition"));
sessionStorage.setItem("slidePosition", slidePosition - 2);
changeSlide(-1);
})

function showSlide(n) {

let i;
let slides = document.getElementsByClassName("eachQuiz");
if (n > slides.length) {
alertBox("<strong>Congratulations</strong> You have come to the end of the training.");
sessionStorage.clear();

let question_answers = {};
document.querySelectorAll('.question input[type="radio"]:checked').forEach(function (eachQuiz) {
let question_id = eachQuiz.getAttribute("name");
let value = eachQuiz.value;
question_answers[question_id] = parseInt(value);
});

$('[name="question_answers"]').val(JSON.stringify(question_answers));

$('form').submit();
}
if (n < 1) { slideIndex = slides.length }
for (i = 0; i < slides.length; i++) {
slides[i].style.display = "none";
}

slides[slideIndex - 1].style.display = "block";
}
//
// $("input[type=radio]").on("click", function () {
// // check if answer is correct and proceed, else take back to training
// let id = $(this).attr("name")
// let value = $(this).val();
// var slidePosition = parseInt(sessionStorage.getItem("slidePosition"))
//
// sessionStorage.setItem("slidePosition", 0)
//
// if (sessionStorage.getItem(id) == value) {
// alertBox("<strong>Yay!</strong> You chose the correct answer.", "success")
// changeSlide(1)
// } else {
// alertBox("<strong>Oops!</strong> You chose a wrong answer, kindly go through the training again.", "danger")
// changeSlide(slidePosition)
// }
// })

$('.checkAnswer').on('click', function () {

let question_id = $('input[type="radio"]:checked', $(this).parent()).attr("name");
if (question_id == undefined) {
alertBox("<strong>Oops!</strong> You did not choose an answer, kindly choose an answer.", "danger");
return
}
let value = $('input[type="radio"]:checked', $(this).parent()).val();
// let slidePosition = parseInt(sessionStorage.getItem("slidePosition"));

sessionStorage.setItem("slidePosition", 0);

if (sessionStorage.getItem(question_id) == value) {
alertBox("<strong>Yay!</strong> You chose the correct answer.", "success");
$('.next', $(this).parent().parent()).removeClass('disabled');
// changeSlide(1);
} else {
alertBox("<strong>Oops!</strong> You chose a wrong answer, kindly go through the previous sections again.", "danger");
// changeSlide(slidePosition);
$('.next', $(this).parent().parent()).addClass('disabled');
}
});

function alertBox(message, _class) {
let alert = document.getElementById("alert");
alert.className = "alert alert-" + _class;
alert.innerHTML = message;
alert.style.display = "block";
setTimeout(function () {
alert.style.display = "none";
}, 3000);
};
77 changes: 77 additions & 0 deletions physionet-django/training/templates/training/quiz.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{% extends "base.html" %}
{% load static %}
{% load physionet_templatetags %}
{% block title %}{{ training.name }}{% endblock %}
{% block local_css %}
<link rel="stylesheet"
type="text/css"
href="{% static 'custom/css/quiz.css' %}">
{% endblock %}
{% block content %}
<div class="container my-3">
<div class="quizContainer">
<div class="quizHeader">
<h1 >{{ training.training_type.name }}</h1>
</div>
<form method="POST" class="training">
{% csrf_token %}
<input type="text" hidden name="question_answers" value=""/>
{% for item in quiz_content %}
{% if item.body %}
<div class="eachQuiz">
<div>{{ item.body | safe }}</div>
<div class="row ml-1">
{% if forloop.counter != 1 %}<a class="btn btn-warning previous">Back</a>{% endif %}
<a class="btn btn-success next text-white ml-auto mr-3">
{% if forloop.last %}
Submit Training
{% else %}
Next
{% endif %}
</a>
</div>
</div>
{% else %}
<div class="eachQuiz question">
<div class="list-group">
<div class="list-group-item py-4">{{ item.question | safe }}</div>
{% for choice in item.choices.all %}
<label class="list-group-item list-group-item-action q_{{ item.id }} {{ item.id }}_{{ choice.id }} mb-0"
for="{{ item.id }}_{{ choice.id }}">
<input type="radio"
id="{{ item.id }}_{{ choice.id }}"
name="{{ item.id }}"
value="{{ choice.id }}">
{{ choice.body | safe }}
</label>
{# add a check answer button #}
{% endfor %}
<a type="button"
class="btn btn-secondary text-white checkAnswer"
data-question="{{ item.id }}"
data-answer="{{ item.answer.id }}">Check Answer</a>
</div>
<br>
<div class="row ml-1">
{% if forloop.counter != 1 %}<a class="btn btn-warning previous">Back</a>{% endif %}
<a class="btn btn-success next text-white ml-auto mr-3 disabled">
{% if forloop.last %}
Submit Training
{% else %}
Next
{% endif %}
</a>
</div>
</div>
{% endif %}
{% endfor %}
</form>
</div>
<br>
<div id="alert" class="alert" role="alert"></div>
</div>
{% endblock %}
{% block local_js_bottom %}
{% for quiz, answer in quiz_answer %}<script>sessionStorage.setItem({{quiz }}, {{answer }})</script>{% endfor %}
<script src="{% static 'custom/js/quiz.js' %}"></script>
{% endblock %}
2 changes: 2 additions & 0 deletions physionet-django/training/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@

urlpatterns = [
path('training/', views.on_platform_training, name='op_training'),
path('settings/platform-training/<int:training_id>/', views.take_training, name='platform_training'),
path('settings/platform-training/', views.take_training, name='start_training')
]
64 changes: 64 additions & 0 deletions physionet-django/training/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,70 @@
from training.serializers import TrainingTypeSerializer


@login_required
def take_training(request, training_id=None):

if request.method == 'POST':
if request.POST.get('training_type'):
return redirect('platform_training', request.POST['training_type'])

op_training = OnPlatformTraining.objects.select_related('training_type').filter(
training_type__id=training_id).last()

# check if the questions are answered correctly
try:
user_question_answers = json.loads(request.POST['question_answers'])
# convert the keys to int(javascript sends them as string)
user_question_answers = {int(k): v for k, v in user_question_answers.items()}
for question in op_training.quizzes.all():
correct_answer = question.choices.filter(is_correct=True).first().id
if (question.id not in user_question_answers
or user_question_answers.get(question.id) != correct_answer):
messages.error(request, 'Please answer all questions correctly.')
return redirect("edit_training")

except json.JSONDecodeError:
messages.error(request, 'Please submit the training correctly.')
return redirect("edit_training")

# save a training object to database in Training
training = Training()
slug = get_random_string(20)
while Training.objects.filter(slug=slug).exists():
slug = get_random_string(20)

training.slug = slug
training.training_type = op_training.training_type
training.user = request.user
training.process_datetime = timezone.now()
training.status = TrainingStatus.ACCEPTED
training.save()

training_questions = []
for question in op_training.training_type.questions.all():
training_questions.append(TrainingQuestion(training=training, question=question))

TrainingQuestion.objects.bulk_create(training_questions)

messages.success(
request, f'Congratulations! You completed the training {op_training.training_type.name} successfully.')

return redirect("edit_training",)

training = OnPlatformTraining.objects.prefetch_related(
Prefetch("quizzes__choices", queryset=QuizChoice.objects.order_by("?")),
Prefetch("contents", queryset=ContentBlock.objects.all())).filter(
training_type__id=training_id).order_by('version').last()

return render(request, 'training/quiz.html', {
'quiz_content': sorted(chain(
training.quizzes.all(), training.contents.all()),
key=operator.attrgetter('order')),
'quiz_answer': training.quizzes.filter(choices__is_correct=True).values_list("id", "choices"),
'training': training,
})


@permission_required('physionet.change_onplatformtraining', raise_exception=True)
def on_platform_training(request):

Expand Down

0 comments on commit 3705fc9

Please sign in to comment.