diff --git a/accounts/admin.py b/accounts/admin.py index e7f44f6e..b1029136 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -1,19 +1,24 @@ from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from .models import User, Student, Parent +@admin.register(User) +class UserAdmin(BaseUserAdmin): + model = User -class UserAdmin(admin.ModelAdmin): - list_display = [ + list_display = ( "get_full_name", "username", "email", "is_active", "is_student", "is_lecturer", + "is_dep_head", "is_parent", "is_staff", - ] - search_fields = [ + ) + + search_fields = ( "username", "first_name", "last_name", @@ -22,14 +27,42 @@ class UserAdmin(admin.ModelAdmin): "is_lecturer", "is_parent", "is_staff", - ] + ) - class Meta: - managed = True - verbose_name = "User" - verbose_name_plural = "Users" + # Edit form fields + fieldsets = ( + (None, {"fields": ("username", "password")}), + ("Personal info", {"fields": ("first_name", "last_name", "email", "gender", "phone", "address", "picture", "program", "department")}), + ("Permissions", {"fields": ("is_staff", "is_active", "is_student", "is_lecturer", "is_parent", "is_dep_head")}), + ) + # Add form fields (important!) + add_fieldsets = ( + (None, { + "classes": ("wide",), + "fields": ( + "username", + "email", + "password1", + "password2", + "first_name", + "last_name", + "gender", + "phone", + "address", + "picture", + "program", + "department", + "is_staff", + "is_active", + "is_student", + "is_lecturer", + "is_parent", + "is_dep_head", # ✅ now visible on Add form + ), + }), + ) -admin.site.register(User, UserAdmin) admin.site.register(Student) admin.site.register(Parent) + diff --git a/accounts/decoratins.py b/accounts/decoratins.py new file mode 100644 index 00000000..e69de29b diff --git a/accounts/decorators.py b/accounts/decorators.py index 97059b30..fefd029d 100644 --- a/accounts/decorators.py +++ b/accounts/decorators.py @@ -1,5 +1,5 @@ from django.shortcuts import redirect - +from django.contrib import messages def admin_required( function=None, @@ -71,3 +71,13 @@ def wrapper(request, *args, **kwargs): return redirect(redirect_to) return wrapper if function else test_func + +def department_head_required(view_func): + def wrapper(request, *args, **kwargs): + if not request.user.is_authenticated: + return redirect("login") + if not request.user.is_dep_head: + messages.error(request, "Access denied") + return redirect("dashboard") + return view_func(request, *args, **kwargs) + return wrapper diff --git a/config/urls.py b/config/urls.py index 74aa797b..478d5ab7 100644 --- a/config/urls.py +++ b/config/urls.py @@ -11,6 +11,7 @@ urlpatterns = [ path("admin/", admin.site.urls), path("i18n/", include("django.conf.urls.i18n")), + path('courses/', include('course.urls')), ] urlpatterns += i18n_patterns( diff --git a/core/admin.py b/core/admin.py index 6bec2bf0..4f4769e2 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,7 +1,10 @@ from django.contrib import admin from modeltranslation.admin import TranslationAdmin from .models import Session, Semester, NewsAndEvents +from django.contrib import admin +from .models import AcademicCalendar +admin.site.register(AcademicCalendar) class NewsAndEventsAdmin(TranslationAdmin): pass diff --git a/core/migrations/0004_academiccalendar.py b/core/migrations/0004_academiccalendar.py new file mode 100644 index 00000000..1ef419b4 --- /dev/null +++ b/core/migrations/0004_academiccalendar.py @@ -0,0 +1,21 @@ +# Generated by Django 4.0.8 on 2025-12-15 09:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0003_newsandevents_summary_es_newsandevents_summary_fr_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='AcademicCalendar', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('add_drop_start', models.DateField()), + ('add_drop_end', models.DateField()), + ], + ), + ] diff --git a/core/models.py b/core/models.py index b0881fde..09edc25c 100644 --- a/core/models.py +++ b/core/models.py @@ -91,3 +91,14 @@ class ActivityLog(models.Model): def __str__(self): return f"[{self.created_at}]{self.message}" + +class AcademicCalendar(models.Model): + add_drop_start = models.DateField() + add_drop_end = models.DateField() + + def is_add_drop_open(self): + today = timezone.now().date() + return self.add_drop_start <= today <= self.add_drop_end + + def __str__(self): + return "Academic Calendar" diff --git a/course/admin.py b/course/admin.py index 3a304115..c90b525c 100644 --- a/course/admin.py +++ b/course/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin from django.contrib.auth.models import Group -from .models import Program, Course, CourseAllocation, Upload +from .models import Program, Course, CourseAllocation, Upload, Enrollment from modeltranslation.admin import TranslationAdmin class ProgramAdmin(TranslationAdmin): @@ -10,6 +10,10 @@ class CourseAdmin(TranslationAdmin): pass class UploadAdmin(TranslationAdmin): pass +@admin.register(Enrollment) +class EnrollmentAdmin(admin.ModelAdmin): + list_display = ('student', 'course', 'enrolled_on') + list_filter = ('course',) admin.site.register(Program, ProgramAdmin) admin.site.register(Course, CourseAdmin) diff --git a/course/migrations/0005_enrollment.py b/course/migrations/0005_enrollment.py new file mode 100644 index 00000000..9ef2c48d --- /dev/null +++ b/course/migrations/0005_enrollment.py @@ -0,0 +1,28 @@ +# Generated by Django 4.0.8 on 2025-12-15 12:52 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('course', '0004_alter_course_code_alter_course_credit_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Enrollment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('enrolled_on', models.DateTimeField(auto_now_add=True)), + ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course.course')), + ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('student', 'course')}, + }, + ), + ] diff --git a/course/models.py b/course/models.py index 37377458..727e706a 100644 --- a/course/models.py +++ b/course/models.py @@ -254,3 +254,20 @@ class CourseOffer(models.Model): def __str__(self): return str(self.dep_head) + +class Enrollment(models.Model): + student = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE + ) + course = models.ForeignKey( + Course, + on_delete=models.CASCADE + ) + enrolled_on = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = ('student', 'course') + + def __str__(self): + return f"{self.student} -> {self.course}" diff --git a/course/templates/course/manage_courses.html b/course/templates/course/manage_courses.html new file mode 100644 index 00000000..982806dc --- /dev/null +++ b/course/templates/course/manage_courses.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% block content %} +

