Skip to content

Commit

Permalink
create training basics --> Add create training from admin console by …
Browse files Browse the repository at this point in the history
…uploading json (#1950)

## What?
Here i have added a feature that will allow admin to create training
from admin console by uploading json file. This also lays foundation to
implement take training feature and resume training feature for user
later.

## Why?
A training feature was desired by HDN so that we can build/customize a
onplatform training for users which will ultimately be used to control
access to projects.
The new onplatform training will work just like the existing Trainings
and work with projects without any additional changes, Currently users
take training outside the platform and then upload the certificate to
the platform.

## How?
We created a separate app called Training. This app will be used to
create/manage training courses, and allow users to take onplatform
training.

We created two major types of Models in  training.models

**1. Platform Training (`training.models.OnPlatformTraining`)**

The idea here is that a new Training Course will be defined with
`user.models.TrainingType`. This is the ultimate(top) model for a
training course.

The training content for `TrainingType` model will be implemented by the
new training app. On Training app, `OnPlatformTraining` instance can be
created for each TrainingType. For different versions of the same
training course, we can create as many `OnPlatformTraining` models as we
want as long as the version is different.

A training is divided into modules. Each module has a description and a
list of contents and quizzes. Modules are like chapters in a book. Each
module has a list of contents and quizzes. Contents are like paragraphs
in a chapter. Quizzes are like questions in a chapter. Contents and
quizes can be organized in any order which is controlled by `order`
field. For ordering the modules, contents and quizzes, we have used
`order` field. The ordering is unique for each instance of the parent
model.

For example : Under Module 1, we have content 1(order=1), content
2(order=2), content 3(order=4) and quiz 1(order=3). Then the ordering of
the contents and quizzes will be content 1, content 2, quiz 1, content
3.

It is expected that the order will start from 1 and will be incremented
by 1 with no gaps.

**2. Tracking User Progress during training**
(`training.models.OnPlatformTrainingProgress`)

When a user starts a training, a `OnPlatformTrainingProgress` instance
should be created for that user and the version of the training type
that they are taking. This model tracks the progress of the user during
the training. Similarly, when a user starts a module, a `ModuleProgress`
model should be created for each module in the training. For quiz,
content progress, we should create a instance of `CompletedContent` or
`CompletedQuiz` model when the user completes a content or quiz.

I don't think we need to track when someone started a content or quiz as
these are expected to complete in few minutes.

## Testing?

Next PR #1951

## Screenshots (optional)


## Anything Else?

### How to create a training from admin console?

1. Login as admin
2. Navigate to `Training` tab in admin console
3. Click on `Create/Update OP Training` button
4. Select `Create New Training` from the dropdown
5. Upload the example json from
`training/fixtures/example-training-create.json`

### How to edit a training from admin console?

1. Follow steps 1-3 from above
2. Select Existing Training from the dropdown
3. Upload the example json from
`training/fixtures/example-training-update.json`

Notes:
This is the first part of Training PR, there is another PR
#1951 which is rebased on
this PR.
  • Loading branch information
tompollard authored Mar 26, 2024
2 parents 83c80c5 + de3e23c commit a694080
Show file tree
Hide file tree
Showing 38 changed files with 2,074 additions and 47 deletions.
2 changes: 2 additions & 0 deletions physionet-django/console/navbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ def get_menu_items(self, request):
NavLink(_('Training check'), 'training_list', 'school',
view_args=['review']),

NavLink(_('Courses'), 'courses', 'chalkboard-teacher'),

NavSubmenu(_('Events'), 'events', 'clipboard-list', [
NavLink(_('Active'), 'event_active'),
NavLink(_('Archived'), 'event_archive'),
Expand Down
72 changes: 34 additions & 38 deletions physionet-django/console/templates/console/console_navbar.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,42 +13,37 @@
<ul class="navbar-nav navbar-sidenav" id="sideAccordion">
{% console_nav_menu_items request as nav_menu_items %}
{% for item in nav_menu_items %}
{% if item.subitems %}
<li class="nav-item">
<a id="nav_{{ item.name }}_dropdown"
class="nav-link nav-link-collapse drop {{ item.active|yesno:',collapsed' }}"
data-toggle="collapse"
data-target="#nav_{{ item.name }}_components"
data-parent="#sideAccordion"
aria-expanded="{{ item.active|yesno:'true,false' }}"
href="#">
{% if item.icon %}
<span class="nav-link-icon fa fa-fw fa-{{ item.icon }}"></span>
{% endif %}
<span class="nav-link-text">{{ item.title }}</span>
</a>
<ul class="sidenav-second-level collapse {{ item.active|yesno:'show,' }}"
id="nav_{{ item.name }}_components">
{% for subitem in item.subitems %}
<li class="nav-item {{ subitem.active|yesno:'active,' }}">
<a id="nav_{{ subitem.name }}" class="nav-link"
href="{{ subitem.url }}">{{ subitem.title }}</a>
</li>
{% endfor %}
</ul>
</li>
{% else %}
<li class="nav-item {{ item.active|yesno:'active,' }}">
<a id="nav_{{ item.name }}" class="nav-link" href="{{ item.url }}">
{% if item.icon %}
<span class="nav-link-icon fa fa-fw fa-{{ item.icon }}"></span>
{% endif %}
<span class="nav-link-text">{{ item.title }}</span>
</a>
{% if item.subitems %}
<li class="nav-item">
<a id="nav_{{ item.name }}_dropdown"
class="nav-link nav-link-collapse drop {{ item.active|yesno:',collapsed' }}" data-toggle="collapse"
data-target="#nav_{{ item.name }}_components" data-parent="#sideAccordion"
aria-expanded="{{ item.active|yesno:'true,false' }}" href="#">
{% if item.icon %}
<span class="nav-link-icon fa fa-fw fa-{{ item.icon }}"></span>
{% endif %}
<span class="nav-link-text">{{ item.title }}</span>
</a>
<ul class="sidenav-second-level collapse {{ item.active|yesno:'show,' }}" id="nav_{{ item.name }}_components">
{% for subitem in item.subitems %}
<li class="nav-item {{ subitem.active|yesno:'active,' }}">
<a id="nav_{{ subitem.name }}" class="nav-link" href="{{ subitem.url }}">{{ subitem.title }}</a>
</li>
{% endif %}
{% endfor %}
</ul>
</li>
{% else %}
<li class="nav-item {{ item.active|yesno:'active,' }}">
<a id="nav_{{ item.name }}" class="nav-link" href="{{ item.url }}">
{% if item.icon %}
<span class="nav-link-icon fa fa-fw fa-{{ item.icon }}"></span>
{% endif %}
<span class="nav-link-text">{{ item.title }}</span>
</a>
</li>
{% endif %}
{% endfor %}
<!-- end of menu items -->
<!-- end of menu items -->
</ul>

<ul class="navbar-nav sidenav-toggler">
Expand All @@ -64,10 +59,11 @@
</div>
<div class="navbar-search">
<form class="form-inline" action="{% url 'content_index' %}">
<input name="topic" class="search-input" type="text" placeholder="Search">
<span class="input-group-btn">
<button id="search-button" type="submit" class="btn-search my-2 my-sm-0" type="button"><i class="fa fa-search"></i></button>
</span>
<input name="topic" class="search-input" type="text" placeholder="Search">
<span class="input-group-btn">
<button id="search-button" type="submit" class="btn-search my-2 my-sm-0" type="button"><i
class="fa fa-search"></i></button>
</span>
</form>
</div>
</nav>
162 changes: 162 additions & 0 deletions physionet-django/console/templates/console/guidelines_course.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
{% extends "console/base_console.html" %}
{% load static %}
{% block title %}Guidelines for Courses{% endblock %}

{% block content %}
<div class="card mb-3">
<div class="card-header">
Guidelines for creating and updating courses
</div>
<div class="card-body">
<div class="table-responsive">
<p>To create a new course or update a course, you will need to organize all the course content in a json file
and upload via the
<a href="{% url 'courses' %}">Courses</a> page.</p>
<p>Here is the <a href="{% static 'sample/create-course-schema.json' %}">course schema</a> with typing info that you can
follow to create a new course or update a course. Similarly, here is an example json file to <a
href="{% static 'sample/example-course-create.json' %}">create a new course</a> or to <a
href="{% static 'sample/example-course-update.json' %}">update a course</a></p>

<h4>General Schema Explanation</h4>
<pre>
{
"name": "string",
"description": "string",
"valid_duration": "string",
"courses": [{
"version": "string",
"modules": [
{
"contents": [
{
"body": "string",
"order": "integer"
}
],
"quizzes": [
{
"question": "string",
"order": "integer",
"choices": [
{
"body": "string",
"is_correct": "boolean"
}
]
}
]
}
]
}]
}
</pre>
<ol start="1">
<li><strong>Course Information:</strong> Fill out all fields in the JSON file with the appropriate information
about your course, including:
</li>
<ul>
<li>The name of the course</li>
<li>A description of the course in html</li>
<li>The valid duration of the course</li>
<li>The version of the course</li>
</ul>
<small><strong>Note the importance of versioning:</strong> It's essential to keep track of the version number
of your course.
Please make sure the version number is unique for each course update. Even minor changes, such as a spelling
correction,
require an update to the version number in the new JSON file (refer to the create and update JSON examples
above).
Please use the Semantic Versioning system (Major.Minor) for version numbers.
If you update a course's major version (e.g., from 1.5 to 2.0), all existing training certificates provided
to
users will expire after a month. To regain access to resources, users must retake the new version of the
training.</small>

</ol>
<ol start="2">
<li><strong>Modules:</strong> A course may have one or more modules. Each module contains the course content and quizzes.
Modules must include:
section of the course. Each module should include:
</li>
<ul>
<li>A name for the module</li>
<li>A description of the module in html</li>
<li>An order for the module</li>
<li>One or more content items in the module</li>
<li>One or more quizzes in the module</li>
</ul>
</ol>
<ol start="3">
<li><strong>Content:</strong> A module may have one or more content blocks. Each content block should include:</li>
<ul>
<li>A body for the content in html</li>
<li>An order for the content</li>
</ul>
</ol>
<ol start="4">
<li><strong>Quizzes:</strong> A module may have one or more quiz. Each Quiz should include:</li>
<ul>
<li>A question for the quiz</li>
<li>An order for the quiz</li>
<li>One or more choices for the quiz, including the correct answer</li>
<small><strong>Order:</strong> Note that the order is used to determine which content or quiz comes first,
so please
make sure you order the content and quiz properly. <br> For example, here the user will see the content
and quiz in the following order
content1, quiz 1, content 2, quiz 2.
<pre><code>
"contents": [
{
"body": "This content will display first for the given module.",
"order": 1
},
{
"body": "This content will appear third (order = 3), after the quiz below.",
"order": 3
}
],
"quizzes": [
{
"question": "This quiz will appear after the first content block (order = 2).",
"order": 2,
"choices": [
{
"body": "This answer is correct. The user will proceed to the next block if they answer it.",
"is_correct": true
},
{
"body": "This choice is incorrect. The user will return to the beginning of the module if they answer it.",
"is_correct": false
}
]
},
{
"question": "This quiz will appear last, after the final content block (order = 4).",
"order": 4,
"choices": [
{
"body": "choice 1",
"is_correct": true
},
{
"body": "choice 2",
"is_correct": false
}
]
}
]
</code></pre>
</small>
</ul>
</ol>
<ol start="5">
<li><strong>Choices:</strong> A quiz may have one or more choices. Each choice should include:</li>
<ul>
<li>A body for the choice</li>
<li>A boolean value of true for the correct answer</li>
</ul>
</ol>
</div>
</div>
</div>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
{% extends "console/base_console.html" %}

{% load static %}

{% block title %}{{ training_type }}{% endblock %}

{% block content %}
<script>
console.log('update-course event listener triggered');
$('#update-course').on('show.bs.modal', function (event) {
console.log("modal opened");
var button = $(event.relatedTarget); // Button that triggered the modal
var trainingType = button.data('training-type'); // Extract info from data-* attributes
var modal = $(this);
modal.find('.modal-body input#training-type').val(trainingType);
console.log(trainingType);
});
</script>

<div class="card mb-3">
<div class="card-header">
{{ training_type }} <span class="badge badge-pill badge-info"></span>
</div>

<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>
<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="">
<div class="modal-content">
<div class="modal-body">
<p>Learn how to create a course <a href="{% url 'guidelines_course' %}">here</a> or see sample files to <a
href="{% static 'sample/example-course-create.json' %}">create a new course</a></p>
<div>
{% csrf_token %}
<div class="form-group">
<label>Training Type: </label>
<option value="{{ training_type.slug }}">{{ training_type.name|title }}</option>
</div>
<div class="form-group">
<label>File: </label>
<input type="file" name="json_file" id="json_file" required="True" class="form-control">
</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>

<br>
<h3 class="card-title text-center">Active Versions</h3>
<div class="card-body">
<table class="table table-bordered">
<thead>
<tr>
<th>Name</th>
<th>Version</th>
<th>Download</th>
<th>Expire</th>
</tr>
</thead>
<tbody>
{% for course in active_course_versions %}

<tr>
<td>{{ training_type.name|title }}</td>
<td>{{ course.version }}</td>
<td>
<a href="{% url 'download_course_version' training_type.slug course.version %}"><i
class="fa fa-download"></i> Download</a>
</td>
<td>
<form action="{% url 'expire_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>
</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>
</div>
<h3 class="card-title text-center">Archived Versions</h3>
<div class="card-body">
<table class="table table-bordered">
<thead>
<tr>
<th>Name</th>
<th>Version</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for course in inactive_course_versions %}

<tr>
<td>{{ training_type.name|title }}</td>
<td>{{ course.version }}</td>
<td>
<a href="{% url 'download_course_version' training_type.slug course.version %}"><i
class="fa fa-download"></i> Download</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

<script>
document.getElementById('expiry_date').valueAsDate = new Date();
</script>
Loading

0 comments on commit a694080

Please sign in to comment.