Merge branch 'main' into feature/auto-populate-data
This commit is contained in:
commit
14448249c4
78
README.md
78
README.md
@ -6,39 +6,39 @@ Let's enhance the project by contributing! 👩💻👩💻
|
||||
|
||||

|
||||
|
||||
Current features
|
||||
----------------
|
||||
* Dashboard: School demographics and analytics. Restricted to only admins
|
||||
* News And Events: All users can access this page
|
||||
* Admin manages students(Add, Update, Delete)
|
||||
* Admin manages lecturers(Add, Update, Delete)
|
||||
* Students can Add and Drop courses
|
||||
* Lecturers submit students' scores: _Attendance, Mid exam, Final exam, assignment_
|
||||
* The system calculates students' _Total, average, point, and grades automatically_
|
||||
* Grade comment for each student with a **pass**, **fail**, or **pass with a warning**
|
||||
* Assessment result page for students
|
||||
* Grade result page for students
|
||||
* Session/year and semester management
|
||||
* Assessments and grades will be grouped by semester
|
||||
* Upload video and documentation for each course
|
||||
* PDF generator for students' registration slip and grade result
|
||||
* Page access restriction
|
||||
* Storing of quiz results under each user
|
||||
* Question order randomization
|
||||
* Previous quiz scores can be viewed on the 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
|
||||
## Current features
|
||||
|
||||
- Dashboard: School demographics and analytics. Restricted to only admins
|
||||
- News And Events: All users can access this page
|
||||
- Admin manages students(Add, Update, Delete)
|
||||
- Admin manages lecturers(Add, Update, Delete)
|
||||
- Students can Add and Drop courses
|
||||
- Lecturers submit students' scores: _Attendance, Mid exam, Final exam, assignment_
|
||||
- The system calculates students' _Total, average, point, and grades automatically_
|
||||
- Grade comment for each student with a **pass**, **fail**, or **pass with a warning**
|
||||
- Assessment result page for students
|
||||
- Grade result page for students
|
||||
- Session/year and semester management
|
||||
- Assessments and grades will be grouped by semester
|
||||
- Upload video and documentation for each course
|
||||
- PDF generator for students' registration slip and grade result
|
||||
- Page access restriction
|
||||
- Storing of quiz results under each user
|
||||
- Question order randomization
|
||||
- Previous quiz scores can be viewed on the 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
|
||||
|
||||
# Quick note for future contributors
|
||||
|
||||
@ -68,14 +68,19 @@ pip install -r requirements.txt
|
||||
- Create `.env` file inside the root directory and include the following variables
|
||||
|
||||
```bash
|
||||
# Database config
|
||||
DB_NAME=[YOUR_DB_NAME]
|
||||
DB_USER=[DB_ADMIN_NAME]
|
||||
DB_PASSWORD=[DB_ADMIN_PASSWORD]
|
||||
DB_HOST=localhost
|
||||
DB_PORT=[YOUR_POSTGRES_PORT default is 5432]
|
||||
USER_EMAIL=[YOUR_EMAIL]
|
||||
USER_PASSWORD=[EMAIL_PASSWORD]
|
||||
EMAIL_FROM_ADDRESS=[THE DEFAULT FROM ADDRESS FOR SENT EMAILS]
|
||||
|
||||
# Email config
|
||||
EMAIL_FROM_ADDRESS=Django LMS <youremail@example.com>
|
||||
EMAIL_HOST_USER=[YOUR_EMAIL]
|
||||
EMAIL_HOST_PASSWORD=[YOUR_EMAIL_PASSWORD]
|
||||
|
||||
# Other
|
||||
DEBUG=True
|
||||
SECRET_KEY=[YOUR_SECRET_KEY]
|
||||
```
|
||||
@ -91,6 +96,7 @@ python manage.py runserver
|
||||
Last but not least, go to this address http://127.0.0.1:8000
|
||||
|
||||
### References
|
||||
|
||||
- Quiz part: https://github.com/tomwalker/django_quiz
|
||||
|
||||
# Connect with me
|
||||
|
||||
@ -33,34 +33,42 @@ class LecturerFilter(django_filters.FilterSet):
|
||||
|
||||
|
||||
class StudentFilter(django_filters.FilterSet):
|
||||
student__username = django_filters.CharFilter(lookup_expr="exact", label="")
|
||||
student__name = django_filters.CharFilter(method="filter_by_name", label="")
|
||||
student__email = django_filters.CharFilter(lookup_expr="icontains", label="")
|
||||
program__title = django_filters.CharFilter(lookup_expr="icontains", label="")
|
||||
id_no = django_filters.CharFilter(
|
||||
field_name="student__username", lookup_expr="exact", label=""
|
||||
)
|
||||
name = django_filters.CharFilter(
|
||||
field_name="student__name", method="filter_by_name", label=""
|
||||
)
|
||||
email = django_filters.CharFilter(
|
||||
field_name="student__email", lookup_expr="icontains", label=""
|
||||
)
|
||||
program = django_filters.CharFilter(
|
||||
field_name="student__program", lookup_expr="icontains", label=""
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Student
|
||||
fields = [
|
||||
"student__username",
|
||||
"student__name",
|
||||
"student__email",
|
||||
"program__title",
|
||||
"id_no",
|
||||
"name",
|
||||
"email",
|
||||
"program",
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Change html classes and placeholders
|
||||
self.filters["student__username"].field.widget.attrs.update(
|
||||
self.filters["id_no"].field.widget.attrs.update(
|
||||
{"class": "au-input", "placeholder": "ID No."}
|
||||
)
|
||||
self.filters["student__name"].field.widget.attrs.update(
|
||||
self.filters["name"].field.widget.attrs.update(
|
||||
{"class": "au-input", "placeholder": "Name"}
|
||||
)
|
||||
self.filters["student__email"].field.widget.attrs.update(
|
||||
self.filters["email"].field.widget.attrs.update(
|
||||
{"class": "au-input", "placeholder": "Email"}
|
||||
)
|
||||
self.filters["program__title"].field.widget.attrs.update(
|
||||
self.filters["program"].field.widget.attrs.update(
|
||||
{"class": "au-input", "placeholder": "Program"}
|
||||
)
|
||||
|
||||
|
||||
@ -118,22 +118,16 @@ class StaffAddForm(UserCreationForm):
|
||||
user.address = self.cleaned_data.get("address")
|
||||
user.email = self.cleaned_data.get("email")
|
||||
|
||||
# Generate a username based on first and last name and registration date
|
||||
registration_date = datetime.now().strftime("%Y%m%d%H%M")
|
||||
# Generate a username
|
||||
registration_date = datetime.now().strftime("%Y")
|
||||
total_lecturers_count = User.objects.filter(is_lecturer=True).count()
|
||||
generated_username = (
|
||||
f"{user.first_name.lower()}{user.last_name.lower()}{registration_date}"
|
||||
f"{settings.LECTURER_ID_PREFIX}-{registration_date}-{total_lecturers_count}"
|
||||
)
|
||||
|
||||
# Check if the generated username already exists, and regenerate if needed
|
||||
while User.objects.filter(username=generated_username).exists():
|
||||
registration_date = datetime.now().strftime("%Y%m%d%H%M")
|
||||
generated_username = f"{user.first_name.lower()}{user.last_name.lower()}{registration_date}".replace(
|
||||
" ", ""
|
||||
)
|
||||
# Generate a password
|
||||
generated_password = User.objects.make_random_password()
|
||||
|
||||
user.username = generated_username
|
||||
|
||||
generated_password = User.objects.make_random_password()
|
||||
user.set_password(generated_password)
|
||||
|
||||
if commit:
|
||||
@ -141,7 +135,7 @@ class StaffAddForm(UserCreationForm):
|
||||
|
||||
# Send email with the generated credentials
|
||||
send_mail(
|
||||
"Your account credentials",
|
||||
"Your Django LMS account credentials",
|
||||
f"Your username: {generated_username}\nYour password: {generated_password}",
|
||||
"from@example.com",
|
||||
[user.email],
|
||||
@ -275,7 +269,7 @@ class StudentAddForm(UserCreationForm):
|
||||
@transaction.atomic()
|
||||
def save(self, commit=True):
|
||||
user = super().save(commit=False)
|
||||
user.is_lecturer = True
|
||||
user.is_student = True
|
||||
user.first_name = self.cleaned_data.get("first_name")
|
||||
user.last_name = self.cleaned_data.get("last_name")
|
||||
user.gender = self.cleaned_data.get("gender")
|
||||
@ -285,30 +279,29 @@ class StudentAddForm(UserCreationForm):
|
||||
user.email = self.cleaned_data.get("email")
|
||||
|
||||
# Generate a username based on first and last name and registration date
|
||||
registration_date = datetime.now().strftime("%Y%m%d%H%M")
|
||||
registration_date = datetime.now().strftime("%Y")
|
||||
total_students_count = Student.objects.count()
|
||||
generated_username = (
|
||||
f"{user.first_name.lower()}{user.last_name.lower()}{registration_date}"
|
||||
f"{settings.STUDENT_ID_PREFIX}-{registration_date}-{total_students_count}"
|
||||
)
|
||||
|
||||
# Check if the generated username already exists, and regenerate if needed
|
||||
while User.objects.filter(username=generated_username).exists():
|
||||
registration_date = datetime.now().strftime("%Y%m%d%H%M")
|
||||
generated_username = f"{user.first_name.lower()}{user.last_name.lower()}{registration_date}".replace(
|
||||
" ", ""
|
||||
)
|
||||
# Generate a password
|
||||
generated_password = User.objects.make_random_password()
|
||||
|
||||
user.username = generated_username
|
||||
|
||||
generated_password = User.objects.make_random_password()
|
||||
user.set_password(generated_password)
|
||||
|
||||
if commit:
|
||||
user.save()
|
||||
Student.objects.create(
|
||||
student=user,
|
||||
level=self.cleaned_data.get("level"),
|
||||
program=self.cleaned_data.get("program"),
|
||||
)
|
||||
|
||||
# Send email with the generated credentials
|
||||
send_mail(
|
||||
"Your account credentials",
|
||||
f"Your username: {generated_username}\nYour password: {generated_password}",
|
||||
"Your Django LMS account credentials",
|
||||
f"Your ID: {generated_username}\nYour password: {generated_password}",
|
||||
settings.EMAIL_FROM_ADDRESS,
|
||||
[user.email],
|
||||
fail_silently=False,
|
||||
@ -348,6 +341,15 @@ class ProfileUpdateForm(UserChangeForm):
|
||||
label="Last Name",
|
||||
)
|
||||
|
||||
gender = forms.CharField(
|
||||
widget=forms.Select(
|
||||
choices=GENDERS,
|
||||
attrs={
|
||||
"class": "browser-default custom-select form-control",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
phone = forms.CharField(
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
@ -370,7 +372,15 @@ class ProfileUpdateForm(UserChangeForm):
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["email", "phone", "address", "picture", "first_name", "last_name"]
|
||||
fields = [
|
||||
"first_name",
|
||||
"last_name",
|
||||
"gender",
|
||||
"email",
|
||||
"phone",
|
||||
"address",
|
||||
"picture",
|
||||
]
|
||||
|
||||
|
||||
class EmailValidationOnForgotPassword(PasswordResetForm):
|
||||
|
||||
0
accounts/signals.py
Normal file
0
accounts/signals.py
Normal file
@ -354,6 +354,7 @@ def edit_student(request, pk):
|
||||
|
||||
@method_decorator([login_required, admin_required], name="dispatch")
|
||||
class StudentListView(FilterView):
|
||||
queryset = Student.objects.all()
|
||||
filterset_class = StudentFilter
|
||||
template_name = "accounts/student_list.html"
|
||||
paginate_by = 10
|
||||
|
||||
@ -193,13 +193,11 @@ MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
||||
EMAIL_BACKEND = config(
|
||||
"EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend"
|
||||
)
|
||||
EMAIL_HOST = config(
|
||||
"EMAIL_HOST", default="smtp.gmail.com"
|
||||
) # Gmail as the email host, but you can change it
|
||||
EMAIL_HOST = config("EMAIL_HOST", default="smtp.gmail.com")
|
||||
EMAIL_PORT = config("EMAIL_PORT", default=587)
|
||||
EMAIL_USE_TLS = True
|
||||
EMAIL_HOST_USER = config("USER_EMAIL")
|
||||
EMAIL_HOST_PASSWORD = config("USER_PASSWORD")
|
||||
EMAIL_HOST_USER = config("EMAIL_HOST_USER")
|
||||
EMAIL_HOST_PASSWORD = config("EMAIL_HOST_PASSWORD")
|
||||
EMAIL_FROM_ADDRESS = config("EMAIL_FROM_ADDRESS")
|
||||
|
||||
# crispy config
|
||||
@ -250,3 +248,6 @@ LOGGING = {
|
||||
|
||||
# WhiteNoise configuration
|
||||
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||
|
||||
STUDENT_ID_PREFIX = config("STUDENT_ID_PREFIX", "ugr")
|
||||
LECTURER_ID_PREFIX = config("LECTURER_ID_PREFIX", "lec")
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.conf import settings
|
||||
|
||||
from accounts.decorators import admin_required, lecturer_required
|
||||
from accounts.models import User, Student
|
||||
from .forms import SessionForm, SemesterForm, NewsAndEventsForm
|
||||
from .models import *
|
||||
from .models import NewsAndEvents, ActivityLog, Session, Semester
|
||||
|
||||
|
||||
# ########################################################
|
||||
@ -28,9 +27,9 @@ def dashboard_view(request):
|
||||
logs = ActivityLog.objects.all().order_by("-created_at")[:10]
|
||||
gender_count = Student.get_gender_count()
|
||||
context = {
|
||||
"student_count": User.get_student_count(),
|
||||
"lecturer_count": User.get_lecturer_count(),
|
||||
"superuser_count": User.get_superuser_count(),
|
||||
"student_count": User.objects.get_student_count(),
|
||||
"lecturer_count": User.objects.get_lecturer_count(),
|
||||
"superuser_count": User.objects.get_superuser_count(),
|
||||
"males_count": gender_count["M"],
|
||||
"females_count": gender_count["F"],
|
||||
"logs": logs,
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
{{ form.email|as_crispy_field }}
|
||||
{{ form.first_name|as_crispy_field }}
|
||||
{{ form.last_name|as_crispy_field }}
|
||||
{{ form.gender|as_crispy_field }}
|
||||
{{ form.phone|as_crispy_field }}
|
||||
{{ form.address|as_crispy_field }}
|
||||
</div>
|
||||
|
||||
@ -18,20 +18,7 @@
|
||||
|
||||
<p class="title-1"><i class="fas fa-chalkboard-teacher"></i>Lecturers</p>
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
{% if message.tags == 'error' %}
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% include 'snippets/messages.html' %}
|
||||
{% include 'snippets/filter_form.html' %}
|
||||
|
||||
<div class="table-responsive table-shadow table-light table-striped m-0 mt-4">
|
||||
|
||||
@ -22,20 +22,8 @@
|
||||
<div class="title-1"><i class="fas fa-user-graduate"></i>Students</div>
|
||||
<br>
|
||||
<br>
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
{% if message.tags == 'error' %}
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle"></i>{{ message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% include 'snippets/messages.html' %}
|
||||
{% include 'snippets/filter_form.html' %}
|
||||
|
||||
<div class="table-responsive table-shadow table-light table-striped m-0 mt-4">
|
||||
|
||||
@ -305,6 +305,9 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
|
||||
<script>
|
||||
const malesCount = {{ males_count }}
|
||||
const femalesCount = {{ females_count }}
|
||||
|
||||
$(document).ready(function () {
|
||||
|
||||
// Setup
|
||||
@ -478,7 +481,7 @@
|
||||
],
|
||||
datasets: [{
|
||||
label: "Students Gender Dataset",
|
||||
data: [{{ males_count }}, {{ females_count }}],
|
||||
data: [malesCount, femalesCount],
|
||||
backgroundColor: [
|
||||
'rgb(255, 99, 132)',
|
||||
'rgb(54, 162, 235)'
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
{{ form.email|as_crispy_field }}
|
||||
{{ form.first_name|as_crispy_field }}
|
||||
{{ form.last_name|as_crispy_field }}
|
||||
{{ form.gender|as_crispy_field }}
|
||||
{{ form.phone|as_crispy_field }}
|
||||
{{ form.address|as_crispy_field }}
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user