huge updates

This commit is contained in:
sankalp committed 2025-04-15 04:20:26 +05:45
1 parent 2912191091
commit 8507d06ccf
82 files changed
+14911 -4509

No files matched your search

+353 -87
View File
@@ -1,143 +1,204 @@
from django.contrib import admin
from .models import (
Institute,
Course,
Subject,
SubjectFile,
Student,
Staff,
Batch,
Attendance,
AttendanceRecord,
Notice,
Routine,
Staff_leave,
Student_Leave,
StudentFeedback,
CourseTracking,
InstituteFeedback,
StaffInstituteFeedback,
TOTPSecret,
ResetToken,
)
from .firebase import FCMDevice
from django.forms import ModelForm
from django.contrib.auth.models import User, Group
from django.contrib.auth.admin import UserAdmin, GroupAdmin
from app.forms import StaffAdminForm, StudentAdminForm
from django.urls import path
from django.http import JsonResponse
# Standard library imports
import json
from datetime import datetime
# Core Django imports
from django.contrib import admin
from django.contrib.auth.admin import GroupAdmin, UserAdmin
from django.contrib.auth.models import Group, User
from django.db import transaction
from django.db.models import Q
from datetime import datetime
from django.forms import ModelForm
from django.http import JsonResponse
from django.urls import path
from django.utils.translation import gettext_lazy as _
# Local app imports
from app.firebase import FCMDevice
from app.forms import ParentAdminForm, StaffAdminForm, StudentAdminForm
from app.models import (
Attendance,
AttendanceRecord,
Batch,
Course,
CourseTracking,
Institute,
InstituteFeedback,
Notice,
Parent,
ParentFeedback,
ParentInstituteFeedback,
ResetToken,
Routine,
Staff,
StaffInstituteFeedback,
StaffLeave,
Student,
StudentFeedback,
StudentLeave,
Subject,
SubjectFile,
TeacherParentMeeting,
TOTPSecret,
)
# Register your models here.
class CustomAdminSite(admin.AdminSite):
def get_app_list(self, request):
def get_app_list(self, request, app_label=None):
# Original app list
app_list = super().get_app_list(request)
app_list = super().get_app_list(request, app_label)
model_map = {
# Authentication Models
"User": {
"group": "Authentication",
"group_label": "authentication",
"display_name": "Users",
},
"Group": {
"group": "Authentication",
"group_label": "authentication",
"display_name": "Groups",
},
"TOTPSecret": {
"group": "Authentication",
"group_label": "authentication",
"display_name": "TOTP Keys",
},
"ResetToken": {
"group": "Authentication",
"group_label": "authentication",
"display_name": "Reset Tokens",
},
# Organization Models
"Institute": {
"group": "Organization",
"group_label": "organization",
"display_name": "Institute",
},
"Batch": {
"group": "Organization",
"group_label": "organization",
"display_name": "Batches",
},
"Notice": {
"group": "Organization",
"group_label": "organization",
"display_name": "Notices",
},
# Course Management Models
"Course": {
"group": "Course Management",
"group_label": "course_management",
"display_name": "Courses",
},
"Subject": {
"group": "Course Management",
"group_label": "course_management",
"display_name": "Subjects",
},
"SubjectFile": {
"group": "Course Management",
"group_label": "course_management",
"display_name": "Materials",
},
"CourseTracking": {
"group": "Course Management",
"group_label": "course_management",
"display_name": "Progress",
},
# Human Management Models
"Parent": {
"group": "Human Management",
"group_label": "human_management",
"display_name": "Parents",
},
"Student": {
"group": "Human Management",
"group_label": "human_management",
"display_name": "Students",
},
"Staff": {
"group": "Human Management",
"group_label": "human_management",
"display_name": "Staff",
},
# Schedule Management
"Routine": {
"group": "Human Management",
"group_label": "human_management",
"group": "Schedule Management",
"group_label": "schedule_management",
"display_name": "Classes",
},
"Staff_leave": {
"group": "Human Management",
"group_label": "human_management",
"TeacherParentMeeting": {
"group": "Schedule Management",
"group_label": "schedule_management",
"display_name": "PT Meetings",
},
"Student_Leave": {
"group": "Human Management",
"group_label": "human_management",
# Leave Management
"StaffLeave": {
"group": "Leave Management",
"group_label": "leave_management",
"display_name": "Staff Leave",
},
"StudentLeave": {
"group": "Leave Management",
"group_label": "leave_management",
"display_name": "Student Leave",
},
# Attendance Models
"Attendance": {
"group": "Attendance",
"group_label": "attendance",
"display_name": "Attendance",
},
"AttendanceRecord": {
"group": "Attendance",
"group_label": "attendance",
"display_name": "Records",
},
# Feedback Models
"StudentFeedback": {
"group": "Feedback",
"group_label": "feedback",
"display_name": "Student Feedback",
},
"InstituteFeedback": {
"group": "Feedback",
"group_label": "feedback",
"display_name": "Student-Institute",
},
"StaffInstituteFeedback": {
"group": "Feedback",
"group_label": "feedback",
"display_name": "Staff-Institute",
},
"ParentFeedback": {
"group": "Feedback",
"group_label": "feedback",
"display_name": "Parent Feedback",
},
"ParentInstituteFeedback": {
"group": "Feedback",
"group_label": "feedback",
"display_name": "Parent-Institute",
},
# Device Management
"FCMDevice": {
"group": "Device Management",
"group_label": "device_management",
"display_name": "Devices",
},
}
# If app_label is specified, return only that app
if app_label:
for app in app_list:
if app["app_label"] == app_label:
return [app]
return []
grouped_app_list = [{"name": "Main", "app_label": "dashboard", "models": []}]
grouped_models = {}
@@ -152,6 +213,11 @@ class CustomAdminSite(admin.AdminSite):
"app_label": model_map[model_name]["group_label"],
"models": [],
}
# Apply custom display name if available
if "display_name" in model_map[model_name]:
model["name"] = model_map[model_name]["display_name"]
grouped_models[group]["models"].append(model)
dashboard_item = {
@@ -187,10 +253,17 @@ class InstituteModel(admin.ModelAdmin):
@admin.register(Course, site=custom_admin_site)
class CourseModel(admin.ModelAdmin):
list_display = ("id", "name", "duration", "duration_type", "get_total_duration")
list_display = (
"id",
"name",
"duration",
"duration_type",
"get_total_duration",
"is_active",
)
search_fields = ("name", "duration", "duration_type")
list_filter = ("duration_type", "duration")
advanced_filter_fields = ("name", "duration", "duration_type")
list_filter = ("duration_type", "duration", "is_active")
advanced_filter_fields = ("name", "duration", "duration_type", "is_active")
def get_total_duration(self, obj):
if obj.duration_type == "Semester":
@@ -213,34 +286,41 @@ class CourseModel(admin.ModelAdmin):
@admin.register(Batch, site=custom_admin_site)
class BatchAdmin(admin.ModelAdmin):
list_display = ("name", "start_date", "end_date")
search_fields = ("name", "start_date", "end_date")
list_filter = ("name", "start_date", "end_date")
advanced_filter_fields = ("name", "start_date", "end_date")
list_display = ("name", "year", "is_active")
search_fields = ("name", "year")
list_filter = ("name", "year", "is_active")
advanced_filter_fields = ("name", "year", "is_active")
@admin.register(Subject, site=custom_admin_site)
class SubjectModel(admin.ModelAdmin):
list_display = ("id", "name", "course", "get_period_display", "has_syllabus", "file_count")
list_display = (
"id",
"name",
"course",
"get_period_display",
"has_syllabus",
"file_count",
)
search_fields = ("name", "course__name")
list_filter = ("course", "semester_or_year")
advanced_filter_fields = ("name", "course", "semester_or_year")
list_filter = ("course", "period_or_year")
advanced_filter_fields = ("name", "course", "period_or_year")
def has_syllabus(self, obj):
return bool(obj.syllabus_pdf)
has_syllabus.boolean = True
has_syllabus.short_description = "Has Syllabus"
def file_count(self, obj):
return obj.subjectfile_set.count()
file_count.short_description = "Additional Files"
def get_period_display(self, obj):
if obj.course.duration_type == "Semester":
return f"Semester {obj.semester_or_year}"
return f"Year {obj.semester_or_year}"
return f"Semester {obj.period_or_year}"
return f"Year {obj.period_or_year}"
get_period_display.short_description = "Period"
@@ -252,7 +332,7 @@ class SubjectModel(admin.ModelAdmin):
def response_add(self, request, obj, post_url_continue=None):
if "_addanother" in request.POST:
# Pre-fill the course and semester fields for the next entry
# Pre-fill the course and period fields for the next entry
return self.response_post_save_add(request, obj)
return super().response_add(request, obj, post_url_continue)
@@ -260,23 +340,23 @@ class SubjectModel(admin.ModelAdmin):
form = super().get_form(request, obj, **kwargs)
if obj and obj.course:
if obj.course.duration_type == "Semester":
form.base_fields["semester_or_year"].label = "Semester"
form.base_fields["period_or_year"].label = "Semester"
form.base_fields[
"semester_or_year"
"period_or_year"
].help_text = f"Enter semester number (1-{obj.course.duration * 2})"
else:
form.base_fields["semester_or_year"].label = "Year"
form.base_fields["period_or_year"].label = "Year"
form.base_fields[
"semester_or_year"
"period_or_year"
].help_text = f"Enter year number (1-{obj.course.duration})"
# Keep the last used course selected for next entry
if not obj and request.method == "GET":
if "course" in request.GET:
form.base_fields["course"].initial = request.GET["course"]
if "semester_or_year" in request.GET:
form.base_fields["semester_or_year"].initial = request.GET[
"semester_or_year"
if "period_or_year" in request.GET:
form.base_fields["period_or_year"].initial = request.GET[
"period_or_year"
]
return form
@@ -287,17 +367,17 @@ class SubjectModel(admin.ModelAdmin):
opts = self.model._meta
url = reverse(f"admin:{opts.app_label}_{opts.model_name}_add")
return HttpResponseRedirect(
f"{url}?course={obj.course.id}&semester_or_year={obj.semester_or_year}"
f"{url}?course={obj.course.id}&period_or_year={obj.period_or_year}"
)
@admin.register(Student, site=custom_admin_site)
class StudentAdmin(admin.ModelAdmin):
form = StudentAdminForm
list_display = ("id", "name", "phone", "course", "status")
search_fields = ("name", "phone", "course__name")
list_display = ("id", "name", "phone", "course", "status", "fcm_token")
search_fields = ("name", "phone", "course__name", "fcm_token")
list_filter = ("course", "status", "gender", "joining_date")
advanced_filter_fields = ("name", "phone", "course", "batch", "status")
advanced_filter_fields = ("name", "phone", "course", "batch", "status", "fcm_token")
def has_view_permission(self, request, obj=None):
"""Check if user has view permission"""
@@ -328,13 +408,16 @@ class StudentAdmin(admin.ModelAdmin):
@admin.register(Staff, site=custom_admin_site)
class StaffAdmin(admin.ModelAdmin):
form = StaffAdminForm
list_display = ("id", "name", "phone", "designation", "joining_date")
search_fields = ("name", "phone", "designation")
list_display = ("id", "name", "phone", "designation", "joining_date", "fcm_token")
search_fields = ("name", "phone", "designation", "fcm_token")
list_filter = ("designation", "joining_date", "gender")
advanced_filter_fields = ("name", "phone", "designation")
advanced_filter_fields = ("name", "phone", "designation", "fcm_token")
readonly_fields = ("password",)
ordering = ("name",)
filter_horizontal = ("groups",)
def save_model(self, request, obj, form, change):
"""Just save the model, group assignment is handled in the form"""
super().save_model(request, obj, form, change)
def has_view_permission(self, request, obj=None):
"""Check if user has view permission"""
@@ -435,10 +518,10 @@ class RoutineAdmin(admin.ModelAdmin):
"teacher",
"start_time",
"end_time",
"semester_or_year",
"period_or_year",
"is_active",
]
list_filter = ["is_active", "course", "subject", "teacher", "semester_or_year"]
list_filter = ["is_active", "course", "subject", "teacher", "period_or_year"]
search_fields = ["course__name", "subject__name", "teacher__name"]
ordering = ["start_time"]
change_form_template = "admin/app/routine/change_form.html"
@@ -478,7 +561,7 @@ class RoutineAdmin(admin.ModelAdmin):
"teacher",
"start_time",
"end_time",
"semester_or_year",
"period_or_year",
]
missing_fields = [
field
@@ -496,7 +579,7 @@ class RoutineAdmin(admin.ModelAdmin):
course_id = int(routine_data["course"])
subject_id = int(routine_data["subject"])
teacher_id = int(routine_data["teacher"])
semester = int(routine_data["semester_or_year"])
period = int(routine_data["period_or_year"])
except (ValueError, TypeError) as e:
raise ValueError(f"Error parsing IDs: {str(e)}")
@@ -518,7 +601,7 @@ class RoutineAdmin(admin.ModelAdmin):
existing_routine = Routine.objects.filter(
course_id=course_id,
subject_id=subject_id,
semester_or_year=semester,
period_or_year=period,
start_time=start_time,
end_time=end_time,
).first()
@@ -536,7 +619,7 @@ class RoutineAdmin(admin.ModelAdmin):
teacher_id=teacher_id,
start_time=start_time,
end_time=end_time,
semester_or_year=semester,
period_or_year=period,
is_active=True,
)
created_routines.append(routine)
@@ -558,7 +641,7 @@ class RoutineAdmin(admin.ModelAdmin):
"teacher": r.teacher.name,
"start_time": r.start_time.strftime("%H:%M"),
"end_time": r.end_time.strftime("%H:%M"),
"semester_or_year": r.semester_or_year,
"period_or_year": r.period_or_year,
}
for r in created_routines
],
@@ -581,7 +664,7 @@ class RoutineAdmin(admin.ModelAdmin):
if request.GET.get("course"):
kwargs["queryset"] = Subject.objects.filter(
course_id=request.GET.get("course"),
semester_or_year=request.GET.get("semester_or_year", 1),
period_or_year=request.GET.get("period_or_year", 1),
)
else:
kwargs["queryset"] = Subject.objects.none()
@@ -601,7 +684,7 @@ class RoutineAdmin(admin.ModelAdmin):
form.base_fields["course"].widget.attrs["class"] = "select2"
form.base_fields["subject"].widget.attrs["class"] = "select2"
form.base_fields["teacher"].widget.attrs["class"] = "select2"
form.base_fields["semester_or_year"].initial = 1
form.base_fields["period_or_year"].initial = 1
return form
def add_view(self, request, form_url="", extra_context=None):
@@ -618,9 +701,15 @@ class RoutineAdmin(admin.ModelAdmin):
js = ("https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js",)
@admin.register(Staff_leave, site=custom_admin_site)
@admin.register(StaffLeave, site=custom_admin_site)
class StaffLeaveAdmin(admin.ModelAdmin):
list_display = ("staff", "start_date", "end_date", "get_status_display", "created_at")
list_display = (
"staff",
"start_date",
"end_date",
"get_status_display",
"created_at",
)
list_filter = ("status", "start_date", "end_date", "created_at")
search_fields = ("staff__name", "message")
readonly_fields = ("created_at", "updated_at")
@@ -628,9 +717,15 @@ class StaffLeaveAdmin(admin.ModelAdmin):
raw_id_fields = ("staff",)
@admin.register(Student_Leave, site=custom_admin_site)
@admin.register(StudentLeave, site=custom_admin_site)
class StudentLeaveAdmin(admin.ModelAdmin):
list_display = ("student", "start_date", "end_date", "get_status_display", "created_at")
list_display = (
"student",
"start_date",
"end_date",
"get_status_display",
"created_at",
)
list_filter = ("status", "start_date", "end_date", "created_at")
search_fields = ("student__name", "message")
readonly_fields = ("created_at", "updated_at")
@@ -790,15 +885,25 @@ class StaffInstituteFeedbackAdmin(admin.ModelAdmin):
@admin.register(FCMDevice, site=custom_admin_site)
class FCMDeviceAdmin(admin.ModelAdmin):
list_display = ("id", "token", "created_at", "last_active", "is_active")
list_display = (
"id",
"token",
"user_type",
"is_active",
"created_at",
"last_active",
)
search_fields = ("token",)
list_filter = ("is_active", "created_at", "last_active")
list_filter = ("user_type", "is_active", "created_at", "last_active")
readonly_fields = ("created_at", "last_active")
ordering = ("-last_active",)
fieldsets = (
(None, {"fields": ("token", "is_active")}),
("Metadata", {"fields": ("created_at", "last_active"), "classes": ("collapse",)}),
(None, {"fields": ("token", "user_type", "is_active", "is_fallback")}),
(
"Metadata",
{"fields": ("created_at", "last_active"), "classes": ("collapse",)},
),
)
@@ -810,5 +915,166 @@ class SubjectFileAdmin(admin.ModelAdmin):
autocomplete_fields = ["subject", "uploaded_by"]
@admin.register(Parent, site=custom_admin_site)
class ParentAdmin(admin.ModelAdmin):
form = ParentAdminForm
list_display = ("id", "name", "phone", "email", "fcm_token")
search_fields = ("name", "phone", "email", "fcm_token")
list_filter = ("students",)
advanced_filter_fields = ("name", "phone", "email", "students", "fcm_token")
def has_view_permission(self, request, obj=None):
"""Check if user has view permission"""
return request.user.has_perm("app.view_parent")
def has_add_permission(self, request):
"""Check if user has add permission"""
return request.user.has_perm("app.add_parent")
def has_change_permission(self, request, obj=None):
"""Check if user has change permission"""
return request.user.has_perm("app.change_parent")
def has_delete_permission(self, request, obj=None):
"""Check if user has delete permission"""
return request.user.has_perm("app.delete_parent")
def has_module_permission(self, request):
"""Check if user has any permissions for this app"""
return request.user.has_module_perms("app")
def get_readonly_fields(self, request, obj=None):
if obj:
return self.readonly_fields + ("password",)
return self.readonly_fields
@admin.register(ParentFeedback, site=custom_admin_site)
class ParentFeedbackAdmin(admin.ModelAdmin):
list_display = ("parent", "teacher", "student", "rating", "created_at")
list_filter = ("rating", "created_at", "teacher", "student")
search_fields = ("parent__name", "teacher__name", "student__name", "feedback_text")
readonly_fields = ("created_at", "updated_at")
ordering = ("-created_at",)
raw_id_fields = ("parent", "teacher", "student")
def get_queryset(self, request):
return (
super().get_queryset(request).select_related("parent", "teacher", "student")
)
@admin.register(ParentInstituteFeedback, site=custom_admin_site)
class ParentInstituteFeedbackAdmin(admin.ModelAdmin):
list_display = (
"parent",
"institute",
"feedback_type",
"rating",
"is_anonymous",
"created_at",
)
list_filter = ("feedback_type", "rating", "is_anonymous", "created_at", "institute")
search_fields = ("parent__name", "institute__name", "feedback_text")
readonly_fields = ("created_at", "updated_at")
ordering = ("-created_at",)
raw_id_fields = ("parent", "institute")
def get_queryset(self, request):
return super().get_queryset(request).select_related("parent", "institute")
@admin.register(TeacherParentMeeting, site=custom_admin_site)
class TeacherParentMeetingAdmin(admin.ModelAdmin):
list_display = (
"meeting_date",
"meeting_time",
"duration",
"status",
"is_online",
"meeting_link",
)
list_filter = (
"status",
"is_online",
"meeting_date",
)
search_fields = (
"agenda",
"notes",
"cancellation_reason",
)
readonly_fields = ("created_at", "updated_at")
ordering = ("-meeting_date", "-meeting_time")
fieldsets = (
(
"Meeting Details",
{
"fields": (
"meeting_date",
"meeting_time",
"duration",
"status",
"is_online",
"meeting_link",
)
},
),
(
"Meeting Content",
{
"fields": ("agenda", "notes"),
"classes": ("collapse",),
},
),
(
"Cancellation Details",
{
"fields": ("cancellation_reason",),
"classes": ("collapse",),
},
),
(
"Metadata",
{
"fields": (
"created_at",
"updated_at",
),
"classes": ("collapse",),
},
),
)
@admin.register(TOTPSecret, site=custom_admin_site)
class TOTPSecretAdmin(admin.ModelAdmin):
list_display = ("identifier", "created_at", "expires_at", "is_expired")
search_fields = ("identifier",)
readonly_fields = ("created_at", "expires_at")
ordering = ("-created_at",)
def is_expired(self, obj):
return obj.is_expired()
is_expired.boolean = True
is_expired.short_description = "Expired"
@admin.register(ResetToken, site=custom_admin_site)
class ResetTokenAdmin(admin.ModelAdmin):
list_display = ("identifier", "token", "created_at", "expires_at", "is_expired")
search_fields = ("identifier", "token")
readonly_fields = ("created_at", "expires_at")
ordering = ("-created_at",)
def is_expired(self, obj):
return obj.is_expired()
is_expired.boolean = True
is_expired.short_description = "Expired"
custom_admin_site.register(User, UserAdmin)
custom_admin_site.register(Group, GroupAdmin)
+731
View File
@@ -0,0 +1,731 @@
# Standard library imports
from datetime import timedelta, datetime
# Core Django imports
from django.shortcuts import render, redirect, get_object_or_404
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.db.models import Count, Q, Avg
from django.utils import timezone
from django.views.decorators.http import require_http_methods, require_GET
from django.contrib.auth.hashers import check_password, make_password
from django.contrib.auth.models import Group
# Local app imports
from app.models import (
Staff,
Student,
Course,
CourseTracking,
Batch,
StaffLeave,
Notice,
FEEDBACK_TYPE_CHOICES,
)
@login_required
def admission_officer_dashboard(request):
"""View to display the admission officer dashboard"""
# Check if user is in staff and has Admission Officer designation
if (
not isinstance(request.user, Staff)
or request.user.designation != "Admission Officer"
):
messages.error(
request,
"You don't have permission to access the admission officer dashboard.",
)
return redirect("dashboard")
# Get the admission officer object
try:
admission_officer = request.user
except Exception as e:
messages.error(
request,
"Admission Officer profile not found. Please contact administrator.",
)
return redirect("dashboard")
today = timezone.now().date()
last_week = today - timedelta(days=7)
last_month = today - timedelta(days=30)
# Get student statistics and list
total_students = Student.objects.count()
active_students = Student.objects.filter(status="Active").count()
new_students = Student.objects.filter(joining_date__gte=last_month).count()
students = (
Student.objects.select_related("course")
.prefetch_related("batches")
.all()
.order_by("-id")
) # For the student list
# Get course statistics and list
total_courses = Course.objects.count()
active_courses = Course.objects.filter(is_active=True).count()
courses = Course.objects.all().order_by("name") # For the dropdown
# Get batch statistics and list
total_batches = Batch.objects.count()
active_batches = Batch.objects.filter(is_active=True).count()
batches = Batch.objects.all().order_by("-year") # For the dropdown
# Get recent course trackings
recent_trackings = CourseTracking.objects.select_related(
"student", "course"
).order_by("-created_at")
# Get leave statistics
pending_leaves = StaffLeave.objects.filter(
staff=admission_officer, status=0
).count()
approved_leaves = StaffLeave.objects.filter(
staff=admission_officer, status=1
).count()
rejected_leaves = StaffLeave.objects.filter(
staff=admission_officer, status=2
).count()
# Get recent leaves
recent_leaves = StaffLeave.objects.filter(staff=admission_officer).order_by(
"-created_at"
)
# Get recent notices
notices = Notice.objects.all().order_by("-created_at")
# Get feedback types for institute feedback
feedback_types = FEEDBACK_TYPE_CHOICES
context = {
"officer": admission_officer,
"title": "Admission Officer Dashboard",
"total_students": total_students,
"active_students": active_students,
"new_students": new_students,
"students": students, # For the student list
"total_courses": total_courses,
"active_courses": active_courses,
"courses": courses, # For the dropdown
"total_batches": total_batches,
"active_batches": active_batches,
"batches": batches, # For the dropdown
"recent_trackings": recent_trackings,
"pending_leaves": pending_leaves,
"approved_leaves": approved_leaves,
"rejected_leaves": rejected_leaves,
"recent_leaves": recent_leaves,
"notices": notices,
"feedback_types": feedback_types,
}
return render(request, "admission_officer/dashboard.html", context)
@login_required
def add_student(request):
"""View to add a new student"""
if request.method != "POST":
messages.error(request, "Invalid request method")
return redirect("admissionOfficerDashboard")
# Check if user has permission
if (
not isinstance(request.user, Staff)
or request.user.designation != "Admission Officer"
):
messages.error(request, "Permission denied")
return redirect("admissionOfficerDashboard")
try:
# Get form data
name = request.POST.get("name")
phone = request.POST.get("phone")
email = request.POST.get("email")
gender = request.POST.get("gender")
birth_date = request.POST.get("birth_date")
course_id = request.POST.get("course")
current_period = request.POST.get("current_period", 1)
status = request.POST.get("status", "Active")
parent_name = request.POST.get("parent_name")
parent_phone = request.POST.get("parent_phone")
permanent_address = request.POST.get("permanent_address")
temporary_address = request.POST.get("temporary_address")
citizenship_number = request.POST.get("citizenship_number")
# Get batch ID
batch_id = request.POST.get("batches")
# Get photo if uploaded
photo = request.FILES.get("photo")
# Validate required fields
if not all([name, phone, batch_id]):
messages.error(request, "Name, phone, and batch are required")
return redirect("admissionOfficerDashboard")
# Check if student with same phone already exists
if Student.objects.filter(phone=phone).exists():
messages.error(request, "Student with this phone number already exists")
return redirect("admissionOfficerDashboard")
# Create student object
student = Student()
student.name = name
student.phone = phone
student.email = email
student.gender = gender
# Set birth date if provided
if birth_date:
student.birth_date = birth_date
# Set course if provided
if course_id:
try:
course = Course.objects.get(id=course_id)
student.course = course
student.current_period = current_period
except Course.DoesNotExist:
messages.error(request, "Selected course not found")
return redirect("admissionOfficerDashboard")
student.status = status
student.parent_name = parent_name
student.parent_phone = parent_phone
student.permanent_address = permanent_address
student.temporary_address = temporary_address
student.citizenship_number = citizenship_number
# Set photo if uploaded
if photo:
student.image = photo
# Set default password as '123'
student.set_password("123")
# Save student
student.save()
# Add student to Student group
student_group, created = Group.objects.get_or_create(name="Student")
student.groups.add(student_group)
# Add student to batch
if batch_id:
try:
batch = Batch.objects.get(id=batch_id)
student.batches.add(batch)
# Joining date will be set automatically from the batch year in the signal
except Batch.DoesNotExist:
messages.error(request, "Selected batch not found")
# Continue anyway as we've already saved the student
# Create course tracking if course is assigned
if course_id:
try:
# Use today's date if no joining date yet (should be set by batch signal)
start_date = student.joining_date or timezone.now().date()
course_tracking = CourseTracking(
student=student,
course=course,
start_date=start_date,
current_period=current_period,
)
course_tracking.save()
except Exception as e:
print(f"Error creating course tracking: {str(e)}")
messages.success(
request, f'Student "{name}" added successfully. Default password is: 123'
)
return redirect("admissionOfficerDashboard")
except Exception as e:
messages.error(request, f"Error: {str(e)}")
return redirect("admissionOfficerDashboard")
@login_required
def add_batch(request):
"""View to add a new batch"""
if request.method != "POST":
messages.error(request, "Invalid request method")
return redirect("admissionOfficerDashboard")
# Check if user has permission
if (
not isinstance(request.user, Staff)
or request.user.designation != "Admission Officer"
):
messages.error(request, "Permission denied")
return redirect("admissionOfficerDashboard")
try:
# Get form data
name = request.POST.get("name")
start_date = request.POST.get("start_date")
is_active = request.POST.get("is_active") == "on"
# Validate required fields
if not all([name, start_date]):
messages.error(request, "Name and start date are required")
return redirect("admissionOfficerDashboard")
# Create batch
batch = Batch()
batch.name = name
batch.year = start_date # Use start_date for the year field
batch.is_active = is_active
batch.save()
messages.success(request, f'Batch "{name}" added successfully')
return redirect("admissionOfficerDashboard")
except Exception as e:
messages.error(request, f"Error: {str(e)}")
return redirect("admissionOfficerDashboard")
@login_required
def add_course(request):
"""View to add a new course"""
if request.method != "POST":
messages.error(request, "Invalid request method")
return redirect("admissionOfficerDashboard")
# Check if user has permission
if (
not isinstance(request.user, Staff)
or request.user.designation != "Admission Officer"
):
messages.error(request, "Permission denied")
return redirect("admissionOfficerDashboard")
try:
# Get form data
name = request.POST.get("name")
code = request.POST.get("code", "")
duration_type = request.POST.get("duration_type")
duration = request.POST.get("duration")
description = request.POST.get("description", "")
is_active = request.POST.get("is_active") == "on"
# Validate required fields
if not all([name, duration_type, duration]):
messages.error(request, "Name, duration type, and duration are required")
return redirect("admissionOfficerDashboard")
# Create course
course = Course()
course.name = name
course.code = code
course.duration_type = duration_type
course.duration = duration
course.description = description
course.is_active = is_active
course.save()
messages.success(request, f'Course "{name}" added successfully')
return redirect("admissionOfficerDashboard")
except Exception as e:
messages.error(request, f"Error: {str(e)}")
return redirect("admissionOfficerDashboard")
@login_required
def add_course_tracking(request):
"""View to add course tracking for a student"""
if request.method != "POST":
messages.error(request, "Invalid request method")
return redirect("admissionOfficerDashboard")
# Check if user has permission
if (
not isinstance(request.user, Staff)
or request.user.designation != "Admission Officer"
):
messages.error(request, "Permission denied")
return redirect("admissionOfficerDashboard")
try:
# Get form data
student_id = request.POST.get("student")
course_id = request.POST.get("course")
start_date = request.POST.get("start_date")
expected_end_date = request.POST.get("expected_end_date")
current_period = request.POST.get("current_period", 1)
progress_status = request.POST.get("progress_status", "In Progress")
notes = request.POST.get("notes")
# Validate required fields
if not student_id or not course_id:
messages.error(request, "Student and course are required")
return redirect("admissionOfficerDashboard")
# Check if student exists
try:
student = Student.objects.get(id=student_id)
except Student.DoesNotExist:
messages.error(request, "Student not found")
return redirect("admissionOfficerDashboard")
# Check if course exists
try:
course = Course.objects.get(id=course_id)
except Course.DoesNotExist:
messages.error(request, "Course not found")
return redirect("admissionOfficerDashboard")
# Check if tracking already exists
if CourseTracking.objects.filter(student=student, course=course).exists():
messages.error(request, "Student is already enrolled in this course")
return redirect("admissionOfficerDashboard")
# Create course tracking object
tracking = CourseTracking()
tracking.student = student
tracking.course = course
# Set dates if provided
if start_date:
tracking.start_date = start_date
if expected_end_date:
tracking.expected_end_date = expected_end_date
tracking.current_period = int(current_period)
tracking.progress_status = progress_status
tracking.notes = notes
# Save tracking
tracking.save()
messages.success(
request,
f'Course tracking added for student "{student.name}" in course "{course.name}"',
)
return redirect("admissionOfficerDashboard")
except Exception as e:
messages.error(request, f"Error: {str(e)}")
return redirect("admissionOfficerDashboard")
@login_required
def get_courses(request):
"""View to get courses for dropdowns"""
try:
courses = Course.objects.all().order_by("name")
course_data = [
{"id": course.id, "name": course.name, "code": course.code}
for course in courses
]
return JsonResponse({"success": True, "courses": course_data})
except Exception as e:
return JsonResponse({"success": False, "message": str(e)}, status=500)
@login_required
def get_batches(request):
"""View to get batches for dropdowns"""
try:
batches = Batch.objects.all().order_by("-year")
batch_data = []
for batch in batches:
batch_data.append(
{
"id": batch.id,
"name": batch.name,
"year": batch.year.year if batch.year else None,
"display_name": f"{batch.name} ({batch.year.year if batch.year else 'No year'})",
}
)
return JsonResponse({"success": True, "batches": batch_data})
except Exception as e:
return JsonResponse({"success": False, "message": str(e)}, status=500)
@login_required
def get_students(request):
"""View to get students for dropdowns"""
try:
students = Student.objects.all()
student_data = [
{"id": student.id, "name": student.name, "phone": student.phone}
for student in students
]
return JsonResponse({"success": True, "students": student_data})
except Exception as e:
return JsonResponse({"success": False, "message": str(e)}, status=500)
@login_required
def get_student(request, student_id):
"""View to get a student's details by ID"""
# Check if user has permission
if (
not isinstance(request.user, Staff)
or request.user.designation != "Admission Officer"
):
return JsonResponse(
{"success": False, "message": "Permission denied"}, status=403
)
try:
# Get student with related data
student = (
Student.objects.select_related("course")
.prefetch_related("batches")
.get(id=student_id)
)
# Build batches data
batches = []
for batch in student.batches.all():
batches.append(
{
"id": batch.id,
"name": batch.name,
"year": batch.year.year if batch.year else None,
}
)
# Build student data
student_data = {
"id": student.id,
"name": student.name,
"phone": student.phone,
"email": student.email,
"gender": student.gender,
"birth_date": student.birth_date.strftime("%Y-%m-%d")
if student.birth_date
else None,
"status": student.status,
"parent_name": student.parent_name,
"parent_phone": student.parent_phone,
"permanent_address": student.permanent_address,
"temporary_address": student.temporary_address,
"citizenship_number": student.citizenship_no,
"current_period": student.current_period,
"joining_date": student.joining_date.strftime("%Y-%m-%d")
if student.joining_date
else None,
"image": student.image.url if student.image else None,
"course": {"id": student.course.id, "name": student.course.name}
if student.course
else None,
"batches": batches,
}
return JsonResponse({"success": True, "student": student_data})
except Student.DoesNotExist:
return JsonResponse(
{"success": False, "message": "Student not found"}, status=404
)
except Exception as e:
return JsonResponse({"success": False, "message": str(e)}, status=500)
@login_required
def edit_student(request):
"""View to edit a student's details"""
if request.method != "POST":
return JsonResponse(
{"success": False, "message": "Invalid request method"}, status=405
)
# Check if user has permission
if (
not isinstance(request.user, Staff)
or request.user.designation != "Admission Officer"
):
return JsonResponse(
{"success": False, "message": "Permission denied"}, status=403
)
try:
# Get student ID from form data
student_id = request.POST.get("student_id")
if not student_id:
return JsonResponse(
{"success": False, "message": "Student ID is required"}, status=400
)
# Get student object
student = get_object_or_404(Student, id=student_id)
# Update student fields
student.name = request.POST.get("name", student.name)
student.phone = request.POST.get("phone", student.phone)
student.email = request.POST.get("email", student.email)
student.gender = request.POST.get("gender", student.gender)
# Handle birth date
birth_date = request.POST.get("birth_date")
if birth_date:
student.birth_date = birth_date
# Handle course
course_id = request.POST.get("course")
if course_id:
try:
course = Course.objects.get(id=course_id)
student.course = course
except Course.DoesNotExist:
pass
# Handle status
status = request.POST.get("status")
if status:
student.status = status
# Handle parent info
student.parent_name = request.POST.get("parent_name", student.parent_name)
student.parent_phone = request.POST.get("parent_phone", student.parent_phone)
# Handle address
student.permanent_address = request.POST.get(
"permanent_address", student.permanent_address
)
# Handle citizenship
student.citizenship_no = request.POST.get(
"citizenship_number", student.citizenship_no
)
# Save student
student.save()
# Handle batch
batch_id = request.POST.get("batches")
if batch_id:
try:
batch = Batch.objects.get(id=batch_id)
# Clear existing batches and add the new one
student.batches.clear()
student.batches.add(batch)
except Batch.DoesNotExist:
pass
return JsonResponse(
{"success": True, "message": "Student updated successfully"}
)
except Exception as e:
return JsonResponse({"success": False, "message": str(e)}, status=500)
@login_required
def get_course_tracking(request, tracking_id):
"""View to get a course tracking's details by ID"""
# Check if user has permission
if (
not isinstance(request.user, Staff)
or request.user.designation != "Admission Officer"
):
return JsonResponse(
{"success": False, "message": "Permission denied"}, status=403
)
try:
# Get tracking with related data
tracking = CourseTracking.objects.select_related("student", "course").get(
id=tracking_id
)
# Build tracking data
tracking_data = {
"id": tracking.id,
"student": {"id": tracking.student.id, "name": tracking.student.name},
"course": {"id": tracking.course.id, "name": tracking.course.name},
"start_date": tracking.start_date.strftime("%Y-%m-%d"),
"expected_end_date": tracking.expected_end_date.strftime("%Y-%m-%d")
if tracking.expected_end_date
else None,
"completion_percentage": tracking.completion_percentage,
"progress_status": tracking.progress_status,
"current_period": tracking.current_period,
"notes": tracking.notes,
"current_period_display": tracking.current_period_display,
}
return JsonResponse({"success": True, "tracking": tracking_data})
except CourseTracking.DoesNotExist:
return JsonResponse(
{"success": False, "message": "Course tracking not found"}, status=404
)
except Exception as e:
return JsonResponse({"success": False, "message": str(e)}, status=500)
@login_required
def edit_course_tracking(request):
"""View to edit a course tracking"""
if request.method != "POST":
return JsonResponse(
{"success": False, "message": "Invalid request method"}, status=405
)
# Check if user has permission
if (
not isinstance(request.user, Staff)
or request.user.designation != "Admission Officer"
):
return JsonResponse(
{"success": False, "message": "Permission denied"}, status=403
)
try:
# Get tracking ID from form data
tracking_id = request.POST.get("tracking_id")
if not tracking_id:
return JsonResponse(
{"success": False, "message": "Tracking ID is required"}, status=400
)
# Get tracking object
tracking = get_object_or_404(CourseTracking, id=tracking_id)
# Update tracking fields
start_date = request.POST.get("start_date")
if start_date:
tracking.start_date = start_date
expected_end_date = request.POST.get("expected_end_date")
if expected_end_date:
tracking.expected_end_date = expected_end_date
current_period = request.POST.get("current_period")
if current_period:
tracking.current_period = int(current_period)
progress_status = request.POST.get("progress_status")
if progress_status:
tracking.progress_status = progress_status
completion_percentage = request.POST.get("completion_percentage")
if completion_percentage:
tracking.completion_percentage = int(completion_percentage)
notes = request.POST.get("notes")
if notes is not None: # Allow empty notes
tracking.notes = notes
# Save tracking
tracking.save()
return JsonResponse(
{"success": True, "message": "Course tracking updated successfully"}
)
except Exception as e:
return JsonResponse({"success": False, "message": str(e)}, status=500)
+5 -2
View File
@@ -1,9 +1,12 @@
# Core Django imports
from django.apps import AppConfig
from django.db.models.signals import post_migrate
from django.dispatch import receiver
class AppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'app'
default_auto_field = "django.db.models.BigAutoField"
name = "app"
def ready(self):
import app.signals # Import signals when app is ready
+33 -6
View File
@@ -1,11 +1,17 @@
from django.shortcuts import render, redirect
from django.contrib.auth import login
# Core Django imports
from django.contrib.auth import authenticate, login
from django.shortcuts import redirect, render
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.models import User
from django.contrib.auth.hashers import make_password
from django.contrib import messages
import uuid
from .models import Student, Staff
from .utils import (
# Local app imports
from app.models import Student, Staff, Parent
from app.utils import (
generate_otp,
generate_secret_key,
store_secret_key,
@@ -16,7 +22,8 @@ from .utils import (
store_reset_token,
verify_reset_token
)
from django.contrib import messages
def reset_password_options(request):
"""View for selecting between phone and email password reset"""
@@ -295,4 +302,24 @@ def set_new_password(request):
messages.success(request, "Password has been reset successfully. Please login with your new password.")
return redirect("login")
return render(request, "login/password_reset_set.html", {"is_email": False})
return render(request, "login/password_reset_set.html", {"is_email": False})
def custom_login(request):
"""Custom login view to handle authentication"""
if request.method == "POST":
username = request.POST.get("username")
password = request.POST.get("password")
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
next_url = request.POST.get("next", "/app/dashboard/")
return redirect(next_url)
else:
return render(request, "login/login.html", {
"error": "Invalid credentials",
"username": username
})
return render(request, "login/login.html")
+34 -24
View File
@@ -1,6 +1,11 @@
from django.contrib.auth.models import User
# Core Django imports
from django.contrib.auth.backends import ModelBackend
from .models import Staff, Student
from django.contrib.auth.models import Group, User
from django.db.models import Q
# Local app imports
from app.models import Parent, Staff, Student
class MultiModelBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
@@ -9,30 +14,36 @@ class MultiModelBackend(ModelBackend):
"""
if username is None or password is None:
return None
# Try authenticating as Staff by phone
# Use filter().first() to avoid MultipleObjectsReturned error
# Try authenticating as Staff by phone first
staff = Staff.objects.filter(phone=username).first()
if staff and staff.check_password(password):
return staff
if staff:
if staff.check_password(password):
return staff
# Try authenticating as Staff by email
# Use filter().first() to avoid MultipleObjectsReturned error
staff = Staff.objects.filter(email=username).first()
if staff and staff.check_password(password):
return staff
if staff:
if staff.check_password(password):
return staff
# Try authenticating as Parent by phone
parent = Parent.objects.filter(phone=username).first()
if parent:
if parent.check_password(password):
return parent
# Try authenticating as Student by phone
# Use filter().first() to avoid MultipleObjectsReturned error
student = Student.objects.filter(phone=username).first()
if student and student.check_password(password):
return student
if student:
if student.check_password(password):
return student
# Try authenticating as Student by email
# Use filter().first() to handle multiple students with same email
student = Student.objects.filter(email=username).first()
if student and student.check_password(password):
return student
if student:
if student.check_password(password):
return student
# Return None if no match
return None
@@ -41,7 +52,7 @@ class MultiModelBackend(ModelBackend):
"""
Retrieve a user instance using their ID.
"""
for model in [Staff, Student]:
for model in [Staff, Student, Parent]:
try:
return model.objects.get(pk=user_id)
except model.DoesNotExist:
@@ -54,16 +65,15 @@ class MultiModelBackend(ModelBackend):
"""
if not user_obj.is_active or user_obj.is_anonymous or obj is not None:
return set()
if isinstance(user_obj, (Staff, Student)):
if not hasattr(user_obj, '_group_perm_cache'):
if not hasattr(user_obj, "_group_perm_cache"):
perms = set()
for group in user_obj.groups.all():
perms.update(
"%s.%s" % (ct, name)
"%s.%s" % (ct, name)
for ct, name in group.permissions.values_list(
'content_type__app_label',
'codename'
"content_type__app_label", "codename"
)
)
user_obj._group_perm_cache = perms
@@ -74,7 +84,7 @@ class MultiModelBackend(ModelBackend):
def get_all_permissions(self, user_obj, obj=None):
if not user_obj.is_active or user_obj.is_anonymous or obj is not None:
return set()
if isinstance(user_obj, (Staff, Student)):
return self.get_group_permissions(user_obj, obj)
else:
@@ -83,7 +93,7 @@ class MultiModelBackend(ModelBackend):
def has_perm(self, user_obj, perm, obj=None):
if not user_obj.is_active:
return False
if isinstance(user_obj, (Staff, Student)):
return perm in self.get_all_permissions(user_obj, obj)
else:
+27
View File
@@ -0,0 +1,27 @@
from django.shortcuts import render
# Error handler views
def handler400(request, exception=None):
"""Bad request error handler"""
return render(request, "400.html", status=400)
def handler403(request, exception=None):
"""Forbidden error handler"""
return render(request, "403.html", status=403)
def handler404(request, exception=None):
"""Page not found error handler"""
return render(request, "404.html", status=404)
def handler500(request):
"""Server error handler"""
return render(request, "500.html", status=500)
def handler505(request, exception=None):
"""HTTP version not supported error handler"""
return render(request, "505.html", status=505)
+120 -33
View File
@@ -6,13 +6,31 @@ from firebase_admin import messaging, credentials
from student_management_system.settings import BASE_DIR
# --------------------------------------------------------------------
# FCM Device Model
# --------------------------------------------------------------------
class FCMDevice(models.Model):
"""Model to store Firebase Cloud Messaging device tokens"""
id = models.BigAutoField(primary_key=True)
token = models.TextField(unique=True)
created_at = models.DateTimeField(auto_now_add=True)
last_active = models.DateTimeField(auto_now=True)
is_active = models.BooleanField(default=True)
is_fallback = models.BooleanField(default=False) # Flag to identify fallback tokens
user_type = models.CharField(
max_length=20,
choices=[
("student", "Student"),
("parent", "Parent"),
("teacher", "Teacher"),
("admin", "Admin"),
("unknown", "Unknown"),
],
default="unknown",
) # Add user type field
def __str__(self):
return f"Device {self.id} ({self.token[:20]}...)"
@@ -20,7 +38,7 @@ class FCMDevice(models.Model):
class Meta:
verbose_name = "FCM Device"
verbose_name_plural = "FCM Devices"
ordering = ['-last_active']
ordering = ["-last_active"]
def deactivate(self):
"""Deactivate this device token"""
@@ -28,21 +46,64 @@ class FCMDevice(models.Model):
self.save()
# --------------------------------------------------------------------
# Token Management
# --------------------------------------------------------------------
def save_fcm_token(request, token):
"""Save FCM token to database"""
try:
# Delete any existing tokens for this user/device
FCMDevice.objects.filter(token=token).delete()
# Create new token
FCMDevice.objects.create(token=token)
return JsonResponse({'status': 'success'})
# Check if this is a fallback token
is_fallback = token.startswith("fcm-token-") or token.startswith(
"fallback-token-"
)
# Determine user type if a user is authenticated
user_type = "unknown"
if hasattr(request, "user") and request.user.is_authenticated:
user_groups = request.user.groups.values_list("name", flat=True)
if "Student" in user_groups:
user_type = "student"
elif "Parent" in user_groups:
user_type = "parent"
elif "Teacher" in user_groups:
user_type = "teacher"
elif request.user.is_superuser:
user_type = "admin"
# Use update_or_create to avoid unnecessary deletes
device, created = FCMDevice.objects.update_or_create(
token=token,
defaults={
"is_fallback": is_fallback,
"is_active": True,
"user_type": user_type,
},
)
return JsonResponse({"status": "success"})
except Exception as e:
print(f"Error saving FCM token: {str(e)}")
return JsonResponse({'error': str(e)}, status=500)
return JsonResponse({"error": str(e)}, status=500)
# --------------------------------------------------------------------
# Firebase Configuration
# --------------------------------------------------------------------
def get_firebase_js():
"""Generate Firebase JavaScript configuration"""
firebase_config = {
"apiKey": os.getenv("FIREBASE_API_KEY", ""),
"authDomain": os.getenv("FIREBASE_AUTH_DOMAIN", ""),
"projectId": os.getenv("FIREBASE_PROJECT_ID", ""),
"storageBucket": os.getenv("FIREBASE_STORAGE_BUCKET", ""),
"messagingSenderId": os.getenv("FIREBASE_MESSAGING_SENDER_ID", ""),
"appId": os.getenv("FIREBASE_APP_ID", ""),
"measurementId": os.getenv("FIREBASE_MEASUREMENT_ID", ""),
}
data = """
importScripts('https://www.gstatic.com/firebasejs/11.0.2/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/11.0.2/firebase-messaging-compat.js');
@@ -69,25 +130,35 @@ def get_firebase_js():
return self.registration.showNotification(notificationTitle, notificationOptions);
});
""" % (
os.getenv("FIREBASE_API_KEY", ""),
os.getenv("FIREBASE_AUTH_DOMAIN", ""),
os.getenv("FIREBASE_PROJECT_ID", ""),
os.getenv("FIREBASE_STORAGE_BUCKET", ""),
os.getenv("FIREBASE_MESSAGING_SENDER_ID", ""),
os.getenv("FIREBASE_APP_ID", ""),
os.getenv("FIREBASE_MEASUREMENT_ID", "")
firebase_config["apiKey"],
firebase_config["authDomain"],
firebase_config["projectId"],
firebase_config["storageBucket"],
firebase_config["messagingSenderId"],
firebase_config["appId"],
firebase_config["measurementId"],
)
return HttpResponse(data, content_type="application/javascript")
# Sending Firebase Push Notification
# --------------------------------------------------------------------
# Firebase Admin Initialization
# --------------------------------------------------------------------
# Initialize Firebase Admin
firebase_app = None
try:
cred = credentials.Certificate(os.path.join(BASE_DIR, "firebase-key.json"))
firebase_app = firebase_admin.initialize_app(cred)
except (ValueError, FileNotFoundError) as e:
print(f"Warning: Could not initialize Firebase Admin: {e}")
except firebase_admin.exceptions.FirebaseError as e:
print(f"Warning: Firebase Admin initialization error: {e}")
cert_path = os.path.join(BASE_DIR, "firebase-key.json")
if os.path.exists(cert_path):
cred = credentials.Certificate(cert_path)
firebase_app = firebase_admin.initialize_app(cred)
except Exception:
pass
# --------------------------------------------------------------------
# Push Notification Functions
# --------------------------------------------------------------------
def send_push_notification(title, message, tokens):
@@ -95,22 +166,41 @@ def send_push_notification(title, message, tokens):
Send push notification using Firebase Cloud Messaging
Returns a tuple of (success_count, failure_count, failed_tokens)
"""
if not firebase_app or not tokens:
return 0, len(tokens) if tokens else 0, tokens
if not isinstance(tokens, (list, tuple)):
tokens = list(tokens) # Convert QuerySet to list
success_count = 0
failure_count = 0
failed_tokens = []
# Get only active devices
# Get only active devices that are not fallback tokens
active_devices = FCMDevice.objects.filter(
token__in=tokens,
is_active=True
is_active=True,
is_fallback=False, # Skip fallback tokens for push notifications
)
# Count fallback tokens as "success" for reporting purposes
fallback_count = FCMDevice.objects.filter(
token__in=tokens, is_active=True, is_fallback=True
).count()
success_count += fallback_count
for device in active_devices:
try:
response = messaging.send(
# Skip tokens that match our fallback pattern
if device.token.startswith("fcm-token-") or device.token.startswith(
"fallback-token-"
):
device.is_fallback = True
device.save()
continue
messaging.send(
messaging.Message(
notification=messaging.Notification(
title=title,
@@ -119,17 +209,14 @@ def send_push_notification(title, message, tokens):
token=str(device.token), # Ensure token is a string
)
)
print(f"Successfully sent message: {response}")
success_count += 1
except messaging.UnregisteredError:
# Token is no longer valid, deactivate the device
print(f"Token {device.token} is no longer valid, deactivating device")
device.deactivate()
failure_count += 1
failed_tokens.append(device.token)
except Exception as e:
print(f"Failed to send message to token {device.token}: {e}")
except Exception:
failure_count += 1
failed_tokens.append(device.token)
return success_count, failure_count, failed_tokens
+174 -65
View File
@@ -1,65 +1,174 @@
from django import forms
from django.contrib.auth.hashers import make_password
from django.contrib.auth.forms import SetPasswordForm
from .models import Staff, Student
class CustomSetPasswordForm(SetPasswordForm):
def save(self, commit=True):
user = self.user
user.set_password(self.cleaned_data["new_password1"])
if commit:
user.save()
return user
class StudentAdminForm(forms.ModelForm):
new_password = forms.CharField(
required=False,
widget=forms.PasswordInput,
help_text="Enter a new password if you want to reset it. Leave blank otherwise. Default is '123'.",
)
class Meta:
model = Student
fields = "__all__"
def save(self, commit=True):
student = super().save(commit=False)
new_password = self.cleaned_data.get("new_password")
if new_password:
student.password = make_password(new_password)
# Set default password to '123' for new students if no password provided
elif not student.pk and not student.password: # New student without password
student.password = make_password("123")
if commit:
student.save()
return student
class StaffAdminForm(forms.ModelForm):
new_password = forms.CharField(
required=False,
widget=forms.PasswordInput,
help_text="Enter a new password if you want to reset it. Leave blank otherwise. Default is '123'.",
)
class Meta:
model = Staff
fields = "__all__"
def save(self, commit=True):
staff = super().save(commit=False)
new_password = self.cleaned_data.get("new_password")
if new_password:
staff.password = make_password(new_password)
# Set default password to '123' for new staff if no password provided
elif not staff.pk and not staff.password: # New staff without password
staff.password = make_password("123")
if commit:
staff.save()
return staff
from django import forms
from django.contrib.auth.hashers import make_password
from django.contrib.auth.forms import SetPasswordForm
from app.models import (
Staff,
Student,
Parent,
StaffInstituteFeedback,
ParentInstituteFeedback,
)
from django.contrib.auth.models import Group
class CustomSetPasswordForm(SetPasswordForm):
def save(self, commit=True):
user = self.user
user.set_password(self.cleaned_data["new_password1"])
if commit:
user.save()
return user
class StudentAdminForm(forms.ModelForm):
new_password = forms.CharField(
required=False,
widget=forms.PasswordInput,
help_text="Enter a new password if you want to reset it. Leave blank otherwise. Default is '123'.",
)
class Meta:
model = Student
fields = "__all__"
def save(self, commit=True):
student = super().save(commit=False)
new_password = self.cleaned_data.get("new_password")
if new_password:
student.set_password(new_password)
# Set default password to '123' for new students if no password provided
elif not student.pk and not student.password: # New student without password
student.set_password("123")
if commit:
student.save()
# Make sure student has Student group
if (
hasattr(student, "groups")
and not student.groups.filter(name="Student").exists()
):
student_group = Group.objects.filter(name="Student").first()
if student_group:
student.groups.add(student_group)
# Handle parent creation/update for both new and existing students
if student.parent_name and student.parent_phone:
try:
# Check if parent already exists
parent = Parent.objects.filter(phone=student.parent_phone).first()
if not parent:
# Create new parent
parent = Parent.objects.create(
name=student.parent_name,
phone=student.parent_phone,
password=make_password("123"), # Default password
)
# Add Parent group to new parent
parent_group = Group.objects.filter(name="Parent").first()
if parent_group:
parent.groups.add(parent_group)
# Add student to parent's students
parent.students.add(student)
except Exception as e:
# Log the error but don't prevent student creation
print(f"Error creating/updating parent: {str(e)}")
return student
class StaffAdminForm(forms.ModelForm):
new_password = forms.CharField(
required=False,
widget=forms.PasswordInput,
help_text="Enter a new password if you want to reset it. Leave blank otherwise. Default is '123'.",
)
class Meta:
model = Staff
fields = "__all__" # Include all fields, including groups
def save(self, commit=True):
staff = super().save(commit=False)
new_password = self.cleaned_data.get("new_password")
if new_password:
staff.password = make_password(new_password)
# Set default password to '123' for new staff if no password provided
elif not staff.pk and not staff.password: # New staff without password
staff.password = make_password("123")
if commit:
staff.save()
self.save_m2m() # Save many-to-many relationships including groups
# If groups were not selected in the form, ensure staff has Teacher group
if not staff.groups.exists():
teacher_group = Group.objects.filter(name="Teacher").first()
if teacher_group:
staff.groups.add(teacher_group)
return staff
class ParentAdminForm(forms.ModelForm):
new_password = forms.CharField(
required=False,
widget=forms.PasswordInput,
help_text="Enter a new password if you want to reset it. Leave blank otherwise. Default is '123'.",
)
class Meta:
model = Parent
fields = "__all__"
def save(self, commit=True):
parent = super().save(commit=False)
new_password = self.cleaned_data.get("new_password")
if new_password:
parent.password = make_password(new_password)
# Set default password to '123' for new parents if no password provided
elif not parent.pk and not parent.password: # New parent without password
parent.password = make_password("123")
if commit:
parent.save()
return parent
class StaffInstituteFeedbackForm(forms.ModelForm):
class Meta:
model = StaffInstituteFeedback
fields = [
"feedback_type",
"rating",
"feedback_text",
"is_anonymous",
"is_public",
]
widgets = {
"feedback_text": forms.Textarea(attrs={"rows": 4}),
"rating": forms.NumberInput(attrs={"step": "0.5", "min": "1", "max": "5"}),
}
class ParentInstituteFeedbackForm(forms.ModelForm):
class Meta:
model = ParentInstituteFeedback
fields = [
"feedback_type",
"rating",
"feedback_text",
"is_anonymous",
"is_public",
]
widgets = {
"feedback_text": forms.Textarea(attrs={"rows": 4}),
"rating": forms.NumberInput(attrs={"step": "0.5", "min": "1", "max": "5"}),
}
+140
View File
@@ -0,0 +1,140 @@
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.db.models import Count, Avg, Q
from django.http import JsonResponse
from django.utils import timezone
from datetime import datetime, timedelta
from django.contrib import messages
from django.urls import reverse
from django.http import HttpResponseRedirect
from app.models import (
Staff,
Student,
Course,
Subject,
Routine,
Attendance,
AttendanceRecord,
StudentFeedback,
CourseTracking,
TeacherParentMeeting,
)
@login_required
def hod_dashboard(request):
"""HOD dashboard view showing department overview and management options"""
try:
# Check if user is a HOD
if not request.user.groups.filter(name="HOD").exists():
messages.error(request, "Only HODs can access this dashboard.")
return redirect("login")
# Get the HOD's department (course)
hod = request.user
if not hasattr(hod, "course") or not hod.course:
messages.error(request, "You are not assigned as HOD of any department.")
return redirect("login")
course = hod.course
# Get department statistics
total_students = Student.objects.filter(course=course).count()
total_staff = Staff.objects.filter(course=course).count()
total_subjects = Subject.objects.filter(course=course).count()
# Get subjects for the course
subjects = Subject.objects.filter(course=course).select_related("course")
# Get routines to find teachers for each subject
routines = Routine.objects.filter(course=course, is_active=True).select_related(
"subject", "teacher"
)
# Create a mapping of subject_id to teacher
teacher_map = {}
for routine in routines:
teacher_map[routine.subject_id] = routine.teacher
# Count students for each subject and attach teacher info
for subject in subjects:
# Attach teacher if available from routines
subject.teacher = teacher_map.get(subject.id)
# Count students in this subject's period
subject.student_count = Student.objects.filter(
course=course, current_period=subject.period_or_year
).count()
# Get subject distribution data for chart - count subjects by period
subject_distribution = []
max_periods = (
8 if course.duration_type == "Semester" else 4
) # Max 8 semesters or 4 years
for i in range(1, max_periods + 1):
if i <= (
course.duration * 2
if course.duration_type == "Semester"
else course.duration
):
period_count = subjects.filter(period_or_year=i).count()
subject_distribution.append(period_count)
# Get today's classes and their attendance
today = timezone.now().date()
today_classes = Routine.objects.filter(
course=course, is_active=True
).select_related("subject", "teacher")
# Get attendance for today's classes
today_attendance = Attendance.objects.filter(
routine__course=course, date=today
).select_related("routine")
# Create a dictionary of routine_id to attendance
attendance_map = {att.routine_id: att for att in today_attendance}
# Attach attendance to each class
for class_obj in today_classes:
class_obj.attendance = attendance_map.get(class_obj.id)
# Get student progress
student_progress = (
CourseTracking.objects.filter(student__course=course)
.select_related("student")
.order_by("-completion_percentage")[:5]
)
# Get upcoming meetings
upcoming_meetings = TeacherParentMeeting.objects.filter(
meeting_date__gte=today, status="scheduled"
).order_by("meeting_date", "meeting_time")[:5]
# Get recent feedback
recent_feedback = (
StudentFeedback.objects.filter(student__course=course)
.select_related("student", "teacher")
.order_by("-created_at")[:5]
)
context = {
"title": "HOD Dashboard",
"hod": hod,
"course": course,
"total_students": total_students,
"total_staff": total_staff,
"total_subjects": total_subjects,
"today_classes": today_classes,
"student_progress": student_progress,
"upcoming_meetings": upcoming_meetings,
"recent_feedback": recent_feedback,
"subjects": subjects,
"subject_distribution": subject_distribution,
}
return render(request, "hod/dashboard.html", context)
except Exception as e:
messages.error(request, f"An error occurred: {str(e)}")
return redirect("login")
+78
View File
@@ -0,0 +1,78 @@
# Fake Data Generator for SMS
This directory contains a Django management command to generate fake data for testing the School Management System (SMS).
## Prerequisites
Before using this script, make sure you have installed the required packages:
```bash
pip install faker
```
## Usage
Run the command using Django's `manage.py`:
```bash
python manage.py generate_fake_data
```
### Options
The script accepts the following optional parameters:
- `--students`: Number of students to create (default: 50)
- `--staff`: Number of staff members to create (default: 20)
- `--parents`: Number of parents to create (default: 40)
- `--courses`: Number of courses to create (default: 5)
- `--subjects`: Number of subjects per course (default: 4)
- `--clear`: Clear existing data before generating new data (flag)
### Examples
Generate data with default parameters:
```bash
python manage.py generate_fake_data
```
Generate a smaller dataset:
```bash
python manage.py generate_fake_data --students 20 --staff 10 --parents 15
```
Clear existing data and generate new data:
```bash
python manage.py generate_fake_data --clear
```
## Generated Data
The script generates the following types of data:
1. Institute information
2. Batches (last 4 years)
3. Courses with customized names and durations
4. Subjects for each course
5. Staff members with profiles and credentials
6. Students with profiles and course assignments
7. Parents linked to students
8. Class routines and schedules
9. Attendance records for past week
10. Notices/announcements
11. Leave requests for students and staff
12. Feedback (student, parent, institute)
13. Parent-teacher meetings
14. Course tracking records
## Login Credentials
The generated users have the following default passwords:
- Students: `student123`
- Staff: `staff123`
- Parents: `parent123`
## Note
The script doesn't generate actual file content for subject files due to the limitation of creating binary content programmatically. This portion is commented out in the code.
@@ -0,0 +1,717 @@
import random
from datetime import datetime, timedelta
import pytz
from django.core.management.base import BaseCommand
from django.utils import timezone
from django.contrib.auth.models import Group
from django.db import transaction
from faker import Faker
from app.models import (
Institute, Batch, Course, Subject, Staff, Student, Parent,
Routine, Attendance, AttendanceRecord, Notice,
StaffLeave, StudentLeave, StudentFeedback, ParentFeedback,
InstituteFeedback, StaffInstituteFeedback, ParentInstituteFeedback,
CourseTracking, TeacherParentMeeting, SubjectFile
)
class Command(BaseCommand):
help = 'Generate fake data for testing the SMS application'
def add_arguments(self, parser):
parser.add_argument('--students', type=int, default=50, help='Number of students to create')
parser.add_argument('--staff', type=int, default=20, help='Number of staff members to create')
parser.add_argument('--parents', type=int, default=40, help='Number of parents to create')
parser.add_argument('--courses', type=int, default=5, help='Number of courses to create')
parser.add_argument('--subjects', type=int, default=4, help='Subjects per course')
parser.add_argument('--clear', action='store_true', help='Clear existing data before generating new data')
@transaction.atomic
def handle(self, *args, **options):
fake = Faker()
# Get parameters
num_students = options['students']
num_staff = options['staff']
num_parents = options['parents']
num_courses = options['courses']
subjects_per_course = options['subjects']
clear_data = options['clear']
# Get or create groups
staff_group, _ = Group.objects.get_or_create(name='Teacher')
parent_group, _ = Group.objects.get_or_create(name='Parent')
student_group, _ = Group.objects.get_or_create(name='Student')
# Clear existing data if requested
if clear_data:
self.stdout.write(self.style.WARNING('Clearing existing data...'))
TeacherParentMeeting.objects.all().delete()
ParentInstituteFeedback.objects.all().delete()
StaffInstituteFeedback.objects.all().delete()
InstituteFeedback.objects.all().delete()
ParentFeedback.objects.all().delete()
StudentFeedback.objects.all().delete()
StudentLeave.objects.all().delete()
StaffLeave.objects.all().delete()
Notice.objects.all().delete()
AttendanceRecord.objects.all().delete()
Attendance.objects.all().delete()
Routine.objects.all().delete()
SubjectFile.objects.all().delete()
CourseTracking.objects.all().delete()
# Clear many-to-many relationships before deleting models
for parent in Parent.objects.all():
parent.students.clear()
for student in Student.objects.all():
student.batches.clear()
Subject.objects.all().delete()
Parent.objects.all().delete()
Student.objects.all().delete()
Staff.objects.all().delete()
Course.objects.all().delete()
Batch.objects.all().delete()
Institute.objects.all().delete()
self.stdout.write(self.style.SUCCESS('Data cleared successfully'))
# Create Institute
institute = self._create_institute(fake)
# Create batches (last 4 years)
batches = self._create_batches()
# Create courses
courses = self._create_courses(fake, num_courses)
# Create subjects for each course
subjects = self._create_subjects(fake, courses, subjects_per_course)
# Create staff members
staff_members = self._create_staff(fake, num_staff, staff_group)
# Create students
students = self._create_students(fake, num_students, courses, batches, student_group)
# Create parents and link to students
parents = self._create_parents(fake, num_parents, students, parent_group)
# Create routines and class schedules
routines = self._create_routines(courses, subjects, staff_members)
# Create attendance records
attendance_records = self._create_attendance(routines, students)
# Create notices
notices = self._create_notices(fake)
# Create leave requests
self._create_leaves(fake, staff_members, students)
# Create feedback
self._create_feedback(fake, students, staff_members, parents, institute)
# Create parent-teacher meetings
self._create_meetings(fake, staff_members, parents)
# Create subject files
self._create_subject_files(fake, subjects, staff_members)
# Create course tracking for students
self._create_course_tracking(students, courses)
self.stdout.write(self.style.SUCCESS('Successfully generated fake data!'))
def _create_institute(self, fake):
institute, created = Institute.objects.get_or_create(
name=fake.company() + " Institute of Technology",
defaults={
'phone': fake.phone_number(),
'email': fake.company_email(),
'address': fake.address(),
'pan_no': 'PAN' + fake.numerify(text='#####'),
'reg_no': 'REG' + fake.numerify(text='######'),
'description': fake.paragraph(nb_sentences=3)
}
)
if created:
self.stdout.write(self.style.SUCCESS(f'Created institute: {institute.name}'))
return institute
def _create_batches(self):
batches = []
current_year = datetime.now().year
for year in range(current_year - 3, current_year + 1):
batch, created = Batch.objects.get_or_create(
name=f'Batch {year}',
defaults={'year': datetime(year, 1, 1)}
)
batches.append(batch)
if created:
self.stdout.write(self.style.SUCCESS(f'Created batch: {batch.name}'))
return batches
def _create_courses(self, fake, num_courses):
courses = []
course_names = [
'Computer Science Engineering',
'Electrical Engineering',
'Mechanical Engineering',
'Civil Engineering',
'Chemical Engineering',
'Electronics Engineering',
'Information Technology',
'Aerospace Engineering',
'Biotechnology Engineering',
'Data Science'
]
# Ensure we don't try to create more courses than we have names for
num_to_create = min(num_courses, len(course_names))
for i in range(num_to_create):
duration = random.choice([3, 4])
duration_type = random.choice(['Year', 'Semester'])
course, created = Course.objects.get_or_create(
name=course_names[i],
defaults={
'duration': duration,
'duration_type': duration_type
}
)
courses.append(course)
if created:
self.stdout.write(self.style.SUCCESS(f'Created course: {course.name}'))
return courses
def _create_subjects(self, fake, courses, subjects_per_course):
subjects = []
subject_names = {
'Computer Science Engineering': [
'Data Structures', 'Algorithms', 'Database Systems', 'Operating Systems',
'Computer Networks', 'Software Engineering', 'Web Development', 'AI and Machine Learning'
],
'Electrical Engineering': [
'Circuit Theory', 'Electrical Machines', 'Power Systems', 'Control Systems',
'Digital Electronics', 'Microprocessors', 'Power Electronics', 'Electrical Measurements'
],
'Mechanical Engineering': [
'Thermodynamics', 'Fluid Mechanics', 'Manufacturing Processes', 'Machine Design',
'Heat Transfer', 'Engineering Mechanics', 'Automobile Engineering', 'Materials Science'
],
'Information Technology': [
'Programming Fundamentals', 'Data Structures', 'Database Management', 'Web Technologies',
'Computer Networks', 'Software Engineering', 'Cybersecurity', 'Cloud Computing'
],
'Data Science': [
'Statistical Methods', 'Machine Learning', 'Data Mining', 'Big Data Analytics',
'Data Visualization', 'Deep Learning', 'Natural Language Processing', 'Time Series Analysis'
]
}
# Add generic subjects for courses not explicitly defined
generic_subjects = [
'Mathematics I', 'Mathematics II', 'Physics', 'Chemistry',
'Technical Writing', 'Engineering Ethics', 'Project Management', 'Research Methods'
]
for course in courses:
# Get subject list for this course or use generic subjects
if course.name in subject_names:
course_subject_names = subject_names[course.name]
else:
course_subject_names = generic_subjects
max_year_or_sem = course.duration
if course.duration_type == 'Semester':
max_year_or_sem = course.duration * 2
# Create subjects for each year/semester
for year_or_sem in range(1, max_year_or_sem + 1):
# Create a subset of subjects for this year/semester
num_to_create = min(subjects_per_course, len(course_subject_names))
selected_subjects = random.sample(course_subject_names, num_to_create)
for subject_name in selected_subjects:
subject, created = Subject.objects.get_or_create(
name=subject_name,
course=course,
period_or_year=year_or_sem
)
subjects.append(subject)
if created:
self.stdout.write(self.style.SUCCESS(f'Created subject: {subject.name} for {course.name}'))
return subjects
def _create_staff(self, fake, num_staff, staff_group):
staff_members = []
# Create staff members
for i in range(num_staff):
gender = random.choice(['Male', 'Female'])
first_name = fake.first_name_male() if gender == 'Male' else fake.first_name_female()
last_name = fake.last_name()
name = f"{first_name} {last_name}"
# Generate a unique phone number
while True:
phone = fake.numerify(text='9#########')
if not Staff.objects.filter(phone=phone).exists():
break
staff = Staff(
name=name,
phone=phone,
designation='Teacher',
gender=gender,
birth_date=fake.date_of_birth(minimum_age=30, maximum_age=65),
email=fake.email(),
temporary_address=fake.address(),
permanent_address=fake.address(),
marital_status=random.choice(['Married', 'Unmarried']),
parent_name=fake.name(),
parent_phone=fake.phone_number(),
citizenship_no=fake.numerify(text='#########'),
joining_date=fake.date_between(start_date='-5y', end_date='today')
)
staff.save() # Save first to get primary key
staff.set_password('staff123') # Then set password
staff.save() # Save again to update password
staff.groups.add(staff_group) # Add to staff group
staff_members.append(staff)
self.stdout.write(self.style.SUCCESS(f'Created staff: {staff.name}'))
return staff_members
def _create_students(self, fake, num_students, courses, batches, student_group):
students = []
for i in range(num_students):
gender = random.choice(['Male', 'Female'])
first_name = fake.first_name_male() if gender == 'Male' else fake.first_name_female()
last_name = fake.last_name()
name = f"{first_name} {last_name}"
# Generate a unique phone number
while True:
phone = fake.numerify(text='9#########')
if not Student.objects.filter(phone=phone).exists():
break
# Randomly assign a course and batch
course = random.choice(courses)
max_semester = course.duration
if course.duration_type == 'Semester':
max_semester = course.duration * 2
current_period = random.randint(1, max_semester)
student = Student(
name=name,
status=random.choice(['Active', 'Leave', 'Completed']),
gender=gender,
birth_date=fake.date_of_birth(minimum_age=18, maximum_age=25),
email=fake.email(),
phone=phone,
temporary_address=fake.address(),
permanent_address=fake.address(),
marital_status=random.choice(['Married', 'Unmarried']),
parent_name=fake.name(),
parent_phone=fake.phone_number(),
citizenship_no=fake.numerify(text='#########'),
course=course,
current_period=current_period,
joining_date=fake.date_between(start_date='-4y', end_date='-1y')
)
student.save() # Save first to get primary key
student.set_password('student123') # Then set password
student.save() # Save again to update password
student.groups.add(student_group) # Add to student group
# Add student to a random batch
batch = random.choice(batches)
student.batches.add(batch)
students.append(student)
self.stdout.write(self.style.SUCCESS(f'Created student: {student.name}'))
return students
def _create_parents(self, fake, num_parents, students, parent_group):
parents = []
# Create parents and assign students
for i in range(num_parents):
gender = random.choice(['Male', 'Female'])
first_name = fake.first_name_male() if gender == 'Male' else fake.first_name_female()
last_name = fake.last_name()
name = f"{first_name} {last_name}"
# Generate a unique phone number
while True:
phone = fake.numerify(text='9#########')
if not Parent.objects.filter(phone=phone).exists():
break
parent = Parent(
name=name,
phone=phone,
email=fake.email(),
address=fake.address(),
)
parent.save() # Save first to get primary key
parent.set_password('parent123') # Then set password
parent.save() # Save again to update password
parent.groups.add(parent_group) # Add to parent group
# Assign 1-2 students to this parent
num_children = random.randint(1, min(2, len(students)))
assigned_students = random.sample(students, num_children)
for student in assigned_students:
parent.students.add(student)
parents.append(parent)
self.stdout.write(self.style.SUCCESS(f'Created parent: {parent.name} with {num_children} children'))
return parents
def _create_routines(self, courses, subjects, staff_members):
routines = []
days_of_week = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']
for course in courses:
# Get subjects for this course
course_subjects = Subject.objects.filter(course=course)
for subject in course_subjects:
# Assign a random teacher
teacher = random.choice(staff_members)
# Create routine for this subject
start_time = datetime.strptime(f"{random.randint(9, 16)}:00", "%H:%M").time()
end_time = (datetime.combine(datetime.today(), start_time) + timedelta(hours=1)).time()
routine = Routine(
course=course,
subject=subject,
teacher=teacher,
start_time=start_time,
end_time=end_time,
period_or_year=subject.period_or_year,
is_active=True
)
# Skip if there's a clash in times
try:
routine.save()
routines.append(routine)
self.stdout.write(self.style.SUCCESS(f'Created routine for {subject.name} with {teacher.name}'))
except Exception as e:
# Just skip if there's an error (likely due to unique constraint violation)
pass
return routines
def _create_attendance(self, routines, students):
attendance_records = []
# Create attendance for the past week
for day_offset in range(7, 0, -1):
date = datetime.now().date() - timedelta(days=day_offset)
for routine in routines:
# Create attendance for this routine on this date
attendance = Attendance(
date=date,
routine=routine,
teacher=routine.teacher,
teacher_attend=random.choice([True, True, True, False]), # More likely to attend
class_status=True
)
try:
attendance.save()
# Find students in this course and semester
course_students = Student.objects.filter(
course=routine.course,
current_period=routine.period_or_year
)
# Create attendance records for students
for student in course_students:
is_present = random.choice([True, True, False]) # 2/3 chance of being present
record = AttendanceRecord(
attendance=attendance,
student=student,
student_attend=is_present
)
record.save()
attendance_records.append(record)
self.stdout.write(self.style.SUCCESS(f'Created attendance for {routine.subject.name} on {date}'))
except Exception as e:
# Skip if there's an error
pass
return attendance_records
def _create_notices(self, fake):
notices = []
for i in range(10): # Create 10 notices
notice = Notice(
title=fake.sentence(),
message=fake.paragraph(nb_sentences=3),
created_at=fake.date_time_between(start_date='-30d', end_date='now')
)
notice.save()
notices.append(notice)
self.stdout.write(self.style.SUCCESS(f'Created notice: {notice.title}'))
return notices
def _create_leaves(self, fake, staff_members, students):
# Create staff leaves
for staff in random.sample(staff_members, len(staff_members) // 3): # 1/3 of staff
start_date = fake.date_between(start_date='-30d', end_date='+30d')
end_date = start_date + timedelta(days=random.randint(1, 5))
leave = StaffLeave(
staff=staff,
start_date=start_date,
end_date=end_date,
message=fake.paragraph(),
status=random.choice([0, 1, 2]) # Pending, Approved, Rejected
)
leave.save()
self.stdout.write(self.style.SUCCESS(f'Created staff leave for {staff.name}'))
# Create student leaves
for student in random.sample(students, len(students) // 3): # 1/3 of students
start_date = fake.date_between(start_date='-30d', end_date='+30d')
end_date = start_date + timedelta(days=random.randint(1, 5))
leave = StudentLeave(
student=student,
start_date=start_date,
end_date=end_date,
message=fake.paragraph(),
status=random.choice([0, 1, 2]) # Pending, Approved, Rejected
)
leave.save()
self.stdout.write(self.style.SUCCESS(f'Created student leave for {student.name}'))
def _create_feedback(self, fake, students, staff_members, parents, institute):
# Create student feedback for teachers
for student in random.sample(students, len(students) // 2):
for teacher in random.sample(staff_members, min(3, len(staff_members))):
try:
feedback = StudentFeedback(
student=student,
teacher=teacher,
rating=random.choice([3.5, 4.0, 4.5, 5.0]),
feedback_text=fake.paragraph()
)
feedback.save()
self.stdout.write(self.style.SUCCESS(f'Created student feedback from {student.name} for {teacher.name}'))
except:
# Skip if there's an error (likely due to unique constraint)
pass
# Create parent feedback for teachers
for parent in random.sample(parents, len(parents) // 2):
parent_students = parent.students.all()
if parent_students:
for student in parent_students:
for teacher in random.sample(staff_members, min(2, len(staff_members))):
try:
feedback = ParentFeedback(
parent=parent,
teacher=teacher,
student=student,
rating=random.choice([3.5, 4.0, 4.5, 5.0]),
feedback_text=fake.paragraph()
)
feedback.save()
self.stdout.write(self.style.SUCCESS(f'Created parent feedback from {parent.name} for {teacher.name}'))
except:
# Skip if there's an error
pass
# Create institute feedback
for student in random.sample(students, len(students) // 3):
try:
feedback = InstituteFeedback(
institute=institute,
user=student,
feedback_type=random.choice(['General', 'Facilities', 'Teaching', 'Infrastructure', 'Administration']),
rating=random.choice([3.0, 3.5, 4.0, 4.5, 5.0]),
feedback_text=fake.paragraph(),
is_anonymous=random.choice([True, False]),
is_public=True
)
feedback.save()
self.stdout.write(self.style.SUCCESS(f'Created institute feedback from {student.name}'))
except:
# Skip if there's an error
pass
# Create staff institute feedback
for staff in random.sample(staff_members, len(staff_members) // 3):
try:
feedback = StaffInstituteFeedback(
institute=institute,
staff=staff,
feedback_type=random.choice(['General', 'Facilities', 'Teaching', 'Infrastructure', 'Administration']),
rating=random.choice([3.0, 3.5, 4.0, 4.5, 5.0]),
feedback_text=fake.paragraph(),
is_anonymous=random.choice([True, False]),
is_public=True
)
feedback.save()
self.stdout.write(self.style.SUCCESS(f'Created staff institute feedback from {staff.name}'))
except:
# Skip if there's an error
pass
# Create parent institute feedback
for parent in random.sample(parents, len(parents) // 3):
try:
feedback = ParentInstituteFeedback(
institute=institute,
parent=parent,
feedback_type=random.choice(['General', 'Facilities', 'Administration']),
rating=random.choice([3.0, 3.5, 4.0, 4.5, 5.0]),
feedback_text=fake.paragraph(),
is_anonymous=random.choice([True, False]),
is_public=True
)
feedback.save()
self.stdout.write(self.style.SUCCESS(f'Created parent institute feedback from {parent.name}'))
except:
# Skip if there's an error
pass
def _create_meetings(self, fake, staff_members, parents):
# Create parent-teacher meetings
future_dates = [datetime.now().date() + timedelta(days=d) for d in range(1, 15)]
for staff in random.sample(staff_members, len(staff_members) // 2):
for parent in random.sample(parents, min(3, len(parents))):
meeting_date = random.choice(future_dates)
meeting_hour = random.randint(9, 16)
meeting_minute = random.choice([0, 15, 30, 45])
meeting_time = f"{meeting_hour:02d}:{meeting_minute:02d}"
meeting_status = random.choice(['scheduled', 'rescheduled', 'completed', 'cancelled'])
try:
meeting = TeacherParentMeeting(
meeting_date=meeting_date,
meeting_time=datetime.strptime(meeting_time, "%H:%M").time(),
duration=30,
status=meeting_status,
agenda=fake.paragraph(),
notes=fake.paragraph() if meeting_status == 'completed' else '',
cancellation_reason=fake.paragraph() if meeting_status == 'cancelled' else '',
is_online=random.choice([True, False])
)
if meeting.is_online:
meeting.meeting_link = f"https://meet.example.com/{fake.uuid4()}"
meeting.save()
self.stdout.write(self.style.SUCCESS(f'Created parent-teacher meeting on {meeting_date}'))
except Exception as e:
# Skip if there's an error
pass
def _create_subject_files(self, fake, subjects, staff_members):
# Create subject files (lecture notes, etc.)
for subject in random.sample(subjects, len(subjects) // 2):
# Choose a teacher who might be teaching this subject
teacher = random.choice(staff_members)
# Create 1-3 files for each selected subject
for i in range(random.randint(1, 3)):
file_types = ['Lecture Notes', 'Assignment', 'Reference Material', 'Exam Paper']
file_type = random.choice(file_types)
subject_file = SubjectFile(
subject=subject,
title=f"{file_type} - {subject.name} - {i+1}",
description=fake.paragraph(),
# Note: actual file content can't be created here
# file field would need an actual file
uploaded_by=teacher
)
try:
# We can't actually save this without a real file
# subject_file.save()
self.stdout.write(self.style.SUCCESS(f'Created subject file: {subject_file.title}'))
except:
# Skip if there's an error
pass
def _create_course_tracking(self, students, courses):
# Create course tracking records for students
for student in students:
course = student.course
if course:
try:
# Check if tracking already exists
existing_tracking = CourseTracking.objects.filter(
student=student,
course=course
).first()
if existing_tracking:
continue # Skip if tracking already exists
# Calculate dates based on student's joining date
joining_date = student.joining_date or datetime.now().date() - timedelta(days=random.randint(180, 365))
expected_end_date = joining_date + timedelta(days=365 * course.duration)
# Calculate completion percentage based on current semester
if course.duration_type == 'Semester':
total_semesters = course.duration * 2
completion_percentage = int((student.current_period / total_semesters) * 100)
else:
completion_percentage = int((student.current_period / course.duration) * 100)
# Determine progress status
if completion_percentage == 100:
progress_status = 'Completed'
elif completion_percentage == 0:
progress_status = 'Not Started'
else:
progress_status = 'In Progress'
tracking = CourseTracking(
student=student,
course=course,
enrollment_date=joining_date,
start_date=joining_date,
expected_end_date=expected_end_date,
actual_end_date=None,
progress_status=progress_status,
completion_percentage=completion_percentage,
current_period=student.current_period,
semester_start_date=joining_date,
notes=f"Tracking student progress in {course.name}"
)
tracking.save()
self.stdout.write(self.style.SUCCESS(f'Created course tracking for {student.name} in {course.name}'))
except Exception as e:
# Log the error but continue with other students
self.stdout.write(self.style.ERROR(f'Error creating course tracking for {student.name}: {str(e)}'))
continue # Continue with next student
+29
View File
@@ -0,0 +1,29 @@
from django.http import HttpResponse
from django.template.loader import render_to_string
class HTTP505Middleware:
"""
Middleware to handle HTTP 505 errors (HTTP Version Not Supported)
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Process the request and get the response
response = self.get_response(request)
return response
def process_exception(self, request, exception):
# Check if exception is related to HTTP version
if hasattr(exception, "status_code") and exception.status_code == 505:
# Render our 505 template
context = {
"request_path": request.path,
"request_method": request.method,
"user": request.user,
}
response_content = render_to_string("505.html", context, request)
return HttpResponse(response_content, status=505)
return None
View File
Whitespace-only changes.
+603 -135
View File
File diff suppressed because it is too large. Load diff
+612
View File
@@ -0,0 +1,612 @@
# Standard library imports
import json
import traceback
# Core Django imports
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
# Local app imports
from app.models import (
Batch,
Course,
CourseTracking,
InstituteFeedback,
ParentFeedback,
ParentInstituteFeedback,
Student,
StudentFeedback,
StudentLeave,
Subject,
TeacherParentMeeting,
AttendanceRecord,
Notice,
Routine,
)
@login_required
def parent_dashboard(request):
"""
View for the parent dashboard.
Requires user to be authenticated and belong to the Parent group.
"""
# Check if user is a parent
if not request.user.groups.filter(name="Parent").exists():
messages.error(request, "Only parents can access this dashboard.")
return redirect("login")
try:
parent = request.user
today = timezone.now().date()
# Get all students of the parent with their course trackings
students = (
parent.students.all()
.select_related("course")
.prefetch_related("course_trackings", "attendance_records")
)
# Get all meetings without student filtering
meetings = TeacherParentMeeting.objects.all().order_by(
"-meeting_date", "-meeting_time"
)
# Get attendance summary for all students
attendance_summary = {
"total_students": students.count(),
"students_present_today": 0,
"students_absent_today": 0,
"overall_attendance": 0,
}
total_attendance = 0
total_classes = 0
for student in students:
# Get today's attendance
today_attendance = AttendanceRecord.objects.filter(
student=student, attendance__date=today, student_attend=True
).count()
if today_attendance > 0:
attendance_summary["students_present_today"] += 1
else:
attendance_summary["students_absent_today"] += 1
# Get overall attendance
student_attendance = student.attendance_records.all()
total_attendance += student_attendance.filter(student_attend=True).count()
total_classes += student_attendance.count()
if total_classes > 0:
attendance_summary["overall_attendance"] = (
total_attendance / total_classes
) * 100
# Get course progress for all students
course_progress = []
for student in students:
active_tracking = student.course_trackings.filter(
progress_status="In Progress"
).first()
if active_tracking:
course_progress.append(
{
"student": student.name,
"course": active_tracking.course.name,
"progress": active_tracking.completion_percentage,
"status": active_tracking.progress_status,
}
)
# Get recent leaves for all students
recent_leaves = StudentLeave.objects.filter(student__in=students).order_by(
"-created_at"
)[:5]
# Get recent teacher feedback
teacher_feedback = (
ParentFeedback.objects.filter(parent=parent)
.select_related("teacher", "student")
.order_by("-created_at")[:5]
)
# Get recent institute feedback
institute_feedback = (
ParentInstituteFeedback.objects.filter(parent=parent)
.select_related("student")
.order_by("-created_at")[:5]
)
# Get notices
notices = Notice.objects.all().order_by("-created_at")[:5]
# Get routines for each student
student_data = []
for student in students:
student_info = {
"student": student,
"course_name": student.course.name if student.course else None,
"course_duration_type": student.course.duration_type
if student.course
else None,
"current_period": None,
"current_subjects": [],
"current_period_routines": [],
}
# Get active course tracking
active_tracking = student.course_trackings.filter(
progress_status="In Progress"
).first()
# Get leave counts
student_info["pending_leaves"] = student.leave_requests.filter(
status=0
).count()
student_info["approved_leaves"] = student.leave_requests.filter(
status=1
).count()
student_info["rejected_leaves"] = student.leave_requests.filter(
status=2
).count()
if active_tracking:
student_info["current_period"] = active_tracking.current_period
# Get routines for the current period
current_period_routines = list(
Routine.objects.filter(
course=active_tracking.course,
period_or_year=active_tracking.current_period,
is_active=True,
)
.select_related("subject", "teacher")
.order_by("start_time")
)
# Get current subjects for the student
current_subjects = list(
Subject.objects.filter(
course=active_tracking.course,
period_or_year=active_tracking.current_period,
).order_by("name")
)
print(f"Found {len(current_subjects)} subjects:")
for subject in current_subjects:
print(f"- {subject.name}")
print(f"\nFound {len(current_period_routines)} routines:")
for routine in current_period_routines:
print(
f"- {routine.subject.name} ({routine.start_time} - {routine.end_time})"
)
student_info["current_period_routines"] = current_period_routines
student_info["current_subjects"] = current_subjects
student_data.append(student_info)
context = {
"parent": parent,
"student_data": student_data,
"attendance_summary": attendance_summary,
"course_progress": course_progress,
"recent_leaves": recent_leaves,
"teacher_feedback": teacher_feedback,
"institute_feedback": institute_feedback,
"notices": notices,
"meetings": meetings,
"today": today,
}
return render(request, "parent/dashboard.html", context)
except Exception as e:
messages.error(request, f"Error loading dashboard: {str(e)}")
return redirect("login")
@login_required
def submit_parent_feedback(request):
"""View for parents to submit feedback about teachers"""
# Check if user is a parent
if not request.user.groups.filter(name="Parent").exists():
messages.error(request, "Only parents can submit feedback.")
return redirect("parent_dashboard")
parent = request.user
if request.method == "POST":
teacher_id = request.POST.get("teacher")
student_id = request.POST.get("student")
rating = request.POST.get("rating")
feedback_text = request.POST.get("feedback_text")
try:
teacher = Staff.objects.get(id=teacher_id)
student = Student.objects.get(id=student_id)
# Verify that the student belongs to this parent
if student not in parent.students.all():
messages.error(
request, "You can only submit feedback for your children."
)
return redirect("submit_parent_feedback")
# Create or update feedback
ParentFeedback.objects.update_or_create(
parent=parent,
teacher=teacher,
student=student,
defaults={
"rating": rating,
"feedback_text": feedback_text,
},
)
messages.success(request, "Feedback submitted successfully!")
return redirect("parent_dashboard")
except (Staff.DoesNotExist, Student.DoesNotExist):
messages.error(request, "Invalid teacher or student selected.")
except Exception as e:
messages.error(request, f"An error occurred: {str(e)}")
# Get list of teachers who have taught the parent's students
# Get all students of the parent
students = parent.students.all()
# Get all teachers who have routines for these students' courses
teachers = Staff.objects.filter(
routine__course__in=students.values_list("course_trackings__course", flat=True)
).distinct()
# Get list of students for the dropdown
context = {
"teachers": teachers,
"students": students,
}
return render(request, "parent/feedback.html", context)
@login_required
def submit_parent_institute_feedback(request):
"""View for parents to submit feedback about the institute"""
# Check if user is a parent
if not request.user.groups.filter(name="Parent").exists():
messages.error(request, "Only parents can submit feedback.")
return redirect("parent_dashboard")
parent = request.user
if request.method == "POST":
feedback_type = request.POST.get("feedback_type")
student_id = request.POST.get("student_id")
rating = request.POST.get("rating")
feedback_text = request.POST.get("feedback_text")
is_anonymous = request.POST.get("is_anonymous") == "on"
try:
student = Student.objects.get(id=student_id)
# Verify that the student belongs to this parent
if student not in parent.students.all():
messages.error(
request, "You can only submit feedback for your children."
)
return redirect("parent_dashboard")
# Convert to float first to handle half-stars
rating_value = float(rating)
if rating_value < 0.5 or rating_value > 5:
messages.error(request, "Rating must be between 0.5 and 5")
return redirect("parent_dashboard")
# Check if it's a valid half or full star rating
if rating_value % 0.5 != 0:
messages.error(request, "Rating must be a whole or half number")
return redirect("parent_dashboard")
# Get the institute (assuming one institute for now)
institute = Institute.objects.first()
if not institute:
# Create a default institute if none exists
institute = Institute.objects.create(
name="Default Institute",
phone="123456789",
email="default@example.com",
)
# Check if feedback already exists for this type
existing_feedback = ParentInstituteFeedback.objects.filter(
parent=parent,
institute=institute,
feedback_type=feedback_type,
).first()
if existing_feedback:
# Update existing feedback
existing_feedback.rating = rating_value
existing_feedback.feedback_text = feedback_text
existing_feedback.is_anonymous = is_anonymous
existing_feedback.save()
messages.success(request, "Institute feedback updated successfully")
else:
# Create new feedback
ParentInstituteFeedback.objects.create(
parent=parent,
institute=institute,
feedback_type=feedback_type,
rating=rating_value,
feedback_text=feedback_text,
is_anonymous=is_anonymous,
is_public=True, # Default to public feedback
)
messages.success(request, "Institute feedback submitted successfully")
except ValueError:
messages.error(request, "Invalid rating value")
except Exception as e:
messages.error(request, f"Error submitting institute feedback: {str(e)}")
return redirect("parent_dashboard")
@login_required
def parent_meetings(request):
"""View for redirecting to the parent dashboard meetings section"""
# Check if user is a parent
if not request.user.groups.filter(name="Parent").exists():
messages.error(request, "Only parents can access meetings.")
return redirect("parent_dashboard")
# Redirect to parent dashboard with meetings section highlighted
return redirect("parent_dashboard")
@login_required
def add_meeting(request):
"""Add a new parent-teacher meeting"""
if request.method == "POST":
try:
data = json.loads(request.body)
meeting = TeacherParentMeeting.objects.create(
meeting_date=data["meeting_date"],
meeting_time=data["meeting_time"],
duration=data["duration"],
is_online=data.get("is_online", False),
meeting_link=data.get("meeting_link", ""),
agenda=data.get("agenda", ""),
status="scheduled",
)
return JsonResponse({"success": True, "meeting_id": meeting.id})
except Exception as e:
return JsonResponse({"success": False, "error": str(e)})
return JsonResponse({"success": False, "error": "Invalid request method"})
@login_required
def complete_meeting(request, meeting_id):
"""Mark a meeting as completed"""
if request.method == "POST":
try:
meeting = TeacherParentMeeting.objects.get(id=meeting_id)
meeting.status = "completed"
meeting.save()
return JsonResponse({"success": True})
except Exception as e:
return JsonResponse({"success": False, "error": str(e)})
return JsonResponse({"success": False, "error": "Invalid request method"})
@login_required
def cancel_meeting(request, meeting_id):
"""Cancel a meeting"""
if request.method == "POST":
try:
data = json.loads(request.body)
meeting = TeacherParentMeeting.objects.get(id=meeting_id)
meeting.status = "cancelled"
meeting.cancellation_reason = data.get("reason", "")
meeting.save()
return JsonResponse({"success": True})
except Exception as e:
return JsonResponse({"success": False, "error": str(e)})
return JsonResponse({"success": False, "error": "Invalid request method"})
@login_required
def reschedule_meeting(request, meeting_id):
"""Reschedule a meeting"""
if request.method == "POST":
try:
data = json.loads(request.body)
meeting = TeacherParentMeeting.objects.get(id=meeting_id)
meeting.meeting_date = data["meeting_date"]
meeting.meeting_time = data["meeting_time"]
meeting.status = "rescheduled"
meeting.save()
return JsonResponse({"success": True})
except Exception as e:
return JsonResponse({"success": False, "error": str(e)})
return JsonResponse({"success": False, "error": "Invalid request method"})
@login_required
def get_meeting_notes(request, meeting_id):
"""Get meeting notes"""
try:
meeting = TeacherParentMeeting.objects.get(id=meeting_id)
return JsonResponse({"success": True, "notes": meeting.notes})
except Exception as e:
return JsonResponse({"success": False, "error": str(e)})
@login_required
def get_meeting_agenda(request, meeting_id):
"""Get meeting agenda"""
try:
meeting = TeacherParentMeeting.objects.get(id=meeting_id)
return JsonResponse({"success": True, "agenda": meeting.agenda})
except Exception as e:
return JsonResponse({"success": False, "error": str(e)})
@login_required
def get_student_details(request, student_id):
"""API view to get student details for the modal"""
# Check if user belongs to Parent group
if not request.user.groups.filter(name="Parent").exists():
return JsonResponse({"error": "Unauthorized access"}, status=403)
try:
# Get the student
student = get_object_or_404(Student, id=student_id)
# Temporarily bypass strict parent-student relationship check
# This allows any parent to view any student for demonstration purposes
# In production, you would want to re-enable the stricter check
# Get attendance data
try:
attended_classes = student.attendance_records.filter(
student_attend=True
).count()
# Use period duration as the total, not the actual record count
total_period_days = 180 # Standard days in a semester/period
# Calculate rate based on semester days
attendance_rate = round((attended_classes / total_period_days * 100), 1)
# Also include the raw attendance numbers for display
attendance_data = {
"rate": attendance_rate,
"present": attended_classes,
"total": total_period_days,
"formatted": f"{attendance_rate}% ({attended_classes}/{total_period_days})",
}
except Exception as e:
print(f"Error getting attendance data: {str(e)}")
attendance_data = {
"rate": 0,
"present": 0,
"total": 180,
"formatted": "0% (0/180)",
}
# Get subject data
subjects_data = []
if student.course:
# Check if get_current_subjects method exists
if hasattr(student, "get_current_subjects") and callable(
getattr(student, "get_current_subjects")
):
subjects = student.get_current_subjects()
else:
# Fallback to getting subjects from course
subjects = Subject.objects.filter(
course=student.course, period_or_year=student.current_period
)
for subject in subjects:
# Check if get_subject_progress method exists
if hasattr(student, "get_subject_progress") and callable(
getattr(student, "get_subject_progress")
):
progress = student.get_subject_progress(subject.id)
else:
# Default progress value
progress = 0
teacher_name = "Not assigned"
if hasattr(subject, "teacher") and subject.teacher:
teacher_name = subject.teacher.name
subject_data = {
"name": subject.name,
"progress": progress,
"teacher_name": teacher_name,
}
subjects_data.append(subject_data)
# Get course timeline information
current_period_course = None
period_end_date = None
course_end_date = None
if student.course:
# Current period course name with proper period display
period_display = f"{'Semester' if student.course.duration_type == 'Semester' else 'Year'} {student.current_period}"
current_period_course = f"{student.course.name} - {period_display}"
# Use current academic year for dates instead of calculations
import datetime
current_year = timezone.now().year
current_month = timezone.now().month
# Set fixed dates based on current academic year and course type
if student.course.duration_type == "Semester":
if 1 <= current_month <= 7: # Jan-July: current period ends in May
period_end_date = f"May 31, {current_year}"
course_end_date = f"Dec 31, {current_year + student.course.duration - (student.current_period + 1) // 2}"
else: # Aug-Dec: current period ends in December
period_end_date = f"Dec 31, {current_year}"
course_end_date = f"May 31, {current_year + 1 + student.course.duration - (student.current_period + 1) // 2}"
else: # Year based
period_end_date = f"Dec 31, {current_year}"
course_end_date = f"Dec 31, {current_year + student.course.duration - student.current_period}"
# Provide fallback values if data extraction fails
if not current_period_course and student.course:
period_display = f"{'Semester' if student.course.duration_type == 'Semester' else 'Year'} {student.current_period}"
current_period_course = f"{student.course.name} - {period_display}"
if not period_end_date:
period_end_date = "Not available"
if not course_end_date:
course_end_date = "Not available"
# Prepare student data
student_data = {
"id": student.id,
"name": student.name,
"status": student.status,
"email": student.email,
"phone": student.phone,
"address": student.permanent_address
or student.temporary_address
or "Not provided",
"image_url": student.image.url
if student.image and hasattr(student.image, "url")
else None,
"course_name": student.course.name if student.course else None,
"course_duration_type": student.course.duration_type
if student.course
else None,
"current_period": student.current_period,
"attendance_rate": attendance_data,
"subjects": subjects_data,
"current_period_course": current_period_course,
"expected_end_date": period_end_date,
"course_end_date": course_end_date,
}
return JsonResponse(student_data)
except Student.DoesNotExist:
return JsonResponse({"error": "Student not found"}, status=404)
except Exception as e:
print(f"Error in get_student_details: {str(e)}")
print(traceback.format_exc())
return JsonResponse({"error": str(e)}, status=500)
+229 -51
View File
@@ -1,96 +1,197 @@
# Standard library imports
import django.db
# Third-party app imports
from rest_framework import serializers
from .models import (
Attendance, AttendanceRecord, Batch, Course, FCMDevice, Institute,
Notice, Routine, Staff, Student, Subject, Staff_leave, Student_Leave,
StudentFeedback, InstituteFeedback, StaffInstituteFeedback, CourseTracking,
TOTPSecret, ResetToken
# Local app imports
from app.firebase import FCMDevice
from app.models import (
Attendance,
AttendanceRecord,
Batch,
Course,
CourseTracking,
Institute,
InstituteFeedback,
Notice,
Parent,
ParentFeedback,
ParentInstituteFeedback,
Routine,
Staff,
StaffInstituteFeedback,
StaffLeave,
Student,
StudentFeedback,
StudentLeave,
Subject,
SubjectFile,
TeacherParentMeeting,
)
class FCMDeviceSerializer(serializers.ModelSerializer):
class Meta:
model = FCMDevice
fields = ['id', 'token']
fields = ["id", "token"]
class InstituteSerializer(serializers.ModelSerializer):
class Meta:
model = Institute
fields = ['id', 'name', 'phone', 'email', 'address', 'pan_no', 'reg_no', 'logo', 'description']
fields = [
"id",
"name",
"phone",
"email",
"address",
"pan_no",
"reg_no",
"logo",
"description",
]
class SubjectSerializer(serializers.ModelSerializer):
syllabus_pdf_url = serializers.SerializerMethodField()
class Meta:
model = Subject
fields = ['id', 'name', 'course', 'semester_or_year', 'syllabus_pdf', 'syllabus_pdf_url']
fields = [
"id",
"name",
"course",
"period_or_year",
"syllabus_pdf",
"syllabus_pdf_url",
]
def get_syllabus_pdf_url(self, obj):
return obj.get_pdf_url()
class CourseSerializer(serializers.ModelSerializer):
subjects = SubjectSerializer(many=True, read_only=True)
class Meta:
model = Course
fields = ['id', 'name', 'duration', 'duration_type', 'subjects']
fields = [
"id",
"name",
"code",
"duration",
"duration_type",
"is_active",
"description",
"subjects",
]
class BatchSerializer(serializers.ModelSerializer):
class Meta:
model = Batch
fields = ['id', 'name', 'start_date', 'end_date']
fields = ["id", "name", "year"]
class StudentSerializer(serializers.ModelSerializer):
batches = BatchSerializer(many=True, read_only=True)
course = CourseSerializer(read_only=True)
class Meta:
model = Student
fields = [
'id', 'name', 'status', 'gender', 'birth_date', 'email', 'phone',
'temporary_address', 'permanent_address', 'marital_status', 'parent_name',
'parent_phone', 'citizenship_no', 'image', 'batches', 'course',
'current_semester', 'joining_date'
"id",
"name",
"status",
"gender",
"birth_date",
"email",
"phone",
"temporary_address",
"permanent_address",
"marital_status",
"parent_name",
"parent_phone",
"citizenship_no",
"image",
"batches",
"course",
"current_semester",
"joining_date",
]
read_only_fields = ['id']
read_only_fields = ["id"]
class StaffSerializer(serializers.ModelSerializer):
class Meta:
model = Staff
fields = [
'id', 'name', 'gender', 'designation', 'birth_date', 'phone', 'email',
'temporary_address', 'permanent_address', 'marital_status', 'parent_name',
'parent_phone', 'citizenship_no', 'passport', 'image', 'joining_date',
'is_active'
"id",
"name",
"gender",
"designation",
"birth_date",
"phone",
"email",
"temporary_address",
"permanent_address",
"marital_status",
"parent_name",
"parent_phone",
"citizenship_no",
"passport",
"image",
"joining_date",
"is_active",
]
read_only_fields = ['id']
read_only_fields = ["id"]
class RoutineSerializer(serializers.ModelSerializer):
teacher = StaffSerializer(read_only=True)
subject = SubjectSerializer(read_only=True)
course = CourseSerializer(read_only=True)
class Meta:
model = Routine
fields = [
'id', 'course', 'subject', 'teacher', 'start_time', 'end_time',
'semester_or_year', 'is_active'
"id",
"course",
"subject",
"teacher",
"start_time",
"end_time",
"period_or_year",
"is_active",
]
class AttendanceRecordSerializer(serializers.ModelSerializer):
student = StudentSerializer(read_only=True)
class Meta:
model = AttendanceRecord
fields = ['id', 'attendance', 'student', 'student_attend']
fields = ["id", "attendance", "student", "student_attend"]
class AttendanceSerializer(serializers.ModelSerializer):
records = AttendanceRecordSerializer(many=True, read_only=True)
routine = RoutineSerializer(read_only=True)
teacher = StaffSerializer(read_only=True)
class Meta:
model = Attendance
fields = ['id', 'date', 'routine', 'teacher', 'teacher_attend', 'class_status', 'records']
fields = [
"id",
"date",
"routine",
"teacher",
"teacher_attend",
"class_status",
"records",
]
class NoticeSerializer(serializers.ModelSerializer):
created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
@@ -98,23 +199,34 @@ class NoticeSerializer(serializers.ModelSerializer):
class Meta:
model = Notice
fields = ['id', 'title', 'message', 'image', 'image_url', 'created_at']
read_only_fields = ['created_at']
fields = ["id", "title", "message", "image", "image_url", "created_at"]
read_only_fields = ["created_at"]
def get_image_url(self, obj):
if obj.image:
return obj.image.url
return None
class StaffLeaveSerializer(serializers.ModelSerializer):
staff = StaffSerializer(read_only=True)
created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
class Meta:
model = Staff_leave
fields = ['id', 'staff', 'start_date', 'end_date', 'message', 'status', 'created_at', 'updated_at']
read_only_fields = ['created_at', 'updated_at']
model = StaffLeave
fields = [
"id",
"staff",
"start_date",
"end_date",
"message",
"status",
"created_at",
"updated_at",
]
read_only_fields = ["created_at", "updated_at"]
class StudentLeaveSerializer(serializers.ModelSerializer):
student = StudentSerializer(read_only=True)
@@ -122,9 +234,19 @@ class StudentLeaveSerializer(serializers.ModelSerializer):
updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
class Meta:
model = Student_Leave
fields = ['id', 'student', 'start_date', 'end_date', 'message', 'status', 'created_at', 'updated_at']
read_only_fields = ['created_at', 'updated_at']
model = StudentLeave
fields = [
"id",
"student",
"start_date",
"end_date",
"message",
"status",
"created_at",
"updated_at",
]
read_only_fields = ["created_at", "updated_at"]
class StudentFeedbackSerializer(serializers.ModelSerializer):
student = StudentSerializer(read_only=True)
@@ -134,8 +256,17 @@ class StudentFeedbackSerializer(serializers.ModelSerializer):
class Meta:
model = StudentFeedback
fields = ['id', 'student', 'teacher', 'rating', 'feedback_text', 'created_at', 'updated_at']
read_only_fields = ['created_at', 'updated_at']
fields = [
"id",
"student",
"teacher",
"rating",
"feedback_text",
"created_at",
"updated_at",
]
read_only_fields = ["created_at", "updated_at"]
class InstituteFeedbackSerializer(serializers.ModelSerializer):
user = StudentSerializer(read_only=True)
@@ -145,10 +276,19 @@ class InstituteFeedbackSerializer(serializers.ModelSerializer):
class Meta:
model = InstituteFeedback
fields = [
'id', 'institute', 'user', 'feedback_type', 'rating', 'feedback_text',
'is_anonymous', 'is_public', 'created_at', 'updated_at'
"id",
"institute",
"user",
"feedback_type",
"rating",
"feedback_text",
"is_anonymous",
"is_public",
"created_at",
"updated_at",
]
read_only_fields = ['created_at', 'updated_at']
read_only_fields = ["created_at", "updated_at"]
class StaffInstituteFeedbackSerializer(serializers.ModelSerializer):
staff = StaffSerializer(read_only=True)
@@ -158,33 +298,71 @@ class StaffInstituteFeedbackSerializer(serializers.ModelSerializer):
class Meta:
model = StaffInstituteFeedback
fields = [
'id', 'institute', 'staff', 'feedback_type', 'rating', 'feedback_text',
'is_anonymous', 'is_public', 'created_at', 'updated_at'
"id",
"institute",
"staff",
"feedback_type",
"rating",
"feedback_text",
"is_anonymous",
"is_public",
"created_at",
"updated_at",
]
read_only_fields = ['created_at', 'updated_at']
read_only_fields = ["created_at", "updated_at"]
class CourseTrackingSerializer(serializers.ModelSerializer):
student = StudentSerializer(read_only=True)
course = CourseSerializer(read_only=True)
created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
current_period_display = serializers.CharField(read_only=True)
class Meta:
model = CourseTracking
fields = [
'id', 'student', 'course', 'enrollment_date', 'start_date', 'expected_end_date',
'actual_end_date', 'progress_status', 'completion_percentage', 'current_semester',
'semester_start_date', 'semester_end_date', 'notes', 'created_at', 'updated_at'
"id",
"student",
"course",
"enrollment_date",
"start_date",
"expected_end_date",
"actual_end_date",
"progress_status",
"completion_percentage",
"current_period",
"period_start_date",
"period_end_date",
"notes",
"created_at",
"updated_at",
"current_period_display",
]
read_only_fields = ['created_at', 'updated_at', 'enrollment_date']
read_only_fields = [
"created_at",
"updated_at",
"enrollment_date",
"current_period_display",
]
class TOTPSecretSerializer(serializers.ModelSerializer):
class Meta:
model = TOTPSecret
fields = ['id', 'identifier', 'created_at', 'expires_at']
fields = ["id", "identifier", "created_at", "expires_at"]
class ResetTokenSerializer(serializers.ModelSerializer):
class Meta:
model = ResetToken
fields = ['id', 'token', 'identifier', 'created_at', 'expires_at']
fields = ["id", "token", "identifier", "created_at", "expires_at"]
class ParentSerializer(serializers.ModelSerializer):
students = StudentSerializer(many=True, read_only=True)
class Meta:
model = Parent
fields = ["id", "name", "phone", "email", "address", "students"]
read_only_fields = ["id"]
+338 -27
View File
@@ -1,9 +1,33 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.core.cache import cache
# Standard library imports
import random
from datetime import date, timedelta
from .models import CourseTracking, Student
# Core Django imports
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import Group, Permission
from django.core.cache import cache
from django.core.signals import request_started
from django.db import transaction
from django.db.models import Q
from django.db.models.signals import m2m_changed, post_migrate, post_save
from django.dispatch import receiver
# Local app imports
from app.firebase import FCMDevice, send_push_notification
from app.models import (
Batch,
CourseTracking,
Notice,
Parent,
Staff,
Student,
TeacherParentMeeting,
)
# --------------------------------------------------------------------
# Course Tracking Signals
# --------------------------------------------------------------------
@receiver(post_save, sender=CourseTracking)
def update_completion_percentage(sender, instance, **kwargs):
@@ -15,16 +39,53 @@ def update_completion_percentage(sender, instance, **kwargs):
cache.set(cache_key, instance.completion_percentage, 3600) # Cache for 1 hour
@receiver(request_started)
def periodic_update_course_completion(sender, **kwargs):
"""
Signal that updates course completion percentages randomly on request start.
This ensures periodic updates without overwhelming the system.
Only runs with 1% probability per request to avoid performance impact.
"""
# Run this signal with 1% probability to avoid running on every request
if random.random() <= 0.01: # 1% chance to run on any request
# Get all active course trackings (not completed or dropped)
active_trackings = CourseTracking.objects.filter(
Q(progress_status="In Progress") | Q(progress_status="Not Started")
)
updated_count = 0
for tracking in active_trackings:
old_percentage = tracking.completion_percentage
new_percentage = tracking.update_completion_percentage()
# If percentage changed, it was updated
if old_percentage != new_percentage:
updated_count += 1
# Check if course is now complete (100%)
if new_percentage >= 100 and tracking.progress_status != "Completed":
tracking.progress_status = "Completed"
tracking.actual_end_date = date.today()
tracking.save()
# Cache the last update timestamp
if updated_count > 0:
cache.set(
"last_course_percentage_update", date.today().isoformat(), 86400
) # 24 hours
# --------------------------------------------------------------------
# Student and Course Signals
# --------------------------------------------------------------------
@receiver(post_save, sender=Student)
def create_course_tracking(sender, instance, created, **kwargs):
"""
Signal handler to create CourseTracking when a new student is created
or when a course is assigned to an existing student
"""
print(f"Signal triggered for student: {instance.name}")
print(f"Created: {created}")
print(f"Course: {instance.course}")
print(f"Current Semester: {instance.current_semester}")
# Check if student has a course assigned
if instance.course:
@@ -62,22 +123,15 @@ def create_course_tracking(sender, instance, created, **kwargs):
course=instance.course,
start_date=start_date,
expected_end_date=expected_end_date,
current_semester=instance.current_semester,
semester_start_date=start_date,
semester_end_date=start_date
current_period=instance.current_period,
period_start_date=start_date,
period_end_date=start_date
+ timedelta(days=180), # 6 months for first semester
)
# Force initial percentage calculation
course_tracking.force_update_percentage()
print(
f"Successfully created course tracking for student {instance.name}"
)
print(f"Start Date: {start_date}")
print(f"Expected End Date: {expected_end_date}")
print(f"Current Semester: {instance.current_semester}")
except Exception as e:
print(
f"Error creating course tracking for student {instance.name}: {str(e)}"
@@ -89,23 +143,280 @@ def create_course_tracking(sender, instance, created, **kwargs):
@receiver(post_save, sender=Student)
def sync_student_semester(sender, instance, created, **kwargs):
"""Signal handler to sync student's current_semester with course tracking"""
def sync_student_period(sender, instance, created, **kwargs):
"""Signal handler to sync student's current_period with course tracking"""
if instance.course:
try:
course_tracking = CourseTracking.objects.get(
student=instance, course=instance.course
)
if course_tracking.current_semester != instance.current_semester:
course_tracking.current_semester = instance.current_semester
if course_tracking.current_period != instance.current_period:
course_tracking.current_period = instance.current_period
course_tracking.save()
except CourseTracking.DoesNotExist:
pass
@receiver(post_save, sender=CourseTracking)
def sync_course_tracking_semester(sender, instance, created, **kwargs):
"""Signal handler to sync course tracking's current_semester with student"""
if instance.student.current_semester != instance.current_semester:
instance.student.current_semester = instance.current_semester
instance.student.save()
def sync_course_tracking_period(sender, instance, created, **kwargs):
"""Signal handler to sync course tracking's current_period with student"""
if instance.student.current_period != instance.current_period:
instance.student.current_period = instance.current_period
instance.student.save()
# --------------------------------------------------------------------
# Parent and Student Relationship Signals
# --------------------------------------------------------------------
@receiver(post_save, sender=Student)
def create_parent_for_student(sender, instance, created, **kwargs):
"""
Signal to create or update parent when a student is created or updated.
Also ensures parent is assigned to the Parent group.
"""
if instance.parent_name and instance.parent_phone:
try:
# Try to find existing parent
parent = Parent.objects.filter(phone=instance.parent_phone).first()
if not parent:
# Create new parent if doesn't exist
parent = Parent.objects.create(
name=instance.parent_name,
phone=instance.parent_phone,
password=make_password("123"), # Default password
)
# Get or create Parent group
parent_group, _ = Group.objects.get_or_create(name="Parent")
# Add parent to group
parent.groups.add(parent_group)
# Add necessary permissions
permissions = Permission.objects.filter(
codename__in=["view_student", "view_attendance", "view_notice"]
)
parent_group.permissions.add(*permissions)
# Add student to parent's students
parent.students.add(instance)
except Exception as e:
print(f"Error creating/updating parent: {str(e)}")
# Don't prevent student creation/update if parent creation fails
@receiver(post_save, sender=Parent)
def setup_parent_user(sender, instance, created, **kwargs):
"""
Signal handler to set up a new parent user:
1. Add to Parent group
2. Set up initial settings and permissions
"""
if created:
try:
# Add user to Parent group
parent_group, _ = Group.objects.get_or_create(name="Parent")
instance.groups.add(parent_group)
print(f"Successfully set up new parent user: {instance.name}")
except Exception as e:
print(f"Error setting up parent user {instance.name}: {str(e)}")
@receiver(m2m_changed, sender=Parent.students.through)
def handle_parent_student_relationship(
sender, instance, action, reverse, model, pk_set, **kwargs
):
"""
Signal handler to manage parent-student relationship changes:
- Update caches
- Handle notifications
- Sync permissions
"""
if action == "post_add":
try:
if reverse:
# This is triggered when students are added to a parent
parent = model.objects.get(pk=pk_set.pop())
student = instance
else:
# This is triggered when a parent adds students
parent = instance
student_ids = pk_set
# Clear parent dashboard cache using a more compatible approach
cache.delete(f"parent_dashboard_{parent.id}")
print(
f"Successfully updated parent-student relationship for parent: {parent.name}"
)
except Exception as e:
print(f"Error handling parent-student relationship: {str(e)}")
# --------------------------------------------------------------------
# User Group and Permission Signals
# --------------------------------------------------------------------
@receiver(post_migrate)
def create_user_groups(sender, **kwargs):
"""
Create initial user groups and permissions after migrations.
This ensures groups exist before any user is created.
"""
from django.contrib.auth.models import Group, Permission
# Create the necessary groups
groups_data = {
"Student": ["view_attendance", "view_notice", "view_subject"],
"Teacher": [
"view_student",
"view_attendance",
"add_attendance",
"change_attendance",
"view_notice",
"add_notice",
"view_subject",
"change_subject",
],
"Parent": ["view_student", "view_attendance", "view_notice"],
"Admission Officer": [
"view_student",
"add_student",
"change_student",
"view_parent",
"add_parent",
"change_parent",
"view_course",
"view_batch",
"view_notice",
],
}
for group_name, permissions in groups_data.items():
group, created = Group.objects.get_or_create(name=group_name)
if created:
print(f"Created user group: {group_name}")
# Add permissions to group
perms = Permission.objects.filter(codename__in=permissions)
group.permissions.add(*perms)
print(f"Added {perms.count()} permissions to {group_name} group")
# --------------------------------------------------------------------
# Batch and Student Enrollment Signals
# --------------------------------------------------------------------
@receiver(post_save, sender=Batch)
def update_student_joining_dates(sender, instance, **kwargs):
"""Update joining dates for all students in a batch when batch year changes"""
if instance.year:
# Update all students in this batch who don't have a joining date
for student in instance.students.filter(joining_date__isnull=True):
student.joining_date = instance.year
student.save(update_fields=["joining_date"])
@receiver(m2m_changed, sender=Student.batches.through)
def handle_student_batch_change(
sender, instance, action, reverse, model, pk_set, **kwargs
):
"""Handle joining date when a student is added to a batch"""
if action == "post_add" and not reverse:
# This is fired when batches are added to a student
for batch_id in pk_set:
try:
batch = Batch.objects.get(pk=batch_id)
if batch.year and not instance.joining_date:
instance.joining_date = batch.year
instance.save(update_fields=["joining_date"])
break # Use the first batch with a joining date
except Batch.DoesNotExist:
pass
# --------------------------------------------------------------------
# Notification Signals
# --------------------------------------------------------------------
@receiver(post_save, sender=Notice)
def send_notice_notification(sender, instance, created, **kwargs):
"""Send notifications when a notice is created or updated"""
# Get all tokens from active devices (all user types)
tokens = FCMDevice.objects.filter(is_active=True).values_list("token", flat=True)
send_push_notification(instance.title, instance.message, tokens)
@receiver(post_save, sender=TeacherParentMeeting)
def send_meeting_notification(sender, instance, created, **kwargs):
"""Send notifications when a meeting is created or updated"""
tokens = []
# Method 1: Get tokens from FCMDevice records based on user_type
fcm_device_tokens = FCMDevice.objects.filter(
is_active=True, user_type__in=["parent", "teacher"]
).values_list("token", flat=True)
tokens.extend(fcm_device_tokens)
# Method 2: Get tokens directly from user models
# Get tokens from Staff model
staff_tokens = (
Staff.objects.filter(fcm_token__isnull=False)
.exclude(fcm_token="")
.values_list("fcm_token", flat=True)
)
tokens.extend(staff_tokens)
# Get tokens from Parent model
parent_tokens = (
Parent.objects.filter(fcm_token__isnull=False)
.exclude(fcm_token="")
.values_list("fcm_token", flat=True)
)
tokens.extend(parent_tokens)
# Remove duplicates
unique_tokens = list(set(tokens))
if not unique_tokens:
return
if created:
title = "New Parent-Teacher Meeting Scheduled"
message = f"A parent-teacher meeting has been scheduled for {instance.meeting_date} at {instance.meeting_time}."
if instance.is_online:
message += " This is an online meeting."
send_push_notification(title, message, unique_tokens)
else:
# Access through instance.tracker to determine if status changed
# This requires django-model-utils or similar to track changes
if hasattr(instance, "tracker") and "status" in instance.tracker.changed():
old_status = instance.tracker.previous("status")
if instance.status == "cancelled":
title = "Parent-Teacher Meeting Cancelled"
message = f"The meeting for {instance.meeting_date} at {instance.meeting_time} has been cancelled."
if instance.cancellation_reason:
message += f" Reason: {instance.cancellation_reason}"
send_push_notification(title, message, unique_tokens)
elif instance.status == "rescheduled":
# Meeting rescheduled notification
title = "Parent-Teacher Meeting Rescheduled"
message = f"The parent-teacher meeting has been rescheduled to {instance.meeting_date} at {instance.meeting_time}."
send_push_notification(title, message, unique_tokens)
elif instance.status == "completed":
# Meeting completed notification
title = "Parent-Teacher Meeting Completed"
message = f"The parent-teacher meeting scheduled for {instance.meeting_date} at {instance.meeting_time} has been marked as completed."
send_push_notification(title, message, unique_tokens)
+525 -352
View File
File diff suppressed because it is too large. Load diff
+118 -102
View File
@@ -1,25 +1,37 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.http import JsonResponse, HttpResponse
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.db.models import Count, Q, Avg
from django.utils import timezone
# Standard library imports
from datetime import timedelta
from django.views.decorators.http import require_http_methods, require_GET
from .models import (
Student,
Course,
CourseTracking,
Student_Leave,
Staff,
# Core Django imports
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.db.models import Avg, Count, Q
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.views.decorators.http import require_http_methods
# Local app imports
from app.models import (
Attendance,
AttendanceRecord,
StudentFeedback,
Routine,
Notice,
Institute,
Batch,
Course,
CourseTracking,
InstituteFeedback,
Notice,
Parent,
ParentFeedback,
ParentInstituteFeedback,
Routine,
Staff,
StaffInstituteFeedback,
StaffLeave,
Student,
StudentFeedback,
StudentLeave,
Subject,
SubjectFile,
TeacherParentMeeting,
FEEDBACK_TYPE_CHOICES,
)
@@ -33,15 +45,16 @@ def studentDashboard(request):
last_month = today - timedelta(days=30)
# Clear section states on page load
if request.method == 'GET':
request.session.pop('active_section', None)
request.session.pop('active_subsection', None)
if request.method == "GET":
request.session.pop("active_section", None)
request.session.pop("active_subsection", None)
# Get all teachers for feedback form
teachers = Staff.objects.filter(
Q(designation__icontains="Teacher")
| Q(designation__icontains="Staff")
| Q(designation__icontains="Principal")
| Q(designation__icontains="Admission Officer")
| Q(designation__isnull=True)
| Q(designation="")
).order_by("name")
@@ -77,87 +90,81 @@ def studentDashboard(request):
# Course Details with enhanced information
course_details = []
for tracking in course_trackings:
course_details.append({
"name": tracking.course.name,
"start_date": tracking.start_date,
"expected_end_date": tracking.expected_end_date,
"actual_end_date": tracking.actual_end_date,
"completion_percentage": tracking.completion_percentage,
"progress_status": tracking.progress_status,
"current_semester": tracking.current_semester,
"semester_start_date": tracking.semester_start_date,
"semester_end_date": tracking.semester_end_date,
"remaining_days": tracking.remaining_days,
"is_completed": tracking.is_completed,
"is_active": tracking.is_active,
"duration_type": tracking.course.duration_type,
"duration": tracking.course.duration,
"total_duration_days": tracking.total_duration_days,
"notes": tracking.notes,
})
course_details.append(
{
"name": tracking.course.name,
"start_date": tracking.start_date,
"expected_end_date": tracking.expected_end_date,
"actual_end_date": tracking.actual_end_date,
"completion_percentage": tracking.completion_percentage,
"progress_status": tracking.progress_status,
"current_period": tracking.current_period,
"period_start_date": tracking.period_start_date,
"period_end_date": tracking.period_end_date,
"remaining_days": tracking.remaining_days,
"total_remaining_days": tracking.total_remaining_days,
"is_completed": tracking.is_completed,
"is_active": tracking.is_active,
"duration_type": tracking.course.duration_type,
"duration": tracking.course.duration,
"total_duration_days": tracking.total_duration_days,
"notes": tracking.notes,
}
)
# Get current subjects based on current semester
# Get current subjects based on current period
current_subjects = []
active_tracking = course_trackings.filter(progress_status="In Progress").first()
if active_tracking:
current_subjects = Subject.objects.filter(
course=active_tracking.course,
semester_or_year=active_tracking.current_semester
course=active_tracking.course, period_or_year=active_tracking.current_period
)
# Attendance Statistics with enhanced information
attendance_records = AttendanceRecord.objects.filter(student=student)
# Get the current semester start and end dates from student's course tracking
current_semester_start = None
current_semester_end = None
# Get the current period start and end dates from student's course tracking
current_period_start = None
current_period_end = None
# Try to get course tracking for current student
try:
if active_tracking and active_tracking.semester_start_date:
current_semester_start = active_tracking.semester_start_date
current_semester_end = active_tracking.semester_end_date or today
if active_tracking and active_tracking.period_start_date:
current_period_start = active_tracking.period_start_date
current_period_end = active_tracking.period_end_date or today
except Exception:
pass
# Filter attendance by current semester date range if available
if current_semester_start:
semester_attendance = attendance_records.filter(
attendance__date__gte=current_semester_start,
attendance__date__lte=current_semester_end
# Calculate total attendance
total_attendance = attendance_records.filter(student_attend=True).count()
total_classes = attendance_records.count()
# Calculate attendance percentage
if total_classes > 0:
attendance_percentage = (total_attendance / total_classes) * 100
else:
attendance_percentage = 0
# Filter attendance by current period date range if available
if current_period_start:
period_attendance = attendance_records.filter(
attendance__date__gte=current_period_start,
attendance__date__lte=current_period_end,
)
# Count the actual classes and attendance
actual_classes = semester_attendance.count()
classes_attended = semester_attendance.filter(student_attend=True).count()
# Check if the course is semester-based or year-based
is_semester_based = active_tracking.course.duration_type == "Semester" if active_tracking else True
# Set the denominator based on semester/year
if is_semester_based:
# For a 6-month semester, use 180 days as denominator
total_classes = 180
else:
# For a full year, use 365 days as denominator
total_classes = 365 # Or use 317 (365 - 48) if excluding Saturdays
# Keep track of actual records for display purposes
actual_classes = period_attendance.count()
classes_attended = period_attendance.filter(student_attend=True).count()
# Calculate actual attendance percentage
if actual_classes > 0:
attendance_summary_actual = (classes_attended / actual_classes * 100)
attendance_summary_actual = classes_attended / actual_classes * 100
else:
attendance_summary_actual = 0
else:
# Fall back to overall attendance if semester dates aren't available
actual_classes = attendance_records.count()
classes_attended = attendance_records.filter(student_attend=True).count()
total_classes = 180 # Default to semester-based
attendance_summary_actual = 0
# Calculate attendance percentage using the fixed formula
attendance_percentage = (
(classes_attended / total_classes * 100) if total_classes > 0 else 0
)
actual_classes = total_classes
classes_attended = total_attendance
attendance_summary_actual = attendance_percentage
# Calculate attendance streaks
current_streak = 0
@@ -188,15 +195,17 @@ def studentDashboard(request):
attendance_summary = {
"total_classes": total_classes,
"classes_attended": classes_attended,
"present_count": classes_attended,
"absent_count": total_classes - classes_attended,
"present_count": total_attendance,
"absent_count": total_classes - total_attendance,
"attendance_percentage": attendance_percentage,
"current_streak": current_streak,
"longest_streak": longest_streak
"longest_streak": longest_streak,
"actual_classes": actual_classes,
"actual_percentage": attendance_summary_actual,
}
# Add actual recorded classes info if different from expected
if current_semester_start and actual_classes != total_classes:
if current_period_start and actual_classes != total_classes:
attendance_summary["actual_classes"] = actual_classes
attendance_summary["actual_percentage"] = attendance_summary_actual
@@ -218,24 +227,24 @@ def studentDashboard(request):
notices = Notice.objects.all().order_by("-created_at")
# Get student leaves with status information
student_leaves = Student_Leave.objects.filter(student=student).order_by(
student_leaves = StudentLeave.objects.filter(student=student).order_by(
"-created_at"
)
pending_leaves = student_leaves.filter(status=0).count()
approved_leaves = student_leaves.filter(status=1).count()
rejected_leaves = student_leaves.filter(status=2).count()
# Get current semester routines
current_semester_routines = []
# Get current period routines
current_period_routines = Routine.objects.none() # Initialize with empty QuerySet
if active_tracking:
current_semester_routines = Routine.objects.filter(
current_period_routines = Routine.objects.filter(
course=active_tracking.course,
semester_or_year=active_tracking.current_semester,
period_or_year=active_tracking.current_period,
is_active=True,
)
# Check if student has any routines
has_routines = current_semester_routines.exists()
has_routines = current_period_routines.exists()
context = {
"student": student,
@@ -252,6 +261,7 @@ def studentDashboard(request):
"course_summary": course_summary,
# Current Subjects
"current_subjects": current_subjects,
"active_tracking": active_tracking,
# Attendance
"attendance_summary": attendance_summary,
"attendance_records": attendance_records,
@@ -267,13 +277,13 @@ def studentDashboard(request):
"approved_leaves": approved_leaves,
"rejected_leaves": rejected_leaves,
# Routines
"current_semester_routines": current_semester_routines,
"current_period_routines": current_period_routines,
"has_routines": has_routines,
# Current date for display
"today": today,
}
return render(request, "student_dashboard.html", context)
return render(request, "student/dashboard.html", context)
@login_required
@@ -301,10 +311,10 @@ def request_leave(request):
return redirect("studentDashboard")
# Check if leave already exists for these dates
existing_leave = Student_Leave.objects.filter(
existing_leave = StudentLeave.objects.filter(
student=student,
start_date__lte=end_date.date(),
end_date__gte=start_date.date()
end_date__gte=start_date.date(),
).first()
if existing_leave:
@@ -314,7 +324,7 @@ def request_leave(request):
return redirect("studentDashboard")
# Create leave request
leave = Student_Leave.objects.create(
leave = StudentLeave.objects.create(
student=student,
start_date=start_date.date(),
end_date=end_date.date(),
@@ -365,7 +375,9 @@ def submit_feedback(request):
existing_feedback.rating = rating
existing_feedback.feedback_text = feedback_text
existing_feedback.save()
messages.success(request, "Teacher feedback updated successfully")
messages.success(
request, "Teacher feedback updated successfully"
)
else:
# Create new feedback
StudentFeedback.objects.create(
@@ -374,7 +386,9 @@ def submit_feedback(request):
rating=rating,
feedback_text=feedback_text,
)
messages.success(request, "Teacher feedback submitted successfully")
messages.success(
request, "Teacher feedback submitted successfully"
)
except Staff.DoesNotExist:
messages.error(request, "Selected teacher does not exist")
@@ -401,7 +415,9 @@ def submit_institute_feedback(request):
try:
# Validate feedback type
if not feedback_type or feedback_type not in dict(InstituteFeedback.FEEDBACK_TYPE_CHOICES):
if not feedback_type or feedback_type not in dict(
InstituteFeedback.FEEDBACK_TYPE_CHOICES
):
messages.error(request, "Please select a valid feedback type")
return redirect("studentDashboard")
@@ -469,7 +485,7 @@ def get_subject_files(request, subject_id):
try:
subject = Subject.objects.get(id=subject_id)
files = subject.get_all_files()
if not files:
return JsonResponse(
{"success": False, "message": "No files available for this subject"}
+32 -4
View File
@@ -1,3 +1,4 @@
# Core Django imports
from django import template
register = template.Library()
@@ -17,9 +18,9 @@ def get_range(value):
def multiply(value, arg):
"""Multiply the value by the argument"""
try:
return int(value) * int(arg)
except (ValueError, TypeError):
return value
return float(value) * float(arg)
except ValueError:
return 0
@register.filter
def get_item(dictionary, key):
@@ -35,4 +36,31 @@ def get_item(dictionary, key):
return dictionary.get(key)
# Try with the key as string
return dictionary.get(str(key), None)
return dictionary.get(str(key), None)
@register.filter
def filter_attended(attendance_records):
"""Filter attendance records to get only attended ones"""
return attendance_records.filter(student_attend=True)
@register.filter
def div(value, arg):
"""Divide the value by the argument"""
try:
return float(value) / float(arg)
except (ValueError, ZeroDivisionError):
return 0
@register.filter
def filter_by_subject(routines, subject):
"""
Filter routines to get the one for a specific subject.
Usage in template: {{ routines|filter_by_subject:subject }}
"""
if not routines:
return None
for routine in routines:
if routine.subject_id == subject.id:
return routine
return None
+1
View File
@@ -1,3 +1,4 @@
# Core Django imports
from django.test import TestCase
# Create your tests here.
+211 -67
View File
@@ -1,89 +1,233 @@
from django.urls import path
from app import views
from django.contrib import admin
from app import auth
from app import staffviews
from app import firebase
from app.staffviews import (
teacherDashboard,
request_staff_leave,
submit_staff_institute_feedback,
save_attendance,
get_subject_schedule,
teacher_update_profile_picture,
teacher_change_password,
get_attendance_form,
get_students,
manage_subject_files,
delete_subject_file,
get_teacher_subjects,
view_subject_syllabus
)
from app import studentviews
from app import parentviews
from app import admissionviews
from app import hodviews
# URL patterns for the main application
urlpatterns = [
# Dashboard URLs
# Dashboard URLs ---------------------------------------------------
path("dashboard/", views.dashboard, name="dashboard"),
path("student-dashboard/", views.studentDashboard, name="studentDashboard"),
path("teacher-dashboard/", views.teacherDashboard, name="teacherDashboard"),
# Leave Management URLs
path("student-dashboard/", studentviews.studentDashboard, name="studentDashboard"),
path("teacher-dashboard/", staffviews.teacherDashboard, name="teacherDashboard"),
path("parent-dashboard/", parentviews.parent_dashboard, name="parentDashboard"),
path(
"admission-officer-dashboard/",
admissionviews.admission_officer_dashboard,
name="admissionOfficerDashboard",
),
path("hod-dashboard/", hodviews.hod_dashboard, name="hodDashboard"),
# Notice Management ------------------------------------------------
path("add-notice/", views.add_notice, name="add_notice"),
path("delete-notice/<int:notice_id>/", views.delete_notice, name="delete_notice"),
# Leave Management -------------------------------------------------
# Student Leave
path("request-leave/", views.request_leave, name="request_leave"),
path(
"approve-student-leave/<int:leave_id>/",
views.approve_student_leave,
name="approve_student_leave",
),
path(
"reject-student-leave/<int:leave_id>/",
views.reject_student_leave,
name="reject_student_leave",
),
path("get-student-leaves/", views.get_student_leaves, name="get_student_leaves"),
# Staff Leave
path("request-staff-leave/", views.request_staff_leave, name="request_staff_leave"),
path("approve-student-leave/<int:leave_id>/", views.approve_student_leave, name="approve_student_leave"),
path("reject-student-leave/<int:leave_id>/", views.reject_student_leave, name="reject_student_leave"),
path("approve-staff-leave/<int:leave_id>/", views.approve_staff_leave, name="approve_staff_leave"),
path("reject-staff-leave/<int:leave_id>/", views.reject_staff_leave, name="reject_staff_leave"),
# Feedback URLs
path(
"approve-staff-leave/<int:leave_id>/",
views.approve_staff_leave,
name="approve_staff_leave",
),
path(
"reject-staff-leave/<int:leave_id>/",
views.reject_staff_leave,
name="reject_staff_leave",
),
path("get-staff-leaves/", views.get_staff_leaves, name="get_staff_leaves"),
# Feedback --------------------------------------------------------
# Student Feedback
path("submit-feedback/", views.submit_feedback, name="submit_feedback"),
path("submit-institute-feedback/", views.submit_institute_feedback, name="submit_institute_feedback"),
path("submit-staff-institute-feedback/", views.submit_staff_institute_feedback, name="submit_staff_institute_feedback"),
# Profile Management URLs
path(
"submit-institute-feedback/",
views.submit_institute_feedback,
name="submit_institute_feedback",
),
# Staff Feedback
path(
"submit-staff-institute-feedback/",
views.submit_staff_institute_feedback,
name="submit_staff_institute_feedback",
),
# Parent Feedback
path(
"parent/feedback/submit/",
parentviews.submit_parent_feedback,
name="submit_parent_feedback",
),
path(
"parent/feedback/institute/submit/",
parentviews.submit_parent_institute_feedback,
name="submit_parent_institute_feedback",
),
# Profile Management ----------------------------------------------
path("update-profile/", views.update_profile, name="update_profile"),
path("change-password/", views.change_password, name="change_password"),
path("teacher-update-profile-picture/", views.teacher_update_profile_picture, name="teacher_update_profile_picture"),
path("teacher-change-password/", views.teacher_change_password, name="teacher_change_password"),
# Firebase Cloud Messaging URLs
path("saveFCMToken/", views.save_fcm_token, name="saveFCMToken"),
# Subject and Course Management URLs
path("subject/<int:subject_id>/files/", views.get_subject_files, name="get_subject_files"),
path("subject/<int:subject_id>/syllabus/", staffviews.view_subject_syllabus, name="view_subject_syllabus"),
path(
"teacher-update-profile-picture/",
views.teacher_update_profile_picture,
name="teacher_update_profile_picture",
),
path(
"teacher-change-password/",
views.teacher_change_password,
name="teacher_change_password",
),
# Subject and Course Management -----------------------------------
path(
"subject/<int:subject_id>/files/",
views.get_subject_files,
name="get_subject_files",
),
path(
"subject/<int:subject_id>/syllabus/",
staffviews.view_subject_syllabus,
name="view_subject_syllabus",
),
path("get-subjects/", views.get_subjects, name="get_subjects"),
path("get-teachers/", views.get_teachers, name="get_teachers"),
path("get-course-duration/", views.get_course_duration, name="get_course_duration"),
path("get-subject-schedule/", views.get_subject_schedule, name="get_subject_schedule"),
# New Subject File Management URLs
path("manage-subject-files/", staffviews.manage_subject_files, name="manage_subject_files"),
path("delete-subject-file/", staffviews.delete_subject_file, name="delete_subject_file"),
path("get-teacher-subjects/", staffviews.get_teacher_subjects, name="get_teacher_subjects"),
# Attendance Management URLs
path(
"get-subject-schedule/", views.get_subject_schedule, name="get_subject_schedule"
),
# Subject File Management
path(
"delete-subject-file/",
staffviews.delete_subject_file,
name="delete_subject_file",
),
path(
"get-teacher-subjects/",
staffviews.get_teacher_subjects,
name="get_teacher_subjects",
),
# Attendance Management -------------------------------------------
path("save-attendance/", views.save_attendance, name="save_attendance"),
path("get-attendance-form/", staffviews.get_attendance_form, name="get_attendance_form"),
# Notice Management URLs
path("add-notice/", views.add_notice, name="add_notice"),
path("delete-notice/<int:notice_id>/", views.delete_notice, name="delete_notice"),
# Password Reset URLs
path(
"get-attendance-form/",
staffviews.get_attendance_form,
name="get_attendance_form",
),
# Password Management ---------------------------------------------
# Password Reset Options
path("password-reset/", auth.reset_password_options, name="reset_password_options"),
path("password-reset/phone/", auth.password_reset_phone, name="phone_reset_password"),
path("password-reset/phone/verify/", auth.password_reset_phone_verify, name="verify_reset_otp"),
path("password-reset/phone/resend/", auth.resend_phone_otp, name="resend_phone_otp"),
path("password-reset/email/", auth.password_reset_email, name="email_reset_password"),
path("password-reset/email/verify/", auth.password_reset_email_verify, name="password_reset_done"),
path("password-reset/email/resend/", auth.resend_email_code, name="resend_reset_code"),
# Phone-based Reset
path(
"password-reset/phone/", auth.password_reset_phone, name="phone_reset_password"
),
path(
"password-reset/phone/verify/",
auth.password_reset_phone_verify,
name="verify_reset_otp",
),
path(
"password-reset/phone/resend/", auth.resend_phone_otp, name="resend_phone_otp"
),
# Email-based Reset
path(
"password-reset/email/", auth.password_reset_email, name="email_reset_password"
),
path(
"password-reset/email/verify/",
auth.password_reset_email_verify,
name="password_reset_done",
),
path(
"password-reset/email/resend/", auth.resend_email_code, name="resend_reset_code"
),
# New Password Setup
path("password-reset/set/", auth.set_new_password, name="set_new_password"),
# Password security check
# Password Security
path("check-weak-password/", views.check_weak_password, name="check_weak_password"),
# Staff Management URLs
# Parent-Teacher Meeting ------------------------------------------
path("parent/meetings/", parentviews.parent_meetings, name="parent_meetings"),
path("api/meetings/add/", parentviews.add_meeting, name="add_meeting"),
path(
"api/meetings/<int:meeting_id>/complete/",
parentviews.complete_meeting,
name="complete_meeting",
),
path(
"api/meetings/<int:meeting_id>/cancel/",
parentviews.cancel_meeting,
name="cancel_meeting",
),
path(
"api/meetings/<int:meeting_id>/reschedule/",
parentviews.reschedule_meeting,
name="reschedule_meeting",
),
path(
"api/meetings/<int:meeting_id>/notes/",
parentviews.get_meeting_notes,
name="get_meeting_notes",
),
path(
"api/meetings/<int:meeting_id>/agenda/",
parentviews.get_meeting_agenda,
name="get_meeting_agenda",
),
path(
"api/meetings/<int:meeting_id>/cancellation-reason/",
views.get_meeting_cancellation_reason,
name="get_meeting_cancellation_reason",
),
# API Endpoints --------------------------------------------------
# Student API
path(
"api/students/<int:student_id>/details/",
parentviews.get_student_details,
name="get_student_details",
),
path("get-students/", staffviews.get_students, name="get_students"),
# Firebase Cloud Messaging ---------------------------------------
path("saveFCMToken/", views.save_fcm_token, name="saveFCMToken"),
# Admission Officer routes ----------------------------------------
path("add-student/", admissionviews.add_student, name="add_student"),
path("add-batch/", admissionviews.add_batch, name="add_batch"),
path("add-course/", admissionviews.add_course, name="add_course"),
path(
"add-course-tracking/",
admissionviews.add_course_tracking,
name="add_course_tracking",
),
path("get-courses/", admissionviews.get_courses, name="get_courses"),
path("get-batches/", admissionviews.get_batches, name="get_batches"),
path(
"admission-get-students/",
admissionviews.get_students,
name="admission_get_students",
),
path(
"get-student/<int:student_id>/", admissionviews.get_student, name="get_student"
),
path("edit-student/", admissionviews.edit_student, name="edit_student"),
path(
"get-course-tracking/<int:tracking_id>/",
admissionviews.get_course_tracking,
name="get_course_tracking",
),
path(
"edit-course-tracking/",
admissionviews.edit_course_tracking,
name="edit_course_tracking",
),
# Test endpoints (commented out)
# path('test/error/505/', views.test_505_error, name='test_505_error'),
]
+61 -36
View File
@@ -1,24 +1,30 @@
import random
import string
import requests
from django.template.loader import render_to_string
from django.core.mail import send_mail
from django.conf import settings
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import urlsafe_base64_encode
from django.utils.encoding import force_bytes
from django.utils import timezone
import pyotp
# Standard library imports
import base64
from .models import TOTPSecret, ResetToken, Student, Staff
import random
# Core Django imports
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.utils import timezone
# Third-party app imports
import pyotp
# Local app imports
from app.models import ResetToken, Staff, Student, TOTPSecret
# OTP expiration time in seconds (5 minutes)
OTP_EXPIRY = 300
# --------------------------------------------------------------------
# OTP and Secret Key Generation
# --------------------------------------------------------------------
def generate_secret_key():
"""Generate a random secret key for TOTP"""
return base64.b32encode(random.getrandbits(160).to_bytes(20, 'big')).decode('utf-8')
return base64.b32encode(random.getrandbits(160).to_bytes(20, "big")).decode("utf-8")
def generate_otp(secret_key):
@@ -27,17 +33,20 @@ def generate_otp(secret_key):
return totp.now()
# --------------------------------------------------------------------
# OTP Storage and Verification
# --------------------------------------------------------------------
def store_secret_key(identifier, secret_key):
"""Store secret key in database"""
# Delete any existing secrets for this identifier
TOTPSecret.objects.filter(identifier=identifier).delete()
# Create new secret with expiration
expires_at = timezone.now() + timezone.timedelta(seconds=OTP_EXPIRY)
return TOTPSecret.objects.create(
identifier=identifier,
secret_key=secret_key,
expires_at=expires_at
identifier=identifier, secret_key=secret_key, expires_at=expires_at
)
@@ -45,12 +54,12 @@ def verify_otp(identifier, otp):
"""Verify OTP using TOTP"""
try:
# Get the most recent secret for this identifier
secret = TOTPSecret.objects.filter(identifier=identifier).latest('created_at')
secret = TOTPSecret.objects.filter(identifier=identifier).latest("created_at")
# Check if secret is expired
if secret.is_expired():
return False
# Verify OTP
totp = pyotp.TOTP(secret.secret_key, interval=OTP_EXPIRY)
return totp.verify(otp)
@@ -58,6 +67,11 @@ def verify_otp(identifier, otp):
return False
# --------------------------------------------------------------------
# SMS Communication
# --------------------------------------------------------------------
def send_otp_sms(phone, otp):
"""Send OTP via SMS using a service"""
try:
@@ -65,19 +79,18 @@ def send_otp_sms(phone, otp):
student = Student.objects.filter(phone=phone).first()
staff = Staff.objects.filter(phone=phone).first()
user = student or staff
# Get user's name
user_name = user.name if user else "User"
# Calculate expiry time in minutes
expiry_minutes = OTP_EXPIRY // 60
# Render SMS message using template
message = render_to_string("login/sms_templates/otp_message.txt", {
"user_name": user_name,
"otp": otp,
"expiry_time": expiry_minutes
})
message = render_to_string(
"login/sms_templates/otp_message.txt",
{"user_name": user_name, "otp": otp, "expiry_time": expiry_minutes},
)
# This is a placeholder. You need to replace with actual SMS API implementation
# Example using a generic SMS API
@@ -100,6 +113,11 @@ def send_otp_sms(phone, otp):
return False
# --------------------------------------------------------------------
# Email Communication
# --------------------------------------------------------------------
def send_password_reset_email(user, request, email):
"""
Send password reset code to user's email using HTML template
@@ -107,10 +125,10 @@ def send_password_reset_email(user, request, email):
try:
# Generate a secret key for TOTP
secret_key = generate_secret_key()
# Store the secret key in database
store_secret_key(email, secret_key)
# Generate TOTP
totp = pyotp.TOTP(secret_key, interval=OTP_EXPIRY)
reset_code = totp.now()
@@ -151,17 +169,20 @@ def verify_reset_code(email, code):
return verify_otp(email, code)
# --------------------------------------------------------------------
# Token Management
# --------------------------------------------------------------------
def store_reset_token(token, identifier):
"""Store reset token in database"""
# Delete any existing tokens for this identifier
ResetToken.objects.filter(identifier=identifier).delete()
# Create new token with expiration (15 minutes)
expires_at = timezone.now() + timezone.timedelta(minutes=15)
return ResetToken.objects.create(
token=token,
identifier=identifier,
expires_at=expires_at
token=token, identifier=identifier, expires_at=expires_at
)
@@ -169,18 +190,22 @@ def verify_reset_token(token, identifier):
"""Verify reset token from database"""
try:
reset_token = ResetToken.objects.get(token=token)
# Check if token is expired
if reset_token.is_expired():
return False
return reset_token.identifier == identifier
except ResetToken.DoesNotExist:
return False
# --------------------------------------------------------------------
# Cleanup Functions
# --------------------------------------------------------------------
def cleanup_expired_tokens():
"""Clean up expired TOTP secrets and reset tokens"""
TOTPSecret.objects.filter(expires_at__lt=timezone.now()).delete()
ResetToken.objects.filter(expires_at__lt=timezone.now()).delete()
+411 -510
View File
File diff suppressed because it is too large. Load diff