[\w\-]+)/$',
29 | student.take_questions, name='student_question'),
30 |
31 |
32 |
33 |
34 |
35 | ]
36 |
--------------------------------------------------------------------------------
/classroom/templates/classroom/includes/form.html:
--------------------------------------------------------------------------------
1 | {% load widget_tweaks %}
2 |
3 | {% if form.non_field_errors %}
4 |
5 | {% for error in form.non_field_errors %}
6 | {{ error }}
7 | {% endfor %}
8 |
9 | {% endif %}
10 |
11 | {% for field in form.visible_fields %}
12 | {{ field.label_tag }}
13 |
14 | {% autoescape off %}
15 | {% if field.help_text %}
16 |
{{ field.help_text }}
17 | {% endif %}
18 | {% endautoescape %}
19 |
20 | {% if form.is_bound %}
21 | {% if field.errors %}
22 | {% for error in field.errors %}
23 |
24 | {{ error }}
25 |
26 | {% endfor %}
27 | {% render_field field class="w3-input w3-round w3-border w3-border-red" %}
28 |
29 | {% else %}
30 | {% render_field field class="w3-input w3-round w3-border w3-border-green" %}
31 | {% endif %}
32 | {% else %}
33 | {% render_field field class="w3-input w3-round w3-border" %}
34 | {% endif %}
35 |
36 |
37 |
38 | {% endfor %}
39 |
--------------------------------------------------------------------------------
/classroom/static/classroom/js/instructor/assignments.js:
--------------------------------------------------------------------------------
1 | function accordionFunction(id) {
2 | var x = document.getElementById(id);
3 | if (x.className.indexOf("w3-show") == -1) {
4 | x.className += " w3-show";
5 | } else {
6 | x.className = x.className.replace(" w3-show", "");
7 | }
8 | }
9 |
10 |
11 | $(document).ready(function () {
12 |
13 |
14 |
15 | // $("#js-add-new-choice-btn").click( function () {
16 | // button = $(this);
17 | // $.ajax({
18 | // url: button.attr("data-href"),
19 | // type: 'get',
20 | // dataType: 'json',
21 |
22 | // success: function (data) {
23 | // $("#js-add-new-choice-modal").show();
24 | // $("#display-form-content").html(data.html_form);
25 | // }
26 |
27 | // });
28 | // });
29 |
30 |
31 |
32 |
33 |
34 | $(".js-add-question-assignment").click( function () {
35 | button = $(this);
36 | $.ajax({
37 | url: button.attr("data-href"),
38 | type: 'get',
39 | dataType: 'json',
40 |
41 | success: function (data) {
42 | $("#js-add-new-choice-modal").show();
43 | $("#display-form-content").html(data.html_form);
44 | }
45 |
46 | });
47 | });
48 |
49 |
50 |
51 |
52 | });
--------------------------------------------------------------------------------
/accounts/migrations/0002_auto_20180521_1519.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11 on 2018-05-21 15:19
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('classroom', '0002_auto_20180521_1519'),
12 | ('accounts', '0001_initial'),
13 | ]
14 |
15 | operations = [
16 | migrations.RemoveField(
17 | model_name='instructor',
18 | name='instructor',
19 | ),
20 | migrations.RemoveField(
21 | model_name='lmsadmin',
22 | name='admin',
23 | ),
24 | migrations.RemoveField(
25 | model_name='student',
26 | name='student',
27 | ),
28 | migrations.AddField(
29 | model_name='user',
30 | name='user_type',
31 | field=models.CharField(choices=[('AD', 'Admin'), ('IN', 'Instructor'), ('ST', 'Student')], default='AD', max_length=2),
32 | preserve_default=False,
33 | ),
34 | migrations.DeleteModel(
35 | name='Instructor',
36 | ),
37 | migrations.DeleteModel(
38 | name='LMSAdmin',
39 | ),
40 | migrations.DeleteModel(
41 | name='Student',
42 | ),
43 | ]
44 |
--------------------------------------------------------------------------------
/accounts/templates/registration/login.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load static %}
3 | {% load widget_tweaks %}
4 |
5 |
6 | {% block boilerplate_html %}
7 |
8 |
9 |
10 |
11 |
12 |
Login
13 |
14 |