Add / Drop Courses

+ +
+ {% csrf_token %} + + + + + + + + + + {% for course in courses %} + + + + + + {% endfor %} + +
SelectCourseCode
+ + {{ course.title }}{{ course.code }}
+ + +
+{% endblock %} + + diff --git a/course/urls.py b/course/urls.py index ef29b76a..3f2262a0 100644 --- a/course/urls.py +++ b/course/urls.py @@ -1,6 +1,6 @@ from django.urls import path from . import views - +from .views import add_drop_view urlpatterns = [ # Program urls @@ -74,4 +74,8 @@ path("course/registration/", views.course_registration, name="course_registration"), path("course/drop/", views.course_drop, name="course_drop"), path("my_courses/", views.user_course_list, name="user_course_list"), + path("add-drop/", add_drop_view, name="add_drop"), + path("manage-courses/", views.manage_courses, name="manage_courses"), + path("en/programs/course/registration/", views.manage_courses, name="manage_courses"), + ] diff --git a/course/views.py b/course/views.py index 73466e00..2b68fc96 100644 --- a/course/views.py +++ b/course/views.py @@ -28,7 +28,9 @@ UploadVideo, ) from result.models import TakenCourse - +from accounts.decorators import department_head_required +from core.models import AcademicCalendar +from .models import Course, Enrollment # ######################################################## # Program Views @@ -502,3 +504,90 @@ def user_course_list(request): # For other users return render(request, "course/user_course_list.html") + +@department_head_required +def add_drop_view(request): + + # Calendar restriction + calendar = AcademicCalendar.objects.first() + if not calendar or not calendar.is_add_drop_open(): + messages.error(request, "Add/Drop period is closed") + return redirect("dashboard") + + # Get courses allocated to this department head + allocation = CourseAllocation.objects.filter(lecturer=request.user).first() + + if not allocation: + messages.error(request, "No courses assigned to your department.") + courses = [] + else: + courses = allocation.courses.all() + + # Render page + return render( + request, + "course/add_drop.html", + {"courses": courses} + ) + + +@login_required +def manage_courses(request): + all_courses = Course.objects.all() + + enrolled_courses = Enrollment.objects.filter( + student=request.user + ).values_list('course_id', flat=True) + + if request.method == "POST": + selected_courses = request.POST.getlist("course_ids") + + # Drop unchecked courses + Enrollment.objects.filter( + student=request.user + ).exclude(course_id__in=selected_courses).delete() + + # Add newly selected courses + for course_id in selected_courses: + Enrollment.objects.get_or_create( + student=request.user, + course_id=course_id + ) + + return redirect("manage_courses") + + return render( + request, + "course/manage_courses.html", + { + "courses": all_courses, + "enrolled_courses": enrolled_courses + } + ) + +@login_required +def course_registration_view(request): + all_courses = Course.objects.all() + enrolled_courses = Enrollment.objects.filter( + student=request.user + ).values_list('course_id', flat=True) + + if request.method == "POST": + selected_courses = request.POST.getlist("course_ids") + + # Drop unchecked courses + Enrollment.objects.filter(student=request.user)\ + .exclude(course_id__in=selected_courses).delete() + + # Add newly selected courses + for course_id in selected_courses: + Enrollment.objects.get_or_create(student=request.user, course_id=course_id) + + return redirect("manage_courses") + + context = { + "courses": all_courses, + "enrolled_courses": enrolled_courses, + # include other existing context variables like total_registered_credit + } + return render(request, "course/course_registration.html", context) diff --git a/templates/course/add_drop.html b/templates/course/add_drop.html new file mode 100644 index 00000000..6c966520 --- /dev/null +++ b/templates/course/add_drop.html @@ -0,0 +1,21 @@ +

Add / Drop Courses

+ +{% for message in messages %} +

{{ message }}

+{% endfor %} + + + + + + + + {% for course in courses %} + + + + + + {% endfor %} +
CourseCodeProgram
{{ course.title }}{{ course.code }}{{ course.program }}
+ diff --git a/templates/course/base.html b/templates/course/base.html new file mode 100644 index 00000000..3648ad92 --- /dev/null +++ b/templates/course/base.html @@ -0,0 +1,4 @@ +{% if user.is_dep_head %} + Add / Drop +{% endif %} + diff --git a/templates/course/course_registration.html b/templates/course/course_registration.html index 3afbec0d..90a2a02c 100644 --- a/templates/course/course_registration.html +++ b/templates/course/course_registration.html @@ -209,46 +209,8 @@
{% trans 'Check the university calender' %} - {% for course in registered_courses %} - - - - - {{ course.code }} - {{ course.title }} - {{ course.credit }} - {{ course.year }} - {% if course.is_elective %} - {% trans 'Elective' %} - {% else %} - {% trans 'Core' %} - {% endif %} - - - - {% empty %} - - - - - - - {% trans 'No Course.' %} - - - - - - - {% endfor %} - - - - - - - {% trans 'Total credit(s):' %} {{ total_registered_credit }} - - + {% include "course/manage_courses.html" %} + @@ -265,4 +227,4 @@
{% trans 'Check the university calender' %}Add / Drop Courses
+ +
+ {% csrf_token %} + + + + + + + + + + {% for course in courses %} + + + + + + {% endfor %} + +
SelectCourseCode
+ + {{ course.title }}{{ course.code }}
+ + +
+{% endblock %} + diff --git a/urls.py b/urls.py new file mode 100644 index 00000000..08f41fc8 --- /dev/null +++ b/urls.py @@ -0,0 +1,8 @@ +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + path('courses/', include('course.urls')), # <- this line is required +] +