Skip to content

Commit

Permalink
create training basics --> implement take training feature and resume…
Browse files Browse the repository at this point in the history
… training feature for user (#1951)

## What?
Here i have added a feature that will allow users to take the training
from settings. User can choose from a list of available
trainings and take the onplatform training. Users can also see the
progress of the training and can resume the training from where they
left off.

## Why?
This is a follow up PR for
#1950

## How?
Here we create 3 new views to handle the training.

**1. `training.views.take_training`**

This view will take care of training homepage. It will show progress of
the training if the user has already started the training, show them
details of the training and allow them to start/resume and review the
training.


**2.  `training.views.take_module_training`**

This view will take care of letting users take individual modules of the
training. It will allow users to take the module from where they left
off.
Users will need to finish module1 before they can start module2 and so
on.

Here we have used a considerable amount of javascript to make the
training interactive. How it works is when the user loads the module
page,
we initially load all the content, quizzes and order them based on the
`order` field, and initially we hide all the content and quizzes.

Then we show either the first content,quiz or the next one after a
content, quiz that they had completed last time. and we keep doing this
until the user has completed the module.

**3. `training.views.update_module_progress`**

This view uses a simple post request and expects an ajax request from
the client. It will update the progress of the module and the progress
of the training.


## Testing? [WIP]

Following tests will be added
1. Check access to the training homepage
2. Check access to the training module page
3. Check valid and invalid submission of the training module
4. Check valid and invalid submission of the training


## Screenshots (optional)


![image](https://user-images.githubusercontent.com/24412619/228581570-568f28bc-28d3-4e22-a699-bd05092f77f2.png)


![image](https://user-images.githubusercontent.com/24412619/228581290-4615bc2e-8fbe-4aaf-9706-e19f34e7e1e0.png)


![image](https://user-images.githubusercontent.com/24412619/228581693-d42ab459-9563-49ee-98a0-bb5b61dfce2f.png)


![image](https://user-images.githubusercontent.com/24412619/228581764-e4368b6d-755c-460e-9496-0252c109a449.png)


## Anything Else?

### How to take a training as a user?

1. Login as user
2. Navigate to `Settings`
3. Select the training from the dropdown
4. Start the training
  • Loading branch information
tompollard authored Aug 20, 2024
2 parents c1949d0 + 64ef39e commit e782806
Show file tree
Hide file tree
Showing 20 changed files with 868 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<div class="card mb-3">
<div class="card-body">
<div><button type="button" class="btn btn-sm btn-success" data-toggle="modal"
data-target="#create-course">Update</button></div>
data-target="#create-course">Upload new version</button></div>
<div class="modal fade" id="create-course" tabindex="-1">
<div class="modal-dialog">
<form action="{% url 'course_details' training_type.slug %}" method="POST" enctype="multipart/form-data" class="">
Expand Down Expand Up @@ -79,19 +79,16 @@ <h3 class="card-title text-center">Active Versions</h3>
class="fa fa-download"></i> Download</a>
</td>
<td>
<form action="{% url 'expire_course_version' training_type.slug course.version %}" method="POST">
<form action="{% url 'archive_course_version' training_type.slug course.version %}" method="POST">
{% csrf_token %}
<input type="date" name="expiry_date">
<button class="btn btn-sm btn-danger" type="submit"> Expire</button>
<button class="btn btn-sm btn-danger" type="submit"> Archive</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><b>Note:</b> Users that have taken the particular version of the course that is getting expired,
will need to retake the course or else they will loose credentialing after the number of
days specified above while expiring the course. </p>
<p><b>Note:</b> Users can no longer take the course if there are no active versions available.</p>
</div>
<h3 class="card-title text-center">Archived Versions</h3>
<div class="card-body">
Expand Down
4 changes: 2 additions & 2 deletions physionet-django/console/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@
path('courses/<training_slug>/', training_views.course_details, name='course_details'),
path('courses/<training_slug>/download/<str:version>',
training_views.download_course, name='download_course_version'),
path('courses/<training_slug>/expire/<str:version>',
training_views.expire_course, name='expire_course_version'),
path('courses/<training_slug>/archive/<str:version>',
training_views.archive_course, name='archive_course_version'),
]

# Parameters for testing URLs (see physionet/test_urls.py)
Expand Down
1 change: 1 addition & 0 deletions physionet-django/physionet/test_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ def _handle_request(self, url, _user_=None, _query_={}, _skip_=False,
self.client.force_login(user)

response = self.client.get(url, _query_)

self.assertGreaterEqual(response.status_code, 200)
self.assertLess(response.status_code, 400)

Expand Down
21 changes: 21 additions & 0 deletions physionet-django/static/custom/css/quiz.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.eachQuiz{
display: none
}

.blockContent{
display: none
}

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

.blockContainer {
max-width: 100%;
position: relative;
margin: auto;
display: block;
}
2 changes: 1 addition & 1 deletion physionet-django/static/sample/example-course-update.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"title": "Course 1 Updated",
"title": "Course 1",
"description": "<p>Test content description Updated</div>",
"valid_duration": "1095 00:00:00",
"version": "1.5.1",
Expand Down
39 changes: 37 additions & 2 deletions physionet-django/training/fixtures/demo-training.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,19 @@
"pk": 1,
"fields": {
"training_type": 2,
"version": 1.0
"version": 1.2,
"description": "This course provides an introduction to the continents and countries of the world. It covers the major continents, including North America, South America, Europe, Asia, Africa, and Australia. The course also explores the countries within each continent, highlighting their geography, history, and culture.",
"valid_duration": "1095"
}
},
{
"model": "training.course",
"pk": 2,
"fields": {
"training_type": 2,
"version": 1.0,
"description": "This course provides an introduction to the continents and countries of the world. It covers the major continents, including North America, South America, Europe, Asia, Africa, and Australia. The course also explores the countries within each continent, highlighting their geography, history, and culture.",
"valid_duration": "1095"
}
},
{
Expand Down Expand Up @@ -351,7 +363,7 @@
"order": 9
}
},
{
{
"model": "training.quizchoice",
"pk": 17,
"fields": {
Expand Down Expand Up @@ -431,5 +443,28 @@
"body": "Cameroon",
"is_correct": false
}
},
{
"model": "training.courseprogress",
"pk": 1,
"fields": {
"user": 206,
"course": 1,
"status": "IP",
"started_at": "2024-05-12T00:01:05.884Z",
"completed_at": null
}
},
{
"model": "training.moduleprogress",
"pk": 1,
"fields": {
"course_progress": 1,
"module": 1,
"status": "IP",
"last_completed_order": 1,
"started_at": null,
"updated_at": "2024-05-12T00:01:09.318Z"
}
}
]
33 changes: 33 additions & 0 deletions physionet-django/training/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from django import forms
from django.db.models import Max, F, OuterRef

from training.models import Course
from user.models import TrainingType
from user.enums import RequiredField


class CourseForm(forms.ModelForm):

class Meta:
model = Course
fields = ('training_type', )
labels = {'training_type': 'Training Type'}

def __init__(self, *args, **kwargs):
training_id = kwargs.pop('training_type', None)
super().__init__(*args, **kwargs)

self.fields['training_type'].queryset = self.fields['training_type'].queryset.annotate(
max_version=Max('courses__version')
).filter(
courses__version=F('max_version'),
required_field=RequiredField.PLATFORM,
courses__is_active=True
)

self.training_type = TrainingType.objects.filter(id=training_id).first()

self.fields['training_type'].initial = self.training_type

if self.training_type:
self.fields['training_type'].help_text = self.training_type.description
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 4.2.11 on 2024-08-20 18:56

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("training", "0005_course_trainings"),
]

operations = [
migrations.RemoveField(
model_name="course",
name="trainings",
),
migrations.AlterUniqueTogether(
name="completedcontent",
unique_together={("module_progress", "content")},
),
migrations.AlterUniqueTogether(
name="completedquiz",
unique_together={("module_progress", "quiz")},
),
migrations.AlterUniqueTogether(
name="moduleprogress",
unique_together={("course_progress", "module")},
),
]
38 changes: 32 additions & 6 deletions physionet-django/training/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from token import NUMBER
from django.db import models
from django.utils import timezone

Expand All @@ -21,7 +20,6 @@ class Course(models.Model):
training_type = models.ForeignKey(
"user.TrainingType", on_delete=models.CASCADE, related_name="courses"
)
trainings = models.ManyToManyField("user.Training")
version = models.CharField(
max_length=15, default="", blank=True, validators=[validate_version]
)
Expand All @@ -38,14 +36,12 @@ class Meta:
("can_view_course_guidelines", "Can view course guidelines"),
]

def expire_course_version(self, instance, number_of_days):
def archive_course_version(self):
"""
This method expires the course by setting the is_active field to False and expires all
This method archives the course by setting the is_active field to False and expires all
the trainings associated with it.
"""
self.is_active = False
# reset the valid_duration to the number of days
self.valid_duration = timezone.timedelta(days=number_of_days)
self.save()

def __str__(self):
Expand Down Expand Up @@ -224,6 +220,30 @@ class Status(models.TextChoices):
def __str__(self):
return f"{self.course_progress.user.username} - {self.module}"

class Meta:
unique_together = ("course_progress", "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):
"""
Expand All @@ -249,6 +269,9 @@ class CompletedContent(models.Model):
def __str__(self):
return f"{self.module_progress.course_progress.user.username} - {self.content}"

class Meta:
unique_together = ("module_progress", "content")


class CompletedQuiz(models.Model):
"""
Expand All @@ -273,3 +296,6 @@ class CompletedQuiz(models.Model):

def __str__(self):
return f"{self.module_progress.course_progress.user.username} - {self.quiz}"

class Meta:
unique_together = ("module_progress", "quiz")
74 changes: 74 additions & 0 deletions physionet-django/training/templates/training/course.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{% extends 'base.html' %}

{% load static %}

{% block title %}Training - {{ course.training_type.name }}{% endblock %}

{% block local_css %}
<link rel="stylesheet"
type="text/css"
href="{% static 'project/css/project-home.css' %}">
{% endblock %}

{% block content %}
<div class="container">
<h1>{{ course.training_type.name }}</h1>
{% include "message_snippet.html" %}
<div class="description">
<h2>Training Description</h2>
{{ course.training_type.description | safe }}
</div>

<div class="card mb-3">
<div class="card-header">
<h3>Training Modules</h3>
</div>
<div class="card-body">
{% if modules %}
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Module</th>
<th scope="col">Progress</th>
<th scope="col">Dates</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
{% for module in modules %}
<tr>
<td>
<strong>{{ module.name }}</strong>
</td>
<td>{{ module.progress_status }}</td>
<td>{{ module.progress_updated_date}}</td>
<td>
<a href="{% url 'current_module_block' course.training_type.slug module.pk module.last_completed_order %}">
{% if module.progress_status == ModuleStatus.COMPLETED.label %}
Review
{% elif module.progress_status == ModuleStatus.IN_PROGRESS.label %}
Continue
{% else %}
Start
{% endif %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No datasets available.</p>
{% endif %}
</div>
</div>
<br>

</div>
{% endblock %}

{% block local_js_bottom %}
<script>
</script>
<script src="{% static 'custom/js/resize-ck.js' %}"></script>
{% endblock %}
Loading

0 comments on commit e782806

Please sign in to comment.