15 |
16 |
38 |
39 |
40 |
41 |
42 | {% endblock boilerplate_html %}
43 |
--------------------------------------------------------------------------------
/classroom/templates/classroom/student/courses.html:
--------------------------------------------------------------------------------
1 | {% extends "classroom/student_base.html" %}
2 |
3 | {% load static %}
4 |
5 | {% block choice %}
6 | Courses
7 | List of all the courses you are enrolled in
8 | {% endblock choice %}
9 |
10 | {% block add_button %}
11 |
12 | {% endblock add_button %}
13 |
14 | {% block content %}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | | Course code |
23 | Course title |
24 |
25 |
26 |
27 |
28 | {% for course in courses %}
29 |
30 | | {{course.code}} |
31 | {{course.title}} |
32 |
33 |
34 | {% empty %}
35 |
36 |
37 | | No data to display |
38 |
39 |
40 | {% endfor %}
41 |
42 |
43 |
44 |
45 |
46 |
47 | {% endblock content %}
48 |
49 | {% block student_javascript %}
50 |
51 |
52 |
53 |
54 | {% endblock student_javascript %}
55 |
--------------------------------------------------------------------------------
/classroom/templates/classroom/student/grades.html:
--------------------------------------------------------------------------------
1 | {% extends "classroom/student_base.html" %}
2 |
3 | {% load student_tags %}
4 |
5 | {% load static %}
6 |
7 | {% block choice %}
8 | Grades
9 | List of all your grades
10 | {% endblock choice %}
11 |
12 | {% block add_button %}
13 |
14 | {% endblock add_button %}
15 |
16 | {% block content %}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | | Name of assignment/quiz |
25 | Course code |
26 | Grade |
27 |
28 |
29 |
30 |
31 | {% for grade in grades %}
32 |
33 | | {{grade.quiz_or_assignment.name}} |
34 | {{grade.course.code}} |
35 | {% get_grade_and_percentage grade.score %} |
36 |
37 |
38 | {% empty %}
39 |
40 |
41 | | No data to display |
42 |
43 |
44 | {% endfor %}
45 |
46 |
47 |
48 |
49 |
50 |
51 | {% endblock content %}
52 |
53 | {% block student_javascript %}
54 |
55 |
56 | {% endblock student_javascript %}
57 |
--------------------------------------------------------------------------------
/classroom/migrations/0015_auto_20180601_0311.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11 on 2018-06-01 03:11
3 | from __future__ import unicode_literals
4 |
5 | from django.conf import settings
6 | from django.db import migrations, models
7 | import django.db.models.deletion
8 |
9 |
10 | class Migration(migrations.Migration):
11 |
12 | dependencies = [
13 | ('classroom', '0014_auto_20180531_1538'),
14 | ]
15 |
16 | operations = [
17 | migrations.RemoveField(
18 | model_name='discussion',
19 | name='comments',
20 | ),
21 | migrations.RemoveField(
22 | model_name='quizorassignment',
23 | name='comment',
24 | ),
25 | migrations.AddField(
26 | model_name='comment',
27 | name='discussion',
28 | field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='classroom.Discussion'),
29 | ),
30 | migrations.AlterField(
31 | model_name='comment',
32 | name='author',
33 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
34 | ),
35 | migrations.AlterField(
36 | model_name='discussion',
37 | name='created_by',
38 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
39 | ),
40 | ]
41 |
--------------------------------------------------------------------------------
/classroom/templates/classroom/includes/formset.html:
--------------------------------------------------------------------------------
1 | {% load widget_tweaks %}
2 |
3 | {{ formset.management_form }}
4 |
5 | {{ formset.non_form_errors.as_ul}}
6 |
7 |
8 | {% for form in formset.forms %}
9 | {{form.id}}
10 | {% if forloop.first %}
11 |
12 |
13 |
14 | {% for field in form.visible_fields %}
15 |
16 | | {{ field.label|capfirst }} |
17 | {% endfor %}
18 |
19 |
20 |
21 | {% endif %}
22 |
23 |
24 | {% for field in form.visible_fields %}
25 |
26 |
27 | {% if form.is_bound %}
28 |
29 | {% if field.errors %}
30 |
31 | | {% render_field field class="w3-input w3-round w3-border w3-border-red" %} |
32 |
33 | {% else %}
34 |
35 | {% render_field field class="w3-input w3-round w3-border w3-border-green" %} |
36 |
37 | {% endif %}
38 |
39 | {% else %}
40 |
41 | {% render_field field class="w3-input w3-round w3-border" %} |
42 |
43 | {% endif %}
44 |
45 |
46 | {% endfor %}
47 |
48 |
49 |
50 | {% endfor %}
51 |
--------------------------------------------------------------------------------
/classroom/migrations/0010_auto_20180530_0955.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11 on 2018-05-30 09:55
3 | from __future__ import unicode_literals
4 |
5 | from django.db import migrations, models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('classroom', '0009_quiz_comment'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='question',
17 | name='answer',
18 | field=models.TextField(),
19 | ),
20 | migrations.AlterField(
21 | model_name='question',
22 | name='first_option',
23 | field=models.TextField(verbose_name='A'),
24 | ),
25 | migrations.AlterField(
26 | model_name='question',
27 | name='fourth_option',
28 | field=models.TextField(verbose_name='D'),
29 | ),
30 | migrations.AlterField(
31 | model_name='question',
32 | name='second_option',
33 | field=models.TextField(verbose_name='B'),
34 | ),
35 | migrations.AlterField(
36 | model_name='question',
37 | name='third_option',
38 | field=models.TextField(verbose_name='C'),
39 | ),
40 | migrations.AlterField(
41 | model_name='quiz',
42 | name='date_of_submission',
43 | field=models.DateTimeField(help_text='Date and time of submission'),
44 | ),
45 | ]
46 |
--------------------------------------------------------------------------------
/classroom/templates/classroom/lms_admin/courses.html:
--------------------------------------------------------------------------------
1 | {% extends "classroom/lms_admin_base.html" %}
2 |
3 | {% load static %}
4 |
5 | {% block choice %}
6 | Instructors
7 | {% endblock choice %}
8 |
9 | {% block add_button %}
10 |
11 | {% endblock add_button %}
12 |
13 | {% block content %}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | | Course code |
22 | Course title |
23 |
24 |
25 |
26 |
27 | {% for course in courses %}
28 |
29 | | {{course.code}} |
30 | {{course.title}} |
31 |
32 |
33 | {% empty %}
34 |
35 |
36 | | No data to display |
37 |
38 |
39 | {% endfor %}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | {% endblock content %}
48 |
49 | {% block lms_admin_javascript %}
50 |
51 |
52 |
53 |
54 | {% endblock lms_admin_javascript %}
55 |
--------------------------------------------------------------------------------
/classroom/static/classroom/js/lms_admin/lms_admins.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function () {
2 |
3 |
4 | $("#js-add-new-choice-btn").click( function () {
5 | button = $(this);
6 | $.ajax({
7 | url: button.attr("data-href"),
8 | type: 'get',
9 | dataType: 'json',
10 |
11 | success: function (data) {
12 | $("#js-add-new-choice-modal").show();
13 | $("#display-form-content").html(data.html_form);
14 | }
15 |
16 | });
17 | });
18 |
19 |
20 | $(document).on("submit","#js-new-choice-form", function (e) {
21 | e.preventDefault();
22 | form = $(this);
23 | form = form.append($("#token").find('input[name=csrfmiddlewaretoken]')[0]);
24 | $.ajax({
25 | url: form.attr("action"),
26 | type: "POST",
27 | headers: {'X-CSRFToken': getCookie('csrftoken)')},
28 | data: form.serialize(),
29 | dataType: 'json',
30 | success: function (info) {
31 | if (info.valid) {
32 | $("#js-add-new-choice-modal").hide();
33 | location.reload();
34 | }
35 | else {
36 | input = jQuery(" ");
37 | $("#token").append(input);
38 | $("#display-form-content").html(info.html_form);
39 | }
40 | }
41 | });
42 | });
43 |
44 | });
--------------------------------------------------------------------------------
/classroom/static/classroom/js/lms_admin/students.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function () {
2 |
3 |
4 | $("#js-add-new-choice-btn").click( function () {
5 | button = $(this);
6 | $.ajax({
7 | url: button.attr("data-href"),
8 | type: 'get',
9 | dataType: 'json',
10 |
11 | success: function (data) {
12 | $("#js-add-new-choice-modal").show();
13 | $("#display-form-content").html(data.html_form);
14 | }
15 |
16 | });
17 | });
18 |
19 |
20 | $(document).on("submit","#js-new-choice-form", function (e) {
21 | e.preventDefault();
22 | form = $(this);
23 | form = form.append($("#token").find('input[name=csrfmiddlewaretoken]')[0]);
24 | $.ajax({
25 | url: form.attr("action"),
26 | type: "POST",
27 | headers: {'X-CSRFToken': getCookie('csrftoken)')},
28 | data: form.serialize(),
29 | dataType: 'json',
30 | success: function (info) {
31 | if (info.valid) {
32 | $("#js-add-new-choice-modal").hide();
33 | location.reload();
34 | }
35 | else {
36 | input = jQuery(" ");
37 | $("#token").append(input);
38 | $("#display-form-content").html(info.html_form);
39 | }
40 | }
41 | });
42 | });
43 |
44 | });
--------------------------------------------------------------------------------
/classroom/static/classroom/js/lms_admin/instructors.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function () {
2 |
3 |
4 | $("#js-add-new-choice-btn").click( function () {
5 | button = $(this);
6 | $.ajax({
7 | url: button.attr("data-href"),
8 | type: 'get',
9 | dataType: 'json',
10 |
11 | success: function (data) {
12 | $("#js-add-new-choice-modal").show();
13 | $("#display-form-content").html(data.html_form);
14 | }
15 |
16 | });
17 | });
18 |
19 |
20 | $(document).on("submit","#js-new-choice-form", function (e) {
21 | e.preventDefault();
22 | form = $(this);
23 | form = form.append($("#token").find('input[name=csrfmiddlewaretoken]')[0]);
24 | $.ajax({
25 | url: form.attr("action"),
26 | type: "POST",
27 | headers: {'X-CSRFToken': getCookie('csrftoken)')},
28 | data: form.serialize(),
29 | dataType: 'json',
30 | success: function (info) {
31 | if (info.valid) {
32 | $("#js-add-new-choice-modal").hide();
33 | location.reload();
34 | }
35 | else {
36 | input = jQuery(" ");
37 | $("#token").append(input);
38 | $("#display-form-content").html(info.html_form);
39 | }
40 | }
41 | });
42 | });
43 |
44 |
45 |
46 |
47 | });
--------------------------------------------------------------------------------
/classroom/static/classroom/js/lms_admin/courses.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function () {
2 |
3 |
4 | $("#js-add-new-choice-btn").click( function () {
5 | button = $(this)
6 | $.ajax({
7 | url: button.attr("data-href"),
8 | type: 'get',
9 | dataType: 'json',
10 |
11 | success: function (data) {
12 | $("#js-add-new-choice-modal").show();
13 | $("#display-form-content").html(data.html_form);
14 | }
15 |
16 | });
17 | });
18 |
19 |
20 | $(document).on("submit","#js-new-choice-form", function (e) {
21 | e.preventDefault();
22 | form = $(this);
23 | form = form.append($("#token").find('input[name=csrfmiddlewaretoken]')[0]);
24 | $.ajax({
25 | url: form.attr("action"),
26 | type: "POST",
27 | headers: {'X-CSRFToken': getCookie('csrftoken)')},
28 | data: form.serialize(),
29 | dataType: 'json',
30 | success: function (info) {
31 | if (info.valid) {
32 | $("#js-add-new-choice-modal").hide();
33 | location.reload();
34 | }
35 | else {
36 | input = jQuery(" ");
37 | $("#token").append(input);
38 | $("#display-form-content").html(info.html_form);
39 | }
40 | }
41 | });
42 | });
43 |
44 |
45 |
46 |
47 |
48 | });
--------------------------------------------------------------------------------
/classroom/templates/classroom/lms_admin/lms_admins.html:
--------------------------------------------------------------------------------
1 | {% extends "classroom/lms_admin_base.html" %}
2 |
3 | {% load static %}
4 |
5 | {% block choice %}
6 | Admin
7 | {% endblock choice %}
8 |
9 | {% block add_button %}
10 |
11 |
12 | {% endblock add_button %}
13 |
14 | {% block content %}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | | First Name |
23 | Last Name |
24 | E-mail |
25 |
26 |
27 |
28 |
29 | {% for admin in lms_admins %}
30 |
31 | | {{admin.first_name}} |
32 | {{admin.last_name}} |
33 | {{admin.email}} |
34 |
35 |
36 | {% empty %}
37 |
38 |
39 | | No data to display |
40 |
41 |
42 | {% endfor %}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | {% endblock content %}
51 |
52 | {% block lms_admin_javascript %}
53 |
54 |
55 |
56 |
57 | {% endblock lms_admin_javascript %}
58 |
--------------------------------------------------------------------------------
/classroom/templates/classroom/lms_admin/students.html:
--------------------------------------------------------------------------------
1 | {% extends "classroom/lms_admin_base.html" %}
2 |
3 | {% load static %}
4 |
5 | {% block choice %}
6 | Students
7 | {% endblock choice %}
8 |
9 | {% block add_button %}
10 |
11 |
12 | {% endblock add_button %}
13 |
14 |
15 | {% block content %}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | | First Name |
24 | Last Name |
25 | E-mail |
26 |
27 |
28 |
29 |
30 | {% for student in students %}
31 |
32 | | {{student.first_name}} |
33 | {{student.last_name}} |
34 | {{student.email}} |
35 |
36 |
37 | {% empty %}
38 |
39 |
40 | | No data to display |
41 |
42 |
43 | {% endfor %}
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | {% endblock content %}
52 |
53 | {% block lms_admin_javascript %}
54 |
55 |
56 |
57 |
58 | {% endblock lms_admin_javascript %}
59 |
--------------------------------------------------------------------------------
/classroom/templates/classroom/lms_admin/instructors.html:
--------------------------------------------------------------------------------
1 | {% extends "classroom/lms_admin_base.html" %}
2 |
3 | {% load static %}
4 |
5 | {% block choice %}
6 | Instructors
7 | {% endblock choice %}
8 |
9 | {% block add_button %}
10 |
11 |
12 | {% endblock add_button %}
13 |
14 | {% block content %}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | | First Name |
23 | Last Name |
24 | E-mail |
25 |
26 |
27 |
28 |
29 | {% for instructor in instructors %}
30 |
31 | | {{instructor.first_name}} |
32 | {{instructor.last_name}} |
33 | {{instructor.email}} |
34 |
35 |
36 | {% empty %}
37 |
38 |
39 | | No data to display |
40 |
41 |
42 | {% endfor %}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | {% endblock content %}
51 |
52 | {% block lms_admin_javascript %}
53 |
54 |
55 |
56 |
57 | {% endblock lms_admin_javascript %}
58 |
--------------------------------------------------------------------------------
/classroom/templates/classroom/lms_admin/teaching_assistants.html:
--------------------------------------------------------------------------------
1 | {% extends "classroom/lms_admin_base.html" %}
2 |
3 | {% load static %}
4 |
5 | {% block choice %}
6 | Teaching Assistants
7 | {% endblock choice %}
8 |
9 | {% block add_button %}
10 |
11 |
12 | {% endblock add_button %}
13 |
14 | {% block content %}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | | First Name |
23 | Last Name |
24 | E-mail |
25 |
26 |
27 |
28 |
29 | {% for teaching_assistant in teaching_assistants %}
30 |
31 | | {{teaching_assistant.first_name}} |
32 | {{teaching_assistant.last_name}} |
33 | {{teaching_assistant.email}} |
34 |
35 |
36 | {% empty %}
37 |
38 |
39 | | No data to display |
40 |
41 |
42 | {% endfor %}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | {% endblock content %}
51 |
52 | {% block lms_admin_javascript %}
53 |
54 |
55 |
56 |
57 | {% endblock lms_admin_javascript %}
58 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 |
49 | # Translations
50 | *.mo
51 | *.pot
52 |
53 | # Django stuff:
54 | *.log
55 | local_settings.py
56 |
57 | # Flask stuff:
58 | instance/
59 | .webassets-cache
60 |
61 | # Scrapy stuff:
62 | .scrapy
63 |
64 | # Sphinx documentation
65 | docs/_build/
66 |
67 | # PyBuilder
68 | target/
69 |
70 | # Jupyter Notebook
71 | .ipynb_checkpoints
72 |
73 | # pyenv
74 | .python-version
75 |
76 | # celery beat schedule file
77 | celerybeat-schedule
78 |
79 | # SageMath parsed files
80 | *.sage.py
81 |
82 | # dotenv
83 | .env
84 |
85 | # virtualenv
86 | .venv
87 | venv/
88 | ENV/
89 |
90 | # Spyder project settings
91 | .spyderproject
92 | .spyproject
93 |
94 | # Rope project settings
95 | .ropeproject
96 |
97 | # mkdocs documentation
98 | /site
99 |
100 | # mypy
101 | .mypy_cache/
102 |
103 | #*.sqlite3
104 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Learning Management System
2 |
3 | A basic but scalable management system that can be used in a university
4 |
5 | ## Introduction
6 |
7 | The system is made up of 3 major users: admin, instructors and students. Each user has different level of access or permissions.
8 |
9 | ### Prerequisites
10 |
11 | [](https://python.org)
12 | [](https://djangoproject.com)
13 | [](http://virtualenvwrapper.readthedocs.io/en/latest/install.html)
14 |
15 |
16 |
17 | ### Running the project locally
18 |
19 | First clone the repository to your local machine
20 |
21 | ```
22 | $ git clone https://github.com/Akohrr/Learning-Management-System.git
23 | ```
24 |
25 | Change your directory
26 |
27 | ```
28 | $ cd lms
29 | ```
30 |
31 | create a virtual environment
32 |
33 | ```
34 | $ mkvirtualenv django-lms
35 | ```
36 |
37 | To activate the virtual environment
38 |
39 | ```
40 | $ workon django-lms
41 | ```
42 |
43 | Install the dependencies
44 |
45 | ```
46 | $ pip install -r requirements.txt
47 | ```
48 |
49 | Finally, run the development server
50 |
51 | ```
52 | $ python manage.py runserver
53 | ```
54 |
55 | The project would be available at 127.0.0.1:8000
56 |
57 | ## Running the tests
58 |
59 | Use the details below to login and access the different features and roles of the system
60 |
61 | | username | password | Role |
62 | |:------------:|:-----------:|-----------:|
63 | | kdutchburn2 | randompass | Admin |
64 | | bosheilds1u | randompass | Admin |
65 | | cschachter98 | randompass | Admin |
66 | | aashurst48 | randompass | Instructor |
67 | | acolbyea | randompass | Instructor |
68 | | aedger6j | randompass | Instructor |
69 | | abasond5 | randompass | Student |
70 | | aberny | randompass | Student |
71 | | adeverale8y | randompass | Student |
72 |
73 | login to the system using one of the login credentials in the table above
74 |
--------------------------------------------------------------------------------
/classroom/templates/classroom/instructor/quizzes.html:
--------------------------------------------------------------------------------
1 | {% extends "classroom/instructor_base.html" %}
2 | {% load static %}
3 |
4 | {% block choice %}
5 | Quiz
6 | {% endblock choice %}
7 |
8 | {% block add_button %}
9 |
10 | {% endblock add_button %}
11 |
12 | {% block content %}
13 |
14 |
15 |
16 | {% for quiz in quizzes %}
17 |
18 |
19 | {% for question in questions %}
20 |
21 | {% if quiz.name == question.quiz_or_assignment.name %}
22 |
{{ question |truncatechars:30 }}
23 | {% endif %}
24 |
25 | {% empty %}
26 |
No question(s)
27 | {% endfor %}
28 |
29 |
30 |
31 |
32 |
33 |
34 | {% empty %}
35 |
36 |
39 | {% endfor %}
40 |
41 |
42 |
43 |
44 |
45 |
46 | {% endblock content %}
47 |
48 | {% block instructor_javascript %}
49 |
50 |
51 |
52 |
53 | {% endblock instructor_javascript %}
54 |
--------------------------------------------------------------------------------
/classroom/static/classroom/js/instructor/quizzes.js:
--------------------------------------------------------------------------------
1 | function accordionFunction(id) {
2 | var x = document.getElementById(id);
3 | if (x.className.indexOf("w3-show") == -1) {
4 | x.className += " w3-show";
5 | } else {
6 | x.className = x.className.replace(" w3-show", "");
7 | }
8 | }
9 |
10 |
11 |
12 | $("#js-close-quiz-modal").click( function () {
13 | $("#quiz-modal").hide();
14 | });
15 |
16 |
17 | // $("#js-add-new-choice-btn").click( function () {
18 | // button = $(this);
19 | // $.ajax({
20 | // url: button.attr("data-href"),
21 | // type: 'get',
22 | // dataType: 'json',
23 |
24 | // success: function (data) {
25 | // $("#js-add-new-choice-modal").show();
26 | // $("#display-form-content").html(data.html_form);
27 | // }
28 |
29 | // });
30 | // });
31 |
32 |
33 | // $(document).on("submit","#js-new-choice-form", function (e) {
34 | // e.preventDefault();
35 | // form = $(this);
36 | // form = form.append($("#token").find('input[name=csrfmiddlewaretoken]')[0]);
37 | // $.ajax({
38 | // url: form.attr("action"),
39 | // type: "POST",
40 | // data: form.serialize(),
41 | // dataType: 'json',
42 | // success: function (info) {
43 | // if (info.valid) {
44 | // $("#js-add-new-choice-modal").hide();
45 | // location.reload();
46 | // }
47 | // else {
48 | // input = jQuery(" ");
49 | // $("#token").append(input);
50 | // $("#display-form-content").html(info.html_form);
51 | // }
52 | // }
53 | // });
54 | // });
55 |
56 |
57 | $(".js-add-question-quiz").click( function () {
58 | button = $(this);
59 | $.ajax({
60 | url: button.attr("data-href"),
61 | type: 'get',
62 | dataType: 'json',
63 |
64 | success: function (data) {
65 | $("#js-add-new-choice-modal").show();
66 | $("#display-form-content").html(data.html_form);
67 | }
68 |
69 | });
70 | });
71 |
72 |
73 |
--------------------------------------------------------------------------------
/classroom/migrations/0002_auto_20180521_1519.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11 on 2018-05-21 15:19
3 | from __future__ import unicode_literals
4 |
5 | from django.conf import settings
6 | from django.db import migrations, models
7 | import django.db.models.deletion
8 |
9 |
10 | class Migration(migrations.Migration):
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ('classroom', '0001_initial'),
15 | ]
16 |
17 | operations = [
18 | migrations.AlterField(
19 | model_name='assignment',
20 | name='created_by',
21 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
22 | ),
23 | migrations.AlterField(
24 | model_name='course',
25 | name='grade',
26 | field=models.CharField(blank=True, choices=[('A', 'A(80-100)'), ('B', 'B(70-79)'), ('C', 'C(60-69)'), ('D', 'D(50-59)'), ('E', 'E(40-49)'), ('F', 'F(30-39)')], max_length=1, null=True),
27 | ),
28 | migrations.RemoveField(
29 | model_name='course',
30 | name='instructors',
31 | ),
32 | migrations.AddField(
33 | model_name='course',
34 | name='instructors',
35 | field=models.ManyToManyField(related_name='course_instructor', to=settings.AUTH_USER_MODEL),
36 | ),
37 | migrations.RemoveField(
38 | model_name='course',
39 | name='students',
40 | ),
41 | migrations.AddField(
42 | model_name='course',
43 | name='students',
44 | field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
45 | ),
46 | migrations.AlterField(
47 | model_name='discussion',
48 | name='created_by',
49 | field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
50 | ),
51 | migrations.AlterField(
52 | model_name='quiz',
53 | name='created_by',
54 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
55 | ),
56 | ]
57 |
--------------------------------------------------------------------------------
/classroom/templates/classroom/student/discussions.html:
--------------------------------------------------------------------------------
1 | {% extends "classroom/instructor_base.html" %}
2 | {% load static %}
3 |
4 | {% block choice %}
5 | Discussions
6 | {% endblock choice %}
7 |
8 | {% block add_button %}
9 |
10 | {% endblock add_button %}
11 |
12 | {% block content %}
13 |
14 |
15 |
Click on the discussion to view the comments
16 |
17 | {% for discussion in discussions %}
18 |
19 |
34 |
35 | {% empty %}
36 |
37 |
40 | {% endfor %}
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | {% endblock content %}
50 |
51 | {% block instructor_javascript %}
52 |
53 |
54 |
55 |
56 | {% endblock instructor_javascript %}
57 |
--------------------------------------------------------------------------------
/classroom/templates/classroom/instructor/assignments.html:
--------------------------------------------------------------------------------
1 | {% extends "classroom/instructor_base.html" %}
2 | {% load static %}
3 |
4 | {% block choice %}
5 | Assignment
6 | {% endblock choice %}
7 |
8 | {% block add_button %}
9 |
10 | {% endblock add_button %}
11 |
12 | {% block content %}
13 |
14 |
15 |
16 | {% for assignment in assignments %}
17 |
18 |
19 | {% for question in questions %}
20 |
21 | {% if assignment.name == question.quiz_or_assignment.name %}
22 |
{{ question |truncatechars:30 }}
23 | {% endif %}
24 |
25 | {% empty %}
26 |
No question(s)
27 | {% endfor %}
28 |
29 |
30 |
31 |
32 |
33 |
34 | {% empty %}
35 |
36 |
39 | {% endfor %}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | {% endblock content %}
49 |
50 | {% block instructor_javascript %}
51 |
52 |
53 |
54 |
55 | {% endblock instructor_javascript %}
56 |
--------------------------------------------------------------------------------
/classroom/templates/classroom/instructor/discussions.html:
--------------------------------------------------------------------------------
1 | {% extends "classroom/instructor_base.html" %}
2 | {% load static %}
3 |
4 | {% block choice %}
5 | Discussion
6 | {% endblock choice %}
7 |
8 | {% block add_button %}
9 |
10 | {% endblock add_button %}
11 |
12 | {% block content %}
13 |
14 |
15 |
16 |
Click on the discussion to view the comments
17 |
18 | {% for discussion in discussions %}
19 |
20 |
35 |
36 | {% empty %}
37 |
38 |
41 | {% endfor %}
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | {% endblock content %}
51 |
52 | {% block instructor_javascript %}
53 |
54 |
55 |
56 |
57 | {% endblock instructor_javascript %}
58 |
--------------------------------------------------------------------------------
/classroom/migrations/0007_auto_20180529_0922.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11 on 2018-05-29 09:22
3 | from __future__ import unicode_literals
4 |
5 | from django.conf import settings
6 | from django.db import migrations, models
7 | import django.db.models.deletion
8 |
9 |
10 | class Migration(migrations.Migration):
11 |
12 | dependencies = [
13 | ('classroom', '0006_auto_20180528_1140'),
14 | ]
15 |
16 | operations = [
17 | migrations.AddField(
18 | model_name='question',
19 | name='answer',
20 | field=models.TextField(default='default answer', help_text='Copy the text of the correct option and past it here'),
21 | preserve_default=False,
22 | ),
23 | migrations.AddField(
24 | model_name='question',
25 | name='first_option',
26 | field=models.TextField(default='first_option'),
27 | preserve_default=False,
28 | ),
29 | migrations.AddField(
30 | model_name='question',
31 | name='fourth_option',
32 | field=models.TextField(default='fourth option'),
33 | preserve_default=False,
34 | ),
35 | migrations.AddField(
36 | model_name='question',
37 | name='second_option',
38 | field=models.TextField(default='second option'),
39 | preserve_default=False,
40 | ),
41 | migrations.AddField(
42 | model_name='question',
43 | name='third_option',
44 | field=models.TextField(default='ak'),
45 | preserve_default=False,
46 | ),
47 | migrations.AlterField(
48 | model_name='question',
49 | name='quiz',
50 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='classroom.Quiz'),
51 | ),
52 | migrations.AlterField(
53 | model_name='quiz',
54 | name='course',
55 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='classroom.Course'),
56 | ),
57 | migrations.AlterField(
58 | model_name='quiz',
59 | name='name',
60 | field=models.CharField(max_length=255, unique=True),
61 | ),
62 | migrations.AlterField(
63 | model_name='quiz',
64 | name='owner',
65 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
66 | ),
67 | ]
68 |
--------------------------------------------------------------------------------
/classroom/templates/classroom/instructor_base.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load static %}
3 | {% block boilerplate_html %}
4 |
5 |
6 |
7 |
24 |
25 |
26 |
27 |
28 |
Learning Management System portal
29 |
30 |
31 |
32 |
33 |
34 |
{% block choice %}
35 |
36 | {% endblock choice %}
37 |
38 |
39 | {% block add_button %}
40 |
41 | {% endblock add_button %}
42 |
43 | {% csrf_token %}
44 |
45 |
46 |
47 |
×
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | {% block content %}
58 |
59 | {% endblock content %}
60 |
61 |
62 |
63 |
64 |
65 | {% endblock boilerplate_html %}
66 |
67 | {% block javascript %}
68 |
69 |
70 |
71 |
72 | {% block instructor_javascript %}
73 |
74 | {% endblock instructor_javascript %}
75 |
76 |
77 | {% endblock javascript %}
--------------------------------------------------------------------------------
/classroom/templates/classroom/lms_admin_base.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load static %}
3 | {% block boilerplate_html %}
4 |
5 |
6 |
7 |
24 |
25 |
26 |
27 |
28 |
Learning Management System portal
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
{% block choice %}
37 |
38 | {% endblock choice %}
39 |
40 |
41 | {% block add_button %}
42 |
43 |
44 | {% endblock add_button %}
45 |
46 | {% csrf_token %}
47 |
48 |
49 |
50 |
×
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | {% block content %}
61 |
62 | {% endblock content %}
63 |
64 |
65 |
66 |
67 |
68 | {% endblock boilerplate_html %}
69 |
70 | {% block javascript %}
71 |
72 |
73 |
74 |
75 | {% block lms_admin_javascript %}
76 |
77 | {% endblock lms_admin_javascript %}
78 |
79 |
80 | {% endblock javascript %}
81 |
82 |
--------------------------------------------------------------------------------
/classroom/templates/classroom/student_base.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load static %}
3 | {% block boilerplate_html %}
4 |
5 | {% block css %}
6 |
7 | {% endblock css %}
8 |
9 |
26 |
27 |
28 |
29 |
30 |
Learning Management System portal
31 |
32 |
33 |
34 |
35 |
36 |
{% block choice %}
37 |
38 | {% endblock choice %}
39 |
40 |
41 | {% block add_button %}
42 |
43 | {% endblock add_button %}
44 |
45 | {% csrf_token %}
46 |
47 |
48 |
49 |
×
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | {% block content %}
60 |
61 | {% endblock content %}
62 |
63 |
64 |
65 |
66 |
67 | {% endblock boilerplate_html %}
68 |
69 | {% block javascript %}
70 |
71 |
72 |
73 |
74 | {% block student_javascript %}
75 |
76 | {% endblock student_javascript %}
77 |
78 |
79 | {% endblock javascript %}
--------------------------------------------------------------------------------
/classroom/static/classroom/js/student/assignments.js:
--------------------------------------------------------------------------------
1 | function countDown (time_of_submission) {
2 | // Set the date we're counting down to
3 | var countDownDate = new Date(String(time_of_submission)).getTime();
4 |
5 | // Update the count down every 1 second
6 | var x = setInterval(function() {
7 |
8 | // Get todays date and time
9 | var now = new Date().getTime();
10 |
11 | // Find the distance between now an the count down date
12 | var distance = countDownDate - now;
13 |
14 | // Time calculations for days, hours, minutes and seconds
15 | var days = Math.floor(distance / (1000 * 60 * 60 * 24));
16 | var hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
17 | var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
18 | var seconds = Math.floor((distance % (1000 * 60)) / 1000);
19 |
20 | // Output the result in an element with id="demo"
21 | document.getElementById("answer-countdown").innerHTML = days + "d " + hours + "h "
22 | + minutes + "m " + seconds + "s ";
23 |
24 | // If the count down is over, write some text
25 | if (distance < 0) {
26 | clearInterval(x);
27 | document.getElementById("answer-countdown").innerHTML = "EXPIRED";
28 | $("#js-question-modal").hide();
29 | location.reload();
30 | }
31 | }, 1000);
32 | }
33 |
34 | $('.js-answer-assignment').click( function () {
35 | row = $(this);
36 | $.ajax({
37 | url: row.attr("data-href"),
38 | type: 'get',
39 | dataType: 'json',
40 |
41 | success: function (data) {
42 | $("#js-question-modal").show();
43 | console.log('akoh');
44 | $("#display-form-content").html(data.html_form);
45 | time_of_submission = $("#answer-countdown").html();
46 | countDown(time_of_submission);
47 |
48 | },
49 | error: function(xhr) { // if error occured
50 | swal("Please wait", "No questions have been added to the assignment. Please check back later", "info");
51 |
52 | },
53 |
54 | });
55 | });
56 |
57 | $(document).on("submit","#js-question-form", function (e) {
58 | e.preventDefault();
59 | form = $(this);
60 | form = form.append($("#token").find('input[name=csrfmiddlewaretoken]')[0]);
61 | $.ajax({
62 | url: form.attr("action"),
63 | type: "POST",
64 | data: form.serialize(),
65 | dataType: 'json',
66 | success: function (info) {
67 | if (info.submitted_successfully) {
68 | $("#js-question-modal").hide();
69 | location.reload();
70 | }else if(info.already_submitted){
71 | console.log(info.already_submitted)
72 | console.log('no me p')
73 | $("#js-question-modal").hide();
74 | swal("You are only allowed to submit once", "Submitted assignments are graded automatically after submission", "info");
75 | } else {
76 | input = jQuery(" ");
77 | $("#token").append(input);
78 | $("#display-form-content").html(info.html_form);
79 | }
80 | }
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/classroom/templates/classroom/student/quizzes.html:
--------------------------------------------------------------------------------
1 | {% extends "classroom/student_base.html" %}
2 |
3 | {% load static %}
4 |
5 | {% block choice %}
6 | Quizzes
7 | List of all quizzes
8 | {% endblock choice %}
9 |
10 | {% block add_button %}
11 |
12 | {% endblock add_button %}
13 |
14 | {% block content %}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
Pending quizzes
26 |
27 |
28 |
29 |
30 |
31 |
32 | | Quiz name |
33 | Course |
34 | Date of submission |
35 |
36 |
37 |
38 |
39 | {% for quiz in pending_quizzes %}
40 |
41 | | {{quiz.name}} |
42 | {{quiz.course.code}} |
43 | {{quiz.date_of_submission}} |
44 |
45 |
46 | {% empty %}
47 |
48 |
49 | | No data to display |
50 |
51 |
52 | {% endfor %}
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
Submitted quizzes
61 |
62 |
63 |
64 |
65 |
66 | | Assignment name |
67 | Course |
68 | Date of submission |
69 |
70 |
71 |
72 |
73 | {% for quiz in submitted_quizzes %}
74 |
75 | | {{quiz.name}} |
76 | {{quiz.course.code}} |
77 | {{quiz.date_of_submission}} |
78 |
79 |
80 | {% empty %}
81 |
82 |
83 | | No data to display |
84 |
85 |
86 | {% endfor %}
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | {% endblock content %}
98 |
99 | {% block student_javascript %}
100 |
101 |
102 |
103 | {% endblock student_javascript %}
104 |
--------------------------------------------------------------------------------
/classroom/templates/classroom/student/assignments.html:
--------------------------------------------------------------------------------
1 | {% extends "classroom/student_base.html" %}
2 |
3 | {% load static %}
4 |
5 | {% block choice %}
6 | Assignments
7 | List of all the assignments
8 | {% endblock choice %}
9 |
10 | {% block add_button %}
11 |
12 | {% endblock add_button %}
13 |
14 | {% block content %}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
Pending assignments
26 |
27 |
28 |
29 |
30 |
31 |
32 | | Assignment name |
33 | Course |
34 | Date of submission |
35 |
36 |
37 |
38 |
39 | {% for assignment in pending_assignments %}
40 |
41 | | {{assignment.name}} |
42 | {{assignment.course.code}} |
43 | {{assignment.date_of_submission}} |
44 |
45 |
46 | {% empty %}
47 |
48 |
49 | | No data to display |
50 |
51 |
52 | {% endfor %}
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
Submitted assignments
61 |
62 |
63 |
64 |
65 |
66 | | Assignment name |
67 | Course |
68 | Date of submission |
69 |
70 |
71 |
72 |
73 | {% for assignment in submitted_assignments %}
74 |
75 | | {{assignment.name}} |
76 | {{assignment.course.code}} |
77 | {{assignment.date_of_submission}} |
78 |
79 |
80 | {% empty %}
81 |
82 |
83 | | No data to display |
84 |
85 |
86 | {% endfor %}
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | {% endblock content %}
98 |
99 | {% block student_javascript %}
100 |
101 |
102 |
103 |
104 | {% endblock student_javascript %}
105 |
--------------------------------------------------------------------------------
/classroom/static/classroom/js/main.js:
--------------------------------------------------------------------------------
1 | // using jQuery to obtain csrf tokens from cookies
2 | function getCookie(name) {
3 | var cookieValue = null;
4 | if (document.cookie && document.cookie !== '') {
5 | var cookies = document.cookie.split(';');
6 | for (var i = 0; i < cookies.length; i++) {
7 | var cookie = jQuery.trim(cookies[i]);
8 | // Does this cookie string begin with the name we want?
9 | if (cookie.substring(0, name.length + 1) === (name + '=')) {
10 | cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
11 | break;
12 | }
13 | }
14 | }
15 | return cookieValue;
16 | }
17 |
18 |
19 | function accordionFunction(id) {
20 | var x = document.getElementById(id);
21 | if (x.className.indexOf("w3-show") == -1) {
22 | x.className += " w3-show";
23 | } else {
24 | x.className = x.className.replace(" w3-show", "");
25 | }
26 | }
27 |
28 |
29 | function openStatus(evt, statusName) {
30 | var i, x, tablinks;
31 | x = document.getElementsByClassName("status");
32 | for (i = 0; i < x.length; i++) {
33 | x[i].style.display = "none";
34 | }
35 | tablinks = document.getElementsByClassName("tablink");
36 | for (i = 0; i < x.length; i++) {
37 | tablinks[i].className = tablinks[i].className.replace(" w3-teal", "");
38 | }
39 | document.getElementById(statusName).style.display = "block";
40 | evt.currentTarget.className += " w3-teal";
41 | }
42 |
43 |
44 | $("#js-close-new-choice-modal").click( function () {
45 | $("#js-add-new-choice-modal").hide();
46 | });
47 |
48 | $("#js-close-question-modal").click( function () {
49 | $("#js-question-modal").hide();
50 | });
51 |
52 | $("#js-add-new-choice-btn").click( function () {
53 | button = $(this);
54 | $.ajax({
55 | url: button.attr("data-href"),
56 | type: 'get',
57 | dataType: 'json',
58 |
59 | success: function (data) {
60 | $("#js-add-new-choice-modal").show();
61 | $("#display-form-content").html(data.html_form);
62 | }
63 |
64 | });
65 | });
66 |
67 |
68 |
69 | $(document).on("submit","#js-new-choice-form", function (e) {
70 | e.preventDefault();
71 | form = $(this);
72 | form = form.append($("#token").find('input[name=csrfmiddlewaretoken]')[0]);
73 | $.ajax({
74 | url: form.attr("action"),
75 | type: "POST",
76 | data: form.serialize(),
77 | dataType: 'json',
78 | success: function (info) {
79 | if (info.valid) {
80 | $("#js-add-new-choice-modal").hide();
81 | location.reload();
82 | } else {
83 | input = jQuery(" ");
84 | $("#token").append(input);
85 | $("#display-form-content").html(info.html_form);
86 | }
87 | }
88 | });
89 | });
90 |
91 |
92 |
93 | $(document).ready(function () {
94 | $('table.w3-table').DataTable({
95 | language: {
96 | emptyTable: "", //
97 | loadingRecords: "Please wait .. ", // default Loading...
98 | zeroRecords: ""
99 | }
100 | });
101 | });
102 |
103 |
--------------------------------------------------------------------------------
/classroom/views/lms_admin.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | from django.shortcuts import render, redirect
4 | from django.views.generic import CreateView, TemplateView, ListView
5 | from .. import forms
6 | from django.template.loader import render_to_string
7 | from accounts.models import User
8 | from django.http import JsonResponse, Http404
9 | from django.contrib.auth.models import Group
10 | from django.views.generic.edit import FormMixin
11 | from django.contrib.auth.mixins import UserPassesTestMixin
12 | from .. models import Course
13 | from django.core.exceptions import PermissionDenied
14 |
15 |
16 | class TestLMSAdmin(UserPassesTestMixin):
17 |
18 | def test_func(self):
19 | if not self.request.user.is_authenticated:
20 | # This will redirect to the 403 page
21 | raise PermissionDenied
22 | if not self.request.user.groups.filter(name='Admin Role').exists():
23 | # Redirect the user to 403 page
24 | raise PermissionDenied
25 | return self.dispatch
26 |
27 |
28 | class ChoiceList(TestLMSAdmin, ListView):
29 |
30 | def get_context_object_name(self, object_list):
31 | object_name = self.kwargs['choice']
32 | return object_name
33 |
34 | def get_queryset(self):
35 | choice = self.kwargs['choice']
36 | user_type = {
37 | 'lms_admins': 'LA',
38 | 'instructors': 'IN',
39 | 'students': 'ST',
40 | }
41 | if choice in user_type:
42 | queryset = User.objects.filter(user_type=user_type[choice])
43 |
44 | elif choice == 'courses':
45 | queryset = Course.objects.all()
46 | else:
47 | raise Http404
48 |
49 | return queryset
50 |
51 | def get_template_names(self):
52 | template = {
53 | 'lms_admins': r'classroom/lms_admin/lms_admins.html',
54 | 'instructors': r'classroom/lms_admin/instructors.html',
55 | 'students': r'classroom/lms_admin/students.html',
56 | 'courses': r'classroom/lms_admin/courses.html',
57 | }[self.kwargs['choice']]
58 | return [template]
59 |
60 |
61 | # view used to handle creation of lms_admin, instructors, students, courses
62 | class SignUpView(TestLMSAdmin, CreateView):
63 | info = dict()
64 | model = User
65 |
66 | def get_form(self, form_class=None):
67 | choice = self.kwargs['choice']
68 | form = {
69 | 'admin': forms.LMSAdminSignUpForm,
70 | 'instructor': forms.InstructorSignUpForm,
71 | 'student': forms.StudentSignUpForm,
72 | 'course': forms.CourseForm,
73 | }[choice]
74 | return form(**self.get_form_kwargs())
75 |
76 | def get(self, request, *args, **kwargs):
77 | choice = self.kwargs['choice']
78 | form = self.get_form()
79 | path = request.META.get('PATH_INFO')
80 | context = {'form': form, 'choice': choice.title(), 'path': path}
81 | self.info['html_form'] = render_to_string(
82 | 'classroom/includes/new_form_modal.html', context)
83 | return JsonResponse(self.info)
84 |
85 | def form_valid(self, form):
86 | form.save()
87 | self.info['valid'] = True
88 | return JsonResponse(self.info)
89 |
90 | def form_invalid(self, form):
91 | path = self.request.META.get('PATH_INFO')
92 | context = {'form': form,
93 | 'choice': self.kwargs['choice'].title(), 'path': path}
94 | self.info['valid'] = False
95 | self.info['html_form'] = render_to_string(
96 | 'classroom/includes/new_form_modal.html', context)
97 | return JsonResponse(self.info)
98 |
--------------------------------------------------------------------------------
/classroom/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.conf import settings
3 | from django.core.urlresolvers import reverse
4 | # Create your models here.
5 |
6 |
7 | class Module(models.Model):
8 | name = models.CharField(max_length=30)
9 | text = models.TextField()
10 | class_file = models.FileField()
11 |
12 | def __str__(self):
13 | return self.name
14 |
15 |
16 |
17 | #modules refers to content of the course
18 | class Course(models.Model):
19 | title = models.CharField(max_length=255)
20 | code = models.CharField(max_length=6, unique=True, primary_key=True)
21 | instructors = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='instructors')
22 | syllabus = models.TextField()
23 | modules = models.ManyToManyField(Module)
24 | students = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='students')
25 | teaching_assistants = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='teaching_assistant', blank=True)
26 |
27 |
28 | def __str__(self):
29 | return '{0}'.format(self.code)
30 |
31 |
32 |
33 |
34 | #used to handle both quiz and assignments
35 | class QuizOrAssignment(models.Model):
36 | owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
37 | name = models.CharField(max_length=255, unique=True)
38 | course = models.ForeignKey(Course, on_delete=models.CASCADE)
39 | date_of_submission = models.DateTimeField(help_text='Date and time of submission')
40 | is_assignment = models.BooleanField(default=False)
41 |
42 | def __str__(self):
43 | return self.name
44 |
45 | def get_absolute_url(self):
46 | return reverse('classroom:instructor_add_choice', kwargs={'choice':'quiz', 'pk': self.pk })
47 |
48 | class Meta:
49 | permissions = (
50 | ('modify_quiz_or_assignment', 'Modify quiz or assignment'),
51 | )
52 |
53 |
54 | class Question(models.Model):
55 | quiz_or_assignment = models.ForeignKey(QuizOrAssignment, on_delete=models.CASCADE)
56 | text = models.TextField('Question')
57 | first_option = models.TextField('A')
58 | second_option = models.TextField('B')
59 | third_option = models.TextField('C')
60 | fourth_option = models.TextField('D')
61 | answer = models.TextField()
62 | def __str__(self):
63 | return self.text
64 |
65 |
66 | class Grade(models.Model):
67 | score = models.DecimalField(default=0, decimal_places=2, max_digits=100, help_text='Score is expressed in percentage')
68 | quiz_or_assignment = models.ForeignKey(QuizOrAssignment, on_delete=models.CASCADE, null=True, default=None)
69 | course = models.ForeignKey(Course, on_delete=models.CASCADE)
70 | student = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
71 |
72 | def __str__(self):
73 | return self.student.username
74 |
75 | class Discussion(models.Model):
76 | title = models.CharField(max_length=30)
77 | created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
78 | course = models.ForeignKey(Course, on_delete=models.CASCADE, null=True)
79 |
80 | def __str__(self):
81 | return self.title
82 |
83 | def get_absolute_url(self):
84 | #choice represents discussion
85 | return reverse('classroom:instructor_add_choice', kwargs={'choice':'discussion', 'pk': self.pk })
86 |
87 |
88 | class Comment(models.Model):
89 | discussion = models.ForeignKey(Discussion, on_delete=models.CASCADE, default=None, null=True)
90 | created = models.DateTimeField(auto_now=True)
91 | body = models.TextField()
92 | author = models.ForeignKey(settings.AUTH_USER_MODEL)
93 |
94 | def __str__(self):
95 | return self.body
--------------------------------------------------------------------------------
/accounts/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11 on 2018-05-21 13:58
3 | from __future__ import unicode_literals
4 |
5 | from django.conf import settings
6 | import django.contrib.auth.models
7 | import django.contrib.auth.validators
8 | from django.db import migrations, models
9 | import django.db.models.deletion
10 | import django.utils.timezone
11 |
12 |
13 | class Migration(migrations.Migration):
14 |
15 | initial = True
16 |
17 | dependencies = [
18 | ('auth', '0008_alter_user_username_max_length'),
19 | ]
20 |
21 | operations = [
22 | migrations.CreateModel(
23 | name='User',
24 | fields=[
25 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
26 | ('password', models.CharField(max_length=128, verbose_name='password')),
27 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
28 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
29 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
30 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
31 | ('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')),
32 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
33 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
34 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
35 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
36 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
37 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
38 | ],
39 | options={
40 | 'verbose_name': 'user',
41 | 'verbose_name_plural': 'users',
42 | 'abstract': False,
43 | },
44 | managers=[
45 | ('objects', django.contrib.auth.models.UserManager()),
46 | ],
47 | ),
48 | migrations.CreateModel(
49 | name='Instructor',
50 | fields=[
51 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
52 | ('instructor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
53 | ],
54 | ),
55 | migrations.CreateModel(
56 | name='LMSAdmin',
57 | fields=[
58 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
59 | ('admin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
60 | ],
61 | ),
62 | migrations.CreateModel(
63 | name='Student',
64 | fields=[
65 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
66 | ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
67 | ],
68 | ),
69 | ]
70 |
--------------------------------------------------------------------------------
/classroom/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11 on 2018-05-21 13:58
3 | from __future__ import unicode_literals
4 |
5 | from django.conf import settings
6 | from django.db import migrations, models
7 | import django.db.models.deletion
8 |
9 |
10 | class Migration(migrations.Migration):
11 |
12 | initial = True
13 |
14 | dependencies = [
15 | ('accounts', '0001_initial'),
16 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
17 | ]
18 |
19 | operations = [
20 | migrations.CreateModel(
21 | name='Assignment',
22 | fields=[
23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
24 | ('created', models.DateTimeField(auto_now=True)),
25 | ],
26 | ),
27 | migrations.CreateModel(
28 | name='Comments',
29 | fields=[
30 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
31 | ('created', models.DateTimeField(auto_now=True)),
32 | ('author', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
33 | ],
34 | ),
35 | migrations.CreateModel(
36 | name='Content',
37 | fields=[
38 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
39 | ('text', models.TextField()),
40 | ('class_file', models.FileField(upload_to='')),
41 | ],
42 | ),
43 | migrations.CreateModel(
44 | name='Course',
45 | fields=[
46 | ('title', models.TextField()),
47 | ('code', models.CharField(max_length=6, primary_key=True, serialize=False, unique=True)),
48 | ('grade', models.CharField(blank=True, choices=[(1, 'A'), (2, 'B'), (3, 'C'), (4, 'D'), (5, 'E'), (6, 'F')], max_length=1, null=True)),
49 | ('syllabus', models.TextField()),
50 | ('content', models.ManyToManyField(to='classroom.Content')),
51 | ('instructors', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.Instructor')),
52 | ('students', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.Student')),
53 | ],
54 | ),
55 | migrations.CreateModel(
56 | name='Discussion',
57 | fields=[
58 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
59 | ('title', models.CharField(max_length=30)),
60 | ('comments', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='classroom.Comments')),
61 | ('created_by', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='accounts.Instructor')),
62 | ],
63 | ),
64 | migrations.CreateModel(
65 | name='Quiz',
66 | fields=[
67 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
68 | ('created', models.DateTimeField(auto_now=True)),
69 | ('comments', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='classroom.Comments')),
70 | ('content', models.ManyToManyField(to='classroom.Content')),
71 | ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.Instructor')),
72 | ],
73 | ),
74 | migrations.AddField(
75 | model_name='assignment',
76 | name='comments',
77 | field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='classroom.Comments'),
78 | ),
79 | migrations.AddField(
80 | model_name='assignment',
81 | name='content',
82 | field=models.ManyToManyField(to='classroom.Content'),
83 | ),
84 | migrations.AddField(
85 | model_name='assignment',
86 | name='created_by',
87 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.Instructor'),
88 | ),
89 | ]
90 |
--------------------------------------------------------------------------------
/lms/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for lms project.
3 |
4 | Generated by 'django-admin startproject' using Django 1.11.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.11/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/1.11/ref/settings/
11 | """
12 |
13 | import os
14 |
15 | import dj_database_url
16 | from django.core.urlresolvers import reverse_lazy
17 |
18 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
19 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
20 |
21 |
22 | # Quick-start development settings - unsuitable for production
23 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
24 |
25 | # SECURITY WARNING: keep the secret key used in production secret!
26 | SECRET_KEY = '^vibz^7+fm5ee+fbvt)-fo-@0s)ehj_258y()gjx#j(s*5-izf'
27 |
28 | # SECURITY WARNING: don't run with debug turned on in production!
29 | DEBUG = True
30 |
31 | ALLOWED_HOSTS = ['*']
32 |
33 |
34 | # Application definition
35 |
36 | INSTALLED_APPS = [
37 | 'django.contrib.auth',
38 | 'django.contrib.contenttypes',
39 | 'django.contrib.sessions',
40 | 'django.contrib.messages',
41 | 'django.contrib.staticfiles',
42 |
43 | #local apps
44 | 'accounts',
45 | 'classroom',
46 |
47 | #3rd party apps
48 | 'widget_tweaks', #widget_tweaks: use of third party app to render forms manually
49 |
50 | #admin app
51 | 'django.contrib.admin',
52 |
53 | ]
54 |
55 | MIDDLEWARE = [
56 | 'django.middleware.security.SecurityMiddleware',
57 | 'django.contrib.sessions.middleware.SessionMiddleware',
58 | 'django.middleware.common.CommonMiddleware',
59 | 'django.middleware.csrf.CsrfViewMiddleware',
60 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
61 | 'django.contrib.messages.middleware.MessageMiddleware',
62 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
63 | ]
64 |
65 | ROOT_URLCONF = 'lms.urls'
66 |
67 | TEMPLATES = [
68 | {
69 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
70 | 'DIRS': [
71 | os.path.join(BASE_DIR, 'templates'),
72 |
73 | ],
74 | 'APP_DIRS': True,
75 | 'OPTIONS': {
76 | 'context_processors': [
77 | 'django.template.context_processors.debug',
78 | 'django.template.context_processors.request',
79 | 'django.contrib.auth.context_processors.auth',
80 | 'django.contrib.messages.context_processors.messages',
81 | ],
82 | },
83 | },
84 | ]
85 |
86 | WSGI_APPLICATION = 'lms.wsgi.application'
87 |
88 |
89 | # Database
90 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases
91 |
92 | DATABASES = {
93 | 'default': {
94 | 'ENGINE': 'django.db.backends.sqlite3',
95 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
96 | }
97 | }
98 |
99 |
100 | # Password validation
101 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
102 |
103 | AUTH_PASSWORD_VALIDATORS = [
104 | {
105 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
106 | },
107 | {
108 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
109 | },
110 | {
111 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
112 | },
113 | {
114 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
115 | },
116 | ]
117 |
118 |
119 | # Internationalization
120 | # https://docs.djangoproject.com/en/1.11/topics/i18n/
121 |
122 | LANGUAGE_CODE = 'en-us'
123 |
124 | TIME_ZONE = 'UTC'
125 |
126 | USE_I18N = True
127 |
128 | USE_L10N = True
129 |
130 | USE_TZ = True
131 |
132 |
133 | # Static files (CSS, JavaScript, Images)
134 | # https://docs.djangoproject.com/en/1.11/howto/static-files/
135 |
136 | STATIC_URL = '/static/'
137 |
138 | STATIC_ROOT = os.path.normpath(os.path.join(BASE_DIR, 'staticfiles'))
139 |
140 |
141 | STATICFILES_DIRS = [
142 | os.path.join(BASE_DIR, 'static')
143 |
144 | ]
145 |
146 |
147 | AUTH_USER_MODEL = 'accounts.User'
148 |
149 | LOGIN_REDIRECT_URL = reverse_lazy('home')
150 |
151 |
152 |
--------------------------------------------------------------------------------
/classroom/static/classroom/js/student/quizzes.js:
--------------------------------------------------------------------------------
1 |
2 | // $(document).on("submit","#js-new-choice-form", function (e) {
3 | // e.preventDefault();
4 | // form = $(this);
5 | // form = form.append($("#token").find('input[name=csrfmiddlewaretoken]')[0]);
6 | // $.ajax({
7 | // url: form.attr("action"),
8 | // type: "POST",
9 | // data: form.serialize(),
10 | // dataType: 'json',
11 | // success: function (info) {
12 | // if (info.valid) {
13 | // $("#js-add-new-choice-modal").hide();
14 | // location.reload();
15 | // }
16 | // else {
17 | // input = jQuery(" ");
18 | // $("#token").append(input);
19 | // $("#display-form-content").html(info.html_form);
20 | // }
21 | // }
22 | // });
23 | // });
24 |
25 |
26 | // $(document).ready( function () {
27 |
28 | // $('.js-answer-quiz').click( function () {
29 | // row = $(this);
30 | // $.ajax({
31 | // url: row.attr("data-href"),
32 | // type: 'get',
33 | // dataType: 'json',
34 |
35 | // success: function (data) {
36 | // $("#js-add-new-choice-modal").show();
37 | // $("#display-form-content").html(data.html_form);
38 | // }
39 |
40 | // });
41 | // });
42 |
43 | // });
44 |
45 |
46 |
47 | function countDown (time_of_submission) {
48 | // Set the date we're counting down to
49 | var countDownDate = new Date(String(time_of_submission)).getTime();
50 |
51 | // Update the count down every 1 second
52 | var x = setInterval(function() {
53 |
54 | // Get todays date and time
55 | var now = new Date().getTime();
56 |
57 | // Find the distance between now an the count down date
58 | var distance = countDownDate - now;
59 |
60 | // Time calculations for days, hours, minutes and seconds
61 | var days = Math.floor(distance / (1000 * 60 * 60 * 24));
62 | var hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
63 | var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
64 | var seconds = Math.floor((distance % (1000 * 60)) / 1000);
65 |
66 | // Output the result in an element with id="demo"
67 | document.getElementById("answer-countdown").innerHTML = days + "d " + hours + "h "
68 | + minutes + "m " + seconds + "s ";
69 |
70 | // If the count down is over, write some text
71 | if (distance < 0) {
72 | clearInterval(x);
73 | document.getElementById("answer-countdown").innerHTML = "EXPIRED";
74 | $("#js-question-modal").hide();
75 | location.reload();
76 | }
77 | }, 1000);
78 | }
79 |
80 | $('.js-answer-quiz').click( function () {
81 | row = $(this);
82 | $.ajax({
83 | url: row.attr("data-href"),
84 | type: 'get',
85 | dataType: 'json',
86 |
87 | success: function (data) {
88 | $("#js-question-modal").show();
89 | console.log('akoh');
90 | $("#display-form-content").html(data.html_form);
91 | time_of_submission = $("#answer-countdown").html();
92 | countDown(time_of_submission);
93 |
94 | },
95 | error: function(xhr) { // if error occured
96 | swal("Please wait", "No questions have been added to the quiz. Please check back later", "info");
97 |
98 | },
99 |
100 | });
101 | });
102 |
103 | $(document).on("submit","#js-question-form", function (e) {
104 | e.preventDefault();
105 | form = $(this);
106 | form = form.append($("#token").find('input[name=csrfmiddlewaretoken]')[0]);
107 | $.ajax({
108 | url: form.attr("action"),
109 | type: "POST",
110 | data: form.serialize(),
111 | dataType: 'json',
112 | success: function (info) {
113 | if (info.submitted_successfully) {
114 | $("#js-question-modal").hide();
115 | location.reload();
116 | }else if(info.already_submitted){
117 | console.log(info.already_submitted)
118 | console.log('no me p')
119 | $("#js-question-modal").hide();
120 | swal("You are only allowed to submit once", "Submitted quiz are graded automatically after submission", "info");
121 | } else {
122 | input = jQuery(" ");
123 | $("#token").append(input);
124 | $("#display-form-content").html(info.html_form);
125 | }
126 | }
127 | });
128 | });
129 |
130 |
--------------------------------------------------------------------------------
/classroom/views/instructor.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render, redirect
2 | from django.views.generic import CreateView, ListView
3 | from ..models import QuizOrAssignment, Question, Discussion, Comment
4 | from .. import forms
5 | from django.http import JsonResponse
6 | # from django.contrib.auth.decorators import user_passes_test
7 | from django.template.loader import render_to_string
8 | from django.contrib.auth.mixins import UserPassesTestMixin
9 | from django.core.exceptions import PermissionDenied
10 |
11 |
12 |
13 | class TestInstructor(UserPassesTestMixin):
14 |
15 | def test_func(self):
16 | if not self.request.user.is_authenticated:
17 | # Redirect the user to 403 page
18 | raise PermissionDenied
19 | if not self.request.user.groups.filter(name='Instructor Role').exists():
20 | # Redirect the user to 403 page
21 | raise PermissionDenied
22 |
23 | # Checks pass, let http method handlers process the request
24 | return self.dispatch
25 |
26 | class ChoiceList(TestInstructor, ListView):
27 |
28 | def get_context_object_name(self, object_list):
29 | object_name = self.kwargs['choice']
30 | return object_name
31 |
32 | def get(self, request, *args, **kwargs):
33 | context = get_context_variables(self.kwargs['choice'], self.request.user)
34 | return self.render_to_response(context)
35 |
36 | def get_template_names(self):
37 | template = {
38 | 'quizzes' : r'classroom/instructor/quizzes.html',
39 | 'assignments': r'classroom/instructor/assignments.html',
40 | 'grades' : r'classroom/instructor/grades.html',
41 | 'discussions': r'classroom/instructor/discussions.html',
42 | }[self.kwargs['choice']]
43 | return [template]
44 |
45 |
46 | class Choice(TestInstructor, CreateView):
47 | info = dict()
48 |
49 | def get_form(self, form_class=None):
50 | choice = self.kwargs['choice']
51 | form = {
52 | 'assignment': forms.AssignmentForm,
53 | 'quiz' : forms.QuizForm,
54 | 'comment' : forms.CommentForm,
55 | 'discussion': forms.DiscussionForm,
56 | }[choice]
57 | return form(**self.get_form_kwargs())
58 |
59 | def get_form_kwargs(self):
60 | form_kwargs = super(Choice, self).get_form_kwargs()
61 | form_kwargs['user'] = self.request.user
62 | return form_kwargs
63 |
64 | def get(self, request, *args, **kwargs):
65 | path = request.META.get('PATH_INFO')
66 | form = self.get_form()
67 | context = {'path': path, 'form': form, 'choice': self.kwargs['choice']}
68 | self.info['html_form'] = render_to_string(
69 | 'classroom/includes/new_form_modal.html', context)
70 | return JsonResponse(self.info)
71 |
72 | def form_valid(self, form):
73 | form.save()
74 | self.info['valid'] = True
75 | return JsonResponse(self.info)
76 |
77 | def form_invalid(self, form):
78 | path = self.request.META.get('PATH_INFO')
79 | context = {'path': path, 'form': form, 'choice': self.kwargs['choice']}
80 | self.info['valid'] = False
81 | self.info['html_form'] = render_to_string(
82 | 'classroom/includes/new_form_modal.html', context)
83 | return JsonResponse(self.info)
84 |
85 |
86 | class QuestionAndCommentHandler(TestInstructor, CreateView):
87 | info = dict()
88 |
89 | def get_form(self, form_class=None):
90 | choice = self.kwargs['choice']
91 | form = {
92 | 'quiz' : forms.QuestionForm,
93 | 'discussion' : forms.CommentForm,
94 | }[choice]
95 | return form(**self.get_form_kwargs())
96 |
97 | def get_form_kwargs(self):
98 | form_kwargs = super(QuestionAndCommentHandler, self).get_form_kwargs()
99 | form_kwargs['user'] = self.request.user
100 | form_kwargs['pk'] = self.kwargs['pk']
101 | return form_kwargs
102 |
103 | def get(self, request, *args, **kwargs):
104 | path = request.META.get('PATH_INFO')
105 | choice = ('Comment' if kwargs['choice'] == 'discussion' else 'Question')
106 |
107 | form = self.get_form()
108 | context = {'path': path, 'form': form, 'choice':choice}
109 | self.info['html_form'] = render_to_string(
110 | 'classroom/includes/new_form_modal.html', context)
111 | return JsonResponse(self.info)
112 |
113 | def form_valid(self, form):
114 | form.save()
115 | self.info['valid'] = True
116 | return JsonResponse(self.info)
117 |
118 | def form_invalid(self, form):
119 | path = self.request.META.get('PATH_INFO')
120 | choice = ('Comment' if self.kwargs['choice'] == 'discussion' else 'Question')
121 | context = {'path': path, 'form': form, 'choice': choice}
122 | self.info['valid'] = False
123 | self.info['html_form'] = render_to_string(
124 | 'classroom/includes/new_form_modal.html', context)
125 | return JsonResponse(self.info)
126 |
127 |
128 |
129 | def get_context_variables(choice, user=None):
130 | if user.user_type == 'IN':
131 | if choice == 'quizzes':
132 | quiz = QuizOrAssignment.objects.filter(is_assignment=False, owner=user).order_by('date_of_submission')[:21]
133 | questions = Question.objects.filter(quiz_or_assignment__owner=user)
134 | context = {'quizzes': quiz, 'questions':questions}
135 | elif choice == 'assignments':
136 | assignments = QuizOrAssignment.objects.filter(is_assignment=True, owner=user).order_by('date_of_submission')[:21]
137 | questions = Question.objects.filter(quiz_or_assignment__owner=user)
138 | context = {'assignments': assignments, 'questions':questions}
139 | elif choice == 'discussions':
140 | discussions = Discussion.objects.filter(course__instructors=user)
141 | comments = Comment.objects.filter(discussion__created_by=user)
142 | context = {'discussions':discussions, 'comments':comments}
143 |
144 |
145 | return context
146 |
147 | else:
148 | raise PermissionDenied
--------------------------------------------------------------------------------
/classroom/migrations/0003_auto_20180523_0854.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11 on 2018-05-23 08:54
3 | from __future__ import unicode_literals
4 |
5 | from django.conf import settings
6 | from django.db import migrations, models
7 | import django.db.models.deletion
8 | import django.utils.timezone
9 |
10 |
11 | class Migration(migrations.Migration):
12 |
13 | dependencies = [
14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15 | ('classroom', '0002_auto_20180521_1519'),
16 | ]
17 |
18 | operations = [
19 | migrations.CreateModel(
20 | name='Answer',
21 | fields=[
22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
23 | ('text', models.CharField(max_length=255, verbose_name='Answer')),
24 | ('is_correct', models.BooleanField(default=False, verbose_name='Correct answer')),
25 | ],
26 | ),
27 | migrations.CreateModel(
28 | name='Grades',
29 | fields=[
30 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
31 | ('grade', models.CharField(blank=True, choices=[('A', 'A(80-100)'), ('B', 'B(70-79)'), ('C', 'C(60-69)'), ('D', 'D(50-59)'), ('E', 'E(40-49)'), ('F', 'F(30-39)')], max_length=1, null=True)),
32 | ],
33 | ),
34 | migrations.CreateModel(
35 | name='Module',
36 | fields=[
37 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
38 | ('name', models.CharField(max_length=30)),
39 | ('text', models.TextField()),
40 | ('class_file', models.FileField(upload_to='')),
41 | ],
42 | ),
43 | migrations.CreateModel(
44 | name='Question',
45 | fields=[
46 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
47 | ('text', models.CharField(max_length=255, verbose_name='Question')),
48 | ],
49 | ),
50 | migrations.RemoveField(
51 | model_name='assignment',
52 | name='comments',
53 | ),
54 | migrations.RemoveField(
55 | model_name='assignment',
56 | name='content',
57 | ),
58 | migrations.RemoveField(
59 | model_name='assignment',
60 | name='created_by',
61 | ),
62 | migrations.RemoveField(
63 | model_name='course',
64 | name='content',
65 | ),
66 | migrations.RemoveField(
67 | model_name='course',
68 | name='grade',
69 | ),
70 | migrations.RemoveField(
71 | model_name='quiz',
72 | name='content',
73 | ),
74 | migrations.RemoveField(
75 | model_name='quiz',
76 | name='created',
77 | ),
78 | migrations.RemoveField(
79 | model_name='quiz',
80 | name='created_by',
81 | ),
82 | migrations.AddField(
83 | model_name='comments',
84 | name='body',
85 | field=models.TextField(default='body to fill'),
86 | preserve_default=False,
87 | ),
88 | migrations.AddField(
89 | model_name='discussion',
90 | name='course',
91 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='classroom.Course'),
92 | ),
93 | migrations.AddField(
94 | model_name='quiz',
95 | name='course',
96 | field=models.ForeignKey(default='Course.objects.get(id=1)', on_delete=django.db.models.deletion.CASCADE, related_name='quiz', to='classroom.Course'),
97 | preserve_default=False,
98 | ),
99 | migrations.AddField(
100 | model_name='quiz',
101 | name='date_of_submission',
102 | field=models.DateTimeField(default=django.utils.timezone.now),
103 | preserve_default=False,
104 | ),
105 | migrations.AddField(
106 | model_name='quiz',
107 | name='is_assignment',
108 | field=models.BooleanField(default=False),
109 | ),
110 | migrations.AddField(
111 | model_name='quiz',
112 | name='name',
113 | field=models.CharField(default='random', max_length=255),
114 | preserve_default=False,
115 | ),
116 | migrations.AddField(
117 | model_name='quiz',
118 | name='owner',
119 | field=models.ForeignKey(default='1', on_delete=django.db.models.deletion.CASCADE, related_name='quizzes', to=settings.AUTH_USER_MODEL),
120 | preserve_default=False,
121 | ),
122 | migrations.AlterField(
123 | model_name='course',
124 | name='instructors',
125 | field=models.ManyToManyField(related_name='instructors', to=settings.AUTH_USER_MODEL),
126 | ),
127 | migrations.AlterField(
128 | model_name='course',
129 | name='title',
130 | field=models.CharField(max_length=255),
131 | ),
132 | migrations.DeleteModel(
133 | name='Assignment',
134 | ),
135 | migrations.DeleteModel(
136 | name='Content',
137 | ),
138 | migrations.AddField(
139 | model_name='question',
140 | name='quiz',
141 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='classroom.Quiz'),
142 | ),
143 | migrations.AddField(
144 | model_name='grades',
145 | name='course',
146 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='classroom.Course'),
147 | ),
148 | migrations.AddField(
149 | model_name='grades',
150 | name='student',
151 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
152 | ),
153 | migrations.AddField(
154 | model_name='answer',
155 | name='question',
156 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='classroom.Question'),
157 | ),
158 | migrations.AddField(
159 | model_name='course',
160 | name='modules',
161 | field=models.ManyToManyField(to='classroom.Module'),
162 | ),
163 | ]
164 |
--------------------------------------------------------------------------------
/classroom/views/student.py:
--------------------------------------------------------------------------------
1 | from django.views.generic import ListView
2 | from ..models import Course, QuizOrAssignment
3 | # from datetime import datetime
4 | from django.utils import timezone
5 | from django.views.generic.edit import CreateView
6 | from ..forms import QuestionFormSet
7 | from django.http import JsonResponse
8 | from django.template.loader import render_to_string
9 | from classroom.models import Question, Grade, QuizOrAssignment, Discussion, Comment
10 | from django.core.exceptions import PermissionDenied
11 | from django.contrib.auth.mixins import UserPassesTestMixin
12 |
13 |
14 | class TestStudent(UserPassesTestMixin):
15 |
16 | def test_func(self):
17 | if not self.request.user.is_authenticated:
18 | # Redirect the user to 403 page
19 | raise PermissionDenied
20 | if not self.request.user.user_type == 'ST':
21 | # Redirect the user to 403 page
22 | raise PermissionDenied
23 |
24 | # Checks pass, let http method handlers process the request
25 | return self.dispatch
26 |
27 |
28 | class ChoiceList(TestStudent, ListView):
29 | """class-based view to list courses, assignments, quizzes and grades
30 |
31 | Arguments:
32 | TestStudent {class} -- Custom Mixin to prevent unauthorized users
33 | ListView {class} -- Django generic List View
34 |
35 | Raises:
36 | PermissionDenied -- exception thrown to prevent unauthorized access to this view
37 | PermissionDenied -- exception thrown to prevent unauthorized access to this view
38 |
39 | """
40 | def get_context_object_name(self, object_list):
41 | object_name = self.kwargs['choice']
42 | return object_name
43 |
44 | def get(self, request, *args, **kwargs):
45 | context = get_context_variables(
46 | self.kwargs['choice'], self.request.user)
47 | return self.render_to_response(context)
48 |
49 | def get_template_names(self):
50 | template = {
51 | 'courses': r'classroom/student/courses.html',
52 | 'assignments': r'classroom/student/assignments.html',
53 | 'quizzes': r'classroom/student/quizzes.html',
54 | 'grades': r'classroom/student/grades.html',
55 | 'discussions': r'classroom/student/discussions.html',
56 | }[self.kwargs['choice']]
57 | return [template]
58 |
59 |
60 | def take_questions(request, pk):
61 | """
62 | Returns questions as a formset to be answered by student
63 | """
64 | info = dict()
65 | questions = Question.objects.filter(quiz_or_assignment__id=pk)
66 | formset = QuestionFormSet(queryset=questions)
67 | date_of_submission = str(
68 | questions[0].quiz_or_assignment.date_of_submission)
69 | path = request.META.get('PATH_INFO')
70 | score = 0
71 |
72 | if request.method == 'POST':
73 | formset = QuestionFormSet(request.POST)
74 | if formset.is_valid():
75 | for form in formset:
76 | student_option = form.cleaned_data['answer'].title()
77 | correct_option = form.instance.answer
78 | if student_option == correct_option:
79 | score += 1
80 | score = (score/len(formset)) * 100
81 | status = grade(request.user, score, pk)
82 | if status:
83 | info['submitted_successfully'] = True
84 |
85 | else:
86 | info['already_submitted'] = True
87 |
88 | return JsonResponse(info)
89 | else:
90 | info['submitted_successfully'] = False
91 |
92 | context = {'formset': formset, 'is_formset': True,
93 | 'path': path, 'date_of_submission': date_of_submission}
94 | info['html_form'] = render_to_string(
95 | 'classroom/includes/answer_question_modal.html', context)
96 |
97 | return JsonResponse(info)
98 |
99 |
100 | def get_context_variables(choice, user):
101 | """To get the appropriate context variables based on the section
102 | of the site a student wants to view
103 |
104 | Arguments:
105 | choice {string} -- section of the site to be viewed by a student.
106 | Possible options are course, quiz, assignment
107 |
108 | Keyword Arguments:
109 | user {object} -- user instance to cross-check that a that the user is a student
110 |
111 | Raises:
112 | PermissionDenied -- Prevents unauthorized users from viewing a students page
113 | PermissionDenied -- Prevents unauthorized users from viewing a students page
114 |
115 | Returns:
116 | [dictionary] -- appropriate context variables
117 | """
118 |
119 | if user.user_type == 'ST':
120 | if choice == 'courses':
121 | courses = Course.objects.filter(students=user)
122 | context = {'courses': courses}
123 | elif choice == 'assignments':
124 | pending_assignments = QuizOrAssignment.objects.filter(
125 | date_of_submission__gt=timezone.now(), course__students=user, is_assignment=True)
126 | submitted_assignments = QuizOrAssignment.objects.filter(
127 | date_of_submission__lt=timezone.now(), course__students=user, is_assignment=True)
128 | context = {'pending_assignments': pending_assignments,
129 | 'submitted_assignments': submitted_assignments}
130 | elif choice == 'quizzes':
131 | pending_quizzes = QuizOrAssignment.objects.filter(
132 | date_of_submission__gt=timezone.now(), course__students=user, is_assignment=False)
133 | submitted_quizzes = QuizOrAssignment.objects.filter(
134 | date_of_submission__lt=timezone.now(), course__students=user, is_assignment=False)
135 | context = {'pending_quizzes': pending_quizzes,
136 | 'submitted_quizzes': submitted_quizzes}
137 | elif choice == 'discussions':
138 | discussions = Discussion.objects.filter(course__students=user)
139 | comments = Comment.objects.filter(
140 | discussion__course__students=user)
141 | context = {'discussions': discussions, 'comments': comments}
142 | elif choice == 'grades':
143 | grades = Grade.objects.filter(student=user)
144 | context = {'grades': grades}
145 | else:
146 | raise PermissionDenied
147 |
148 | return context
149 |
150 | else:
151 | raise PermissionDenied
152 |
153 |
154 | def grade(user, score, pk):
155 | """used to grade students based on their score in an assignment or quiz
156 |
157 | Arguments:
158 | user {object} -- user that is taking the assignment or quiz
159 | score {double} -- the number of questions answered correctly
160 | divided by total number of questions
161 |
162 | pk {int} -- primary key of the quiz or assignment
163 |
164 | NOTE: multiple questions belong to a quiz or assignment
165 |
166 | Returns:
167 | status {bool} -- The fuction checks to make sure the student has no been
168 | graded in that particular quiz or assignment
169 | True == No grade before
170 | """
171 | quiz_or_assignment = QuizOrAssignment.objects.get(pk=pk)
172 | code = quiz_or_assignment.course.code
173 | # check if a grade exists for a student in that particular course
174 | if Grade.objects.filter(quiz_or_assignment=quiz_or_assignment, student=user, course__code=code).exists():
175 | return False
176 | else:
177 | # if grade does not exist create a new grade for the student in the particular course
178 | course = Course.objects.get(code=code)
179 | Grade.objects.create(quiz_or_assignment=quiz_or_assignment,
180 | student=user, course=course, score=score)
181 | return True
182 |
--------------------------------------------------------------------------------
/classroom/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from accounts.models import User
3 | from .models import Course, QuizOrAssignment, Question, Discussion, Comment
4 | from django.contrib.auth.forms import UserCreationForm
5 | from django.utils.translation import ugettext_lazy as _
6 | from django.contrib.auth.models import Group
7 |
8 |
9 | class LMSAdminSignUpForm(UserCreationForm):
10 | first_name = forms.CharField(max_length=30)
11 | last_name = forms.CharField(max_length=30)
12 | email = forms.EmailField(
13 | help_text='Admin staff must use school(.lms) email address')
14 |
15 | class Meta(UserCreationForm.Meta):
16 | model = User
17 | fields = ('first_name', 'last_name', 'email', 'username')
18 |
19 | def clean_email(self):
20 | if '.lms' not in self.cleaned_data['email']:
21 | raise forms.ValidationError(
22 | _('Invalid email address'), code='invalid')
23 |
24 | def save(self, commit=True):
25 | user = super().save(commit=False)
26 | user.user_type = 'LA'
27 | lms_admins = Group.objects.get(name='Admin Role')
28 | if commit:
29 | user.save()
30 | user.groups.add(lms_admins)
31 | return user
32 |
33 |
34 | class InstructorSignUpForm(UserCreationForm):
35 | first_name = forms.CharField(max_length=30)
36 | last_name = forms.CharField(max_length=30)
37 | email = forms.EmailField(help_text='Please enter a valid email address')
38 |
39 | class Meta(UserCreationForm.Meta):
40 | model = User
41 | fields = ('first_name', 'last_name', 'email', 'username')
42 |
43 | def save(self, commit=True):
44 | user = super().save(commit=False)
45 | user.user_type = 'IN'
46 | instructors = Group.objects.get(name='Instructor Role')
47 | if commit:
48 | user.save()
49 | user.groups.add(instructors)
50 |
51 | return user
52 |
53 |
54 | class StudentSignUpForm(UserCreationForm):
55 | first_name = forms.CharField(max_length=30)
56 | last_name = forms.CharField(max_length=30)
57 |
58 | class Meta(UserCreationForm.Meta):
59 | model = User
60 | fields = ('first_name', 'last_name', 'username')
61 |
62 | def save(self, commit=True):
63 | user = super().save(commit=False)
64 | user.user_type = 'ST'
65 | if commit:
66 | user.save()
67 | return user
68 |
69 |
70 | class CourseForm(forms.ModelForm):
71 |
72 | class Meta:
73 | model = Course
74 | exclude = ('syllabus', 'modules',)
75 |
76 | def __init__(self, *args, **kwargs):
77 | super(CourseForm, self).__init__(*args, **kwargs)
78 | self.fields['instructors'].queryset = User.objects.filter(
79 | user_type='IN').order_by('username')
80 | self.fields['students'].queryset = User.objects.filter(
81 | user_type='ST').order_by('username')
82 | self.fields['teaching_assistants'].queryset = User.objects.filter(
83 | user_type='TA').order_by('username')
84 |
85 |
86 | class AssignmentForm(forms.ModelForm):
87 |
88 | class Meta:
89 | model = QuizOrAssignment
90 | exclude = ('comment', 'owner', 'is_assignment',)
91 |
92 | def __init__(self, user, *args, **kwargs):
93 | super(AssignmentForm, self).__init__(*args, **kwargs)
94 | # declaring self.user so I can use (user) variable in save method
95 | self.user = user
96 | self.fields['course'].queryset = Course.objects.filter(
97 | instructors=user).order_by('instructors')
98 |
99 | def save(self, commit=True):
100 | assignment = super().save(commit=False)
101 | assignment.is_assignment = True
102 | assignment.owner = self.user
103 | if commit:
104 | assignment.save()
105 | return assignment
106 |
107 |
108 | class QuizForm(forms.ModelForm):
109 |
110 | class Meta:
111 | model = QuizOrAssignment
112 | exclude = ('comment', 'owner', 'is_assignment',)
113 |
114 | def __init__(self, user, *args, **kwargs):
115 | super(QuizForm, self).__init__(*args, **kwargs)
116 | # declaring self.user so I can use (user) variable in save method
117 | self.user = user
118 | self.fields['course'].queryset = Course.objects.filter(
119 | instructors=user).order_by('instructors')
120 |
121 | def save(self, commit=True):
122 | quiz = super().save(commit=False)
123 | quiz.owner = self.user
124 | if commit:
125 | quiz.save()
126 | return quiz
127 |
128 |
129 | class QuestionForm(forms.ModelForm):
130 | answer = forms.CharField(
131 | max_length=1, help_text='Enter the correct option (A or B or C or D)')
132 |
133 | class Meta:
134 | model = Question
135 | exclude = ('quiz',)
136 |
137 | widgets = {
138 | 'first_option': forms.Textarea(attrs={'rows': '3'}),
139 | 'second_option': forms.Textarea(attrs={'rows': '3'}),
140 | 'third_option': forms.Textarea(attrs={'rows': '3'}),
141 | 'fourth_option': forms.Textarea(attrs={'rows': '3'}),
142 | }
143 |
144 | def __init__(self, pk, user, *args, **kwargs):
145 | super(QuestionForm, self).__init__(*args, **kwargs)
146 | # declaring self.user and self.pk so I can use them in save method
147 | self.user = user
148 | self.pk = pk
149 |
150 | def clean_answer(self):
151 | data = self.cleaned_data
152 | correct_option = data['answer'].title()
153 | if correct_option not in ['A', 'B', 'C', 'D']:
154 | raise forms.ValidationError(
155 | _("Answer does not match any option"), code="no match")
156 |
157 | return correct_option
158 |
159 | def save(self, commit=True):
160 | quiz = QuizOrAssignment.objects.get(pk=self.pk)
161 | question = super().save(commit=False)
162 | question.quiz = quiz
163 | if commit:
164 | question.save()
165 | return question
166 |
167 |
168 | class StudentQuestionForm(forms.ModelForm):
169 | answer = forms.CharField(max_length=1)
170 |
171 | class Meta:
172 | model = Question
173 | exclude = ('quiz_or_assignment', 'answer',)
174 |
175 | widgets = {
176 | 'text': forms.Textarea(attrs={'readonly': 'readonly'}),
177 | 'first_option': forms.Textarea(attrs={'readonly': 'readonly'}),
178 | 'second_option': forms.Textarea(attrs={'readonly': 'readonly'}),
179 | 'third_option': forms.Textarea(attrs={'readonly': 'readonly'}),
180 | 'fourth_option': forms.Textarea(attrs={'readonly': 'readonly'}),
181 | }
182 | # def clean_sanswer(self):
183 | # if self.cleaned_data['answer'].title() not in ['A', 'B', 'C', 'D']:
184 | # raise forms.ValidationError(_('Answer does not match any option'), code="no match")
185 |
186 |
187 | class BaseQuestionFormSet(forms.BaseModelFormSet):
188 | score = 0
189 |
190 | def clean(self):
191 | super().clean()
192 |
193 | for form in self.forms:
194 | try:
195 | if form.cleaned_data['answer'].title() not in ['A', 'B', 'C', 'D']:
196 | raise forms.ValidationError(
197 | _('One or more of your answer(s) does not match any option'), code='no match')
198 | except KeyError:
199 | raise forms.ValidationError(_('Please, answer all question before submitting'),code='invalid')
200 |
201 | QuestionFormSet = forms.modelformset_factory(
202 | Question, form=StudentQuestionForm, extra=0, formset=BaseQuestionFormSet, can_delete=False)
203 |
204 |
205 | class DiscussionForm(forms.ModelForm):
206 | class Meta:
207 | model = Discussion
208 | exclude = ('created_by',)
209 |
210 | def __init__(self, user, *args, **kwargs):
211 | super(DiscussionForm, self).__init__(*args, **kwargs)
212 | # declaring self.user so I can use (user) variable in save method
213 | self.user = user
214 | self.fields['course'].queryset = Course.objects.filter(
215 | instructors=user)
216 |
217 | def save(self, commit=True):
218 | discussion = super().save(commit=False)
219 | discussion.created_by = self.user
220 | if commit:
221 | discussion.save()
222 | return discussion
223 |
224 |
225 | class CommentForm(forms.ModelForm):
226 | class Meta:
227 | model = Comment
228 | exclude = ('author', 'discussion',)
229 |
230 | def __init__(self, pk, user, *args, **kwargs):
231 | super(CommentForm, self).__init__(*args, **kwargs)
232 | # declaring self.user and self.pk so I can use them in save method
233 | self.user = user
234 | self.pk = pk
235 |
236 | def save(self, commit=True):
237 | discussion = Discussion.objects.get(pk=self.pk)
238 | comment = super().save(commit=False)
239 | comment.author = self.user
240 | comment.discussion = discussion
241 | if commit:
242 | comment.save()
243 | return comment
244 |
--------------------------------------------------------------------------------
/static/vendor/w3.css:
--------------------------------------------------------------------------------
1 | /* W3.CSS 4.10 February 2018 by Jan Egil and Borge Refsnes */
2 | html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
3 | /* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
4 | html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
5 | article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}
6 | audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
7 | audio:not([controls]){display:none;height:0}[hidden],template{display:none}
8 | a{background-color:transparent;-webkit-text-decoration-skip:objects}
9 | a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
10 | dfn{font-style:italic}mark{background:#ff0;color:#000}
11 | small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
12 | sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}svg:not(:root){overflow:hidden}
13 | code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
14 | button,input,select,textarea{font:inherit;margin:0}optgroup{font-weight:bold}
15 | button,input{overflow:visible}button,select{text-transform:none}
16 | button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button}
17 | button::-moz-focus-inner, [type=button]::-moz-focus-inner, [type=reset]::-moz-focus-inner, [type=submit]::-moz-focus-inner{border-style:none;padding:0}
18 | button:-moz-focusring, [type=button]:-moz-focusring, [type=reset]:-moz-focusring, [type=submit]:-moz-focusring{outline:1px dotted ButtonText}
19 | fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
20 | legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
21 | [type=checkbox],[type=radio]{padding:0}
22 | [type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
23 | [type=search]{-webkit-appearance:textfield;outline-offset:-2px}
24 | [type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}
25 | ::-webkit-input-placeholder{color:inherit;opacity:0.54}
26 | ::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
27 | /* End extract */
28 | html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
29 | h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}.w3-serif{font-family:serif}
30 | h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
31 | hr{border:0;border-top:1px solid #eee;margin:20px 0}
32 | .w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
33 | .w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
34 | .w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
35 | .w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
36 | .w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
37 | .w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
38 | .w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
39 | .w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
40 | .w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
41 | .w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
42 | .w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
43 | .w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
44 | .w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
45 | .w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
46 | .w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
47 | .w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
48 | .w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
49 | .w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
50 | .w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
51 | .w3-dropdown-hover:hover .w3-dropdown-content{display:block}
52 | .w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
53 | .w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
54 | .w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
55 | .w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
56 | .w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
57 | .w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
58 | .w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
59 | .w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
60 | .w3-main,#main{transition:margin-left .4s}
61 | .w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
62 | .w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
63 | .w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
64 | .w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
65 | .w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
66 | .w3-bar .w3-button{white-space:normal}
67 | .w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
68 | .w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
69 | .w3-responsive{display:block;overflow-x:auto}
70 | .w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
71 | .w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
72 | .w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
73 | .w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
74 | .w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
75 | .w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
76 | @media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
77 | .w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
78 | .w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
79 | @media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
80 | .w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
81 | .w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
82 | .w3-content{max-width:980px;margin:auto}.w3-rest{overflow:hidden}
83 | .w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
84 | .w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
85 | .w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
86 | @media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
87 | .w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
88 | .w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
89 | .w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
90 | @media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
91 | @media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
92 | @media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
93 | @media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}}
94 | .w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
95 | .w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
96 | .w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
97 | .w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
98 | .w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
99 | .w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
100 | .w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
101 | .w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
102 | .w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
103 | .w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
104 | .w3-display-position{position:absolute}
105 | .w3-circle{border-radius:50%}
106 | .w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
107 | .w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
108 | .w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
109 | .w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
110 | .w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
111 | .w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
112 | .w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
113 | .w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
114 | .w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
115 | .w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
116 | .w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
117 | .w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
118 | .w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
119 | .w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
120 | .w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
121 | .w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
122 | .w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
123 | .w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
124 | .w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
125 | .w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
126 | .w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
127 | .w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
128 | .w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
129 | .w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
130 | .w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
131 | .w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
132 | .w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
133 | .w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
134 | .w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
135 | .w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
136 | .w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
137 | .w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
138 | .w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
139 | .w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
140 | .w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
141 | .w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
142 | .w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
143 | .w3-left{float:left!important}.w3-right{float:right!important}
144 | .w3-button:hover{color:#000!important;background-color:#ccc!important}
145 | .w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
146 | .w3-hover-none:hover{box-shadow:none!important}
147 | /* Colors */
148 | .w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
149 | .w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
150 | .w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
151 | .w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
152 | .w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
153 | .w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
154 | .w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
155 | .w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
156 | .w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
157 | .w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
158 | .w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
159 | .w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
160 | .w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
161 | .w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
162 | .w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
163 | .w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
164 | .w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
165 | .w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
166 | .w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
167 | .w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
168 | .w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
169 | .w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
170 | .w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
171 | .w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
172 | .w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
173 | .w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
174 | .w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
175 | .w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
176 | .w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
177 | .w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
178 | .w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
179 | .w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
180 | .w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
181 | .w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
182 | .w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
183 | .w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
184 | .w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
185 | .w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
186 | .w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
187 | .w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
188 | .w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
189 | .w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
190 | .w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
191 | .w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
192 | .w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
193 | .w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
194 | .w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
195 | .w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
196 | .w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
197 | .w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
198 | .w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
199 | .w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
200 | .w3-text-black,.w3-hover-text-black:hover{color:#000!important}
201 | .w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
202 | .w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
203 | .w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
204 | .w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
205 | .w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
206 | .w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
207 | .w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
208 | .w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
209 | .w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
210 | .w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
211 | .w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
212 | .w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
213 | .w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
214 | .w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
215 | .w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
216 | .w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
217 | .w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
218 | .w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
219 | .w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
220 | .w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
221 | .w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
222 | .w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
223 | .w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
224 | .w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
225 | .w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
226 | .w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
227 | .w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
228 | .w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
229 | .w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
230 | .w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
231 | .w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
--------------------------------------------------------------------------------