├── media └── readme.txt ├── static ├── robots.txt ├── fonts │ ├── custom │ │ ├── custom.ttf │ │ ├── custom.eot │ │ └── custom.woff │ ├── Lato │ │ ├── Lato-Black.ttf │ │ ├── Lato-Bold.ttf │ │ ├── Lato-Light.ttf │ │ ├── Lato-Italic.ttf │ │ ├── Lato-Medium.ttf │ │ ├── Lato-Regular.ttf │ │ ├── Lato-BoldItalic.ttf │ │ ├── Lato-Hairline.ttf │ │ ├── Lato-SemiBold.ttf │ │ ├── Lato-BlackItalic.ttf │ │ ├── Lato-LightItalic.ttf │ │ ├── Lato-MediumItalic.ttf │ │ ├── Lato-HairlineItalic.ttf │ │ └── Lato-SemiboldItalic.ttf │ └── bootstrap │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 ├── images │ ├── 404-error.jpg │ ├── login-page.jpeg │ ├── pwa │ │ ├── icon-72x72.png │ │ ├── icon-96x96.png │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ └── icon-512x512.png │ └── signup-page.jpeg ├── manifest.json └── scripts │ ├── libs │ └── pikaday.jquery.js │ ├── main.js │ └── main.min.js ├── apps ├── common │ ├── __init__.py │ └── paginator.py ├── courses │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── permissions.py │ │ ├── urls.py │ │ ├── serializers.py │ │ └── views.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── delete_courses.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0009_auto_20210829_1816.py │ │ ├── 0007_auto_20200719_1650.py │ │ ├── 0010_auto_20220721_1312.py │ │ ├── 0005_auto_20190224_1658.py │ │ ├── 0004_auto_20190223_2230.py │ │ ├── 0006_cluster.py │ │ ├── 0003_review.py │ │ ├── 0008_badgeaward.py │ │ ├── 0002_auto_20180530_1936.py │ │ └── 0001_initial.py │ ├── templatetags │ │ ├── __init__.py │ │ ├── course.py │ │ ├── gravatar.py │ │ └── badges_tags.py │ ├── tests.py │ ├── apps.py │ ├── tasks.py │ ├── badges.py │ ├── middleware.py │ ├── fields.py │ ├── admin.py │ ├── urls.py │ ├── forms.py │ ├── search.py │ └── models.py ├── students │ ├── __init__.py │ ├── views │ │ ├── __init__.py │ │ ├── classroom.py │ │ └── students.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_remove_tag_subject.py │ │ ├── 0007_auto_20220721_1312.py │ │ ├── 0008_alter_user_first_name.py │ │ ├── 0006_auto_20200821_1729.py │ │ ├── 0004_auto_20190720_1417.py │ │ ├── 0005_profile.py │ │ ├── 0003_auto_20190720_1413.py │ │ └── 0001_initial.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ ├── add_profile_user.py │ │ │ └── enroll_reminder.py │ ├── tests.py │ ├── apps.py │ ├── authentication.py │ ├── middleware.py │ ├── decorators.py │ ├── admin.py │ ├── urls.py │ ├── models.py │ └── forms.py └── __init__.py ├── staticfiles └── robots.txt ├── templates ├── offline.html ├── courses │ ├── content │ │ ├── text.html │ │ ├── image.html │ │ ├── file.html │ │ └── video.html │ ├── manage │ │ ├── course │ │ │ ├── delete.html │ │ │ ├── form.html │ │ │ └── list.html │ │ ├── module │ │ │ ├── formset.html │ │ │ └── content_list.html │ │ └── content │ │ │ └── form.html │ └── course │ │ └── detail.html ├── search │ ├── search_submit.html │ └── search_results.html ├── partial │ └── tabs.html ├── students │ ├── contact │ │ ├── contact_template.txt │ │ └── contact_form.html │ ├── student │ │ ├── take_quiz_form.html │ │ ├── _title.html │ │ ├── interests_form.html │ │ ├── taken_quiz_list.html │ │ └── quiz_list.html │ ├── user │ │ └── detail.html │ ├── course │ │ ├── list.html │ │ └── detail.html │ └── teacher │ │ ├── quiz_add_form.html │ │ ├── question_add_form.html │ │ ├── quiz_delete_confirm.html │ │ ├── question_delete_confirm.html │ │ ├── quiz_results.html │ │ ├── quiz_change_list.html │ │ ├── quiz_change_form.html │ │ └── question_change_form.html ├── registration │ ├── password_reset_email.html │ ├── password_change_done.html │ ├── password_reset_complete.html │ ├── password_reset_done.html │ ├── signup.html │ ├── password_change_form.html │ ├── password_reset_confirm.html │ ├── signup_form.html │ ├── password_reset_form.html │ ├── edit.html │ └── login.html ├── flatpages │ └── default.html ├── about.html ├── 404.html ├── 500.html ├── service-worker.js ├── base2.html └── videos │ └── list.html ├── runtime.txt ├── .bash_profile ├── myelearning ├── __init__.py ├── storage_backends.py ├── wsgi.py ├── celery.py ├── settings_production.py ├── urls.py └── settings.py ├── Procfile ├── .github └── workflows │ └── django.yml ├── requirements.txt ├── requirements-dev.txt ├── manage.py ├── docs └── resources-django.md ├── LICENSE ├── README.md └── .gitignore /media/readme.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /staticfiles/robots.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/offline.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/courses/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/courses/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/students/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/students/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.19 2 | -------------------------------------------------------------------------------- /static/fonts/custom/custom.ttf: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/courses/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/courses/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/courses/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/students/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/students/management/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/courses/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/students/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.bash_profile: -------------------------------------------------------------------------------- 1 | alias git_sync="git pull -r && git push" 2 | alias ll="ls -laG" 3 | -------------------------------------------------------------------------------- /apps/courses/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /apps/students/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /myelearning/__init__.py: -------------------------------------------------------------------------------- 1 | from .celery import app as celery_app 2 | 3 | __all__ = ("celery_app",) 4 | -------------------------------------------------------------------------------- /apps/__init__.py: -------------------------------------------------------------------------------- 1 | from .courses import * 2 | from .students import * 3 | 4 | __all__ = ['courses', 'students'] -------------------------------------------------------------------------------- /static/images/404-error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/images/404-error.jpg -------------------------------------------------------------------------------- /templates/courses/content/text.html: -------------------------------------------------------------------------------- 1 | 2 | {{ item|linebreaks|safe }} 3 | -------------------------------------------------------------------------------- /static/images/login-page.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/images/login-page.jpeg -------------------------------------------------------------------------------- /static/fonts/Lato/Lato-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/fonts/Lato/Lato-Black.ttf -------------------------------------------------------------------------------- /static/fonts/Lato/Lato-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/fonts/Lato/Lato-Bold.ttf -------------------------------------------------------------------------------- /static/fonts/Lato/Lato-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/fonts/Lato/Lato-Light.ttf -------------------------------------------------------------------------------- /static/fonts/custom/custom.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/fonts/custom/custom.eot -------------------------------------------------------------------------------- /static/fonts/custom/custom.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/fonts/custom/custom.woff -------------------------------------------------------------------------------- /static/images/pwa/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/images/pwa/icon-72x72.png -------------------------------------------------------------------------------- /static/images/pwa/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/images/pwa/icon-96x96.png -------------------------------------------------------------------------------- /static/images/signup-page.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/images/signup-page.jpeg -------------------------------------------------------------------------------- /static/fonts/Lato/Lato-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/fonts/Lato/Lato-Italic.ttf -------------------------------------------------------------------------------- /static/fonts/Lato/Lato-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/fonts/Lato/Lato-Medium.ttf -------------------------------------------------------------------------------- /static/fonts/Lato/Lato-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/fonts/Lato/Lato-Regular.ttf -------------------------------------------------------------------------------- /static/images/pwa/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/images/pwa/icon-128x128.png -------------------------------------------------------------------------------- /static/images/pwa/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/images/pwa/icon-144x144.png -------------------------------------------------------------------------------- /static/images/pwa/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/images/pwa/icon-152x152.png -------------------------------------------------------------------------------- /static/images/pwa/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/images/pwa/icon-512x512.png -------------------------------------------------------------------------------- /templates/courses/content/image.html: -------------------------------------------------------------------------------- 1 | 2 |

3 | -------------------------------------------------------------------------------- /static/fonts/Lato/Lato-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/fonts/Lato/Lato-BoldItalic.ttf -------------------------------------------------------------------------------- /static/fonts/Lato/Lato-Hairline.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/fonts/Lato/Lato-Hairline.ttf -------------------------------------------------------------------------------- /static/fonts/Lato/Lato-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/fonts/Lato/Lato-SemiBold.ttf -------------------------------------------------------------------------------- /apps/courses/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoursesConfig(AppConfig): 5 | name = 'apps.courses' 6 | -------------------------------------------------------------------------------- /apps/students/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class StudentsConfig(AppConfig): 5 | name = 'apps.students' 6 | -------------------------------------------------------------------------------- /static/fonts/Lato/Lato-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/fonts/Lato/Lato-BlackItalic.ttf -------------------------------------------------------------------------------- /static/fonts/Lato/Lato-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/fonts/Lato/Lato-LightItalic.ttf -------------------------------------------------------------------------------- /static/fonts/Lato/Lato-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/fonts/Lato/Lato-MediumItalic.ttf -------------------------------------------------------------------------------- /static/fonts/Lato/Lato-HairlineItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/fonts/Lato/Lato-HairlineItalic.ttf -------------------------------------------------------------------------------- /static/fonts/Lato/Lato-SemiboldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/fonts/Lato/Lato-SemiboldItalic.ttf -------------------------------------------------------------------------------- /templates/courses/content/file.html: -------------------------------------------------------------------------------- 1 | 2 |

Download file

3 | -------------------------------------------------------------------------------- /templates/courses/content/video.html: -------------------------------------------------------------------------------- 1 | 2 | {% load embed_video_tags %} 3 | {% video item.url 'small' is_secure=True %} 4 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn myelearning.wsgi:application --preload 2 | worker: celery -A myelearning worker --loglevel=info -B 3 | release: python3 manage.py migrate 4 | -------------------------------------------------------------------------------- /templates/search/search_submit.html: -------------------------------------------------------------------------------- 1 | {% if query %} 2 | {% include 'search/search_results.html' %} 3 | {% else %} 4 |

There are not courses.

