diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d272c32
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,93 @@
+# Created by https://www.gitignore.io
+
+### OSX ###
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear on external disk
+.Spotlight-V100
+.Trashes
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+
+### Python ###
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.cache
+nosetests.xml
+coverage.xml
+
+# Translations
+*.mo
+*.pot
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+
+### Django ###
+*.log
+*.pot
+*.pyc
+__pycache__/
+local_settings.py
+
+.vscode
+.env
+db.sqlite3
+
+media
+static
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..53aca17
--- /dev/null
+++ b/README.md
@@ -0,0 +1,68 @@
+# Advanced student management and e-learning system
+My portfolio website -> https://ezop.herokuapp.com
+
+I build this student management and e-learning system using django and Bootstrap for the front-end. You can watch the video on YouTube to find out how the app works in real time https://youtu.be/ytP-k95F0ug
+
+For the quiz part, I used this repo as a reference -> https://github.com/tomwalker/django_quiz
+
+Quizzes
+
+
+
+Current features
+----------------
+* News And Events
+* The admin can Add Students
+* The admin can Add Lecturers
+* Students can Add and Drop courses
+* Lecturers submit students score (Attendance, Mid exam, Final exam, assignment)
+* The system calculates the Total, Avarage, point, and grade of each student then save it
+* Also, the system tells the student whether he/she pass, fail or pass with a warning
+* Assessment result
+* Grade result
+* Documentations
+* Video Tutorials
+* PDF generator for students registration slip
+* PDF generator for students result
+* Storing of quiz results under each user
+* Question order randomisation
+* Previous quiz scores can be viewed on category page
+* Correct answers can be shown after each question or all at once at the end
+* Logged in users can return to an incomplete quiz to finish it and non-logged in users can complete a quiz if their session persists
+* The quiz can be limited to one attempt per user
+* Questions can be given a category
+* Success rate for each category can be monitored on a progress page
+* Explanation for each question result can be given
+* Pass marks can be set
+* Multiple choice question type
+* True/False question type
+* Essay question type
+* Custom message displayed for those that pass or fail a quiz
+* Custom permission (view_sittings) added, allowing users with that permission to view quiz results from users
+* A marking page which lists completed quizzes, can be filtered by quiz or user, and is used to mark essay questions
+
+* Another cool feature of the app is, unnecessary files are removed automatically for memory efficiency
+
+
+
+
+
+
+
+
+
+After the student finished the quiz, here is how the result display
+
+
+
+# Installation
+
+First Clone the repo with `git clone https://github.com/adilmohak/django_sms_and_e-learning.git`
+
+Run the following commands
+`pip install -r requirements.txt`
+`python manage.py runserver`
+
+Finally go to this address http://127.0.0.1:8000
+
+# Thank You!!
diff --git a/SMS/__init__.py b/SMS/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/SMS/settings.py b/SMS/settings.py
new file mode 100644
index 0000000..8e28336
--- /dev/null
+++ b/SMS/settings.py
@@ -0,0 +1,159 @@
+"""
+Django settings for SMS project.
+
+Generated by 'django-admin startproject' using Django 2.2.3.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/2.2/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/2.2/ref/settings/
+"""
+
+import os
+import posixpath
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'o!ld8nrt4vc*h1zoey*wj48x*q0#ss12h=+zh)kk^6b3aygg=!'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+# change the default user models to our custom model
+AUTH_USER_MODEL = 'accounts.User'
+
+# Application definition
+
+INSTALLED_APPS = [
+
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'django_cleanup',
+
+ # custom apps
+ 'app.apps.AppConfig',
+ 'accounts.apps.AccountsConfig',
+ 'course.apps.CourseConfig',
+ 'result.apps.ResultConfig',
+ 'search.apps.SearchConfig',
+ 'quiz.apps.QuizConfig',
+ 'crispy_forms'
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'SMS.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [os.path.join(BASE_DIR, 'templates')],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+
+ # 'django.template.context_processors.i18n',
+ # 'django.template.context_processors.media',
+ # 'django.template.context_processors.static',
+ # 'django.template.context_processors.tz',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'SMS.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/2.2/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/2.2/howto/static-files/
+
+STATIC_URL = '/static/'
+STATIC_ROOT = posixpath.join(*(BASE_DIR.split(os.path.sep) + ['staticfiles']))
+STATICFILES_DIRS = [
+ os.path.join(BASE_DIR, "static"),
+]
+
+MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
+MEDIA_URL = '/media/'
+
+EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
+EMAIL_HOST = 'smtp.gmail.com'
+EMAIL_PORT = 587
+EMAIL_USE_TLS = True
+EMAIL_HOST_USER = os.environ.get('USER_EMAIL')
+EMAIL_HOST_PASSWORD = os.environ.get('USER_PASSWORD')
+
+CRISPY_TEMPLATE_PACK = 'bootstrap4'
+
+LOGIN_REDIRECT_URL = '/'
+
+LOGOUT_REDIRECT_URL = '/'
diff --git a/SMS/urls.py b/SMS/urls.py
new file mode 100644
index 0000000..563f383
--- /dev/null
+++ b/SMS/urls.py
@@ -0,0 +1,27 @@
+from django.contrib import admin
+from django.conf.urls import url
+from django.urls import path, include
+from django.conf.urls import handler404, handler500, handler400
+from django.conf import settings
+from django.conf.urls.static import static
+
+
+urlpatterns = [
+ path('accounts/', include('django.contrib.auth.urls')),
+ url(r'^', include('app.urls')),
+ url(r'^accounts/', include('accounts.urls')),
+ url(r'^programs/', include('course.urls')),
+ url(r'^result/', include('result.urls')),
+ url(r'^search/', include('search.urls')),
+ url(r'^quiz/', include('quiz.urls')),
+
+ url(r'^admin/', admin.site.urls),
+]
+
+if settings.DEBUG:
+ urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
+ urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+
+# handler404 = 'app.views.handler404'
+# handler500 = 'app.views.handler500'
+# handler400 = 'app.views.handler400'
diff --git a/SMS/wsgi.py b/SMS/wsgi.py
new file mode 100644
index 0000000..7160374
--- /dev/null
+++ b/SMS/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for SMS project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'SMS.settings')
+
+application = get_wsgi_application()
diff --git a/accounts/__init__.py b/accounts/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/accounts/admin.py b/accounts/admin.py
new file mode 100644
index 0000000..0a51a87
--- /dev/null
+++ b/accounts/admin.py
@@ -0,0 +1,28 @@
+from django.contrib import admin
+from django.contrib.auth.models import Group
+
+from .models import User, Student, Parent
+
+
+class UserAdmin(admin.ModelAdmin):
+ list_display = ['get_full_name', 'username', 'email', 'is_active', 'is_student', 'is_lecturer', 'is_parent', 'is_staff']
+ search_fields = ['username', 'first_name', 'last_name', 'email', 'is_active', 'is_lecturer', 'is_parent', 'is_staff']
+
+ class Meta:
+ managed = True
+ verbose_name = 'User'
+ verbose_name_plural = 'Users'
+
+
+# class ScoreAdmin(admin.ModelAdmin):
+# list_display = [
+# 'student', 'course', 'assignment', 'mid_exam', 'quiz',
+# 'attendance', 'final_exam', 'total', 'grade', 'comment'
+# ]
+
+
+admin.site.register(User, UserAdmin)
+admin.site.register(Student)
+admin.site.register(Parent)
+
+# admin.site.unregister(Group)
diff --git a/accounts/apps.py b/accounts/apps.py
new file mode 100644
index 0000000..9b3fc5a
--- /dev/null
+++ b/accounts/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class AccountsConfig(AppConfig):
+ name = 'accounts'
diff --git a/accounts/decorators.py b/accounts/decorators.py
new file mode 100644
index 0000000..5672166
--- /dev/null
+++ b/accounts/decorators.py
@@ -0,0 +1,48 @@
+from django.contrib.auth import REDIRECT_FIELD_NAME
+from django.http import Http404
+from django.contrib.auth.decorators import user_passes_test
+
+
+def student_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=Http404):
+ """
+ Decorator for views that checks that the logged in user is a student,
+ redirects to the log-in page if necessary.
+ """
+ actual_decorator = user_passes_test(
+ lambda u: u.is_active and u.is_student or u.is_superuser,
+ login_url=login_url,
+ redirect_field_name=redirect_field_name
+ )
+ if function:
+ return actual_decorator(function)
+ return actual_decorator
+
+
+def lecturer_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=Http404):
+ """
+ Decorator for views that checks that the logged in user is a teacher,
+ redirects to the log-in page if necessary.
+ """
+ actual_decorator = user_passes_test(
+ lambda u: u.is_active and u.is_lecturer or u.is_superuser,
+ login_url=login_url,
+ redirect_field_name=redirect_field_name
+ )
+ if function:
+ return actual_decorator(function)
+ return actual_decorator
+
+
+def admin_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=Http404):
+ """
+ Decorator for views that checks that the logged in user is a teacher,
+ redirects to the log-in page if necessary.
+ """
+ actual_decorator = user_passes_test(
+ lambda u: u.is_active and u.is_superuser,
+ login_url=login_url,
+ redirect_field_name=redirect_field_name
+ )
+ if function:
+ return actual_decorator(function)
+ return actual_decorator
diff --git a/accounts/forms.py b/accounts/forms.py
new file mode 100644
index 0000000..218a717
--- /dev/null
+++ b/accounts/forms.py
@@ -0,0 +1,322 @@
+from django import forms
+from django.db import transaction
+from django.contrib.auth.forms import UserCreationForm, UserChangeForm, PasswordChangeForm
+# from django.contrib.auth.models import User
+from django.contrib.auth.forms import PasswordResetForm
+
+from course.models import Program
+# from .models import User, Student, LEVEL
+from .models import *
+
+
+class StaffAddForm(UserCreationForm):
+ username = forms.CharField(
+ max_length=30, widget=forms.TextInput(attrs={'type': 'text', 'class': 'form-control', }),
+ label="Username", )
+
+ first_name = forms.CharField(
+ max_length=30, widget=forms.TextInput(attrs={'type': 'text', 'class': 'form-control', }),
+ label="First Name", )
+
+ last_name = forms.CharField(
+ max_length=30, widget=forms.TextInput(attrs={'type': 'text', 'class': 'form-control', }),
+ label="Last Name", )
+
+ address = forms.CharField(
+ max_length=30, widget=forms.TextInput(attrs={'type': 'text', 'class': 'form-control', }),
+ label="Address", )
+
+ phone = forms.CharField(
+ max_length=30, widget=forms.TextInput(attrs={'type': 'text', 'class': 'form-control', }),
+ label="Mobile No.", )
+
+ email = forms.CharField(
+ max_length=30, widget=forms.TextInput(attrs={'type': 'text', 'class': 'form-control', }),
+ label="Email", )
+
+ password1 = forms.CharField(
+ max_length=30, widget=forms.TextInput(attrs={'type': 'password', 'class': 'form-control', }),
+ label="Password", )
+
+ password2 = forms.CharField(
+ max_length=30, widget=forms.TextInput(attrs={'type': 'password', 'class': 'form-control', }),
+ label="Password Confirmation", )
+
+ class Meta(UserCreationForm.Meta):
+ model = User
+
+ @transaction.atomic()
+ def save(self, commit=True):
+ user = super().save(commit=False)
+ user.is_lecturer = True
+ user.first_name = self.cleaned_data.get('first_name')
+ user.last_name = self.cleaned_data.get('last_name')
+ user.phone = self.cleaned_data.get('phone')
+ user.address = self.cleaned_data.get('address')
+ user.email = self.cleaned_data.get('email')
+ if commit:
+ user.save()
+ return user
+
+
+class StudentAddForm(UserCreationForm):
+ username = forms.CharField(
+ max_length=30,
+ widget=forms.TextInput(
+ attrs={
+ 'type': 'text',
+ 'class': 'form-control',
+ }
+ ),
+ label="Username",
+ )
+ address = forms.CharField(
+ max_length=30,
+ widget=forms.TextInput(
+ attrs={
+ 'type': 'text',
+ 'class': 'form-control',
+ }
+ ),
+ label="Address",
+ )
+
+ phone = forms.CharField(
+ max_length=30,
+ widget=forms.TextInput(
+ attrs={
+ 'type': 'text',
+ 'class': 'form-control',
+ }
+ ),
+ label="Mobile No.",
+ )
+
+ first_name = forms.CharField(
+ max_length=30,
+ widget=forms.TextInput(
+ attrs={
+ 'type': 'text',
+ 'class': 'form-control',
+ }
+ ),
+ label="First name",
+ )
+
+ last_name = forms.CharField(
+ max_length=30,
+ widget=forms.TextInput(
+ attrs={
+ 'type': 'text',
+ 'class': 'form-control',
+ }
+ ),
+ label="Last name",
+ )
+
+ level = forms.CharField(
+ widget=forms.Select(
+ choices=LEVEL,
+ attrs={
+ 'class': 'browser-default custom-select form-control',
+ }
+ ),
+ )
+
+ department = forms.ModelChoiceField(
+ queryset=Program.objects.all(),
+ widget=forms.Select(attrs={'class': 'browser-default custom-select form-control'}),
+ label="Department",
+ )
+
+ email = forms.EmailField(
+ widget=forms.TextInput(
+ attrs={
+ 'type': 'email',
+ 'class': 'form-control',
+ }
+ ),
+ label="Email Address",
+ )
+
+ password1 = forms.CharField(
+ max_length=30, widget=forms.TextInput(attrs={'type': 'password', 'class': 'form-control', }),
+ label="Password", )
+
+ password2 = forms.CharField(
+ max_length=30, widget=forms.TextInput(attrs={'type': 'password', 'class': 'form-control', }),
+ label="Password Confirmation", )
+
+ # def validate_email(self):
+ # email = self.cleaned_data['email']
+ # if User.objects.filter(email__iexact=email, is_active=True).exists():
+ # raise forms.ValidationError("Email has taken, try another email address. ")
+
+ class Meta(UserCreationForm.Meta):
+ model = User
+
+ @transaction.atomic()
+ def save(self):
+ user = super().save(commit=False)
+ user.is_student = True
+ user.first_name = self.cleaned_data.get('first_name')
+ user.last_name = self.cleaned_data.get('last_name')
+ user.address = self.cleaned_data.get('address')
+ user.phone = self.cleaned_data.get('phone')
+ user.email = self.cleaned_data.get('email')
+ user.save()
+ student = Student.objects.create(
+ student=user,
+ level=self.cleaned_data.get('level'),
+ department=self.cleaned_data.get('department')
+ )
+ student.save()
+ return user
+
+
+class ProfileUpdateForm(UserChangeForm):
+ email = forms.EmailField(
+ widget=forms.TextInput(attrs={'type': 'email', 'class': 'form-control', }),
+ label="Email Address", )
+
+ first_name = forms.CharField(
+ widget=forms.TextInput(attrs={'type': 'text', 'class': 'form-control', }),
+ label="First Name", )
+
+ last_name = forms.CharField(
+ widget=forms.TextInput(attrs={'type': 'text', 'class': 'form-control', }),
+ label="Last Name", )
+
+ phone = forms.CharField(
+ widget=forms.TextInput(attrs={'type': 'text', 'class': 'form-control', }),
+ label="Phone No.", )
+
+ address = forms.CharField(
+ widget=forms.TextInput(attrs={'type': 'text', 'class': 'form-control', }),
+ label="Address / city", )
+
+ class Meta:
+ model = User
+ fields = ['email', 'phone', 'address', 'picture', 'first_name', 'last_name']
+
+
+class EmailValidationOnForgotPassword(PasswordResetForm):
+ def clean_email(self):
+ email = self.cleaned_data['email']
+ if not User.objects.filter(email__iexact=email, is_active=True).exists():
+ msg = "There is no user registered with the specified E-mail address. "
+ self.add_error('email', msg)
+ return email
+
+
+class ParentAddForm(UserCreationForm):
+ username = forms.CharField(
+ max_length=30,
+ widget=forms.TextInput(
+ attrs={
+ 'type': 'text',
+ 'class': 'form-control',
+ }
+ ),
+ label="Username",
+ )
+ address = forms.CharField(
+ max_length=30,
+ widget=forms.TextInput(
+ attrs={
+ 'type': 'text',
+ 'class': 'form-control',
+ }
+ ),
+ label="Address",
+ )
+
+ phone = forms.CharField(
+ max_length=30,
+ widget=forms.TextInput(
+ attrs={
+ 'type': 'text',
+ 'class': 'form-control',
+ }
+ ),
+ label="Mobile No.",
+ )
+
+ first_name = forms.CharField(
+ max_length=30,
+ widget=forms.TextInput(
+ attrs={
+ 'type': 'text',
+ 'class': 'form-control',
+ }
+ ),
+ label="First name",
+ )
+
+ last_name = forms.CharField(
+ max_length=30,
+ widget=forms.TextInput(
+ attrs={
+ 'type': 'text',
+ 'class': 'form-control',
+ }
+ ),
+ label="Last name",
+ )
+
+ email = forms.EmailField(
+ widget=forms.TextInput(
+ attrs={
+ 'type': 'email',
+ 'class': 'form-control',
+ }
+ ),
+ label="Email Address",
+ )
+
+ student = forms.ModelChoiceField(
+ queryset=Student.objects.all(),
+ widget=forms.Select(attrs={'class': 'browser-default custom-select form-control'}),
+ label="Student",
+ )
+
+ relation_ship = forms.CharField(
+ widget=forms.Select(
+ choices=RELATION_SHIP,
+ attrs={'class': 'browser-default custom-select form-control',}
+ ),
+ )
+
+ password1 = forms.CharField(
+ max_length=30, widget=forms.TextInput(attrs={'type': 'password', 'class': 'form-control', }),
+ label="Password", )
+
+ password2 = forms.CharField(
+ max_length=30, widget=forms.TextInput(attrs={'type': 'password', 'class': 'form-control', }),
+ label="Password Confirmation", )
+
+ # def validate_email(self):
+ # email = self.cleaned_data['email']
+ # if User.objects.filter(email__iexact=email, is_active=True).exists():
+ # raise forms.ValidationError("Email has taken, try another email address. ")
+
+ class Meta(UserCreationForm.Meta):
+ model = User
+
+ @transaction.atomic()
+ def save(self):
+ user = super().save(commit=False)
+ user.is_parent = True
+ user.first_name = self.cleaned_data.get('first_name')
+ user.last_name = self.cleaned_data.get('last_name')
+ user.address = self.cleaned_data.get('address')
+ user.phone = self.cleaned_data.get('phone')
+ user.email = self.cleaned_data.get('email')
+ user.save()
+ parent = Parent.objects.create(
+ user=user,
+ student=self.cleaned_data.get('student'),
+ relation_ship=self.cleaned_data.get('relation_ship')
+ )
+ parent.save()
+ return user
diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py
new file mode 100644
index 0000000..2d6f928
--- /dev/null
+++ b/accounts/migrations/0001_initial.py
@@ -0,0 +1,53 @@
+# Generated by Django 2.2.3 on 2020-07-29 15:25
+
+import django.contrib.auth.models
+import django.contrib.auth.validators
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='User',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('password', models.CharField(max_length=128, verbose_name='password')),
+ ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
+ ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
+ ('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')),
+ ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
+ ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
+ ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
+ ('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')),
+ ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
+ ('is_student', models.BooleanField(default=False)),
+ ('is_lecturer', models.BooleanField(default=False)),
+ ('phone', models.CharField(blank=True, max_length=60, null=True)),
+ ('address', models.CharField(blank=True, max_length=60, null=True)),
+ ('picture', models.ImageField(default='default.png', null=True, upload_to='profile_pictures')),
+ ('email', models.EmailField(blank=True, max_length=254, null=True)),
+ ],
+ options={
+ 'verbose_name': 'user',
+ 'verbose_name_plural': 'users',
+ 'abstract': False,
+ },
+ managers=[
+ ('objects', django.contrib.auth.models.UserManager()),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Student',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('level', models.CharField(choices=[('Level course', 'Level course'), ('Bachloar', 'Bachloar'), ('Master', 'Master')], max_length=25, null=True)),
+ ],
+ ),
+ ]
diff --git a/accounts/migrations/0002_auto_20200729_1825.py b/accounts/migrations/0002_auto_20200729_1825.py
new file mode 100644
index 0000000..1b3fed5
--- /dev/null
+++ b/accounts/migrations/0002_auto_20200729_1825.py
@@ -0,0 +1,39 @@
+# Generated by Django 2.2.3 on 2020-07-29 15:25
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('accounts', '0001_initial'),
+ ('auth', '0011_update_proxy_permissions'),
+ ('course', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='student',
+ name='department',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='course.Program'),
+ ),
+ migrations.AddField(
+ model_name='student',
+ name='student',
+ field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='groups',
+ 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'),
+ ),
+ migrations.AddField(
+ model_name='user',
+ name='user_permissions',
+ 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'),
+ ),
+ ]
diff --git a/accounts/migrations/0003_auto_20200730_0740.py b/accounts/migrations/0003_auto_20200730_0740.py
new file mode 100644
index 0000000..f0eaaea
--- /dev/null
+++ b/accounts/migrations/0003_auto_20200730_0740.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.3 on 2020-07-30 04:40
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0002_auto_20200729_1825'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='student',
+ name='level',
+ field=models.CharField(choices=[('Bachloar', 'Bachloar Degree'), ('Master', 'Master Degree')], max_length=25, null=True),
+ ),
+ ]
diff --git a/accounts/migrations/0004_auto_20200822_2238.py b/accounts/migrations/0004_auto_20200822_2238.py
new file mode 100644
index 0000000..bfbf2d9
--- /dev/null
+++ b/accounts/migrations/0004_auto_20200822_2238.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.3 on 2020-08-22 19:38
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0003_auto_20200730_0740'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='user',
+ name='picture',
+ field=models.ImageField(default='default.png', null=True, upload_to='profile_pictures/%y/%m/%d/'),
+ ),
+ ]
diff --git a/accounts/migrations/0005_auto_20200822_2246.py b/accounts/migrations/0005_auto_20200822_2246.py
new file mode 100644
index 0000000..9e7546e
--- /dev/null
+++ b/accounts/migrations/0005_auto_20200822_2246.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.3 on 2020-08-22 19:46
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0004_auto_20200822_2238'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='user',
+ name='picture',
+ field=models.ImageField(blank=True, null=True, upload_to='profile_pictures/%y/%m/%d/'),
+ ),
+ ]
diff --git a/accounts/migrations/0006_auto_20200822_2308.py b/accounts/migrations/0006_auto_20200822_2308.py
new file mode 100644
index 0000000..c161d39
--- /dev/null
+++ b/accounts/migrations/0006_auto_20200822_2308.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.3 on 2020-08-22 20:08
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0005_auto_20200822_2246'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='user',
+ name='picture',
+ field=models.ImageField(default='default.png', null=True, upload_to='profile_pictures/%y/%m/%d/'),
+ ),
+ ]
diff --git a/accounts/migrations/0007_auto_20200825_1248.py b/accounts/migrations/0007_auto_20200825_1248.py
new file mode 100644
index 0000000..f4abdfd
--- /dev/null
+++ b/accounts/migrations/0007_auto_20200825_1248.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1 on 2020-08-25 09:48
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0006_auto_20200822_2308'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='user',
+ name='first_name',
+ field=models.CharField(blank=True, max_length=150, verbose_name='first name'),
+ ),
+ ]
diff --git a/accounts/migrations/0008_auto_20200831_1315.py b/accounts/migrations/0008_auto_20200831_1315.py
new file mode 100644
index 0000000..724c835
--- /dev/null
+++ b/accounts/migrations/0008_auto_20200831_1315.py
@@ -0,0 +1,38 @@
+# Generated by Django 2.2 on 2020-08-31 10:15
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0007_auto_20200825_1248'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='user',
+ name='is_parent',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='first_name',
+ field=models.CharField(blank=True, max_length=30, verbose_name='first name'),
+ ),
+ migrations.CreateModel(
+ name='Family',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('first_name', models.CharField(max_length=120)),
+ ('last_name', models.CharField(max_length=120)),
+ ('phone', models.CharField(blank=True, max_length=60, null=True)),
+ ('address', models.CharField(blank=True, max_length=60, null=True)),
+ ('email', models.EmailField(blank=True, max_length=254, null=True)),
+ ('relation_ship', models.TextField(blank=True, choices=[('Father', 'Father'), ('Mother', 'Mother'), ('Brother', 'Brother'), ('Sister', 'Sister'), ('Grand mother', 'Grand mother'), ('Grand father', 'Grand father'), ('Other', 'Other')])),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ ]
diff --git a/accounts/migrations/0009_auto_20200906_1403.py b/accounts/migrations/0009_auto_20200906_1403.py
new file mode 100644
index 0000000..0d07b7e
--- /dev/null
+++ b/accounts/migrations/0009_auto_20200906_1403.py
@@ -0,0 +1,27 @@
+# Generated by Django 2.2 on 2020-09-06 11:03
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0008_auto_20200831_1315'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Parent',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('relation_ship', models.TextField(blank=True, choices=[('Father', 'Father'), ('Mother', 'Mother'), ('Brother', 'Brother'), ('Sister', 'Sister'), ('Grand mother', 'Grand mother'), ('Grand father', 'Grand father'), ('Other', 'Other')])),
+ ('student', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.Student')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.DeleteModel(
+ name='Family',
+ ),
+ ]
diff --git a/accounts/migrations/__init__.py b/accounts/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/accounts/models.py b/accounts/models.py
new file mode 100644
index 0000000..882b1a5
--- /dev/null
+++ b/accounts/models.py
@@ -0,0 +1,161 @@
+from django.db import models
+from django.urls import reverse
+from django.contrib.auth.models import AbstractUser
+from django.conf import settings
+
+from django.db.models import Q
+from PIL import Image
+
+from course.models import Program
+from .validators import ASCIIUsernameValidator
+
+User = settings.AUTH_USER_MODEL
+
+# LEVEL_COURSE = "Level course"
+BACHLOAR_DEGREE = "Bachloar"
+MASTER_DEGREE = "Master"
+
+LEVEL = (
+ # (LEVEL_COURSE, "Level course"),
+ (BACHLOAR_DEGREE, "Bachloar Degree"),
+ (MASTER_DEGREE, "Master Degree"),
+)
+
+FATHER = "Father"
+MOTHER = "Mother"
+BROTHER = "Brother"
+SISTER = "Sister"
+GRAND_MOTHER = "Grand mother"
+GRAND_FATHER = "Grand father"
+OTHER = "Other"
+
+RELATION_SHIP = (
+ (FATHER, "Father"),
+ (MOTHER, "Mother"),
+ (BROTHER, "Brother"),
+ (SISTER, "Sister"),
+ (GRAND_MOTHER, "Grand mother"),
+ (GRAND_FATHER, "Grand father"),
+ (OTHER, "Other"),
+)
+
+# class UserManager(models.Manager):
+# def search(self, query=None):
+# qs = self.get_queryset()
+# if query is not None:
+# or_lookup = (Q(username__icontains=query) |
+# Q(first_name__icontains=query)|
+# Q(last_name__icontains=query)|
+# Q(email__icontains=query)
+# )
+# qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
+# return qs
+
+
+class User(AbstractUser):
+ is_student = models.BooleanField(default=False)
+ is_lecturer = models.BooleanField(default=False)
+ is_parent = models.BooleanField(default=False)
+ phone = models.CharField(max_length=60, blank=True, null=True)
+ address = models.CharField(max_length=60, blank=True, null=True)
+ picture = models.ImageField(upload_to='profile_pictures/%y/%m/%d/', default='default.png', null=True)
+ email = models.EmailField(blank=True, null=True)
+
+ username_validator = ASCIIUsernameValidator()
+
+ # objects = UserManager()
+
+ @property
+ def get_full_name(self):
+ full_name = self.username
+ if self.first_name and self.last_name:
+ full_name = self.first_name + " " + self.last_name
+ return full_name
+
+ def __str__(self):
+ return '{} ({})'.format(self.username, self.get_full_name)
+
+ @property
+ def get_user_role(self):
+ if self.is_superuser:
+ return "Admin"
+ elif self.is_student:
+ return "Student"
+ elif self.is_lecturer:
+ return "Lecturer"
+ elif self.is_parent:
+ return "Parent"
+
+ def get_picture(self):
+ no_picture = settings.MEDIA_URL + 'default.png'
+ try:
+ return self.picture.url
+ except:
+ return no_picture
+
+ def get_absolute_url(self):
+ return reverse('profile_single', kwargs={'id': self.id})
+
+ def save(self, *args, **kwargs):
+ super().save(*args, **kwargs)
+ try:
+ img = Image.open(self.picture.path)
+ if img.height > 300 or img.width > 300:
+ output_size = (300, 300)
+ img.thumbnail(output_size)
+ img.save(self.picture.path)
+ except:
+ pass
+
+ def delete(self, *args, **kwargs):
+ if self.picture.url != settings.MEDIA_URL + 'default.png':
+ self.picture.delete()
+ super().delete(*args, **kwargs)
+
+
+class StudentManager(models.Manager):
+ def search(self, query=None):
+ qs = self.get_queryset()
+ if query is not None:
+ or_lookup = (Q(level__icontains=query) |
+ Q(department__icontains=query)
+ )
+ qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
+ return qs
+
+
+class Student(models.Model):
+ student = models.OneToOneField(User, on_delete=models.CASCADE)
+ # id_number = models.CharField(max_length=20, unique=True, blank=True)
+ level = models.CharField(max_length=25, choices=LEVEL, null=True)
+ department = models.ForeignKey(Program, on_delete=models.CASCADE, null=True)
+
+ objects = StudentManager()
+
+ def __str__(self):
+ return self.student.get_full_name
+
+ def get_absolute_url(self):
+ return reverse('profile_single', kwargs={'id': self.id})
+
+ def delete(self, *args, **kwargs):
+ self.student.delete()
+ super().delete(*args, **kwargs)
+
+
+# class Parent(models.Model):
+# parent = models.
+
+
+class Parent(models.Model):
+ user = models.OneToOneField(User, on_delete=models.CASCADE)
+ student = models.OneToOneField(Student, null=True, on_delete=models.SET_NULL)
+ # first_name = models.CharField(max_length=120)
+ # last_name = models.CharField(max_length=120)
+ # phone = models.CharField(max_length=60, blank=True, null=True)
+ # address = models.CharField(max_length=60, blank=True, null=True)
+ # email = models.EmailField(blank=True, null=True)
+ relation_ship = models.TextField(choices=RELATION_SHIP, blank=True)
+
+ def __str__(self):
+ return self.user.username
diff --git a/accounts/templates/accounts/add_staff.html b/accounts/templates/accounts/add_staff.html
new file mode 100644
index 0000000..340be15
--- /dev/null
+++ b/accounts/templates/accounts/add_staff.html
@@ -0,0 +1,87 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+{% load crispy_forms_tags %}
+{% load static %}
+
+{% block content %}
+
+
+ Lecturer Add Form
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
+{% endblock content %}
diff --git a/accounts/templates/accounts/add_student.html b/accounts/templates/accounts/add_student.html
new file mode 100644
index 0000000..d3721f6
--- /dev/null
+++ b/accounts/templates/accounts/add_student.html
@@ -0,0 +1,110 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+{% load crispy_forms_tags %}
+{% load static %}
+
+{% block content %}
+
+
+ Student Add Form
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
+{% endblock content %}
diff --git a/accounts/templates/accounts/edit_lecturer.html b/accounts/templates/accounts/edit_lecturer.html
new file mode 100644
index 0000000..c4947c1
--- /dev/null
+++ b/accounts/templates/accounts/edit_lecturer.html
@@ -0,0 +1,72 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+{% load crispy_forms_tags %}
+{% load static %}
+
+{% block content %}
+
+
+ Lecturer Update Form
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+{% endblock content %}
diff --git a/accounts/templates/accounts/edit_student.html b/accounts/templates/accounts/edit_student.html
new file mode 100644
index 0000000..f4f54ad
--- /dev/null
+++ b/accounts/templates/accounts/edit_student.html
@@ -0,0 +1,73 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+{% load crispy_forms_tags %}
+{% load static %}
+
+{% block content %}
+
+
+ Student Update Form
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+{% endblock content %}
diff --git a/accounts/templates/accounts/lecturer_list.html b/accounts/templates/accounts/lecturer_list.html
new file mode 100644
index 0000000..1467e5c
--- /dev/null
+++ b/accounts/templates/accounts/lecturer_list.html
@@ -0,0 +1,119 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+
+{% block content %}
+
+
+
+{% if request.user.is_superuser %}
+ Add Lecturer
+{% endif %}
+
+ Lecturers List
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
+
+
+
+
+
+
+ #
+ ID No.
+ Full Name
+ Email
+ Mob No.
+ Address/city
+ Last login
+ {% if request.user.is_superuser %}
+ Action
+ {% endif %}
+
+
+
+ {% for lecturer in object_list %}
+
+ {{ forloop.counter }}.
+ {{ lecturer.username }}
+ {{ lecturer.get_full_name }}
+ {{ lecturer.email }}
+ {{ lecturer.phone }}
+ {{ lecturer.address }}
+ {{ lecturer.last_login }}
+ {% if request.user.is_superuser %}
+
+
+
+ {% endif %}
+
+ {% empty %}
+
+
+
+
+
+ No Lecturer(s).
+ {% if request.user.is_superuser %}
+
+
+ Add Lecturer Now.
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+ {% endfor %}
+
+
+
+{% endblock content %}
+
+{% block js %}
+
+{% endblock %}
diff --git a/accounts/templates/accounts/parent_form.html b/accounts/templates/accounts/parent_form.html
new file mode 100644
index 0000000..115afcf
--- /dev/null
+++ b/accounts/templates/accounts/parent_form.html
@@ -0,0 +1,13 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+{% load crispy_forms_tags %}
+{% load static %}
+
+{% block content %}
+
+
+
+{% endblock %}
diff --git a/accounts/templates/accounts/profile.html b/accounts/templates/accounts/profile.html
new file mode 100644
index 0000000..53ab409
--- /dev/null
+++ b/accounts/templates/accounts/profile.html
@@ -0,0 +1,129 @@
+{% extends 'base.html' %}
+{% block title %} {{ title }} | DjangoSMS{% endblock title %}
+
+{% load static %}
+{% load i18n %}
+
+{% block content %}
+
+{% if user.is_authenticated %}
+
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
+
+
+
+
+
+
+
Last login: {{ user.last_login|date }}
+
Full Name: {{ user.get_full_name|title }}
+
Role:
+ {{ user.get_user_role }}
+
+
+
+
+
+
+
+
+
+ {% if user.is_lecturer %}
+
My Courses
+
+ {% if courses %}
+
+ {% for course in courses %}
+
+ {% endfor %}
+
+ {% else %}
+
No courses!
+ {% endif %}
+
+ {% endif %}
+
+
Personal Info
+
+
First Name: {{ user.first_name|title }}
+
Last Name: {{ user.last_name|title }}
+
ID No.: {{ user.username }}
+
+ {% if user.is_student %}
+
Applicant Info
+
+
School: Hawas Preparatory School
+
Level: {{ level.level }}
+
+ {% endif %}
+
+
Contact Info
+
+
Email: {{ user.email }}
+
Tel No.: {{ user.phone }}
+
Address/city: {{ user.address }}
+
+
+
Important Dates
+
+
Last login: {{ user.last_login }}
+
Academic Year: {{ current_semester }} Semester {{ current_session }}
+
Registered Date: {{ user.date_joined|date }}
+
+
+
+
+
+
+ {% if user.is_superuser %}
+
+ Message - You can manage everything in the system but your normal job is only the list in the admin panel , please do not do anything that is not listed in the admin panel unless it is very important.
+
+
+
+ {% endif %}
+
+
+
+
+
+{% endif %}
+
+{% endblock content %}
diff --git a/accounts/templates/accounts/profile_single.html b/accounts/templates/accounts/profile_single.html
new file mode 100644
index 0000000..d25cc82
--- /dev/null
+++ b/accounts/templates/accounts/profile_single.html
@@ -0,0 +1,127 @@
+{% extends 'base.html' %}
+{% block title %} {{ title }} | DjangoSMS{% endblock title %}
+{% load static %}
+{% load i18n %}
+{% block content %}
+
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+{% if request.user.is_superuser %}
+
+{% endif %}
+
+
+
+
+
+
+
+
Last login: {{ user.last_login|date }}
+
Full Name: {{ user.get_full_name|title }}
+
Role: {{ user_type }}
+
+
+
+
+
+
+
+
+ {% if user.is_lecturer %}
+
My Courses
+
+ {% if courses %}
+
+ {% for course in courses %}
+
+ {% endfor %}
+
+ {% else %}
+
No courses!
+ {% endif %}
+
+ {% endif %}
+
+
Personal Info
+
+
First Name: {{ user.first_name|title }}
+
Last Name: {{ user.last_name|title }}
+
ID No.: {{ user.username }}
+
+ {% if user.is_student %}
+
Applicant Info
+
+
School: {{ student.get_student_school }}
+
Department: {{ student.department }}
+
Level: {{ student.level }}
+
+ {% endif %}
+
+
Contact Info
+
+
Email: {{ user.email }}
+
Tel No.: {{ user.phone }}
+
Address/city: {{ user.address }}
+
+
+
Important Dates
+
+
Last login: {{ user.last_login }}
+
Academic Year: {{ current_semester }} Semester {{ current_session }}
+
Registered Date: {{ user.date_joined|date }}
+
+
+
+
+
+
+ {% if user.is_superuser %}
+
+ You can control everything in the system but your normal work is only in the admin panel, please do not do anything that is not listed in the admin panel unless it is very important.
+
+ {% endif %}
+ {% if user.is_student %}
+
+
+
+
+
+
+ {% endif %}
+
+
+
+
+{% endblock content %}
diff --git a/accounts/templates/accounts/student_list.html b/accounts/templates/accounts/student_list.html
new file mode 100644
index 0000000..e675baa
--- /dev/null
+++ b/accounts/templates/accounts/student_list.html
@@ -0,0 +1,110 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+
+{% block content %}
+
+
+
+{% if request.user.is_superuser %}
+ Add Student
+{% endif %}
+
+ Students List
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
+
+
+
+
+
+
+
+ #
+ ID No.
+ Full Name
+ Email
+ Department
+ {% if request.user.is_superuser %}
+ Action
+ {% endif %}
+
+
+
+ {% for student in object_list %}
+
+ {{ forloop.counter }}.
+ {{ student.student.username }}
+ {{ student.student.get_full_name }}
+ {{ student.student.email }}
+ {{ student.department }}
+
+ {% if request.user.is_superuser %}
+
+
+
+ {% endif %}
+
+ {% empty %}
+
+
+
+
+
+ No Student.
+ {% if request.user.is_superuser %}
+
+
+ Add Student Now.
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+ {% endfor %}
+
+
+
+{% endblock content %}
diff --git a/accounts/templates/registration/login.html b/accounts/templates/registration/login.html
new file mode 100644
index 0000000..ac67259
--- /dev/null
+++ b/accounts/templates/registration/login.html
@@ -0,0 +1,86 @@
+{% extends 'registration/registration_base.html' %}
+{% block title %}DjangoSMS - Login{% endblock title %}
+{% load crispy_forms_tags %}
+{% block content %}
+
+
+
+{% endblock content %}
diff --git a/accounts/templates/registration/password_reset.html b/accounts/templates/registration/password_reset.html
new file mode 100644
index 0000000..eb7cff7
--- /dev/null
+++ b/accounts/templates/registration/password_reset.html
@@ -0,0 +1,19 @@
+{% extends 'registration/registration_base.html' %}
+{% block title %}Password Reset | DjangoSMS{% endblock title %}
+{% load crispy_forms_tags %}
+{% block content %}
+
+{% endblock content %}
diff --git a/accounts/templates/registration/password_reset_complete.html b/accounts/templates/registration/password_reset_complete.html
new file mode 100644
index 0000000..fa84049
--- /dev/null
+++ b/accounts/templates/registration/password_reset_complete.html
@@ -0,0 +1,15 @@
+{% extends 'registration/registration_base.html' %}
+{% block title %}Password Reset Complete | DjangoSMS{% endblock title %}
+{% load crispy_forms_tags %}
+
+{% block content %}
+
+
Password Reset Complete
+
+
+ Your password has been set, you are now able to Log In!
+
+
Sign In Here
+
+
+{% endblock content %}
diff --git a/accounts/templates/registration/password_reset_confirm.html b/accounts/templates/registration/password_reset_confirm.html
new file mode 100644
index 0000000..081aa48
--- /dev/null
+++ b/accounts/templates/registration/password_reset_confirm.html
@@ -0,0 +1,34 @@
+{% extends 'registration/registration_base.html' %}
+{% block title %}New Password | DjangoSMS{% endblock title %}
+{% load crispy_forms_tags %}
+{% block content %}
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
Confirm New Password
+
+
+
+
+{% endblock content %}
diff --git a/accounts/templates/registration/password_reset_done.html b/accounts/templates/registration/password_reset_done.html
new file mode 100644
index 0000000..c1a8e75
--- /dev/null
+++ b/accounts/templates/registration/password_reset_done.html
@@ -0,0 +1,16 @@
+{% extends 'registration/registration_base.html' %}
+{% block title %}Email Sent | DjangoSMS{% endblock title %}
+{% load crispy_forms_tags %}
+
+{% block content %}
+
+
Email sent
+
+
+ An Email has been sent with instructions
+ to reset your password, check your email.
+
+
Back To Login
+
+
+{% endblock content %}
diff --git a/accounts/templates/registration/registration_base.html b/accounts/templates/registration/registration_base.html
new file mode 100644
index 0000000..dcd3844
--- /dev/null
+++ b/accounts/templates/registration/registration_base.html
@@ -0,0 +1,24 @@
+{% load static %}
+
+
+
+
+
+
+ {% block title %}DjangoSMS - Login{% endblock title %}
+
+
+
+
+
+
+
+
+
+
+
+ {% block content %}
+ {% endblock content %}
+
+
+
diff --git a/accounts/templates/setting/admin_panel.html b/accounts/templates/setting/admin_panel.html
new file mode 100644
index 0000000..e48cf75
--- /dev/null
+++ b/accounts/templates/setting/admin_panel.html
@@ -0,0 +1,62 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+{% load crispy_forms_tags %}
+{% load static %}
+
+{% block content %}
+
+
+ Admin Panel
+
+
+List of actions that only the admin can access
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
+
+ Manage
Lecturers »
+
⟶ CRUD (Create, Retriev, Update & Delete) lecturers
+ Manage
Students »
+
⟶ CRUD (Create, Retriev, Update & Delete) students
+ Manage
Session »
+
⟶ CRUD (Create, Retriev, Update & Delete) sessions
+ Manage
Semester »
+
⟶ CRUD (Create, Retriev, Update & Delete) semesters
+
+
+ Course Add & Drop
+
+
+
+
+
+
⟶ Switch
+ ON or OFF
+
+
+ Manage
Programs & Courses »
+
⟶ CRUD (Create, Retriev, Update & Delete) programs
+
⟶ CRUD (Create, Retriev, Update & Delete) courses
+ Manage
Course Allocations »
+
⟶ CRUD (Create, Retriev, Update & Delete) course allocations
+ Manage
News & Events »
+
⟶ CRUD (Create, Retriev, Update & Delete) News & Events
+
+
+
+
+{% endblock %}
diff --git a/accounts/templates/setting/password_change.html b/accounts/templates/setting/password_change.html
new file mode 100644
index 0000000..2a7f2ef
--- /dev/null
+++ b/accounts/templates/setting/password_change.html
@@ -0,0 +1,60 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+{% load crispy_forms_tags %}
+{% load static %}
+
+{% block content %}
+
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
+{% endblock content %}
diff --git a/accounts/templates/setting/profile_info_change.html b/accounts/templates/setting/profile_info_change.html
new file mode 100644
index 0000000..f313c56
--- /dev/null
+++ b/accounts/templates/setting/profile_info_change.html
@@ -0,0 +1,72 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+{% load crispy_forms_tags %}
+{% load static %}
+
+{% block content %}
+
+
+
+ Account Settings
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+{% endblock content %}
diff --git a/accounts/tests.py b/accounts/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/accounts/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/accounts/urls.py b/accounts/urls.py
new file mode 100644
index 0000000..f0dfa09
--- /dev/null
+++ b/accounts/urls.py
@@ -0,0 +1,68 @@
+from django.conf.urls import url
+from django.urls import path
+from django.contrib.auth.views import (
+ PasswordResetView, PasswordResetDoneView, PasswordResetConfirmView,
+ PasswordResetCompleteView, LoginView, LogoutView
+ )
+from .views import (
+ profile, profile_single, admin_panel,
+ profile_update, change_password,
+ LecturerListView, StudentListView,
+ staff_add_view, edit_staff,
+ delete_staff, student_add_view,
+ edit_student, delete_student, ParentAdd
+ )
+from .forms import EmailValidationOnForgotPassword
+
+
+urlpatterns = [
+ url(r'^admin_panel/$', admin_panel, name='admin_panel'),
+
+ url(r'^profile/$', profile, name='profile'),
+ url(r'^profile/(?P\d+)/detail/$', profile_single, name='profile_single'),
+ url(r'^setting/$', profile_update, name='edit_profile'),
+ url(r'^change_password/$', change_password, name='change_password'),
+
+ url(r'^lecturers/$', LecturerListView.as_view(), name='lecturer_list'),
+ url(r'^lecturer/add/$', staff_add_view, name='add_lecturer'),
+ url(r'^staff/(?P\d+)/edit/$', edit_staff, name='staff_edit'),
+ url(r'^lecturers/(?P\d+)/delete/$', delete_staff, name='lecturer_delete'),
+
+ url(r'^students/$', StudentListView.as_view(), name='student_list'),
+ url(r'^student/add/$', student_add_view, name='add_student'),
+ url(r'^student/(?P\d+)/edit/$', edit_student, name='student_edit'),
+ url(r'^students/(?P\d+)/delete/$', delete_student, name='student_delete'),
+
+ url(r'^parents/add/$', ParentAdd.as_view(), name='add_parent'),
+
+ # url(r'^add-student/$', StudentAddView.as_view(), name='add_student'),
+
+ # url(r'^programs/course/delete/(?P\d+)/$', course_delete, name='delete_course'),
+
+ # Setting urls
+ # url(r'^profile/(?P\d+)/edit/$', profileUpdateView, name='edit_profile'),
+ # url(r'^profile/(?P\d+)/change-password/$', changePasswordView, name='change_password'),
+
+ # ################################################################
+ # url(r'^login/$', LoginView.as_view(), name='login'),
+ # url(r'^logout/$', LogoutView.as_view(), name='logout', kwargs={'next_page': '/'}),
+
+ # url(r'^password-reset/$', PasswordResetView.as_view(
+ # form_class=EmailValidationOnForgotPassword,
+ # template_name='registration/password_reset.html'
+ # ),
+ # name='password_reset'),
+ # url(r'^password-reset/done/$', PasswordResetDoneView.as_view(
+ # template_name='registration/password_reset_done.html'
+ # ),
+ # name='password_reset_done'),
+ # url(r'^password-reset-confirm///$', PasswordResetConfirmView.as_view(
+ # template_name='registration/password_reset_confirm.html'
+ # ),
+ # name='password_reset_confirm'),
+ # url(r'^password-reset-complete/$', PasswordResetCompleteView.as_view(
+ # template_name='registration/password_reset_complete.html'
+ # ),
+ # name='password_reset_complete')
+ # ################################################################
+]
diff --git a/accounts/validators.py b/accounts/validators.py
new file mode 100644
index 0000000..248c7c8
--- /dev/null
+++ b/accounts/validators.py
@@ -0,0 +1,15 @@
+import re
+
+from django.core import validators
+from django.utils.deconstruct import deconstructible
+from django.utils.translation import gettext_lazy as _
+
+
+@deconstructible
+class ASCIIUsernameValidator(validators.RegexValidator):
+ regex = r'^[a-zA-Z]+\/(...)\/(....)'
+ message = _(
+ 'Enter a valid username. This value may contain only English letters, '
+ 'numbers, and @/./+/-/_ characters.'
+ )
+ flags = re.ASCII
diff --git a/accounts/views.py b/accounts/views.py
new file mode 100644
index 0000000..a86eb32
--- /dev/null
+++ b/accounts/views.py
@@ -0,0 +1,320 @@
+from django.shortcuts import render, redirect, get_object_or_404
+from django.http import Http404
+from django.contrib import messages
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth import update_session_auth_hash
+from django.views.generic import CreateView, ListView
+from django.core.paginator import Paginator
+from django.db.models import Q
+from django.utils.decorators import method_decorator
+from django.contrib.auth.forms import UserCreationForm, UserChangeForm, PasswordChangeForm
+
+from .decorators import lecturer_required, student_required, admin_required
+from course.models import Course
+from result.models import TakenCourse
+from app.models import Session, Semester
+from .forms import StaffAddForm, StudentAddForm, ProfileUpdateForm, ParentAddForm
+from .models import User, Student, Parent
+
+
+@login_required
+def profile(request):
+ """ Show profile of any user that fire out the request """
+ try:
+ current_session = get_object_or_404(Session, is_current_session=True)
+ current_semester = get_object_or_404(Semester, is_current_semester=True, session=current_session)
+
+ except Semester.MultipleObjectsReturned and Semester.DoesNotExist and Session.DoesNotExist:
+ raise Http404
+
+ if request.user.is_lecturer:
+ courses = Course.objects.filter(allocated_course__lecturer__pk=request.user.id).filter(
+ semester=current_semester)
+ return render(request, 'accounts/profile.html', {
+ 'title': request.user.get_full_name,
+ "courses": courses,
+ 'current_session': current_session,
+ 'current_semester': current_semester,
+ })
+ elif request.user.is_student:
+ level = Student.objects.get(student__pk=request.user.id)
+ try:
+ parent = Parent.objects.get(student=level)
+ except:
+ parent = "no parent set"
+ courses = TakenCourse.objects.filter(student__student__id=request.user.id, course__level=level.level)
+ context = {
+ 'title': request.user.get_full_name,
+ 'parent': parent,
+ 'courses': courses,
+ 'level': level,
+ 'current_session': current_session,
+ 'current_semester': current_semester,
+ }
+ return render(request, 'accounts/profile.html', context)
+ else:
+ staff = User.objects.filter(is_lecturer=True)
+ return render(request, 'accounts/profile.html', {
+ 'title': request.user.get_full_name,
+ "staff": staff,
+ 'current_session': current_session,
+ 'current_semester': current_semester,
+ })
+
+
+@login_required
+@admin_required
+def profile_single(request, id):
+ """ Show profile of any selected user """
+ if request.user.id == id:
+ return redirect("/profile/")
+
+ current_session = get_object_or_404(Session, is_current_session=True)
+ current_semester = get_object_or_404(Semester, is_current_semester=True, session=current_session)
+ user = User.objects.get(pk=id)
+ if user.is_lecturer:
+ courses = Course.objects.filter(allocated_course__lecturer__pk=id).filter(semester=current_semester)
+ context = {
+ 'title': user.get_full_name,
+ "user": user,
+ "user_type": "Lecturer",
+ "courses": courses,
+ 'current_session': current_session,
+ 'current_semester': current_semester,
+ }
+ return render(request, 'accounts/profile_single.html', context)
+ elif user.is_student:
+ student = Student.objects.get(student__pk=id)
+ courses = TakenCourse.objects.filter(student__student__id=id, course__level=student.level)
+ context = {
+ 'title': user.get_full_name,
+ 'user': user,
+ "user_type": "student",
+ 'courses': courses,
+ 'student': student,
+ 'current_session': current_session,
+ 'current_semester': current_semester,
+ }
+ return render(request, 'accounts/profile_single.html', context)
+ else:
+ context = {
+ 'title': user.get_full_name,
+ "user": user,
+ "user_type": "superuser",
+ 'current_session': current_session,
+ 'current_semester': current_semester,
+ }
+ return render(request, 'accounts/profile_single.html', context)
+
+
+@login_required
+@admin_required
+def admin_panel(request):
+ return render(request, 'setting/admin_panel.html', {})
+# ########################################################
+
+
+# ########################################################
+# Setting views
+# ########################################################
+@login_required
+def profile_update(request):
+ if request.method == 'POST':
+ form = ProfileUpdateForm(request.POST, request.FILES, instance=request.user)
+ if form.is_valid():
+ form.save()
+ messages.success(request, 'Your profile has been updated successfully.')
+ return redirect('profile')
+ else:
+ messages.error(request, 'Please correct the error(s) below.')
+ else:
+ form = ProfileUpdateForm(instance=request.user)
+ return render(request, 'setting/profile_info_change.html', {
+ 'title': 'Setting | DjangoSMS',
+ 'form': form,
+ })
+
+
+@login_required
+def change_password(request):
+ if request.method == 'POST':
+ form = PasswordChangeForm(request.user, request.POST)
+ if form.is_valid():
+ user = form.save()
+ update_session_auth_hash(request, user)
+ messages.success(request, 'Your password was successfully updated!')
+ return redirect('profile')
+ else:
+ messages.error(request, 'Please correct the error(s) below. ')
+ else:
+ form = PasswordChangeForm(request.user)
+ return render(request, 'setting/password_change.html', {
+ 'form': form,
+ })
+# ########################################################
+
+@login_required
+@admin_required
+def staff_add_view(request):
+ if request.method == 'POST':
+ form = StaffAddForm(request.POST)
+ first_name = request.POST.get('first_name')
+ last_name = request.POST.get('last_name')
+ if form.is_valid():
+ form.save()
+ messages.success(request, "Account for lecturer " + first_name + ' ' + last_name + " has been created.")
+ return redirect("lecturer_list")
+ else:
+ form = StaffAddForm()
+
+ context = {
+ 'title': 'Lecturer Add | DjangoSMS',
+ 'form': form,
+ }
+
+ return render(request, 'accounts/add_staff.html', context)
+
+
+@login_required
+@admin_required
+def edit_staff(request, pk):
+ instance = get_object_or_404(User, is_lecturer=True, pk=pk)
+ if request.method == 'POST':
+ form = ProfileUpdateForm(request.POST, request.FILES, instance=instance)
+ full_name = instance.get_full_name
+ if form.is_valid():
+ form.save()
+
+ messages.success(request, 'Lecturer ' + full_name + ' has been updated.')
+ return redirect('lecturer_list')
+ else:
+ messages.error(request, 'Please correct the error below.')
+ else:
+ form = ProfileUpdateForm(instance=instance)
+ return render(request, 'accounts/edit_lecturer.html', {
+ 'title': 'Edit Lecturer | DjangoSMS',
+ 'form': form,
+ })
+
+
+@method_decorator([login_required, admin_required], name='dispatch')
+class LecturerListView(ListView):
+ queryset = User.objects.filter(is_lecturer=True)
+ template_name = "accounts/lecturer_list.html"
+ paginate_by = 10 # if pagination is desired
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context['title'] = "Lecturers | DjangoSMS"
+ return context
+
+
+# @login_required
+# @lecturer_required
+# def delete_staff(request, pk):
+# staff = get_object_or_404(User, pk=pk)
+# staff.delete()
+# return redirect('lecturer_list')
+
+@login_required
+@admin_required
+def delete_staff(request, pk):
+ lecturer = get_object_or_404(User, pk=pk)
+ full_name = lecturer.get_full_name
+ lecturer.delete()
+ messages.success(request, 'Lecturer ' + full_name + ' has been deleted.')
+ return redirect('lecturer_list')
+# ########################################################
+
+
+# ########################################################
+# Student views
+# ########################################################
+@login_required
+@admin_required
+def student_add_view(request):
+ if request.method == 'POST':
+ form = StudentAddForm(request.POST)
+ first_name = request.POST.get('first_name')
+ last_name = request.POST.get('last_name')
+ if form.is_valid():
+ form.save()
+ messages.success(request, 'Account for ' + first_name + ' ' + last_name + ' has been created.')
+ return redirect('student_list')
+ else:
+ messages.error(request, 'Correct the error(s) below.')
+ else:
+ form = StudentAddForm()
+
+ return render(request, 'accounts/add_student.html', {
+ 'title': "Add Student | DjangoSMS",
+ 'form': form
+ })
+
+
+@login_required
+@admin_required
+def edit_student(request, pk):
+ # instance = User.objects.get(pk=pk)
+ instance = get_object_or_404(User, is_student=True, pk=pk)
+ if request.method == 'POST':
+ form = ProfileUpdateForm(request.POST, request.FILES, instance=instance)
+ full_name = instance.get_full_name
+ if form.is_valid():
+ form.save()
+
+ messages.success(request, ('Student ' + full_name + ' has been updated.'))
+ return redirect('student_list')
+ else:
+ messages.error(request, 'Please correct the error below.')
+ else:
+ form = ProfileUpdateForm(instance=instance)
+ return render(request, 'accounts/edit_student.html', {
+ 'title': 'Edit-profile | DjangoSMS',
+ 'form': form,
+ })
+
+
+@method_decorator([login_required, admin_required], name='dispatch')
+class StudentListView(ListView):
+ template_name = "accounts/student_list.html"
+ paginate_by = 10 # if pagination is desired
+
+ def get_queryset(self):
+ queryset = Student.objects.all()
+ query = self.request.GET.get('student_id')
+ if query is not None:
+ queryset = queryset.filter(Q(department=query))
+ return queryset
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context['title'] = "Students | DjangoSMS"
+ return context
+
+
+@login_required
+@admin_required
+def delete_student(request, pk):
+ student = get_object_or_404(Student, pk=pk)
+ # full_name = student.user.get_full_name
+ student.delete()
+ messages.success(request, 'Student has been deleted.')
+ return redirect('student_list')
+# ########################################################
+
+
+class ParentAdd(CreateView):
+ model = Parent
+ form_class = ParentAddForm
+ template_name = 'accounts/parent_form.html'
+
+
+# def parent_add(request):
+# if request.method == 'POST':
+# form = ParentAddForm(request.POST)
+# if form.is_valid():
+# form.save()
+# return redirect('student_list')
+# else:
+# form = ParentAddForm(request.POST)
diff --git a/app/__init__.py b/app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/admin.py b/app/admin.py
new file mode 100644
index 0000000..e992034
--- /dev/null
+++ b/app/admin.py
@@ -0,0 +1,9 @@
+from django.contrib import admin
+from django.contrib.auth.models import Group
+
+from .models import Session, Semester, NewsAndEvents
+
+
+admin.site.register(Semester)
+admin.site.register(Session)
+admin.site.register(NewsAndEvents)
diff --git a/app/apps.py b/app/apps.py
new file mode 100644
index 0000000..80b2c8d
--- /dev/null
+++ b/app/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class AppConfig(AppConfig):
+ name = 'app'
diff --git a/app/forms.py b/app/forms.py
new file mode 100644
index 0000000..38fb9a9
--- /dev/null
+++ b/app/forms.py
@@ -0,0 +1,74 @@
+from django import forms
+from django.db import transaction
+
+from .models import NewsAndEvents, Session, Semester, SEMESTER
+
+
+# news and events
+class NewsAndEventsForm(forms.ModelForm):
+ class Meta:
+ model = NewsAndEvents
+ fields = ('title', 'summary', 'posted_as',)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields['title'].widget.attrs.update({'class': 'form-control'})
+ self.fields['summary'].widget.attrs.update({'class': 'form-control'})
+ self.fields['posted_as'].widget.attrs.update({'class': 'form-control'})
+
+
+class SessionForm(forms.ModelForm):
+ next_session_begins = forms.DateTimeField(
+ widget=forms.TextInput(
+ attrs={
+ 'type': 'date',
+ }
+ ),
+ required=True)
+
+ class Meta:
+ model = Session
+ fields = ['session', 'is_current_session', 'next_session_begins']
+
+
+class SemesterForm(forms.ModelForm):
+ semester = forms.CharField(
+ widget=forms.Select(
+ choices=SEMESTER,
+ attrs={
+ 'class': 'browser-default custom-select',
+ }
+ ),
+ label="semester",
+ )
+ is_current_semester = forms.CharField(
+ widget=forms.Select(
+ choices=((True, 'Yes'), (False, 'No')),
+ attrs={
+ 'class': 'browser-default custom-select',
+ }
+ ),
+ label="is current semester ?",
+ )
+ session = forms.ModelChoiceField(
+ queryset=Session.objects.all(),
+ widget=forms.Select(
+ attrs={
+ 'class': 'browser-default custom-select',
+ }
+ ),
+ required=True
+ )
+
+ next_semester_begins = forms.DateTimeField(
+ widget=forms.TextInput(
+ attrs={
+ 'type': 'date',
+ 'class': 'form-control',
+ }
+ ),
+ required=True)
+
+ class Meta:
+ model = Semester
+ fields = ['semester', 'is_current_semester', 'session', 'next_semester_begins']
diff --git a/app/migrations/0001_initial.py b/app/migrations/0001_initial.py
new file mode 100644
index 0000000..49f5410
--- /dev/null
+++ b/app/migrations/0001_initial.py
@@ -0,0 +1,45 @@
+# Generated by Django 2.2.3 on 2020-07-29 15:25
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='NewsAndEvents',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=200, null=True)),
+ ('summary', models.TextField(blank=True, max_length=200, null=True)),
+ ('posted_as', models.CharField(choices=[('News', 'News'), ('Event', 'Event')], max_length=10)),
+ ('updated_date', models.DateTimeField(auto_now=True, null=True)),
+ ('upload_time', models.DateTimeField(auto_now_add=True, null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Session',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('session', models.CharField(max_length=200, unique=True)),
+ ('is_current_session', models.BooleanField(blank=True, default=False, null=True)),
+ ('next_session_begins', models.DateField(blank=True, null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Semester',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('semester', models.CharField(blank=True, choices=[('First', 'First'), ('Second', 'Second'), ('Third', 'Third')], max_length=10)),
+ ('is_current_semester', models.BooleanField(blank=True, default=False, null=True)),
+ ('next_semester_begins', models.DateField(blank=True, null=True)),
+ ('session', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='app.Session')),
+ ],
+ ),
+ ]
diff --git a/app/migrations/0002_auto_20200730_0746.py b/app/migrations/0002_auto_20200730_0746.py
new file mode 100644
index 0000000..1aaa8c1
--- /dev/null
+++ b/app/migrations/0002_auto_20200730_0746.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.3 on 2020-07-30 04:46
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('app', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='semester',
+ name='semester',
+ field=models.CharField(blank=True, choices=[('First', 'First'), ('Second', 'Second'), ('Third', 'Third')], max_length=10, unique=True),
+ ),
+ ]
diff --git a/app/migrations/0003_auto_20200730_0756.py b/app/migrations/0003_auto_20200730_0756.py
new file mode 100644
index 0000000..612a245
--- /dev/null
+++ b/app/migrations/0003_auto_20200730_0756.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.3 on 2020-07-30 04:56
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('app', '0002_auto_20200730_0746'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='semester',
+ name='semester',
+ field=models.CharField(blank=True, choices=[('First', 'First'), ('Second', 'Second'), ('Third', 'Third')], max_length=10),
+ ),
+ ]
diff --git a/app/migrations/__init__.py b/app/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/models.py b/app/models.py
new file mode 100644
index 0000000..cd4e16b
--- /dev/null
+++ b/app/models.py
@@ -0,0 +1,82 @@
+from django.db import models
+from django.urls import reverse
+from django.core.validators import FileExtensionValidator
+from django.contrib.auth.models import AbstractUser
+from django.db.models import Q
+
+NEWS = "News"
+EVENTS = "Event"
+
+POST = (
+ (NEWS, "News"),
+ (EVENTS, "Event"),
+)
+
+FIRST = "First"
+SECOND = "Second"
+THIRD = "Third"
+
+SEMESTER = (
+ (FIRST, "First"),
+ (SECOND, "Second"),
+ (THIRD, "Third"),
+)
+
+
+class NewsAndEventsQuerySet(models.query.QuerySet):
+
+ def search(self, query):
+ lookups = (Q(title__icontains=query) |
+ Q(summary__icontains=query) |
+ Q(posted_as__icontains=query)
+ )
+ return self.filter(lookups).distinct()
+
+
+class NewsAndEventsManager(models.Manager):
+ def get_queryset(self):
+ return NewsAndEventsQuerySet(self.model, using=self._db)
+
+ def all(self):
+ return self.get_queryset()
+
+ def get_by_id(self, id):
+ qs = self.get_queryset().filter(id=id) # NewsAndEvents.objects == self.get_queryset()
+ if qs.count() == 1:
+ return qs.first()
+ return None
+
+ def search(self, query):
+ return self.get_queryset().search(query)
+
+
+class NewsAndEvents(models.Model):
+ title = models.CharField(max_length=200, null=True)
+ summary = models.TextField(max_length=200, blank=True, null=True)
+ posted_as = models.CharField(choices=POST, max_length=10)
+ updated_date = models.DateTimeField(auto_now=True, auto_now_add=False, null=True)
+ upload_time = models.DateTimeField(auto_now=False, auto_now_add=True, null=True)
+
+ objects = NewsAndEventsManager()
+
+ def __str__(self):
+ return self.title
+
+
+class Session(models.Model):
+ session = models.CharField(max_length=200, unique=True)
+ is_current_session = models.BooleanField(default=False, blank=True, null=True)
+ next_session_begins = models.DateField(blank=True, null=True)
+
+ def __str__(self):
+ return self.session
+
+
+class Semester(models.Model):
+ semester = models.CharField(max_length=10, choices=SEMESTER, blank=True)
+ is_current_semester = models.BooleanField(default=False, blank=True, null=True)
+ session = models.ForeignKey(Session, on_delete=models.CASCADE, blank=True, null=True)
+ next_semester_begins = models.DateField(null=True, blank=True)
+
+ def __str__(self):
+ return self.semester
diff --git a/app/templates/app/index.html b/app/templates/app/index.html
new file mode 100644
index 0000000..89fc8b1
--- /dev/null
+++ b/app/templates/app/index.html
@@ -0,0 +1,90 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+{% load static %}
+
+{% block content %}
+
+Home
+
+{% if request.user.is_superuser %}
+ Add New Post
+{% endif %}
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+News & Events
+
+
+{% if items %}
+
+ {% for item in items %}
+
+
+
{{ item.title|title }}
+
+ {{ item.posted_as }}
+
+ {% if request.user.is_superuser %}
+
+
+
+ {% endif %}
+
{{ item.summary }}
+
{{ item.updated_date|timesince }} ago
+ {% if forloop.counter|divisibleby:3 %}
+ {% else %}
+
+
+ {% endif %}
+ {% endfor %}
+
+{% else %}
+
+
No News & Events yet.
+
+{% endif %}
+
+{% endblock content %}
diff --git a/app/templates/app/post_add.html b/app/templates/app/post_add.html
new file mode 100644
index 0000000..a5d1549
--- /dev/null
+++ b/app/templates/app/post_add.html
@@ -0,0 +1,39 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+{% load crispy_forms_tags %}
+{% load static %}
+
+{% block content %}
+
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
+
+
Item Post & Update Form
+
+
+
+
+
+{% endblock content %}
diff --git a/app/templates/app/semester_list.html b/app/templates/app/semester_list.html
new file mode 100644
index 0000000..4b3931f
--- /dev/null
+++ b/app/templates/app/semester_list.html
@@ -0,0 +1,91 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+
+{% block content %}
+
+
+
+{% if request.user.is_superuser %}
+ Add New Semester
+{% endif %}
+
+ Semester List
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
+
+
+ #
+ Semester
+ Is Current semester
+ Session
+ Next Semester Begins
+ {% if request.user.is_superuser %}
+ Actions
+ {% endif %}
+
+
+
+ {% for semester in semesters %}
+
+ {{ forloop.counter }}.
+ {{ semester.semester }}
+ {% if semester.is_current_semester == False %}
+ {% else %}
+
+
+ {% endif %}
+
+ {{ semester.session }}
+ {{ semester.next_semester_begins }}
+
+ {% if request.user.is_superuser %}
+
+
+ {% endif %}
+
+ {% empty %}
+
+
+
+
+
+ No Semester.
+ {% if request.user.is_superuser %}
+
+
+ Add Semester Now.
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+ {% endfor %}
+
+
+
+{% endblock content %}
diff --git a/app/templates/app/semester_update.html b/app/templates/app/semester_update.html
new file mode 100644
index 0000000..0e0bd4f
--- /dev/null
+++ b/app/templates/app/semester_update.html
@@ -0,0 +1,39 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+{% load crispy_forms_tags %}
+{% load static %}
+
+{% block content %}
+
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
+
+
Semester Add & update Form
+
+
+
+
+
+
+
+{% endblock content %}
diff --git a/app/templates/app/session_list.html b/app/templates/app/session_list.html
new file mode 100644
index 0000000..c640455
--- /dev/null
+++ b/app/templates/app/session_list.html
@@ -0,0 +1,90 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+
+{% block content %}
+
+
+
+{% if request.user.is_superuser %}
+ Add New Session
+{% endif %}
+
+ Session List
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
+
+
+ #
+ Session
+ Is Current Session
+ Next Session Begins
+ {% if request.user.is_superuser %}
+ Actions
+ {% endif %}
+
+
+
+ {% for session in sessions %}
+
+ {{ forloop.counter }}.
+ {{ session.session }}
+
+ {% if session.is_current_session == True %}
+
+ {% else %}
+
+ {% endif %}
+
+ {{ session.next_session_begins }}
+
+ {% if request.user.is_superuser %}
+
+
+ {% endif %}
+
+ {% empty %}
+
+
+
+
+
+ No Session.
+ {% if request.user.is_superuser %}
+
+
+ Add Session Now.
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+ {% endfor %}
+
+
+
+{% endblock content %}
diff --git a/app/templates/app/session_update.html b/app/templates/app/session_update.html
new file mode 100644
index 0000000..296b17f
--- /dev/null
+++ b/app/templates/app/session_update.html
@@ -0,0 +1,39 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+{% load crispy_forms_tags %}
+{% load static %}
+
+{% block content %}
+
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
+
+
Session Add & update Form
+
+
+
+
+
+
+
+{% endblock content %}
diff --git a/app/templates/common/404.html b/app/templates/common/404.html
new file mode 100644
index 0000000..6141ae2
--- /dev/null
+++ b/app/templates/common/404.html
@@ -0,0 +1,10 @@
+{% extends 'base.html' %}
+{% block title %}Page Not Found 404 | CFE{% endblock title %}
+
+{% block content %}
+
+
+{% endblock content %}
diff --git a/app/tests.py b/app/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/app/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/app/urls.py b/app/urls.py
new file mode 100644
index 0000000..dcb0455
--- /dev/null
+++ b/app/urls.py
@@ -0,0 +1,27 @@
+from django.conf.urls import url
+from django.urls import path
+
+from .views import (
+ home_view, post_add, edit_post, delete_post,
+ session_list_view, session_add_view, session_update_view, session_delete_view,
+ semester_list_view, semester_add_view, semester_update_view, semester_delete_view
+)
+
+
+urlpatterns = [
+ # Accounts url
+ url(r'^$', home_view, name='home'),
+ url(r'^add_item/$', post_add, name='add_item'),
+ url(r'^item/(?P\d+)/edit/$', edit_post, name='edit_post'),
+ url(r'^item/(?P\d+)/delete/$', delete_post, name='delete_post'),
+
+ url(r'^session/$', session_list_view, name="session_list"),
+ url(r'^session/add/$', session_add_view, name="add_session"),
+ url(r'^session/(?P\d+)/edit/$', session_update_view, name="edit_session"),
+ url(r'^session/(?P\d+)/delete/$', session_delete_view, name="delete_session"),
+
+ url(r'^semester/$', semester_list_view, name="semester_list"),
+ url(r'^semester/add/$', semester_add_view, name="add_semester"),
+ url(r'^semester/(?P\d+)/edit/$', semester_update_view, name="edit_semester"),
+ url(r'^semester/(?P\d+)/delete/$', semester_delete_view, name="delete_semester"),
+]
diff --git a/app/views.py b/app/views.py
new file mode 100644
index 0000000..ae55598
--- /dev/null
+++ b/app/views.py
@@ -0,0 +1,292 @@
+from django.shortcuts import render, redirect, get_object_or_404
+from django.contrib import messages
+from django.http import HttpResponseRedirect
+from django.urls import reverse_lazy
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth import update_session_auth_hash
+from django.views.generic import CreateView, ListView
+from django.core.paginator import Paginator
+from django.conf import settings
+from django.utils.decorators import method_decorator
+
+from accounts.decorators import lecturer_required, student_required
+from .forms import SessionForm, SemesterForm, NewsAndEventsForm
+from .models import *
+
+User = settings.AUTH_USER_MODEL
+
+# ########################################################
+# News & Events
+# ########################################################
+@login_required
+def home_view(request):
+ items = NewsAndEvents.objects.all().order_by('-updated_date')
+ context = {
+ 'title': "News & Events | DjangoSMS",
+ 'items': items,
+ }
+ return render(request, 'app/index.html', context)
+
+
+@login_required
+def post_add(request):
+ if request.method == 'POST':
+ form = NewsAndEventsForm(request.POST)
+ title = request.POST.get('title')
+ if form.is_valid():
+ form.save()
+
+ messages.success(request, (title + ' has been uploaded.'))
+ return redirect('home')
+ else:
+ messages.error(request, 'Please correct the error(s) below.')
+ else:
+ form = NewsAndEventsForm()
+ return render(request, 'app/post_add.html', {
+ 'title': 'Add Post | DjangoSMS',
+ 'form': form,
+ })
+
+
+@login_required
+@lecturer_required
+def edit_post(request, pk):
+ instance = get_object_or_404(NewsAndEvents, pk=pk)
+ if request.method == 'POST':
+ form = NewsAndEventsForm(request.POST, instance=instance)
+ title = request.POST.get('title')
+ if form.is_valid():
+ form.save()
+
+ messages.success(request, (title + ' has been updated.'))
+ return redirect('home')
+ else:
+ messages.error(request, 'Please correct the error(s) below.')
+ else:
+ form = NewsAndEventsForm(instance=instance)
+ return render(request, 'app/post_add.html', {
+ 'title': 'Edit Post | DjangoSMS',
+ 'form': form,
+ })
+
+
+@login_required
+@lecturer_required
+def delete_post(request, pk):
+ post = get_object_or_404(NewsAndEvents, pk=pk)
+ title = post.title
+ post.delete()
+ messages.success(request, (title + ' has been deleted.'))
+ return redirect('home')
+
+# ########################################################
+# Session
+# ########################################################
+@login_required
+@lecturer_required
+def session_list_view(request):
+ """ Show list of all sessions """
+ sessions = Session.objects.all().order_by('-is_current_session', '-session')
+ return render(request, 'app/session_list.html', {"sessions": sessions})
+
+
+@login_required
+@lecturer_required
+def session_add_view(request):
+ """ check request method, if POST we add session otherwise show empty form """
+ if request.method == 'POST':
+ form = SessionForm(request.POST)
+ if form.is_valid():
+ data = form.data.get('is_current_session') # returns string of 'True' if the user selected Yes
+ print(data)
+ if data == 'true':
+ sessions = Session.objects.all()
+ if sessions:
+ for session in sessions:
+ if session.is_current_session == True:
+ unset = Session.objects.get(is_current_session=True)
+ unset.is_current_session = False
+ unset.save()
+ form.save()
+ else:
+ form.save()
+ else:
+ form.save()
+ messages.success(request, 'Session added successfully. ')
+ return redirect('session_list')
+
+ else:
+ form = SessionForm()
+ return render(request, 'app/session_update.html', {'form': form})
+
+
+@login_required
+@lecturer_required
+def session_update_view(request, pk):
+ session = Session.objects.get(pk=pk)
+ if request.method == 'POST':
+ form = SessionForm(request.POST, instance=session)
+ data = form.data.get('is_current_session')
+ if data == 'true':
+ sessions = Session.objects.all()
+ if sessions:
+ for session in sessions:
+ if session.is_current_session == True:
+ unset = Session.objects.get(is_current_session=True)
+ unset.is_current_session = False
+ unset.save()
+
+ if form.is_valid():
+ form.save()
+ messages.success(request, 'Session updated successfully. ')
+ return redirect('session_list')
+ else:
+ form = SessionForm(request.POST, instance=session)
+ if form.is_valid():
+ form.save()
+ messages.success(request, 'Session updated successfully. ')
+ return redirect('session_list')
+
+ else:
+ form = SessionForm(instance=session)
+ return render(request, 'app/session_update.html', {'form': form})
+
+
+@login_required
+@lecturer_required
+def session_delete_view(request, pk):
+ session = get_object_or_404(Session, pk=pk)
+
+ if session.is_current_session:
+ messages.error(request, "You cannot delete current session")
+ return redirect('session_list')
+ else:
+ session.delete()
+ messages.success(request, "Session successfully deleted")
+ return redirect('session_list')
+# ########################################################
+
+
+# ########################################################
+# Semester
+# ########################################################
+@login_required
+@lecturer_required
+def semester_list_view(request):
+ semesters = Semester.objects.all().order_by('-is_current_semester', '-semester')
+ return render(request, 'app/semester_list.html', {"semesters": semesters, })
+
+
+@login_required
+@lecturer_required
+def semester_add_view(request):
+ if request.method == 'POST':
+ form = SemesterForm(request.POST)
+ if form.is_valid():
+ data = form.data.get('is_current_semester') # returns string of 'True' if the user selected Yes
+ if data == 'True':
+ semester = form.data.get('semester')
+ ss = form.data.get('session')
+ session = Session.objects.get(pk=ss)
+ try:
+ if Semester.objects.get(semester=semester, session=ss):
+ messages.error(request, semester + " semester in " + session.session + " session already exist")
+ return redirect('add_semester')
+ except:
+ semesters = Semester.objects.all()
+ sessions = Session.objects.all()
+ if semesters:
+ for semester in semesters:
+ if semester.is_current_semester == True:
+ unset_semester = Semester.objects.get(is_current_semester=True)
+ unset_semester.is_current_semester = False
+ unset_semester.save()
+ for session in sessions:
+ if session.is_current_session == True:
+ unset_session = Session.objects.get(is_current_session=True)
+ unset_session.is_current_session = False
+ unset_session.save()
+
+ new_session = request.POST.get('session')
+ set_session = Session.objects.get(pk=new_session)
+ set_session.is_current_session = True
+ set_session.save()
+ form.save()
+ messages.success(request, 'Semester added successfully.')
+ return redirect('semester_list')
+
+ form.save()
+ messages.success(request, 'Semester added successfully. ')
+ return redirect('semester_list')
+ else:
+ form = SemesterForm()
+ return render(request, 'app/semester_update.html', {'form': form})
+
+
+@login_required
+@lecturer_required
+def semester_update_view(request, pk):
+ semester = Semester.objects.get(pk=pk)
+ if request.method == 'POST':
+ if request.POST.get('is_current_semester') == 'True': # returns string of 'True' if the user selected yes for 'is current semester'
+ unset_semester = Semester.objects.get(is_current_semester=True)
+ unset_semester.is_current_semester = False
+ unset_semester.save()
+ unset_session = Session.objects.get(is_current_session=True)
+ unset_session.is_current_session = False
+ unset_session.save()
+ new_session = request.POST.get('session')
+ form = SemesterForm(request.POST, instance=semester)
+ if form.is_valid():
+ set_session = Session.objects.get(pk=new_session)
+ set_session.is_current_session = True
+ set_session.save()
+ form.save()
+ messages.success(request, 'Semester updated successfully !')
+ return redirect('semester_list')
+ else:
+ form = SemesterForm(request.POST, instance=semester)
+ if form.is_valid():
+ form.save()
+ return redirect('semester_list')
+
+ else:
+ form = SemesterForm(instance=semester)
+ return render(request, 'app/semester_update.html', {'form': form})
+
+
+@login_required
+@lecturer_required
+def semester_delete_view(request, pk):
+ semester = get_object_or_404(Semester, pk=pk)
+ if semester.is_current_semester:
+ messages.error(request, "You cannot delete current semester")
+ return redirect('semester_list')
+ else:
+ semester.delete()
+ messages.success(request, "Semester successfully deleted")
+ return redirect('semester_list')
+# ########################################################
+
+
+# from django.shortcuts import render_to_response
+# from django.template import RequestContext
+
+# def handler404(request, exception, template_name="common/404.html"):
+# response = render_to_response("common/404.html")
+# response.status_code = 404
+# return response
+
+
+# def handler500(request, *args, **argv):
+# response = render_to_response('common/500.html', {}, context_instance=RequestContext(request))
+# response.status_code = 500
+
+# return response
+
+
+# def handler400(request, exception, template_name="common/400.html"):
+# response = render_to_response('common/400.html', context_instance=RequestContext(request))
+# response.status_code = 400
+
+# return response
diff --git a/course/__init__.py b/course/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/course/admin.py b/course/admin.py
new file mode 100644
index 0000000..1ddab5e
--- /dev/null
+++ b/course/admin.py
@@ -0,0 +1,10 @@
+from django.contrib import admin
+from django.contrib.auth.models import Group
+
+from .models import Program, Course, CourseAllocation, Upload
+
+
+admin.site.register(Program)
+admin.site.register(Course)
+admin.site.register(CourseAllocation)
+admin.site.register(Upload)
diff --git a/course/apps.py b/course/apps.py
new file mode 100644
index 0000000..9e578cc
--- /dev/null
+++ b/course/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class CourseConfig(AppConfig):
+ name = 'course'
diff --git a/course/forms.py b/course/forms.py
new file mode 100644
index 0000000..6d2274d
--- /dev/null
+++ b/course/forms.py
@@ -0,0 +1,105 @@
+from django import forms
+from django.db import transaction
+from django.conf import settings
+from django.contrib.auth.models import User
+
+from accounts.models import User
+from .models import Program, Course, CourseAllocation, Upload, UploadVideo
+
+# User = settings.AUTH_USER_MODEL
+
+class ProgramForm(forms.ModelForm):
+ class Meta:
+ model = Program
+ fields = '__all__'
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields['title'].widget.attrs.update({'class': 'form-control'})
+ self.fields['summary'].widget.attrs.update({'class': 'form-control'})
+
+
+class CourseAddForm(forms.ModelForm):
+ class Meta:
+ model = Course
+ fields = '__all__'
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields['title'].widget.attrs.update({'class': 'form-control'})
+ self.fields['code'].widget.attrs.update({'class': 'form-control'})
+ # self.fields['courseUnit'].widget.attrs.update({'class': 'form-control'})
+ self.fields['credit'].widget.attrs.update({'class': 'form-control'})
+ self.fields['summary'].widget.attrs.update({'class': 'form-control'})
+ self.fields['program'].widget.attrs.update({'class': 'form-control'})
+ self.fields['level'].widget.attrs.update({'class': 'form-control'})
+ self.fields['year'].widget.attrs.update({'class': 'form-control'})
+ self.fields['semester'].widget.attrs.update({'class': 'form-control'})
+
+
+class CourseAllocationForm(forms.ModelForm):
+ courses = forms.ModelMultipleChoiceField(
+ queryset=Course.objects.all().order_by('level'),
+ widget=forms.CheckboxSelectMultiple(attrs={'class': 'browser-default checkbox'}),
+ required=True
+ )
+ lecturer = forms.ModelChoiceField(
+ queryset=User.objects.filter(is_lecturer=True),
+ widget=forms.Select(attrs={'class': 'browser-default custom-select'}),
+ label="lecturer",
+ )
+
+ class Meta:
+ model = CourseAllocation
+ fields = ['lecturer', 'courses']
+
+ def __init__(self, *args, **kwargs):
+ user = kwargs.pop('user')
+ super(CourseAllocationForm, self).__init__(*args, **kwargs)
+ self.fields['lecturer'].queryset = User.objects.filter(is_lecturer=True)
+
+
+class EditCourseAllocationForm(forms.ModelForm):
+ courses = forms.ModelMultipleChoiceField(
+ queryset=Course.objects.all().order_by('level'),
+ widget=forms.CheckboxSelectMultiple,
+ required=True
+ )
+ lecturer = forms.ModelChoiceField(
+ queryset=User.objects.filter(is_lecturer=True),
+ widget=forms.Select(attrs={'class': 'browser-default custom-select'}),
+ label="lecturer",
+ )
+
+ class Meta:
+ model = CourseAllocation
+ fields = ['lecturer', 'courses']
+
+ def __init__(self, *args, **kwargs):
+ # user = kwargs.pop('user')
+ super(EditCourseAllocationForm, self).__init__(*args, **kwargs)
+ self.fields['lecturer'].queryset = User.objects.filter(is_lecturer=True)
+
+
+# Upload files to specific course
+class UploadFormFile(forms.ModelForm):
+ class Meta:
+ model = Upload
+ fields = ('title', 'file', 'course',)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields['title'].widget.attrs.update({'class': 'form-control'})
+ self.fields['file'].widget.attrs.update({'class': 'form-control'})
+
+
+# Upload video to specific course
+class UploadFormVideo(forms.ModelForm):
+ class Meta:
+ model = UploadVideo
+ fields = ('title', 'video', 'course',)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields['title'].widget.attrs.update({'class': 'form-control'})
+ self.fields['video'].widget.attrs.update({'class': 'form-control'})
diff --git a/course/migrations/0001_initial.py b/course/migrations/0001_initial.py
new file mode 100644
index 0000000..bd6002e
--- /dev/null
+++ b/course/migrations/0001_initial.py
@@ -0,0 +1,67 @@
+# Generated by Django 2.2.3 on 2020-07-29 15:25
+
+from django.conf import settings
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('app', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Course',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('slug', models.SlugField(blank=True, unique=True)),
+ ('title', models.CharField(max_length=200, null=True)),
+ ('code', models.CharField(max_length=200, null=True, unique=True)),
+ ('credit', models.IntegerField(default=0, null=True)),
+ ('summary', models.TextField(blank=True, max_length=200, null=True)),
+ ('level', models.CharField(choices=[('Level course', 'Level course'), ('Bachloar', 'Bachloar'), ('Master', 'Master')], max_length=25, null=True)),
+ ('year', models.IntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (4, '5'), (4, '6')], default=0)),
+ ('semester', models.CharField(choices=[('First', 'First'), ('Second', 'Second'), ('Third', 'Third')], max_length=200)),
+ ('is_elective', models.BooleanField(blank=True, default=False, null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Program',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=150, unique=True)),
+ ('summary', models.TextField(blank=True, null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Upload',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=100)),
+ ('file', models.FileField(upload_to='course_files/', validators=[django.core.validators.FileExtensionValidator(['pdf', 'docx', 'doc', 'xls', 'xlsx', 'ppt', 'pptx', 'zip', 'rar', '7zip'])])),
+ ('updated_date', models.DateTimeField(auto_now=True, null=True)),
+ ('upload_time', models.DateTimeField(auto_now_add=True, null=True)),
+ ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.Course')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='CourseAllocation',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('courses', models.ManyToManyField(related_name='allocated_course', to='course.Course')),
+ ('lecturer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allocated_lecturer', to=settings.AUTH_USER_MODEL)),
+ ('session', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='app.Session')),
+ ],
+ ),
+ migrations.AddField(
+ model_name='course',
+ name='program',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.Program'),
+ ),
+ ]
diff --git a/course/migrations/0002_uploadvideo.py b/course/migrations/0002_uploadvideo.py
new file mode 100644
index 0000000..0930890
--- /dev/null
+++ b/course/migrations/0002_uploadvideo.py
@@ -0,0 +1,27 @@
+# Generated by Django 2.2.3 on 2020-08-03 09:24
+
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('course', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='UploadVideo',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=100)),
+ ('slug', models.SlugField(blank=True, unique=True)),
+ ('video', models.FileField(upload_to='videos/', validators=[django.core.validators.FileExtensionValidator(['mp4', 'mkv', 'wmv', '3gp', 'f4v', 'avi', 'mp3'])])),
+ ('summary', models.TextField(blank=True, null=True)),
+ ('timestamp', models.DateTimeField(auto_now_add=True, null=True)),
+ ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.Course')),
+ ],
+ ),
+ ]
diff --git a/course/migrations/0003_auto_20200803_1335.py b/course/migrations/0003_auto_20200803_1335.py
new file mode 100644
index 0000000..b213821
--- /dev/null
+++ b/course/migrations/0003_auto_20200803_1335.py
@@ -0,0 +1,19 @@
+# Generated by Django 2.2.3 on 2020-08-03 10:35
+
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('course', '0002_uploadvideo'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='uploadvideo',
+ name='video',
+ field=models.FileField(upload_to='course_videos/', validators=[django.core.validators.FileExtensionValidator(['mp4', 'mkv', 'wmv', '3gp', 'f4v', 'avi', 'mp3'])]),
+ ),
+ ]
diff --git a/course/migrations/0004_auto_20200822_2238.py b/course/migrations/0004_auto_20200822_2238.py
new file mode 100644
index 0000000..1f8a15b
--- /dev/null
+++ b/course/migrations/0004_auto_20200822_2238.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.3 on 2020-08-22 19:38
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('course', '0003_auto_20200803_1335'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='course',
+ name='level',
+ field=models.CharField(choices=[('Bachloar', 'Bachloar Degree'), ('Master', 'Master Degree')], max_length=25, null=True),
+ ),
+ ]
diff --git a/course/migrations/__init__.py b/course/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/course/models.py b/course/models.py
new file mode 100644
index 0000000..6b5c955
--- /dev/null
+++ b/course/models.py
@@ -0,0 +1,184 @@
+from django.db import models
+from django.urls import reverse
+from django.conf import settings
+from django.core.validators import FileExtensionValidator
+from django.db.models.signals import pre_save
+
+from django.db.models import Q
+
+from app.models import Session
+from .utils import *
+
+User = settings.AUTH_USER_MODEL
+
+YEARS = (
+ (1, '1'),
+ (2, '2'),
+ (3, '3'),
+ (4, '4'),
+ (4, '5'),
+ (4, '6'),
+ )
+
+# LEVEL_COURSE = "Level course"
+BACHLOAR_DEGREE = "Bachloar"
+MASTER_DEGREE = "Master"
+
+LEVEL = (
+ # (LEVEL_COURSE, "Level course"),
+ (BACHLOAR_DEGREE, "Bachloar Degree"),
+ (MASTER_DEGREE, "Master Degree"),
+)
+
+FIRST = "First"
+SECOND = "Second"
+THIRD = "Third"
+
+SEMESTER = (
+ (FIRST, "First"),
+ (SECOND, "Second"),
+ (THIRD, "Third"),
+)
+
+
+class ProgramManager(models.Manager):
+ def search(self, query=None):
+ qs = self.get_queryset()
+ if query is not None:
+ or_lookup = (Q(title__icontains=query) |
+ Q(summary__icontains=query)
+ )
+ qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
+ return qs
+
+
+class Program(models.Model):
+ title = models.CharField(max_length=150, unique=True)
+ summary = models.TextField(null=True, blank=True)
+
+ objects = ProgramManager()
+
+ def __str__(self):
+ return self.title
+
+ def get_absolute_url(self):
+ return reverse('program_detail', kwargs={'pk': self.pk})
+
+
+class CourseManager(models.Manager):
+ def search(self, query=None):
+ qs = self.get_queryset()
+ if query is not None:
+ or_lookup = (Q(title__icontains=query) |
+ Q(summary__icontains=query)|
+ Q(code__icontains=query)|
+ Q(slug__icontains=query)
+ )
+ qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
+ return qs
+
+
+class Course(models.Model):
+ slug = models.SlugField(blank=True, unique=True)
+ title = models.CharField(max_length=200, null=True)
+ code = models.CharField(max_length=200, unique=True, null=True)
+ credit = models.IntegerField(null=True, default=0)
+ summary = models.TextField(max_length=200, blank=True, null=True)
+ program = models.ForeignKey(Program, on_delete=models.CASCADE)
+ level = models.CharField(max_length=25, choices=LEVEL, null=True)
+ year = models.IntegerField(choices=YEARS, default=0)
+ semester = models.CharField(choices=SEMESTER, max_length=200)
+ is_elective = models.BooleanField(default=False, blank=True, null=True)
+
+ objects = CourseManager()
+
+ def __str__(self):
+ return "{0} ({1})".format(self.title, self.code)
+
+ def get_absolute_url(self):
+ return reverse('course_detail', kwargs={'slug': self.slug})
+
+ @property
+ def is_current_semester(self):
+ from app.models import Semester
+ current_semester = Semester.objects.get(is_current_semester=True)
+
+ if self.semester == current_semester.semester:
+ return True
+ else:
+ return False
+
+
+def course_pre_save_receiver(sender, instance, *args, **kwargs):
+ if not instance.slug:
+ instance.slug = unique_slug_generator(instance)
+
+pre_save.connect(course_pre_save_receiver, sender=Course)
+
+
+class CourseAllocation(models.Model):
+ lecturer = models.ForeignKey(User, on_delete=models.CASCADE, related_name='allocated_lecturer')
+ courses = models.ManyToManyField(Course, related_name='allocated_course')
+ session = models.ForeignKey(Session, on_delete=models.CASCADE, blank=True, null=True)
+
+ def __str__(self):
+ return self.lecturer.get_full_name
+
+ def get_absolute_url(self):
+ return reverse('edit_allocated_course', kwargs={'pk': self.pk})
+
+
+class Upload(models.Model):
+ title = models.CharField(max_length=100)
+ course = models.ForeignKey(Course, on_delete=models.CASCADE)
+ file = models.FileField(upload_to='course_files/', validators=[FileExtensionValidator(['pdf', 'docx', 'doc', 'xls', 'xlsx', 'ppt', 'pptx', 'zip', 'rar', '7zip'])])
+ updated_date = models.DateTimeField(auto_now=True, auto_now_add=False, null=True)
+ upload_time = models.DateTimeField(auto_now=False, auto_now_add=True, null=True)
+
+ def __str__(self):
+ return str(self.file)[6:]
+
+ def get_extension_short(self):
+ ext = str(self.file).split(".")
+ ext = ext[len(ext)-1]
+
+ if ext == 'doc' or ext == 'docx':
+ return 'word'
+ elif ext == 'pdf':
+ return 'pdf'
+ elif ext == 'xls' or ext == 'xlsx':
+ return 'excel'
+ elif ext == 'ppt' or ext == 'pptx':
+ return 'powerpoint'
+ elif ext == 'zip' or ext == 'rar' or ext == '7zip':
+ return 'archive'
+
+ def delete(self, *args, **kwargs):
+ self.file.delete()
+ super().delete(*args, **kwargs)
+
+
+class UploadVideo(models.Model):
+ title = models.CharField(max_length=100)
+ slug = models.SlugField(blank=True, unique=True)
+ course = models.ForeignKey(Course, on_delete=models.CASCADE)
+ video = models.FileField(upload_to='course_videos/', validators=[FileExtensionValidator(['mp4', 'mkv', 'wmv', '3gp', 'f4v', 'avi', 'mp3'])])
+ summary = models.TextField(null=True, blank=True)
+ timestamp = models.DateTimeField(auto_now=False, auto_now_add=True, null=True)
+
+ def __str__(self):
+ return str(self.title)
+
+ def get_absolute_url(self):
+ return reverse('video_single', kwargs={'slug': self.course.slug, 'video_slug': self.slug})
+
+ def delete(self, *args, **kwargs):
+ self.video.delete()
+ super().delete(*args, **kwargs)
+
+
+def video_pre_save_receiver(sender, instance, *args, **kwargs):
+ if not instance.slug:
+ instance.slug = unique_slug_generator(instance)
+
+pre_save.connect(video_pre_save_receiver, sender=UploadVideo)
diff --git a/course/templates/course/course_add.html b/course/templates/course/course_add.html
new file mode 100644
index 0000000..b552542
--- /dev/null
+++ b/course/templates/course/course_add.html
@@ -0,0 +1,88 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+{% load crispy_forms_tags %}
+{% load static %}
+
+{% block content %}
+
+
+Course Add & Update Form
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
+{% endblock content %}
diff --git a/course/templates/course/course_allocation_form.html b/course/templates/course/course_allocation_form.html
new file mode 100644
index 0000000..a5ba949
--- /dev/null
+++ b/course/templates/course/course_allocation_form.html
@@ -0,0 +1,51 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+{% load crispy_forms_tags %}
+{% load static %}
+
+{% block content %}
+
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
+
+
Course Allocation Form
+
+
+
+
+
+
+{% endblock content %}
diff --git a/course/templates/course/course_allocation_view.html b/course/templates/course/course_allocation_view.html
new file mode 100644
index 0000000..d1e6a03
--- /dev/null
+++ b/course/templates/course/course_allocation_view.html
@@ -0,0 +1,92 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+
+{% block content %}
+
+
+{% if request.user.is_superuser %}
+ Allocate Now
+{% endif %}
+
+ Course Allocation List
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
+
+
+
+
+
+
+ #
+ Lecturer
+ Courses
+ {% if request.user.is_superuser %}
+ Action
+ {% endif %}
+
+
+ {% for course in allocated_courses %}
+
+
+ {{ forloop.counter }}.
+ {{ course.lecturer.get_full_name }}
+ {% for i in course.courses.all %}
+
+ {% endfor %}
+
+ {% if request.user.is_superuser %}
+
+
+ {% endif %}
+
+ {% empty %}
+
+
+
+
+ No Course Allocated.
+ {% if request.user.is_superuser %}
+
+
+ Allocate now
+
+ {% endif %}
+
+
+
+
+
+
+ {% endfor %}
+
+
+{% endblock content %}
diff --git a/course/templates/course/course_registration.html b/course/templates/course/course_registration.html
new file mode 100644
index 0000000..09fde40
--- /dev/null
+++ b/course/templates/course/course_registration.html
@@ -0,0 +1,272 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+{% load static %}
+
+{% load crispy_forms_tags %}
+
+{% block content %}
+
+
+Course Add & Drop
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+{% if not all_courses_are_registered %}
+
+
+{% endif %}
+
+
+
+{% if not no_course_is_registered %}
+
+
+
+ Print Registration Form
+
+
+
+
+{% endif %}
+
+{% endblock content %}
diff --git a/course/templates/course/course_single.html b/course/templates/course/course_single.html
new file mode 100644
index 0000000..a700636
--- /dev/null
+++ b/course/templates/course/course_single.html
@@ -0,0 +1,289 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }} | DjangoSMS{% endblock title %}
+{% load static %}
+
+{% block content %}
+
+
+
+
+
+
+
+
{{ course }}
+
+
{{ course.summary }}
+
+{% if request.user.is_superuser %}
+
+{% endif %}
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
+
+
+
Video Tutorials
+
+
+
+
+ #
+ Video Title
+ Uploaded Date
+ Get Started
+ {% if request.user.is_superuser or request.user.is_lecturer %}
+ Actions
+ {% endif %}
+
+
+
+ {% for video in videos %}
+
+ {{ forloop.counter }}
+
+
+ {{ video.title|title }}
+
+
+ {{ video.timestamp|date }}
+
+
+
+
+ {% if request.user.is_superuser or request.user.is_lecturer %}
+
+
+ {% endif %}
+
+ {% empty %}
+
+
+
+
+
+ No video Uploaded.
+ {% if request.user.is_superuser or request.user.is_lecturer %}
+
+
+ Upload now.
+
+ {% endif %}
+
+
+
+
+
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
Documentations
+
+
+
+
+ #
+ File name
+ Uploaded Date
+ Updated Date
+ Downloads
+ {% if request.user.is_superuser or request.user.is_lecturer %}
+ Actions
+ {% endif %}
+
+
+
+ {% for file in files %}
+
+ {{ forloop.counter }}
+
+
+ {{ file.title|title }}
+
+
+ {{ file.upload_time|date }}
+ {{ file.updated_date|date }}
+
+
+
+
+ {% if request.user.is_superuser or request.user.is_lecturer %}
+
+
+ {% endif %}
+
+ {% empty %}
+
+
+
+
+
+ No File Uploaded.
+ {% if request.user.is_superuser or request.user.is_lecturer %}
+
+
+ Upload now.
+
+ {% endif %}
+
+
+
+
+
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
Instructor(s)
+
+
+
+ {% for lecturer in lecturers %}
+
+
+
{{ lecturer|title }}
+
{{ lecturer.lecturer.email }}
+
Donec sed odio dui. Etiam porta sem malesuada magna mollis euismod. Nullam id dolor id nibh ultricies vehicula ut id elit.
+
+
+
+
+
+
+
+ {% endfor %}
+
+
+
+
+
+{% endblock content %}
diff --git a/course/templates/course/program_add.html b/course/templates/course/program_add.html
new file mode 100644
index 0000000..a5c478c
--- /dev/null
+++ b/course/templates/course/program_add.html
@@ -0,0 +1,48 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+{% load crispy_forms_tags %}
+{% load static %}
+
+{% block content %}
+
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+{% endblock content %}
diff --git a/course/templates/course/program_list.html b/course/templates/course/program_list.html
new file mode 100644
index 0000000..8522c3b
--- /dev/null
+++ b/course/templates/course/program_list.html
@@ -0,0 +1,115 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+
+{% block content %}
+
+
+{% if request.user.is_superuser %}
+ Add Program
+{% endif %}
+
+ Program List
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
+
+
+
+
+
+{% for program in programs %}
+
+
+
+
+ Are you sure you want to delete this item?
+
+
+
+
+
+{% endfor %}
+
+
+
+
+
+
+
+ #
+ Program Name
+ Summary
+ {% if request.user.is_superuser %}
+ Action
+ {% endif %}
+
+
+
+ {% for program in programs %}
+
+ {{ forloop.counter }}.
+
+ {{ program.title}}
+ {{ program.summary }}
+ {% if request.user.is_superuser %}
+
+
+
+
+
+
+
+
+
+ Are you sure you want to delete this program?
+
+
+
+
+
+
+
+ {% endif %}
+
+ {% endfor %}
+
+
+
+
+{% endblock content %}
diff --git a/course/templates/course/program_single.html b/course/templates/course/program_single.html
new file mode 100644
index 0000000..95a6df0
--- /dev/null
+++ b/course/templates/course/program_single.html
@@ -0,0 +1,111 @@
+{% extends 'base.html' %}
+{% block title %} {{ title }} | DjangoSMS{% endblock title %}
+{% load static %}
+
+{% block content %}
+
+
+
+{% if request.user.is_superuser %}
+ Add Course
+{% endif %}
+
+
+{% if program %}
+ {{ program.title }}
+
+ {% if program.summary %}
+ {{ program.summary }}
+ {% endif %}
+{% endif %}
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
+
+
+
+
+
+
+
+ #
+ Course Name
+ Course Code
+ Cr.Hr
+ Level
+ Year
+ Semester
+ Current Semester
+ {% if request.user.is_superuser %}
+ Action
+ {% endif %}
+
+
+
+ {% for course in courses %}
+
+ {{ forloop.counter }}.
+
+ {{ course.title }}
+ {{ course.code }}
+ {{ course.credit }}
+ {{ course.level }}
+ {{ course.year }}
+ {{ course.semester }}
+ {% if course.is_current_semester == False %}
+ {% elif course.is_current_semester == True %}
+ {% endif %}
+
+ {% if request.user.is_superuser %}
+
+
+ {% endif %}
+
+ {% endfor %}
+
+
+
+
+
+{% if courses.paginator.page_range|length > 1 %}
+
+
+
+{% endif %}
+
+{% endblock content %}
diff --git a/course/templates/course/user_course_list.html b/course/templates/course/user_course_list.html
new file mode 100644
index 0000000..f6f8626
--- /dev/null
+++ b/course/templates/course/user_course_list.html
@@ -0,0 +1,136 @@
+{% extends 'base.html' %}
+{% block title %} My Courses | DjangoSMS{% endblock title %}
+{% load static %}
+
+{% block content %}
+
+
+
+{% if request.user.is_student %}
+ {{ student.department.title }}
+
+ {% if student.department.summary %}
+ {{ student.department.summary }}
+ {% endif %}
+{% endif %}
+
+{% if request.user.is_lecturer %}
+ My Courses
+
+{% endif %}
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+{% if request.user.is_student %}
+
+
Taken Courses:
+
+
+
+
+ #
+ Course Name
+ Course Code
+ Cr.Hr
+ Year
+ Semester
+ Current Semester
+ Taken
+
+
+
+ {% for course in taken_courses %}
+
+ {{ forloop.counter }}.
+
+ {{ course.course.title }}
+ {{ course.course.code }}
+ {{ course.course.credit }}
+ {{ course.course.year }}
+ {{ course.course.semester }}
+
+ {% if course.course.is_current_semester == False %}
+
+ {% else %}
+
+ {% endif %}
+
+
+ Taken
+
+
+ {% endfor %}
+
+
+
+
+{% endif %}
+
+
+
All Courses:
+
+
+
+
+ #
+ Course Name
+ Course Code
+ Cr.Hr
+ Year
+ Semester
+ Current Semester
+
+
+
+ {% for course in courses %}
+
+ {{ forloop.counter }}.
+
+ {{ course.title }}
+ {{ course.code }}
+ {{ course.credit }}
+ {{ course.year }}
+ {{ course.semester }}
+
+ {% if course.is_current_semester == False %}
+
+ {% else %}
+
+ {% endif %}
+
+
+ {% endfor %}
+
+
+
+
+
+{% if courses.paginator.page_range|length > 1 %}
+
+
+
+{% endif %}
+
+{% endblock content %}
diff --git a/course/templates/upload/upload_file_form.html b/course/templates/upload/upload_file_form.html
new file mode 100644
index 0000000..b01bfb2
--- /dev/null
+++ b/course/templates/upload/upload_file_form.html
@@ -0,0 +1,63 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+{% load crispy_forms_tags %}
+
+{% block content %}
+
+
+
+File upload for {{ course }}
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
+
+
+{% endblock content %}
diff --git a/course/templates/upload/upload_video_form.html b/course/templates/upload/upload_video_form.html
new file mode 100644
index 0000000..ed40276
--- /dev/null
+++ b/course/templates/upload/upload_video_form.html
@@ -0,0 +1,63 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+{% load crispy_forms_tags %}
+
+{% block content %}
+
+
+
+
Video upload for {{ course }}
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
+
+
+{% endblock content %}
diff --git a/course/templates/upload/video_single.html b/course/templates/upload/video_single.html
new file mode 100644
index 0000000..6ad9c97
--- /dev/null
+++ b/course/templates/upload/video_single.html
@@ -0,0 +1,33 @@
+{% extends 'base.html' %}
+{% block title %}{{ video.title }} | DjangoSMS{% endblock title %}
+{% load static %}
+
+{% block content %}
+
+
+
+
{{ video.title }}
+
+
+
Video tutorial for course {{ video.course }}
+
+
+
+
+
{{ video.timestamp|timesince }} ago
+
+
+
+
{{ video.summary }}
+
+{% endblock content %}
diff --git a/course/tests.py b/course/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/course/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/course/urls.py b/course/urls.py
new file mode 100644
index 0000000..5fe28e1
--- /dev/null
+++ b/course/urls.py
@@ -0,0 +1,53 @@
+from django.conf.urls import url
+from django.urls import path
+from django.contrib.auth.views import (
+ PasswordResetView, PasswordResetDoneView, PasswordResetConfirmView,
+ PasswordResetCompleteView, LoginView, LogoutView
+)
+# from .views import (
+# program_view, program_detail, program_add, program_edit, program_delete,
+# course_single, course_add, course_edit, course_delete,
+# CourseAllocationFormView, course_allocation_view, edit_allocated_course, deallocate_course,
+# handle_file_upload, handle_file_edit, handle_file_delete,
+# course_registration, course_drop, user_course_list
+# )
+from .views import *
+
+
+urlpatterns = [
+ # Program urls
+ url(r'^$', program_view, name='programs'),
+ url(r'^(?P
\d+)/detail/$', program_detail, name='program_detail'),
+ url(r'^add/$', program_add, name='add_program'),
+ url(r'^(?P\d+)/edit/$', program_edit, name='edit_program'),
+ url(r'^(?P\d+)/delete/$', program_delete, name='program_delete'),
+
+ # Course urls
+ url(r'^course/(?P[\w-]+)/detail/$', course_single, name='course_detail'),
+ url(r'^(?P\d+)/course/add/$', course_add, name='course_add'),
+ url(r'^course/(?P[\w-]+)/edit/$', course_edit, name='edit_course'),
+ url(r'^course/delete/(?P[\w-]+)/$', course_delete, name='delete_course'),
+
+ # CourseAllocation urls
+ url(r'^course/assign/$', CourseAllocationFormView.as_view(), name='course_allocation'),
+ url(r'^course/allocated/$', course_allocation_view, name='course_allocation_view'),
+ url(r'^allocated_course/(?P\d+)/edit/$', edit_allocated_course, name='edit_allocated_course'),
+ url(r'^course/(?P\d+)/deallocate/$', deallocate_course, name='course_deallocate'),
+
+ # File uploads urls
+ url(r'^course/(?P[\w-]+)/documentations/upload/$', handle_file_upload, name='upload_file_view'),
+ url(r'^course/(?P[\w-]+)/documentations/(?P\d+)/edit/$', handle_file_edit, name='upload_file_edit'),
+ url(r'^course/(?P[\w-]+)/documentations/(?P\d+)/delete/$', handle_file_delete, name='upload_file_delete'),
+
+ # Video uploads urls
+ url(r'^course/(?P[\w-]+)/video_tutorials/upload/$', handle_video_upload, name='upload_video'),
+ url(r'^course/(?P[\w-]+)/video_tutorials/(?P[\w-]+)/detail/$', handle_video_single, name='video_single'),
+ url(r'^course/(?P[\w-]+)/video_tutorials/(?P[\w-]+)/edit/$', handle_video_edit, name='upload_video_edit'),
+ url(r'^course/(?P[\w-]+)/video_tutorials/(?P[\w-]+)/delete/$', handle_video_delete, name='upload_video_delete'),
+
+ # course registration
+ url(r'^course/registration/$', course_registration, name='course_registration'),
+ url(r'^course/drop/$', course_drop, name='course_drop'),
+
+ url(r'^my_courses/$', user_course_list, name="user_course_list"),
+]
diff --git a/course/utils.py b/course/utils.py
new file mode 100644
index 0000000..33dce5e
--- /dev/null
+++ b/course/utils.py
@@ -0,0 +1,31 @@
+import datetime
+import os
+import random
+import string
+
+from django.utils.text import slugify
+
+
+def random_string_generator(size=10, chars=string.ascii_lowercase + string.digits):
+ return ''.join(random.choice(chars) for _ in range(size))
+
+
+def unique_slug_generator(instance, new_slug=None):
+ """
+ This is for a Django project and it assumes your instance
+ has a model with a slug field and a title character (char) field.
+ """
+ if new_slug is not None:
+ slug = new_slug
+ else:
+ slug = slugify(instance.title)
+
+ Klass = instance.__class__
+ qs_exists = Klass.objects.filter(slug=slug).exists()
+ if qs_exists:
+ new_slug = "{slug}-{randstr}".format(
+ slug=slug,
+ randstr=random_string_generator(size=4)
+ )
+ return unique_slug_generator(instance, new_slug=new_slug)
+ return slug
diff --git a/course/views.py b/course/views.py
new file mode 100644
index 0000000..c742a52
--- /dev/null
+++ b/course/views.py
@@ -0,0 +1,475 @@
+from django.shortcuts import render, redirect, get_object_or_404
+from django.contrib import messages
+from django.db.models import Sum, Avg, Max, Min, Count
+from django.contrib.auth.decorators import login_required
+from django.views.generic import CreateView
+from django.core.paginator import Paginator
+from django.conf import settings
+from django.utils.decorators import method_decorator
+from django.views.generic import ListView
+
+from accounts.models import User, Student
+from app.models import Session, Semester
+from result.models import TakenCourse
+from accounts.decorators import lecturer_required, student_required
+from .forms import (
+ ProgramForm, CourseAddForm, CourseAllocationForm,
+ EditCourseAllocationForm, UploadFormFile, UploadFormVideo
+)
+from .models import Program, Course, CourseAllocation, Upload, UploadVideo
+
+
+# ########################################################
+# Program views
+# ########################################################
+@login_required
+def program_view(request):
+ programs = Program.objects.all()
+
+ program_filter = request.GET.get('program_filter')
+ if program_filter:
+ programs = Program.objects.filter(title__icontains=program_filter)
+
+ return render(request, 'course/program_list.html', {
+ 'title': "Programs | DjangoSMS",
+ 'programs': programs,
+ })
+
+
+@login_required
+@lecturer_required
+def program_add(request):
+ if request.method == 'POST':
+ form = ProgramForm(request.POST)
+ if form.is_valid():
+ form.save()
+ messages.success(request, request.POST.get('title') + ' program has been created.')
+ return redirect('programs')
+ else:
+ messages.error(request, 'Correct the error(S) below.')
+ else:
+ form = ProgramForm()
+
+ return render(request, 'course/program_add.html', {
+ 'title': "Add Program | DjangoSMS",
+ 'form': form,
+ })
+
+
+@login_required
+def program_detail(request, pk):
+ program = Program.objects.get(pk=pk)
+ courses = Course.objects.filter(program_id=pk).order_by('-year')
+ credits = Course.objects.aggregate(Sum('credit'))
+
+ paginator = Paginator(courses, 10)
+ page = request.GET.get('page')
+
+ courses = paginator.get_page(page)
+
+ return render(request, 'course/program_single.html', {
+ 'title': program.title,
+ 'program': program, 'courses': courses, 'credits': credits
+ }, )
+
+
+@login_required
+@lecturer_required
+def program_edit(request, pk):
+ program = Program.objects.get(pk=pk)
+
+ if request.method == 'POST':
+ form = ProgramForm(request.POST, instance=program)
+ if form.is_valid():
+ form.save()
+ messages.success(request, str(request.POST.get('title')) + ' program has been updated.')
+ return redirect('programs')
+ else:
+ form = ProgramForm(instance=program)
+
+ return render(request, 'course/program_add.html', {
+ 'title': "Edit Program | DjangoSMS",
+ 'form': form
+ })
+
+
+@login_required
+@lecturer_required
+def program_delete(request, pk):
+ program = Program.objects.get(pk=pk)
+ title = program.title
+ program.delete()
+ messages.success(request, 'Program ' + title + ' has been deleted.')
+
+ return redirect('programs')
+# ########################################################
+
+# ########################################################
+# Course views
+# ########################################################
+@login_required
+def course_single(request, slug):
+ course = Course.objects.get(slug=slug)
+ files = Upload.objects.filter(course__slug=slug)
+ videos = UploadVideo.objects.filter(course__slug=slug)
+
+ # lecturers = User.objects.filter(allocated_lecturer__pk=course.id)
+ lecturers = CourseAllocation.objects.filter(courses__pk=course.id)
+
+ return render(request, 'course/course_single.html', {
+ 'title': course.title,
+ 'course': course,
+ 'files': files,
+ 'videos': videos,
+ 'lecturers': lecturers,
+ 'media_url': settings.MEDIA_ROOT,
+ }, )
+
+
+@login_required
+@lecturer_required
+def course_add(request, pk):
+ users = User.objects.all()
+ if request.method == 'POST':
+ form = CourseAddForm(request.POST)
+ course_name = request.POST.get('title')
+ course_code = request.POST.get('code')
+ if form.is_valid():
+ form.save()
+ messages.success(request, (course_name + '(' + course_code + ')' + ' has been created.'))
+ return redirect('program_detail', pk=request.POST.get('program'))
+ else:
+ messages.error(request, 'Correct the error(s) below.')
+ else:
+ form = CourseAddForm(initial={'program': Program.objects.get(pk=pk)})
+
+ return render(request, 'course/course_add.html', {
+ 'title': "Add Course | DjangoSMS",
+ 'form': form, 'program': pk, 'users': users
+ }, )
+
+
+@login_required
+@lecturer_required
+def course_edit(request, slug):
+ course = get_object_or_404(Course, slug=slug)
+ if request.method == 'POST':
+ form = CourseAddForm(request.POST, instance=course)
+ course_name = request.POST.get('title')
+ course_code = request.POST.get('code')
+ if form.is_valid():
+ form.save()
+ messages.success(request, (course_name + '(' + course_code + ')' + ' has been updated.'))
+ return redirect('program_detail', pk=request.POST.get('program'))
+ else:
+ messages.error(request, 'Correct the error(s) below.')
+ else:
+ form = CourseAddForm(instance=course)
+
+ return render(request, 'course/course_add.html', {
+ 'title': "Edit Course | DjangoSMS",
+ # 'form': form, 'program': pk, 'course': pk
+ 'form': form
+ }, )
+
+
+@login_required
+@lecturer_required
+def course_delete(request, slug):
+ course = Course.objects.get(slug=slug)
+ # course_name = course.title
+ course.delete()
+ messages.success(request, 'Course ' + course.title + ' has been deleted.')
+
+ return redirect('program_detail', pk=course.program.id)
+# ########################################################
+
+
+# ########################################################
+# Course Allocation
+# ########################################################
+@method_decorator([login_required], name='dispatch')
+class CourseAllocationFormView(CreateView):
+ form_class = CourseAllocationForm
+ template_name = 'course/course_allocation_form.html'
+
+ def get_form_kwargs(self):
+ kwargs = super(CourseAllocationFormView, self).get_form_kwargs()
+ kwargs['user'] = self.request.user
+ return kwargs
+
+ def form_valid(self, form):
+ # if a staff has been allocated a course before update it else create new
+ lecturer = form.cleaned_data['lecturer']
+ selected_courses = form.cleaned_data['courses']
+ courses = ()
+ for course in selected_courses:
+ courses += (course.pk,)
+ # print(courses)
+
+ try:
+ a = CourseAllocation.objects.get(lecturer=lecturer)
+ except:
+ a = CourseAllocation.objects.create(lecturer=lecturer)
+ for i in range(0, selected_courses.count()):
+ a.courses.add(courses[i])
+ a.save()
+ return redirect('course_allocation_view')
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context['title'] = "Assign Course | DjangoSMS"
+ return context
+
+
+@login_required
+def course_allocation_view(request):
+ allocated_courses = CourseAllocation.objects.all()
+ return render(request, 'course/course_allocation_view.html', {
+ 'title': "Course Allocations | DjangoSMS",
+ "allocated_courses": allocated_courses
+ })
+
+
+@login_required
+@lecturer_required
+def edit_allocated_course(request, pk):
+ allocated = get_object_or_404(CourseAllocation, pk=pk)
+ if request.method == 'POST':
+ form = EditCourseAllocationForm(request.POST, instance=allocated)
+ if form.is_valid():
+ form.save()
+ messages.success(request, 'course assigned has been updated.')
+ return redirect('course_allocation_view')
+ else:
+ form = EditCourseAllocationForm(instance=allocated)
+
+ return render(request, 'course/course_allocation_form.html', {
+ 'title': "Edit Course Allocated | DjangoSMS",
+ 'form': form, 'allocated': pk
+ }, )
+
+
+@login_required
+@lecturer_required
+def deallocate_course(request, pk):
+ course = CourseAllocation.objects.get(pk=pk)
+ course.delete()
+ messages.success(request, 'successfully deallocate!')
+ return redirect("course_allocation_view")
+# ########################################################
+
+
+# ########################################################
+# File Upload views
+# ########################################################
+@login_required
+@lecturer_required
+def handle_file_upload(request, slug):
+ course = Course.objects.get(slug=slug)
+ if request.method == 'POST':
+ form = UploadFormFile(request.POST, request.FILES, {'course': course})
+ # file_name = request.POST.get('name')
+ if form.is_valid():
+ form.save()
+ messages.success(request, (request.POST.get('title') + ' has been uploaded.'))
+ return redirect('course_detail', slug=slug)
+ else:
+ form = UploadFormFile()
+ return render(request, 'upload/upload_file_form.html', {
+ 'title': "File Upload | DjangoSMS",
+ 'form': form, 'course': course
+ })
+
+
+@login_required
+@lecturer_required
+def handle_file_edit(request, slug, file_id):
+ course = Course.objects.get(slug=slug)
+ instance = Upload.objects.get(pk=file_id)
+ if request.method == 'POST':
+ form = UploadFormFile(request.POST, request.FILES, instance=instance)
+ # file_name = request.POST.get('name')
+ if form.is_valid():
+ form.save()
+ messages.success(request, (request.POST.get('title') + ' has been updated.'))
+ return redirect('course_detail', slug=slug)
+ else:
+ form = UploadFormFile(instance=instance)
+
+ return render(request, 'upload/upload_file_form.html', {
+ 'title': instance.title,
+ 'form': form, 'course': course})
+
+
+def handle_file_delete(request, slug, file_id):
+ file = Upload.objects.get(pk=file_id)
+ # file_name = file.name
+ file.delete()
+
+ messages.success(request, (file.title + ' has been deleted.'))
+ return redirect('course_detail', slug=slug)
+
+# ########################################################
+# Video Upload views
+# ########################################################
+@login_required
+@lecturer_required
+def handle_video_upload(request, slug):
+ course = Course.objects.get(slug=slug)
+ if request.method == 'POST':
+ form = UploadFormVideo(request.POST, request.FILES, {'course': course})
+ if form.is_valid():
+ form.save()
+ messages.success(request, (request.POST.get('title') + ' has been uploaded.'))
+ return redirect('course_detail', slug=slug)
+ else:
+ form = UploadFormVideo()
+ return render(request, 'upload/upload_video_form.html', {
+ 'title': "Video Upload | DjangoSMS",
+ 'form': form, 'course': course
+ })
+
+
+@login_required
+# @lecturer_required
+def handle_video_single(request, slug, video_slug):
+ course = get_object_or_404(Course, slug=slug)
+ video = get_object_or_404(UploadVideo, slug=video_slug)
+ return render(request, 'upload/video_single.html', {'video': video})
+
+
+@login_required
+@lecturer_required
+def handle_video_edit(request, slug, video_slug):
+ course = Course.objects.get(slug=slug)
+ instance = UploadVideo.objects.get(slug=video_slug)
+ if request.method == 'POST':
+ form = UploadFormVideo(request.POST, request.FILES, instance=instance)
+ if form.is_valid():
+ form.save()
+ messages.success(request, (request.POST.get('title') + ' has been updated.'))
+ return redirect('course_detail', slug=slug)
+ else:
+ form = UploadFormVideo(instance=instance)
+
+ return render(request, 'upload/upload_video_form.html', {
+ 'title': instance.title,
+ 'form': form, 'course': course})
+
+
+def handle_video_delete(request, slug, video_slug):
+ video = get_object_or_404(UploadVideo, slug=video_slug)
+ # video = UploadVideo.objects.get(slug=video_slug)
+ video.delete()
+
+ messages.success(request, (video.title + ' has been deleted.'))
+ return redirect('course_detail', slug=slug)
+# ########################################################
+
+
+# ########################################################
+# Course Registration
+# ########################################################
+@login_required
+@student_required
+def course_registration(request):
+ if request.method == 'POST':
+ ids = ()
+ data = request.POST.copy()
+ data.pop('csrfmiddlewaretoken', None) # remove csrf_token
+ for key in data.keys():
+ ids = ids + (str(key),)
+ for s in range(0, len(ids)):
+ student = Student.objects.get(student__pk=request.user.id)
+ course = Course.objects.get(pk=ids[s])
+ obj = TakenCourse.objects.create(student=student, course=course)
+ obj.save()
+ messages.success(request, 'Courses Registered Successfully!')
+ return redirect('course_registration')
+ else:
+ # student = Student.objects.get(student__pk=request.user.id)
+ student = get_object_or_404(Student, student__id=request.user.id)
+ taken_courses = TakenCourse.objects.filter(student__student__id=request.user.id)
+ t = ()
+ for i in taken_courses:
+ t += (i.course.pk,)
+ current_semester = Semester.objects.get(is_current_semester=True)
+
+ courses = Course.objects.filter(program__pk=student.department.id, level=student.level, semester=current_semester
+ ).exclude(id__in=t).order_by('year')
+ all_courses = Course.objects.filter(level=student.level, program__pk=student.department.id)
+
+ no_course_is_registered = False # Check if no course is registered
+ all_courses_are_registered = False
+
+ registered_courses = Course.objects.filter(level=student.level).filter(id__in=t)
+ if registered_courses.count() == 0: # Check if number of registered courses is 0
+ no_course_is_registered = True
+
+ if registered_courses.count() == all_courses.count():
+ all_courses_are_registered = True
+
+ total_first_semester_credit = 0
+ total_sec_semester_credit = 0
+ total_registered_credit = 0
+ for i in courses:
+ if i.semester == "First":
+ total_first_semester_credit += int(i.credit)
+ if i.semester == "Second":
+ total_sec_semester_credit += int(i.credit)
+ for i in registered_courses:
+ total_registered_credit += int(i.credit)
+ context = {
+ "all_courses_are_registered": all_courses_are_registered,
+ "no_course_is_registered": no_course_is_registered,
+ "current_semester": current_semester,
+ "courses": courses,
+ "total_first_semester_credit": total_first_semester_credit,
+ "total_sec_semester_credit": total_sec_semester_credit,
+ "registered_courses": registered_courses,
+ "total_registered_credit": total_registered_credit,
+ "student": student,
+ }
+ return render(request, 'course/course_registration.html', context)
+
+
+@login_required
+@student_required
+def course_drop(request):
+ if request.method == 'POST':
+ ids = ()
+ data = request.POST.copy()
+ data.pop('csrfmiddlewaretoken', None) # remove csrf_token
+ for key in data.keys():
+ ids = ids + (str(key),)
+ for s in range(0, len(ids)):
+ student = Student.objects.get(student__pk=request.user.id)
+ course = Course.objects.get(pk=ids[s])
+ obj = TakenCourse.objects.get(student=student, course=course)
+ obj.delete()
+ messages.success(request, 'Successfully Dropped!')
+ return redirect('course_registration')
+# ########################################################
+
+
+@login_required
+def user_course_list(request):
+ if request.user.is_lecturer:
+ courses = Course.objects.filter(allocated_course__lecturer__pk=request.user.id)
+
+ return render(request, 'course/user_course_list.html', {'courses': courses})
+
+ elif request.user.is_student:
+ student = Student.objects.get(student__pk=request.user.id)
+ taken_courses = TakenCourse.objects.filter(student__student__id=student.student.id)
+ courses = Course.objects.filter(level=student.level).filter(program__pk=student.department.id)
+
+ return render(request, 'course/user_course_list.html', {
+ 'student': student,
+ 'taken_courses': taken_courses,
+ 'courses': courses
+ })
+
+ else:
+ return render(request, 'course/user_course_list.html')
diff --git a/manage.py b/manage.py
new file mode 100644
index 0000000..be3c524
--- /dev/null
+++ b/manage.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'SMS.settings')
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/quiz/__init__.py b/quiz/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/quiz/admin.py b/quiz/admin.py
new file mode 100644
index 0000000..ee1ddcc
--- /dev/null
+++ b/quiz/admin.py
@@ -0,0 +1,74 @@
+from django import forms
+from django.contrib import admin
+from django.contrib.admin.widgets import FilteredSelectMultiple
+from django.utils.translation import ugettext_lazy as _
+
+from .models import Quiz, Progress, Question, MCQuestion, Choice, Essay_Question, Sitting
+
+
+class ChoiceInline(admin.TabularInline):
+ model = Choice
+
+
+class QuizAdminForm(forms.ModelForm):
+
+ class Meta:
+ model = Quiz
+ exclude = []
+
+ questions = forms.ModelMultipleChoiceField(
+ queryset=Question.objects.all().select_subclasses(),
+ required=False,
+ label=_("Questions"),
+ widget=FilteredSelectMultiple(
+ verbose_name=_("Questions"),
+ is_stacked=False))
+
+ def __init__(self, *args, **kwargs):
+ super(QuizAdminForm, self).__init__(*args, **kwargs)
+ if self.instance.pk:
+ self.fields['questions'].initial = self.instance.question_set.all().select_subclasses()
+
+ def save(self, commit=True):
+ quiz = super(QuizAdminForm, self).save(commit=False)
+ quiz.save()
+ quiz.question_set.set(self.cleaned_data['questions'])
+ self.save_m2m()
+ return quiz
+
+
+class QuizAdmin(admin.ModelAdmin):
+ form = QuizAdminForm
+
+ list_display = ('title', )
+ # list_filter = ('category',)
+ search_fields = ('description', 'category', )
+
+
+class MCQuestionAdmin(admin.ModelAdmin):
+ list_display = ('content', )
+ # list_filter = ('category',)
+ fields = ('content', 'figure', 'quiz', 'explanation', 'choice_order')
+
+ search_fields = ('content', 'explanation')
+ filter_horizontal = ('quiz',)
+
+ inlines = [ChoiceInline]
+
+
+class ProgressAdmin(admin.ModelAdmin):
+ search_fields = ('user', 'score', )
+
+
+class EssayQuestionAdmin(admin.ModelAdmin):
+ list_display = ('content', )
+ # list_filter = ('category',)
+ fields = ('content', 'quiz', 'explanation', )
+ search_fields = ('content', 'explanation')
+ filter_horizontal = ('quiz',)
+
+admin.site.register(Quiz, QuizAdmin)
+admin.site.register(MCQuestion, MCQuestionAdmin)
+admin.site.register(Progress, ProgressAdmin)
+admin.site.register(Essay_Question, EssayQuestionAdmin)
+admin.site.register(Sitting)
diff --git a/quiz/apps.py b/quiz/apps.py
new file mode 100644
index 0000000..eaa49da
--- /dev/null
+++ b/quiz/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class QuizConfig(AppConfig):
+ name = 'quiz'
diff --git a/quiz/forms.py b/quiz/forms.py
new file mode 100644
index 0000000..8f23e6f
--- /dev/null
+++ b/quiz/forms.py
@@ -0,0 +1,63 @@
+from django import forms
+from django.forms.widgets import RadioSelect, Textarea
+from django.contrib.admin.widgets import FilteredSelectMultiple
+from django.utils.translation import ugettext_lazy as _
+from django.db import transaction
+
+from django.forms.models import inlineformset_factory
+
+from accounts.models import User
+from .models import Question, Quiz, MCQuestion, Choice
+
+
+class QuestionForm(forms.Form):
+ def __init__(self, question, *args, **kwargs):
+ super(QuestionForm, self).__init__(*args, **kwargs)
+ choice_list = [x for x in question.get_choices_list()]
+ self.fields["answers"] = forms.ChoiceField(choices=choice_list, widget=RadioSelect)
+
+
+class EssayForm(forms.Form):
+ def __init__(self, question, *args, **kwargs):
+ super(EssayForm, self).__init__(*args, **kwargs)
+ self.fields["answers"] = forms.CharField(
+ widget=Textarea(attrs={'style': 'width:100%'}))
+
+
+class QuizAddForm(forms.ModelForm):
+
+ class Meta:
+ model = Quiz
+ exclude = []
+
+ questions = forms.ModelMultipleChoiceField(
+ queryset=Question.objects.all().select_subclasses(),
+ required=False,
+ label=_("Questions"),
+ widget=FilteredSelectMultiple(
+ verbose_name=_("Questions"),
+ is_stacked=False))
+
+ def __init__(self, *args, **kwargs):
+ super(QuizAddForm, self).__init__(*args, **kwargs)
+ if self.instance.pk:
+ self.fields['questions'].initial = self.instance.question_set.all().select_subclasses()
+
+ def save(self, commit=True):
+ quiz = super(QuizAddForm, self).save(commit=False)
+ quiz.save()
+ quiz.question_set.set(self.cleaned_data['questions'])
+ self.save_m2m()
+ return quiz
+
+
+class MCQuestionForm(forms.ModelForm):
+
+ class Meta:
+ model = MCQuestion
+ exclude = ()
+
+
+MCQuestionFormSet = inlineformset_factory(
+ MCQuestion, Choice, form=MCQuestionForm, fields=['choice', 'correct'], can_delete=True, extra=5
+)
diff --git a/quiz/migrations/0001_initial.py b/quiz/migrations/0001_initial.py
new file mode 100644
index 0000000..2db3dc3
--- /dev/null
+++ b/quiz/migrations/0001_initial.py
@@ -0,0 +1,128 @@
+# Generated by Django 2.2.3 on 2020-07-29 15:25
+
+from django.conf import settings
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+import re
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('course', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Question',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('figure', models.ImageField(blank=True, null=True, upload_to='uploads/%Y/%m/%d', verbose_name='Figure')),
+ ('content', models.CharField(help_text='Enter the question text that you want displayed', max_length=1000, verbose_name='Question')),
+ ('explanation', models.TextField(blank=True, help_text='Explanation to be shown after the question has been answered.', max_length=2000, verbose_name='Explanation')),
+ ],
+ options={
+ 'verbose_name': 'Question',
+ 'verbose_name_plural': 'Questions',
+ },
+ ),
+ migrations.CreateModel(
+ name='Quiz',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=60, verbose_name='Title')),
+ ('slug', models.SlugField(blank=True, unique=True)),
+ ('description', models.TextField(blank=True, help_text='a description of the quiz', verbose_name='Description')),
+ ('category', models.TextField(blank=True, choices=[('assignment', 'Assignment'), ('exam', 'Exam'), ('practice', 'Practice Quiz')])),
+ ('random_order', models.BooleanField(default=False, help_text='Display the questions in a random order or as they are set?', verbose_name='Random Order')),
+ ('answers_at_end', models.BooleanField(default=False, help_text='Correct answer is NOT shown after question. Answers displayed at the end.', verbose_name='Answers at end')),
+ ('exam_paper', models.BooleanField(default=False, help_text='If yes, the result of each attempt by a user will be stored. Necessary for marking.', verbose_name='Exam Paper')),
+ ('single_attempt', models.BooleanField(default=False, help_text='If yes, only one attempt by a user will be permitted.', verbose_name='Single Attempt')),
+ ('pass_mark', models.SmallIntegerField(blank=True, default=50, help_text='Percentage required to pass exam.', validators=[django.core.validators.MaxValueValidator(100)], verbose_name='Pass Mark')),
+ ('draft', models.BooleanField(blank=True, default=False, help_text='If yes, the quiz is not displayed in the quiz list and can only be taken by users who can edit quizzes.', verbose_name='Draft')),
+ ('timestamp', models.DateTimeField(auto_now=True)),
+ ('course', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='course.Course')),
+ ],
+ options={
+ 'verbose_name': 'Quiz',
+ 'verbose_name_plural': 'Quizzes',
+ },
+ ),
+ migrations.CreateModel(
+ name='Essay_Question',
+ fields=[
+ ('question_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='quiz.Question')),
+ ],
+ options={
+ 'verbose_name': 'Essay style question',
+ 'verbose_name_plural': 'Essay style questions',
+ },
+ bases=('quiz.question',),
+ ),
+ migrations.CreateModel(
+ name='MCQuestion',
+ fields=[
+ ('question_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='quiz.Question')),
+ ('choice_order', models.CharField(blank=True, choices=[('content', 'Content'), ('random', 'Random'), ('none', 'None')], help_text='The order in which multichoice choice options are displayed to the user', max_length=30, null=True, verbose_name='Choice Order')),
+ ],
+ options={
+ 'verbose_name': 'Multiple Choice Question',
+ 'verbose_name_plural': 'Multiple Choice Questions',
+ },
+ bases=('quiz.question',),
+ ),
+ migrations.CreateModel(
+ name='Sitting',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('question_order', models.CharField(max_length=1024, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:,\\d+)*\\Z'), code='invalid', message='Enter only digits separated by commas.')], verbose_name='Question Order')),
+ ('question_list', models.CharField(max_length=1024, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:,\\d+)*\\Z'), code='invalid', message='Enter only digits separated by commas.')], verbose_name='Question List')),
+ ('incorrect_questions', models.CharField(blank=True, max_length=1024, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:,\\d+)*\\Z'), code='invalid', message='Enter only digits separated by commas.')], verbose_name='Incorrect questions')),
+ ('current_score', models.IntegerField(verbose_name='Current Score')),
+ ('complete', models.BooleanField(default=False, verbose_name='Complete')),
+ ('user_answers', models.TextField(blank=True, default='{}', verbose_name='User Answers')),
+ ('start', models.DateTimeField(auto_now_add=True, verbose_name='Start')),
+ ('end', models.DateTimeField(blank=True, null=True, verbose_name='End')),
+ ('course', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='course.Course', verbose_name='Course')),
+ ('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='quiz.Quiz', verbose_name='Quiz')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
+ ],
+ options={
+ 'permissions': (('view_sittings', 'Can see completed exams.'),),
+ },
+ ),
+ migrations.AddField(
+ model_name='question',
+ name='quiz',
+ field=models.ManyToManyField(blank=True, to='quiz.Quiz', verbose_name='Quiz'),
+ ),
+ migrations.CreateModel(
+ name='Progress',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('score', models.CharField(max_length=1024, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:,\\d+)*\\Z'), code='invalid', message='Enter only digits separated by commas.')], verbose_name='Score')),
+ ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
+ ],
+ options={
+ 'verbose_name': 'User Progress',
+ 'verbose_name_plural': 'User progress records',
+ },
+ ),
+ migrations.CreateModel(
+ name='Choice',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('choice', models.CharField(help_text='Enter the choice text that you want displayed', max_length=1000, verbose_name='Content')),
+ ('correct', models.BooleanField(default=False, help_text='Is this a correct answer?', verbose_name='Correct')),
+ ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='quiz.MCQuestion', verbose_name='Question')),
+ ],
+ options={
+ 'verbose_name': 'Choice',
+ 'verbose_name_plural': 'Choices',
+ },
+ ),
+ ]
diff --git a/quiz/migrations/__init__.py b/quiz/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/quiz/models.py b/quiz/models.py
new file mode 100644
index 0000000..ecc4e1c
--- /dev/null
+++ b/quiz/models.py
@@ -0,0 +1,446 @@
+import re
+import json
+
+from django.db import models
+from django.urls import reverse
+from django.core.exceptions import ValidationError, ImproperlyConfigured
+from django.core.validators import (MaxValueValidator, validate_comma_separated_integer_list,)
+from django.utils.translation import ugettext_lazy as _
+from django.utils.timezone import now
+from django.conf import settings
+from django.db.models.signals import pre_save
+
+from django.db.models import Q
+
+from model_utils.managers import InheritanceManager
+from course.models import Course
+from .utils import *
+
+CHOICE_ORDER_OPTIONS = (
+ ('content', _('Content')),
+ ('random', _('Random')),
+ ('none', _('None'))
+)
+
+CATEGORY_OPTIONS = (
+ ('assignment', _('Assignment')),
+ ('exam', _('Exam')),
+ ('practice', _('Practice Quiz'))
+)
+
+
+class QuizManager(models.Manager):
+ def search(self, query=None):
+ qs = self.get_queryset()
+ if query is not None:
+ or_lookup = (Q(title__icontains=query) |
+ Q(description__icontains=query)|
+ Q(category__icontains=query)|
+ Q(slug__icontains=query)
+ )
+ qs = qs.filter(or_lookup).distinct() # distinct() is often necessary with Q lookups
+ return qs
+
+
+class Quiz(models.Model):
+ course = models.ForeignKey(Course, on_delete=models.CASCADE, null=True)
+ title = models.CharField(verbose_name=_("Title"), max_length=60, blank=False)
+ slug = models.SlugField(blank=True, unique=True)
+ description = models.TextField(verbose_name=_("Description"), blank=True, help_text=_("a description of the quiz"))
+ category = models.TextField(choices=CATEGORY_OPTIONS, blank=True)
+ random_order = models.BooleanField(blank=False, default=False, verbose_name=_("Random Order"),
+ help_text=_("Display the questions in a random order or as they are set?"))
+
+ # max_questions = models.PositiveIntegerField(blank=True, null=True, verbose_name=_("Max Questions"),
+ # help_text=_("Number of questions to be answered on each attempt."))
+
+ answers_at_end = models.BooleanField(blank=False, default=False, verbose_name=_("Answers at end"),
+ help_text=_("Correct answer is NOT shown after question. Answers displayed at the end."))
+
+ exam_paper = models.BooleanField(blank=False, default=False, verbose_name=_("Exam Paper"),
+ help_text=_("If yes, the result of each attempt by a user will be stored. Necessary for marking."))
+
+ single_attempt = models.BooleanField(blank=False, default=False, verbose_name=_("Single Attempt"),
+ help_text=_("If yes, only one attempt by a user will be permitted."))
+
+ pass_mark = models.SmallIntegerField(blank=True, default=50, verbose_name=_("Pass Mark"), validators=[MaxValueValidator(100)],
+ help_text=_("Percentage required to pass exam."))
+
+ draft = models.BooleanField(blank=True, default=False, verbose_name=_("Draft"),
+ help_text=_("If yes, the quiz is not displayed in the quiz list and can only be taken by users who can edit quizzes."))
+
+ timestamp = models.DateTimeField(auto_now=True)
+
+ objects = QuizManager()
+
+ def save(self, force_insert=False, force_update=False, *args, **kwargs):
+
+ if self.single_attempt is True:
+ self.exam_paper = True
+
+ if self.pass_mark > 100:
+ raise ValidationError('%s is above 100' % self.pass_mark)
+ if self.pass_mark < 0:
+ raise ValidationError('%s is below 0' % self.pass_mark)
+
+ super(Quiz, self).save(force_insert, force_update, *args, **kwargs)
+
+ class Meta:
+ verbose_name = _("Quiz")
+ verbose_name_plural = _("Quizzes")
+
+ def __str__(self):
+ return self.title
+
+ def get_questions(self):
+ return self.question_set.all().select_subclasses()
+
+ @property
+ def get_max_score(self):
+ return self.get_questions().count()
+
+ def get_absolute_url(self):
+ # return reverse('quiz_start_page', kwargs={'pk': self.pk})
+ return reverse('quiz_index', kwargs={'slug': self.course.slug})
+
+
+def quiz_pre_save_receiver(sender, instance, *args, **kwargs):
+ if not instance.slug:
+ instance.slug = unique_slug_generator(instance)
+
+pre_save.connect(quiz_pre_save_receiver, sender=Quiz)
+
+
+class ProgressManager(models.Manager):
+
+ def new_progress(self, user):
+ new_progress = self.create(user=user, score="")
+ new_progress.save()
+ return new_progress
+
+
+class Progress(models.Model):
+ user = models.OneToOneField(settings.AUTH_USER_MODEL, verbose_name=_("User"), on_delete=models.CASCADE)
+ score = models.CharField(max_length=1024, verbose_name=_("Score"), validators=[validate_comma_separated_integer_list])
+
+ objects = ProgressManager()
+
+ class Meta:
+ verbose_name = _("User Progress")
+ verbose_name_plural = _("User progress records")
+
+ # @property
+ def list_all_cat_scores(self):
+ score_before = self.score
+ output = {}
+
+ if len(self.score) > len(score_before):
+ # If a new category has been added, save changes.
+ self.save()
+
+ return output
+
+ def update_score(self, question, score_to_add=0, possible_to_add=0):
+ # category_test = Category.objects.filter(category=question.category).exists()
+
+ if any([item is False for item in [score_to_add, possible_to_add, isinstance(score_to_add, int), isinstance(possible_to_add, int)]]):
+ return _("error"), _("category does not exist or invalid score")
+
+ to_find = re.escape(str(question.quiz)) + r",(?P\d+),(?P\d+),"
+
+ match = re.search(to_find, self.score, re.IGNORECASE)
+
+ if match:
+ updated_score = int(match.group('score')) + abs(score_to_add)
+ updated_possible = int(match.group('possible')) + abs(possible_to_add)
+
+ new_score = ",".join([str(question.quiz), str(updated_score), str(updated_possible), ""])
+
+ # swap old score for the new one
+ self.score = self.score.replace(match.group(), new_score)
+ self.save()
+
+ else:
+ # if not present but existing, add with the points passed in
+ self.score += ",".join([str(question.quiz), str(score_to_add), str(possible_to_add), ""])
+ self.save()
+
+ def show_exams(self):
+ if self.user.is_superuser:
+ return Sitting.objects.filter(complete=True).order_by('-end')
+ else:
+ return Sitting.objects.filter(user=self.user, complete=True).order_by('-end')
+
+
+class SittingManager(models.Manager):
+
+ def new_sitting(self, user, quiz, course):
+ if quiz.random_order is True:
+ question_set = quiz.question_set.all().select_subclasses().order_by('?')
+ else:
+ question_set = quiz.question_set.all().select_subclasses()
+
+ question_set = [item.id for item in question_set]
+
+ if len(question_set) == 0:
+ raise ImproperlyConfigured('Question set of the quiz is empty. Please configure questions properly')
+
+ # if quiz.max_questions and quiz.max_questions < len(question_set):
+ # question_set = question_set[:quiz.max_questions]
+
+ questions = ",".join(map(str, question_set)) + ","
+
+ new_sitting = self.create(
+ user=user, quiz=quiz, course=course, question_order=questions,
+ question_list=questions, incorrect_questions="",
+ current_score=0,
+ complete=False,
+ user_answers='{}'
+ )
+ return new_sitting
+
+ def user_sitting(self, user, quiz, course):
+ if quiz.single_attempt is True and self.filter(user=user, quiz=quiz, course=course, complete=True).exists():
+ return False
+ try:
+ sitting = self.get(user=user, quiz=quiz, course=course, complete=False)
+ except Sitting.DoesNotExist:
+ sitting = self.new_sitting(user, quiz, course)
+ except Sitting.MultipleObjectsReturned:
+ sitting = self.filter(user=user, quiz=quiz, course=course, complete=False)[0]
+ return sitting
+
+
+class Sitting(models.Model):
+ user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_("User"), on_delete=models.CASCADE)
+ quiz = models.ForeignKey(Quiz, verbose_name=_("Quiz"), on_delete=models.CASCADE)
+ course = models.ForeignKey(Course, null=True, verbose_name=_("Course"), on_delete=models.CASCADE)
+
+ question_order = models.CharField(max_length=1024, verbose_name=_("Question Order"),
+ validators=[validate_comma_separated_integer_list])
+
+ question_list = models.CharField(max_length=1024, verbose_name=_("Question List"),
+ validators=[validate_comma_separated_integer_list])
+
+ incorrect_questions = models.CharField(max_length=1024, blank=True, verbose_name=_("Incorrect questions"),
+ validators=[validate_comma_separated_integer_list])
+
+ current_score = models.IntegerField(verbose_name=_("Current Score"))
+ complete = models.BooleanField(default=False, blank=False, verbose_name=_("Complete"))
+ user_answers = models.TextField(blank=True, default='{}', verbose_name=_("User Answers"))
+ start = models.DateTimeField(auto_now_add=True, verbose_name=_("Start"))
+ end = models.DateTimeField(null=True, blank=True, verbose_name=_("End"))
+
+ objects = SittingManager()
+
+ class Meta:
+ permissions = (("view_sittings", _("Can see completed exams.")),)
+
+ def get_first_question(self):
+ if not self.question_list:
+ return False
+
+ first, _ = self.question_list.split(',', 1)
+ question_id = int(first)
+ return Question.objects.get_subclass(id=question_id)
+
+ def remove_first_question(self):
+ if not self.question_list:
+ return
+
+ _, others = self.question_list.split(',', 1)
+ self.question_list = others
+ self.save()
+
+ def add_to_score(self, points):
+ self.current_score += int(points)
+ self.save()
+
+ @property
+ def get_current_score(self):
+ return self.current_score
+
+ def _question_ids(self):
+ return [int(n) for n in self.question_order.split(',') if n]
+
+ @property
+ def get_percent_correct(self):
+ dividend = float(self.current_score)
+ divisor = len(self._question_ids())
+ if divisor < 1:
+ return 0 # prevent divide by zero error
+
+ if dividend > divisor:
+ return 100
+
+ correct = int(round((dividend / divisor) * 100))
+
+ if correct >= 1:
+ return correct
+ else:
+ return 0
+
+ def mark_quiz_complete(self):
+ self.complete = True
+ self.end = now()
+ self.save()
+
+ def add_incorrect_question(self, question):
+ if len(self.incorrect_questions) > 0:
+ self.incorrect_questions += ','
+ self.incorrect_questions += str(question.id) + ","
+ if self.complete:
+ self.add_to_score(-1)
+ self.save()
+
+ @property
+ def get_incorrect_questions(self):
+ return [int(q) for q in self.incorrect_questions.split(',') if q]
+
+ def remove_incorrect_question(self, question):
+ current = self.get_incorrect_questions
+ current.remove(question.id)
+ self.incorrect_questions = ','.join(map(str, current))
+ self.add_to_score(1)
+ self.save()
+
+ @property
+ def check_if_passed(self):
+ return self.get_percent_correct >= self.quiz.pass_mark
+
+ @property
+ def result_message(self):
+ if self.check_if_passed:
+ return f"You have passed this quiz, congratulation"
+ else:
+ return f"You failed this quiz, don't give up! try until you have passed."
+
+ def add_user_answer(self, question, guess):
+ current = json.loads(self.user_answers)
+ current[question.id] = guess
+ self.user_answers = json.dumps(current)
+ self.save()
+
+ def get_questions(self, with_answers=False):
+ question_ids = self._question_ids()
+ questions = sorted(self.quiz.question_set.filter(id__in=question_ids).select_subclasses(), key=lambda q: question_ids.index(q.id))
+
+ if with_answers:
+ user_answers = json.loads(self.user_answers)
+ for question in questions:
+ question.user_answer = user_answers[str(question.id)]
+
+ return questions
+
+ @property
+ def questions_with_user_answers(self):
+ return {q: q.user_answer for q in self.get_questions(with_answers=True)}
+
+ @property
+ def get_max_score(self):
+ return len(self._question_ids())
+
+ def progress(self):
+ answered = len(json.loads(self.user_answers))
+ total = self.get_max_score
+ return answered, total
+
+
+class Question(models.Model):
+ quiz = models.ManyToManyField(Quiz, verbose_name=_("Quiz"), blank=True)
+ figure = models.ImageField(upload_to='uploads/%Y/%m/%d', blank=True, null=True, verbose_name=_("Figure"))
+ content = models.CharField(max_length=1000, blank=False,
+ help_text=_("Enter the question text that you want displayed"), verbose_name=_('Question'))
+ explanation = models.TextField(max_length=2000, blank=True,
+ help_text=_("Explanation to be shown after the question has been answered."),
+ verbose_name=_('Explanation'))
+
+ objects = InheritanceManager()
+
+ class Meta:
+ verbose_name = _("Question")
+ verbose_name_plural = _("Questions")
+
+ def __str__(self):
+ return self.content
+
+
+class MCQuestion(Question):
+
+ choice_order = models.CharField(
+ max_length=30, null=True, blank=True,
+ choices=CHOICE_ORDER_OPTIONS,
+ help_text=_("The order in which multichoice choice options are displayed to the user"),
+ verbose_name=_("Choice Order"))
+
+ def check_if_correct(self, guess):
+ answer = Choice.objects.get(id=guess)
+
+ if answer.correct is True:
+ return True
+ else:
+ return False
+
+ def order_choices(self, queryset):
+ if self.choice_order == 'content':
+ return queryset.order_by('choice')
+ if self.choice_order == 'random':
+ return queryset.order_by('?')
+ if self.choice_order == 'none':
+ return queryset.order_by()
+ return queryset
+
+ def get_choices(self):
+ return self.order_choices(Choice.objects.filter(question=self))
+
+ def get_choices_list(self):
+ return [(choice.id, choice.choice) for choice in
+ self.order_choices(Choice.objects.filter(question=self))]
+
+ def answer_choice_to_string(self, guess):
+ return Choice.objects.get(id=guess).choice
+
+ class Meta:
+ verbose_name = _("Multiple Choice Question")
+ verbose_name_plural = _("Multiple Choice Questions")
+
+
+class Choice(models.Model):
+ question = models.ForeignKey(MCQuestion, verbose_name=_("Question"), on_delete=models.CASCADE)
+
+ choice = models.CharField(max_length=1000, blank=False,
+ help_text=_("Enter the choice text that you want displayed"),
+ verbose_name=_("Content"))
+
+ correct = models.BooleanField(blank=False, default=False,
+ help_text=_("Is this a correct answer?"),
+ verbose_name=_("Correct"))
+
+ def __str__(self):
+ return self.choice
+
+ class Meta:
+ verbose_name = _("Choice")
+ verbose_name_plural = _("Choices")
+
+
+class Essay_Question(Question):
+
+ def check_if_correct(self, guess):
+ return False
+
+ def get_answers(self):
+ return False
+
+ def get_answers_list(self):
+ return False
+
+ def answer_choice_to_string(self, guess):
+ return str(guess)
+
+ def __str__(self):
+ return self.content
+
+ class Meta:
+ verbose_name = _("Essay style question")
+ verbose_name_plural = _("Essay style questions")
diff --git a/quiz/templates/correct_answer.html b/quiz/templates/correct_answer.html
new file mode 100644
index 0000000..6310b0a
--- /dev/null
+++ b/quiz/templates/correct_answer.html
@@ -0,0 +1,32 @@
+{% load i18n %}
+{% if previous.answers %}
+
+ {% if user_was_incorrect %}
+
+ {% trans "You answered the above question incorrectly" %}
+
+ {% endif %}
+
+
+
+ {% for answer in previous.answers %}
+ {% if answer.correct %}
+
+ {{ answer }}
+ {% trans "This is the correct answer" %}
+ {% else %}
+
+ {{ answer }}
+
+ {% if previous.question_type.MCQuestion %}
+ {% if answer.id|add:"0" == previous.previous_answer|add:"0" %}
+ {% trans "This was your answer." %}
+ {% endif %}
+ {% endif %}
+
+ {% endif %}
+
+ {% endfor %}
+
+
+{% endif %}
diff --git a/quiz/templates/progress.html b/quiz/templates/progress.html
new file mode 100644
index 0000000..3e675d4
--- /dev/null
+++ b/quiz/templates/progress.html
@@ -0,0 +1,92 @@
+{% extends "base.html" %}
+{% load i18n %}
+
+{% block title %} {% trans "Progress Page" %} {% endblock %}
+{% block description %} {% trans "User Progress Page" %} {% endblock %}
+
+{% block content %}
+
+
+
+ {% if cat_scores %}
+
+
+
+
+
+
+
+
+ {% trans "Category" %}
+ {% trans "Correctly answererd" %}
+ {% trans "Incorrect" %}
+ %
+
+
+
+
+
+
+ {% for cat, value in cat_scores.items %}
+
+ {{ cat }}
+ {{ value.0 }}
+ {{ value.1 }}
+ {{ value.2 }}
+
+
+ {% endfor %}
+
+
+
+
+
+
+ {% endif %}
+
+ {% if exams %}
+
+
+
+
+
+ {% trans "Below are the results of exams that you have sat." %}
+
+ Total complete exams: {{ exams_counter }}
+
+
+
+
+
+ #
+ {% trans "Quiz Title" %}
+ {% trans "Score" %}
+ {% trans "Possible Score" %}
+ Out of 100%
+
+
+
+
+
+ {% for exam in exams %}
+
+
+ {{ forloop.counter }}
+ {{ exam.quiz.title }}
+ {{ exam.current_score }}
+ {{ exam.get_max_score }}
+ {{ exam.get_percent_correct }}%
+
+
+ {% endfor %}
+
+
+
+
+
+ {% endif %}
+ {% if not cat_scores and not exams %}
+
No recordes yet. to get a good recorde, try to do some quizzes in your course.
+ {% endif %}
+
+{% endblock %}
diff --git a/quiz/templates/question.html b/quiz/templates/question.html
new file mode 100644
index 0000000..ad1b67e
--- /dev/null
+++ b/quiz/templates/question.html
@@ -0,0 +1,169 @@
+{% extends "base.html" %}
+{% load i18n%}
+
+
+{% block title %} {{ quiz.title }} {% endblock %}
+{% block description %} {{ quiz.title }} - {{ quiz.description }} {% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+ {% if previous.answers %}
+
+
{% trans "The previous question" %}:
+
{{ previous.previous_question }}
+
+ {% if previous.previous_outcome %}
+
+ {% else %}
+
+ {% endif %}
+
+ {% trans "Your answer was" %}
+
+ {{ previous.previous_outcome|yesno:"correct,incorrect" }}
+
+
+
+
+
+ {% load i18n %}
+ {% if previous.answers %}
+
+ {% if user_was_incorrect %}
+
+ {% trans "You answered the above question incorrectly" %}
+
+ {% endif %}
+
+
+
+ {% for answer in previous.answers %}
+ {% if answer.correct %}
+
+ {{ answer }}
+ {% trans "This is the correct answer" %}
+ {% else %}
+
+ {{ answer }}
+
+ {% if previous.question_type.MCQuestion %}
+ {% if answer.id|add:"0" == previous.previous_answer|add:"0" %}
+ {% trans "This was your answer." %}
+ {% endif %}
+ {% endif %}
+
+ {% endif %}
+
+ {% endfor %}
+
+
+ {% endif %}
+
+
+
{% trans "Explanation" %}:
+
+ {% if previous.previous_question.explanation %}
+ {{ previous.previous_question.explanation }}
+ {% else %}
+ {% trans "No explanation set to this question." %}
+ {% endif %}
+
+
+
+
+
+
+ {% endif %}
+
+
+
+ {% if question %}
+
+ {% if progress.0|add:1 == 1 %}
+
+
+ {% endif %}
+
+ {% if progress %}
+
+ {% trans "Question" %} {{ progress.0|add:1 }} {% trans "of" %} {{ progress.1 }}
+
+ {% endif %}
+
+
+ {% trans "Quiz category" %}:
+ {{ quiz.category }}
+
+
+
+
{{ question.content }}
+
+ {% if question.figure %}
+
+
+
+ {% endif %}
+
+
+ {% endif %}
+
+
+
+
+
+{% endblock %}
+
+{% block js %}
+
+{% endblock js %}
diff --git a/quiz/templates/quiz/mcquestion_form.html b/quiz/templates/quiz/mcquestion_form.html
new file mode 100644
index 0000000..f98c57e
--- /dev/null
+++ b/quiz/templates/quiz/mcquestion_form.html
@@ -0,0 +1,161 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+
{{ quizQuestions }} question added
+
+
+
+
+
+
+
+
+
+{% endblock %}
+
+
diff --git a/quiz/templates/quiz/quiz_form.html b/quiz/templates/quiz/quiz_form.html
new file mode 100644
index 0000000..f0073b6
--- /dev/null
+++ b/quiz/templates/quiz/quiz_form.html
@@ -0,0 +1,200 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+
+
+
+
+{% endblock %}
+
+{% block js %}
+
+{% endblock js %}
diff --git a/quiz/templates/quiz/quiz_list.html b/quiz/templates/quiz/quiz_list.html
new file mode 100644
index 0000000..0139850
--- /dev/null
+++ b/quiz/templates/quiz/quiz_list.html
@@ -0,0 +1,95 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+{% load i18n %}
+{% load static %}
+
+{% block content %}
+
+
+
+
+
+ {% if messages %}
+
+ {% endif %}
+
+
+
+
+
+
+ {% if request.user.is_superuser or request.user.is_lecturer %}
+
+ Add Quiz
+ {% endif %}
+
+
+ {% for quiz in quizzes %}
+
+
+ {% if request.user.is_superuser or request.user.is_lecturer %}
+
+ {% endif %}
+
+
{{ quiz.category|title }} Quiz
+
+ {{ quiz.get_questions.count }} Questions
+
+
+
+
+
+ {% if quiz.single_attempt %}
+
{% trans "You will only get one attempt at this quiz" %}.
+ {% endif %}
+
{{ quiz.description }} here is the description of this quiz...
+
+
+
{% trans "Start quiz" %} »
+
+
+ {% if forloop.counter|divisibleby:3 %}
{% endif %}
+ {% endfor %}
+
+
+
+{% endblock %}
+
+{% block js %}
+
+{% endblock js %}
diff --git a/quiz/templates/quiz/sitting_detail.html b/quiz/templates/quiz/sitting_detail.html
new file mode 100644
index 0000000..a9bf191
--- /dev/null
+++ b/quiz/templates/quiz/sitting_detail.html
@@ -0,0 +1,66 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}
+{% trans "Result of" %} {{ sitting.quiz.title }} {% trans "for" %} {{ sitting.user }}
+{% endblock %}
+
+{% block content %}
+
+
+
+
+ {% trans "Category" %}: {{ sitting.quiz.category }}
+
+
+
{{ sitting.quiz.description }}
+
+
{% trans "User" %}: {{ sitting.user }}
+
{% trans "Completed" %}: {{ sitting.end|date }}
+
{% trans "Score" %}: {{ sitting.get_percent_correct }}%
+
+
+
+
+
+
+ {% trans "Question" %}
+ {% trans "User answer" %}
+
+
+
+
+
+
+{% for question in questions %}
+
+
+
+ {{ question.content }}
+ {% if question.figure %}
+
+ {% endif %}
+
+ {{ question }}
+
+ {% if question.id in sitting.get_incorrect_questions %}
+ {% trans "incorrect" %}
+ {% else %}
+ {% trans "Correct" %}
+ {% endif %}
+
+
+
+
+
+
+{% endfor %}
+
+
+
+
+{% endblock %}
diff --git a/quiz/templates/quiz/sitting_list.html b/quiz/templates/quiz/sitting_list.html
new file mode 100644
index 0000000..8f7522c
--- /dev/null
+++ b/quiz/templates/quiz/sitting_list.html
@@ -0,0 +1,59 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "All Quizzes" %}{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+{% for student in students %}
{{ student.student.user.get_full_name }} {% endfor %}
+{% for marking in marking_list %}
{{ marking }} {{ forloop.counter }} {% endfor %}
+
+
+
+{% if sitting_list %}
+
+
Total complete exams: {{ sitting_list.count }}
+
+
+
+
+ #
+ {% trans "User" %}
+ {% trans "Course" %}
+ {% trans "Quiz" %}
+ {% trans "Completed" %}
+ {% trans "Score" %}(%)
+
+
+
+
+ {% for sitting in sitting_list %}
+
+ {{ forloop.counter }}
+ {{ sitting.user }}
+ {{ sitting.quiz.course }}
+ {{ sitting.quiz }}
+ {{ sitting.end|date }}
+ {{ sitting.get_percent_correct }}%
+
+
+ {% trans "View details" %}
+
+
+ {% endfor %}
+
+
+
+{% else %}
+
{% trans "There are no matching results for your search..." %}.
+{% endif %}
+
+{% endblock %}
diff --git a/quiz/templates/result.html b/quiz/templates/result.html
new file mode 100644
index 0000000..23a1b7b
--- /dev/null
+++ b/quiz/templates/result.html
@@ -0,0 +1,290 @@
+{% extends "base.html" %}
+{% load i18n %}
+
+{% load quiz_tags %}
+
+
+{% block title %} {{ quiz.title}} {% endblock %}
+{% block description %} {% trans "Exam Results for" %} {{ quiz.title }} {% endblock %}
+
+{% block content %}
+
+
+
+
+
+
Caculating your result...
+
+
+
+
+
+
+
+
+ {% if previous.answers %}
+
+
{% trans "The previous question" %}:
+
{{ previous.previous_question }}
+
Your answer was
+
+ {{ previous.previous_outcome|yesno:"correct,incorrect" }}
+
+
+
+ {% load i18n %}
+ {% if previous.answers %}
+
+ {% if user_was_incorrect %}
+
+ {% trans "You answered the above question incorrectly" %}
+
+ {% endif %}
+
+
+
+ {% for answer in previous.answers %}
+ {% if answer.correct %}
+
+ {{ answer }}
+ {% trans "This is the correct answer" %}
+ {% else %}
+
+ {{ answer }}
+
+ {% if previous.question_type.MCQuestion %}
+ {% if answer.id|add:"0" == previous.previous_answer|add:"0" %}
+ {% trans "This was your answer." %}
+ {% endif %}
+ {% endif %}
+
+ {% endif %}
+
+ {% endfor %}
+
+
+ {% endif %}
+
+
{% trans "Explanation" %}:
+
+ {% if previous.previous_question.explanation %}
+ {{ previous.previous_question.explanation }}
+ {% else %}
+ {% trans "No explanation set to this question." %}
+ {% endif %}
+
+
+
+
+ {% endif %}
+
+ {% if max_score %}
+
+
+
+
+
+ {% trans "Exam title" %}:
+ {{ quiz.title }}
+
+
+ {% trans "You answered" %} {{ score }} {% trans "questions correctly out of" %} {{ max_score }}, {% trans "giving you" %} {{ percent }}{% trans "% correct" %}
+
+
+
+
+ {% if quiz.pass_mark %}
+
+
⊙ {{ sitting.result_message }}
+ {% endif %}
+
⟶ {% trans "Review the questions below and try the exam again in the future"%}.
+
+
+ ⟶
+ {% trans "The result of this exam will be stored in your progress section" %}
+ Here
+ {% trans "so you can review and monitor your progression" %}.
+
+
+
+
+
+ {% endif %}
+
+
+
+
+ {% if possible %}
+
+
+ {% trans "Your session score is" %} {{ session }} {% trans "out of a possible" %} {{ possible }}
+
+
+
+
+ {% endif %}
+
+ {% if questions %}
+
+
Over view
+ {% for question in questions %}
+
+
+ {{ forloop.counter }}, {{ question.content }}
+
+
+
{% trans "Explanation" %}:
+
+ {% if question.explanation %}
+ {{ question.explanation|safe }}
+ {% else %}
+ {% trans "No explanation set for this question." %}
+ {% endif %}
+
+
+ {% correct_answer_for_all question %}
+
+ {% if question.user_answer %}
+
{% trans "Your answer" %}: {{ question|answer_choice_to_string:question.user_answer }}
+ {% endif %}
+
+
+
+ {% endfor %}
+
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block js %}
+
+{% endblock js %}
diff --git a/quiz/templatetags/__init__.py b/quiz/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/quiz/templatetags/quiz_tags.py b/quiz/templatetags/quiz_tags.py
new file mode 100644
index 0000000..d256bb0
--- /dev/null
+++ b/quiz/templatetags/quiz_tags.py
@@ -0,0 +1,25 @@
+from django import template
+
+register = template.Library()
+
+
+@register.inclusion_tag('correct_answer.html', takes_context=True)
+def correct_answer_for_all(context, question):
+ """
+ processes the correct answer based on a given question object
+ if the answer is incorrect, informs the user
+ """
+ answers = question.get_choices()
+ incorrect_list = context.get('incorrect_questions', [])
+ if question.id in incorrect_list:
+ user_was_incorrect = True
+ else:
+ user_was_incorrect = False
+
+ return {'previous': {'answers': answers},
+ 'user_was_incorrect': user_was_incorrect}
+
+
+@register.filter
+def answer_choice_to_string(question, answer):
+ return question.answer_choice_to_string(answer)
diff --git a/quiz/tests.py b/quiz/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/quiz/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/quiz/urls.py b/quiz/urls.py
new file mode 100644
index 0000000..5bf34ef
--- /dev/null
+++ b/quiz/urls.py
@@ -0,0 +1,23 @@
+from django.conf.urls import url
+
+from .views import *
+
+urlpatterns = [
+
+ url(r'^(?P
[\w-]+)/quizzes/$', quiz_list, name='quiz_index'),
+
+ url(r'^progress/$', view=QuizUserProgressView.as_view(), name='quiz_progress'),
+
+ # url(r'^marking/(?P[\d.]+)/$', view=QuizMarkingList.as_view(), name='quiz_marking'),
+ url(r'^marking_list/$', view=QuizMarkingList.as_view(), name='quiz_marking'),
+
+ url(r'^marking/(?P[\d.]+)/$', view=QuizMarkingDetail.as_view(), name='quiz_marking_detail'),
+
+ url(r'^(?P[\d.]+)/(?P[\w-]+)/take/$', view=QuizTake.as_view(), name='quiz_take'),
+
+ url(r'^(?P[\w-]+)/quiz_add/$', QuizCreateView.as_view(), name='quiz_create'),
+ url(r'^(?P[\w-]+)/(?P[\d.]+)/add/$', QuizUpdateView.as_view(), name='quiz_update'),
+ url(r'^(?P[\w-]+)/(?P[\d.]+)/delete/$', quiz_delete, name='quiz_delete'),
+ url(r'^mc-question/add/(?P[\w-]+)/(?P[\d.]+)/$', MCQuestionCreate.as_view(), name='mc_create'),
+ # url(r'^mc-question/add/(?P[\d.]+)/(?P[\d.]+)/$', MCQuestionCreate.as_view(), name='mc_create'),
+]
diff --git a/quiz/utils.py b/quiz/utils.py
new file mode 100644
index 0000000..33dce5e
--- /dev/null
+++ b/quiz/utils.py
@@ -0,0 +1,31 @@
+import datetime
+import os
+import random
+import string
+
+from django.utils.text import slugify
+
+
+def random_string_generator(size=10, chars=string.ascii_lowercase + string.digits):
+ return ''.join(random.choice(chars) for _ in range(size))
+
+
+def unique_slug_generator(instance, new_slug=None):
+ """
+ This is for a Django project and it assumes your instance
+ has a model with a slug field and a title character (char) field.
+ """
+ if new_slug is not None:
+ slug = new_slug
+ else:
+ slug = slugify(instance.title)
+
+ Klass = instance.__class__
+ qs_exists = Klass.objects.filter(slug=slug).exists()
+ if qs_exists:
+ new_slug = "{slug}-{randstr}".format(
+ slug=slug,
+ randstr=random_string_generator(size=4)
+ )
+ return unique_slug_generator(instance, new_slug=new_slug)
+ return slug
diff --git a/quiz/views.py b/quiz/views.py
new file mode 100644
index 0000000..53be81d
--- /dev/null
+++ b/quiz/views.py
@@ -0,0 +1,321 @@
+import random
+
+from django.contrib.auth.decorators import login_required, permission_required
+from django.core.exceptions import PermissionDenied
+from django.shortcuts import get_object_or_404, render, redirect
+from django.utils.decorators import method_decorator
+from django.views.generic import DetailView, ListView, TemplateView, FormView, CreateView, FormView, DeleteView, UpdateView
+from django.contrib import messages
+from django.urls import reverse_lazy
+from django.db import transaction
+from django.forms import inlineformset_factory
+from django.http import HttpResponseRedirect
+
+from accounts.decorators import student_required, lecturer_required
+from .models import *
+from .forms import *
+
+
+@method_decorator([login_required, lecturer_required], name='dispatch')
+class QuizCreateView(CreateView):
+ model = Quiz
+ form_class = QuizAddForm
+
+ def get_context_data(self, *args, **kwargs):
+ context = super(QuizCreateView, self).get_context_data(**kwargs)
+ context['course'] = Course.objects.get(slug=self.kwargs['slug'])
+ if self.request.POST:
+ context['form'] = QuizAddForm(self.request.POST)
+ # context['quiz'] = self.request.POST.get('quiz')
+ else:
+ context['form'] = QuizAddForm(initial={'course': Course.objects.get(slug=self.kwargs['slug'])})
+ return context
+
+ def form_valid(self, form, **kwargs):
+ context = self.get_context_data()
+ form = context['form']
+ with transaction.atomic():
+ self.object = form.save()
+ if form.is_valid():
+ form.instance = self.object
+ form.save()
+ return redirect('mc_create', slug=self.kwargs['slug'], quiz_id=form.instance.id)
+ return super(QuizCreateView, self).form_invalid(form)
+
+
+@method_decorator([login_required, lecturer_required], name='dispatch')
+class QuizUpdateView(UpdateView):
+ model = Quiz
+ form_class = QuizAddForm
+
+ def get_context_data(self, *args, **kwargs):
+ context = super(QuizUpdateView, self).get_context_data(**kwargs)
+ context['course'] = Course.objects.get(slug=self.kwargs['slug'])
+ quiz = Quiz.objects.get(pk=self.kwargs['pk'])
+ if self.request.POST:
+ context['form'] = QuizAddForm(self.request.POST, instance=quiz)
+ else:
+ context['form'] = QuizAddForm(instance=quiz)
+ return context
+
+ def form_valid(self, form, **kwargs):
+ context = self.get_context_data()
+ course = context['course']
+ form = context['form']
+ with transaction.atomic():
+ self.object = form.save()
+ if form.is_valid():
+ form.instance = self.object
+ form.save()
+ return redirect('quiz_index', course.slug)
+ return super(QuizUpdateView, self).form_invalid(form)
+
+
+@login_required
+@lecturer_required
+def quiz_delete(request, slug, pk):
+ quiz = Quiz.objects.get(pk=pk)
+ course = Course.objects.get(slug=slug)
+ quiz.delete()
+ messages.success(request, f'successfuly deleted.')
+ return redirect('quiz_index', quiz.course.slug)
+
+
+@method_decorator([login_required, lecturer_required], name='dispatch')
+class MCQuestionCreate(CreateView):
+ model = MCQuestion
+ form_class = MCQuestionForm
+
+ def get_context_data(self, **kwargs):
+ context = super(MCQuestionCreate, self).get_context_data(**kwargs)
+ context['course'] = Course.objects.get(slug=self.kwargs['slug'])
+ context['quiz_obj'] = Quiz.objects.get(id=self.kwargs['quiz_id'])
+ context['quizQuestions'] = Question.objects.filter(quiz=self.kwargs['quiz_id']).count()
+ if self.request.POST:
+ context['form'] = MCQuestionForm(self.request.POST)
+ context['formset'] = MCQuestionFormSet(self.request.POST)
+ else:
+ context['form'] = MCQuestionForm(initial={'quiz': self.kwargs['quiz_id']})
+ context['formset'] = MCQuestionFormSet()
+
+ return context
+
+ def form_valid(self, form):
+ context = self.get_context_data()
+ formset = context['formset']
+ course = context['course']
+ with transaction.atomic():
+ form.instance.question = self.request.POST.get('content')
+ self.object = form.save()
+ if formset.is_valid():
+ formset.instance = self.object
+ formset.save()
+ if "another" in self.request.POST:
+ return redirect('mc_create', slug=self.kwargs['slug'], quiz_id=self.kwargs['quiz_id'])
+ return redirect('quiz_index', course.slug)
+ return super(MCQuestionCreate, self).form_invalid(form)
+
+
+@login_required
+def quiz_list(request, slug):
+ quizzes = Quiz.objects.filter(course__slug = slug).order_by('-timestamp')
+ course = Course.objects.get(slug = slug)
+ return render(request, 'quiz/quiz_list.html', {'quizzes': quizzes, 'course': course})
+ # return render(request, 'quiz/quiz_list.html', {'quizzes': quizzes})
+
+
+@method_decorator([login_required, lecturer_required], name='dispatch')
+class QuizMarkerMixin(object):
+ @method_decorator(login_required)
+ # @method_decorator(permission_required('quiz.view_sittings'))
+ def dispatch(self, *args, **kwargs):
+ return super(QuizMarkerMixin, self).dispatch(*args, **kwargs)
+
+
+# @method_decorator([login_required, lecturer_required], name='get_queryset')
+class SittingFilterTitleMixin(object):
+ def get_queryset(self):
+ queryset = super(SittingFilterTitleMixin, self).get_queryset()
+ quiz_filter = self.request.GET.get('quiz_filter')
+ if quiz_filter:
+ queryset = queryset.filter(quiz__title__icontains=quiz_filter)
+
+ return queryset
+
+
+@method_decorator([login_required], name='dispatch')
+class QuizUserProgressView(TemplateView):
+ template_name = 'progress.html'
+
+ def dispatch(self, request, *args, **kwargs):
+ return super(QuizUserProgressView, self).dispatch(request, *args, **kwargs)
+
+ def get_context_data(self, **kwargs):
+ context = super(QuizUserProgressView, self).get_context_data(**kwargs)
+ progress, c = Progress.objects.get_or_create(user=self.request.user)
+ context['cat_scores'] = progress.list_all_cat_scores
+ context['exams'] = progress.show_exams()
+ context['exams_counter'] = progress.show_exams().count()
+ return context
+
+from result.models import TakenCourse
+
+@method_decorator([login_required, lecturer_required], name='dispatch')
+class QuizMarkingList(QuizMarkerMixin, SittingFilterTitleMixin, ListView):
+ model = Sitting
+ # def get_context_data(self, **kwargs):
+ # context = super(QuizMarkingList, self).get_context_data(**kwargs)
+ # context['queryset_counter'] = super(QuizMarkingList, self).get_queryset().filter(complete=True).filter(course__allocated_course__lecturer__pk=self.request.user.id).count()
+ # context['marking_list'] = super(QuizMarkingList, self).get_queryset().filter(complete=True).filter(course__allocated_course__lecturer__pk=self.request.user.id)
+ # return context
+ def get_queryset(self):
+ if self.request.user.is_superuser:
+ queryset = super(QuizMarkingList, self).get_queryset().filter(complete=True)
+ else:
+ queryset = super(QuizMarkingList, self).get_queryset().filter(quiz__course__allocated_course__lecturer__pk=self.request.user.id).filter(complete=True)
+
+ # search by user
+ user_filter = self.request.GET.get('user_filter')
+ if user_filter:
+ queryset = queryset.filter(user__username__icontains=user_filter)
+
+ return queryset
+
+
+@method_decorator([login_required, lecturer_required], name='dispatch')
+class QuizMarkingDetail(QuizMarkerMixin, DetailView):
+ model = Sitting
+
+ def post(self, request, *args, **kwargs):
+ sitting = self.get_object()
+
+ q_to_toggle = request.POST.get('qid', None)
+ if q_to_toggle:
+ q = Question.objects.get_subclass(id=int(q_to_toggle))
+ if int(q_to_toggle) in sitting.get_incorrect_questions:
+ sitting.remove_incorrect_question(q)
+ else:
+ sitting.add_incorrect_question(q)
+
+ return self.get(request)
+
+ def get_context_data(self, **kwargs):
+ context = super(QuizMarkingDetail, self).get_context_data(**kwargs)
+ context['questions'] = context['sitting'].get_questions(with_answers=True)
+ return context
+
+
+# @method_decorator([login_required, student_required], name='dispatch')
+@method_decorator([login_required], name='dispatch')
+class QuizTake(FormView):
+ form_class = QuestionForm
+ template_name = 'question.html'
+ result_template_name = 'result.html'
+ # single_complete_template_name = 'single_complete.html'
+
+ def dispatch(self, request, *args, **kwargs):
+ self.quiz = get_object_or_404(Quiz, slug=self.kwargs['slug'])
+ self.course = get_object_or_404(Course, pk=self.kwargs['pk'])
+ quizQuestions = Question.objects.filter(quiz=self.quiz).count()
+ course = get_object_or_404(Course, pk=self.kwargs['pk'])
+
+ if quizQuestions <= 0:
+ messages.warning(request, f'Question set of the quiz is empty. try later!')
+ return redirect('quiz_index', self.course.slug)
+
+ if self.quiz.draft and not request.user.has_perm('quiz.change_quiz'):
+ raise PermissionDenied
+
+ self.sitting = Sitting.objects.user_sitting(request.user, self.quiz, self.course)
+
+ if self.sitting is False:
+ # return render(request, self.single_complete_template_name)
+ messages.info(request, f'You have already sat this exam and only one sitting is permitted')
+ return redirect('quiz_index', self.course.slug)
+
+ return super(QuizTake, self).dispatch(request, *args, **kwargs)
+
+ def get_form(self, *args, **kwargs):
+ self.question = self.sitting.get_first_question()
+ self.progress = self.sitting.progress()
+
+ if self.question.__class__ is Essay_Question:
+ form_class = EssayForm
+ else:
+ form_class = self.form_class
+
+ return form_class(**self.get_form_kwargs())
+
+ def get_form_kwargs(self):
+ kwargs = super(QuizTake, self).get_form_kwargs()
+
+ return dict(kwargs, question=self.question)
+
+ def form_valid(self, form):
+ self.form_valid_user(form)
+ if self.sitting.get_first_question() is False:
+ return self.final_result_user()
+
+ self.request.POST = {}
+
+ return super(QuizTake, self).get(self, self.request)
+
+ def get_context_data(self, **kwargs):
+ context = super(QuizTake, self).get_context_data(**kwargs)
+ context['question'] = self.question
+ context['quiz'] = self.quiz
+ context['course'] = get_object_or_404(Course, pk=self.kwargs['pk'])
+ if hasattr(self, 'previous'):
+ context['previous'] = self.previous
+ if hasattr(self, 'progress'):
+ context['progress'] = self.progress
+ return context
+
+ def form_valid_user(self, form):
+ progress, c = Progress.objects.get_or_create(user=self.request.user)
+ guess = form.cleaned_data['answers']
+ is_correct = self.question.check_if_correct(guess)
+
+ if is_correct is True:
+ self.sitting.add_to_score(1)
+ progress.update_score(self.question, 1, 1)
+ else:
+ self.sitting.add_incorrect_question(self.question)
+ progress.update_score(self.question, 0, 1)
+
+ if self.quiz.answers_at_end is not True:
+ self.previous = {
+ 'previous_answer': guess,
+ 'previous_outcome': is_correct,
+ 'previous_question': self.question,
+ 'answers': self.question.get_choices(),
+ 'question_type': {self.question.__class__.__name__: True}
+ }
+ else:
+ self.previous = {}
+
+ self.sitting.add_user_answer(self.question, guess)
+ self.sitting.remove_first_question()
+
+ def final_result_user(self):
+ results = {
+ 'course': get_object_or_404(Course, pk=self.kwargs['pk']),
+ 'quiz': self.quiz,
+ 'score': self.sitting.get_current_score,
+ 'max_score': self.sitting.get_max_score,
+ 'percent': self.sitting.get_percent_correct,
+ 'sitting': self.sitting,
+ 'previous': self.previous,
+ 'course': get_object_or_404(Course, pk=self.kwargs['pk'])
+ }
+
+ self.sitting.mark_quiz_complete()
+
+ if self.quiz.answers_at_end:
+ results['questions'] = self.sitting.get_questions(with_answers=True)
+ results['incorrect_questions'] = self.sitting.get_incorrect_questions
+
+ if self.quiz.exam_paper is False or self.request.user.is_superuser or self.request.user.is_lecturer :
+ self.sitting.delete()
+
+ return render(self.request, self.result_template_name, results)
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..9ff2514
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,9 @@
+asgiref==3.3.1
+Django==3.1.3
+django-cleanup==5.1.0
+django-crispy-forms==1.9.2
+django-model-utils==4.1.1
+Pillow==8.1.2
+pytz==2021.1
+reportlab==3.5.56
+sqlparse==0.4.1
diff --git a/result/__init__.py b/result/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/result/admin.py b/result/admin.py
new file mode 100644
index 0000000..d93129a
--- /dev/null
+++ b/result/admin.py
@@ -0,0 +1,15 @@
+from django.contrib import admin
+from django.contrib.auth.models import Group
+
+from .models import TakenCourse, Result
+
+
+class ScoreAdmin(admin.ModelAdmin):
+ list_display = [
+ 'student', 'course', 'assignment', 'mid_exam', 'quiz',
+ 'attendance', 'final_exam', 'total', 'grade', 'comment'
+ ]
+
+
+admin.site.register(TakenCourse, ScoreAdmin)
+admin.site.register(Result)
diff --git a/result/apps.py b/result/apps.py
new file mode 100644
index 0000000..71c04bb
--- /dev/null
+++ b/result/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class ResultConfig(AppConfig):
+ name = 'result'
diff --git a/result/migrations/0001_initial.py b/result/migrations/0001_initial.py
new file mode 100644
index 0000000..e5483aa
--- /dev/null
+++ b/result/migrations/0001_initial.py
@@ -0,0 +1,47 @@
+# Generated by Django 2.2.3 on 2020-07-29 15:25
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('accounts', '0001_initial'),
+ ('course', '0001_initial'),
+ ('app', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='TakenCourse',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('assignment', models.DecimalField(decimal_places=2, default=0.0, max_digits=5)),
+ ('mid_exam', models.DecimalField(decimal_places=2, default=0.0, max_digits=5)),
+ ('quiz', models.DecimalField(decimal_places=2, default=0.0, max_digits=5)),
+ ('attendance', models.DecimalField(decimal_places=2, default=0.0, max_digits=5)),
+ ('final_exam', models.DecimalField(decimal_places=2, default=0.0, max_digits=5)),
+ ('total', models.DecimalField(decimal_places=2, default=0.0, max_digits=5)),
+ ('grade', models.CharField(blank=True, choices=[('A+', 'A+'), ('A', 'A'), ('A-', 'A-'), ('B+', 'B+'), ('B', 'B'), ('B-', 'B-'), ('C+', 'C+'), ('C', 'C'), ('C-', 'C-'), ('D', 'D'), ('F', 'F'), ('NG', 'NG')], max_length=1)),
+ ('point', models.DecimalField(decimal_places=2, default=0.0, max_digits=5)),
+ ('comment', models.CharField(blank=True, choices=[('PASS', 'PASS'), ('FAIL', 'FAIL')], max_length=200)),
+ ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='taken_courses', to='course.Course')),
+ ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.Student')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Result',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('gpa', models.FloatField(null=True)),
+ ('cgpa', models.FloatField(null=True)),
+ ('semester', models.CharField(choices=[('First', 'First'), ('Second', 'Second'), ('Third', 'Third')], max_length=100)),
+ ('level', models.CharField(choices=[('Level course', 'Level course'), ('Bachloar', 'Bachloar'), ('Master', 'Master')], max_length=25, null=True)),
+ ('session', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='app.Session')),
+ ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.Student')),
+ ],
+ ),
+ ]
diff --git a/result/migrations/0002_auto_20200729_2233.py b/result/migrations/0002_auto_20200729_2233.py
new file mode 100644
index 0000000..0cec2e8
--- /dev/null
+++ b/result/migrations/0002_auto_20200729_2233.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.3 on 2020-07-29 19:33
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('result', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='result',
+ name='session',
+ field=models.CharField(blank=True, max_length=100, null=True),
+ ),
+ ]
diff --git a/result/migrations/0003_auto_20200822_2238.py b/result/migrations/0003_auto_20200822_2238.py
new file mode 100644
index 0000000..979e811
--- /dev/null
+++ b/result/migrations/0003_auto_20200822_2238.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.3 on 2020-08-22 19:38
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('result', '0002_auto_20200729_2233'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='result',
+ name='level',
+ field=models.CharField(choices=[('Bachloar', 'Bachloar Degree'), ('Master', 'Master Degree')], max_length=25, null=True),
+ ),
+ ]
diff --git a/result/migrations/0004_auto_20200825_1248.py b/result/migrations/0004_auto_20200825_1248.py
new file mode 100644
index 0000000..6e43823
--- /dev/null
+++ b/result/migrations/0004_auto_20200825_1248.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1 on 2020-08-25 09:48
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('result', '0003_auto_20200822_2238'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='takencourse',
+ name='grade',
+ field=models.CharField(blank=True, choices=[('A+', 'A+'), ('A', 'A'), ('A-', 'A-'), ('B+', 'B+'), ('B', 'B'), ('B-', 'B-'), ('C+', 'C+'), ('C', 'C'), ('C-', 'C-'), ('D', 'D'), ('F', 'F'), ('NG', 'NG')], max_length=2),
+ ),
+ ]
diff --git a/result/migrations/__init__.py b/result/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/result/models.py b/result/models.py
new file mode 100644
index 0000000..d5d7fcf
--- /dev/null
+++ b/result/models.py
@@ -0,0 +1,281 @@
+from django.db import models
+from django.urls import reverse
+
+from accounts.models import Student
+from app.models import Session, Semester
+from course.models import Course
+
+YEARS = (
+ (1, '1'),
+ (2, '2'),
+ (3, '3'),
+ (4, '4'),
+ (4, '5'),
+ (4, '6'),
+ )
+
+# LEVEL_COURSE = "Level course"
+BACHLOAR_DEGREE = "Bachloar"
+MASTER_DEGREE = "Master"
+
+LEVEL = (
+ # (LEVEL_COURSE, "Level course"),
+ (BACHLOAR_DEGREE, "Bachloar Degree"),
+ (MASTER_DEGREE, "Master Degree"),
+)
+
+FIRST = "First"
+SECOND = "Second"
+THIRD = "Third"
+
+SEMESTER = (
+ (FIRST, "First"),
+ (SECOND, "Second"),
+ (THIRD, "Third"),
+)
+
+A_plus = "A+"
+A = "A"
+A_minus = "A-"
+B_plus = "B+"
+B = "B"
+B_minus = "B-"
+C_plus = "C+"
+C = "C"
+C_minus = "C-"
+D = "D"
+F = "F"
+NG = "NG"
+
+GRADE = (
+ (A_plus, "A+"),
+ (A, "A"),
+ (A_minus, "A-"),
+ (B_plus, "B+"),
+ (B, "B"),
+ (B_minus, "B-"),
+ (C_plus, "C+"),
+ (C, "C"),
+ (C_minus, "C-"),
+ (D, "D"),
+ (F, "F"),
+ (NG, "NG"),
+)
+
+PASS = "PASS"
+FAIL = "FAIL"
+
+COMMENT = (
+ (PASS, "PASS"),
+ (FAIL, "FAIL"),
+)
+
+
+class TakenCourseManager(models.Manager):
+ def new_or_get(self, request):
+ cart_id = request.session.get("cart_id", None)
+ qs = self.get_queryset().filter(id=cart_id)
+ if qs.count() == 1:
+ new_obj = False
+ cart_obj = qs.first()
+ if request.user.is_authenticated() and cart_obj.user is None:
+ cart_obj.user = request.user
+ cart_obj.save()
+ else:
+ cart_obj = Cart.objects.new(user=request.user)
+ new_obj = True
+ request.session['cart_id'] = cart_obj.id
+ return cart_obj, new_obj
+
+ def new(self, user=None):
+ user_obj = None
+ if user is not None:
+ if user.is_authenticated():
+ user_obj = user
+ return self.model.objects.create(user=user_obj)
+
+
+class TakenCourse(models.Model):
+ student = models.ForeignKey(Student, on_delete=models.CASCADE)
+ course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='taken_courses')
+ assignment = models.DecimalField(max_digits=5, decimal_places=2, default=0.0)
+ mid_exam = models.DecimalField(max_digits=5, decimal_places=2, default=0.0)
+ quiz = models.DecimalField(max_digits=5, decimal_places=2, default=0.0)
+ attendance = models.DecimalField(max_digits=5, decimal_places=2, default=0.0)
+ final_exam = models.DecimalField(max_digits=5, decimal_places=2, default=0.0)
+ total = models.DecimalField(max_digits=5, decimal_places=2, default=0.0)
+ grade = models.CharField(choices=GRADE, max_length=2, blank=True)
+ point = models.DecimalField(max_digits=5, decimal_places=2, default=0.0)
+ comment = models.CharField(choices=COMMENT, max_length=200, blank=True)
+
+ def get_absolute_url(self):
+ return reverse('course_detail', kwargs={'slug': self.course.slug})
+
+ def __str__(self):
+ return "{0} ({1})".format(self.course.title, self.course.code)
+
+ # @staticmethod
+ def get_total(self, assignment, mid_exam, quiz, attendance, final_exam):
+ return float(assignment) + float(mid_exam) + float(quiz) + float(attendance) + float(final_exam)
+
+ # @staticmethod
+ def get_grade(self, total):
+ # total = float(assignment) + float(mid_exam) + float(quiz) + float(attendance) + float(final_exam)
+ # total = self.get_total(assignment=assignment, mid_exam=mid_exam, quiz=quiz, attendance=attendance, final_exam=final_exam)
+ # total = total
+ if total >= 90:
+ grade = A_plus
+ elif total >= 85:
+ grade = A
+ elif total >= 80:
+ grade = A_minus
+ elif total >= 75:
+ grade = B_plus
+ elif total >= 70:
+ grade = B
+ elif total >= 65:
+ grade = B_minus
+ elif total >= 60:
+ grade = C_plus
+ elif total >= 55:
+ grade = C
+ elif total >= 50:
+ grade = C_minus
+ elif total >= 45:
+ grade = D
+ elif total < 45:
+ grade = F
+ else:
+ grade = NG
+ return grade
+
+ # @staticmethod
+ def get_comment(self, grade):
+ if grade == F or grade == NG:
+ comment = FAIL
+ # elif grade == NG:
+ # comment = FAIL
+ else:
+ comment = PASS
+ return comment
+
+ def get_point(self, grade):
+ p = 0
+ # point = 0
+ # for i in student:
+ credit = self.course.credit
+ if self.grade == A_plus:
+ point = 4
+ elif self.grade == A:
+ point = 4
+ elif self.grade == A_minus:
+ point = 3.75
+ elif self.grade == B_plus:
+ point = 3.5
+ elif self.grade == B:
+ point = 3
+ elif self.grade == B_minus:
+ point = 2.75
+ elif self.grade == C_plus:
+ point = 2.5
+ elif self.grade == C:
+ point = 2
+ elif self.grade == C_minus:
+ point = 1.75
+ elif self.grade == D:
+ point = 1
+ else:
+ point = 0
+ p += int(credit) * point
+ return p
+
+ def calculate_gpa(self, total_credit_in_semester):
+ current_semester = Semester.objects.get(is_current_semester=True)
+ student = TakenCourse.objects.filter(student=self.student, course__level=self.student.level, course__semester=current_semester)
+ p = 0
+ point = 0
+ for i in student:
+ credit = i.course.credit
+ if i.grade == A_plus:
+ point = 4
+ elif i.grade == A:
+ point = 4
+ elif i.grade == A_minus:
+ point = 3.75
+ elif i.grade == B_plus:
+ point = 3.5
+ elif i.grade == B:
+ point = 3
+ elif i.grade == B_minus:
+ point = 2.75
+ elif i.grade == C_plus:
+ point = 2.5
+ elif i.grade == C:
+ point = 2
+ elif i.grade == C_minus:
+ point = 1.75
+ elif i.grade == D:
+ point = 1
+ else:
+ point = 0
+ p += int(credit) * point
+ try:
+ gpa = (p / total_credit_in_semester)
+ return round(gpa, 2)
+ except ZeroDivisionError:
+ return 0
+
+ def calculate_cgpa(self):
+ current_semester = Semester.objects.get(is_current_semester=True)
+ previousResult = Result.objects.filter(student__id=self.student.id, level__lt=self.student.level)
+ previousCGPA = 0
+ for i in previousResult:
+ if i.cgpa is not None:
+ previousCGPA += i.cgpa
+ cgpa = 0
+ if str(current_semester) == SECOND:
+ first_sem_gpa = 0.0
+ sec_sem_gpa = 0.0
+ try:
+ first_sem_result = Result.objects.get(student=self.student.id, semester=FIRST, level=self.student.level)
+ first_sem_gpa += first_sem_result.gpa
+ except:
+ first_sem_gpa = 0
+
+ try:
+ sec_sem_result = Result.objects.get(student=self.student.id, semester=SECOND, level=self.student.level)
+ sec_sem_gpa += sec_sem_result.gpa
+ except:
+ sec_sem_gpa = 0
+
+ taken_courses = TakenCourse.objects.filter(student=self.student, student__level=self.student.level)
+ TCC = 0
+ TCP = 0
+ for i in taken_courses:
+ TCP += float(i.point)
+ for i in taken_courses:
+ TCC += int(i.course.credit)
+ # cgpa = (first_sem_gpa + sec_sem_gpa) / 2
+
+ print("TCP = ", TCP)
+ print("TCC = ", TCC)
+ print("first_sem_gpa = ", first_sem_gpa)
+ print("sec_sem_gpa = ", sec_sem_gpa)
+ print("cgpa = ", round(TCP / TCC, 2))
+
+ try:
+ cgpa = TCP / TCC
+ return round(cgpa, 2)
+ except ZeroDivisionError:
+ return 0
+
+ # return round(cgpa, 2)
+
+
+class Result(models.Model):
+ student = models.ForeignKey(Student, on_delete=models.CASCADE)
+ gpa = models.FloatField(null=True)
+ cgpa = models.FloatField(null=True)
+ semester = models.CharField(max_length=100, choices=SEMESTER)
+ session = models.CharField(max_length=100, blank=True, null=True)
+ level = models.CharField(max_length=25, choices=LEVEL, null=True)
diff --git a/result/templates/result/add_score.html b/result/templates/result/add_score.html
new file mode 100644
index 0000000..fbf896f
--- /dev/null
+++ b/result/templates/result/add_score.html
@@ -0,0 +1,65 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+{% load static %}
+
+{% block content %}
+
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+ Manage Score
+
+
+
+
+
+ {{ current_semester }} Semester - {{ current_session }}
+
+
+
No course selected.
+
+
+
+
+
+
+
+
To manage score first select your course in the buttom above.
+
+
+
+{% endblock content %}
diff --git a/result/templates/result/add_score_for.html b/result/templates/result/add_score_for.html
new file mode 100644
index 0000000..6aeb149
--- /dev/null
+++ b/result/templates/result/add_score_for.html
@@ -0,0 +1,148 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+{% load static %}
+
+{% block content %}
+
+
+
+
+
+
+
+
+
+
+{{ myclass.lecturer }}
+
+
+ {% for i in myclass.student.all %}
+
+ {% endfor %}
+
+
+Submit score for {{ course }} Students
+
+{{ course.summary }}
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+
+
+{% endblock content %}
diff --git a/result/templates/result/assessment_results.html b/result/templates/result/assessment_results.html
new file mode 100644
index 0000000..c8e9021
--- /dev/null
+++ b/result/templates/result/assessment_results.html
@@ -0,0 +1,116 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+
+{% block content %}
+
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+ Assesment Results
+
+
+{{ student.level }} Result
+
+
+
First Semester:
+
+
+
+
+ #
+ Course Title
+ Course Code
+ Cr.Hr(s)
+ Assignment
+ Mid exam
+ Quiz
+ Attendance
+ Final exam
+ Total
+
+
+ {% for course in courses %}
+ {% if course.course.semester == "First" %}
+
+
+ {{ forloop.counter }}
+ {{ course.course.title }}
+ {{ course.course.code }}
+ {{ course.course.credit }}
+ {{ course.assignment }}
+ {{ course.mid_exam }}
+ {{ course.quiz }}
+ {{ course.attendance }}
+ {{ course.final_exam }}
+ {% if course.total >= 45 %}
+ {{ course.total }}
+ {% else %}
+ {{ course.total }}
+ {% endif %}
+
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+
+
Second Semester:
+
+
+
+
+ #
+ Course Title
+ Course Code
+ Cr.Hr(s)
+ Assignment
+ Mid exam
+ Quiz
+ Attendance
+ Final exam
+ Total
+
+
+ {% for course in courses %}
+ {% if course.course.semester == "Second" %}
+
+
+ {{ forloop.counter }}
+ {{ course.course.title }}
+ {{ course.course.code }}
+ {{ course.course.credit }}
+ {{ course.assignment }}
+ {{ course.mid_exam }}
+ {{ course.quiz }}
+ {{ course.attendance }}
+ {{ course.final_exam }}
+ {% if course.total >= 45 %}
+ {{ course.total }}
+ {% else %}
+ {{ course.total }}
+ {% endif %}
+
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+
+{% endblock %}
diff --git a/result/templates/result/grade_results.html b/result/templates/result/grade_results.html
new file mode 100644
index 0000000..255d1e2
--- /dev/null
+++ b/result/templates/result/grade_results.html
@@ -0,0 +1,394 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock title %}
+
+{% block content %}
+
+
+
+{% if messages %}
+ {% for message in messages %}
+ {% if message.tags == 'error' %}
+
+ {{ message }}
+
+ {% else %}
+
+ {{ message }}
+
+ {% endif %}
+ {% endfor %}
+{% endif %}
+
+ Grade Results
+
+
+{{ student.level }} Result
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
First Semester:
+
+
+
+
+ #
+ Course Title
+ Course Code
+ Cr.Hr
+ Grade
+ Points
+ Comment
+
+
+ {% for course in courses %}
+ {% if course.course.semester == "First" %}
+
+
+ {{ forloop.counter }}
+ {{ course.course.title }}
+ {{ course.course.code }}
+ {{ course.course.credit }}
+
+ {{ course.grade }}
+ {{ course.point }}
+
+ {% if course.comment == 'PASS' %}
+ PASS
+ {% elif course.comment == 'FAIL' %}
+ FAIL
+ {% else %}
+
+ {% endif %}
+
+
+
+ {% endif %}
+ {% endfor %}
+
+ {% for result in results %}
+ {% if result.semester == "First" %}
+
+
+
+
+
+
+
+ Total first semester credit: {{ total_first_semester_credit }}
+
+
+
+
+
+
+
+
+ First Semester GPA: {{ result.gpa }}
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+
+
+
Second Semester:
+
+
+
+
+ #
+ Course Title
+ Course Code
+ Cr.Hr
+ GRADE
+ Points
+ Comment
+
+
+ {% for course in courses %}
+ {% if course.course.semester == "Second" %}
+
+
+ {{ forloop.counter }}
+ {{ course.course.title }}
+ {{ course.course.code }}
+ {{ course.course.credit }}
+
+ {{ course.grade }}
+ {{ course.point }}
+
+ {% if course.comment == 'PASS' %}
+ PASS
+ {% elif course.comment == 'FAIL' %}
+ FAIL
+ {% else %}
+
+ {% endif %}
+
+
+
+ {% endif %}
+ {% endfor %}
+
+ {% for result in results %}
+ {% if result.semester == "Second" %}
+
+
+ Total second semester credit: {{ total_sec_semester_credit }}
+
+
+
+
+ Total Credit: {{ total_first_and_second_semester_credit }}
+
+
+
+
+
+
+
+
+ Second Semester GPA: {{ result.gpa }}
+
+
+
+
+
+
+
+
+ Previous CGPA: {{ previousCGPA }}
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+
+
+
+ {% for result in results %}
+ {% if result.semester == "First" %}
+
+
+
+ First Semester GPA: {{ result.gpa }}
+
+
+ {% elif result.semester == "Second" %}
+
+
+
+ Second Semester GPA: {{ result.gpa }}
+
+
+ {% endif %}
+ {% endfor %}
+
+
+
+ Previous CGPA: {{ previousCGPA }}
+
+
+
+{% endblock %}
diff --git a/result/tests.py b/result/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/result/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/result/urls.py b/result/urls.py
new file mode 100644
index 0000000..58f8eae
--- /dev/null
+++ b/result/urls.py
@@ -0,0 +1,18 @@
+from django.conf.urls import url
+from django.urls import path
+from .views import (
+ add_score, add_score_for, grade_result, assessment_result,
+ course_registration_form, result_sheet_pdf_view
+)
+
+
+urlpatterns = [
+ url(r'^manage-score/$', add_score, name='add_score'),
+ url(r'^manage-score/(?P\d+)/$', add_score_for, name='add_score_for'),
+
+ url(r'^grade/$', grade_result, name="grade_results"),
+ url(r'^assessment/$', assessment_result, name="ass_results"),
+
+ url(r'^result/print/(?P\d+)/$', result_sheet_pdf_view, name='result_sheet_pdf_view'),
+ url(r'^registration/form/$', course_registration_form, name='course_registration_form'),
+]
diff --git a/result/views.py b/result/views.py
new file mode 100644
index 0000000..28ea632
--- /dev/null
+++ b/result/views.py
@@ -0,0 +1,571 @@
+from django.shortcuts import render, redirect, get_object_or_404
+from django.contrib import messages
+from django.http import HttpResponseRedirect
+from django.urls import reverse_lazy
+from django.conf import settings
+from django.contrib.auth.decorators import login_required
+from django.core.paginator import Paginator
+
+from accounts.models import User, Student
+from app.models import Session, Semester
+from course.models import Course
+from accounts.decorators import lecturer_required, student_required
+from .models import TakenCourse, Result
+
+User = settings.AUTH_USER_MODEL
+
+#pdf
+from django.core.files.storage import FileSystemStorage
+from django.http import HttpResponse, JsonResponse
+
+from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image, LongTable
+from reportlab.lib.styles import getSampleStyleSheet, black, ParagraphStyle
+from reportlab.lib.enums import TA_JUSTIFY,TA_LEFT,TA_CENTER,TA_RIGHT
+from reportlab.platypus.tables import Table
+from reportlab.lib.units import inch
+from reportlab.lib import colors
+cm = 2.54
+
+from SMS.settings import MEDIA_ROOT, BASE_DIR, STATIC_URL
+import os
+
+from .models import *
+# ########################################################
+# Score Add & Add for
+# ########################################################
+@login_required
+@lecturer_required
+def add_score(request):
+ """
+ Shows a page where a lecturer will select a course allocated to him for score entry.
+ in a specific semester and session
+
+ """
+ current_session = Session.objects.get(is_current_session=True)
+ current_semester = get_object_or_404(Semester, is_current_semester=True, session=current_session)
+ # semester = Course.objects.filter(allocated_course__lecturer__pk=request.user.id, semester=current_semester)
+ courses = Course.objects.filter(allocated_course__lecturer__pk=request.user.id).filter(semester=current_semester)
+ context = {
+ "current_session": current_session,
+ "current_semester": current_semester,
+ "courses": courses,
+ }
+ return render(request, 'result/add_score.html', context)
+
+
+@login_required
+@lecturer_required
+def add_score_for(request, id):
+ """
+ Shows a page where a lecturer will add score for students that are taking courses allocated to him
+ in a specific semester and session
+ """
+ current_session = Session.objects.get(is_current_session=True)
+ current_semester = get_object_or_404(Semester, is_current_semester=True, session=current_session)
+ if request.method == 'GET':
+ courses = Course.objects.filter(allocated_course__lecturer__pk=request.user.id).filter(
+ semester=current_semester)
+ course = Course.objects.get(pk=id)
+ # myclass = Class.objects.get(lecturer__pk=request.user.id)
+ # myclass = get_object_or_404(Class, lecturer__pk=request.user.id)
+
+ # students = TakenCourse.objects.filter(course__allocated_course__lecturer__pk=request.user.id).filter(
+ # course__id=id).filter(student__allocated_student__lecturer__pk=request.user.id).filter(
+ # course__semester=current_semester)
+ students = TakenCourse.objects.filter(course__allocated_course__lecturer__pk=request.user.id).filter(
+ course__id=id).filter(course__semester=current_semester)
+ context = {
+ "title": "Submit Score | DjangoSMS",
+ "courses": courses,
+ "course": course,
+ # "myclass": myclass,
+ "students": students,
+ "current_session": current_session,
+ "current_semester": current_semester,
+ }
+ return render(request, 'result/add_score_for.html', context)
+
+ if request.method == 'POST':
+ ids = ()
+ data = request.POST.copy()
+ data.pop('csrfmiddlewaretoken', None) # remove csrf_token
+ for key in data.keys():
+ ids = ids + (str(key),) # gather all the all students id (i.e the keys) in a tuple
+ for s in range(0, len(ids)): # iterate over the list of student ids gathered above
+ student = TakenCourse.objects.get(id=ids[s])
+ # print(student)
+ # print(student.student)
+ # print(student.student.department.id)
+ courses = Course.objects.filter(level=student.student.level).filter(program__pk=student.student.department.id).filter(
+ semester=current_semester) # all courses of a specific level in current semester
+ total_credit_in_semester = 0
+ for i in courses:
+ if i == courses.count():
+ break
+ else:
+ total_credit_in_semester += int(i.credit)
+ score = data.getlist(ids[s]) # get list of score for current student in the loop
+ assignment = score[0] # subscript the list to get the fisrt value > ca score
+ mid_exam = score[1] # do the same for exam score
+ quiz = score[2]
+ attendance = score[3]
+ final_exam = score[4]
+ obj = TakenCourse.objects.get(pk=ids[s]) # get the current student data
+ obj.assignment = assignment # set current student assignment score
+ obj.mid_exam = mid_exam # set current student mid_exam score
+ obj.quiz = quiz # set current student quiz score
+ obj.attendance = attendance # set current student attendance score
+ obj.final_exam = final_exam # set current student final_exam score
+
+ obj.total = obj.get_total(assignment=assignment, mid_exam=mid_exam, quiz=quiz, attendance=attendance, final_exam=final_exam)
+ obj.grade = obj.get_grade(total=obj.total)
+
+ # obj.total = obj.get_total(assignment, mid_exam, quiz, attendance, final_exam)
+ # obj.grade = obj.get_grade(assignment, mid_exam, quiz, attendance, final_exam)
+
+ obj.point = obj.get_point(grade=obj.grade)
+
+ obj.comment = obj.get_comment(grade=obj.grade)
+ # obj.carry_over(obj.grade)
+ # obj.is_repeating()
+ obj.save()
+ gpa = obj.calculate_gpa(total_credit_in_semester)
+ cgpa = obj.calculate_cgpa()
+
+ try:
+ a = Result.objects.get(student=student.student, semester=current_semester, session=current_session, level=student.student.level)
+ a.gpa = gpa
+ a.cgpa = cgpa
+ a.save()
+ except:
+ Result.objects.get_or_create(student=student.student, gpa=gpa, semester=current_semester,
+ session=current_session, level=student.student.level)
+
+ # try:
+ # a = Result.objects.get(student=student.student, semester=current_semester, level=student.student.level)
+ # a.gpa = gpa
+ # a.cgpa = cgpa
+ # a.save()
+ # except:
+ # Result.objects.get_or_create(student=student.student, gpa=gpa, semester=current_semester, level=student.student.level)
+
+ messages.success(request, 'Successfully Recorded! ')
+ return HttpResponseRedirect(reverse_lazy('add_score_for', kwargs={'id': id}))
+ return HttpResponseRedirect(reverse_lazy('add_score_for', kwargs={'id': id}))
+# ########################################################
+
+
+@login_required
+@student_required
+def grade_result(request):
+ student = Student.objects.get(student__pk=request.user.id)
+ courses = TakenCourse.objects.filter(student__student__pk=request.user.id).filter(course__level=student.level)
+ # total_credit_in_semester = 0
+ results = Result.objects.filter(student__student__pk=request.user.id)
+
+ result_set = set()
+
+ for result in results:
+ result_set.add(result.session)
+
+ sorted_result = sorted(result_set)
+
+ total_first_semester_credit = 0
+ total_sec_semester_credit = 0
+ for i in courses:
+ if i.course.semester == "First":
+ total_first_semester_credit += int(i.course.credit)
+ if i.course.semester == "Second":
+ total_sec_semester_credit += int(i.course.credit)
+
+ previousCGPA = 0
+ # previousLEVEL = 0
+ # calculate_cgpa
+ for i in results:
+ previousLEVEL = i.level
+ try:
+ a = Result.objects.get(student__student__pk=request.user.id, level=previousLEVEL, semester="Second")
+ previousCGPA = a.cgpa
+ break
+ except:
+ previousCGPA = 0
+
+ context = {
+ "courses": courses,
+ "results": results,
+ "sorted_result": sorted_result,
+ "student": student,
+ 'total_first_semester_credit': total_first_semester_credit,
+ 'total_sec_semester_credit': total_sec_semester_credit,
+ 'total_first_and_second_semester_credit': total_first_semester_credit + total_sec_semester_credit,
+ "previousCGPA": previousCGPA,
+ }
+
+ return render(request, 'result/grade_results.html', context)
+
+
+@login_required
+@student_required
+def assessment_result(request):
+ student = Student.objects.get(student__pk=request.user.id)
+ courses = TakenCourse.objects.filter(student__student__pk=request.user.id, course__level=student.level)
+ result = Result.objects.filter(student__student__pk=request.user.id)
+
+ context = {
+ "courses": courses,
+ "result": result,
+ "student": student,
+ }
+
+ return render(request, 'result/assessment_results.html', context)
+
+
+@login_required
+@lecturer_required
+def result_sheet_pdf_view(request, id):
+ current_semester = Semester.objects.get(is_current_semester=True)
+ current_session = Session.objects.get(is_current_session=True)
+ result = TakenCourse.objects.filter(course__pk=id)
+ course = get_object_or_404(Course, id=id)
+ no_of_pass = TakenCourse.objects.filter(course__pk=id, comment="PASS").count()
+ no_of_fail = TakenCourse.objects.filter(course__pk=id, comment="FAIL").count()
+ fname = str(current_semester) + '_semester_' + str(current_session) + '_' + str(course) + '_resultSheet.pdf'
+ fname = fname.replace("/", "-")
+ flocation = settings.MEDIA_ROOT + "/result_sheet/" + fname
+
+ doc = SimpleDocTemplate(flocation, rightMargin=0, leftMargin=6.5 * cm, topMargin=0.3 * cm, bottomMargin=0)
+ styles = getSampleStyleSheet()
+ styles.add(ParagraphStyle( name="ParagraphTitle", fontSize=11, fontName="FreeSansBold"))
+ Story = [Spacer(1,.2)]
+ style = styles["Normal"]
+
+ # picture = request.user.picture
+ # l_pic = Image(picture, 1*inch, 1*inch)
+ # l_pic.__setattr__("_offs_x", 200)
+ # l_pic.__setattr__("_offs_y", -130)
+ # Story.append(l_pic)
+
+ # logo = settings.MEDIA_ROOT + "/logo/logo-mini.png"
+ # im_logo = Image(logo, 1*inch, 1*inch)
+ # im_logo.__setattr__("_offs_x", -218)
+ # im_logo.__setattr__("_offs_y", -60)
+ # Story.append(im_logo)
+
+ logo = settings.MEDIA_ROOT + "/logo/you-logo-here.png"
+ im = Image(logo, 1*inch, 1*inch)
+ im.__setattr__("_offs_x", -200)
+ im.__setattr__("_offs_y", -45)
+ Story.append(im)
+
+ style = getSampleStyleSheet()
+ normal = style["Normal"]
+ normal.alignment = TA_CENTER
+ normal.fontName = "Helvetica"
+ normal.fontSize = 12
+ normal.leading = 15
+ title = " "+str(current_semester) + " Semester " + str(current_session) + " Result Sheet "
+ title = Paragraph(title.upper(), normal)
+ Story.append(title)
+ Story.append(Spacer(1,0.1*inch))
+
+ style = getSampleStyleSheet()
+ normal = style["Normal"]
+ normal.alignment = TA_CENTER
+ normal.fontName = "Helvetica"
+ normal.fontSize = 10
+ normal.leading = 15
+ title = "Course lecturer: " + request.user.get_full_name + " "
+ title = Paragraph(title.upper(), normal)
+ Story.append(title)
+ Story.append(Spacer(1,0.1*inch))
+
+ normal = style["Normal"]
+ normal.alignment = TA_CENTER
+ normal.fontName = "Helvetica"
+ normal.fontSize = 10
+ normal.leading = 15
+ level = result.filter(course_id=id).first()
+ title = "Level: " + str(level.course.level)
+ title = Paragraph(title.upper(), normal)
+ Story.append(title)
+ Story.append(Spacer(1,.6*inch))
+
+ elements = []
+ count = 0
+ header = [('S/N', 'ID NO.', 'FULL NAME', 'TOTAL', 'GRADE', 'POINT', 'COMMENT')]
+
+ table_header = Table(header, [inch], [0.5*inch])
+ table_header.setStyle(
+ TableStyle([
+ ('BACKGROUND',(0,0),(-1,-1),colors.black),
+ ('TEXTCOLOR',(1,0),(-1,-1),colors.white),
+ ('TEXTCOLOR',(0,0),(0,0),colors.cyan),
+ ('ALIGN',(0,0),(-1,-1),'CENTER'),
+ ('VALIGN',(0,0),(-1,-1),'MIDDLE'),
+ ('BOX',(0,0),(-1,-1),1,colors.black),
+ ]))
+ Story.append(table_header)
+
+ for student in result:
+
+ data = [(count+1, student.student.student.username.upper(), Paragraph(student.student.student.get_full_name.capitalize(), styles['Normal']),
+ student.total, student.grade, student.point, student.comment)]
+ color = colors.black
+ if student.grade == 'F':
+ color = colors.red
+ count += 1
+
+ t_body = Table(data, colWidths=[inch])
+ t_body.setStyle(
+ TableStyle([
+ ('INNERGRID', (0,0), (-1,-1), 0.05, colors.black),
+ ('BOX', (0,0), (-1,-1), 0.1, colors.black),
+ ]))
+ Story.append(t_body)
+
+ Story.append(Spacer(1,1*inch))
+ style_right = ParagraphStyle(name='right', parent=styles['Normal'], alignment=TA_RIGHT)
+ tbl_data = [
+ [Paragraph("Date: _____________________________", styles["Normal"]), Paragraph("No. of PASS: " + str(no_of_pass), style_right)],
+ [Paragraph("Siganture / Stamp: _____________________________", styles["Normal"]), Paragraph("No. of FAIL: " + str(no_of_fail), style_right)]]
+ tbl = Table(tbl_data)
+ Story.append(tbl)
+
+ doc.build(Story)
+
+ fs = FileSystemStorage(settings.MEDIA_ROOT + "/result_sheet")
+ with fs.open(fname) as pdf:
+ response = HttpResponse(pdf, content_type='application/pdf')
+ response['Content-Disposition'] = 'inline; filename=' + fname + ''
+ return response
+ return response
+
+
+@login_required
+@student_required
+def course_registration_form(request):
+ current_semester = Semester.objects.get(is_current_semester=True)
+ current_session = Session.objects.get(is_current_session=True)
+ courses = TakenCourse.objects.filter(student__student__id=request.user.id)
+ fname = request.user.username + '.pdf'
+ fname = fname.replace("/", "-")
+ # flocation = '/tmp/' + fname
+ # print(MEDIA_ROOT + "\\" + fname)
+ flocation = settings.MEDIA_ROOT + "/registration_form/" + fname
+ doc = SimpleDocTemplate(flocation, rightMargin=15, leftMargin=15, topMargin=0, bottomMargin=0)
+ styles = getSampleStyleSheet()
+
+ Story = [Spacer(1,0.5)]
+ Story.append(Spacer(1,0.4*inch))
+ style = styles["Normal"]
+
+ style = getSampleStyleSheet()
+ normal = style["Normal"]
+ normal.alignment = TA_CENTER
+ normal.fontName = "Helvetica"
+ normal.fontSize = 12
+ normal.leading = 18
+ title = "EZOD UNIVERSITY OF TECHNOLOGY, ADAMA "
+ title = Paragraph(title.upper(), normal)
+ Story.append(title)
+ style = getSampleStyleSheet()
+
+ school = style["Normal"]
+ school.alignment = TA_CENTER
+ school.fontName = "Helvetica"
+ school.fontSize = 10
+ school.leading = 18
+ school_title = "SCHOOL OF ELECTRICAL ENGINEERING & COMPUTING "
+ school_title = Paragraph(school_title.upper(), school)
+ Story.append(school_title)
+
+ style = getSampleStyleSheet()
+ Story.append(Spacer(1,0.1*inch))
+ department = style["Normal"]
+ department.alignment = TA_CENTER
+ department.fontName = "Helvetica"
+ department.fontSize = 9
+ department.leading = 18
+ department_title = "DEPARTMENT OF COMPUTER SCIENCE & ENGINEERING "
+ department_title = Paragraph(department_title, department)
+ Story.append(department_title)
+ Story.append(Spacer(1,.3*inch))
+
+ title = "STUDENT COURSE REGISTRATION FORM "
+ title = Paragraph(title.upper(), normal)
+ Story.append(title)
+ student = Student.objects.get(student__pk=request.user.id)
+
+ style_right = ParagraphStyle(name='right', parent=styles['Normal'])
+ tbl_data = [
+ [Paragraph("Registration Number : " + request.user.username.upper() + " ", styles["Normal"])],
+ [Paragraph("Name : " + request.user.get_full_name.upper() + " ", styles["Normal"])],
+ [Paragraph("Session : " + current_session.session.upper() + " ", styles["Normal"]), Paragraph("Level: " + student.level + " ", styles["Normal"])
+ ]]
+ tbl = Table(tbl_data)
+ Story.append(tbl)
+ Story.append(Spacer(1, 0.6*inch))
+
+ style = getSampleStyleSheet()
+ semester = style["Normal"]
+ semester.alignment = TA_LEFT
+ semester.fontName = "Helvetica"
+ semester.fontSize = 9
+ semester.leading = 18
+ semester_title = "FIRST SEMESTER "
+ semester_title = Paragraph(semester_title, semester)
+ Story.append(semester_title)
+
+ elements = []
+
+ # FIRST SEMESTER
+ count = 0
+ header = [('S/No', 'Course Code', 'Course Title', 'Unit', Paragraph('Name, Siganture of course lecturer & Date', style['Normal']))]
+ table_header = Table(header,1*[1.4*inch], 1*[0.5*inch])
+ table_header.setStyle(
+ TableStyle([
+ ('ALIGN',(-2,-2), (-2,-2),'CENTER'),
+ ('VALIGN',(-2,-2), (-2,-2),'MIDDLE'),
+ ('ALIGN',(1,0), (1,0),'CENTER'),
+ ('VALIGN',(1,0), (1,0),'MIDDLE'),
+ ('ALIGN',(0,0), (0,0),'CENTER'),
+ ('VALIGN',(0,0), (0,0),'MIDDLE'),
+ ('ALIGN',(-4,0), (-4,0),'LEFT'),
+ ('VALIGN',(-4,0), (-4,0),'MIDDLE'),
+ ('ALIGN',(-3,0), (-3,0),'LEFT'),
+ ('VALIGN',(-3,0), (-3,0),'MIDDLE'),
+ ('TEXTCOLOR',(0,-1),(-1,-1),colors.black),
+ ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
+ ('BOX', (0,0), (-1,-1), 0.25, colors.black),
+ ]))
+ Story.append(table_header)
+
+ first_semester_unit = 0
+ for course in courses:
+ if course.course.semester == FIRST:
+ first_semester_unit += int(course.course.credit)
+ data = [(count+1, course.course.code.upper(), Paragraph(course.course.title, style['Normal']), course.course.credit, '')]
+ color = colors.black
+ count += 1
+ table_body=Table(data,1*[1.4*inch], 1*[0.3*inch])
+ table_body.setStyle(
+ TableStyle([
+ ('ALIGN',(-2,-2), (-2,-2),'CENTER'),
+ ('ALIGN',(1,0), (1,0),'CENTER'),
+ ('ALIGN',(0,0), (0,0),'CENTER'),
+ ('ALIGN',(-4,0), (-4,0),'LEFT'),
+ ('TEXTCOLOR',(0,-1),(-1,-1),colors.black),
+ ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
+ ('BOX', (0,0), (-1,-1), 0.25, colors.black),
+ ]))
+ Story.append(table_body)
+
+ style = getSampleStyleSheet()
+ semester = style["Normal"]
+ semester.alignment = TA_LEFT
+ semester.fontName = "Helvetica"
+ semester.fontSize = 8
+ semester.leading = 18
+ semester_title = "Total Second First Credit : " + str(first_semester_unit) + " "
+ semester_title = Paragraph(semester_title, semester)
+ Story.append(semester_title)
+
+ # FIRST SEMESTER ENDS HERE
+ Story.append(Spacer(1, 0.6*inch))
+
+ style = getSampleStyleSheet()
+ semester = style["Normal"]
+ semester.alignment = TA_LEFT
+ semester.fontName = "Helvetica"
+ semester.fontSize = 9
+ semester.leading = 18
+ semester_title = "SECOND SEMESTER "
+ semester_title = Paragraph(semester_title, semester)
+ Story.append(semester_title)
+ # SECOND SEMESTER
+ count = 0
+ header = [('S/No', 'Course Code', 'Course Title', 'Unit', Paragraph('Name, Signature of course lecturer & Date ', style['Normal']))]
+ table_header = Table(header,1*[1.4*inch], 1*[0.5*inch])
+ table_header.setStyle(
+ TableStyle([
+ ('ALIGN',(-2,-2), (-2,-2),'CENTER'),
+ ('VALIGN',(-2,-2), (-2,-2),'MIDDLE'),
+ ('ALIGN',(1,0), (1,0),'CENTER'),
+ ('VALIGN',(1,0), (1,0),'MIDDLE'),
+ ('ALIGN',(0,0), (0,0),'CENTER'),
+ ('VALIGN',(0,0), (0,0),'MIDDLE'),
+ ('ALIGN',(-4,0), (-4,0),'LEFT'),
+ ('VALIGN',(-4,0), (-4,0),'MIDDLE'),
+ ('ALIGN',(-3,0), (-3,0),'LEFT'),
+ ('VALIGN',(-3,0), (-3,0),'MIDDLE'),
+ ('TEXTCOLOR',(0,-1),(-1,-1),colors.black),
+ ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
+ ('BOX', (0,0), (-1,-1), 0.25, colors.black),
+ ]))
+ Story.append(table_header)
+
+ second_semester_unit = 0
+ for course in courses:
+ if course.course.semester == SECOND:
+ second_semester_unit += int(course.course.credit)
+ data = [(count+1, course.course.code.upper(), Paragraph(course.course.title, style['Normal']), course.course.credit, '')]
+ color = colors.black
+ count += 1
+ table_body=Table(data,1*[1.4*inch], 1*[0.3*inch])
+ table_body.setStyle(
+ TableStyle([
+ ('ALIGN',(-2,-2), (-2,-2),'CENTER'),
+ ('ALIGN',(1,0), (1,0),'CENTER'),
+ ('ALIGN',(0,0), (0,0),'CENTER'),
+ ('ALIGN',(-4,0), (-4,0),'LEFT'),
+ ('TEXTCOLOR',(0,-1),(-1,-1),colors.black),
+ ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
+ ('BOX', (0,0), (-1,-1), 0.25, colors.black),
+ ]))
+ Story.append(table_body)
+
+ style = getSampleStyleSheet()
+ semester = style["Normal"]
+ semester.alignment = TA_LEFT
+ semester.fontName = "Helvetica"
+ semester.fontSize = 8
+ semester.leading = 18
+ semester_title = "Total Second Semester Credit : " + str(second_semester_unit) + " "
+ semester_title = Paragraph(semester_title, semester)
+ Story.append(semester_title)
+
+ Story.append(Spacer(1, 2))
+ style = getSampleStyleSheet()
+ certification = style["Normal"]
+ certification.alignment = TA_JUSTIFY
+ certification.fontName = "Helvetica"
+ certification.fontSize = 8
+ certification.leading = 18
+ student = Student.objects.get(student__pk=request.user.id)
+ certification_text = "CERTIFICATION OF REGISTRATION: I certify that " + str(request.user.get_full_name.upper()) + " \
+ has been duly registered for the " + student.level + " level of study in the department\
+ of COMPUTER SICENCE & ENGINEERING and that the courses and credits registered are as approved by the senate of the University"
+ certification_text = Paragraph(certification_text, certification)
+ Story.append(certification_text)
+
+ # FIRST SEMESTER ENDS HERE
+
+ logo = MEDIA_ROOT + "/logo/you-logo-here.png"
+ im_logo = Image(logo, 1*inch, 1*inch)
+ im_logo.__setattr__("_offs_x", -218)
+ im_logo.__setattr__("_offs_y", 480)
+ Story.append(im_logo)
+
+ picture = BASE_DIR + request.user.get_picture()
+ im = Image(picture, 1.0*inch, 1.0*inch)
+ im.__setattr__("_offs_x", 218)
+ im.__setattr__("_offs_y", 550)
+ Story.append(im)
+
+ doc.build(Story)
+ fs = FileSystemStorage(settings.MEDIA_ROOT + "/registration_form")
+ with fs.open(fname) as pdf:
+ response = HttpResponse(pdf, content_type='application/pdf')
+ response['Content-Disposition'] = 'inline; filename='+fname+''
+ return response
+ return response
diff --git a/search/__init__.py b/search/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/search/admin.py b/search/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/search/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/search/apps.py b/search/apps.py
new file mode 100644
index 0000000..5726231
--- /dev/null
+++ b/search/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class SearchConfig(AppConfig):
+ name = 'search'
diff --git a/search/migrations/__init__.py b/search/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/search/models.py b/search/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/search/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/search/templates/search/search_view.html b/search/templates/search/search_view.html
new file mode 100644
index 0000000..bff5900
--- /dev/null
+++ b/search/templates/search/search_view.html
@@ -0,0 +1,133 @@
+{% extends 'base.html' %}
+{% block title %}Search result for {{ query }} | DjangoSMS{% endblock title %}
+
+{% load class_name %}
+
+{% block content %}
+
+
+
+
+
+
+
+
About {{ count }} results for {{ query }}
+
+ {% for object in object_list %}
+ {% with object|class_name as klass %}
+ {% if klass == 'Program' %}
+
+
+
+
+
{{ object.summary }}
+
+
+ {% elif klass == 'Course' %}
+
+
+
Program of {{ object.program }}
+
+
{{ object.summary }}
+
+
+ {% elif klass == 'NewsAndEvents' %}
+
+
+
Date: {{ object.updated_date|timesince }} ago
+
+
{{ object.summary }}
+
+
+ {% elif klass == 'Quiz' %}
+
+
+
{{ object.category }} quiz, Course: {{ object.course }}
+
+
{{ object.description }}
+
+
+ {% else %}
+
+
+
+
+
{{ object.description }}
+
+ {% endif %}
+
+ {% endwith %}
+
+{% empty %}
+
+
+
No result for your search
+
+
+
Search by using:
+
+ Program > Title or Description
+ Course > Title, Code or Description
+ News And Events > Title, Description or just by typing "news" or "event"
+ Quiz > Title, Description or Category(practice, assignment and exam)
+
+
+
+
+
+
+
+{% endfor %}
+
+
+{% endblock content %}
diff --git a/search/templatetags/__init__.py b/search/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/search/templatetags/class_name.py b/search/templatetags/class_name.py
new file mode 100644
index 0000000..6061bfd
--- /dev/null
+++ b/search/templatetags/class_name.py
@@ -0,0 +1,8 @@
+# search.templatetags.class_name.py
+from django import template
+
+register = template.Library()
+
+@register.filter()
+def class_name(value):
+ return value.__class__.__name__
diff --git a/search/tests.py b/search/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/search/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/search/urls.py b/search/urls.py
new file mode 100644
index 0000000..6fd1b6b
--- /dev/null
+++ b/search/urls.py
@@ -0,0 +1,8 @@
+from django.conf.urls import url
+
+from .views import SearchView
+
+urlpatterns = [
+ url(r'^$', SearchView.as_view(), name='query'),
+]
+
diff --git a/search/views.py b/search/views.py
new file mode 100644
index 0000000..d8e1e88
--- /dev/null
+++ b/search/views.py
@@ -0,0 +1,83 @@
+# from django.shortcuts import render
+# from django.views.generic import ListView
+# from app.models import NewsAndEvents
+
+
+# class SearchNewsAndEventsView(ListView):
+# template_name = "search/search_view.html"
+
+# def get_context_data(self, *args, **kwargs):
+# context = super(SearchNewsAndEventsView, self).get_context_data(*args, **kwargs)
+# query = self.request.GET.get('q')
+# context['query'] = query
+# context['obj_counter'] = NewsAndEvents.objects.search(query).count()
+# # SearchQuery.objects.create(query=query)
+# return context
+
+# def get_queryset(self, *args, **kwargs):
+# request = self.request
+# method_dict = request.GET
+# query = method_dict.get('q', None) # method_dict['q']
+# if query is not None:
+# return NewsAndEvents.objects.search(query)
+# return NewsAndEvents.objects.all()
+# '''
+# __icontains = field contains this
+# __iexact = fields is exactly this
+# '''
+
+
+
+
+
+
+
+
+
+
+# search.views.py
+from itertools import chain
+from django.views.generic import ListView
+
+from django.db.models import Q
+
+from accounts.models import User, Student
+from app.models import NewsAndEvents
+from course.models import Program, Course
+from quiz.models import Quiz
+
+
+class SearchView(ListView):
+ template_name = 'search/search_view.html'
+ paginate_by = 20
+ count = 0
+
+ def get_context_data(self, *args, **kwargs):
+ context = super().get_context_data(*args, **kwargs)
+ context['count'] = self.count or 0
+ context['query'] = self.request.GET.get('q')
+ return context
+
+ def get_queryset(self):
+ request = self.request
+ query = request.GET.get('q', None)
+
+ if query is not None:
+ news_events_results = NewsAndEvents.objects.search(query)
+ program_results = Program.objects.search(query)
+ course_results = Course.objects.search(query)
+ quiz_results = Quiz.objects.search(query)
+
+ # combine querysets
+ queryset_chain = chain(
+ news_events_results,
+ program_results,
+ course_results,
+ quiz_results
+ )
+ qs = sorted(queryset_chain,
+ key=lambda instance: instance.pk,
+ reverse=True)
+ self.count = len(qs) # since qs is actually a list
+ return qs
+ return NewsAndEvents.objects.none() # just an empty queryset as default
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..080b53d
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,243 @@
+{% load static %}
+
+
+
+
+
+
+ {% block title %}DjangoSMS{% endblock title %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ request.user.get_user_role }}
+
+
+
+ {% url 'home' as hom %} {% url 'profile' as prof %} {% url 'lecturer_list' as lec %}
+ {% url 'student_list' as stu %} {% url 'course_allocation_view' as cav %}
+ {% url 'programs' as pro %} {% url 'session_list' as sess %} {% url 'semester_list' as sem %}
+ {% url 'add_score' as ascore %} {% url 'grade_results' as vr %}{% url 'ass_results' as ar %}
+ {% url 'course_registration' as cr %} {% url 'edit_profile' as ep %} {% url 'change_password' as cp %}
+ {% url 'quiz_progress' as qpr %} {% url 'quiz_marking' as qce %} {% url 'user_course_list' as ucl %}
+ {% url 'admin_panel' as admin_p %}
+
+
+
+
+
+
+ {% block content %}{% endblock content %}
+
+
+
+
+
+
+
+
+
+
+
+
+ {% block js %}
+ {% endblock js %}
+
+
+
+
+
+