├── lms ├── __init__.py ├── tests │ ├── __init__.py │ └── test_views.py ├── wsgi.py ├── urls.py ├── views.py └── settings.py ├── accounts ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0003_auto_20180523_1017.py │ ├── 0005_auto_20180524_0633.py │ ├── 0006_auto_20180525_1532.py │ ├── 0004_auto_20180524_0626.py │ ├── 0007_auto_20180608_0944.py │ ├── 0002_auto_20180521_1519.py │ └── 0001_initial.py ├── tests.py ├── views.py ├── apps.py ├── urls.py ├── admin.py ├── templates │ └── registration │ │ ├── logged_out.html │ │ └── login.html ├── models.py └── forms.py ├── classroom ├── __init__.py ├── tests │ ├── __init__.py │ ├── test_views_instructors.py │ ├── test_views_student.py │ └── test_views_lms_admin.py ├── views │ ├── __init__.py │ ├── lms_admin.py │ ├── instructor.py │ └── student.py ├── migrations │ ├── __init__.py │ ├── 0019_auto_20180608_1536.py │ ├── 0014_auto_20180531_1538.py │ ├── 0009_quiz_comment.py │ ├── 0013_auto_20180531_0938.py │ ├── 0012_auto_20180531_0257.py │ ├── 0011_auto_20180530_1458.py │ ├── 0018_auto_20180608_0944.py │ ├── 0005_auto_20180527_0958.py │ ├── 0008_auto_20180529_0938.py │ ├── 0016_auto_20180602_0845.py │ ├── 0020_auto_20180608_1607.py │ ├── 0017_auto_20180602_1209.py │ ├── 0006_auto_20180528_1140.py │ ├── 0004_auto_20180525_1532.py │ ├── 0015_auto_20180601_0311.py │ ├── 0010_auto_20180530_0955.py │ ├── 0002_auto_20180521_1519.py │ ├── 0007_auto_20180529_0922.py │ ├── 0001_initial.py │ └── 0003_auto_20180523_0854.py ├── templatetags │ ├── __init__.py │ └── student_tags.py ├── apps.py ├── templates │ └── classroom │ │ ├── instructor │ │ ├── question_list.html │ │ ├── quizzes.html │ │ ├── assignments.html │ │ └── discussions.html │ │ ├── includes │ │ ├── update_form_modal.html │ │ ├── new_form_modal.html │ │ ├── answer_question_modal.html │ │ ├── form.html │ │ └── formset.html │ │ ├── student │ │ ├── courses.html │ │ ├── grades.html │ │ ├── discussions.html │ │ ├── quizzes.html │ │ └── assignments.html │ │ ├── lms_admin │ │ ├── courses.html │ │ ├── lms_admins.html │ │ ├── students.html │ │ ├── instructors.html │ │ └── teaching_assistants.html │ │ ├── instructor_base.html │ │ ├── lms_admin_base.html │ │ └── student_base.html ├── static │ └── classroom │ │ ├── js │ │ ├── instructor │ │ │ ├── discussions.js │ │ │ ├── assignments.js │ │ │ └── quizzes.js │ │ ├── student │ │ │ ├── discussions.js │ │ │ ├── assignments.js │ │ │ └── quizzes.js │ │ ├── lms_admin │ │ │ ├── lms_admins.js │ │ │ ├── students.js │ │ │ ├── instructors.js │ │ │ └── courses.js │ │ └── main.js │ │ └── css │ │ └── student │ │ └── styles.css ├── admin.py ├── urls.py ├── models.py └── forms.py ├── runtime.txt ├── templates ├── landing_page.html ├── 403.html └── base.html ├── Procfile ├── db.sqlite3 ├── static ├── img │ └── student_avatar.png └── vendor │ └── w3.css ├── requirements.txt ├── manage.py ├── .gitignore └── README.md /lms/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /accounts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /classroom/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lms/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.6.5 -------------------------------------------------------------------------------- /classroom/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /classroom/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/landing_page.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /accounts/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /classroom/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | 2 | web: gunicorn lms.wsgi -------------------------------------------------------------------------------- /classroom/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /accounts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /accounts/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akohrr/Learning-Management-System/HEAD/db.sqlite3 -------------------------------------------------------------------------------- /accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | name = 'accounts' 6 | -------------------------------------------------------------------------------- /static/img/student_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akohrr/Learning-Management-System/HEAD/static/img/student_avatar.png -------------------------------------------------------------------------------- /classroom/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ClassroomConfig(AppConfig): 5 | name = 'classroom' 6 | -------------------------------------------------------------------------------- /classroom/templates/classroom/instructor/question_list.html: -------------------------------------------------------------------------------- 1 | {% for question in questions %} 2 | 3 | 4 | {{forloop.counter}} 5 | {{ question |truncatechars:30 } 6 | 7 | 8 | {% endfor %} 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dj-database-url==0.5.0 2 | Django==1.11.15 3 | django-heroku==0.3.1 4 | django-sortedm2m==1.5.0 5 | django-widget-tweaks==1.4.2 6 | gunicorn==19.8.1 7 | psycopg2==2.7.4 8 | psycopg2-binary==2.7.4 9 | pytz==2018.4 10 | six==1.11.0 11 | whitenoise==3.3.1 12 | -------------------------------------------------------------------------------- /accounts/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.contrib.auth import views as authviews 3 | 4 | 5 | urlpatterns = [ 6 | url(r'^login/$', authviews.login, name='login'), 7 | url(r'^logout/$', authviews.logout, name='logout'), 8 | url(r'^logout-then-login/$', authviews.logout_then_login, name='logout_then_login'), 9 | ] -------------------------------------------------------------------------------- /templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | 5 | {% block boilerplate_html %} 6 | 7 | 8 |
9 | 10 | 11 |

You do not have the appropriate permission(s) to view this page. Click here to go Home.

12 | 13 | 14 | 15 |
16 | 17 | {% endblock boilerplate_html %} 18 | -------------------------------------------------------------------------------- /accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.conf import settings 3 | from .forms import UserCreationForm 4 | 5 | from .models import User 6 | #register your models here. 7 | 8 | class UserAdmin(admin.ModelAdmin): 9 | list_filter = ('user_type',) 10 | list_display = ('first_name', 'last_name', 'username', 'email', 'user_type',) 11 | 12 | 13 | admin.site.register(User, UserAdmin) -------------------------------------------------------------------------------- /classroom/static/classroom/js/instructor/discussions.js: -------------------------------------------------------------------------------- 1 | $(".js-add-comment-discussion").click( function () { 2 | button = $(this); 3 | $.ajax({ 4 | url: button.attr("data-href"), 5 | type: 'get', 6 | dataType: 'json', 7 | 8 | success: function (data) { 9 | $("#js-add-new-choice-modal").show(); 10 | $("#display-form-content").html(data.html_form); 11 | } 12 | 13 | }); 14 | }); -------------------------------------------------------------------------------- /accounts/templates/registration/logged_out.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Log out 7 | 8 | 9 | 10 |

You have logged out successfully. Click here to login again

11 | 12 | 13 | -------------------------------------------------------------------------------- /lms/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for lms project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | 15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "lms.settings") 16 | 17 | application = get_wsgi_application() 18 | 19 | -------------------------------------------------------------------------------- /classroom/static/classroom/css/student/styles.css: -------------------------------------------------------------------------------- 1 | .errorlist { 2 | color:red; 3 | } 4 | 5 | 6 | div.scrollmenu { 7 | background-color: white; 8 | overflow: auto; 9 | white-space: nowrap; 10 | } 11 | 12 | div.scrollmenu a { 13 | display: inline-block; 14 | color: white; 15 | text-align: center; 16 | padding: 14px; 17 | text-decoration: none; 18 | } 19 | 20 | div.scrollmenu a:hover { 21 | background-color: #777; 22 | } 23 | 24 | textarea{ 25 | height: 200px; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /classroom/tests/test_views_instructors.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from lms.tests.test_views import create_test_user 3 | from django.urls import reverse 4 | class TestViews(TestCase): 5 | 6 | def test_forbids_unauthorized_users(self): 7 | instructor = create_test_user('test-user', 'lms-admin') 8 | self.client.force_login(instructor) 9 | response = self.client.get(reverse('classroom:instructor_view', kwargs={'choice':'assignments'})) 10 | self.assertEqual(response.status_code, 403) 11 | 12 | 13 | -------------------------------------------------------------------------------- /classroom/tests/test_views_student.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from lms.tests.test_views import create_test_user 3 | from django.urls import reverse 4 | 5 | 6 | class TestViews(TestCase): 7 | 8 | def test_forbids_unauthorized_users(self): 9 | instructor = create_test_user('test-user', 'instructor') 10 | self.client.force_login(instructor) 11 | response = self.client.get(reverse('classroom:student_view', kwargs={'choice':'assignments'})) 12 | self.assertEqual(response.status_code, 403) 13 | 14 | 15 | -------------------------------------------------------------------------------- /classroom/migrations/0019_auto_20180608_1536.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-06-08 15:36 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('classroom', '0018_auto_20180608_0944'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameField( 16 | model_name='grade', 17 | old_name='quiz', 18 | new_name='quiz_or_assignment', 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /classroom/templates/classroom/includes/update_form_modal.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Add new {{ choice }}

4 |
5 | 6 | 7 |
8 |
9 | 10 | {% include "classroom/includes/form.html" with form=form %} 11 | 12 | 13 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /classroom/migrations/0014_auto_20180531_1538.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-05-31 15:38 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', '0013_auto_20180531_0938'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='grade', 17 | name='grade', 18 | field=models.SmallIntegerField(default=0), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /classroom/migrations/0009_quiz_comment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-05-29 09:53 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', '0008_auto_20180529_0938'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='quiz', 17 | name='comment', 18 | field=models.ManyToManyField(to='classroom.Comments'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /classroom/migrations/0013_auto_20180531_0938.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-05-31 09:38 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', '0012_auto_20180531_0257'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='question', 17 | name='text', 18 | field=models.TextField(verbose_name='Question'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /classroom/templatetags/student_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | @register.simple_tag 6 | def get_grade_and_percentage(score): 7 | # score = score * 100 8 | grade = '' 9 | if score > 80 and score <= 100: 10 | grade = 'A' 11 | elif score > 70 and score < 80: 12 | grade = 'B' 13 | elif score > 60 and score < 70: 14 | grade = 'C' 15 | elif score > 50 and score < 60: 16 | grade = 'D' 17 | else: 18 | grade = 'F' 19 | 20 | result = '{0}({1})'.format(score,grade) 21 | return result -------------------------------------------------------------------------------- /classroom/migrations/0012_auto_20180531_0257.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-05-31 02:57 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', '0011_auto_20180530_1458'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='quizorassignment', 17 | name='comment', 18 | field=models.ManyToManyField(blank=True, to='classroom.Comment'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /classroom/migrations/0011_auto_20180530_1458.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-05-30 14:58 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('classroom', '0010_auto_20180530_0955'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameModel( 16 | old_name='Comments', 17 | new_name='Comment', 18 | ), 19 | migrations.RenameModel( 20 | old_name='Quiz', 21 | new_name='QuizOrAssignment', 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /accounts/migrations/0003_auto_20180523_1017.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-05-23 10:17 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 | ('accounts', '0002_auto_20180521_1519'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='user', 17 | name='user_type', 18 | field=models.CharField(choices=[('AD', 'Admin'), ('LA', 'Admin'), ('IN', 'Instructor'), ('ST', 'Student')], max_length=2), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /lms/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url, include 2 | from django.contrib import admin 3 | from .views import landing_page, home_page 4 | from django.conf import settings 5 | from django.conf.urls.static import static 6 | 7 | urlpatterns = [ 8 | url(r'^$', landing_page, name='landing_page'), 9 | url(r'^home/$', home_page, name='home'), 10 | url(r'^accounts/', include(('accounts.urls'), namespace='accounts')), 11 | url(r'^classroom/', include(('classroom.urls'), namespace='classroom')), 12 | url(r'^admin/', admin.site.urls), 13 | 14 | ] 15 | 16 | # if settings.DEBUG: 17 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 18 | -------------------------------------------------------------------------------- /classroom/migrations/0018_auto_20180608_0944.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-06-08 09:44 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('classroom', '0017_auto_20180602_1209'), 12 | ] 13 | 14 | operations = [ 15 | migrations.DeleteModel( 16 | name='Assignment', 17 | ), 18 | migrations.AlterModelOptions( 19 | name='quizorassignment', 20 | options={'permissions': (('modify_quiz_or_assignment', 'Modify quiz or assignment'),)}, 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /classroom/migrations/0005_auto_20180527_0958.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-05-27 09:58 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('classroom', '0004_auto_20180525_1532'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='course', 18 | name='teaching_assistants', 19 | field=models.ManyToManyField(blank=True, related_name='teaching_assistant', to=settings.AUTH_USER_MODEL), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /accounts/migrations/0005_auto_20180524_0633.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-05-24 06:33 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 | ('accounts', '0004_auto_20180524_0626'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='user', 17 | name='user_type', 18 | field=models.CharField(choices=[('AD', 'System Admin'), ('LA', 'LMS Admin'), ('IN', 'Instructor'), ('TA', 'Teaching Assistant'), ('ST', 'Student')], max_length=2), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /accounts/migrations/0006_auto_20180525_1532.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-05-25 15:32 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 | ('accounts', '0005_auto_20180524_0633'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='user', 17 | name='user_type', 18 | field=models.CharField(choices=[('AD', 'System Admin'), ('LA', 'Admin'), ('IN', 'Instructor'), ('TA', 'Teaching Assistant'), ('ST', 'Student')], max_length=2), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /accounts/migrations/0004_auto_20180524_0626.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-05-24 06:26 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 | ('accounts', '0003_auto_20180523_1017'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='user', 17 | name='user_type', 18 | field=models.CharField(choices=[('System Admin', 'AD'), ('Admin', 'LA'), ('Instructor', 'IN'), ('Teaching Assistant', 'TA'), ('Student', 'Student')], max_length=2), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /accounts/migrations/0007_auto_20180608_0944.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-06-08 09:44 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('accounts', '0006_auto_20180525_1532'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='user', 17 | options={'permissions': (('create_lms_admin', 'Create LMS Admin'), ('create_instructor', 'Create Instructor'), ('create_teaching_assistant', 'Create Teaching Assistant'), ('create_student', 'Create Student'))}, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /classroom/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from accounts.models import User 3 | from .models import Comment, Course, QuizOrAssignment, Question, Discussion, Grade 4 | from .forms import CourseForm 5 | 6 | 7 | admin.site.register(Comment) 8 | 9 | class CourseAdmin(admin.ModelAdmin): 10 | 11 | form = CourseForm 12 | list_display = ('code', 'title',) 13 | 14 | class QuizOrAssignmentAdmin(admin.ModelAdmin): 15 | raw_id_fields = ('owner',) 16 | 17 | admin.site.register(QuizOrAssignment, QuizOrAssignmentAdmin) 18 | 19 | 20 | admin.site.register(Course, CourseAdmin) 21 | 22 | admin.site.register(Question) 23 | 24 | 25 | admin.site.register(Discussion) 26 | 27 | admin.site.register(Grade) -------------------------------------------------------------------------------- /classroom/migrations/0008_auto_20180529_0938.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-05-29 09:38 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('classroom', '0007_auto_20180529_0922'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='answer', 17 | name='question', 18 | ), 19 | migrations.RemoveField( 20 | model_name='quiz', 21 | name='comment', 22 | ), 23 | migrations.DeleteModel( 24 | name='Answer', 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /classroom/static/classroom/js/student/discussions.js: -------------------------------------------------------------------------------- 1 | $(".js-add-comment-discussion").click( function () { 2 | button = $(this); 3 | $.ajax({ 4 | url: button.attr("data-href"), 5 | type: 'get', 6 | dataType: 'json', 7 | 8 | success: function (data) { 9 | $("#js-add-new-choice-modal").show(); 10 | $("#display-form-content").html(data.html_form); 11 | } 12 | 13 | }); 14 | }); 15 | 16 | 17 | function timer (count) { 18 | alert(count); 19 | setInterval(function(){ 20 | count--; 21 | document.getElementById('countDown').innerHTML = count; 22 | if (count == 0) { 23 | window.location = '/'; 24 | } 25 | },1000);('Redirect()', 5000); 26 | 27 | }; -------------------------------------------------------------------------------- /classroom/templates/classroom/includes/new_form_modal.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Add new {{ choice }}

4 |
5 | 6 |
7 | 8 |
9 |
10 | 11 | {% if is_formset %} 12 | {% include "classroom/includes/formset.html" with formset=formset %} 13 | {% else %} 14 | {% include "classroom/includes/form.html" with form=form %} 15 | {% endif %} 16 | 17 | 18 | 19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /classroom/migrations/0016_auto_20180602_0845.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-06-02 08:45 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', '0015_auto_20180601_0311'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Assignment', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ], 20 | ), 21 | migrations.RenameField( 22 | model_name='grade', 23 | old_name='grade', 24 | new_name='score', 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /classroom/tests/test_views_lms_admin.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from django.test import TestCase 3 | from accounts.models import User 4 | from django.urls import reverse 5 | from lms.tests.test_views import create_test_user 6 | 7 | 8 | 9 | class TestViews(TestCase): 10 | 11 | def test_forbids_unauthorized_users(self): 12 | instructor = create_test_user('test-user', 'instructor') 13 | self.client.force_login(instructor) 14 | response = self.client.get(reverse('classroom:lms_admin_view', kwargs={'choice':'assignments'})) 15 | self.assertEqual(response.status_code, 403) 16 | 17 | # def test_lms_admin_permissions(self): 18 | # lms_admin = create_test_user('test-user', 'lms-admin') 19 | # self.client.force_login(lms_admin) 20 | # self.client.get() 21 | 22 | 23 | -------------------------------------------------------------------------------- /classroom/migrations/0020_auto_20180608_1607.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-06-08 16:07 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', '0019_auto_20180608_1536'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameField( 16 | model_name='question', 17 | old_name='quiz', 18 | new_name='quiz_or_assignment', 19 | ), 20 | migrations.AlterField( 21 | model_name='grade', 22 | name='score', 23 | field=models.DecimalField(decimal_places=2, default=0, help_text='Score is expressed in percentage', max_digits=100), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /accounts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.conf import settings 3 | from django.contrib.auth.models import AbstractUser 4 | from django.core.urlresolvers import reverse 5 | 6 | # Create your models here. 7 | class User(AbstractUser): 8 | USER_ROLE = ( 9 | ('AD', 'System Admin'), 10 | ('LA', 'Admin'), 11 | ('IN', 'Instructor'), 12 | ('TA', 'Teaching Assistant'), 13 | ('ST', 'Student'), 14 | ) 15 | 16 | user_type = models.CharField(max_length=2, choices=USER_ROLE) 17 | 18 | class Meta: 19 | permissions = ( 20 | ('create_lms_admin', 'Create LMS Admin'), 21 | ('create_instructor', 'Create Instructor'), 22 | ('create_teaching_assistant', 'Create Teaching Assistant'), 23 | ('create_student', 'Create Student'), 24 | ) 25 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "lms.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /classroom/migrations/0017_auto_20180602_1209.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-06-02 12:09 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('classroom', '0016_auto_20180602_0845'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='grade', 18 | name='quiz', 19 | field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='classroom.QuizOrAssignment'), 20 | ), 21 | migrations.AlterField( 22 | model_name='grade', 23 | name='score', 24 | field=models.DecimalField(decimal_places=2, default=0, max_digits=100), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /lms/tests/test_views.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from django.test import TestCase 3 | from accounts.models import User 4 | from django.urls import reverse 5 | 6 | 7 | def create_test_user(username, user_role): 8 | user_type = {'Lms-Admin': 'LA', 'Instructor': 'IN', 9 | 'Student': 'ST'}[user_role.title()] 10 | user = User.objects.create( 11 | username=username, user_type=user_type) 12 | user.set_password=('makemigrations') 13 | user.save() 14 | 15 | return user 16 | 17 | 18 | class TestViews(TestCase): 19 | def test_redirect_index_to_login_page(self): 20 | response = self.client.get('/') 21 | self.assertEqual(response.status_code, 302) 22 | 23 | def test_redirect_anonymous_user_to_login_page(self): 24 | response = self.client.get(reverse('home')) 25 | self.assertEqual(response.url, reverse('accounts:login')) 26 | 27 | 28 | -------------------------------------------------------------------------------- /classroom/templates/classroom/includes/answer_question_modal.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Please, answer the following questions

4 |
5 | 6 |
7 |

Time left: {{ date_of_submission }}

8 |
9 |
10 | 11 | {% if is_formset %} 12 | {% include "classroom/includes/formset.html" with formset=formset %} 13 | {% else %} 14 | {% include "classroom/includes/form.html" with form=form %} 15 | {% endif %} 16 | 17 | 18 | 19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /lms/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect 2 | from django.core.urlresolvers import reverse 3 | from django.core.exceptions import PermissionDenied 4 | from django.contrib.auth.decorators import login_required 5 | 6 | def landing_page(request): 7 | return redirect('accounts:login') 8 | 9 | 10 | @login_required 11 | def home_page(request): 12 | try: 13 | if request.user.user_type == 'LA': 14 | return redirect("classroom:lms_admin_view", choice='lms_admins') 15 | 16 | elif request.user.user_type == 'IN': 17 | return redirect("classroom:instructor_view", choice='assignments') 18 | 19 | elif request.user.user_type == 'ST': 20 | return redirect("classroom:student_view", choice='courses') 21 | 22 | else: 23 | raise PermissionDenied 24 | 25 | except AttributeError: 26 | 27 | return redirect('accounts:login') 28 | -------------------------------------------------------------------------------- /classroom/migrations/0006_auto_20180528_1140.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-05-28 11:40 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('classroom', '0005_auto_20180527_0958'), 13 | ] 14 | 15 | operations = [ 16 | migrations.RenameModel( 17 | old_name='Grades', 18 | new_name='Grade', 19 | ), 20 | migrations.RemoveField( 21 | model_name='quiz', 22 | name='comments', 23 | ), 24 | migrations.AddField( 25 | model_name='quiz', 26 | name='comment', 27 | field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='classroom.Comments'), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /classroom/migrations/0004_auto_20180525_1532.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-05-25 15:32 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ('classroom', '0003_auto_20180523_0854'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AddField( 18 | model_name='course', 19 | name='teaching_assistants', 20 | field=models.ManyToManyField(related_name='teaching_assistant', to=settings.AUTH_USER_MODEL), 21 | ), 22 | migrations.AlterField( 23 | model_name='course', 24 | name='students', 25 | field=models.ManyToManyField(related_name='students', to=settings.AUTH_USER_MODEL), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /accounts/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | # from django.contrib.auth.forms import UserCreationForm 3 | from .models import User 4 | 5 | 6 | class UserCreationForm(forms.ModelForm): 7 | password = forms.CharField(widget=forms.PasswordInput) 8 | password2 = forms.CharField(label='Password Confirmation', widget=forms.PasswordInput) 9 | 10 | class Meta: 11 | model = User 12 | exclude = () 13 | 14 | def clean_password2(self): 15 | password1 = self.cleaned_data.get('password1') 16 | password2 = self.cleaned_data.get('password2') 17 | if password1 != password2: 18 | raise forms.ValidationError('Passwords do not match') 19 | return password2 20 | 21 | def save(self, commit=True): 22 | user = super(UserCreationForm, self).save(commit = False) # Call the real save() method 23 | user.set_password(self.cleaned_data.get('password1')) 24 | if commit: 25 | user.save() 26 | 27 | return user -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | Learning Management System 9 | 10 | 11 | 12 | 13 | 14 | {% block css %} 15 | 16 | {% endblock css %} 17 | 18 | 19 | 20 | 21 | {% block boilerplate_html %} 22 | 23 | 24 | {% endblock boilerplate_html %} 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% block javascript %} 32 | 33 | {% endblock javascript %} 34 | 35 | -------------------------------------------------------------------------------- /classroom/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from .views import lms_admin, instructor, student 3 | 4 | urlpatterns = [ 5 | 6 | url(r'^lms-admin/(?P[\w\-]+)/$', 7 | lms_admin.ChoiceList.as_view(), name='lms_admin_view'), 8 | 9 | url(r'^lms-admin/add/(?P[\w\-]+)/$', 10 | lms_admin.SignUpView.as_view(), name='lms_admin_add'), 11 | 12 | # url(r'^update/(?P[\w\-]+)/(?P\d+)/$',lms.), 13 | 14 | url(r'^instructor/(?P[\w\-]+)/$', 15 | instructor.ChoiceList.as_view(), name='instructor_view'), 16 | 17 | # url used to add a quiz or assignment 18 | url(r'^instructor/add/(?P[\w\-]+)/$', 19 | instructor.Choice.as_view(), name='instructor_add'), 20 | 21 | # url used to add questions to both quiz and assignment 22 | url(r'^instructor/add/(?P[\w\-]+)/(?P\d+)/$', 23 | instructor.QuestionAndCommentHandler.as_view(), name='instructor_add_choice'), 24 | 25 | url(r'^student/(?P[\w\-]+)/$', 26 | student.ChoiceList.as_view(), name='student_view'), 27 | 28 | url(r'^student/answer-question/(?P[\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 | Avatar 15 | 16 |
17 | {% csrf_token %} 18 | 19 | {% if form.non_field_errors %} 20 |
21 | {% for error in form.non_field_errors %} 22 |

{{ error }}

23 | {% endfor %} 24 |
25 | {% endif %} 26 | 27 |
28 | {% with WIDGET_ERROR_CLASS="w3-teal" %} 29 | {% render_field form.username class="w3-input" add_error_class="w3-border-red" name="email" placeholder="Username" %} 30 | {% render_field form.password class="w3-input" add_error_class="w3-border-red" name="pass" placeholder="Password" %} 31 | 32 | {% endwith %} 33 | 34 | 35 | 36 |
37 |
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 | 23 | 24 | 25 | 26 | 27 | 28 | {% for course in courses %} 29 | 30 | 31 | 32 | 33 | 34 | {% empty %} 35 | 36 | 37 | 38 | 39 | 40 | {% endfor %} 41 | 42 | 43 |
Course codeCourse title
{{course.code}}{{course.title}}
No data to display
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 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% for grade in grades %} 32 | 33 | 34 | 35 | 36 | 37 | 38 | {% empty %} 39 | 40 | 41 | 42 | 43 | 44 | {% endfor %} 45 | 46 | 47 |
Name of assignment/quizCourse codeGrade
{{grade.quiz_or_assignment.name}}{{grade.course.code}}{% get_grade_and_percentage grade.score %}
No data to display
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 | 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 | 32 | 33 | {% else %} 34 | 35 | 36 | 37 | {% endif %} 38 | 39 | {% else %} 40 | 41 | 42 | 43 | {% endif %} 44 | 45 | 46 | {% endfor %} 47 | 48 | 49 | 50 | {% endfor %} 51 |
{{ field.label|capfirst }}
{% render_field field class="w3-input w3-round w3-border w3-border-red" %}{% render_field field class="w3-input w3-round w3-border w3-border-green" %}{% render_field field class="w3-input w3-round w3-border" %}
-------------------------------------------------------------------------------- /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 | 22 | 23 | 24 | 25 | 26 | 27 | {% for course in courses %} 28 | 29 | 30 | 31 | 32 | 33 | {% empty %} 34 | 35 | 36 | 37 | 38 | 39 | {% endfor %} 40 | 41 | 42 |
Course codeCourse title
{{course.code}}{{course.title}}
No data to display
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 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% for admin in lms_admins %} 30 | 31 | 32 | 33 | 34 | 35 | 36 | {% empty %} 37 | 38 | 39 | 40 | 41 | 42 | {% endfor %} 43 | 44 | 45 |
First NameLast NameE-mail
{{admin.first_name}}{{admin.last_name}}{{admin.email}}
No data to display
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 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% for student in students %} 31 | 32 | 33 | 34 | 35 | 36 | 37 | {% empty %} 38 | 39 | 40 | 41 | 42 | 43 | {% endfor %} 44 | 45 | 46 |
First NameLast NameE-mail
{{student.first_name}}{{student.last_name}}{{student.email}}
No data to display
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 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% for instructor in instructors %} 30 | 31 | 32 | 33 | 34 | 35 | 36 | {% empty %} 37 | 38 | 39 | 40 | 41 | 42 | {% endfor %} 43 | 44 | 45 |
First NameLast NameE-mail
{{instructor.first_name}}{{instructor.last_name}}{{instructor.email}}
No data to display
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 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% for teaching_assistant in teaching_assistants %} 30 | 31 | 32 | 33 | 34 | 35 | 36 | {% empty %} 37 | 38 | 39 | 40 | 41 | 42 | {% endfor %} 43 | 44 | 45 |
First NameLast NameE-mail
{{teaching_assistant.first_name}}{{teaching_assistant.last_name}}{{teaching_assistant.email}}
No data to display
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 | [![Python Version](https://img.shields.io/badge/python-3.6-brightgreen.svg)](https://python.org)   12 | [![Django Version](https://img.shields.io/badge/django-1.11-brightgreen.svg)](https://djangoproject.com)   13 | [![Virtualenvwrapper](https://img.shields.io/badge/virtualenvwrapper-stable-brightgreen.svg)](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 |
37 | No Question(s) 38 |
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 |
20 | {% for comment in comments %} 21 | 22 | {% if discussion.title == comment.discussion.title %} 23 |

{{ comment |truncatechars:30 }} By: {{ comment.author }}

24 | {% endif %} 25 | 26 | {% empty %} 27 |

No comment(s)

28 | {% endfor %} 29 | 30 | 31 |

32 | 33 |
34 | 35 | {% empty %} 36 | 37 |
38 | No comment(s) 39 |
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 |
37 | No Question(s) 38 |
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 |
21 | {% for comment in comments %} 22 | 23 | {% if discussion.title == comment.discussion.title %} 24 |

{{ comment |truncatechars:30 }} By: {{ comment.author }}

25 | {% endif %} 26 | 27 | {% empty %} 28 |

No comment(s)

29 | {% endfor %} 30 | 31 | 32 |

33 | 34 |
35 | 36 | {% empty %} 37 | 38 |
39 | No comment(s) 40 |
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 |
8 | {% if user.is_authenticated %} 9 |

Welcome {{user.first_name}}, {{user.last_name}} (Logout)

10 | {% else %} 11 | 12 | {% endif %} 13 |

Menu

14 | 15 | 16 | Assignment 17 | Quiz 18 | {% comment %} Grades {% endcomment %} 19 | Discussion 20 | 21 | 22 | 23 |
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 |
8 | {% if user.is_authenticated %} 9 |

Welcome {{user.first_name}}, {{user.last_name}} (Logout)

10 | {% else %} 11 | 12 | {% endif %} 13 |

Menu

14 | 15 | 16 | Admin Staff 17 | Instructor 18 | Students 19 | Courses 20 | 21 | 22 | 23 |
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 |
10 | {% if user.is_authenticated %} 11 |

Welcome {{user.first_name}}, {{user.last_name}} (Logout)

12 | {% else %} 13 | 14 | {% endif %} 15 |

Menu

16 | 17 | Courses 18 | Assignment 19 | Quiz 20 | Grades 21 | Comments 22 | 23 | 24 | 25 |
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 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {% for quiz in pending_quizzes %} 40 | 41 | 42 | 43 | 44 | 45 | 46 | {% empty %} 47 | 48 | 49 | 50 | 51 | 52 | {% endfor %} 53 | 54 | 55 |
Quiz nameCourseDate of submission
{{quiz.name}}{{quiz.course.code}}{{quiz.date_of_submission}}
No data to display
56 | 57 |
58 | 59 | 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 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {% for assignment in pending_assignments %} 40 | 41 | 42 | 43 | 44 | 45 | 46 | {% empty %} 47 | 48 | 49 | 50 | 51 | 52 | {% endfor %} 53 | 54 | 55 |
Assignment nameCourseDate of submission
{{assignment.name}}{{assignment.course.code}}{{assignment.date_of_submission}}
No data to display
56 | 57 |
58 | 59 | 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} --------------------------------------------------------------------------------