5 | {% endif %} -------------------------------------------------------------------------------- /static/fonts/bootstrap/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/fonts/bootstrap/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /static/fonts/bootstrap/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/fonts/bootstrap/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /static/fonts/bootstrap/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/fonts/bootstrap/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /static/fonts/bootstrap/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delitamakanda/elearning/HEAD/static/fonts/bootstrap/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /myelearning/storage_backends.py: -------------------------------------------------------------------------------- 1 | from storages.backends.s3boto3 import S3Boto3Storage 2 | 3 | class MediaStorage(S3Boto3Storage): 4 | location = 'media' 5 | file_overwrite = False 6 | -------------------------------------------------------------------------------- /templates/partial/tabs.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |
3 | {% trans 'Classes' %} 4 |
5 | -------------------------------------------------------------------------------- /templates/students/contact/contact_template.txt: -------------------------------------------------------------------------------- 1 | Contact Name: 2 | {{ contact_name }} 3 | 4 | 5 | Contact Email Address: 6 | {{ contact_email }} 7 | 8 | 9 | Message: 10 | {{ form_content|safe|striptags }} 11 | -------------------------------------------------------------------------------- /apps/courses/api/permissions.py: -------------------------------------------------------------------------------- 1 | from rest_framework.permissions import BasePermission 2 | 3 | class IsEnrolled(BasePermission): 4 | 5 | def has_object_permission(self, request, view, obj): 6 | return obj.students.filter(id=request.user.id).exists() 7 | -------------------------------------------------------------------------------- /apps/courses/templatetags/course.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | @register.filter 6 | def model_name(obj): 7 | try: 8 | return obj._meta.model_name 9 | except AttributeError: 10 | return None 11 | -------------------------------------------------------------------------------- /templates/registration/password_reset_email.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | 3 | {% trans 'Someone asked for password reset for email' %} {{ email }}. 4 | 5 | {% trans 'Follow the link below '%}: {{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %} 6 | 7 | {% trans 'Your username, in case you\'ve forgotten' %}: {{ user.get_username }} 8 | -------------------------------------------------------------------------------- /templates/flatpages/default.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %} 3 | {{ flatpage.title }} 4 | {% endblock %} 5 | 6 | {% block content %} 7 |
8 | 13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /templates/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% load widget_tweaks %} 4 | {% load static %} 5 | {% block title %} 6 | {% trans 'About us' %} 7 | {% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 |

{% trans 'About us' %}

13 | 14 |
15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /templates/search/search_results.html: -------------------------------------------------------------------------------- 1 | {% if query %} 2 |

Recherche: {{ query }}

3 |

{{ title }}

4 | 5 | {% if items %} 6 | 7 | {% for item in items %} 8 |
9 |
10 | 11 | {{ item.title }} 12 | 13 |
14 |
15 | {% endfor %} 16 | 17 | {% endif %} 18 | {% endif %} -------------------------------------------------------------------------------- /myelearning/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for myelearning 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 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myelearning.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /apps/students/migrations/0002_remove_tag_subject.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2018-06-02 18:37 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('students', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='tag', 17 | name='subject', 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /apps/courses/templatetags/gravatar.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from urllib.parse import urlencode 3 | 4 | from django import template 5 | 6 | register = template.Library() 7 | 8 | @register.filter 9 | def gravatar(user): 10 | email = user.email.lower().encode('utf-8') 11 | default = 'retro' 12 | size = 100 13 | url = 'https://www.gravatar.com/avatar/{md5}?{params}'.format( 14 | md5=hashlib.md5(email).hexdigest(), 15 | params=urlencode({'d': default, 's': str(size)}) 16 | ) 17 | 18 | return url 19 | -------------------------------------------------------------------------------- /apps/students/management/commands/add_profile_user.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.management.base import BaseCommand 3 | from apps.students.models import User, Profile 4 | 5 | class Command(BaseCommand): 6 | help = 'add a profile to each user already created' 7 | 8 | def handle(self, *args, **options): 9 | users = User.objects.all() 10 | for u in users: 11 | Profile.objects.get_or_create(user=u) 12 | 13 | self.stdout.write(self.style.SUCCESS('Created {} Profile'.format(u.username))) -------------------------------------------------------------------------------- /apps/students/migrations/0007_auto_20220721_1312.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.28 on 2022-07-21 13:12 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('students', '0006_auto_20200821_1729'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='user', 15 | name='last_name', 16 | field=models.CharField(blank=True, max_length=150, verbose_name='last name'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /apps/students/migrations/0008_alter_user_first_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2024-05-10 23:49 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('students', '0007_auto_20220721_1312'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='user', 15 | name='first_name', 16 | field=models.CharField(blank=True, max_length=150, verbose_name='first name'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /apps/courses/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path as url, include 2 | from rest_framework import routers 3 | from . import views 4 | 5 | router = routers.DefaultRouter() 6 | router.register('courses', views.CourseViewSet) 7 | 8 | urlpatterns = [ 9 | url(r'^subjects/$', views.SubjectListView.as_view(), name='subject_list'), 10 | url(r'^subjects/(?P\d+)/$', views.SubjectDetailView.as_view(), name='subject_detail'), 11 | url(r'^courses/(?P\d+)/enroll/$', views.CourseEnrollView.as_view(), name='course_enroll'), 12 | url(r'^', include(router.urls)), 13 | ] 14 | -------------------------------------------------------------------------------- /apps/courses/migrations/0009_auto_20210829_1816.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.29 on 2021-08-29 18:16 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('courses', '0008_badgeaward'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='cluster', 17 | name='users', 18 | ), 19 | migrations.DeleteModel( 20 | name='Cluster', 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /apps/courses/tasks.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from celery import shared_task 4 | from celery.utils.log import get_task_logger 5 | 6 | from django.core import management 7 | 8 | logger = get_task_logger(__name__) 9 | 10 | @shared_task 11 | def task_example(task_type): 12 | time.sleep(int(task_type) * 10) 13 | return True 14 | 15 | 16 | @shared_task() 17 | def user_email_reminder(): 18 | try: 19 | """ 20 | envoie un email aux users ne s'étant pas connecté depuis 2 semaines 21 | """ 22 | management.call_command("enroll_reminder", "20", verbosity=0) 23 | except: 24 | print("error") 25 | -------------------------------------------------------------------------------- /myelearning/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from celery import Celery 4 | from celery.schedules import crontab 5 | 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myelearning.settings') 7 | 8 | app = Celery('myelearning') 9 | 10 | app.config_from_object('django.conf:settings', namespace='CELERY') 11 | 12 | app.autodiscover_tasks() 13 | 14 | app.conf.beat_schedule = { 15 | 'user_email_reminder_every_week': { 16 | 'task': 'courses.tasks.user_email_reminder', 17 | 'schedule': crontab(hour=7, minute=30, day_of_week=1), 18 | 'args': () 19 | } 20 | } 21 | 22 | app.conf.timezone = 'UTC' 23 | -------------------------------------------------------------------------------- /templates/courses/manage/course/delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Delete course{% endblock %} 3 | {% block content %} 4 |
5 |

Delete course "{{ object.title }}"

6 |
7 |
8 | {% csrf_token %} 9 |

Are you sure you want to delete "{{ object }}"?

10 | 11 |
12 |
13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /apps/courses/migrations/0007_auto_20200719_1650.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.21 on 2020-07-19 16:50 3 | from __future__ import unicode_literals 4 | 5 | import autoslug.fields 6 | from django.db import migrations 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('courses', '0006_cluster'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='course', 18 | name='slug', 19 | field=autoslug.fields.AutoSlugField(editable=False, populate_from='title', unique_with=('created__month',)), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /apps/students/authentication.py: -------------------------------------------------------------------------------- 1 | from apps.students.models import User 2 | 3 | class EmailAuthBackend(object): 4 | """ 5 | Authenticate using an e-mail address 6 | """ 7 | def authenticate(self, request, username=None, password=None): 8 | try: 9 | user = User.objects.get(email=username) 10 | if user.check_password(password): 11 | return user 12 | return None 13 | except User.DoesNotExist: 14 | return None 15 | 16 | def get_user(self, user_id): 17 | try: 18 | return User.objects.get(pk=user_id) 19 | except User.DoesNotExist: 20 | return None -------------------------------------------------------------------------------- /apps/courses/migrations/0010_auto_20220721_1312.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.28 on 2022-07-21 13:12 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('courses', '0009_auto_20210829_1816'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='content', 16 | name='content_type', 17 | field=models.ForeignKey(limit_choices_to={'model__in': ('text', 'video', 'image', 'file')}, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /apps/courses/migrations/0005_auto_20190224_1658.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.20 on 2019-02-24 16:58 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 | ('courses', '0004_auto_20190223_2230'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='review', 18 | name='course', 19 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to='courses.Course'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /templates/registration/password_change_done.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load i18n %} 3 | {% block title %}{% trans 'Password changed' %}{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 | 9 |
10 | 11 |
12 | 13 |

{% trans 'Password changed' %}

14 | 15 |

{% trans 'Your password has been successfully changed' %}.

16 |
17 |
18 |
19 | 20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /templates/students/student/take_quiz_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% load crispy_forms_tags %} 4 | {% block title %} 5 | {{ quiz.name }} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 |
11 |

{{ quiz.name }}

12 |

{{ question.text }}

13 |
14 | {% csrf_token %} 15 | {{ form|crispy }} 16 | 17 |
18 |
19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /templates/students/student/_title.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |

{% trans 'Training' %}

3 |

4 | {% trans 'Tags' %} : {% for tag in user.student.interests.all %} {{ tag.get_html_badge }} {% endfor %} 5 | (update interests) 6 |

7 | 8 | 16 | -------------------------------------------------------------------------------- /templates/registration/password_reset_complete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load i18n %} 3 | {% block title %}{% trans 'Password reset' %}{% endblock %} 4 | 5 | {% block content %} 6 |
7 | 8 |
9 | 10 |
11 |
12 |

{% trans 'Your password has been set.' %}

13 | 14 |

{% trans 'You can ' %} {% trans 'login now' %}

15 |
16 |
17 |
18 | 19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /templates/students/user/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | {% load gravatar %} 4 | {% load badges_tags %} 5 | {% load i18n %} 6 | {% block title %} 7 | {{ ouser.username }} 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
12 |
13 | {{ ouser.username }} 14 |
15 |
{{ ouser.profile.award_points }} {% trans 'award points' %}
16 |
{{ ouser|badge_level_user }}
17 |
{{ ouser.profile.location }}
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /apps/courses/migrations/0004_auto_20190223_2230.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.20 on 2019-02-23 22:30 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 | ('courses', '0003_review'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AlterField( 18 | model_name='review', 19 | name='user_name', 20 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviewers', to=settings.AUTH_USER_MODEL), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base2.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block title %} 5 | {% trans '404 - Page Not Found' %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 |

{% trans '404 - Page Not Found' %}

11 | 404 Resource Not Found 12 |
13 |
14 | {% trans 'Take me back to' %} 15 | {% trans 'awesome courses' %}! 16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% block title %} 4 | {% trans '500 - Server Error' %} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |

{% trans '500 - Server Error' %}

10 |

{% trans 'Something went wrong with the server. Comeback later cause we fix it.' %}

11 |
12 |
13 | {% trans 'Take me back to' %} 14 | {% trans 'awesome courses' %}! 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /templates/students/student/interests_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% block title %} 4 | {% trans 'Update your interests' %} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |

{% trans 'Update your interests' %}

10 |
11 |
12 | {% csrf_token %} 13 | {{ form.as_p }} 14 | 15 | {% trans 'Cancel' %} 16 |
17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /.github/workflows/django.yml: -------------------------------------------------------------------------------- 1 | name: Django CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | max-parallel: 4 15 | matrix: 16 | python-version: [3.9] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install Dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r requirements.txt 28 | - name: Run Tests 29 | run: | 30 | python manage.py test 31 | -------------------------------------------------------------------------------- /apps/students/migrations/0006_auto_20200821_1729.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.29 on 2020-08-21 17:29 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 | ('students', '0005_profile'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='profile', 17 | name='birthdate', 18 | field=models.DateField(blank=True, null=True), 19 | ), 20 | migrations.AddField( 21 | model_name='profile', 22 | name='location', 23 | field=models.CharField(blank=True, max_length=30), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /templates/courses/manage/module/formset.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% block title %} 4 | {% trans 'Edit' %} "{{ course.title }}" 5 | {% endblock %} 6 | {% block content %} 7 |
8 |

Edit "{{ course.title }}"

9 |
10 |

Course modules

11 |
12 | {{ formset.as_p }} 13 | {{ formset.management_form.as_p }} 14 | {% csrf_token %} 15 | 16 |
17 |
18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi 2 | chardet 3 | Django==4.2.24 4 | django-braces==1.15.0 5 | django-embed-video 6 | djangorestframework==3.15.2 7 | idna 8 | olefile 9 | Pillow==10.3.0 10 | python3-memcached 11 | pytz 12 | whitenoise==5.0.1 13 | dj-database-url 14 | gunicorn 15 | psycopg2-binary 16 | python-decouple 17 | django-redis 18 | django-storages 19 | boto3 20 | django-widget-tweaks==1.4.3 21 | google-api-python-client==1.7.4 22 | numpy==1.22.0 23 | pandas==1.1.5 24 | scikit-learn==1.5.0 25 | scipy==1.10.0 26 | sklearn==0.0 27 | django-webpack-loader==0.6.0 28 | django-cors-headers==4.2.0 29 | Markdown==3.1.1 30 | django-taggit==1.1.0 31 | django-taggit-serializer==0.1.7 32 | mistune==2.0.3 33 | pygments==2.15.0 34 | celery==5.4.0 35 | redis==4.4.4 36 | django-autoslug==1.9.8 37 | django-crispy-forms==1.9.2 38 | pymemcache==4.0.0 39 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | certifi==2024.7.4 2 | chardet==3.0.4 3 | Django==4.2.24 4 | django-braces==1.15.0 5 | django-embed-video==1.0.0 6 | django-memcache-status==1.1 7 | djangorestframework==3.15.2 8 | idna==3.7 9 | olefile==0.44 10 | Pillow==10.3.0 11 | python3-memcached 12 | pytz==2017.2 13 | requests==2.32.4 14 | six==1.11.0 15 | urllib3==2.6.0 16 | whitenoise==5.0.1 17 | python-decouple==3.1 18 | django-widget-tweaks==1.4.3 19 | google-api-python-client==1.7.4 20 | numpy==1.22.0 21 | pandas==1.2.1 22 | scikit-learn==1.5.0 23 | scipy==1.10.0 24 | sklearn==0.0 25 | django-storages==1.7.1 26 | django-webpack-loader==0.6.0 27 | django-cors-headers==4.2.0 28 | Markdown==3.1.1 29 | django-taggit==1.1.0 30 | django-taggit-serializer==0.1.7 31 | mistune==2.0.3 32 | pygments==2.15.0 33 | celery==5.4.0 34 | redis==4.4.4 35 | django-autoslug==1.9.8 36 | pymemcache==4.0.0 37 | -------------------------------------------------------------------------------- /templates/registration/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load i18n %} 3 | {% block title %}{% trans 'Reset your password' %}{% endblock %} 4 | 5 | {% block content %} 6 |
7 |
8 | 9 |
10 | 11 |
12 | 13 |

{% trans 'Reset your password' %}

14 | 15 |

{% trans 'We\'ve emailed you instructions for setting your password.' %}

16 | 17 |

{% trans 'If you don\'t receive an email, please make sure you\'ve entered the address you registered with.' %}

18 |
19 |
20 |
21 |
22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /apps/courses/migrations/0006_cluster.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.20 on 2019-02-24 21:45 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 | ('courses', '0005_auto_20190224_1658'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Cluster', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('name', models.CharField(max_length=100)), 22 | ('users', models.ManyToManyField(to=settings.AUTH_USER_MODEL)), 23 | ], 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /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", "myelearning.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 | -------------------------------------------------------------------------------- /apps/students/migrations/0004_auto_20190720_1417.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.21 on 2019-07-20 14:17 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('students', '0003_auto_20190720_1413'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='comment', 17 | name='post', 18 | ), 19 | migrations.RemoveField( 20 | model_name='post', 21 | name='author', 22 | ), 23 | migrations.RemoveField( 24 | model_name='post', 25 | name='tags', 26 | ), 27 | migrations.DeleteModel( 28 | name='Comment', 29 | ), 30 | migrations.DeleteModel( 31 | name='Post', 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /apps/students/migrations/0005_profile.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.29 on 2020-08-21 16:36 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 | ('students', '0004_auto_20190720_1417'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Profile', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('award_points', models.PositiveIntegerField(default=0)), 22 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 23 | ], 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /docs/resources-django.md: -------------------------------------------------------------------------------- 1 | 2 | * https://tailwindui.com/components 3 | * https://vimeo.com/393580241/82c6d7c5f6 4 | * https://tailwindui.com/page-examples/detail-view-01 5 | * https://tailwindui.com/page-examples/landing-page-01 6 | * https://florian-dahlitz.de/blog/build-a-markdown-to-html-conversion-pipeline-using-python 7 | * https://dohliam.github.io/dropin-minimal-css/ 8 | * https://github.com/xz/new.css#Customizing 9 | * https://www.mattlayman.com/blog/2020/tailwind-django-heroku/ 10 | * https://tailblocks.cc/ 11 | 12 | 1. https://testdriven.io/blog/django-performance-testing/ 13 | 2. https://codyhouse.co/blog/post/multi-line-text-background 14 | 3. https://www.quora.com/How-can-I-implement-a-Django-notification-system-Please-with-example-code 15 | 4. https://css-tricks.com/building-a-conference-schedule-with-css-grid/ 16 | 5. https://webkit.org/blog/8840/dark-mode-support-in-webkit/ 17 | 6. https://realpython.com/get-started-with-django-1/ 18 | -------------------------------------------------------------------------------- /templates/courses/manage/course/form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %} 3 | {% if object %} 4 | Edit course "{{ object.title }}" 5 | {% else %} 6 | Create a new course 7 | {% endif %} 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
12 |

13 | {% if object %} 14 | Edit course "{{ object.title }}" 15 | {% else %} 16 | Create a new course 17 | {% endif %} 18 |

19 | 20 |
21 |

Course info

22 |
23 | {{ form.as_p }} 24 | {% csrf_token %} 25 |

26 |
27 |
28 |
29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /templates/courses/manage/content/form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %} 3 | {% if object %} 4 | Edit content "{{ object.title }}" 5 | {% else %} 6 | Add a new content 7 | {% endif %} 8 | {% endblock %} 9 | {% block content %} 10 |
11 |

12 | {% if object %} 13 | Edit content "{{ object.title }}" 14 | {% else %} 15 | Add a new content 16 | {% endif %} 17 |

18 |
19 |

Course info

20 |
21 | {{ form.as_p }} 22 | {% csrf_token %} 23 |

24 |
25 |
26 |
27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /apps/courses/badges.py: -------------------------------------------------------------------------------- 1 | # https://github.com/pinax/pinax-badges/blob/master/pinax/badges/base.py 2 | import datetime 3 | from django.utils import timezone 4 | from apps.courses.models import BadgeAward 5 | 6 | def possibly_award_badge(events, user): 7 | points = user.profile.award_points 8 | now = timezone.now() 9 | last_minute = now - datetime.timedelta(seconds=60) 10 | similar_awards = BadgeAward.objects.filter(user=user, slug=events, awarded_at__gte=last_minute) 11 | 12 | awarded_lvl = 1 13 | if points > 30000: 14 | awarded_lvl = 4 15 | elif points > 10000: 16 | awarded_lvl = 3 17 | elif points > 7500: 18 | awarded_lvl = 2 19 | elif points > 5000: 20 | awarded_lvl = 1 21 | 22 | if not similar_awards: 23 | badge = BadgeAward( 24 | user=user, 25 | slug=events, 26 | level=awarded_lvl 27 | ) 28 | badge.save() 29 | return True 30 | return False 31 | -------------------------------------------------------------------------------- /apps/courses/templatetags/badges_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from apps.courses.models import BadgeAward 3 | 4 | register = template.Library() 5 | 6 | @register.simple_tag 7 | def badge_count(user): 8 | return BadgeAward.objects.filter(user=user).count() 9 | 10 | 11 | @register.simple_tag 12 | def badges_for_user(user): 13 | return BadgeAward.objects.filter(user=user).order_by('-awarded_at') 14 | 15 | 16 | @register.filter 17 | def badge_level_user(user): 18 | try: 19 | badge = BadgeAward.objects.filter(user=user).order_by('-awarded_at').first() 20 | levels = [ 21 | "Bronze", 22 | "Silver", 23 | "Gold", 24 | "Platinum", 25 | ] 26 | for x in levels: 27 | lvl_idx = levels.index(x) 28 | lvl_range = lvl_idx + 1 29 | if lvl_range == badge.level: 30 | return levels[lvl_idx] 31 | except AttributeError: 32 | return levels[0] 33 | -------------------------------------------------------------------------------- /apps/students/middleware.py: -------------------------------------------------------------------------------- 1 | # https://github.com/LabD/django-session-timeout/ 2 | import time 3 | 4 | from django.conf import settings 5 | from django.shortcuts import redirect 6 | 7 | try: 8 | from django.utils.deprecation import MiddlewareMixin 9 | except ImportError: 10 | MiddlewareMixin = object 11 | 12 | 13 | SESSION_TIMEOUT_KEY = '_session_init_timestamp_' 14 | 15 | 16 | class SessionTimeoutMiddleware(MiddlewareMixin): 17 | def process_request(self, request): 18 | if not hasattr(request, 'session') or request.session.is_empty(): 19 | return 20 | 21 | init_time = request.session.setdefault(SESSION_TIMEOUT_KEY, time.time()) 22 | expire_seconds = getattr( 23 | settings, 'SESSION_EXPIRE_SECONDS', settings.SESSION_COOKIE_AGE) 24 | 25 | session_is_expired = time.time() - init_time > expire_seconds 26 | 27 | if session_is_expired: 28 | request.session.flush() 29 | return redirect('/') 30 | -------------------------------------------------------------------------------- /templates/students/course/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% block title %}My courses{% endblock %} 4 | {% block content %} 5 |
6 |

{% trans 'My courses' %}

7 |
8 | {% for course in object_list %} 9 | {% if course.modules.count > 0 %} 10 |
11 |

{{ course.title }}

12 |

{% trans 'Access contents' %}

13 |
14 | {% endif %} 15 | {% empty %} 16 |

17 | {% trans 'You are not enrolled in any courses yet.' %} 18 | {% trans 'Browse courses' %} {% trans 'to enroll in a course.' %} 19 |

20 | {% endfor %} 21 |
22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /apps/courses/middleware.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from django.shortcuts import get_object_or_404, redirect 3 | from .models import Course 4 | from django.http import HttpResponse 5 | try: 6 | from django.utils.deprecation import MiddlewareMixin 7 | except ImportError: 8 | MiddlewareMixin = object 9 | 10 | class SubdomainCourseMiddleware(MiddlewareMixin): 11 | """ 12 | Provides subdomains for courses 13 | """ 14 | 15 | def __init__(self, get_response): 16 | self.get_response = get_response 17 | 18 | def process_request(self, request): 19 | host_parts = request.get_host().split('.') 20 | if len(host_parts) > 2 and host_parts[0] != 'www': 21 | # get course for the given subdomain 22 | course = get_object_or_404(Course, slug=host_parts[0]) 23 | course_url = reverse('courses:course_detail', args=[course.slug]) 24 | # redirect current request to the course_detail view 25 | url = '{}://{}{}'.format(request.scheme, '.'.join(host_parts[1:]), course_url) 26 | 27 | return redirect(url) 28 | -------------------------------------------------------------------------------- /apps/courses/migrations/0003_review.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.20 on 2019-02-23 15:05 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 | ('courses', '0002_auto_20180530_1936'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Review', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('pub_date', models.DateTimeField(auto_now_add=True)), 21 | ('user_name', models.CharField(max_length=100)), 22 | ('comment', models.CharField(max_length=200)), 23 | ('rating', models.IntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5')])), 24 | ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='courses.Course')), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /apps/common/paginator.py: -------------------------------------------------------------------------------- 1 | from django.core.paginator import Paginator 2 | from django.utils.functional import cached_property 3 | from django.db import connection, transaction, OperationalError 4 | 5 | class TimeLimitedPaginator(Paginator): 6 | 7 | """ 8 | Paginator that enforces a timeout on the count operation. 9 | If the operations times out, a fake bogus value will be returned is returned instead 10 | """ 11 | 12 | @cached_property 13 | def count(self): 14 | """ 15 | set timeout to in a db transaction to prevent it from affecting other transactions. 16 | """ 17 | try: 18 | with transaction.atomic(), connection.cursor() as cursor: 19 | cursor.execute('SET LOCAL statement_timeout TO 200;') 20 | return super().count 21 | except OperationalError: 22 | return 9999999999 23 | 24 | 25 | class DumbPaginator(Paginator): 26 | """ 27 | Paginator that does count the rows of a given table 28 | """ 29 | @cached_property 30 | def count(self): 31 | return 9999999999 32 | -------------------------------------------------------------------------------- /static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "My elearning", 3 | "short_name": "elearning", 4 | "description": "Learn to code anywhere", 5 | "display": "standalone", 6 | "orientation": "portrait", 7 | "background_color": "#FFFFFF", 8 | "theme_color": "#222222", 9 | "start_url": "/", 10 | "scope": ".", 11 | "icons": [ 12 | { 13 | "src": "images/pwa/icon-72x72.png", 14 | "sizes": "72x72", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "images/pwa/icon-96x96.png", 19 | "sizes": "96x96", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "images/pwa/icon-128x128.png", 24 | "sizes": "128x128", 25 | "type": "image/png" 26 | }, 27 | { 28 | "src": "images/pwa/icon-144x144.png", 29 | "sizes": "144x144", 30 | "type": "image/png" 31 | }, 32 | { 33 | "src": "images/pwa/icon-152x152.png", 34 | "sizes": "152x152", 35 | "type": "image/png" 36 | }, 37 | { 38 | "src": "images/pwa/icon-512x512.png", 39 | "sizes": "512x512", 40 | "type": "image/png" 41 | } 42 | ], 43 | "splash_pages": null 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Délita Makanda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # my-elearning 2 | 3 | A Django-based e-learning platform built with Python 3. 4 | 5 | [![Django CI](https://github.com/delitamakanda/elearning/actions/workflows/django.yml/badge.svg?branch=master)](https://github.com/delitamakanda/elearning/actions/workflows/django.yml) 6 | 7 | ## Features 8 | - Create and organize courses with modules and lessons 9 | - Students can enroll and track their progress 10 | - Background task processing with Celery 11 | - Full-text search across courses 12 | 13 | ## Getting Started 14 | 15 | ### Installation 16 | ```bash 17 | pip install -r requirements.txt 18 | python manage.py migrate 19 | python manage.py createsuperuser 20 | ``` 21 | 22 | ### Run the project 23 | ```bash 24 | python manage.py runserver 25 | celery -A myelearning worker -l info -B 26 | ``` 27 | 28 | ## Running Tests 29 | ```bash 30 | python manage.py test 31 | ``` 32 | 33 | ## TODO 34 | - [ ] Nice layout 35 | - [ ] Login via Google 36 | - [x] API 37 | - [x] Celery worker 38 | - [x] Reset Password 39 | - [x] Search Form 40 | 41 | ## License 42 | 43 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 44 | 45 | -------------------------------------------------------------------------------- /templates/students/teacher/quiz_add_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load crispy_forms_tags %} 3 | {% load i18n %} 4 | {% block title %} 5 | {% trans 'Add Quiz' %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 |
11 | 17 | 18 |

{% trans 'Add a new quiz' %}

19 | 20 |
21 | {% csrf_token %} 22 | {{ form|crispy }} 23 | 24 | {% trans 'Go back' %} 25 |
26 |
27 |
28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /apps/courses/migrations/0008_badgeaward.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.29 on 2020-08-21 15:16 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 | ('courses', '0007_auto_20200719_1650'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='BadgeAward', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('awarded_at', models.DateTimeField(default=django.utils.timezone.now)), 24 | ('slug', models.CharField(max_length=255)), 25 | ('level', models.IntegerField()), 26 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='badges_earned', to=settings.AUTH_USER_MODEL)), 27 | ], 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /apps/students/management/commands/enroll_reminder.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from django.core.management.base import BaseCommand 3 | from django.core.mail import send_mass_mail 4 | from django.conf import settings 5 | from apps.students.models import User 6 | from django.db.models import Count 7 | 8 | class Command(BaseCommand): 9 | help = 'Sends an e-mail reminder to users registered more than 20 days that are not enrolled into any courses yet' 10 | 11 | def add_arguments(self, parser): 12 | parser.add_argument('--days', dest='days', type=int) 13 | 14 | def handle(self, *args, **options): 15 | emails = [] 16 | subject = 'Enroll in a course' 17 | date_joined = datetime.date.today() - datetime.timedelta(days=options['days']) 18 | users = User.objects.annotate(course_count=Count('courses_joined')).filter(course_count=0, date_joined__lte=date_joined) 19 | for user in users: 20 | message = 'Dear %s, \n\nWe noticed that you didn\'t enroll in any courses yet. What are you waiting for ?' % user.username 21 | emails.append((subject, message, settings.DEFAULT_FROM_EMAIL, [user.email])) 22 | send_mass_mail(emails) 23 | self.stdout.write('Sent %s reminders' % len(emails)) 24 | -------------------------------------------------------------------------------- /apps/courses/management/commands/delete_courses.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from django.utils import timezone 3 | from django.core.management.base import BaseCommand, CommandError 4 | from apps.courses.models import Course 5 | 6 | class Command(BaseCommand): 7 | help = 'Clears courses older than 365 days from today' 8 | 9 | def add_arguments(self, parser): 10 | parser.add_argument('duration') 11 | 12 | def handle(self, *args, **options): 13 | if options['duration'] == 'month': 14 | number_of_days = 30 15 | elif options['duration'] == '3month': 16 | number_of_days = 90 17 | elif options['duration'] == '6month': 18 | number_of_days = 180 19 | elif options['duration'] == '1year': 20 | number_of_days = 365 21 | else: 22 | number_of_days = 30 23 | 24 | self.stdout.write(self.style.SUCCESS('Number of days to delete "%s"' % number_of_days)) 25 | 26 | today = timezone.now() 27 | past_date = today - timedelta(days=number_of_days); 28 | 29 | # this ensures we don't bother running through already marked true 30 | # objects as deleted. 31 | to_delete = Course.objects.filter(created__lte=past_date) 32 | 33 | for item in to_delete: 34 | item.delete() 35 | 36 | self.stdout.write(self.style.SUCCESS('Removed "%s"' % to_delete)) -------------------------------------------------------------------------------- /apps/courses/fields.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.core.exceptions import ObjectDoesNotExist 3 | 4 | class OrderField(models.PositiveIntegerField): 5 | 6 | def __init__(self, for_fields=None, *args, **kwargs): 7 | self.for_fields = for_fields 8 | super(OrderField, self).__init__(*args, **kwargs) 9 | 10 | def pre_save(self, model_instance, add): 11 | if getattr(model_instance, self.attname) is None: 12 | # no current value 13 | try: 14 | qs = self.model.objects.all() 15 | if self.for_fields: 16 | # filter by objects with the same field values 17 | # for the fields in "for_fields" 18 | query = {field: getattr(model_instance, field) for field in self.for_fields} 19 | qs = qs.filter(**query) 20 | # get the order of the last item 21 | last_item = qs.latest(self.attname) 22 | value = last_item.order + 1 23 | except ObjectDoesNotExist: 24 | value = 0 25 | setattr(model_instance, self.attname, value) 26 | return value 27 | else: 28 | return super(OrderField, self).pre_save(model_instance, add) 29 | -------------------------------------------------------------------------------- /apps/students/decorators.py: -------------------------------------------------------------------------------- 1 | # https://github.com/sibtc/django-multiple-user-types-example 2 | from django.contrib.auth import REDIRECT_FIELD_NAME 3 | from django.contrib.auth.decorators import user_passes_test 4 | 5 | 6 | def student_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url='login'): 7 | ''' 8 | Decorator for views that checks that the logged in user is a student, 9 | redirects to the log-in page if necessary. 10 | ''' 11 | actual_decorator = user_passes_test( 12 | lambda u: u.is_active and u.is_student, 13 | login_url=login_url, 14 | redirect_field_name=redirect_field_name 15 | ) 16 | if function: 17 | return actual_decorator(function) 18 | return actual_decorator 19 | 20 | 21 | def teacher_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url='login'): 22 | ''' 23 | Decorator for views that checks that the logged in user is a teacher, 24 | redirects to the log-in page if necessary. 25 | ''' 26 | actual_decorator = user_passes_test( 27 | lambda u: u.is_active and u.is_teacher, 28 | login_url=login_url, 29 | redirect_field_name=redirect_field_name 30 | ) 31 | if function: 32 | return actual_decorator(function) 33 | return actual_decorator 34 | -------------------------------------------------------------------------------- /templates/students/course/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% block title %} 4 | {{ object.title }} 5 | {% endblock %} 6 | {% block content %} 7 |
8 |

9 | {{ module.title }} 10 |

11 |
12 |
27 |
28 |
29 | {% for content in module.contents.all %} 30 |
31 | {% with item=content.item %} 32 | 33 |

{{ item.title }}

34 | {{ item.render }} 35 | 36 | {% endwith %} 37 |
38 | {% endfor %} 39 |
40 |
41 |
42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /templates/students/teacher/question_add_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% load crispy_forms_tags %} 4 | {% block title %} 5 | {% trans 'New question' %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 |
11 | 18 | 19 |

{% trans 'Add question' %}

20 |

{% trans 'First add the text of the question. Next step you will be able to add answers.' %}

21 | 22 |
23 | {% csrf_token %} 24 | {{ form|crispy }} 25 | 26 | {% trans 'Go back' %} 27 |
28 |
29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /templates/students/teacher/quiz_delete_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% block title %} 4 | {% trans 'Confirm quiz delete ?' %} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 | 17 | 18 |

{% trans 'Confirm quiz delete ?' %}

19 |

{% trans 'Are your sure you want to delete the quiz' %} {{ quiz.name }} ? {% trans 'There is no recovery.' %}

20 |
21 | {% csrf_token %} 22 | 23 | {% trans 'Go back' %} 24 |
25 |
26 |
27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /templates/students/student/taken_quiz_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% block title %} 4 | {% trans 'Taken quiz' %} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |

{% trans 'Taken quiz' %}

10 |
11 | {% include 'students/student/_title.html' with active='taken' %} 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for taken_quiz in taken_quizzes %} 24 | 25 | 26 | 27 | 28 | 29 | {% empty %} 30 | 31 | 32 | 33 | {% endfor %} 34 | 35 |
{% trans 'Quiz' %}{% trans 'Tag' %}{% trans 'Score' %}
{{ taken_quiz.quiz.name }}{{ taken_quiz.quiz.get_html_badge }}{{ taken_quiz.score }} %
{% trans 'No quiz completed yet.' %}
36 |
37 |
38 |
39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /templates/students/teacher/question_delete_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% block title %} 4 | {% trans 'Confirm question delete ?' %} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 | 18 | 19 |

{% trans 'Confirm question delete ?' %}

20 |

{% trans 'Are your sure you want to delete the question' %} {{ question.text }} ? {% trans 'There is no recovery.' %}

21 |
22 | {% csrf_token %} 23 | 24 | {% trans 'Go back' %} 25 |
26 |
27 |
28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /apps/students/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | from apps.students.models import (User, Quiz, Question, Answer, Student, TakenQuiz, Profile, StudentAnswer, Tag) 4 | from apps.common.paginator import DumbPaginator, TimeLimitedPaginator 5 | 6 | class TagAdmin(admin.ModelAdmin): 7 | model = Tag 8 | 9 | 10 | class StudentAnswerAdmin(admin.ModelAdmin): 11 | paginator = TimeLimitedPaginator 12 | model = StudentAnswer 13 | 14 | class AnswerInline(admin.StackedInline): 15 | model = Answer 16 | 17 | class QuestionAdmin(admin.ModelAdmin): 18 | inlines = [AnswerInline] 19 | 20 | 21 | class ProfileInline(admin.StackedInline): 22 | model = Profile 23 | can_delete = False 24 | verbose_name_plural = 'Profile' 25 | fk_name = 'user' 26 | 27 | 28 | class CustomUserAdmin(admin.ModelAdmin): 29 | paginator = TimeLimitedPaginator 30 | inlines = (ProfileInline,) 31 | model = User 32 | list_display = ['username', 'email'] 33 | list_filter = ['is_teacher', 'is_student', 'is_staff'] 34 | 35 | def get_inline_instances(self, request, obj=None): 36 | if not obj: 37 | return list() 38 | return super(CustomUserAdmin, self).get_inline_instances(request, obj) 39 | 40 | admin.site.register(User, CustomUserAdmin) 41 | admin.site.register(Quiz) 42 | admin.site.register(Question, QuestionAdmin) 43 | admin.site.register(Student) 44 | admin.site.register(TakenQuiz) 45 | admin.site.register(StudentAnswer, StudentAnswerAdmin) 46 | admin.site.register(Tag, TagAdmin) 47 | # admin.site.register(Profile) 48 | -------------------------------------------------------------------------------- /apps/courses/api/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from ..models import Subject, Course, Module, Content 3 | 4 | class ItemRelatedField(serializers.RelatedField): 5 | 6 | def to_representation(self, value): 7 | return value.render() 8 | 9 | 10 | class ContentSerializer(serializers.ModelSerializer): 11 | item = ItemRelatedField(read_only=True) 12 | 13 | class Meta: 14 | model = Content 15 | fields = ('order', 'item',) 16 | 17 | 18 | class ModuleWithContentsSerializer(serializers.ModelSerializer): 19 | contents = ContentSerializer(many=True) 20 | 21 | class Meta: 22 | model = Module 23 | fields = ('order', 'title', 'description', 'contents',) 24 | 25 | class CourseWithContentsSerializer(serializers.ModelSerializer): 26 | modules = ModuleWithContentsSerializer(many=True) 27 | 28 | class Meta: 29 | model = Course 30 | fields = ('id', 'subject', 'title', 'slug', 'overview', 'created', 'owner', 'modules', ) 31 | 32 | 33 | class SubjectSerializer(serializers.ModelSerializer): 34 | 35 | class Meta: 36 | model = Subject 37 | fields = ('id', 'title', 'slug',) 38 | 39 | 40 | class ModuleSerializer(serializers.ModelSerializer): 41 | 42 | class Meta: 43 | model = Module 44 | fields = ('order', 'title', 'description', ) 45 | 46 | 47 | 48 | class CourseSerializer(serializers.ModelSerializer): 49 | modules = ModuleSerializer(many=True, read_only=True) 50 | 51 | class Meta: 52 | model = Course 53 | fields = ('id', 'subject', 'title', 'slug', 'overview', 'created', 'overview', 'created', 'owner', 'modules', ) 54 | -------------------------------------------------------------------------------- /templates/students/student/quiz_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% block title %} 4 | {% trans 'Quiz' %} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |

{% trans 'Quiz' %}

10 |
11 | {% include 'students/student/_title.html' with active='new' %} 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for quiz in quizzes %} 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% empty %} 31 | 32 | 33 | 34 | {% endfor %} 35 | 36 |
{% trans 'Quiz' %}{% trans 'Tag' %}{% trans 'Length' %}
{{ quiz.name }}{{ quiz.tag.get_html_badge }}{{ quiz.questions_count }}{% trans 'Start quiz' %}
{% trans 'Sorry! No quiz matching your interests right now.' %}
37 |
38 |
39 |
40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /templates/registration/signup.html: -------------------------------------------------------------------------------- 1 | {% extends "base2.html" %} 2 | {% load i18n %} 3 | {% load static %} 4 | {% block title %}{% trans 'Sign up' %}{% endblock %} 5 | {% block content %} 6 |
7 |
8 |
9 |
10 |

