huge updates
This commit is contained in:
82 files changed
+14911
-4509
No files matched your search
+353
-87
@@ -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)
|
||||
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
Whitespace-only changes.
+603
-135
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
+118
-102
@@ -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"}
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
# Core Django imports
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
+211
-67
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user