{% trans 'Create an account' %} {% trans 'for free' %}

11 |

{% trans 'Sign up as a student for increase your knowledge or become a teacher and provide useful tech courses' %}!

12 | 20 |
21 |
22 | 27 |
28 |
29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /static/scripts/libs/pikaday.jquery.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Pikaday jQuery plugin. 3 | * 4 | * Copyright © 2013 David Bushell | BSD & MIT license | https://github.com/Pikaday/Pikaday 5 | */ 6 | 7 | (function (root, factory) 8 | { 9 | 'use strict'; 10 | 11 | if (typeof exports === 'object') { 12 | // CommonJS module 13 | factory(require('jquery'), require('pikaday')); 14 | } else if (typeof define === 'function' && define.amd) { 15 | // AMD. Register as an anonymous module. 16 | define(['jquery', 'pikaday'], factory); 17 | } else { 18 | // Browser globals 19 | factory(root.jQuery, root.Pikaday); 20 | } 21 | }(this, function ($, Pikaday) 22 | { 23 | 'use strict'; 24 | 25 | $.fn.pikaday = function() 26 | { 27 | var args = arguments; 28 | 29 | if (!args || !args.length) { 30 | args = [{ }]; 31 | } 32 | 33 | return this.each(function() 34 | { 35 | var self = $(this), 36 | plugin = self.data('pikaday'); 37 | 38 | if (!(plugin instanceof Pikaday)) { 39 | if (typeof args[0] === 'object') { 40 | var options = $.extend({}, args[0]); 41 | options.field = self[0]; 42 | self.data('pikaday', new Pikaday(options)); 43 | } 44 | } else { 45 | if (typeof args[0] === 'string' && typeof plugin[args[0]] === 'function') { 46 | plugin[args[0]].apply(plugin, Array.prototype.slice.call(args,1)); 47 | 48 | if (args[0] === 'destroy') { 49 | self.removeData('pikaday'); 50 | } 51 | } 52 | } 53 | }); 54 | }; 55 | 56 | })); 57 | -------------------------------------------------------------------------------- /apps/courses/api/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics, viewsets 2 | from rest_framework.views import APIView 3 | from rest_framework.response import Response 4 | from rest_framework.authentication import BasicAuthentication 5 | from django.shortcuts import get_object_or_404 6 | from ..models import Subject, Course 7 | from .permissions import IsEnrolled 8 | from .serializers import SubjectSerializer, CourseSerializer, CourseWithContentsSerializer 9 | from rest_framework.permissions import IsAuthenticated 10 | from rest_framework.decorators import action 11 | 12 | class SubjectListView(generics.ListAPIView): 13 | queryset = Subject.objects.all() 14 | serializer_class = SubjectSerializer 15 | 16 | 17 | class SubjectDetailView(generics.RetrieveAPIView): 18 | queryset = Subject.objects.all() 19 | serializer_class = SubjectSerializer 20 | 21 | 22 | class CourseEnrollView(APIView): 23 | authentication_classes = (BasicAuthentication,) 24 | permission_classes = (IsAuthenticated,) 25 | 26 | def post(self, request, pk, format=None): 27 | course = get_object_or_404(Course, pk=pk) 28 | course.students.add(request.user) 29 | return Response({'enrolled': True}) 30 | 31 | class CourseViewSet(viewsets.ReadOnlyModelViewSet): 32 | queryset = Course.objects.all() 33 | serializer_class = CourseSerializer 34 | 35 | @action(detail=True, methods=['post'], authentication_classes=[BasicAuthentication], permission_classes=[IsAuthenticated]) 36 | def enroll(self, request, *args, **kwargs): 37 | course = self.get_object() 38 | course.students.add(request.user) 39 | return Response({'enrolled': True}) 40 | 41 | @action(detail=True, methods=['get'], serializer_class=CourseWithContentsSerializer, authentication_classes=[BasicAuthentication], permission_classes=[IsAuthenticated, IsEnrolled]) 42 | def contents(self, request, *args, **kwargs): 43 | return self.retrieve(request, *args, **kwargs) 44 | -------------------------------------------------------------------------------- /templates/students/contact/contact_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% load widget_tweaks %} 4 | {% load static %} 5 | {% block title %} 6 | {% trans 'Feeback' %} 7 | {% endblock %} 8 | 9 | {% block content %} 10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 |

{% trans 'Feedback' %}

18 |

{% trans "Feel free to contact us if you have questions or suggestions" %}

19 |
20 | {% csrf_token %} 21 | {% for field in form %} 22 |
23 | 24 | {% render_field field class="w-full bg-white rounded border border-gray-300 focus:border-green-500 focus:ring-2 focus:ring-green-200 text-base outline-none text-gray-700 py-1 px-3 resize-none leading-6 transition-colors duration-200 ease-in-out" placeholder=field.label %} 25 |
26 | {% endfor %} 27 | 28 |

29 |
30 |
31 |
32 |
33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /templates/registration/password_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load i18n %} 3 | {% load static %} 4 | {% load widget_tweaks %} 5 | {% block title %}{% trans 'Change your password' %}{% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |
11 | 13 |
14 |
15 |

{% trans 'Change Your Password' %}

16 | 17 |

{% trans 'Please enter your new password twice:' %}

18 |
19 |
20 | {% if form.errors %} 21 |

{% trans 'Password did not match. Please try again.' %}

22 | {% endif %} 23 | {% csrf_token %} 24 | 25 | {% for field in form %} 26 |
27 | {% render_field field class="w-full px-3 py-2 text-sm leading-tight text-gray-700 border rounded shadow appearance-none focus:outline-none focus:shadow-outline" placeholder=field.label %} 28 |
29 | {% endfor %} 30 |
31 | 37 |
38 |
39 |
40 |
41 |
42 |
43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /templates/service-worker.js: -------------------------------------------------------------------------------- 1 | var cacheName = 'v5'; 2 | var cacheFiles = []; 3 | var CURRENT_CACHES = { 4 | offline: 'offline-v1' 5 | }; 6 | 7 | var OFFLINE_URL = '/offline.html'; 8 | 9 | self.addEventListener('install', function(e) { 10 | e.waitUntil( 11 | fetch(createCacheBustedRequest(OFFLINE_URL)).then(function(response) { 12 | return caches.open(CURRENT_CACHES.offline).then(function(cache) { 13 | console.log('[ServiceWorker] Caching cacheFiles'); 14 | return cache.put(OFFLINE_URL, response); 15 | }) 16 | }) 17 | ) 18 | 19 | function createCacheBustedRequest(url) { 20 | var request = new Request(url, {cache: 'reload'}); 21 | if ('cache' in request) { 22 | return request; 23 | } 24 | var bustedUrl = new URL(url, self.location.href); 25 | bustedUrl.search += (bustedUrl.search ? '&' : '') + 'cachebust=' + Date.now(); 26 | return new Request(bustedUrl); 27 | } 28 | }); 29 | 30 | self.addEventListener('activate', function(e) { 31 | var expectedCacheNames = Object.keys(CURRENT_CACHES).map(function(key) { 32 | return CURRENT_CACHES[key]; 33 | }); 34 | 35 | e.waitUntil( 36 | caches.keys().then(function(cacheNames) { 37 | return Promise.all( 38 | cacheNames.map(function(cacheName) { 39 | if (expectedCacheNames.indexOf(cacheName) === -1) { 40 | 41 | console.log('[ServiceWorker] Removing Cached Files from Cache - ', cacheName); 42 | return caches.delete(cacheName); 43 | } 44 | }) 45 | ); 46 | }) 47 | ); 48 | }); 49 | 50 | self.addEventListener('fetch', function(event) { 51 | if (event.request.mode === 'navigate' || 52 | (event.request.method === 'GET' && 53 | event.request.headers.get('accept').includes('text/html'))) { 54 | 55 | event.respondWith( 56 | fetch(event.request).catch(function(error) { 57 | return caches.match(OFFLINE_URL); 58 | }) 59 | ) 60 | } 61 | }); 62 | -------------------------------------------------------------------------------- /apps/courses/admin.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import datetime 3 | 4 | from django.http import HttpResponse 5 | from django.contrib import admin 6 | from apps.courses.models import (Subject, Course, Module, Review, BadgeAward) 7 | from django.utils.translation import gettext_lazy as _ 8 | from apps.common.paginator import TimeLimitedPaginator, DumbPaginator 9 | 10 | def export_to_csv(modeladmin, request, queryset): 11 | opts = modeladmin.model._meta 12 | response = HttpResponse(content_type='text/csv') 13 | response['Content-Disposition'] = 'attachment; \ 14 | filename={}.csv'.format(opts.verbose_name) 15 | writer = csv.writer(response) 16 | 17 | fields = [field for field in opts.get_fields() if not field.many_to_many and not field.one_to_many] 18 | writer.writerow([field.verbose_name for field in fields]) 19 | 20 | for obj in queryset: 21 | data_row = [] 22 | for field in fields: 23 | value = getattr(obj, field.name) 24 | if isinstance(value, datetime.datetime): 25 | value = value.strftime('%d/%m/%Y') 26 | data_row.append(value) 27 | writer.writerow(data_row) 28 | return response 29 | 30 | export_to_csv.short_description = 'Export to CSV' 31 | 32 | 33 | @admin.register(Subject) 34 | class SubjectAdmin(admin.ModelAdmin): 35 | list_display = ['title', 'slug'] 36 | prepopulated_fields = {'slug': ('title',)} 37 | 38 | 39 | class ModuleInline(admin.StackedInline): 40 | model = Module 41 | 42 | 43 | @admin.register(Course) 44 | class CourseAdmin(admin.ModelAdmin): 45 | paginator = TimeLimitedPaginator 46 | list_display = ['title', 'subject', 'created'] 47 | list_filter = ['created', 'subject'] 48 | search_fields = ['title', 'overview'] 49 | inlines = [ModuleInline] 50 | 51 | 52 | @admin.register(Review) 53 | class ReviewAdmin(admin.ModelAdmin): 54 | paginator = DumbPaginator 55 | list_display = ['course', 'rating', 'user_name', 'comment', 'pub_date'] 56 | list_filter = ['pub_date', 'user_name'] 57 | search_fields = ['comment'] 58 | actions = [export_to_csv] 59 | 60 | 61 | admin.site.register(BadgeAward) 62 | -------------------------------------------------------------------------------- /templates/students/teacher/quiz_results.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% load crispy_forms_tags humanize %} 4 | {% block title %} 5 | {% trans 'Results' %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 |
11 | 18 | 19 |

{{ quiz.name }} {% trans 'results' %}

20 | 21 |
22 |
23 | {% trans 'Taken Quizzes' %} 24 | {% trans 'Average score' %}{{ quiz_score.average_score|default_if_none:0.0}} % 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {% for taken_quiz in taken_quizzes %} 36 | 37 | 38 | 39 | 40 | 41 | {% endfor %} 42 | 43 |
{% trans 'Student' %}{% trans 'Date' %}{% trans 'Score' %}
{{ taken_quiz.student.user.username }}{{ taken_quiz.date|naturaltime }}{{ taken_quiz.score }} %
44 | 45 | 48 |
49 |
50 |
51 | {% endblock %} 52 | -------------------------------------------------------------------------------- /apps/courses/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path as url 2 | from django.views.generic import TemplateView 3 | from django.views.decorators.cache import never_cache 4 | from django.contrib.auth.decorators import login_required 5 | from . import views 6 | 7 | urlpatterns = [ 8 | url(r'^$', views.CourseListView.as_view(), name='course_list'), 9 | url(r'^dashboard/$', views.ManageCourseListView.as_view(), name='manage_course_list'), 10 | url(r'^create/$', never_cache(login_required(views.CourseCreateView.as_view())), name='course_create'), 11 | url(r'^(?P\d+)/edit/$', views.CourseUpdateView.as_view(), name='course_edit'), 12 | url(r'^(?P\d+)/delete/$', views.CourseDeleteView.as_view(), name='course_delete'), 13 | url(r'^(?P\d+)/module/$', views.CourseModuleUpdateView.as_view(), name='course_module_update'), 14 | url(r'^module/(?P\d+)/content/(?P\w+)/create/$', views.ContentCreateUpdateView.as_view(), name='module_content_create'), 15 | url(r'^module/(?P\d+)/content/(?P\w+)/(?P\d+)/$', views.ContentCreateUpdateView.as_view(), name='module_content_update'), 16 | url(r'^content/(?P\d+)/delete/$', views.ContentDeleteView.as_view(), name='module_content_delete'), 17 | url(r'^module/(?P\d+)/$', views.ModuleContentListView.as_view(), name='module_content_list'), 18 | url(r'^module/order/$', views.ModuleOrderView.as_view(), name='module_order'), 19 | url(r'^content/order/$', views.ContentOrderView.as_view(), name='content_order'), 20 | url(r'^subject/(?P[\w-]+)/$', views.CourseListView.as_view(), name='course_list_subject'), 21 | url(r'^(?P[\w-]+)/$', views.CourseDetailView.as_view(), name='course_detail'), 22 | url(r'^(?P[\w-]+)/add_review/$', views.add_review, name='add_review'), 23 | url(r'^videos$', views.list_videos, name='videos_list'), 24 | url(r'^edit$', views.edit, name='edit'), 25 | url(r'^about-company$', TemplateView.as_view(template_name='about.html'), name='about_company'), 26 | url(r'^search$', views.SearchSubmitView.as_view(), name='search'), 27 | url(r'^search-ajax-submit$', views.SearchAjaxSubmitView.as_view(), name='search-ajax-submit'), 28 | ] 29 | -------------------------------------------------------------------------------- /templates/students/teacher/quiz_change_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% block title %} 4 | {% trans 'My Quizzes' %} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 | 15 | 16 |

{% trans 'My Quizzes' %}

17 | {% trans 'Add Quiz' %} 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% for quiz in quizzes %} 32 | 33 | 34 | 35 | 36 | 37 | 40 | 41 | {% empty %} 42 | 43 | 44 | 45 | {% endfor %} 46 | 47 |
{% trans 'Quiz' %}{% trans 'Tag' %}{% trans 'Question' %}{% trans 'Taken' %}
{{ quiz.name }}{{ quiz.tags.get_html_badge }}{{ quiz.questions_count }}{{ quiz.taken_count }} 38 | {% trans 'View results' %} 39 |
{% trans 'No quiz created right now.' %}
48 |
49 |
50 |
51 | 52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /myelearning/settings_production.py: -------------------------------------------------------------------------------- 1 | from myelearning.settings import * 2 | import dj_database_url 3 | 4 | DATABASES['default'] = dj_database_url.config() 5 | 6 | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 7 | 8 | DEBUG = config('DEBUG', cast=bool) 9 | 10 | # email admin 11 | 12 | SERVER_EMAIL = config('ADMIN_EMAIL') 13 | 14 | ADMINS = [ 15 | (config('ADMIN_NAME'), config('ADMIN_EMAIL')), 16 | ] 17 | 18 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 19 | EMAIL_HOST = config('SENDGRID_SERVER') 20 | EMAIL_PORT = config('SENDGRID_PORT') 21 | EMAIL_HOST_USER = config('SENDGRID_USERNAME') 22 | EMAIL_HOST_PASSWORD = config('SENDGRID_PASSWORD') 23 | EMAIL_USE_TLS = True 24 | EMAIL_USE_SSL = False 25 | EMAIL_TIMEOUT = 500 26 | 27 | ALLOWED_HOSTS = ['*',] 28 | 29 | STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' 30 | 31 | # Configure for SSL 32 | 33 | # SECURE_SSL_REDIRECT = True 34 | # CSRF_COOKIE_SECURE = True 35 | 36 | SESSION_EXPIRE_SECONDS = 18000 # 5 hours 37 | 38 | # Configure Redis for caching results 39 | CACHE_MIDDLEWARE_SECONDS = 60 * 10 # 10 minutes 40 | CACHE_MIDDLEWARE_KEY_PREFIX = 'myelearning' 41 | CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True 42 | 43 | CACHES = { 44 | "default": { 45 | "BACKEND": "django_redis.cache.RedisCache", 46 | "LOCATION": config("HEROKU_REDIS_AQUA_URL"), 47 | "OPTIONS": { 48 | "CLIENT_CLASS": "django_redis.client.DefaultClient", 49 | "MAX_ENTRIES": 1000, 50 | "CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": None}, 51 | }, 52 | "KEY_PREFIX": "myelearning", 53 | "TIMEOUT": 300 54 | } 55 | } 56 | 57 | # Media storages 58 | 59 | AWS_ACCESS_KEY_ID = config('AWS_ACCESS_KEY_ID') 60 | AWS_SECRET_ACCESS_KEY = config('AWS_SECRET_ACCESS_KEY') 61 | AWS_STORAGE_BUCKET_NAME = config('AWS_STORAGE_BUCKET_NAME') 62 | AWS_S3_CUSTOM_DOMAIN = '%s.s3.amazonaws.com' % AWS_STORAGE_BUCKET_NAME 63 | AWS_S3_OBJECT_PARAMETERS = { 64 | 'CacheControl': 'max-age=86400', 65 | } 66 | 67 | DEFAULT_FILE_STORAGE = 'myelearning.storage_backends.MediaStorage' 68 | 69 | # Task async 70 | CELERY_BROKER_URL = config('HEROKU_REDIS_AQUA_URL') 71 | CELERY_RESULT_BACKEND = config('HEROKU_REDIS_AQUA_URL') 72 | 73 | -------------------------------------------------------------------------------- /apps/courses/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.forms.models import inlineformset_factory 3 | from apps.courses.models import Course, Module, Review, Subject, Content 4 | from apps.students.models import ( 5 | User, Profile 6 | ) 7 | 8 | class ModuleForm(forms.ModelForm): 9 | title = forms.CharField(widget=forms.TextInput(attrs={'class':'form-control'})) 10 | description = forms.CharField(widget=forms.Textarea(attrs={'class':'form-control', 'cols': 40, 'rows': 8})) 11 | 12 | class Meta: 13 | model = Module 14 | exclude = () 15 | 16 | ModuleFormSet = inlineformset_factory(Course, Module, form=ModuleForm, fields=['title', 'description',], extra=2, can_delete=True) 17 | 18 | 19 | class UserEditForm(forms.ModelForm): 20 | username = forms.CharField(widget=forms.TextInput(attrs={'readonly': True})) 21 | 22 | class Meta: 23 | model = User 24 | fields = ['username', 'first_name', 'last_name',] 25 | 26 | # def clean(self): 27 | # email = self.cleaned_data.get('email') 28 | # if User.objects.filter(email=email).exists(): 29 | # raise forms.ValidationError("Email already exist") 30 | # return self.cleaned_data 31 | 32 | 33 | class ProfileEditForm(forms.ModelForm): 34 | 35 | class Meta: 36 | model = Profile 37 | fields = ['location', 'birthdate'] 38 | 39 | 40 | class ReviewForm(forms.ModelForm): 41 | class Meta: 42 | model = Review 43 | fields = ['rating', 'comment'] 44 | widgets = { 45 | 'comment': forms.Textarea(attrs={'cols': 40, 'rows': 15, 'class':'no-resize appearance-none block w-full bg-gray-200 text-gray-700 border border-gray-200 rounded py-3 px-4 mb-3 leading-tight focus:outline-none focus:bg-white focus:border-gray-500 h-48 resize-none'}) 46 | } 47 | 48 | 49 | class CourseCreateForm(forms.ModelForm): 50 | subject = forms.ModelChoiceField(queryset=Subject.objects.all(),widget=forms.Select(attrs={'class':'form-control'})) 51 | title = forms.CharField(widget=forms.TextInput(attrs={'class':'form-control'})) 52 | overview = forms.CharField(widget=forms.Textarea(attrs={'class':'form-control', 'cols': 40, 'rows': 15})) 53 | 54 | class Meta: 55 | model = Course 56 | fields = ['subject', 'title', 'overview',] 57 | -------------------------------------------------------------------------------- /apps/students/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path as url, include 2 | from .views import students, classroom, teachers 3 | from django.views.decorators.cache import cache_page 4 | 5 | urlpatterns = [ 6 | url(r'^classroom/$', classroom.index, name='classroom'), 7 | url(r'^contact/$', classroom.contact_us_view, name='contact_us'), 8 | url(r'^users/(?P.+)/$', classroom.user_detail, name='user_detail'), 9 | 10 | url(r'^register/student/$', students.StudentRegistrationView.as_view(), name='student_registration'), 11 | url(r'^enroll-course/$', students.StudentEnrollCourseView.as_view(), name='student_enroll_course'), 12 | url(r'^courses/$', students.StudentCourseListView.as_view(), name='student_course_list'), 13 | url(r'^course/(?P\d+)/$', cache_page(60*15)(students.StudentCourseDetailView.as_view()), name='student_course_detail'), 14 | url(r'^course/(?P\d+)/(?P\d+)/$', cache_page(60*15)(students.StudentCourseDetailView.as_view()), name='student_course_detail_module'), 15 | url(r'^student/quiz/$', students.QuizListView.as_view(), name='student_quiz_list'), 16 | url(r'^interests/$', students.StudentInterestsView.as_view(), name='student_interests'), 17 | url(r'^taken/$', students.TakenQuizListView.as_view(), name='taken_quiz_list'), 18 | url(r'^student/quiz/(?P\d+)/$', students.take_quiz, name='take_quiz'), 19 | 20 | url(r'^register/teacher/$', teachers.TeacherRegistrationView.as_view(), name='teacher_registration'), 21 | url(r'^quiz/$', teachers.TeacherQuizListView.as_view(), name='teacher_quiz_change_list'), 22 | url(r'^quiz/add/$', teachers.QuizCreateView.as_view(), name='teacher_add_quiz'), 23 | url(r'^quiz/(?P\d+)/$', teachers.QuizUpdateView.as_view(), name='teacher_update_quiz'), 24 | url(r'^quiz/(?P\d+)/delete/$', teachers.QuizDeleteView.as_view(), name='teacher_delete_quiz'), 25 | url(r'^quiz/(?P\d+)/results/$', teachers.QuizResultsView.as_view(), name='teacher_quiz_results'), 26 | url(r'^quiz/(?P\d+)/question/add/$', teachers.question_add, name='teacher_add_question'), 27 | url(r'^quiz/(?P\d+)/question/(?P\d+)/$', teachers.question_change, name='teacher_change_question'), 28 | url(r'^quiz/(?P\d+)/question/(?P\d+)/delete/$', teachers.QuestionDeleteView.as_view(), name='teacher_delete_question'), 29 | ] 30 | -------------------------------------------------------------------------------- /templates/registration/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load i18n %} 3 | {% load static %} 4 | {% load widget_tweaks %} 5 | {% block title %}{% trans 'Reset your password' %}{% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |
11 | 13 |
14 |
15 |

{% trans 'Reset Your Password?' %}

16 | 17 | {% if validlink %} 18 |

{% trans 'Please enter your new password twice:' %}

19 | {% else %} 20 |

{% trans 'The password reset link was invalid, possibly because it has already been used. Please request a new password reset' %}.

21 | {% endif %} 22 |
23 | {% if validlink %} 24 |
25 | {% if form.errors %} 26 |

{% trans 'Password did not match. Please try again.' %}

27 | {% endif %} 28 | {% csrf_token %} 29 | 30 | {% for field in form %} 31 |
32 | {{ field.label_tag }} 33 | {% render_field field class="w-full px-3 py-2 text-sm leading-tight text-gray-700 border rounded shadow appearance-none focus:outline-none focus:shadow-outline" %} 34 |
35 | {% endfor %} 36 |
37 | 43 |
44 |
45 | {% endif %} 46 |
47 |
48 |
49 |
50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /apps/students/views/classroom.py: -------------------------------------------------------------------------------- 1 | from decouple import config 2 | from django.shortcuts import render, redirect, get_object_or_404 3 | from django.views.generic import TemplateView 4 | from apps.students.forms import ContactForm 5 | from apps.students.models import User 6 | from django.core.mail import EmailMessage 7 | from django.template import loader 8 | from django.contrib import messages 9 | from django.utils.translation import gettext as _ 10 | 11 | 12 | class SignupView(TemplateView): 13 | template_name = 'registration/signup.html' 14 | 15 | 16 | def index(request): 17 | if request.user.is_authenticated: 18 | if request.user.is_teacher: 19 | return redirect('students:teacher_quiz_change_list') 20 | else: 21 | return redirect('students:student_quiz_list') 22 | else: 23 | return redirect('courses:course_list') 24 | 25 | 26 | def contact_us_view(request): 27 | form_class = ContactForm 28 | 29 | if request.method == 'POST': 30 | form = form_class(data=request.POST) 31 | 32 | if form.is_valid(): 33 | contact_name = request.POST.get('contact_name', '') 34 | contact_email = request.POST.get('contact_email', '') 35 | form_content = request.POST.get('form_content', '') 36 | template = loader.get_template('students/contact/contact_template.txt') 37 | context = { 38 | 'contact_name': contact_name, 39 | 'contact_email': contact_email, 40 | 'form_content': form_content 41 | } 42 | content = template.render(context) 43 | 44 | email = EmailMessage( 45 | _('Nouveau message de myealearning'), 46 | content, 47 | _('myealearning'), 48 | [config('ADMIN_EMAIL')], 49 | headers = { 'Reply-To': contact_email } 50 | ) 51 | email.send() 52 | messages.success(request, _('Thank you ! We will check in as soon as possible ;-)')) 53 | return redirect('students:contact_us') 54 | else: 55 | messages.info(request, _('Oops ! Message not send...')) 56 | return render(request, 'students/contact/contact_form.html', { 'form': form_class }) 57 | 58 | 59 | def user_detail(request, username): 60 | ouser = get_object_or_404(User, username=username, is_active=True) 61 | return render(request, 'students/user/detail.html', {'ouser': ouser}) -------------------------------------------------------------------------------- /templates/registration/signup_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base2.html" %} 2 | {% load widget_tweaks %} 3 | {% load static %} 4 | {% load i18n %} 5 | {% block title %} 6 | {% trans 'Sign up as' %} {{ user_type }} 7 | {% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 |
13 |
14 |
15 |

{% trans 'Sign up ' %}

16 |

{% trans "Enter your details to create an account as" %} {{ user_type }}

17 |
18 | 28 |
29 |

30 | {% trans 'By signing up, you agree to the Terms of Service and Privacy Policy' %} 31 |

32 |
33 |

34 | {% trans 'Already an account ?' %} {% trans 'Login' %} ! 35 |

36 |
37 |
38 |
39 |
40 | 41 |
42 |
43 |
44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /apps/courses/search.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # This sample executes a search request for the specified search term. 4 | # Sample usage: 5 | # python search.py --q=surfing --max-results=10 6 | # NOTE: To use the sample, you must provide a developer key obtained 7 | # in the Google APIs Console. Search for "REPLACE_ME" in this code 8 | # to find the correct place to provide that key.. 9 | import os 10 | import json 11 | 12 | from django.conf import settings 13 | 14 | from googleapiclient.discovery import build 15 | from googleapiclient.errors import HttpError 16 | 17 | 18 | # Set DEVELOPER_KEY to the API key value from the APIs & auth > Registered apps 19 | # tab of 20 | # https://cloud.google.com/console 21 | # Please ensure that you have enabled the YouTube Data API for your project. 22 | DEVELOPER_KEY = settings.DEVELOPER_KEY 23 | YOUTUBE_API_SERVICE_NAME = 'youtube' 24 | YOUTUBE_API_VERSION = 'v3' 25 | 26 | def youtube_search(q, max_results): 27 | youtube = build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION, 28 | developerKey=DEVELOPER_KEY) 29 | 30 | # Call the search.list method to retrieve results matching the specified 31 | # query term. 32 | search_response = youtube.search().list( 33 | q=q, 34 | part='id,snippet', 35 | maxResults=max_results 36 | ).execute() 37 | 38 | 39 | videos = [] 40 | channels = [] 41 | playlists = [] 42 | 43 | # Add each result to the appropriate list, and then display the lists of 44 | # matching videos, channels, and playlists. 45 | for search_result in search_response.get('items', []): 46 | if search_result['id']['kind'] == 'youtube#video': 47 | videos.append('%s (%s)' % (search_result['snippet']['title'], 48 | search_result['id']['videoId'])) 49 | elif search_result['id']['kind'] == 'youtube#channel': 50 | channels.append('%s (%s)' % (search_result['snippet']['title'], 51 | search_result['id']['channelId'])) 52 | elif search_result['id']['kind'] == 'youtube#playlist': 53 | playlists.append('%s (%s)' % (search_result['snippet']['title'], 54 | search_result['id']['playlistId'])) 55 | 56 | 57 | print ('Videos:\n', '\n'.join(videos), '\n') 58 | print ('Channels:\n', '\n'.join(channels), '\n') 59 | print ('Playlists:\n', '\n'.join(playlists), '\n') 60 | 61 | return search_response 62 | 63 | 64 | if __name__ == '__main__': 65 | 66 | try: 67 | youtube_search(q, max_results) 68 | except HttpError as e: 69 | print( 'An HTTP error %d occurred:\n%s' % (e.resp.status, e.content)) 70 | -------------------------------------------------------------------------------- /templates/registration/password_reset_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base2.html' %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% load widget_tweaks %} 5 | {% block title %}{% trans 'Reset your password' %}{% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 | 11 |
12 | 13 | 17 | 18 |
19 |
20 |

{% trans 'Forgot Your Password?' %}

21 |

22 | {% trans 'We get it, stuff happens. Just enter your email address below and we will send you a link to reset your password!' %} 23 |

24 |
25 |
26 | {% if form.errors %} 27 |

{% trans 'Enter your e-mail address to obtain a new password.' %}

28 | {% endif %} 29 | {% csrf_token %} 30 | 31 | {% for field in form %} 32 |
33 | {{ field.label_tag }} 34 | {% render_field field class="w-full px-3 py-2 text-sm leading-tight text-gray-700 border rounded shadow appearance-none focus:outline-none focus:shadow-outline" placeholder="Enter Email Address..." %} 35 |
36 | {% endfor %} 37 |
38 | 44 |
45 |
46 | 54 | 62 |
63 |
64 |
65 |
66 |
67 | {% endblock %} 68 | -------------------------------------------------------------------------------- /myelearning/urls.py: -------------------------------------------------------------------------------- 1 | """myelearning URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.urls import re_path as url, include 17 | from django.conf import settings 18 | from django.views import generic 19 | from django.conf.urls.static import static 20 | from django.contrib import admin 21 | from django.contrib.auth import views as auth_views 22 | from apps.students.views import classroom 23 | 24 | from django.views.generic import TemplateView 25 | 26 | urlpatterns = [ 27 | url(r'^$', generic.RedirectView.as_view(url='/course/', permanent=True)), 28 | 29 | url(r'^accounts/login/$', auth_views.LoginView.as_view(), name='login'), 30 | url(r'^accounts/logout/$', auth_views.LogoutView.as_view(), name='logout'), 31 | url(r'^accounts/signup/$', classroom.SignupView.as_view(), name='signup'), 32 | url(r'^password-change/$', auth_views.PasswordChangeView.as_view(), name='password_change'), 33 | url(r'^password-change/done/$', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'), 34 | url(r'^password-reset/$', auth_views.PasswordResetView.as_view(), name='password_reset'), 35 | url(r'^password-reset/done/$', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'), 36 | url(r'^password-reset/confirm/(?P[-\w]+)/(?P[-\w]+)/$', auth_views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'), 37 | url(r'^password-reset/complete/$', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'), 38 | 39 | url(r'^admin/', admin.site.urls), 40 | url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 41 | 42 | url(r'^course/', include(('apps.courses.urls', 'courses'))), 43 | url(r'^students/', include(('apps.students.urls', 'students'))), 44 | 45 | url(r'^api/', include(('apps.courses.api.urls', 'api'), namespace='api')), 46 | 47 | url(r'^sw.js', (TemplateView.as_view(template_name="service-worker.js", content_type='application/javascript', )), name='sw.js'), 48 | url(r'^offline.html', (TemplateView.as_view(template_name="offline.html")), name='offline.html'), 49 | ] 50 | 51 | if settings.DEBUG: 52 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 53 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 54 | -------------------------------------------------------------------------------- /apps/students/migrations/0003_auto_20190720_1413.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.21 on 2019-07-20 14:13 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 | import taggit.managers 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | dependencies = [ 15 | ('taggit', '0003_taggeditem_add_unique_index'), 16 | ('students', '0002_remove_tag_subject'), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='Comment', 22 | fields=[ 23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('name', models.CharField(max_length=80)), 25 | ('email', models.EmailField(default='', max_length=254)), 26 | ('body', models.TextField()), 27 | ('created', models.DateTimeField(auto_now_add=True)), 28 | ('updated', models.DateTimeField(auto_now=True)), 29 | ('active', models.BooleanField(default=True)), 30 | ], 31 | options={ 32 | 'ordering': ('created',), 33 | }, 34 | ), 35 | migrations.CreateModel( 36 | name='Post', 37 | fields=[ 38 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 39 | ('title', models.CharField(max_length=250)), 40 | ('slug', models.SlugField(max_length=250, unique_for_date='publish')), 41 | ('header', models.CharField(default='', max_length=1000)), 42 | ('body', models.TextField()), 43 | ('publish', models.DateTimeField(default=django.utils.timezone.now)), 44 | ('created', models.DateTimeField(auto_now_add=True)), 45 | ('updated', models.DateTimeField(auto_now=True)), 46 | ('status', models.CharField(choices=[('draft', 'Draft'), ('published', 'Published')], default='draft', max_length=10)), 47 | ('likes', models.PositiveIntegerField(default=0)), 48 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='blog_posts', to=settings.AUTH_USER_MODEL)), 49 | ('tags', taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')), 50 | ], 51 | options={ 52 | 'ordering': ('-publish',), 53 | }, 54 | ), 55 | migrations.AddField( 56 | model_name='comment', 57 | name='post', 58 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='students.Post'), 59 | ), 60 | ] 61 | -------------------------------------------------------------------------------- /templates/registration/edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% load widget_tweaks %} 5 | {% block title %}{% trans 'Edit your account' %}{% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |

{% trans 'Edit your account' %}

11 |
12 | 13 |
14 |

{% trans 'You can edit your account using the following form' %} :

15 | 16 |
17 | {% csrf_token %} 18 | {% for field in user_form %} 19 |
20 | 21 | {% render_field field class="flex-grow h-8 px-2 rounded border border-gray-400" placeholder=field.label %} 22 | 23 | {% if field.help_text %} 24 | 25 | {{ field.help_text }} 26 | 27 | {% endif %} 28 |
29 | {% endfor %} 30 | {% for field in profile_form %} 31 |
32 | 33 | {% render_field field class="flex-grow h-8 px-2 rounded border border-gray-400" placeholder=field.label %} 34 | 35 | {% if field.help_text %} 36 | 37 | {{ field.help_text }} 38 | 39 | {% endif %} 40 |
41 | {% endfor %} 42 | 43 |
44 | 45 |
46 |
47 | {% endblock %} 48 | 49 | {% block extra_scripts %} 50 | 51 | 52 | 53 | 78 | {% endblock %} 79 | -------------------------------------------------------------------------------- /.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 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *.cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # Jupyter Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule.db 77 | 78 | # SageMath parsed files 79 | *.sage.py 80 | 81 | # dotenv 82 | .env 83 | 84 | # virtualenv 85 | .venv 86 | venv/ 87 | ENV/ 88 | elearning/ 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 | db.sqlite3 103 | sketch/ 104 | media/ 105 | .DS_Store 106 | Logs 107 | logs 108 | *.log 109 | npm-debug.log* 110 | yarn-debug.log* 111 | yarn-error.log* 112 | 113 | # Runtime data 114 | pids 115 | *.pid 116 | *.seed 117 | *.pid.lock 118 | 119 | # Directory for instrumented libs generated by jscoverage/JSCover 120 | lib-cov 121 | 122 | # Coverage directory used by tools like istanbul 123 | coverage 124 | 125 | # nyc test coverage 126 | .nyc_output 127 | 128 | .idea 129 | 130 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 131 | .grunt 132 | 133 | # Bower dependency directory (https://bower.io/) 134 | bower_components 135 | 136 | # node-waf configuration 137 | .lock-wscript 138 | 139 | # Compiled binary addons (https://nodejs.org/api/addons.html) 140 | build/Release 141 | 142 | # Dependency directories 143 | node_modules/ 144 | jspm_packages/ 145 | 146 | # TypeScript v1 declaration files 147 | typings/ 148 | 149 | # Optional npm cache directory 150 | .npm 151 | 152 | # Optional eslint cache 153 | .eslintcache 154 | 155 | # Optional REPL history 156 | .node_repl_history 157 | 158 | # Output of 'npm pack' 159 | *.tgz 160 | 161 | # Yarn Integrity file 162 | .yarn-integrity 163 | 164 | # dotenv environment variables file 165 | .env 166 | /libs 167 | 168 | # next.js build output 169 | .next 170 | frontend/images/ 171 | frontend/icons/ 172 | .vscode/ 173 | .idea -------------------------------------------------------------------------------- /apps/courses/migrations/0002_auto_20180530_1936.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2018-05-30 19:36 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 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ('courses', '0001_initial'), 17 | ('contenttypes', '0002_remove_content_type_name'), 18 | ] 19 | 20 | operations = [ 21 | migrations.AddField( 22 | model_name='video', 23 | name='owner', 24 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='video_related', to=settings.AUTH_USER_MODEL), 25 | ), 26 | migrations.AddField( 27 | model_name='text', 28 | name='owner', 29 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='text_related', to=settings.AUTH_USER_MODEL), 30 | ), 31 | migrations.AddField( 32 | model_name='module', 33 | name='course', 34 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='modules', to='courses.Course'), 35 | ), 36 | migrations.AddField( 37 | model_name='image', 38 | name='owner', 39 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='image_related', to=settings.AUTH_USER_MODEL), 40 | ), 41 | migrations.AddField( 42 | model_name='file', 43 | name='owner', 44 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='file_related', to=settings.AUTH_USER_MODEL), 45 | ), 46 | migrations.AddField( 47 | model_name='course', 48 | name='owner', 49 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='courses_created', to=settings.AUTH_USER_MODEL), 50 | ), 51 | migrations.AddField( 52 | model_name='course', 53 | name='students', 54 | field=models.ManyToManyField(blank=True, related_name='courses_joined', to=settings.AUTH_USER_MODEL), 55 | ), 56 | migrations.AddField( 57 | model_name='course', 58 | name='subject', 59 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='courses', to='courses.Subject'), 60 | ), 61 | migrations.AddField( 62 | model_name='content', 63 | name='content_type', 64 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'), 65 | ), 66 | migrations.AddField( 67 | model_name='content', 68 | name='module', 69 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contents', to='courses.Module'), 70 | ), 71 | ] 72 | -------------------------------------------------------------------------------- /templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base2.html" %} 2 | {% load i18n %} 3 | {% load widget_tweaks %} 4 | {% load static %} 5 | {% block title %}{% trans 'Log In' %}{% endblock %} 6 | {% block content %} 7 |
8 |
9 |
10 |
11 |
12 |

{% trans 'Welcome Back' %}

13 |
14 |
15 | {% if form.errors %} 16 |

{% trans 'Your username and password didn\'t match. Please try again.' %}

17 | {% endif %} 18 | 19 | 20 | 21 | {% csrf_token %} 22 | 23 | {% for field in form %} 24 |
25 | 26 | {% render_field field class="flex-grow h-8 px-2 rounded border border-gray-400" placeholder=field.label %} 27 | 28 | {% if field.help_text %} 29 | 30 | {{ field.help_text }} 31 | 32 | {% endif %} 33 |
34 | {% endfor %} 35 | 36 |
37 | 40 |
41 | 42 |
43 | 48 |
49 |

{% trans 'Don\'t have an account yet?' %} {% trans 'Create it !' %}

50 |
51 |
52 |
53 |
54 | 55 |
56 |
57 |
58 | {% endblock %} 59 | -------------------------------------------------------------------------------- /static/scripts/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | $(function() { 4 | // navbar dropdown 5 | var $navbarBurgers = $('body').find('.navbar-burger'); 6 | 7 | if ($navbarBurgers.length > 0) { 8 | $navbarBurgers.each(function(i){ 9 | $(this).on('click', function(e) { 10 | e.preventDefault(); 11 | 12 | var target = $(this).data('target'), 13 | $target = $('#sidebar'); 14 | 15 | $target.toggleClass('is-active'); 16 | }); 17 | }); 18 | } 19 | 20 | // flash message 21 | var $flashMessage = $('.alert'); 22 | if ($flashMessage.length === 0) { 23 | return; 24 | } 25 | 26 | var FLASH_MESSAGE_DELAY_BEFORE_HIDE = 8000; 27 | 28 | // Declare 29 | var hideOne = function ($flashMessage) { 30 | $flashMessage.slideUp(function () { 31 | $(this).detach(); 32 | }); 33 | }; 34 | 35 | var launchTimer = function () { 36 | window.timerFlashMessage = setTimeout(function () { 37 | $flashMessage.each(function () { 38 | hideOne($(this)); 39 | }); 40 | }, FLASH_MESSAGE_DELAY_BEFORE_HIDE); 41 | }; 42 | 43 | // Init 44 | var $closeButton = $flashMessage.find('.close'); 45 | 46 | if ($flashMessage.data('is-auto-close')) { 47 | launchTimer(); 48 | } 49 | 50 | // Events 51 | $closeButton.on('click', function () { 52 | hideOne($(this).parents('.alert')); 53 | }); 54 | }); 55 | 56 | function throttle(fn, delay) { 57 | var last = void 0; 58 | var timer = void 0; 59 | 60 | return function () { 61 | var now = +new Date(); 62 | 63 | if (last && now < last + delay) { 64 | clearTimeout(timer); 65 | 66 | timer = setTimeout(function () { 67 | last = now; 68 | fn (); 69 | }, delay); 70 | } else { 71 | last = now; 72 | fn (); 73 | } 74 | }; 75 | } 76 | 77 | var isOffline = false; 78 | window.addEventListener('load', checkConnectivity); 79 | 80 | function checkConnectivity() { 81 | updateStatus(); 82 | window.addEventListener('online', updateStatus); 83 | window.addEventListener('offline', updateStatus); 84 | } 85 | 86 | function updateStatus() { 87 | if (typeof navigator.onLine !== 'undefined') { 88 | isOffline = !navigator.onLine; 89 | document.documentElement.classList.toggle('is-offline', isOffline); 90 | } 91 | var notification = document.querySelector('#notification'); 92 | if (isOffline) { 93 | notification.textContent = "You appear to be offline "; 94 | notification.removeAttribute('hidden'); 95 | } else { 96 | notification.textContent = ""; 97 | notification.removeAttribute('hidden'); 98 | } 99 | } 100 | 101 | var links = document.querySelectorAll('a[href]'); 102 | 103 | Array.from(links).forEach((link) => { 104 | caches.match(link.href, { ignoreSearch: true }).then((response) => { 105 | if (response) { 106 | link.classList.add('is-cached'); 107 | } 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /static/scripts/main.min.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | $(function() { 4 | // navbar dropdown 5 | var $navbarBurgers = $('body').find('.navbar-burger'); 6 | 7 | if ($navbarBurgers.length > 0) { 8 | $navbarBurgers.each(function(i){ 9 | $(this).on('click', function(e) { 10 | e.preventDefault(); 11 | 12 | var target = $(this).data('target'), 13 | $target = $('#sidebar'); 14 | 15 | $target.toggleClass('is-active'); 16 | }); 17 | }); 18 | } 19 | 20 | // flash message 21 | var $flashMessage = $('.alert'); 22 | if ($flashMessage.length === 0) { 23 | return; 24 | } 25 | 26 | var FLASH_MESSAGE_DELAY_BEFORE_HIDE = 8000; 27 | 28 | // Declare 29 | var hideOne = function ($flashMessage) { 30 | $flashMessage.slideUp(function () { 31 | $(this).detach(); 32 | }); 33 | }; 34 | 35 | var launchTimer = function () { 36 | window.timerFlashMessage = setTimeout(function () { 37 | $flashMessage.each(function () { 38 | hideOne($(this)); 39 | }); 40 | }, FLASH_MESSAGE_DELAY_BEFORE_HIDE); 41 | }; 42 | 43 | // Init 44 | var $closeButton = $flashMessage.find('.close'); 45 | 46 | if ($flashMessage.data('is-auto-close')) { 47 | launchTimer(); 48 | } 49 | 50 | // Events 51 | $closeButton.on('click', function () { 52 | hideOne($(this).parents('.alert')); 53 | }); 54 | }); 55 | 56 | function throttle(fn, delay) { 57 | var last = void 0; 58 | var timer = void 0; 59 | 60 | return function () { 61 | var now = +new Date(); 62 | 63 | if (last && now < last + delay) { 64 | clearTimeout(timer); 65 | 66 | timer = setTimeout(function () { 67 | last = now; 68 | fn (); 69 | }, delay); 70 | } else { 71 | last = now; 72 | fn (); 73 | } 74 | }; 75 | } 76 | 77 | var isOffline = false; 78 | window.addEventListener('load', checkConnectivity); 79 | 80 | function checkConnectivity() { 81 | updateStatus(); 82 | window.addEventListener('online', updateStatus); 83 | window.addEventListener('offline', updateStatus); 84 | } 85 | 86 | function updateStatus() { 87 | if (typeof navigator.onLine !== 'undefined') { 88 | isOffline = !navigator.onLine; 89 | document.documentElement.classList.toggle('is-offline', isOffline); 90 | } 91 | var notification = document.querySelector('#notification'); 92 | if (isOffline) { 93 | notification.textContent = "You appear to be offline "; 94 | notification.removeAttribute('hidden'); 95 | } else { 96 | notification.textContent = ""; 97 | notification.removeAttribute('hidden'); 98 | } 99 | } 100 | 101 | var links = document.querySelectorAll('a[href]'); 102 | 103 | Array.from(links).forEach((link) => { 104 | caches.match(link.href, { ignoreSearch: true }).then((response) => { 105 | if (response) { 106 | link.classList.add('is-cached'); 107 | } 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /templates/students/teacher/quiz_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% load crispy_forms_tags %} 4 | {% block title %} 5 | {% trans 'Add question' %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 |
11 | 17 |

18 | {{ quiz.name }} 19 | {% trans 'View results' %} 20 |

21 |
22 |
23 |
24 | {% csrf_token %} 25 | {{ form|crispy }} 26 | 27 | {% trans 'Go back' %} 28 | {% trans 'Delete' %} 29 |
30 |
31 |
32 | 33 |
34 |
35 |
36 |
37 | {% trans 'Questions' %} 38 |
39 |
40 | {% trans 'Answers' %} 41 |
42 |
43 |
44 |
45 | {% for question in questions %} 46 |
47 |
48 | 51 |
52 | {{ question.answers_count }} 53 |
54 |
55 |
56 | {% empty %} 57 |
58 |

{% trans 'You haven\'t created any questions yet. Go ahead and' %} {% trans 'add the first question' %}.

59 |
60 | {% endfor %} 61 |
62 | 65 |
66 | 67 | 68 |
69 |
70 | 71 | 72 | 73 | {% endblock %} 74 | -------------------------------------------------------------------------------- /templates/students/teacher/question_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% load crispy_forms_tags %} 4 | {% block title %} 5 | {% trans 'Add question' %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 |
11 | 18 | 19 |

{{ question.text }}

20 | 21 |
22 | {% csrf_token %} 23 | {{ formset.management_form }} 24 | {{ form|crispy }} 25 |
26 |
27 |
28 |
29 | {% trans 'Answers' %} 30 |
31 |
32 | {% trans 'Correct ?' %} 33 |
34 |
35 | {% trans 'Delete ?' %} 36 |
37 |
38 |
39 | {% for error in formset.non_form_errors %} 40 |
{{ error }}
41 | {% endfor %} 42 |
43 | {% for form in formset %} 44 |
45 |
46 |
47 | {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %} 48 | {{ form.text|as_crispy_field }} 49 | {% if form.instance.pk and form.text.value != form.instance.text %}

Old answer: {{ form.instance.text }}

{% endif %} 50 |
51 |
52 | {{ form.is_correct }} 53 |
54 |
55 | {% if form.instance.pk %} 56 | {{ form.DELETE }} 57 | {% endif %} 58 |
59 |
60 |
61 | {% endfor %} 62 |
63 |
64 |

{% trans 'Your question must have at least 2 answers and maximum 10 answers. Select at least one correct answer.' %}

65 | 66 | {% trans 'Go back' %} 67 | {% trans 'Delete' %} 68 |
69 |
70 |
71 | {% endblock %} 72 | -------------------------------------------------------------------------------- /templates/base2.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load flatpages %} 3 | {% load i18n %} 4 | {% load gravatar %} 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% trans 'myele@rning' %} - {% block title %} {% endblock %} 12 | 13 | 14 | 15 | 16 |
17 | 18 | {% if messages %} 19 |
20 | {% for message in messages %} 21 | 25 | {% endfor %} 26 |
27 | {% endif %} 28 | 29 | {% block content %} 30 | {% endblock %} 31 | 32 | 33 | 40 |
41 | 42 | 55 | 56 | 57 | 58 | 76 | 77 | 78 | 79 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /templates/courses/manage/module/content_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load course %} 3 | {% load i18n %} 4 | {% block title %} 5 | Module {{ module.order|add:1 }}: {{ module.title }} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 | {% with course=module.course %} 11 |

Course "{{ course.title }}"

12 |
13 |

Modules

14 | 29 |

Edit 30 | modules

31 |
32 |
33 |

Module {{ module.order|add:1 }}: {{ module.title }}

34 |

Module contents:

35 |
36 | {% for content in module.contents.all %} 37 |
38 | {% with item=content.item %} 39 |

{{ item }} ({{ item|model_name}})

40 | {% trans 'Edit' %} 41 |
42 | 43 | {% csrf_token %} 44 |
45 | {% endwith %} 46 |
47 | {% empty %} 48 |

{% trans 'This module has no contents yet.' %}

49 | {% endfor %} 50 | 51 |
52 |
53 |

{% trans 'Add new content' %}:

54 | 60 |
61 | {% endwith %} 62 |
63 | {% endblock %} 64 | 65 | {% block domready %} 66 | 67 | $('#modules').sortable({ 68 | stop: function(event, ui) { 69 | modules_order = {}; 70 | $('#modules').children().each(function() { 71 | $(this).find('.order').text($(this).index() + 1); 72 | modules_order[$(this).data('id')] = $(this).index(); 73 | }); 74 | $.ajax({ 75 | type: 'POST', 76 | url: '{% url "courses:module_order" %}', 77 | contentType: 'application/json; charset=utf-8', 78 | dataType: 'json', 79 | data: JSON.stringify(modules_order) 80 | }); 81 | 82 | } 83 | }); 84 | 85 | $('#module-contents').sortable({ 86 | stop: function(event, ui) { 87 | contents_order = {}; 88 | $('#module-contents').children().each(function() { 89 | contents_order[$(this).data('id')] = $(this).index(); 90 | }); 91 | $.ajax({ 92 | type: 'POST', 93 | url: '{% url "courses:content_order" %}', 94 | contentType: 'application/json; charset=utf-8', 95 | dataType: 'json', 96 | data: JSON.stringify(contents_order) 97 | }); 98 | 99 | } 100 | }); 101 | {% endblock %} 102 | -------------------------------------------------------------------------------- /apps/students/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | from django.db import models 3 | from django.core.exceptions import ObjectDoesNotExist 4 | from django.db.models.signals import post_save 5 | from django.utils.html import escape, mark_safe 6 | from django.db.models.signals import post_save 7 | from django.dispatch import receiver 8 | 9 | class User(AbstractUser): 10 | is_student = models.BooleanField(default=False) 11 | is_teacher = models.BooleanField(default=False) 12 | 13 | 14 | class Tag(models.Model): 15 | name = models.CharField(max_length=30) 16 | color = models.CharField(max_length=7, default='#007bff') 17 | 18 | def __str__(self): 19 | return self.name 20 | 21 | def get_html_badge(self): 22 | name = escape(self.name) 23 | color = escape(self.color) 24 | html = '%s' % (color, name) 25 | return mark_safe(html) 26 | 27 | 28 | class Quiz(models.Model): 29 | owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='quizzes') 30 | name = models.CharField(max_length=255) 31 | tags = models.ForeignKey(Tag, on_delete=models.CASCADE, related_name='quizzes') 32 | 33 | def __str__(self): 34 | return self.name 35 | 36 | 37 | class Question(models.Model): 38 | quiz = models.ForeignKey(Quiz, on_delete=models.CASCADE, related_name='questions') 39 | text = models.CharField('Question', max_length=255) 40 | 41 | def __str__(self): 42 | return self.text 43 | 44 | 45 | class Answer(models.Model): 46 | question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='answers') 47 | text = models.CharField('Answer', max_length=255) 48 | is_correct = models.BooleanField('Correct answer', default=False) 49 | 50 | def __str__(self): 51 | return self.text 52 | 53 | 54 | class Profile(models.Model): 55 | user = models.OneToOneField(User, on_delete=models.CASCADE) 56 | award_points = models.PositiveIntegerField(default=0) 57 | location = models.CharField(max_length=30, blank=True) 58 | birthdate = models.DateField(null=True, blank=True) 59 | 60 | def get_award_points(self, point): 61 | self.award_points += point 62 | self.save() 63 | 64 | def __str__(self): 65 | return self.user.username 66 | 67 | 68 | @receiver(post_save, sender=User) 69 | def create_or_update_user_profile(sender, instance, created, **kwargs): 70 | if created: 71 | Profile.objects.create(user=instance) 72 | instance.profile.save() 73 | 74 | 75 | class Student(models.Model): 76 | user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) 77 | quizzes = models.ManyToManyField(Quiz, through='TakenQuiz') 78 | interests = models.ManyToManyField(Tag, related_name='interested_students') 79 | 80 | def get_unanswered_questions(self, quiz): 81 | answered_questions = self.quiz_answers \ 82 | .filter(answer__question__quiz=quiz) \ 83 | .values_list('answer__question__pk', flat=True) 84 | questions = quiz.questions.exclude(pk__in=answered_questions).order_by('text') 85 | return questions 86 | 87 | def __str__(self): 88 | return '{} - {}'.format(self.user.username, self.user.email) 89 | 90 | 91 | class TakenQuiz(models.Model): 92 | student = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='taken_quizzes') 93 | quiz = models.ForeignKey(Quiz, on_delete=models.CASCADE, related_name='taken_quizzes') 94 | score = models.FloatField() 95 | date = models.DateTimeField(auto_now_add=True) 96 | 97 | def __str__(self): 98 | return '{}. {} {}%'.format(self.quiz, self.student, self.score) 99 | 100 | 101 | class StudentAnswer(models.Model): 102 | student = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='quiz_answers') 103 | answer = models.ForeignKey(Answer, on_delete=models.CASCADE, related_name='+') 104 | 105 | def __str__(self): 106 | return '{} {}'.format(self.student, self.answer) 107 | 108 | -------------------------------------------------------------------------------- /apps/courses/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2018-05-30 19:36 3 | from __future__ import unicode_literals 4 | 5 | import apps.courses.fields 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Content', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('object_id', models.PositiveIntegerField()), 22 | ('order', apps.courses.fields.OrderField(blank=True)), 23 | ], 24 | options={ 25 | 'ordering': ['order'], 26 | }, 27 | ), 28 | migrations.CreateModel( 29 | name='Course', 30 | fields=[ 31 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 32 | ('title', models.CharField(max_length=200)), 33 | ('slug', models.SlugField(max_length=200, unique=True)), 34 | ('overview', models.TextField()), 35 | ('created', models.DateTimeField(auto_now_add=True)), 36 | ], 37 | options={ 38 | 'ordering': ('-created',), 39 | }, 40 | ), 41 | migrations.CreateModel( 42 | name='File', 43 | fields=[ 44 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 45 | ('title', models.CharField(max_length=250)), 46 | ('created', models.DateTimeField(auto_now_add=True)), 47 | ('updated', models.DateTimeField(auto_now=True)), 48 | ('file', models.FileField(upload_to='files')), 49 | ], 50 | options={ 51 | 'abstract': False, 52 | }, 53 | ), 54 | migrations.CreateModel( 55 | name='Image', 56 | fields=[ 57 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 58 | ('title', models.CharField(max_length=250)), 59 | ('created', models.DateTimeField(auto_now_add=True)), 60 | ('updated', models.DateTimeField(auto_now=True)), 61 | ('file', models.FileField(upload_to='images')), 62 | ], 63 | options={ 64 | 'abstract': False, 65 | }, 66 | ), 67 | migrations.CreateModel( 68 | name='Module', 69 | fields=[ 70 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 71 | ('title', models.CharField(max_length=200)), 72 | ('description', models.TextField(blank=True)), 73 | ('order', apps.courses.fields.OrderField(blank=True)), 74 | ], 75 | options={ 76 | 'ordering': ['order'], 77 | }, 78 | ), 79 | migrations.CreateModel( 80 | name='Subject', 81 | fields=[ 82 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 83 | ('title', models.CharField(max_length=200)), 84 | ('slug', models.SlugField(max_length=200, unique=True)), 85 | ], 86 | options={ 87 | 'ordering': ('title',), 88 | }, 89 | ), 90 | migrations.CreateModel( 91 | name='Text', 92 | fields=[ 93 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 94 | ('title', models.CharField(max_length=250)), 95 | ('created', models.DateTimeField(auto_now_add=True)), 96 | ('updated', models.DateTimeField(auto_now=True)), 97 | ('content', models.TextField()), 98 | ], 99 | options={ 100 | 'abstract': False, 101 | }, 102 | ), 103 | migrations.CreateModel( 104 | name='Video', 105 | fields=[ 106 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 107 | ('title', models.CharField(max_length=250)), 108 | ('created', models.DateTimeField(auto_now_add=True)), 109 | ('updated', models.DateTimeField(auto_now=True)), 110 | ('url', models.URLField()), 111 | ], 112 | options={ 113 | 'abstract': False, 114 | }, 115 | ), 116 | ] 117 | -------------------------------------------------------------------------------- /apps/courses/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models import Avg 3 | from django.conf import settings 4 | from django.contrib.contenttypes.models import ContentType 5 | from django.contrib.contenttypes.fields import GenericForeignKey 6 | from apps.courses.fields import OrderField 7 | from django.template.loader import render_to_string 8 | from django.utils.safestring import mark_safe 9 | from django.utils.text import slugify 10 | from django.utils import timezone 11 | from autoslug import AutoSlugField 12 | 13 | import numpy as np 14 | 15 | class Subject(models.Model): 16 | title = models.CharField(max_length=200) 17 | slug = models.SlugField(max_length=200, unique=True) 18 | 19 | class Meta: 20 | ordering = ('title',) 21 | 22 | def __str__(self): 23 | return self.title 24 | 25 | class Course(models.Model): 26 | owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='courses_created', on_delete=models.CASCADE) 27 | subject = models.ForeignKey(Subject, related_name='courses', on_delete=models.CASCADE) 28 | title = models.CharField(max_length=200) 29 | # slug = models.SlugField(max_length=200, unique=True) 30 | slug = AutoSlugField(populate_from='title', unique_with='created__month') 31 | overview = models.TextField() 32 | created = models.DateTimeField(auto_now_add=True) 33 | students = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='courses_joined', blank=True) 34 | 35 | class Meta: 36 | ordering = ('-created',) 37 | 38 | # def save(self, *args, **kwargs): 39 | # if not self.slug: 40 | # self.slug = slugify(self.title) 41 | # super(Course, self).save(*args, **kwargs) 42 | 43 | def average_rating(self): 44 | # all_ratings = map(lambda x: x.rating, self.reviews.all()) 45 | # return np.mean(all_ratings) 46 | return self.reviews.aggregate(Avg('rating'))['rating__avg'] 47 | 48 | def __str__(self): 49 | return self.title 50 | 51 | class Module(models.Model): 52 | course = models.ForeignKey(Course, related_name='modules', on_delete=models.CASCADE) 53 | title = models.CharField(max_length=200) 54 | description = models.TextField(blank=True) 55 | order = OrderField(blank=True, for_fields=['course']) 56 | 57 | class Meta: 58 | ordering = ['order'] 59 | 60 | def __str__(self): 61 | return '{}. {}'.format(self.order, self.title) 62 | 63 | 64 | class ItemBase(models.Model): 65 | owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='%(class)s_related', on_delete=models.CASCADE) 66 | title = models.CharField(max_length=250) 67 | created = models.DateTimeField(auto_now_add=True) 68 | updated = models.DateTimeField(auto_now=True) 69 | 70 | class Meta: 71 | abstract = True 72 | 73 | def render(self): 74 | return render_to_string('courses/content/{}.html'.format(self._meta.model_name), {'item': self}) 75 | 76 | def __str__(self): 77 | return self.title 78 | 79 | class Text(ItemBase): 80 | content = models.TextField() 81 | 82 | class File(ItemBase): 83 | file = models.FileField(upload_to='files') 84 | 85 | class Image(ItemBase): 86 | file = models.FileField(upload_to='images') 87 | 88 | class Video(ItemBase): 89 | url = models.URLField() 90 | 91 | 92 | class Content(models.Model): 93 | module = models.ForeignKey(Module, related_name='contents', on_delete=models.CASCADE) 94 | content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in':('text', 'video', 'image', 'file')}, on_delete=models.CASCADE) 95 | object_id = models.PositiveIntegerField() 96 | item = GenericForeignKey('content_type', 'object_id') 97 | order = OrderField(blank=True, for_fields=['module']) 98 | 99 | class Meta: 100 | ordering = ['order'] 101 | 102 | 103 | class Review(models.Model): 104 | RATING_CHOICES = ( 105 | (1, '1'), 106 | (2, '2'), 107 | (3, '3'), 108 | (4, '4'), 109 | (5, '5') 110 | ) 111 | course = models.ForeignKey(Course, related_name='reviews', on_delete=models.CASCADE) 112 | pub_date = models.DateTimeField(auto_now_add=True) 113 | user_name = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='reviewers', on_delete=models.CASCADE) 114 | comment = models.CharField(max_length=200) 115 | rating = models.IntegerField(choices=RATING_CHOICES) 116 | 117 | 118 | # https://github.com/pinax/pinax-badges 119 | class BadgeAward(models.Model): 120 | user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="badges_earned", on_delete=models.CASCADE) 121 | awarded_at = models.DateTimeField(default=timezone.now) 122 | slug = models.CharField(max_length=255) 123 | level = models.IntegerField() 124 | 125 | def __str__(self): 126 | return "{} : {} points - level {}".format(self.user.username, self.user.profile.award_points, self.level) 127 | -------------------------------------------------------------------------------- /templates/courses/course/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% load static %} 4 | {% load gravatar %} 5 | {% block title %} 6 | {{ object.title }} 7 | {% endblock %} 8 | {% block content %} 9 |
10 | {% with subject=course.subject %} 11 |
12 |
13 |
14 |
15 | 25 |

{{ subject.title }} / {{ course.created | date:"d M Y" }}

26 |

{{ object.title }}

27 |

{{ object.overview|linebreaks }}

28 |

29 | {{ course.modules.count }} {% trans 'module(s)' %} 30 |

31 | {% if course.modules.count > 0 %} 32 |
    33 | {% for module in course.modules.all %} 34 |
  • - {{ module.title }}
  • 35 | {% endfor %} 36 |
37 | {% endif %} 38 | {% if request.user.is_authenticated and course.modules.count > 0%} 39 |
40 | {{ enroll_form }} 41 | {% csrf_token %} 42 | 43 |
44 | {% elif request.user.is_authenticated and course.modules.count == 0 %} 45 |

{% trans "In progress" %}...

46 | {% else %} 47 | 48 | {% trans 'Register to enroll' %} 49 | 50 | {% endif %} 51 |
52 | {% if course.reviews.all %} 53 | {% for review in course.reviews.all %} 54 |
55 |
56 |
57 |
58 | 59 |
60 |
61 | 62 | 63 | 64 |
65 |
66 |
67 |

68 | {{ review.user_name }} 69 |

70 |
71 | {% with ''|center:review.rating as range %} 72 | {% for i in range %} 73 | 74 | {% endfor %} 75 | {% endwith %} 76 | {% trans 'rated' %} {{ review.rating }} {% trans 'of 5' %} 77 |
78 |
79 |

{{ review.comment }}

80 |
81 |
82 |
83 | {% endfor %} 84 | {% endif %} 85 | {% if request.user.is_authenticated %} 86 |
87 |
88 | {% csrf_token %} 89 | {{ review_form.as_p }} 90 | 91 |
92 |
93 | {% endif %} 94 |
95 | {% endwith %} 96 |
97 | {% endblock %} 98 | -------------------------------------------------------------------------------- /templates/courses/manage/course/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | {% load i18n %} 4 | {% block title %}{% trans "My courses"%}{% endblock %} 5 | {% block content %} 6 |
7 |

{% trans "My courses"%}

8 |

{% trans "Create a course, then edit some modules and manage their contents" %}

9 | {% for course in object_list %} 10 |
11 |
12 |
13 |

14 | {{ course.title }} 15 |

16 |
17 |
18 | {% if course.modules.count > 0 %} 19 | {% trans "Manage content" %} 20 | {% endif %} 21 |
22 |
23 | 24 | 27 | {% trans "Created" %}: {{ course.created| date:'d/m/y'}} 28 |
29 |
30 |
31 |
32 | 41 | 42 | 50 | 51 | 52 | 53 | 54 | 57 | {% trans "Edit modules" %} 58 | 59 | 60 | 61 | 62 | 63 | 70 | 71 | 75 | 76 |
77 |
78 | {% empty %} 79 |

You haven't created any courses yet.

80 | {% endfor %} 81 |
82 |
83 | 84 | Create new 85 | course 86 |
87 |
88 | {% endblock %} 89 | -------------------------------------------------------------------------------- /apps/students/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.utils.text import capfirst 3 | from django.utils.translation import gettext_lazy, gettext as _ 4 | from django.contrib.auth.forms import UserCreationForm 5 | from django.db import transaction 6 | from django.forms.utils import ValidationError 7 | from apps.courses.models import Course 8 | from apps.students.models import ( 9 | Answer, 10 | Question, 11 | Student, 12 | StudentAnswer, 13 | Tag, 14 | User 15 | ) 16 | 17 | class CourseEnrollForm(forms.Form): 18 | course = forms.ModelChoiceField(queryset=Course.objects.all(), widget=forms.HiddenInput) 19 | 20 | 21 | class TeacherSignupForm(UserCreationForm): 22 | username = forms.CharField(label='', widget = forms.TextInput(attrs={'class':'flex-grow h-8 mt-4 px-2 rounded border border-gray-400', 'placeholder':'Username'})) 23 | email = forms.EmailField(required = True,label='', widget=forms.TextInput(attrs={'class':'flex-grow mt-4 h-8 px-2 rounded border border-gray-400', 'placeholder': 'E-mail'})) 24 | password1 = forms.CharField(label='', widget=forms.PasswordInput(attrs={'class':'flex-grow h-8 mt-4 px-2 rounded border border-gray-400', 'placeholder': 'Password'})) 25 | password2 = forms.CharField(label='', widget=forms.PasswordInput(attrs={'class':'flex-grow h-8 mt-4 px-2 rounded border border-gray-400', 'placeholder': 'Password verification'})) 26 | 27 | class Meta(UserCreationForm.Meta): 28 | model = User 29 | 30 | def clean(self): 31 | email = self.cleaned_data.get('email') 32 | username = self.cleaned_data.get('username') 33 | if User.objects.filter(email=email).exists() or User.objects.filter(username=username).exists(): 34 | raise forms.ValidationError("Email or Username exists") 35 | return self.cleaned_data 36 | 37 | def save(self, commit=True): 38 | user = super().save(commit=False) 39 | user.is_teacher = True 40 | user.email = self.cleaned_data['email'] 41 | if commit: 42 | user.save() 43 | return user 44 | 45 | 46 | class StudentSignupForm(UserCreationForm): 47 | username = forms.CharField(label='', widget=forms.TextInput(attrs={'class':'flex-grow h-8 mt-4 px-2 rounded border border-gray-400', 'placeholder': 'Username'})) 48 | email = forms.EmailField(required = True,label='', widget=forms.TextInput(attrs={'class':'flex-grow h-8 mt-4 px-2 rounded border border-gray-400','placeholder': 'E-mail'})) 49 | password1 = forms.CharField(label='', widget=forms.PasswordInput(attrs={'class':'flex-grow h-8 mt-4 px-2 rounded border border-gray-400', 'placeholder': 'Password'})) 50 | password2 = forms.CharField(label='', widget=forms.PasswordInput(attrs={'class':'flex-grow h-8 mt-4 px-2 rounded border border-gray-400', 'placeholder': 'Password verification'})) 51 | interests = forms.ModelMultipleChoiceField( 52 | label='', 53 | queryset=Tag.objects.all(), 54 | widget=forms.CheckboxSelectMultiple(attrs={'class':'mt-4 form-control'}), 55 | required=True 56 | ) 57 | 58 | class Meta(UserCreationForm.Meta): 59 | model = User 60 | 61 | def clean(self): 62 | email = self.cleaned_data.get('email') 63 | username = self.cleaned_data.get('username') 64 | if User.objects.filter(email=email).exists() or User.objects.filter(username=username).exists(): 65 | raise forms.ValidationError("Email or Username exists") 66 | return self.cleaned_data 67 | 68 | @transaction.atomic 69 | def save(self, commit=True): 70 | user = super().save(commit=False) 71 | user.is_student = True 72 | user.email = self.cleaned_data['email'] 73 | user.save() 74 | student = Student.objects.create(user=user) 75 | student.interests.add(*self.cleaned_data.get('interests')) 76 | return user 77 | 78 | 79 | class StudentInterestsForm(forms.ModelForm): 80 | 81 | class Meta: 82 | model = Student 83 | fields = ('interests',) 84 | widgets = { 85 | 'interests': forms.CheckboxSelectMultiple 86 | } 87 | 88 | 89 | class QuestionForm(forms.ModelForm): 90 | # text = forms.CharField(label="Question", widget=forms.TextInput(attrs={'class':'form-control'})) 91 | 92 | class Meta: 93 | model = Question 94 | fields = ('text',) 95 | 96 | 97 | class BaseAnswerInlineFormSet(forms.BaseInlineFormSet): 98 | 99 | def clean(self): 100 | super().clean() 101 | 102 | has_one_correct_answer = False 103 | for form in self.forms: 104 | if not form.cleaned_data.get('DELETE', False): 105 | if form.cleaned_data.get('is_correct', False): 106 | has_one_correct_answer = True 107 | break 108 | if not has_one_correct_answer: 109 | raise ValidationError('Mark at least one answer as correct.', code='no_correct_answer') 110 | 111 | 112 | class TakeQuizForm(forms.ModelForm): 113 | answer = forms.ModelChoiceField( 114 | queryset=Answer.objects.all(), 115 | widget=forms.RadioSelect(), 116 | required=True, 117 | empty_label=None 118 | ) 119 | 120 | class Meta: 121 | model = StudentAnswer 122 | fields = ('answer',) 123 | 124 | def __init__(self, *args, **kwargs): 125 | question = kwargs.pop('question') 126 | super().__init__(*args,**kwargs) 127 | self.fields['answer'].queryset = question.answers.order_by('text') 128 | 129 | 130 | class ContactForm(forms.Form): 131 | contact_name = forms.CharField(label="Your name", required=True, widget=forms.TextInput(attrs={'class':'form-control'})) 132 | contact_email = forms.EmailField(label="Your email address", required=True, widget=forms.TextInput(attrs={'class':'form-control'})) 133 | form_content = forms.CharField(label="Your message", required=True, widget=forms.Textarea(attrs={'class':'form-control'})) 134 | 135 | def __init__(self, *args, **kwargs): 136 | super(ContactForm, self).__init__(*args, **kwargs) 137 | self.fields['contact_name'].label = 'Your name:' 138 | self.fields['contact_email'].label = 'Your email address:' 139 | self.fields['form_content'].label = 'Subject of your message:' 140 | -------------------------------------------------------------------------------- /templates/videos/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n %} 3 | {% block title %} 4 | {% trans 'Videos' %} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |
11 |

{% trans 'Search for a video' %}

12 |

{% if q %}{% trans 'Results for' %} : {{ q }}{% endif %}

13 | 14 |
15 | 20 | {% trans 'number of results' %} 21 | {% for subject in subjects %} 22 |
23 | {{ subject }} 24 |
25 | {% endfor %} 26 | 27 |
28 |
29 |
30 |
31 |
32 |
33 | {% if videos %} 34 | {% for item in videos.items %} 35 |
36 | 47 |
48 |

{{ item.snippet.title }}

49 |

50 | {{ item.snippet.description }} 51 |

52 |

53 | {% if item.id.kind == 'youtube#video' %} 54 | {% trans 'video' %} 55 | {% elif item.id.kind == 'youtube#channel' %} 56 | {% trans 'channel' %} 57 | {% elif item.id.kind == 'youtube#playlist' %} 58 | {% trans 'playlist' %} 59 | {% endif %} 60 |

61 |
62 |
63 | {% endfor %} 64 | {% endif %} 65 |
66 |
67 |
68 | 89 |
90 | 100 | {% endblock %} 101 | 102 | {% block domready %} 103 | $('.video').on('click', function(evt) { 104 | evt.preventDefault(); 105 | 106 | $('body #lecteur-video').removeClass('is-hidden'); 107 | 108 | $('html, body').animate({ scrollTop: 0 }, "slow"); 109 | return false; 110 | }); 111 | 112 | $('#closeVideo').on('click', function(evt) { 113 | evt.preventDefault(); 114 | 115 | $('body #lecteur-video').addClass('is-hidden'); 116 | $('#video-iframe').attr('src', ''); 117 | }); 118 | 119 | $('.video').on('click', function(evt) { 120 | evt.preventDefault(); 121 | 122 | var $this = $(this); 123 | var videoSrc = $this.data('video-src'); 124 | 125 | if ($('#video-iframe').length) { 126 | $('#video-iframe').removeClass('hidden'); 127 | $('#video-iframe').attr('src', videoSrc); 128 | 129 | return false 130 | } 131 | 132 | return true; 133 | }); 134 | {% endblock %} 135 | -------------------------------------------------------------------------------- /myelearning/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for myelearning project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11.5. 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 | import sys 15 | from decouple import config 16 | from django.urls 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 | # Add the parent directory to sys.path 22 | PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 23 | sys.path.insert(0, PROJECT_ROOT) 24 | 25 | # Quick-start development settings - unsuitable for production 26 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 27 | 28 | # SECURITY WARNING: keep the secret key used in production secret! 29 | SECRET_KEY = config('SECRET_KEY', 'dummy_secret_key') 30 | 31 | # SECURITY WARNING: don't run with debug turned on in production! 32 | DEBUG = config('DEBUG', 'True', cast=bool) 33 | 34 | ALLOWED_HOSTS = ['*'] 35 | 36 | 37 | # Application definition 38 | 39 | INSTALLED_APPS = [ 40 | 'apps.courses.apps.CoursesConfig', 41 | 'django.contrib.sites', 42 | 'django.contrib.flatpages', 43 | 'django.contrib.admin', 44 | 'django.contrib.auth', 45 | 'django.contrib.contenttypes', 46 | 'django.contrib.sessions', 47 | 'django.contrib.messages', 48 | 'django.contrib.staticfiles', 49 | 'django.contrib.humanize', 50 | 'django.contrib.admindocs', 51 | 'crispy_forms', 52 | 'apps.students.apps.StudentsConfig', 53 | 'embed_video', 54 | 'rest_framework', 55 | 'storages', 56 | 'widget_tweaks', 57 | 'corsheaders', 58 | 'taggit', 59 | 'taggit_serializer', 60 | ] 61 | 62 | MIDDLEWARE = [ 63 | 'django.middleware.security.SecurityMiddleware', 64 | 'whitenoise.middleware.WhiteNoiseMiddleware', 65 | 'django.contrib.sessions.middleware.SessionMiddleware', 66 | 'apps.students.middleware.SessionTimeoutMiddleware', 67 | # 'django.middleware.cache.UpdateCacheMiddleware', 68 | 'django.middleware.common.CommonMiddleware', 69 | # 'django.middleware.cache.FetchFromCacheMiddleware', 70 | 'django.middleware.csrf.CsrfViewMiddleware', 71 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 72 | 'django.contrib.messages.middleware.MessageMiddleware', 73 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 74 | 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', 75 | 'django.contrib.admindocs.middleware.XViewMiddleware', 76 | 'corsheaders.middleware.CorsMiddleware', 77 | ] 78 | 79 | # if DEBUG == False: 80 | # MIDDLEWARE += ('courses.middleware.SubdomainCourseMiddleware') 81 | 82 | 83 | ROOT_URLCONF = 'myelearning.urls' 84 | 85 | TEMPLATES = [ 86 | { 87 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 88 | 'DIRS': [ 89 | # template at the root of the project 90 | os.path.join(BASE_DIR, 'templates'), 91 | ], 92 | 'APP_DIRS': True, 93 | 'OPTIONS': { 94 | 'context_processors': [ 95 | 'django.template.context_processors.debug', 96 | 'django.template.context_processors.request', 97 | 'django.contrib.auth.context_processors.auth', 98 | 'django.contrib.messages.context_processors.messages', 99 | ], 100 | }, 101 | }, 102 | ] 103 | 104 | WSGI_APPLICATION = 'myelearning.wsgi.application' 105 | 106 | SITE_ID = 1 107 | 108 | # Database 109 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 110 | 111 | DATABASES = { 112 | 'default': { 113 | 'ENGINE': 'django.db.backends.sqlite3', 114 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 115 | } 116 | } 117 | 118 | 119 | # Password validation 120 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 121 | 122 | AUTH_PASSWORD_VALIDATORS = [ 123 | { 124 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 125 | }, 126 | { 127 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 128 | }, 129 | { 130 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 131 | }, 132 | { 133 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 134 | }, 135 | ] 136 | 137 | 138 | # Internationalization 139 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 140 | 141 | LANGUAGE_CODE = 'en-us' 142 | 143 | TIME_ZONE = 'UTC' 144 | 145 | USE_I18N = True 146 | 147 | USE_L10N = True 148 | 149 | USE_TZ = False 150 | 151 | 152 | # Static files (CSS, JavaScript, Images) 153 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 154 | 155 | STATIC_URL = '/static/' 156 | MEDIA_URL = '/media/' 157 | 158 | STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') 159 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 160 | 161 | STATICFILES_DIRS = ( 162 | os.path.join(BASE_DIR, 'static'), 163 | ) 164 | 165 | # Custom auth 166 | 167 | AUTH_USER_MODEL = 'students.User' 168 | LOGIN_REDIRECT_URL = reverse_lazy('students:classroom') 169 | LOGIN_URL = reverse_lazy('login') 170 | LOGOOUT_URL = reverse_lazy('logout') 171 | LOGOUT_REDIRECT_URL = reverse_lazy('courses:course_list') 172 | 173 | 174 | AUTHENTICATION_BACKENDS = [ 175 | 'django.contrib.auth.backends.ModelBackend', 176 | 'apps.students.authentication.EmailAuthBackend', 177 | ] 178 | 179 | 180 | # Cache 181 | CACHES = { 182 | 'default': { 183 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 184 | } 185 | } 186 | 187 | CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True 188 | CACHE_MIDDLEWARE_SECONDS = 60 * 15 # 15 minutes 189 | CACHE_MIDDLEWARE_KEY_PREFIX = 'el' 190 | 191 | # DRF 192 | REST_FRAMEWORK = { 193 | 'DEFAULT_PERMISSION_CLASSES': [ 194 | 'rest_framework.permissions.IsAuthenticatedOrReadOnly', 195 | ], 196 | 'TEST_REQUEST_RENDERER_CLASSES': ( 197 | 'rest_framework.renderers.MultiPartRenderer', 198 | 'rest_framework.renderers.JSONRenderer', 199 | 'rest_framework.renderers.TemplateHTMLRenderer', 200 | ) 201 | } 202 | 203 | # Mailer 204 | DEFAULT_FROM_EMAIL = config('ADMIN_EMAIL', 'no-reply@myelearnig.com') 205 | 206 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 207 | 208 | # Session 209 | SESSION_EXPIRE_SECONDS = 18000 # 5 hours 210 | SESSION_EXPIRE_AFTER_LAST_ACTIVITY = True 211 | 212 | DEVELOPER_KEY = config('DEVELOPER_API_KEY', 'developer_key_here') 213 | 214 | # corsheaders 215 | CORS_ORIGIN_ALLOW_ALL = False 216 | CORS_ALLOW_CREDENTIALS = True 217 | 218 | CORS_ORIGIN_WHITELIST = ( 219 | 'https://myelearning.herokuapp.com', 220 | 'http://localhost:8080', 221 | 'http://localhost:8100', 222 | 'http://localhost:8000', 223 | 'http://localhost:3000', 224 | 'https://pwa-myelearning.netlify.app', 225 | 'http://localhost', 226 | ) 227 | 228 | CORS_ALLOW_METHODS = ( 229 | 'GET', 230 | 'POST', 231 | 'PUT', 232 | 'DELETE', 233 | ) 234 | 235 | # Task async 236 | CELERY_BROKER_URL = config('REDIS_URL', 'redis://localhost:6379/0', cast=str) 237 | CELERY_RESULT_BACKEND = config('REDIS_URL', 'redis://localhost:6379/0', cast=str) 238 | 239 | CRISPY_TEMPLATE_PACK = 'bootstrap4' 240 | -------------------------------------------------------------------------------- /apps/students/views/students.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.core.exceptions import ObjectDoesNotExist, FieldDoesNotExist, FieldError 4 | from django.contrib.auth.decorators import login_required 5 | from django.shortcuts import render, redirect, get_object_or_404 6 | from django.db import transaction 7 | from django.db.models import Count 8 | from django.urls import reverse_lazy 9 | from django.utils.decorators import method_decorator 10 | from django.views.generic.edit import CreateView, FormView, UpdateView 11 | from django.views.generic.list import ListView 12 | from django.views.generic.detail import DetailView 13 | from django.contrib.auth.forms import UserCreationForm 14 | from braces.views import LoginRequiredMixin 15 | from django.contrib.auth import authenticate, login 16 | from apps.students.forms import (CourseEnrollForm, StudentInterestsForm, TakeQuizForm, StudentSignupForm) 17 | from apps.courses.models import Course, Review 18 | from apps.students.models import (Quiz, Student, TakenQuiz, User) 19 | from django.core.mail import mail_admins 20 | from django.contrib import messages 21 | from apps.students.decorators import student_required 22 | 23 | from apps.courses.badges import possibly_award_badge 24 | 25 | class StudentCourseListView(LoginRequiredMixin, ListView): 26 | model = Course 27 | template_name = 'students/course/list.html' 28 | 29 | def get_queryset(self): 30 | qs = super(StudentCourseListView, self).get_queryset() 31 | return qs.filter(students__in=[self.request.user]) 32 | 33 | 34 | class StudentCourseDetailView(LoginRequiredMixin, DetailView): 35 | model = Course 36 | template_name = 'students/course/detail.html' 37 | 38 | def get_queryset(self): 39 | qs = super(StudentCourseDetailView, self).get_queryset() 40 | return qs.filter(students__in=[self.request.user]) 41 | 42 | def get_context_data(self, **kwargs): 43 | context = super(StudentCourseDetailView, self).get_context_data(**kwargs) 44 | course = self.get_object() 45 | if 'module_id' in self.kwargs: 46 | #get current module 47 | context['module'] = course.modules.get(id=self.kwargs['module_id']) 48 | else: 49 | #get first module 50 | context['module'] = course.modules.all()[0] 51 | return context 52 | 53 | 54 | class StudentRegistrationView(CreateView): 55 | model = User 56 | template_name = 'registration/signup_form.html' 57 | form_class = StudentSignupForm 58 | success_url = reverse_lazy('students:student_course_list') 59 | 60 | def get_context_data(self, **kwargs): 61 | kwargs['user_type'] = 'student' 62 | return super().get_context_data(**kwargs) 63 | 64 | def form_valid(self, form): 65 | result = super(StudentRegistrationView, self).form_valid(form) 66 | cd = form.cleaned_data 67 | user = authenticate(username=cd['username'], password=cd['password1']) 68 | user.profile.get_award_points(3) 69 | possibly_award_badge("student_signup", user=user) 70 | mail_admins("{} is sign up".format(user.username), "check email on myelearning") 71 | login(self.request, user) 72 | return result 73 | 74 | 75 | class StudentEnrollCourseView(FormView): 76 | course = None 77 | form_class = CourseEnrollForm 78 | 79 | def form_valid(self, form): 80 | self.course = form.cleaned_data['course'] 81 | self.course.students.add(self.request.user) 82 | self.request.user.profile.get_award_points(3) 83 | possibly_award_badge("enroll_course", user=self.request.user) 84 | return super(StudentEnrollCourseView, self).form_valid(form) 85 | 86 | def get_success_url(self): 87 | return reverse_lazy('students:student_course_detail', args=[self.course.id]) 88 | 89 | 90 | @method_decorator([login_required, student_required], name='dispatch') 91 | class StudentInterestsView(UpdateView): 92 | model = Student 93 | form_class = StudentInterestsForm 94 | template_name = 'students/student/interests_form.html' 95 | success_url = reverse_lazy('students:student_quiz_list') 96 | 97 | def get_object(self): 98 | try: 99 | return self.request.user.student 100 | except ObjectDoesNotExist: 101 | return self.request.user 102 | 103 | 104 | def form_valid(self, form): 105 | messages.success(self.request, 'Interests updated with success.') 106 | return super().form_valid(form) 107 | 108 | 109 | @method_decorator([login_required, student_required], name='dispatch') 110 | class QuizListView(ListView): 111 | model = Quiz 112 | ordering = ('name',) 113 | context_object_name = 'quizzes' 114 | template_name = 'students/student/quiz_list.html' 115 | 116 | def get_queryset(self): 117 | try: 118 | student = self.request.user.student 119 | student_interests = student.interests.values_list('pk', flat=True) 120 | taken_quizzes = student.quizzes.values_list('pk', flat=True) 121 | queryset = Quiz.objects.filter(tags__in=student_interests).exclude(pk__in=taken_quizzes).annotate(question_count=Count('questions')).filter(question_count__gt=0) 122 | return queryset 123 | except ObjectDoesNotExist: 124 | return self.request.user 125 | 126 | 127 | @method_decorator([login_required, student_required], name='dispatch') 128 | class TakenQuizListView(ListView): 129 | model = TakenQuiz 130 | context_object_name = 'taken_quizzes' 131 | template_name = 'students/student/taken_quiz_list.html' 132 | 133 | def get_queryset(self): 134 | queryset = self.request.user.student.taken_quizzes.select_related('quiz', 'quiz__tags').order_by('quiz__name') 135 | return queryset 136 | 137 | 138 | @login_required 139 | @student_required 140 | def take_quiz(request, pk): 141 | quiz = get_object_or_404(Quiz, pk=pk) 142 | student = request.user.student 143 | 144 | if student.quizzes.filter(pk=pk).exists(): 145 | return render(request, 'students/student/taken_quiz_list.html') 146 | 147 | total_questions = quiz.questions.count() 148 | unanswered_questions = student.get_unanswered_questions(quiz) 149 | total_unanswered_questions = unanswered_questions.count() 150 | progress = 100 - round(((total_unanswered_questions - 1) / total_questions) * 100) 151 | question = unanswered_questions.first() 152 | 153 | if request.method == 'POST': 154 | form = TakeQuizForm(question=question, data=request.POST) 155 | if form.is_valid(): 156 | with transaction.atomic(): 157 | student_answer = form.save(commit=False) 158 | student_answer.student = student 159 | request.user.profile.get_award_points(10) 160 | possibly_award_badge("take_quiz", user=request.user) 161 | student_answer.save() 162 | if student.get_unanswered_questions(quiz).exists(): 163 | return redirect('students:take_quiz', pk) 164 | else: 165 | correct_answers = student.quiz_answers.filter(answer__question__quiz=quiz, answer__is_correct=True).count() 166 | score = round((correct_answers / total_questions ) * 100.0, 2) 167 | TakenQuiz.objects.create(student=student, quiz=quiz, score=score) 168 | if score < 50.0: 169 | messages.warning(request, 'Good luck for next time! Your score for this quiz %s was %s.' % (quiz.name, score)) 170 | else: 171 | messages.success(request, 'Fantastic! You completed the quiz %s with success! Your scored %s points.' % (quiz.name, score)) 172 | return redirect('students:student_quiz_list') 173 | else: 174 | form = TakeQuizForm(question=question) 175 | 176 | return render(request, 'students/student/take_quiz_form.html', { 177 | 'quiz': quiz, 178 | 'question': question, 179 | 'form': form, 180 | 'progress': progress 181 | }) 182 | -------------------------------------------------------------------------------- /apps/students/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2018-05-30 19:36 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 | ('courses', '0001_initial'), 19 | ('auth', '0008_alter_user_username_max_length'), 20 | ] 21 | 22 | operations = [ 23 | migrations.CreateModel( 24 | name='User', 25 | fields=[ 26 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 27 | ('password', models.CharField(max_length=128, verbose_name='password')), 28 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 29 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 30 | ('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')), 31 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), 32 | ('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')), 33 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 34 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 35 | ('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')), 36 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 37 | ('is_student', models.BooleanField(default=False)), 38 | ('is_teacher', models.BooleanField(default=False)), 39 | ], 40 | options={ 41 | 'verbose_name': 'user', 42 | 'verbose_name_plural': 'users', 43 | 'abstract': False, 44 | }, 45 | managers=[ 46 | ('objects', django.contrib.auth.models.UserManager()), 47 | ], 48 | ), 49 | migrations.CreateModel( 50 | name='Answer', 51 | fields=[ 52 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 53 | ('text', models.CharField(max_length=255, verbose_name='Answer')), 54 | ('is_correct', models.BooleanField(default=False, verbose_name='Correct answer')), 55 | ], 56 | ), 57 | migrations.CreateModel( 58 | name='Question', 59 | fields=[ 60 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 61 | ('text', models.CharField(max_length=255, verbose_name='Question')), 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 | ('name', models.CharField(max_length=255)), 69 | ], 70 | ), 71 | migrations.CreateModel( 72 | name='StudentAnswer', 73 | fields=[ 74 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 75 | ('answer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='students.Answer')), 76 | ], 77 | ), 78 | migrations.CreateModel( 79 | name='Tag', 80 | fields=[ 81 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 82 | ('name', models.CharField(max_length=30)), 83 | ('color', models.CharField(default='#007bff', max_length=7)), 84 | ('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subject', to='courses.Subject')), 85 | ], 86 | ), 87 | migrations.CreateModel( 88 | name='TakenQuiz', 89 | fields=[ 90 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 91 | ('score', models.FloatField()), 92 | ('date', models.DateTimeField(auto_now_add=True)), 93 | ('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='taken_quizzes', to='students.Quiz')), 94 | ], 95 | ), 96 | migrations.CreateModel( 97 | name='Student', 98 | fields=[ 99 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), 100 | ('interests', models.ManyToManyField(related_name='interested_students', to='students.Tag')), 101 | ], 102 | ), 103 | migrations.AddField( 104 | model_name='quiz', 105 | name='owner', 106 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quizzes', to=settings.AUTH_USER_MODEL), 107 | ), 108 | migrations.AddField( 109 | model_name='quiz', 110 | name='tags', 111 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quizzes', to='students.Tag'), 112 | ), 113 | migrations.AddField( 114 | model_name='question', 115 | name='quiz', 116 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='students.Quiz'), 117 | ), 118 | migrations.AddField( 119 | model_name='answer', 120 | name='question', 121 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='students.Question'), 122 | ), 123 | migrations.AddField( 124 | model_name='user', 125 | name='groups', 126 | field=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'), 127 | ), 128 | migrations.AddField( 129 | model_name='user', 130 | name='user_permissions', 131 | field=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'), 132 | ), 133 | migrations.AddField( 134 | model_name='takenquiz', 135 | name='student', 136 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='taken_quizzes', to='students.Student'), 137 | ), 138 | migrations.AddField( 139 | model_name='studentanswer', 140 | name='student', 141 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quiz_answers', to='students.Student'), 142 | ), 143 | migrations.AddField( 144 | model_name='student', 145 | name='quizzes', 146 | field=models.ManyToManyField(through='students.TakenQuiz', to='students.Quiz'), 147 | ), 148 | ] 149 | --------------------------------------------------------------------------------