huge updates i donot track what updates i have made
This commit is contained in:
55 files changed
+11072
-1415
No files matched your search
+438
-134
@@ -10,17 +10,25 @@ from app.models import (
|
||||
AttendanceRecord,
|
||||
Notice,
|
||||
Routine,
|
||||
StudentRoutine,
|
||||
Staff_leave,
|
||||
Student_Leave,
|
||||
StudentFeedback,
|
||||
CourseTracking,
|
||||
InstituteFeedback,
|
||||
StaffInstituteFeedback,
|
||||
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
|
||||
import json
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from datetime import datetime
|
||||
|
||||
# Register your models here.
|
||||
|
||||
|
||||
@@ -39,7 +47,6 @@ class CustomAdminSite(admin.AdminSite):
|
||||
"group": "Authentication",
|
||||
"group_label": "authentication",
|
||||
},
|
||||
|
||||
# Organization Models
|
||||
"Institute": {
|
||||
"group": "Organization",
|
||||
@@ -61,7 +68,6 @@ class CustomAdminSite(admin.AdminSite):
|
||||
"group": "Organization",
|
||||
"group_label": "organization",
|
||||
},
|
||||
|
||||
# Human Management Models
|
||||
"Student": {
|
||||
"group": "Human Management",
|
||||
@@ -99,6 +105,10 @@ class CustomAdminSite(admin.AdminSite):
|
||||
"group": "Human Management",
|
||||
"group_label": "human_management",
|
||||
},
|
||||
"StaffInstituteFeedback": {
|
||||
"group": "Human Management",
|
||||
"group_label": "human_management",
|
||||
},
|
||||
}
|
||||
|
||||
grouped_app_list = [{"name": "Main", "app_label": "dashboard", "models": []}]
|
||||
@@ -117,7 +127,21 @@ class CustomAdminSite(admin.AdminSite):
|
||||
}
|
||||
grouped_models[group]["models"].append(model)
|
||||
|
||||
grouped_app_list = list(grouped_models.values())
|
||||
dashboard_item = {
|
||||
"name": "Dashboard",
|
||||
"app_label": "dashboard",
|
||||
"models": [
|
||||
{
|
||||
"name": "Dashboard",
|
||||
"object_name": "Dashboard",
|
||||
"perms": {"has_module_perms": True},
|
||||
"admin_url": "/app/dashboard",
|
||||
"view_only": True,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
grouped_app_list = [dashboard_item] + list(grouped_models.values())
|
||||
|
||||
return grouped_app_list
|
||||
|
||||
@@ -136,11 +160,29 @@ class InstituteModel(admin.ModelAdmin):
|
||||
|
||||
@admin.register(Course, site=custom_admin_site)
|
||||
class CourseModel(admin.ModelAdmin):
|
||||
list_display = ("id", "name", "duration", "duration_type")
|
||||
list_display = ("id", "name", "duration", "duration_type", "get_total_duration")
|
||||
search_fields = ("name", "duration", "duration_type")
|
||||
list_filter = ("name", "duration", "duration_type")
|
||||
list_filter = ("duration_type", "duration")
|
||||
advanced_filter_fields = ("name", "duration", "duration_type")
|
||||
|
||||
def get_total_duration(self, obj):
|
||||
if obj.duration_type == "Semester":
|
||||
return f"{obj.duration} years ({obj.duration * 2} semesters)"
|
||||
return f"{obj.duration} year{'s' if obj.duration > 1 else ''}"
|
||||
|
||||
get_total_duration.short_description = "Total Duration"
|
||||
|
||||
fieldsets = (
|
||||
(None, {"fields": ("name",)}),
|
||||
(
|
||||
"Duration Settings",
|
||||
{
|
||||
"fields": ("duration", "duration_type"),
|
||||
"description": "For yearly courses, duration is in years. For semester-based courses, duration is in years (2 semesters per year).",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Batch, site=custom_admin_site)
|
||||
class BatchAdmin(admin.ModelAdmin):
|
||||
@@ -152,41 +194,78 @@ class BatchAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(Subject, site=custom_admin_site)
|
||||
class SubjectModel(admin.ModelAdmin):
|
||||
list_display = ("id", "name", "course", "semester_or_year")
|
||||
search_fields = ("name", "course_id", "semester_or_year")
|
||||
list_filter = ("name", "course_id", "semester_or_year")
|
||||
list_display = ("id", "name", "course", "get_period_display", "has_syllabus")
|
||||
search_fields = ("name", "course__name")
|
||||
list_filter = ("course", "semester_or_year")
|
||||
advanced_filter_fields = ("name", "course", "semester_or_year")
|
||||
|
||||
def has_syllabus(self, obj):
|
||||
return bool(obj.syllabus_pdf)
|
||||
|
||||
has_syllabus.boolean = True
|
||||
has_syllabus.short_description = "Has Syllabus"
|
||||
|
||||
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}"
|
||||
|
||||
get_period_display.short_description = "Period"
|
||||
|
||||
def add_view(self, request, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["show_save_and_add_another"] = True
|
||||
extra_context["show_save"] = False
|
||||
return super().add_view(request, form_url, extra_context)
|
||||
|
||||
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
|
||||
return self.response_post_save_add(request, obj)
|
||||
return super().response_add(request, obj, post_url_continue)
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
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[
|
||||
"semester_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[
|
||||
"semester_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"
|
||||
]
|
||||
return form
|
||||
|
||||
def response_post_save_add(self, request, obj):
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
|
||||
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}"
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Student, site=custom_admin_site)
|
||||
class StudentAdmin(admin.ModelAdmin):
|
||||
list_display = ("id", "name", "status", "phone")
|
||||
form = StudentAdminForm
|
||||
search_fields = (
|
||||
"name",
|
||||
"phone",
|
||||
"courses",
|
||||
"gender",
|
||||
"joining_date",
|
||||
)
|
||||
list_filter = (
|
||||
"name",
|
||||
"phone",
|
||||
"courses",
|
||||
"gender",
|
||||
"joining_date",
|
||||
"status",
|
||||
)
|
||||
advanced_filter_fields = (
|
||||
"name",
|
||||
"phone",
|
||||
"courses",
|
||||
"gender",
|
||||
"joining_date",
|
||||
"status",
|
||||
)
|
||||
readonly_fields = ("password",)
|
||||
ordering = ("name",)
|
||||
filter_horizontal = ("groups",)
|
||||
list_display = ("id", "name", "phone", "course", "status")
|
||||
search_fields = ("name", "phone", "course__name")
|
||||
list_filter = ("course", "status", "gender", "joining_date")
|
||||
advanced_filter_fields = ("name", "phone", "course", "batch", "status")
|
||||
|
||||
def has_view_permission(self, request, obj=None):
|
||||
"""Check if user has view permission"""
|
||||
@@ -213,38 +292,14 @@ class StudentAdmin(admin.ModelAdmin):
|
||||
return self.readonly_fields + ("password",)
|
||||
return self.readonly_fields
|
||||
|
||||
class Media:
|
||||
js = ("js/student_admin.js",)
|
||||
|
||||
|
||||
@admin.register(Staff, site=custom_admin_site)
|
||||
class StaffAdmin(admin.ModelAdmin):
|
||||
list_display = ["name", "phone", "designation", "joining_date"]
|
||||
form = StaffAdminForm
|
||||
search_fields = (
|
||||
"name",
|
||||
"phone",
|
||||
"monthly_salary",
|
||||
"designation",
|
||||
"joining_date",
|
||||
"gender",
|
||||
)
|
||||
list_filter = (
|
||||
"name",
|
||||
"phone",
|
||||
"designation",
|
||||
"joining_date",
|
||||
"gender",
|
||||
"groups",
|
||||
)
|
||||
advanced_filter_fields = (
|
||||
"name",
|
||||
"phone",
|
||||
"designation",
|
||||
"joining_date",
|
||||
"gender",
|
||||
"groups",
|
||||
)
|
||||
list_display = ("id", "name", "phone", "designation", "joining_date")
|
||||
search_fields = ("name", "phone", "designation")
|
||||
list_filter = ("designation", "joining_date", "gender")
|
||||
advanced_filter_fields = ("name", "phone", "designation")
|
||||
readonly_fields = ("password",)
|
||||
ordering = ("name",)
|
||||
filter_horizontal = ("groups",)
|
||||
@@ -274,9 +329,6 @@ class StaffAdmin(admin.ModelAdmin):
|
||||
return self.readonly_fields + ("password",)
|
||||
return self.readonly_fields
|
||||
|
||||
class Media:
|
||||
js = ("js/staff_admin.js",)
|
||||
|
||||
|
||||
class AttendanceRecordForm(ModelForm):
|
||||
def clean(self):
|
||||
@@ -334,9 +386,6 @@ class AttendanceAdmin(admin.ModelAdmin):
|
||||
|
||||
formset.save_m2m()
|
||||
|
||||
class Media:
|
||||
js = ("js/attendance_admin.js",)
|
||||
|
||||
|
||||
@admin.register(Notice, site=custom_admin_site)
|
||||
class NoticeAdmin(admin.ModelAdmin):
|
||||
@@ -345,64 +394,242 @@ class NoticeAdmin(admin.ModelAdmin):
|
||||
list_filter = ("title", "message")
|
||||
advanced_filter_fields = ("title", "message")
|
||||
|
||||
class Media:
|
||||
js = ("js/notice_admin.js",)
|
||||
|
||||
|
||||
class StudentRoutineStackInline(admin.StackedInline):
|
||||
model = StudentRoutine
|
||||
extra = 0
|
||||
|
||||
|
||||
@admin.register(Routine, site=custom_admin_site)
|
||||
class RoutineAdmin(admin.ModelAdmin):
|
||||
inlines = [StudentRoutineStackInline]
|
||||
list_display = ["name", "start_time", "end_time", "teacher"]
|
||||
search_fields = ("name", "start_time", "end_time", "teacher__name")
|
||||
list_filter = ("name", "start_time", "end_time", "teacher")
|
||||
advanced_filter_fields = ("name", "start_time", "end_time", "teacher")
|
||||
list_display = [
|
||||
"course",
|
||||
"subject",
|
||||
"teacher",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"semester_or_year",
|
||||
"is_active",
|
||||
]
|
||||
list_filter = ["is_active", "course", "subject", "teacher", "semester_or_year"]
|
||||
search_fields = ["course__name", "subject__name", "teacher__name"]
|
||||
ordering = ["start_time"]
|
||||
change_form_template = "admin/app/routine/change_form.html"
|
||||
|
||||
def get_urls(self):
|
||||
urls = super().get_urls()
|
||||
custom_urls = [
|
||||
path(
|
||||
"add-multiple/",
|
||||
self.admin_site.admin_view(self.save_routines),
|
||||
name="routine-add-multiple",
|
||||
),
|
||||
]
|
||||
return custom_urls + urls
|
||||
|
||||
def save_routines(self, request):
|
||||
if request.method == "POST":
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
routines = data.get("routines", [])
|
||||
|
||||
if not routines:
|
||||
return JsonResponse(
|
||||
{"success": False, "message": "No routines data provided"}
|
||||
)
|
||||
|
||||
with (
|
||||
transaction.atomic()
|
||||
): # Use transaction to ensure all routines are created or none
|
||||
created_routines = []
|
||||
for index, routine_data in enumerate(routines):
|
||||
try:
|
||||
# Validate required fields
|
||||
required_fields = [
|
||||
"course",
|
||||
"subject",
|
||||
"teacher",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"semester_or_year",
|
||||
]
|
||||
missing_fields = [
|
||||
field
|
||||
for field in required_fields
|
||||
if field not in routine_data
|
||||
or not routine_data.get(field)
|
||||
]
|
||||
|
||||
if missing_fields:
|
||||
error_msg = f"Missing required fields: {', '.join(missing_fields)}"
|
||||
raise ValueError(error_msg)
|
||||
|
||||
# Convert IDs to integers
|
||||
try:
|
||||
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"])
|
||||
|
||||
except (ValueError, TypeError) as e:
|
||||
raise ValueError(f"Error parsing IDs: {str(e)}")
|
||||
|
||||
# Validate time format
|
||||
try:
|
||||
start_time = datetime.strptime(
|
||||
routine_data["start_time"], "%H:%M"
|
||||
).time()
|
||||
end_time = datetime.strptime(
|
||||
routine_data["end_time"], "%H:%M"
|
||||
).time()
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
"Invalid time format. Please use HH:MM format."
|
||||
)
|
||||
|
||||
# Check for existing routine
|
||||
existing_routine = Routine.objects.filter(
|
||||
course_id=course_id,
|
||||
subject_id=subject_id,
|
||||
semester_or_year=semester,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
).first()
|
||||
|
||||
if existing_routine:
|
||||
# Update existing routine
|
||||
existing_routine.teacher_id = teacher_id
|
||||
existing_routine.save()
|
||||
created_routines.append(existing_routine)
|
||||
else:
|
||||
# Create new routine
|
||||
routine = Routine.objects.create(
|
||||
course_id=course_id,
|
||||
subject_id=subject_id,
|
||||
teacher_id=teacher_id,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
semester_or_year=semester,
|
||||
is_active=True,
|
||||
)
|
||||
created_routines.append(routine)
|
||||
|
||||
except ValueError as e:
|
||||
raise # Re-raise to trigger rollback
|
||||
except Exception as e:
|
||||
raise # Re-raise to trigger rollback
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Successfully saved {len(created_routines)} routines",
|
||||
"routines": [
|
||||
{
|
||||
"id": r.id,
|
||||
"course": r.course.name,
|
||||
"subject": r.subject.name,
|
||||
"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,
|
||||
}
|
||||
for r in created_routines
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
return JsonResponse({"success": False, "message": str(e)})
|
||||
except json.JSONDecodeError as e:
|
||||
return JsonResponse(
|
||||
{"success": False, "message": f"Invalid JSON data: {str(e)}"}
|
||||
)
|
||||
except Exception as e:
|
||||
return JsonResponse({"success": False, "message": str(e)})
|
||||
|
||||
return JsonResponse({"success": False, "message": "Invalid request method"})
|
||||
|
||||
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||
if db_field.name == "subject":
|
||||
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),
|
||||
)
|
||||
else:
|
||||
kwargs["queryset"] = Subject.objects.none()
|
||||
elif db_field.name == "teacher":
|
||||
kwargs["queryset"] = Staff.objects.filter(
|
||||
Q(designation__icontains="Teacher")
|
||||
| Q(designation__icontains="Staff")
|
||||
| Q(designation__icontains="Principal")
|
||||
| Q(designation__isnull=True)
|
||||
| Q(designation="")
|
||||
).order_by("name")
|
||||
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
form = super().get_form(request, obj, **kwargs)
|
||||
if not obj: # Only for adding new routines
|
||||
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
|
||||
return form
|
||||
|
||||
def add_view(self, request, form_url="", extra_context=None):
|
||||
extra_context = extra_context or {}
|
||||
extra_context["show_save_and_add_another"] = True
|
||||
return super().add_view(request, form_url, extra_context)
|
||||
|
||||
class Media:
|
||||
js = ("js/multiple_student_hover.js", "js/routine_admin.js")
|
||||
css = {
|
||||
"all": (
|
||||
"https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css",
|
||||
)
|
||||
}
|
||||
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)
|
||||
class StaffLeaveAdmin(admin.ModelAdmin):
|
||||
list_display = ('staff', 'date', 'get_status_display', 'created_at')
|
||||
list_filter = ('status', 'date', 'created_at')
|
||||
search_fields = ('staff__name', 'message')
|
||||
readonly_fields = ('created_at', 'updated_at')
|
||||
ordering = ('-created_at',)
|
||||
raw_id_fields = ('staff',)
|
||||
list_display = ("staff", "date", "get_status_display", "created_at")
|
||||
list_filter = ("status", "date", "created_at")
|
||||
search_fields = ("staff__name", "message")
|
||||
readonly_fields = ("created_at", "updated_at")
|
||||
ordering = ("-created_at",)
|
||||
raw_id_fields = ("staff",)
|
||||
|
||||
|
||||
@admin.register(Student_Leave, site=custom_admin_site)
|
||||
class StudentLeaveAdmin(admin.ModelAdmin):
|
||||
list_display = ('student', 'date', 'get_status_display', 'created_at')
|
||||
list_filter = ('status', 'date', 'created_at')
|
||||
search_fields = ('student__name', 'message')
|
||||
readonly_fields = ('created_at', 'updated_at')
|
||||
ordering = ('-created_at',)
|
||||
raw_id_fields = ('student',)
|
||||
list_display = ("student", "date", "get_status_display", "created_at")
|
||||
list_filter = ("status", "date", "created_at")
|
||||
search_fields = ("student__name", "message")
|
||||
readonly_fields = ("created_at", "updated_at")
|
||||
ordering = ("-created_at",)
|
||||
raw_id_fields = ("student",)
|
||||
|
||||
|
||||
@admin.register(StudentFeedback, site=custom_admin_site)
|
||||
class StudentFeedbackAdmin(admin.ModelAdmin):
|
||||
list_display = ('student', 'rating', 'created_at')
|
||||
list_filter = ('rating', 'created_at')
|
||||
search_fields = ('student__name', 'feedback_text')
|
||||
readonly_fields = ('created_at', 'updated_at')
|
||||
ordering = ('-created_at',)
|
||||
list_display = ("student", "rating", "created_at")
|
||||
list_filter = ("rating", "created_at")
|
||||
search_fields = ("student__name", "feedback_text")
|
||||
readonly_fields = ("created_at", "updated_at")
|
||||
ordering = ("-created_at",)
|
||||
|
||||
|
||||
@admin.register(CourseTracking, site=custom_admin_site)
|
||||
class CourseTrackingAdmin(admin.ModelAdmin):
|
||||
list_display = ('student', 'course', 'progress_status', 'completion_percentage', 'start_date', 'expected_end_date')
|
||||
list_filter = ('progress_status', 'course', 'start_date', 'expected_end_date')
|
||||
search_fields = ('student__name', 'course__name', 'notes')
|
||||
readonly_fields = ('enrollment_date', 'created_at', 'updated_at')
|
||||
ordering = ('-created_at',)
|
||||
raw_id_fields = ('student', 'course')
|
||||
list_display = (
|
||||
"student",
|
||||
"course",
|
||||
"progress_status",
|
||||
"completion_percentage",
|
||||
"start_date",
|
||||
"expected_end_date",
|
||||
)
|
||||
list_filter = ("progress_status", "course", "start_date", "expected_end_date")
|
||||
search_fields = ("student__name", "course__name", "notes")
|
||||
readonly_fields = ("enrollment_date", "created_at", "updated_at")
|
||||
ordering = ("-created_at",)
|
||||
raw_id_fields = ("student", "course")
|
||||
list_per_page = 25
|
||||
|
||||
def has_view_permission(self, request, obj=None):
|
||||
@@ -420,44 +647,121 @@ class CourseTrackingAdmin(admin.ModelAdmin):
|
||||
def has_module_permission(self, request):
|
||||
return request.user.has_module_perms("app")
|
||||
|
||||
class Media:
|
||||
js = ("js/course_tracking_admin.js",)
|
||||
|
||||
|
||||
@admin.register(InstituteFeedback, site=custom_admin_site)
|
||||
class InstituteFeedbackAdmin(admin.ModelAdmin):
|
||||
list_display = ('display_name', 'institute', 'feedback_type', 'rating', 'is_anonymous', 'is_public', 'created_at')
|
||||
list_filter = ('feedback_type', 'rating', 'is_anonymous', 'is_public', 'created_at', 'institute')
|
||||
search_fields = ('user__name', 'institute__name', 'feedback_text')
|
||||
readonly_fields = ('created_at', 'updated_at')
|
||||
date_hierarchy = 'created_at'
|
||||
|
||||
list_display = (
|
||||
"display_name",
|
||||
"institute",
|
||||
"feedback_type",
|
||||
"rating",
|
||||
"is_anonymous",
|
||||
"is_public",
|
||||
"created_at",
|
||||
)
|
||||
list_filter = (
|
||||
"feedback_type",
|
||||
"rating",
|
||||
"is_anonymous",
|
||||
"is_public",
|
||||
"created_at",
|
||||
"institute",
|
||||
)
|
||||
search_fields = ("user__name", "institute__name", "feedback_text")
|
||||
readonly_fields = ("created_at", "updated_at")
|
||||
date_hierarchy = "created_at"
|
||||
|
||||
def display_name(self, obj):
|
||||
return obj.display_name
|
||||
display_name.short_description = 'Student Name'
|
||||
|
||||
|
||||
display_name.short_description = "Student Name"
|
||||
|
||||
fieldsets = (
|
||||
('Feedback Information', {
|
||||
'fields': ('institute', 'user', 'feedback_type', 'rating', 'feedback_text')
|
||||
}),
|
||||
('Privacy Settings', {
|
||||
'fields': ('is_anonymous', 'is_public'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Timestamps', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
(None, {"fields": ("institute", "user")}),
|
||||
(
|
||||
"Feedback Details",
|
||||
{
|
||||
"fields": (
|
||||
"feedback_type",
|
||||
"rating",
|
||||
"feedback_text",
|
||||
"is_anonymous",
|
||||
"is_public",
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
"Metadata",
|
||||
{"fields": ("created_at", "updated_at"), "classes": ("collapse",)},
|
||||
),
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).select_related('user', 'institute')
|
||||
return super().get_queryset(request).select_related("user", "institute")
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('admin/css/forms.css',)
|
||||
}
|
||||
js = ('admin/js/collapse.js',)
|
||||
css = {"all": ("admin/css/forms.css",)}
|
||||
js = ("admin/js/collapse.js",)
|
||||
|
||||
|
||||
@admin.register(StaffInstituteFeedback, site=custom_admin_site)
|
||||
class StaffInstituteFeedbackAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
"display_name",
|
||||
"institute",
|
||||
"feedback_type",
|
||||
"rating",
|
||||
"is_anonymous",
|
||||
"is_public",
|
||||
"created_at",
|
||||
)
|
||||
list_filter = (
|
||||
"feedback_type",
|
||||
"rating",
|
||||
"is_anonymous",
|
||||
"is_public",
|
||||
"created_at",
|
||||
"institute",
|
||||
)
|
||||
search_fields = ("staff__name", "institute__name", "feedback_text")
|
||||
readonly_fields = ("created_at", "updated_at")
|
||||
date_hierarchy = "created_at"
|
||||
|
||||
def display_name(self, obj):
|
||||
return obj.display_name
|
||||
|
||||
display_name.short_description = "Staff Name"
|
||||
|
||||
fieldsets = (
|
||||
(None, {"fields": ("institute", "staff")}),
|
||||
(
|
||||
"Feedback Details",
|
||||
{
|
||||
"fields": (
|
||||
"feedback_type",
|
||||
"rating",
|
||||
"feedback_text",
|
||||
"is_anonymous",
|
||||
"is_public",
|
||||
)
|
||||
},
|
||||
),
|
||||
(
|
||||
"Metadata",
|
||||
{"fields": ("created_at", "updated_at"), "classes": ("collapse",)},
|
||||
),
|
||||
)
|
||||
|
||||
def get_queryset(self, request):
|
||||
return super().get_queryset(request).select_related("staff", "institute")
|
||||
|
||||
|
||||
@admin.register(FCMDevice)
|
||||
class FCMDeviceAdmin(admin.ModelAdmin):
|
||||
list_display = ("token",)
|
||||
search_fields = ("token",)
|
||||
list_filter = ("token",)
|
||||
advanced_filter_fields = ("token",)
|
||||
|
||||
|
||||
custom_admin_site.register(User, UserAdmin)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib.auth import login
|
||||
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.core.cache import cache
|
||||
import uuid
|
||||
from .models import Student, Staff
|
||||
from .utils import (
|
||||
generate_otp,
|
||||
store_otp,
|
||||
verify_otp,
|
||||
send_otp_sms,
|
||||
send_password_reset_email,
|
||||
verify_reset_code
|
||||
)
|
||||
|
||||
def reset_password_options(request):
|
||||
"""View for selecting between phone and email password reset"""
|
||||
return render(request, "login/reset_options.html")
|
||||
|
||||
def password_reset_phone(request):
|
||||
"""View for initiating password reset with phone number"""
|
||||
if request.method == "POST":
|
||||
phone = request.POST.get("phone")
|
||||
|
||||
# Validate phone number
|
||||
if not phone or not phone.isdigit():
|
||||
return render(request, "login/password_reset_phone.html", {"error": "Please enter a valid phone number", "is_email": False})
|
||||
|
||||
# Check if a user with this phone exists
|
||||
student = Student.objects.filter(phone=phone).first()
|
||||
staff = Staff.objects.filter(phone=phone).first()
|
||||
|
||||
if not student and not staff:
|
||||
return render(request, "login/password_reset_phone.html", {"error": "No account found with this phone number", "is_email": False})
|
||||
|
||||
# Generate and store OTP
|
||||
otp = generate_otp()
|
||||
store_otp(phone, otp)
|
||||
|
||||
# Send OTP via SMS
|
||||
send_result = send_otp_sms(phone, otp)
|
||||
|
||||
if send_result:
|
||||
# Redirect to OTP verification page
|
||||
return render(request, "login/password_reset_otp.html", {"phone": phone, "is_email": False})
|
||||
else:
|
||||
return render(request, "login/password_reset_phone.html", {"error": "Failed to send OTP. Please try again.", "is_email": False})
|
||||
|
||||
return render(request, "login/password_reset_phone.html", {"is_email": False})
|
||||
|
||||
def password_reset_phone_verify(request):
|
||||
"""View for verifying OTP for password reset"""
|
||||
if request.method == "POST":
|
||||
phone = request.POST.get("phone")
|
||||
otp = request.POST.get("otp")
|
||||
|
||||
if not phone or not otp:
|
||||
return render(request, "login/password_reset_otp.html", {
|
||||
"error": "Phone and OTP are required",
|
||||
"phone": phone,
|
||||
"is_email": False
|
||||
})
|
||||
|
||||
# Verify OTP
|
||||
if verify_otp(phone, otp):
|
||||
# Generate a secure token for this reset
|
||||
token = str(uuid.uuid4())
|
||||
# Store token with phone in cache for 15 minutes
|
||||
cache.set(f"reset_token_{token}", phone, 60 * 15)
|
||||
|
||||
# Redirect to set new password
|
||||
return render(request, "login/password_reset_set.html", {
|
||||
"phone": phone,
|
||||
"token": token,
|
||||
"is_email": False
|
||||
})
|
||||
else:
|
||||
return render(request, "login/password_reset_otp.html", {
|
||||
"error": "Invalid or expired OTP",
|
||||
"phone": phone,
|
||||
"is_email": False
|
||||
})
|
||||
|
||||
# If GET request, redirect to phone login
|
||||
return redirect("phone_reset_password")
|
||||
|
||||
def resend_phone_otp(request):
|
||||
"""View for resending OTP for password reset"""
|
||||
if request.method == "POST":
|
||||
phone = request.POST.get("phone")
|
||||
|
||||
if not phone:
|
||||
return redirect("phone_reset_password")
|
||||
|
||||
# Generate and store new OTP
|
||||
otp = generate_otp()
|
||||
store_otp(phone, otp)
|
||||
|
||||
# Send OTP via SMS
|
||||
send_result = send_otp_sms(phone, otp)
|
||||
|
||||
if send_result:
|
||||
return render(request, "login/password_reset_otp.html", {
|
||||
"phone": phone,
|
||||
"message": "OTP resent successfully",
|
||||
"is_email": False
|
||||
})
|
||||
else:
|
||||
return render(request, "login/password_reset_otp.html", {
|
||||
"phone": phone,
|
||||
"error": "Failed to resend OTP",
|
||||
"is_email": False
|
||||
})
|
||||
|
||||
return redirect("phone_reset_password")
|
||||
|
||||
def password_reset_email(request):
|
||||
"""View for initiating password reset with email"""
|
||||
if request.method == "POST":
|
||||
email = request.POST.get("email")
|
||||
|
||||
# Validate email
|
||||
if not email:
|
||||
return render(request, "login/email_reset.html", {"error": "Please enter a valid email address", "is_email": True})
|
||||
|
||||
# Check if a user with this email exists
|
||||
student = Student.objects.filter(email=email).first()
|
||||
staff = Staff.objects.filter(email=email).first()
|
||||
|
||||
if not student and not staff:
|
||||
return render(request, "login/email_reset.html", {"error": "No account found with this email address", "is_email": True})
|
||||
|
||||
# Get the user
|
||||
user = student or staff
|
||||
|
||||
# Send password reset email
|
||||
if send_password_reset_email(user, request, email):
|
||||
# Redirect to verification page
|
||||
return render(request, "login/password_reset_otp.html", {"email": email, "is_email": True})
|
||||
else:
|
||||
return render(request, "login/email_reset.html", {"error": "Failed to send verification code. Please try again.", "is_email": True})
|
||||
|
||||
return render(request, "login/email_reset.html", {"is_email": True})
|
||||
|
||||
def password_reset_email_verify(request):
|
||||
"""View for verifying reset code for email"""
|
||||
if request.method == "POST":
|
||||
email = request.POST.get("email")
|
||||
code = request.POST.get("otp")
|
||||
|
||||
if not email or not code:
|
||||
return render(request, "login/password_reset_otp.html", {
|
||||
"error": "Email and verification code are required",
|
||||
"email": email,
|
||||
"is_email": True
|
||||
})
|
||||
|
||||
# Verify code
|
||||
if verify_reset_code(email, code):
|
||||
# Generate a secure token for this reset
|
||||
token = str(uuid.uuid4())
|
||||
# Store token with email in cache for 15 minutes
|
||||
cache.set(f"reset_token_{token}", email, 60 * 15)
|
||||
|
||||
# Redirect to set new password
|
||||
return render(request, "login/password_reset_set.html", {
|
||||
"email": email,
|
||||
"token": token,
|
||||
"is_email": True
|
||||
})
|
||||
else:
|
||||
return render(request, "login/password_reset_otp.html", {
|
||||
"error": "Invalid or expired code",
|
||||
"email": email,
|
||||
"is_email": True,
|
||||
"message": "Please check your email for the verification code."
|
||||
})
|
||||
|
||||
# For GET request, show the verification form with email pre-filled
|
||||
email = request.GET.get("email")
|
||||
return render(request, "login/password_reset_otp.html", {
|
||||
"email": email,
|
||||
"is_email": True,
|
||||
"message": "Please check your email for the verification code."
|
||||
})
|
||||
|
||||
def resend_email_code(request):
|
||||
"""View for resending verification code for password reset"""
|
||||
if request.method == "POST":
|
||||
email = request.POST.get("email")
|
||||
|
||||
if not email:
|
||||
return redirect("email_reset_password")
|
||||
|
||||
# Find the user
|
||||
student = Student.objects.filter(email=email).first()
|
||||
staff = Staff.objects.filter(email=email).first()
|
||||
user = student or staff
|
||||
|
||||
if not user:
|
||||
return render(request, "login/password_reset_otp.html", {
|
||||
"email": email,
|
||||
"is_email": True,
|
||||
"error": "User not found"
|
||||
})
|
||||
|
||||
# Send new verification code
|
||||
if send_password_reset_email(user, request, email):
|
||||
return render(request, "login/password_reset_otp.html", {
|
||||
"email": email,
|
||||
"is_email": True,
|
||||
"message": "Verification code resent successfully"
|
||||
})
|
||||
else:
|
||||
return render(request, "login/password_reset_otp.html", {
|
||||
"email": email,
|
||||
"is_email": True,
|
||||
"error": "Failed to resend verification code"
|
||||
})
|
||||
|
||||
return redirect("email_reset_password")
|
||||
|
||||
def set_new_password(request):
|
||||
"""View for setting new password after verification"""
|
||||
if request.method == "POST":
|
||||
phone = request.POST.get("phone")
|
||||
email = request.POST.get("email")
|
||||
token = request.POST.get("token")
|
||||
new_password1 = request.POST.get("new_password1")
|
||||
new_password2 = request.POST.get("new_password2")
|
||||
|
||||
identifier = phone or email
|
||||
is_email = bool(email)
|
||||
|
||||
# Validate inputs
|
||||
if not all([identifier, token, new_password1, new_password2]):
|
||||
return render(request, "login/password_reset_set.html", {
|
||||
"error": "All fields are required",
|
||||
"phone": phone,
|
||||
"email": email,
|
||||
"token": token,
|
||||
"is_email": is_email
|
||||
})
|
||||
|
||||
# Verify token is valid
|
||||
stored_identifier = cache.get(f"reset_token_{token}")
|
||||
if not stored_identifier or stored_identifier != identifier:
|
||||
return render(request, "login/password_reset_set.html", {
|
||||
"error": "Invalid or expired reset session",
|
||||
"phone": phone,
|
||||
"email": email,
|
||||
"token": token,
|
||||
"is_email": is_email
|
||||
})
|
||||
|
||||
# Check passwords match
|
||||
if new_password1 != new_password2:
|
||||
return render(request, "login/password_reset_set.html", {
|
||||
"error": "Passwords do not match",
|
||||
"phone": phone,
|
||||
"email": email,
|
||||
"token": token,
|
||||
"is_email": is_email
|
||||
})
|
||||
|
||||
# Find the user
|
||||
if is_email:
|
||||
student = Student.objects.filter(email=email).first()
|
||||
staff = Staff.objects.filter(email=email).first()
|
||||
else:
|
||||
student = Student.objects.filter(phone=phone).first()
|
||||
staff = Staff.objects.filter(phone=phone).first()
|
||||
|
||||
user = student or staff
|
||||
|
||||
if user:
|
||||
# Set new password
|
||||
user.password = make_password(new_password1)
|
||||
user.save()
|
||||
|
||||
# Invalidate reset token
|
||||
cache.delete(f"reset_token_{token}")
|
||||
|
||||
# Show success message
|
||||
return render(request, "login/password_reset_complete.html")
|
||||
else:
|
||||
return render(request, "login/password_reset_set.html", {
|
||||
"error": "User not found",
|
||||
"phone": phone,
|
||||
"email": email,
|
||||
"token": token,
|
||||
"is_email": is_email
|
||||
})
|
||||
|
||||
# If GET request, redirect to options page
|
||||
return redirect("reset_password_options")
|
||||
+17
-3
@@ -5,12 +5,12 @@ from .models import Staff, Student
|
||||
class MultiModelBackend(ModelBackend):
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
"""
|
||||
Authenticate the user based on username (phone for Student and Staff) and password.
|
||||
Authenticate the user based on username (email or phone) and password.
|
||||
"""
|
||||
if username is None or password is None:
|
||||
return None
|
||||
|
||||
# Try authenticating as Staff
|
||||
# Try authenticating as Staff by phone
|
||||
try:
|
||||
staff = Staff.objects.get(phone=username)
|
||||
if staff.check_password(password):
|
||||
@@ -18,7 +18,15 @@ class MultiModelBackend(ModelBackend):
|
||||
except Staff.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Try authenticating as Student
|
||||
# Try authenticating as Staff by email
|
||||
try:
|
||||
staff = Staff.objects.get(email=username)
|
||||
if staff.check_password(password):
|
||||
return staff
|
||||
except Staff.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Try authenticating as Student by phone
|
||||
try:
|
||||
student = Student.objects.get(phone=username)
|
||||
if student.check_password(password):
|
||||
@@ -26,6 +34,12 @@ class MultiModelBackend(ModelBackend):
|
||||
except Student.DoesNotExist:
|
||||
pass
|
||||
|
||||
# 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
|
||||
|
||||
# Return None if no match
|
||||
return None
|
||||
|
||||
|
||||
+14
-4
@@ -1,8 +1,18 @@
|
||||
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,
|
||||
@@ -36,10 +46,10 @@ class StaffAdminForm(forms.ModelForm):
|
||||
fields = "__all__"
|
||||
|
||||
def save(self, commit=True):
|
||||
student = super().save(commit=False)
|
||||
staff = super().save(commit=False)
|
||||
new_password = self.cleaned_data.get("new_password")
|
||||
if new_password:
|
||||
student.password = make_password(new_password)
|
||||
staff.password = make_password(new_password)
|
||||
if commit:
|
||||
student.save()
|
||||
return student
|
||||
staff.save()
|
||||
return staff
|
||||
+106
-38
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.1.7 on 2025-03-25 17:13
|
||||
# Generated by Django 5.1.7 on 2025-03-27 01:54
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.db.models.deletion
|
||||
@@ -95,6 +95,11 @@ class Migration(migrations.Migration):
|
||||
('temporary_address', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('permanent_address', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('marital_status', models.CharField(blank=True, choices=[('Married', 'Married'), ('Unmarried', 'Unmarried'), ('Divorced', 'Divorced')], max_length=255, null=True)),
|
||||
('parent_name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('parent_phone', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('citizenship_no', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('passport', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('image', models.ImageField(blank=True, null=True, upload_to='staff_image')),
|
||||
('joining_date', models.DateField(blank=True, null=True)),
|
||||
('password', models.CharField(editable=False, max_length=128, null=True)),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this staff belongs to.', related_name='staffs', to='auth.group', verbose_name='groups')),
|
||||
@@ -111,15 +116,16 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='Routine',
|
||||
fields=[
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('start_time', models.TimeField(blank=True, null=True)),
|
||||
('end_time', models.TimeField(blank=True, null=True)),
|
||||
('teacher', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='routines', to='app.staff')),
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('start_time', models.TimeField()),
|
||||
('end_time', models.TimeField()),
|
||||
('semester_or_year', models.PositiveIntegerField()),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.course')),
|
||||
('teacher', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.staff')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Routine',
|
||||
'verbose_name_plural': 'Routines',
|
||||
'ordering': ['start_time'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
@@ -168,11 +174,16 @@ class Migration(migrations.Migration):
|
||||
('phone', models.CharField(blank=True, max_length=255, null=True, unique=True)),
|
||||
('temporary_address', models.CharField(blank=True, max_length=500, null=True)),
|
||||
('permanent_address', models.CharField(blank=True, max_length=500, null=True)),
|
||||
('marital_status', models.CharField(blank=True, choices=[('Married', 'Married'), ('Unmarried', 'Unmarried'), ('Divorced', 'Divorced')], max_length=255, null=True)),
|
||||
('parent_name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('parent_phone', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('citizenship_no', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('image', models.ImageField(blank=True, null=True, upload_to='student_image')),
|
||||
('current_semester', models.PositiveIntegerField(default=1, help_text='Current semester for semester-based courses')),
|
||||
('joining_date', models.DateField(blank=True, null=True)),
|
||||
('password', models.CharField(editable=False, max_length=128, null=True)),
|
||||
('courses', models.ManyToManyField(blank=True, related_name='students', to='app.course')),
|
||||
('batches', models.ManyToManyField(blank=True, related_name='students', to='app.batch')),
|
||||
('course', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='students', to='app.course')),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this student belongs to.', related_name='students', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this student.', related_name='student_set', related_query_name='student', to='auth.permission', verbose_name='user permissions')),
|
||||
],
|
||||
@@ -210,56 +221,113 @@ class Migration(migrations.Migration):
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='StudentRoutine',
|
||||
fields=[
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('course', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='app.course')),
|
||||
('routine', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='students', to='app.routine')),
|
||||
('student', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='routines', to='app.student')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Subject',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('semester_or_year', models.PositiveIntegerField()),
|
||||
('course', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='app.course')),
|
||||
('syllabus_pdf', models.FileField(blank=True, help_text='Upload syllabus or study material PDF', null=True, upload_to='subject_pdfs/')),
|
||||
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.course')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['course', 'semester_or_year', 'name'],
|
||||
'unique_together': {('name', 'course', 'semester_or_year')},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='routine',
|
||||
name='subject',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.subject'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='StaffInstituteFeedback',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('feedback_type', models.CharField(choices=[('General', 'General'), ('Facilities', 'Facilities'), ('Teaching', 'Teaching'), ('Infrastructure', 'Infrastructure'), ('Administration', 'Administration')], default='General', max_length=20)),
|
||||
('rating', models.DecimalField(choices=[(1.0, 1.0), (1.5, 1.5), (2.0, 2.0), (2.5, 2.5), (3.0, 3.0), (3.5, 3.5), (4.0, 4.0), (4.5, 4.5), (5.0, 5.0)], decimal_places=1, max_digits=2)),
|
||||
('feedback_text', models.TextField()),
|
||||
('is_anonymous', models.BooleanField(default=False)),
|
||||
('is_public', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('institute', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='staff_feedbacks', to='app.institute')),
|
||||
('staff', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='institute_feedbacks', to='app.staff')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Staff Institute Feedback',
|
||||
'verbose_name_plural': 'Staff Institute Feedbacks',
|
||||
'ordering': ['-created_at'],
|
||||
'unique_together': {('staff', 'institute', 'feedback_type')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InstituteFeedback',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('feedback_type', models.CharField(choices=[('General', 'General'), ('Facilities', 'Facilities'), ('Teaching', 'Teaching'), ('Infrastructure', 'Infrastructure'), ('Administration', 'Administration')], default='General', max_length=20)),
|
||||
('rating', models.DecimalField(choices=[(1.0, 1.0), (1.5, 1.5), (2.0, 2.0), (2.5, 2.5), (3.0, 3.0), (3.5, 3.5), (4.0, 4.0), (4.5, 4.5), (5.0, 5.0)], decimal_places=1, max_digits=2)),
|
||||
('feedback_text', models.TextField()),
|
||||
('is_anonymous', models.BooleanField(default=False)),
|
||||
('is_public', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('institute', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to='app.institute')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='institute_feedbacks', to='app.student')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Student Institute Feedback',
|
||||
'verbose_name_plural': 'Student Institute Feedbacks',
|
||||
'ordering': ['-created_at'],
|
||||
'unique_together': {('user', 'institute', 'feedback_type')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CourseTracking',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('enrollment_date', models.DateField(auto_now_add=True)),
|
||||
('start_date', models.DateField(blank=True, null=True)),
|
||||
('expected_end_date', models.DateField(blank=True, null=True)),
|
||||
('actual_end_date', models.DateField(blank=True, null=True)),
|
||||
('progress_status', models.CharField(choices=[('Not Started', 'Not Started'), ('In Progress', 'In Progress'), ('Completed', 'Completed'), ('Dropped', 'Dropped')], default='Not Started', max_length=20)),
|
||||
('completion_percentage', models.IntegerField(default=0, help_text='Percentage of course completion (0-100)')),
|
||||
('current_semester', models.PositiveIntegerField(default=1)),
|
||||
('semester_start_date', models.DateField(blank=True, null=True)),
|
||||
('semester_end_date', models.DateField(blank=True, null=True)),
|
||||
('notes', models.TextField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='student_trackings', to='app.course')),
|
||||
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='course_trackings', to='app.student')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Course Tracking',
|
||||
'verbose_name_plural': 'Course Trackings',
|
||||
'ordering': ['-created_at'],
|
||||
'unique_together': {('student', 'course')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='StudentFeedback',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('rating', models.IntegerField(choices=[(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)])),
|
||||
('rating', models.DecimalField(choices=[(1.0, 1.0), (1.5, 1.5), (2.0, 2.0), (2.5, 2.5), (3.0, 3.0), (3.5, 3.5), (4.0, 4.0), (4.5, 4.5), (5.0, 5.0)], decimal_places=1, max_digits=2)),
|
||||
('feedback_text', models.TextField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to='app.student')),
|
||||
('teacher', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.staff')),
|
||||
('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.subject')),
|
||||
('teacher', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to='app.staff')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Student Feedback',
|
||||
'verbose_name_plural': 'Student Feedbacks',
|
||||
'unique_together': {('student', 'subject', 'teacher')},
|
||||
'ordering': ['-created_at'],
|
||||
'unique_together': {('student', 'teacher')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TeacherFeedback',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('feedback_text', models.TextField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.subject')),
|
||||
('teacher', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to='app.staff')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Teacher Feedback',
|
||||
'verbose_name_plural': 'Teacher Feedbacks',
|
||||
'unique_together': {('teacher', 'subject')},
|
||||
},
|
||||
migrations.AlterUniqueTogether(
|
||||
name='routine',
|
||||
unique_together={('course', 'subject', 'teacher', 'start_time', 'end_time', 'semester_or_year')},
|
||||
),
|
||||
]
|
||||
@@ -1,28 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-03-25 17:18
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('app', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='studentfeedback',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='TeacherFeedback',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='studentfeedback',
|
||||
name='subject',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='studentfeedback',
|
||||
name='teacher',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
from django.db import migrations
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('app', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunSQL(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "app_staffinstitutefeedback" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"feedback_type" integer NOT NULL,
|
||||
"feedback_text" text NOT NULL,
|
||||
"rating" integer NOT NULL,
|
||||
"is_anonymous" bool NOT NULL,
|
||||
"created_at" datetime NOT NULL,
|
||||
"staff_id" integer NOT NULL REFERENCES "app_staff" ("id") DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
""",
|
||||
"DROP TABLE IF EXISTS app_staffinstitutefeedback;"
|
||||
),
|
||||
]
|
||||
@@ -1,37 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-03-25 18:18
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('app', '0002_alter_studentfeedback_unique_together_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CourseTracking',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('enrollment_date', models.DateField(auto_now_add=True)),
|
||||
('start_date', models.DateField(blank=True, null=True)),
|
||||
('expected_end_date', models.DateField(blank=True, null=True)),
|
||||
('actual_end_date', models.DateField(blank=True, null=True)),
|
||||
('progress_status', models.CharField(choices=[('Not Started', 'Not Started'), ('In Progress', 'In Progress'), ('Completed', 'Completed'), ('Dropped', 'Dropped')], default='Not Started', max_length=20)),
|
||||
('completion_percentage', models.IntegerField(default=0, help_text='Percentage of course completion (0-100)')),
|
||||
('notes', models.TextField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='student_trackings', to='app.course')),
|
||||
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='course_trackings', to='app.student')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Course Tracking',
|
||||
'verbose_name_plural': 'Course Trackings',
|
||||
'ordering': ['-created_at'],
|
||||
'unique_together': {('student', 'course')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,34 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-03-25 18:27
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('app', '0003_coursetracking'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='studentfeedback',
|
||||
options={'ordering': ['-created_at'], 'verbose_name': 'Student Feedback', 'verbose_name_plural': 'Student Feedbacks'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='studentfeedback',
|
||||
name='subject',
|
||||
field=models.ForeignKey(default='1', on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to='app.subject'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='studentfeedback',
|
||||
name='teacher',
|
||||
field=models.ForeignKey(default='1', on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to='app.staff'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='studentfeedback',
|
||||
unique_together={('student', 'subject', 'teacher')},
|
||||
),
|
||||
]
|
||||
@@ -1,21 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-03-25 19:41
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('app', '0004_alter_studentfeedback_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='studentfeedback',
|
||||
unique_together={('student', 'teacher')},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='studentfeedback',
|
||||
name='subject',
|
||||
),
|
||||
]
|
||||
@@ -1,35 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-03-25 19:44
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('app', '0005_alter_studentfeedback_unique_together_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='InstituteFeedback',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('feedback_type', models.CharField(choices=[('General', 'General'), ('Facilities', 'Facilities'), ('Teaching', 'Teaching'), ('Infrastructure', 'Infrastructure'), ('Administration', 'Administration')], default='General', max_length=20)),
|
||||
('rating', models.IntegerField(choices=[(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)])),
|
||||
('feedback_text', models.TextField()),
|
||||
('is_anonymous', models.BooleanField(default=False)),
|
||||
('is_public', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('institute', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to='app.institute')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='institute_feedbacks', to='app.student')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Institute Feedback',
|
||||
'verbose_name_plural': 'Institute Feedbacks',
|
||||
'ordering': ['-created_at'],
|
||||
'unique_together': {('user', 'institute', 'feedback_type')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 5.1.7 on 2025-03-25 20:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('app', '0006_institutefeedback'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='student',
|
||||
name='is_superuser',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='student',
|
||||
name='is_active',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
]
|
||||
+400
-132
@@ -1,12 +1,11 @@
|
||||
from django.db import models
|
||||
from django.db import models, transaction
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils import timezone
|
||||
from datetime import date
|
||||
from datetime import date, timedelta
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.core.cache import cache
|
||||
from .utils import send_push_notification
|
||||
|
||||
# Create your models here.
|
||||
duration_types_choices = (("Year", "Yearl Based"), ("Semester", "Semester Based"))
|
||||
@@ -61,12 +60,52 @@ class Course(models.Model):
|
||||
class Subject(models.Model):
|
||||
id = models.AutoField(primary_key=True)
|
||||
name = models.CharField(max_length=255)
|
||||
course = models.ForeignKey(Course, on_delete=models.CASCADE, default=1)
|
||||
course = models.ForeignKey(Course, on_delete=models.CASCADE)
|
||||
semester_or_year = models.PositiveIntegerField()
|
||||
syllabus_pdf = models.FileField(upload_to='subject_pdfs/', null=True, blank=True, help_text="Upload syllabus or study material PDF")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.course.name} - {'Semester' if self.course.duration_type == 'Semester' else 'Year'} {self.semester_or_year})"
|
||||
|
||||
def clean(self):
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
# Validate semester/year based on course duration type
|
||||
if self.course.duration_type == "Semester":
|
||||
max_semesters = self.course.duration * 2
|
||||
if self.semester_or_year > max_semesters:
|
||||
raise ValidationError(f"Semester cannot exceed {max_semesters}")
|
||||
else: # Year based
|
||||
if self.semester_or_year > self.course.duration:
|
||||
raise ValidationError(f"Year cannot exceed {self.course.duration}")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_pdf_url(self):
|
||||
if self.syllabus_pdf:
|
||||
return self.syllabus_pdf.url
|
||||
return None
|
||||
|
||||
class Meta:
|
||||
unique_together = ['name', 'course', 'semester_or_year']
|
||||
ordering = ['course', 'semester_or_year', 'name']
|
||||
|
||||
|
||||
class Batch(models.Model):
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
name = models.CharField(max_length=255)
|
||||
start_date = models.DateField(null=True, blank=True)
|
||||
end_date = models.DateField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Batch"
|
||||
verbose_name_plural = "Batches"
|
||||
|
||||
|
||||
class Student(AbstractUser):
|
||||
first_name = None
|
||||
@@ -76,27 +115,21 @@ class Student(AbstractUser):
|
||||
username = None
|
||||
email = None
|
||||
|
||||
# Add these fields
|
||||
is_active = models.BooleanField(default=True)
|
||||
is_staff = models.BooleanField(default=True) # This ensures admin access
|
||||
is_superuser = models.BooleanField(default=False)
|
||||
|
||||
user_permissions = models.ManyToManyField(
|
||||
"auth.Permission",
|
||||
verbose_name="user permissions",
|
||||
blank=True,
|
||||
help_text="Specific permissions for this student.",
|
||||
related_name="student_set", # This fixes the clash
|
||||
related_name="student_set",
|
||||
related_query_name="student",
|
||||
)
|
||||
|
||||
# Similarly, let's also update groups to be explicit
|
||||
groups = models.ManyToManyField(
|
||||
"auth.Group",
|
||||
verbose_name="groups",
|
||||
blank=True,
|
||||
help_text="The groups this student belongs to.",
|
||||
related_name="students", # This matches your existing related_name
|
||||
related_name="students",
|
||||
)
|
||||
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
@@ -112,16 +145,29 @@ class Student(AbstractUser):
|
||||
phone = models.CharField(max_length=255, null=True, blank=True, unique=True)
|
||||
temporary_address = models.CharField(max_length=500, null=True, blank=True)
|
||||
permanent_address = models.CharField(max_length=500, null=True, blank=True)
|
||||
marital_status = models.CharField(
|
||||
max_length=255, choices=marital_status_choices, null=True, blank=True
|
||||
)
|
||||
parent_name = models.CharField(max_length=255, null=True, blank=True)
|
||||
parent_phone = models.CharField(max_length=255, null=True, blank=True)
|
||||
courses = models.ManyToManyField(Course, related_name="students", blank=True)
|
||||
citizenship_no = models.CharField(max_length=255, null=True, blank=True)
|
||||
batches = models.ManyToManyField(Batch, related_name="students", blank=True)
|
||||
image = models.ImageField(upload_to="student_image", null=True, blank=True)
|
||||
course = models.ForeignKey(
|
||||
Course,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="students",
|
||||
)
|
||||
current_semester = models.PositiveIntegerField(
|
||||
default=1, help_text="Current semester for semester-based courses"
|
||||
)
|
||||
joining_date = models.DateField(null=True, blank=True)
|
||||
password = models.CharField(
|
||||
max_length=128, editable=False, null=True
|
||||
) # Editable is set to False to prevent direct editing
|
||||
password = models.CharField(max_length=128, editable=False, null=True)
|
||||
|
||||
USERNAME_FIELD = "phone"
|
||||
REQUIRED_FIELDS = ["name"] # Add required fields
|
||||
REQUIRED_FIELDS = ["name"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -132,41 +178,55 @@ class Student(AbstractUser):
|
||||
|
||||
@property
|
||||
def is_staff(self):
|
||||
"""Check if student has any groups (permissions)"""
|
||||
return self.groups.exists()
|
||||
|
||||
@property
|
||||
def is_authenticated(self):
|
||||
"""Required by Django's authentication system"""
|
||||
return True
|
||||
|
||||
def has_perm(self, perm, obj=None):
|
||||
"""Check if student has a specific permission"""
|
||||
if not self.is_active:
|
||||
return False
|
||||
|
||||
# Get permissions from groups
|
||||
perms = set()
|
||||
for group in self.groups.all():
|
||||
perms.update(
|
||||
group.permissions.values_list("content_type__app_label", "codename")
|
||||
)
|
||||
|
||||
# Convert permission string to app_label and codename
|
||||
app_label, codename = perm.split(".")
|
||||
return (app_label, codename) in perms
|
||||
|
||||
def has_module_perms(self, app_label):
|
||||
"""Check if student has any permissions in the given app"""
|
||||
if not self.is_active:
|
||||
return False
|
||||
|
||||
# Check if any group permissions match the app_label
|
||||
for group in self.groups.all():
|
||||
if group.permissions.filter(content_type__app_label=app_label).exists():
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_current_subjects(self):
|
||||
"""Get subjects for current semester"""
|
||||
if not self.course:
|
||||
return Subject.objects.none()
|
||||
return Subject.objects.filter(
|
||||
course=self.course, semester_or_year=self.current_semester
|
||||
)
|
||||
|
||||
def advance_semester(self):
|
||||
"""Advance to next semester if possible"""
|
||||
if not self.course:
|
||||
return False
|
||||
|
||||
if self.course.duration_type == "Semester":
|
||||
total_semesters = self.course.duration * 2 # 2 semesters per year
|
||||
if self.current_semester < total_semesters:
|
||||
self.current_semester += 1
|
||||
self.save()
|
||||
return True
|
||||
return False
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Remove the course tracking creation from here
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Student"
|
||||
verbose_name_plural = "Students"
|
||||
@@ -213,6 +273,11 @@ class Staff(AbstractUser):
|
||||
marital_status = models.CharField(
|
||||
max_length=255, choices=marital_status_choices, null=True, blank=True
|
||||
)
|
||||
parent_name = models.CharField(max_length=255, null=True, blank=True)
|
||||
parent_phone = models.CharField(max_length=255, null=True, blank=True)
|
||||
citizenship_no = models.CharField(max_length=255, blank=True, null=True)
|
||||
passport = models.CharField(max_length=255, blank=True, null=True)
|
||||
image = models.ImageField(upload_to="staff_image", null=True, blank=True)
|
||||
joining_date = models.DateField(null=True, blank=True)
|
||||
password = models.CharField(
|
||||
max_length=128, editable=False, null=True
|
||||
@@ -269,49 +334,21 @@ class Staff(AbstractUser):
|
||||
verbose_name_plural = "Staffs"
|
||||
|
||||
|
||||
class Batch(models.Model):
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
name = models.CharField(max_length=255)
|
||||
start_date = models.DateField(null=True, blank=True)
|
||||
end_date = models.DateField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Batch"
|
||||
verbose_name_plural = "Batches"
|
||||
|
||||
|
||||
class Routine(models.Model):
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
name = models.CharField(max_length=255, null=True, blank=True)
|
||||
start_time = models.TimeField(null=True, blank=True)
|
||||
end_time = models.TimeField(null=True, blank=True)
|
||||
teacher = models.ForeignKey(
|
||||
Staff, related_name="routines", on_delete=models.CASCADE, null=True, blank=True
|
||||
)
|
||||
course = models.ForeignKey(Course, on_delete=models.CASCADE)
|
||||
subject = models.ForeignKey(Subject, on_delete=models.CASCADE)
|
||||
teacher = models.ForeignKey(Staff, on_delete=models.CASCADE)
|
||||
start_time = models.TimeField()
|
||||
end_time = models.TimeField()
|
||||
semester_or_year = models.PositiveIntegerField()
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
return f"{self.subject.name} - {self.teacher.name} ({self.start_time} - {self.end_time})"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Routine"
|
||||
verbose_name_plural = "Routines"
|
||||
|
||||
|
||||
class StudentRoutine(models.Model):
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
student = models.ForeignKey(
|
||||
Student, related_name="routines", on_delete=models.CASCADE, null=True
|
||||
)
|
||||
routine = models.ForeignKey(
|
||||
Routine, related_name="students", on_delete=models.CASCADE, null=True
|
||||
)
|
||||
course = models.ForeignKey(Course, on_delete=models.CASCADE, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.student.name
|
||||
ordering = ['start_time']
|
||||
unique_together = ['course', 'subject', 'teacher', 'start_time', 'end_time', 'semester_or_year']
|
||||
|
||||
|
||||
class Attendance(models.Model):
|
||||
@@ -395,11 +432,11 @@ class Notice(models.Model):
|
||||
|
||||
class Staff_leave(models.Model):
|
||||
LEAVE_STATUS_CHOICES = (
|
||||
(0, 'Pending'),
|
||||
(1, 'Approved'),
|
||||
(2, 'Rejected'),
|
||||
(0, "Pending"),
|
||||
(1, "Approved"),
|
||||
(2, "Rejected"),
|
||||
)
|
||||
|
||||
|
||||
staff = models.ForeignKey(Staff, on_delete=models.CASCADE)
|
||||
date = models.DateField()
|
||||
message = models.TextField()
|
||||
@@ -413,16 +450,16 @@ class Staff_leave(models.Model):
|
||||
class Meta:
|
||||
verbose_name = "Staff Leave"
|
||||
verbose_name_plural = "Staff Leaves"
|
||||
ordering = ['-created_at']
|
||||
ordering = ["-created_at"]
|
||||
|
||||
|
||||
class Student_Leave(models.Model):
|
||||
LEAVE_STATUS_CHOICES = (
|
||||
(0, 'Pending'),
|
||||
(1, 'Approved'),
|
||||
(2, 'Rejected'),
|
||||
(0, "Pending"),
|
||||
(1, "Approved"),
|
||||
(2, "Rejected"),
|
||||
)
|
||||
|
||||
|
||||
student = models.ForeignKey(Student, on_delete=models.CASCADE)
|
||||
date = models.DateField()
|
||||
message = models.TextField()
|
||||
@@ -436,13 +473,19 @@ class Student_Leave(models.Model):
|
||||
class Meta:
|
||||
verbose_name = "Student Leave"
|
||||
verbose_name_plural = "Student Leaves"
|
||||
ordering = ['-created_at']
|
||||
ordering = ["-created_at"]
|
||||
|
||||
|
||||
class StudentFeedback(models.Model):
|
||||
student = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='feedbacks')
|
||||
teacher = models.ForeignKey(Staff, on_delete=models.CASCADE, related_name='feedbacks')
|
||||
rating = models.IntegerField(choices=[(i, i) for i in range(1, 6)]) # 1-5 rating
|
||||
student = models.ForeignKey(
|
||||
Student, on_delete=models.CASCADE, related_name="feedbacks"
|
||||
)
|
||||
teacher = models.ForeignKey(
|
||||
Staff, on_delete=models.CASCADE, related_name="feedbacks"
|
||||
)
|
||||
rating = models.DecimalField(
|
||||
max_digits=2, decimal_places=1, choices=[(i / 2, i / 2) for i in range(2, 11)]
|
||||
) # 1 to 5 rating
|
||||
feedback_text = models.TextField()
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
@@ -453,23 +496,31 @@ class StudentFeedback(models.Model):
|
||||
class Meta:
|
||||
verbose_name = "Student Feedback"
|
||||
verbose_name_plural = "Student Feedbacks"
|
||||
ordering = ['-created_at']
|
||||
unique_together = ['student', 'teacher'] # Updated unique constraint
|
||||
ordering = ["-created_at"]
|
||||
unique_together = ["student", "teacher"]
|
||||
|
||||
|
||||
class InstituteFeedback(models.Model):
|
||||
FEEDBACK_TYPE_CHOICES = (
|
||||
('General', 'General'),
|
||||
('Facilities', 'Facilities'),
|
||||
('Teaching', 'Teaching'),
|
||||
('Infrastructure', 'Infrastructure'),
|
||||
('Administration', 'Administration'),
|
||||
("General", "General"),
|
||||
("Facilities", "Facilities"),
|
||||
("Teaching", "Teaching"),
|
||||
("Infrastructure", "Infrastructure"),
|
||||
("Administration", "Administration"),
|
||||
)
|
||||
|
||||
institute = models.ForeignKey(Institute, on_delete=models.CASCADE, related_name='feedbacks')
|
||||
user = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='institute_feedbacks')
|
||||
feedback_type = models.CharField(max_length=20, choices=FEEDBACK_TYPE_CHOICES, default='General')
|
||||
rating = models.IntegerField(choices=[(i, i) for i in range(1, 6)]) # 1-5 rating
|
||||
institute = models.ForeignKey(
|
||||
Institute, on_delete=models.CASCADE, related_name="feedbacks"
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
Student, on_delete=models.CASCADE, related_name="institute_feedbacks"
|
||||
)
|
||||
feedback_type = models.CharField(
|
||||
max_length=20, choices=FEEDBACK_TYPE_CHOICES, default="General"
|
||||
)
|
||||
rating = models.DecimalField(
|
||||
max_digits=2, decimal_places=1, choices=[(i / 2, i / 2) for i in range(2, 11)]
|
||||
) # 1 to 5 rating
|
||||
feedback_text = models.TextField()
|
||||
is_anonymous = models.BooleanField(default=False)
|
||||
is_public = models.BooleanField(default=True)
|
||||
@@ -482,33 +533,97 @@ class InstituteFeedback(models.Model):
|
||||
return f"{self.user.name} - {self.institute.name} - {self.feedback_type}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Institute Feedback"
|
||||
verbose_name_plural = "Institute Feedbacks"
|
||||
ordering = ['-created_at']
|
||||
unique_together = ['user', 'institute', 'feedback_type'] # One feedback per type per user
|
||||
verbose_name = "Student Institute Feedback"
|
||||
verbose_name_plural = "Student Institute Feedbacks"
|
||||
ordering = ["-created_at"]
|
||||
unique_together = [
|
||||
"user",
|
||||
"institute",
|
||||
"feedback_type",
|
||||
] # One feedback per type per user
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
"""Return anonymous if feedback is anonymous, otherwise return user's name"""
|
||||
return 'Anonymous' if self.is_anonymous else self.user.name
|
||||
return "Anonymous" if self.is_anonymous else self.user.name
|
||||
|
||||
|
||||
class StaffInstituteFeedback(models.Model):
|
||||
"""Model for tracking feedback from staff members to the institute"""
|
||||
FEEDBACK_TYPE_CHOICES = (
|
||||
("General", "General"),
|
||||
("Facilities", "Facilities"),
|
||||
("Teaching", "Teaching"),
|
||||
("Infrastructure", "Infrastructure"),
|
||||
("Administration", "Administration"),
|
||||
)
|
||||
|
||||
institute = models.ForeignKey(
|
||||
Institute, on_delete=models.CASCADE, related_name="staff_feedbacks"
|
||||
)
|
||||
staff = models.ForeignKey(
|
||||
Staff, on_delete=models.CASCADE, related_name="institute_feedbacks"
|
||||
)
|
||||
feedback_type = models.CharField(
|
||||
max_length=20, choices=FEEDBACK_TYPE_CHOICES, default="General"
|
||||
)
|
||||
rating = models.DecimalField(
|
||||
max_digits=2, decimal_places=1, choices=[(i / 2, i / 2) for i in range(2, 11)]
|
||||
) # 1 to 5 rating
|
||||
feedback_text = models.TextField()
|
||||
is_anonymous = models.BooleanField(default=False)
|
||||
is_public = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
if self.is_anonymous:
|
||||
return f"Anonymous - {self.institute.name} - {self.feedback_type}"
|
||||
return f"{self.staff.name} - {self.institute.name} - {self.feedback_type}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Staff Institute Feedback"
|
||||
verbose_name_plural = "Staff Institute Feedbacks"
|
||||
ordering = ["-created_at"]
|
||||
unique_together = [
|
||||
"staff",
|
||||
"institute",
|
||||
"feedback_type",
|
||||
] # One feedback per type per staff
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
"""Return anonymous if feedback is anonymous, otherwise return staff's name"""
|
||||
return "Anonymous" if self.is_anonymous else self.staff.name
|
||||
|
||||
|
||||
class CourseTracking(models.Model):
|
||||
PROGRESS_STATUS_CHOICES = (
|
||||
('Not Started', 'Not Started'),
|
||||
('In Progress', 'In Progress'),
|
||||
('Completed', 'Completed'),
|
||||
('Dropped', 'Dropped'),
|
||||
("Not Started", "Not Started"),
|
||||
("In Progress", "In Progress"),
|
||||
("Completed", "Completed"),
|
||||
("Dropped", "Dropped"),
|
||||
)
|
||||
|
||||
student = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='course_trackings')
|
||||
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='student_trackings')
|
||||
student = models.ForeignKey(
|
||||
Student, on_delete=models.CASCADE, related_name="course_trackings"
|
||||
)
|
||||
course = models.ForeignKey(
|
||||
Course, on_delete=models.CASCADE, related_name="student_trackings"
|
||||
)
|
||||
enrollment_date = models.DateField(auto_now_add=True)
|
||||
start_date = models.DateField(null=True, blank=True)
|
||||
expected_end_date = models.DateField(null=True, blank=True)
|
||||
actual_end_date = models.DateField(null=True, blank=True)
|
||||
progress_status = models.CharField(max_length=20, choices=PROGRESS_STATUS_CHOICES, default='Not Started')
|
||||
completion_percentage = models.IntegerField(default=0, help_text="Percentage of course completion (0-100)")
|
||||
progress_status = models.CharField(
|
||||
max_length=20, choices=PROGRESS_STATUS_CHOICES, default="Not Started"
|
||||
)
|
||||
completion_percentage = models.IntegerField(
|
||||
default=0, help_text="Percentage of course completion (0-100)"
|
||||
)
|
||||
current_semester = models.PositiveIntegerField(default=1)
|
||||
semester_start_date = models.DateField(null=True, blank=True)
|
||||
semester_end_date = models.DateField(null=True, blank=True)
|
||||
notes = models.TextField(blank=True, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
@@ -516,18 +631,34 @@ class CourseTracking(models.Model):
|
||||
class Meta:
|
||||
verbose_name = "Course Tracking"
|
||||
verbose_name_plural = "Course Trackings"
|
||||
unique_together = ['student', 'course'] # Ensure one tracking per student per course
|
||||
ordering = ['-created_at']
|
||||
unique_together = ["student", "course"]
|
||||
ordering = ["-created_at"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.student.name} - {self.course.name}"
|
||||
|
||||
def clean(self):
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
if not self.student or not self.course:
|
||||
raise ValidationError("Both student and course are required")
|
||||
|
||||
if self.start_date and self.expected_end_date and self.start_date > self.expected_end_date:
|
||||
raise ValidationError("Start date cannot be after expected end date")
|
||||
|
||||
if self.actual_end_date and self.start_date and self.actual_end_date < self.start_date:
|
||||
raise ValidationError("Actual end date cannot be before start date")
|
||||
|
||||
if self.semester_start_date and self.semester_end_date and self.semester_start_date > self.semester_end_date:
|
||||
raise ValidationError("Semester start date cannot be after semester end date")
|
||||
|
||||
if self.current_semester < 1:
|
||||
raise ValidationError("Current semester must be at least 1")
|
||||
|
||||
if self.course.duration_type == "Semester":
|
||||
max_semesters = self.course.duration * 2
|
||||
if self.current_semester > max_semesters:
|
||||
raise ValidationError(f"Current semester cannot exceed {max_semesters}")
|
||||
|
||||
def calculate_completion_percentage(self):
|
||||
if not self.start_date or not self.expected_end_date:
|
||||
@@ -536,9 +667,9 @@ class CourseTracking(models.Model):
|
||||
today = date.today()
|
||||
|
||||
# If course is completed, return 100%
|
||||
if self.progress_status == 'Completed':
|
||||
if self.progress_status == "Completed":
|
||||
return 100
|
||||
elif self.progress_status == 'Dropped':
|
||||
elif self.progress_status == "Dropped":
|
||||
return 0
|
||||
|
||||
# Calculate total duration in days
|
||||
@@ -552,11 +683,18 @@ class CourseTracking(models.Model):
|
||||
return 0
|
||||
|
||||
# Calculate percentage based on course duration type
|
||||
if self.course.duration_type == 'Semester':
|
||||
# For semester-based courses (6 months)
|
||||
max_days = 180 # 6 months = 180 days
|
||||
if elapsed_days >= max_days:
|
||||
return 100
|
||||
if self.course.duration_type == "Semester":
|
||||
# For semester-based courses
|
||||
semester_duration = 180 # 6 months = 180 days
|
||||
total_semesters = self.course.duration * 2 # 2 semesters per year
|
||||
completed_semesters = self.current_semester - 1
|
||||
semester_progress = (completed_semesters / total_semesters) * 100
|
||||
|
||||
# If current semester is completed
|
||||
if self.semester_end_date and today > self.semester_end_date:
|
||||
semester_progress = (self.current_semester / total_semesters) * 100
|
||||
|
||||
return min(100, int(semester_progress))
|
||||
else: # Yearly based
|
||||
max_days = 365 # 1 year = 365 days
|
||||
if elapsed_days >= max_days:
|
||||
@@ -567,32 +705,81 @@ class CourseTracking(models.Model):
|
||||
return percentage
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Validate dates
|
||||
# Calculate dates if this is a new instance
|
||||
if not self.pk:
|
||||
self.start_date = self.start_date or date.today()
|
||||
|
||||
# Calculate expected end date based on course duration
|
||||
if self.course.duration_type == "Year":
|
||||
years = self.course.duration
|
||||
else: # Semester based
|
||||
years = self.course.duration # Duration is already in years
|
||||
|
||||
self.expected_end_date = self.start_date + timedelta(days=years * 365)
|
||||
|
||||
# Set initial semester dates
|
||||
self.semester_start_date = self.start_date
|
||||
self.semester_end_date = self.start_date + timedelta(days=180) # 6 months
|
||||
|
||||
# Validate dates and other fields
|
||||
self.clean()
|
||||
|
||||
|
||||
# Calculate completion percentage
|
||||
self.completion_percentage = self.calculate_completion_percentage()
|
||||
|
||||
|
||||
# Update progress status based on completion percentage
|
||||
if self.completion_percentage >= 100:
|
||||
self.progress_status = 'Completed'
|
||||
self.progress_status = "Completed"
|
||||
if not self.actual_end_date:
|
||||
self.actual_end_date = date.today()
|
||||
elif self.completion_percentage > 0:
|
||||
self.progress_status = 'In Progress'
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
self.progress_status = "In Progress"
|
||||
|
||||
try:
|
||||
super().save(*args, **kwargs)
|
||||
except Exception as e:
|
||||
print(f"Error saving course tracking for {self.student.name}: {str(e)}")
|
||||
raise
|
||||
|
||||
def advance_semester(self):
|
||||
"""Advance to next semester if possible"""
|
||||
if not self.course or self.course.duration_type != "Semester":
|
||||
return False
|
||||
|
||||
total_semesters = self.course.duration * 2
|
||||
if self.current_semester < total_semesters:
|
||||
self.current_semester += 1
|
||||
self.semester_start_date = date.today()
|
||||
self.semester_end_date = self.semester_start_date + timedelta(
|
||||
days=180
|
||||
) # 6 months
|
||||
# Update student's current semester as well
|
||||
self.student.current_semester = self.current_semester
|
||||
self.student.save()
|
||||
self.save()
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_completed(self):
|
||||
return self.progress_status == 'Completed'
|
||||
return self.progress_status == "Completed"
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return self.progress_status == 'In Progress'
|
||||
return self.progress_status == "In Progress"
|
||||
|
||||
@property
|
||||
def remaining_days(self):
|
||||
"""Returns remaining days in current semester"""
|
||||
if not self.semester_end_date:
|
||||
return None
|
||||
today = date.today()
|
||||
remaining = (self.semester_end_date - today).days
|
||||
return max(0, remaining)
|
||||
|
||||
@property
|
||||
def total_remaining_days(self):
|
||||
"""Returns remaining days for entire course"""
|
||||
if not self.expected_end_date:
|
||||
return None
|
||||
today = date.today()
|
||||
@@ -610,25 +797,19 @@ class CourseTracking(models.Model):
|
||||
return (self.expected_end_date - self.start_date).days
|
||||
|
||||
def get_completion_percentage(self):
|
||||
"""
|
||||
Get completion percentage with caching
|
||||
"""
|
||||
return get_cached_percentage(self.id)
|
||||
|
||||
def force_update_percentage(self):
|
||||
"""
|
||||
Force update the completion percentage and clear cache
|
||||
"""
|
||||
cache_key = f'course_tracking_percentage_{self.id}'
|
||||
cache_key = f"course_tracking_percentage_{self.id}"
|
||||
cache.delete(cache_key)
|
||||
self.save() # This will recalculate and update the percentage
|
||||
self.save()
|
||||
|
||||
|
||||
def get_cached_percentage(tracking_id):
|
||||
"""
|
||||
Get cached completion percentage for a course tracking
|
||||
"""
|
||||
cache_key = f'course_tracking_percentage_{tracking_id}'
|
||||
cache_key = f"course_tracking_percentage_{tracking_id}"
|
||||
percentage = cache.get(cache_key)
|
||||
if percentage is None:
|
||||
try:
|
||||
@@ -646,8 +827,95 @@ def update_completion_percentage(sender, instance, **kwargs):
|
||||
Signal to update completion percentage after save
|
||||
Also caches the percentage for 1 hour to avoid frequent recalculations
|
||||
"""
|
||||
cache_key = f'course_tracking_percentage_{instance.id}'
|
||||
cache_key = f"course_tracking_percentage_{instance.id}"
|
||||
cache.set(cache_key, instance.completion_percentage, 3600) # Cache for 1 hour
|
||||
|
||||
|
||||
@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:
|
||||
try:
|
||||
# Check if course tracking already exists
|
||||
existing_tracking = CourseTracking.objects.filter(
|
||||
student=instance,
|
||||
course=instance.course
|
||||
).first()
|
||||
|
||||
if existing_tracking:
|
||||
print(f"Course tracking already exists for {instance.name}")
|
||||
return
|
||||
|
||||
# Calculate dates based on course duration
|
||||
start_date = instance.joining_date or date.today()
|
||||
|
||||
# Calculate expected end date based on course duration type
|
||||
if instance.course.duration_type == "Year":
|
||||
# For year-based courses, each year is 365 days
|
||||
years = instance.course.duration
|
||||
duration_days = years * 365
|
||||
else: # Semester based
|
||||
# For semester-based courses:
|
||||
# - Each year has 2 semesters
|
||||
# - Total years is the same as course duration
|
||||
years = instance.course.duration
|
||||
duration_days = years * 365 # Use full years for total duration
|
||||
|
||||
expected_end_date = start_date + timedelta(days=duration_days)
|
||||
|
||||
# Create course tracking with transaction
|
||||
with transaction.atomic():
|
||||
course_tracking = CourseTracking.objects.create(
|
||||
student=instance,
|
||||
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 + 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)}")
|
||||
# Log the error but don't prevent student creation
|
||||
# You might want to add proper logging here instead of print statements
|
||||
else:
|
||||
print(f"No course assigned to student {instance.name}")
|
||||
|
||||
|
||||
@receiver(post_save, sender=Student)
|
||||
def sync_student_semester(sender, instance, created, **kwargs):
|
||||
"""Signal handler to sync student's current_semester 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
|
||||
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()
|
||||
@@ -0,0 +1,179 @@
|
||||
import django.db
|
||||
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
|
||||
)
|
||||
|
||||
class FCMDeviceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = FCMDevice
|
||||
fields = ['id', 'token']
|
||||
|
||||
class InstituteSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Institute
|
||||
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']
|
||||
|
||||
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']
|
||||
|
||||
class BatchSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Batch
|
||||
fields = ['id', 'name', 'start_date', 'end_date']
|
||||
|
||||
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'
|
||||
]
|
||||
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'
|
||||
]
|
||||
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'
|
||||
]
|
||||
|
||||
class AttendanceRecordSerializer(serializers.ModelSerializer):
|
||||
student = StudentSerializer(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = AttendanceRecord
|
||||
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']
|
||||
|
||||
class NoticeSerializer(serializers.ModelSerializer):
|
||||
created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
|
||||
image_url = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Notice
|
||||
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', 'date', 'message', 'status', 'created_at', 'updated_at']
|
||||
read_only_fields = ['created_at', 'updated_at']
|
||||
|
||||
class StudentLeaveSerializer(serializers.ModelSerializer):
|
||||
student = StudentSerializer(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 = Student_Leave
|
||||
fields = ['id', 'student', 'date', 'message', 'status', 'created_at', 'updated_at']
|
||||
read_only_fields = ['created_at', 'updated_at']
|
||||
|
||||
class StudentFeedbackSerializer(serializers.ModelSerializer):
|
||||
student = StudentSerializer(read_only=True)
|
||||
teacher = 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 = StudentFeedback
|
||||
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)
|
||||
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 = InstituteFeedback
|
||||
fields = [
|
||||
'id', 'institute', 'user', 'feedback_type', 'rating', 'feedback_text',
|
||||
'is_anonymous', 'is_public', 'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = ['created_at', 'updated_at']
|
||||
|
||||
class StaffInstituteFeedbackSerializer(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 = StaffInstituteFeedback
|
||||
fields = [
|
||||
'id', 'institute', 'staff', 'feedback_type', 'rating', 'feedback_text',
|
||||
'is_anonymous', 'is_public', '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)
|
||||
|
||||
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'
|
||||
]
|
||||
read_only_fields = ['created_at', 'updated_at', 'enrollment_date']
|
||||
|
||||
@@ -0,0 +1,585 @@
|
||||
from django.shortcuts import render, redirect
|
||||
from django.http import JsonResponse
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db.models import Avg, Q
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta, datetime
|
||||
from django.views.decorators.http import require_http_methods, require_GET
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.hashers import check_password, make_password
|
||||
from .models import (
|
||||
Staff,
|
||||
Student,
|
||||
Routine,
|
||||
Attendance,
|
||||
AttendanceRecord,
|
||||
StudentFeedback,
|
||||
Staff_leave,
|
||||
Notice,
|
||||
Institute,
|
||||
StaffInstituteFeedback,
|
||||
Subject,
|
||||
InstituteFeedback
|
||||
)
|
||||
from app.admin import custom_admin_site
|
||||
|
||||
|
||||
@login_required
|
||||
def teacherDashboard(request):
|
||||
"""View to display the teacher dashboard with their classes and student information"""
|
||||
teacher = Staff.objects.get(phone=request.user.phone) # Get the Staff object
|
||||
today = timezone.now().date()
|
||||
current_time = timezone.now().time()
|
||||
last_week = today - timedelta(days=7)
|
||||
|
||||
# Get only the active routines assigned to this teacher
|
||||
today_routines = Routine.objects.filter(
|
||||
teacher=teacher, # Filter by the current teacher
|
||||
is_active=True, # Only include active routines
|
||||
).order_by("start_time")
|
||||
|
||||
# Process routines to include status and attendance
|
||||
processed_routines = []
|
||||
for routine in today_routines:
|
||||
attendance = routine.attendances.filter(date=today).first()
|
||||
|
||||
# Get total students from the course associated with the subject
|
||||
total_students = Student.objects.filter(course=routine.subject.course).count()
|
||||
attendance_count = (
|
||||
0
|
||||
if not attendance
|
||||
else AttendanceRecord.objects.filter(
|
||||
attendance=attendance, student_attend=True
|
||||
).count()
|
||||
)
|
||||
|
||||
# Add routine to processed_routines
|
||||
processed_routines.append(
|
||||
{
|
||||
"id": routine.id, # Add the routine ID for use in the template
|
||||
"name": routine.subject.name,
|
||||
"start_time": routine.start_time,
|
||||
"end_time": routine.end_time,
|
||||
"is_completed": attendance is not None, # Only mark as completed if attendance exists
|
||||
"is_ongoing": routine.start_time <= current_time <= routine.end_time,
|
||||
"total_students": total_students,
|
||||
"attendance_count": attendance_count,
|
||||
"semester_or_year": routine.semester_or_year,
|
||||
"course_name": routine.subject.course.name,
|
||||
"has_attendance": attendance is not None, # Flag to check if attendance has been taken
|
||||
}
|
||||
)
|
||||
|
||||
# Calculate class statistics
|
||||
total_classes = today_routines.count()
|
||||
completed_classes = len([r for r in processed_routines if r["is_completed"]])
|
||||
remaining_classes = total_classes - completed_classes
|
||||
|
||||
# Get student statistics - count unique students from teacher's assigned subjects and semesters
|
||||
total_students = Student.objects.filter(status="Active").count()
|
||||
|
||||
# Get present and absent students for today
|
||||
present_students = AttendanceRecord.objects.filter(
|
||||
attendance__date=today,
|
||||
student_attend=True,
|
||||
attendance__routine__teacher=teacher
|
||||
).values('student').distinct().count()
|
||||
|
||||
# Calculate absent students based on teacher's classes today
|
||||
today_class_students = set()
|
||||
for routine in today_routines:
|
||||
students = Student.objects.filter(
|
||||
course=routine.subject.course,
|
||||
current_semester=routine.semester_or_year,
|
||||
status="Active"
|
||||
)
|
||||
today_class_students.update(students.values_list('id', flat=True))
|
||||
|
||||
absent_students = len(today_class_students) - present_students if today_class_students else 0
|
||||
|
||||
# Get average rating from student feedback
|
||||
avg_rating = StudentFeedback.objects.filter(
|
||||
teacher=teacher
|
||||
).aggregate(Avg('rating'))['rating__avg'] or 0
|
||||
avg_rating_rounded = round(avg_rating)
|
||||
remaining_stars = 5 - avg_rating_rounded
|
||||
|
||||
# Get leave statistics
|
||||
pending_leaves = Staff_leave.objects.filter(staff=teacher, status=0).count()
|
||||
approved_leaves = Staff_leave.objects.filter(staff=teacher, status=1).count()
|
||||
rejected_leaves = Staff_leave.objects.filter(staff=teacher, status=2).count()
|
||||
|
||||
# Get recent leaves
|
||||
recent_leaves = Staff_leave.objects.filter(staff=teacher).order_by('-created_at')[:5]
|
||||
|
||||
# Get recent feedback
|
||||
recent_feedback = StudentFeedback.objects.filter(teacher=teacher).order_by('-created_at')[:5]
|
||||
|
||||
# Get notices
|
||||
notices = Notice.objects.all().order_by('-created_at')[:5]
|
||||
|
||||
# Get feedback types for institute feedback
|
||||
feedback_types = InstituteFeedback.FEEDBACK_TYPE_CHOICES
|
||||
|
||||
# Get institute feedback
|
||||
institute_feedback = StaffInstituteFeedback.objects.filter(staff=teacher).order_by('-created_at')[:5]
|
||||
|
||||
# Get all routines for the teacher (for the schedule)
|
||||
all_routines = Routine.objects.filter(
|
||||
teacher=teacher,
|
||||
is_active=True,
|
||||
semester_or_year__in=Student.objects.filter(status="Active")
|
||||
.values_list('current_semester', flat=True)
|
||||
.distinct()
|
||||
).order_by('start_time')
|
||||
|
||||
# Get all attendance records for quick access
|
||||
recent_attendance = (
|
||||
Attendance.objects.filter(teacher=teacher, date__gte=last_week)
|
||||
.select_related("routine")
|
||||
.order_by("-date")
|
||||
)
|
||||
|
||||
# Add present/total counts to each attendance record
|
||||
for attendance in recent_attendance:
|
||||
attendance.present_count = AttendanceRecord.objects.filter(
|
||||
attendance=attendance, student_attend=True
|
||||
).count()
|
||||
attendance.total_count = AttendanceRecord.objects.filter(
|
||||
attendance=attendance
|
||||
).count()
|
||||
|
||||
# Attendance Management Data
|
||||
routines = Routine.objects.filter(
|
||||
teacher=teacher,
|
||||
is_active=True
|
||||
).order_by('start_time')
|
||||
|
||||
context = {
|
||||
"teacher": teacher,
|
||||
"title": "Teacher Dashboard",
|
||||
"today_routines": processed_routines,
|
||||
"total_classes": total_classes,
|
||||
"completed_classes": completed_classes,
|
||||
"remaining_classes": remaining_classes,
|
||||
"total_students": total_students,
|
||||
"present_students": present_students,
|
||||
"absent_students": absent_students,
|
||||
"avg_rating": avg_rating,
|
||||
"avg_rating_rounded": avg_rating_rounded,
|
||||
"remaining_stars": remaining_stars,
|
||||
"pending_leaves": pending_leaves,
|
||||
"approved_leaves": approved_leaves,
|
||||
"rejected_leaves": rejected_leaves,
|
||||
"recent_leaves": recent_leaves,
|
||||
"recent_feedback": recent_feedback,
|
||||
"notices": notices,
|
||||
"feedback_types": feedback_types,
|
||||
"institute_feedback": institute_feedback,
|
||||
"all_routines": all_routines,
|
||||
"recent_attendance": recent_attendance,
|
||||
# Attendance Management Context
|
||||
"routines": routines,
|
||||
"today": today,
|
||||
}
|
||||
|
||||
return render(request, "teacher_dashboard.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def request_staff_leave(request):
|
||||
"""View to handle leave requests from staff"""
|
||||
if request.method == "POST":
|
||||
staff = request.user
|
||||
date = request.POST.get("date")
|
||||
message = request.POST.get("message")
|
||||
|
||||
try:
|
||||
# Convert date string to timezone-aware datetime
|
||||
leave_date = timezone.datetime.strptime(date, "%Y-%m-%d")
|
||||
leave_date = timezone.make_aware(leave_date)
|
||||
today = timezone.now()
|
||||
|
||||
if leave_date.date() < today.date():
|
||||
messages.error(request, "Leave date cannot be in the past.")
|
||||
return redirect("teacherDashboard")
|
||||
|
||||
# Check if leave already exists for this date
|
||||
existing_leave = Staff_leave.objects.filter(
|
||||
staff=staff, date=leave_date.date()
|
||||
).first()
|
||||
|
||||
if existing_leave:
|
||||
messages.error(
|
||||
request, "You have already requested leave for this date."
|
||||
)
|
||||
return redirect("teacherDashboard")
|
||||
|
||||
# Create leave request
|
||||
leave = Staff_leave.objects.create(
|
||||
staff=staff,
|
||||
date=leave_date.date(),
|
||||
message=message,
|
||||
status=0, # Pending status
|
||||
)
|
||||
messages.success(request, "Leave request submitted successfully.")
|
||||
except Exception as e:
|
||||
messages.error(request, f"Error submitting leave request: {str(e)}")
|
||||
|
||||
return redirect("teacherDashboard")
|
||||
|
||||
return redirect("teacherDashboard")
|
||||
|
||||
|
||||
@login_required
|
||||
def submit_staff_institute_feedback(request):
|
||||
"""View to handle feedback submission from staff to institute"""
|
||||
if request.method == "POST":
|
||||
staff = request.user
|
||||
feedback_type = request.POST.get("feedback_type")
|
||||
rating = request.POST.get("rating")
|
||||
feedback_text = request.POST.get("feedback_text")
|
||||
is_anonymous = request.POST.get("is_anonymous") == "on"
|
||||
|
||||
try:
|
||||
# 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("teacherDashboard")
|
||||
|
||||
# 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("teacherDashboard")
|
||||
|
||||
# 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",
|
||||
)
|
||||
|
||||
# Get the staff object
|
||||
teacher = Staff.objects.get(phone=staff.phone)
|
||||
|
||||
# Check if feedback already exists for this type
|
||||
existing_feedback = StaffInstituteFeedback.objects.filter(
|
||||
staff=teacher, 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
|
||||
feedback = StaffInstituteFeedback.objects.create(
|
||||
staff=teacher,
|
||||
institute=institute,
|
||||
feedback_type=feedback_type,
|
||||
rating=rating_value,
|
||||
feedback_text=feedback_text,
|
||||
is_anonymous=is_anonymous,
|
||||
)
|
||||
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("teacherDashboard")
|
||||
|
||||
return redirect("teacherDashboard")
|
||||
|
||||
|
||||
@login_required
|
||||
def save_attendance(request):
|
||||
"""View to save attendance data"""
|
||||
if request.method == "POST":
|
||||
teacher = Staff.objects.get(phone=request.user.phone)
|
||||
routine_id = request.POST.get("routine_id")
|
||||
date_str = request.POST.get("date")
|
||||
|
||||
# Convert class_status to boolean - "Conducted" is True, others are False
|
||||
class_status_str = request.POST.get("class_status", "True")
|
||||
class_status = class_status_str == "True"
|
||||
|
||||
teacher_attend = request.POST.get("teacher_attend") == "on"
|
||||
|
||||
try:
|
||||
# Validate routine - strict check to ensure teacher can only access their assigned routines
|
||||
routine = Routine.objects.get(
|
||||
id=routine_id,
|
||||
teacher=teacher, # Ensure this routine is assigned to this teacher
|
||||
is_active=True, # Only allow active routines
|
||||
)
|
||||
|
||||
# Validate date
|
||||
attendance_date = timezone.datetime.strptime(date_str, "%Y-%m-%d").date()
|
||||
|
||||
# Get or create attendance record
|
||||
attendance, created = Attendance.objects.get_or_create(
|
||||
routine=routine,
|
||||
date=attendance_date,
|
||||
defaults={
|
||||
"teacher": teacher,
|
||||
"class_status": class_status,
|
||||
"teacher_attend": teacher_attend,
|
||||
},
|
||||
)
|
||||
|
||||
if not created:
|
||||
# Update existing attendance
|
||||
attendance.class_status = class_status
|
||||
attendance.teacher_attend = teacher_attend
|
||||
attendance.save()
|
||||
|
||||
# Process student attendance
|
||||
for key, value in request.POST.items():
|
||||
if key.startswith("student_"):
|
||||
student_id = key.replace("student_", "")
|
||||
student_attend = value == "on"
|
||||
|
||||
try:
|
||||
# Convert student_id to integer to ensure proper lookup
|
||||
student_id_int = int(student_id)
|
||||
student = Student.objects.get(id=student_id_int)
|
||||
|
||||
# Get or create attendance record for this student
|
||||
record, _ = AttendanceRecord.objects.get_or_create(
|
||||
attendance=attendance,
|
||||
student=student,
|
||||
defaults={"student_attend": student_attend},
|
||||
)
|
||||
|
||||
# Update if record already exists
|
||||
if record.student_attend != student_attend:
|
||||
record.student_attend = student_attend
|
||||
record.save()
|
||||
|
||||
except Student.DoesNotExist:
|
||||
continue
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f"Attendance for {attendance_date.strftime('%d %b, %Y')} saved successfully.",
|
||||
)
|
||||
|
||||
except Routine.DoesNotExist:
|
||||
messages.error(
|
||||
request,
|
||||
"You do not have permission to take attendance for this class. Only teachers assigned to this class can manage its attendance.",
|
||||
)
|
||||
except ValueError:
|
||||
messages.error(request, "Invalid date format.")
|
||||
except Exception as e:
|
||||
messages.error(request, f"Error saving attendance: {str(e)}")
|
||||
|
||||
# Redirect to teacher dashboard after saving attendance
|
||||
return redirect("teacherDashboard")
|
||||
|
||||
return redirect("teacherDashboard")
|
||||
|
||||
|
||||
@login_required
|
||||
@require_GET
|
||||
def get_subject_schedule(request):
|
||||
"""View to get schedule for a subject"""
|
||||
teacher = Staff.objects.get(phone=request.user.phone)
|
||||
subject_id = request.GET.get("subject_id")
|
||||
|
||||
if not subject_id:
|
||||
return JsonResponse({"success": False, "message": "Subject ID is required"})
|
||||
|
||||
try:
|
||||
routines = Routine.objects.filter(
|
||||
teacher=teacher, subject_id=subject_id, is_active=True
|
||||
).values("id", "start_time", "end_time", "semester_or_year")
|
||||
|
||||
return JsonResponse({"success": True, "schedule": list(routines)})
|
||||
except Exception as e:
|
||||
return JsonResponse({"success": False, "message": str(e)})
|
||||
|
||||
|
||||
@login_required
|
||||
def teacher_update_profile_picture(request):
|
||||
"""View to handle profile picture update for teachers"""
|
||||
if request.method == "POST":
|
||||
teacher = Staff.objects.get(phone=request.user.phone)
|
||||
profile_picture = request.FILES.get("profile_picture")
|
||||
|
||||
if profile_picture:
|
||||
try:
|
||||
# Update the teacher's profile picture
|
||||
teacher.image = profile_picture
|
||||
teacher.save()
|
||||
messages.success(request, "Profile picture updated successfully.")
|
||||
except Exception as e:
|
||||
messages.error(request, f"Error updating profile picture: {str(e)}")
|
||||
else:
|
||||
messages.error(request, "No profile picture uploaded.")
|
||||
|
||||
return redirect("teacherDashboard")
|
||||
|
||||
return redirect("teacherDashboard")
|
||||
|
||||
|
||||
@login_required
|
||||
def teacher_change_password(request):
|
||||
"""View to handle password change for teachers"""
|
||||
if request.method == "POST":
|
||||
current_password = request.POST.get("current_password")
|
||||
new_password = request.POST.get("new_password")
|
||||
confirm_password = request.POST.get("confirm_password")
|
||||
|
||||
# Get the current user
|
||||
user = request.user
|
||||
|
||||
# Validate inputs
|
||||
if not current_password or not new_password or not confirm_password:
|
||||
messages.error(request, "All password fields are required.")
|
||||
return redirect("teacherDashboard")
|
||||
|
||||
# Check if current password is correct
|
||||
if not check_password(current_password, user.password):
|
||||
messages.error(request, "Current password is incorrect.")
|
||||
return redirect("teacherDashboard")
|
||||
|
||||
# Check if new passwords match
|
||||
if new_password != confirm_password:
|
||||
messages.error(request, "New passwords do not match.")
|
||||
return redirect("teacherDashboard")
|
||||
|
||||
# Check if new password is strong enough (add more criteria as needed)
|
||||
if len(new_password) < 8:
|
||||
messages.error(request, "New password must be at least 8 characters long.")
|
||||
return redirect("teacherDashboard")
|
||||
|
||||
try:
|
||||
# Update the password
|
||||
user.password = make_password(new_password)
|
||||
user.save()
|
||||
messages.success(
|
||||
request,
|
||||
"Password changed successfully. Please log in again with your new password.",
|
||||
)
|
||||
return redirect(
|
||||
"login"
|
||||
) # Redirect to login page to log in with new password
|
||||
except Exception as e:
|
||||
messages.error(request, f"Error changing password: {str(e)}")
|
||||
|
||||
return redirect("teacherDashboard")
|
||||
|
||||
return redirect("teacherDashboard")
|
||||
|
||||
|
||||
@login_required
|
||||
def get_attendance_form(request):
|
||||
"""View to handle AJAX request for getting attendance form"""
|
||||
routine_id = request.GET.get('routine_id')
|
||||
date_str = request.GET.get('date')
|
||||
|
||||
if not routine_id or not date_str:
|
||||
return JsonResponse({
|
||||
'error': 'Missing required parameters'
|
||||
})
|
||||
|
||||
try:
|
||||
routine = Routine.objects.get(id=routine_id, teacher=request.user)
|
||||
date = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
|
||||
# Get existing attendance data if any
|
||||
attendance_data = {
|
||||
'teacher_attend': False,
|
||||
'class_status': True,
|
||||
'records': {}
|
||||
}
|
||||
|
||||
try:
|
||||
# Get attendance record with all related data in one query
|
||||
attendance = Attendance.objects.select_related('routine').get(
|
||||
routine=routine,
|
||||
date=date
|
||||
)
|
||||
|
||||
# Get all attendance records for this attendance in one query
|
||||
records = AttendanceRecord.objects.filter(attendance=attendance).select_related('student')
|
||||
|
||||
attendance_data = {
|
||||
'id': attendance.id,
|
||||
'date': attendance.date.strftime('%Y-%m-%d'),
|
||||
'teacher_attend': attendance.teacher_attend,
|
||||
'class_status': attendance.class_status,
|
||||
'records': {
|
||||
str(record.student.id): record.student_attend
|
||||
for record in records
|
||||
}
|
||||
}
|
||||
except Attendance.DoesNotExist:
|
||||
# No existing attendance record found - return default values
|
||||
pass
|
||||
|
||||
return JsonResponse(attendance_data)
|
||||
|
||||
except Routine.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'error': 'Invalid routine selected'
|
||||
}, status=404)
|
||||
except ValueError:
|
||||
return JsonResponse({
|
||||
'error': 'Invalid date format'
|
||||
}, status=400)
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'error': str(e)
|
||||
}, status=500)
|
||||
|
||||
|
||||
@login_required
|
||||
def get_students(request):
|
||||
"""View to handle AJAX request for getting students list"""
|
||||
from django.http import JsonResponse
|
||||
|
||||
routine_id = request.GET.get('routine_id')
|
||||
|
||||
if not routine_id:
|
||||
return JsonResponse({'error': 'Missing routine_id parameter'}, status=400)
|
||||
|
||||
try:
|
||||
routine = Routine.objects.get(id=routine_id, teacher=request.user)
|
||||
|
||||
# Get students for this routine
|
||||
students = Student.objects.filter(
|
||||
course=routine.subject.course,
|
||||
current_semester=routine.semester_or_year
|
||||
).order_by('name')
|
||||
|
||||
# If no students found, try a more flexible approach
|
||||
if not students.exists():
|
||||
students = Student.objects.filter(
|
||||
course=routine.subject.course
|
||||
).order_by('name')
|
||||
|
||||
# Format student data for JSON response
|
||||
student_data = [{
|
||||
'id': student.id,
|
||||
'name': student.name
|
||||
} for student in students]
|
||||
|
||||
return JsonResponse({
|
||||
'students': student_data
|
||||
})
|
||||
|
||||
except Routine.DoesNotExist:
|
||||
return JsonResponse({'error': 'Invalid routine selected'}, status=404)
|
||||
except Exception as e:
|
||||
return JsonResponse({'error': str(e)}, status=500)
|
||||
@@ -0,0 +1,511 @@
|
||||
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
|
||||
from datetime import timedelta
|
||||
from django.views.decorators.http import require_http_methods, require_GET
|
||||
from .models import (
|
||||
Student,
|
||||
Course,
|
||||
CourseTracking,
|
||||
Student_Leave,
|
||||
Staff,
|
||||
Attendance,
|
||||
AttendanceRecord,
|
||||
StudentFeedback,
|
||||
Routine,
|
||||
Notice,
|
||||
Institute,
|
||||
InstituteFeedback,
|
||||
Subject,
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def studentDashboard(request):
|
||||
"""View to display the student dashboard with their personal information and progress"""
|
||||
student = Student.objects.get(phone=request.user.phone) # Get the Student object
|
||||
today = timezone.now().date()
|
||||
last_week = today - timedelta(days=7)
|
||||
last_month = today - timedelta(days=30)
|
||||
|
||||
# Get all teachers for feedback form
|
||||
teachers = Staff.objects.filter(
|
||||
Q(designation__icontains="Teacher")
|
||||
| Q(designation__icontains="Staff")
|
||||
| Q(designation__icontains="Principal")
|
||||
| Q(designation__isnull=True)
|
||||
| Q(designation="")
|
||||
).order_by("name")
|
||||
|
||||
# Get feedback types for institute feedback
|
||||
feedback_types = InstituteFeedback.FEEDBACK_TYPE_CHOICES
|
||||
|
||||
# Course Progress and Tracking
|
||||
course_trackings = CourseTracking.objects.filter(student=student).select_related(
|
||||
"course"
|
||||
)
|
||||
total_courses = course_trackings.count()
|
||||
completed_courses = course_trackings.filter(progress_status="Completed").count()
|
||||
in_progress_courses = course_trackings.filter(progress_status="In Progress").count()
|
||||
dropped_courses = course_trackings.filter(progress_status="Dropped").count()
|
||||
|
||||
# Calculate overall completion percentage
|
||||
overall_completion = 0
|
||||
if total_courses > 0:
|
||||
total_percentage = sum(
|
||||
tracking.completion_percentage for tracking in course_trackings
|
||||
)
|
||||
overall_completion = total_percentage / total_courses
|
||||
|
||||
# Course Summary
|
||||
course_summary = {
|
||||
"total_courses": total_courses,
|
||||
"completed_courses": completed_courses,
|
||||
"ongoing_courses": in_progress_courses,
|
||||
"completion_percentage": overall_completion,
|
||||
}
|
||||
|
||||
# Course Details with enhanced information
|
||||
course_details = []
|
||||
for tracking in course_trackings:
|
||||
# Calculate expected end date based on course duration
|
||||
if tracking.start_date:
|
||||
if tracking.course.duration_type == "Year":
|
||||
duration_days = tracking.course.duration * 365 # Years to days
|
||||
else: # Semester based
|
||||
duration_days = (
|
||||
tracking.course.duration * 365
|
||||
) # Use full years for total duration
|
||||
|
||||
expected_end_date = tracking.start_date + timedelta(days=duration_days)
|
||||
else:
|
||||
expected_end_date = None
|
||||
|
||||
course_details.append(
|
||||
{
|
||||
"name": tracking.course.name,
|
||||
"start_date": tracking.start_date,
|
||||
"expected_end_date": 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,
|
||||
"book": tracking.course.book
|
||||
if hasattr(tracking.course, "book")
|
||||
else None,
|
||||
"topics": [], # This will be populated if you have topic tracking
|
||||
}
|
||||
)
|
||||
|
||||
# Get current subjects based on current semester
|
||||
current_subjects = student.get_current_subjects()
|
||||
|
||||
# Attendance Statistics with enhanced information
|
||||
attendance_records = AttendanceRecord.objects.filter(student=student)
|
||||
total_classes = attendance_records.count()
|
||||
classes_attended = attendance_records.filter(student_attend=True).count()
|
||||
attendance_percentage = (
|
||||
(classes_attended / total_classes * 100) if total_classes > 0 else 0
|
||||
)
|
||||
|
||||
# Calculate attendance streaks
|
||||
current_streak = 0
|
||||
longest_streak = 0
|
||||
temp_streak = 0
|
||||
last_attendance_date = None
|
||||
|
||||
for record in attendance_records.order_by("-attendance__date"):
|
||||
if record.student_attend:
|
||||
if last_attendance_date is None:
|
||||
last_attendance_date = record.attendance.date
|
||||
current_streak = 1
|
||||
temp_streak = 1
|
||||
elif (last_attendance_date - record.attendance.date).days == 1:
|
||||
temp_streak += 1
|
||||
if temp_streak > longest_streak:
|
||||
longest_streak = temp_streak
|
||||
if record.attendance.date == today - timedelta(days=current_streak):
|
||||
current_streak += 1
|
||||
else:
|
||||
temp_streak = 1
|
||||
else:
|
||||
temp_streak = 0
|
||||
if record.attendance.date == today:
|
||||
current_streak = 0
|
||||
|
||||
# Attendance Summary
|
||||
attendance_summary = {
|
||||
"total_classes": total_classes,
|
||||
"classes_attended": classes_attended,
|
||||
"attendance_percentage": attendance_percentage,
|
||||
"current_streak": current_streak,
|
||||
"longest_streak": longest_streak,
|
||||
}
|
||||
|
||||
# Get attendance records with related information
|
||||
attendance_records = attendance_records.select_related(
|
||||
"attendance", "attendance__routine", "attendance__teacher"
|
||||
).order_by("-attendance__date")
|
||||
|
||||
# Get recent attendance (last 7 days)
|
||||
recent_attendance = attendance_records.filter(
|
||||
attendance__date__gte=last_week
|
||||
).order_by("-attendance__date")
|
||||
|
||||
# Get feedback
|
||||
recent_feedback = student.feedbacks.all().order_by("-created_at")[:5]
|
||||
institute_feedback = student.institute_feedbacks.all().order_by("-created_at")[:5]
|
||||
|
||||
# Get notices
|
||||
notices = Notice.objects.all().order_by("-created_at")
|
||||
|
||||
# Get student leaves with status information
|
||||
student_leaves = Student_Leave.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
|
||||
try:
|
||||
# Get routines that match the student's course and current semester directly
|
||||
current_semester_routines = Routine.objects.filter(
|
||||
course=student.course,
|
||||
semester_or_year=student.current_semester,
|
||||
is_active=True,
|
||||
)
|
||||
except Exception as e:
|
||||
current_semester_routines = []
|
||||
|
||||
# Check if student has any routines for any semester
|
||||
has_routines = False
|
||||
try:
|
||||
if student.course:
|
||||
has_routines = Routine.objects.filter(course=student.course).exists()
|
||||
except Exception as e:
|
||||
has_routines = False
|
||||
|
||||
context = {
|
||||
"student": student,
|
||||
"title": "Student Dashboard",
|
||||
"teachers": teachers,
|
||||
"feedback_types": feedback_types,
|
||||
# Course Progress
|
||||
"total_courses": total_courses,
|
||||
"completed_courses": completed_courses,
|
||||
"in_progress_courses": in_progress_courses,
|
||||
"dropped_courses": dropped_courses,
|
||||
"overall_completion": round(overall_completion, 1),
|
||||
"course_details": course_details,
|
||||
"course_summary": course_summary,
|
||||
# Current Subjects
|
||||
"current_subjects": current_subjects,
|
||||
# Attendance
|
||||
"attendance_summary": attendance_summary,
|
||||
"attendance_records": attendance_records,
|
||||
"recent_attendance": recent_attendance,
|
||||
# Feedback
|
||||
"recent_feedback": recent_feedback,
|
||||
"institute_feedback": institute_feedback,
|
||||
# Notices
|
||||
"notices": notices,
|
||||
# Leaves
|
||||
"student_leaves": student_leaves,
|
||||
"pending_leaves": pending_leaves,
|
||||
"approved_leaves": approved_leaves,
|
||||
"rejected_leaves": rejected_leaves,
|
||||
# Routines
|
||||
"current_semester_routines": current_semester_routines,
|
||||
"has_routines": has_routines,
|
||||
# Current date for display
|
||||
"today": today,
|
||||
}
|
||||
|
||||
return render(request, "student_dashboard.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def request_leave(request):
|
||||
if request.method == "POST":
|
||||
student = request.user
|
||||
date = request.POST.get("date")
|
||||
message = request.POST.get("message")
|
||||
|
||||
try:
|
||||
# Convert date string to timezone-aware datetime
|
||||
leave_date = timezone.datetime.strptime(date, "%Y-%m-%d")
|
||||
leave_date = timezone.make_aware(leave_date)
|
||||
today = timezone.now()
|
||||
|
||||
if leave_date.date() < today.date():
|
||||
messages.error(request, "Leave date cannot be in the past.")
|
||||
return redirect("studentDashboard")
|
||||
|
||||
# Check if leave already exists for this date
|
||||
existing_leave = Student_Leave.objects.filter(
|
||||
student=student, date=leave_date.date()
|
||||
).first()
|
||||
|
||||
if existing_leave:
|
||||
messages.error(
|
||||
request, "You have already requested leave for this date."
|
||||
)
|
||||
return redirect("studentDashboard")
|
||||
|
||||
# Create leave request
|
||||
leave = Student_Leave.objects.create(
|
||||
student=student,
|
||||
date=leave_date.date(),
|
||||
message=message,
|
||||
status=0, # Pending status
|
||||
)
|
||||
messages.success(request, "Leave request submitted successfully.")
|
||||
except Exception as e:
|
||||
messages.error(request, f"Error submitting leave request: {str(e)}")
|
||||
|
||||
return redirect("studentDashboard")
|
||||
|
||||
return redirect("studentDashboard")
|
||||
|
||||
|
||||
@login_required
|
||||
def submit_feedback(request):
|
||||
if request.method == "POST":
|
||||
try:
|
||||
feedback_type = request.POST.get("feedback_type")
|
||||
rating = float(request.POST.get("rating", 0))
|
||||
feedback_text = request.POST.get("feedback_text", "").strip()
|
||||
|
||||
# Validate rating range (1 to 5)
|
||||
if not (1 <= rating <= 5):
|
||||
return JsonResponse(
|
||||
{"success": False, "message": "Rating must be between 1 and 5"}
|
||||
)
|
||||
|
||||
if not feedback_text:
|
||||
return JsonResponse(
|
||||
{"success": False, "message": "Please provide feedback text"}
|
||||
)
|
||||
|
||||
if feedback_type == "teacher":
|
||||
teacher_id = request.POST.get("teacher_id")
|
||||
if not teacher_id:
|
||||
return JsonResponse(
|
||||
{"success": False, "message": "Please select a teacher"}
|
||||
)
|
||||
|
||||
try:
|
||||
teacher = Staff.objects.get(id=teacher_id)
|
||||
# Check if feedback already exists
|
||||
existing_feedback = StudentFeedback.objects.filter(
|
||||
student=request.user, teacher=teacher
|
||||
).first()
|
||||
|
||||
if existing_feedback:
|
||||
# Update existing feedback
|
||||
existing_feedback.rating = rating
|
||||
existing_feedback.feedback_text = feedback_text
|
||||
existing_feedback.save()
|
||||
else:
|
||||
# Create new feedback
|
||||
StudentFeedback.objects.create(
|
||||
student=request.user,
|
||||
teacher=teacher,
|
||||
rating=rating,
|
||||
feedback_text=feedback_text,
|
||||
)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"success": True,
|
||||
"message": "Teacher feedback submitted successfully",
|
||||
}
|
||||
)
|
||||
|
||||
except Staff.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"success": False, "message": "Selected teacher does not exist"}
|
||||
)
|
||||
|
||||
elif feedback_type == "institute":
|
||||
category = request.POST.get("category")
|
||||
is_anonymous = request.POST.get("is_anonymous") == "on"
|
||||
|
||||
if not category:
|
||||
return JsonResponse(
|
||||
{
|
||||
"success": False,
|
||||
"message": "Please select a feedback category",
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
institute = Institute.objects.first() # Assuming single institute
|
||||
# Check if feedback already exists for this category - use new structure
|
||||
existing_feedback = InstituteFeedback.objects.filter(
|
||||
user_type="student",
|
||||
content_object_id=request.user.id,
|
||||
institute=institute,
|
||||
feedback_type=category,
|
||||
).first()
|
||||
|
||||
if existing_feedback:
|
||||
# Update existing feedback
|
||||
existing_feedback.rating = rating
|
||||
existing_feedback.feedback_text = feedback_text
|
||||
existing_feedback.is_anonymous = is_anonymous
|
||||
existing_feedback.save()
|
||||
else:
|
||||
# Create new feedback - use new structure
|
||||
InstituteFeedback.objects.create(
|
||||
user_type="student",
|
||||
content_object_id=request.user.id,
|
||||
user=request.user,
|
||||
institute=institute,
|
||||
feedback_type=category,
|
||||
rating=rating,
|
||||
feedback_text=feedback_text,
|
||||
is_anonymous=is_anonymous,
|
||||
)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"success": True,
|
||||
"message": "Institute feedback submitted successfully",
|
||||
}
|
||||
)
|
||||
|
||||
except Institute.DoesNotExist:
|
||||
return JsonResponse(
|
||||
{"success": False, "message": "Institute not found"}
|
||||
)
|
||||
|
||||
else:
|
||||
return JsonResponse(
|
||||
{"success": False, "message": "Invalid feedback type"}
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
return JsonResponse({"success": False, "message": "Invalid rating value"})
|
||||
except Exception as e:
|
||||
return JsonResponse(
|
||||
{"success": False, "message": f"An error occurred: {str(e)}"}
|
||||
)
|
||||
|
||||
return JsonResponse({"success": False, "message": "Invalid request method"})
|
||||
|
||||
|
||||
@login_required
|
||||
def submit_institute_feedback(request):
|
||||
if request.method == "POST":
|
||||
student = request.user
|
||||
feedback_type = request.POST.get("feedback_type")
|
||||
rating = request.POST.get("rating")
|
||||
feedback_text = request.POST.get("feedback_text")
|
||||
is_anonymous = request.POST.get("is_anonymous") == "on"
|
||||
|
||||
try:
|
||||
# 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("studentDashboard")
|
||||
|
||||
# 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("studentDashboard")
|
||||
|
||||
# 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 - using new structure
|
||||
existing_feedback = InstituteFeedback.objects.filter(
|
||||
user_type="student",
|
||||
content_object_id=student.id,
|
||||
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 with new structure
|
||||
feedback = InstituteFeedback.objects.create(
|
||||
user_type="student",
|
||||
content_object_id=student.id,
|
||||
user=student,
|
||||
institute=institute,
|
||||
feedback_type=feedback_type,
|
||||
rating=rating_value,
|
||||
feedback_text=feedback_text,
|
||||
is_anonymous=is_anonymous,
|
||||
)
|
||||
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("studentDashboard")
|
||||
|
||||
return redirect("studentDashboard")
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def get_subject_files(request, subject_id):
|
||||
"""View to get syllabus PDF for a subject"""
|
||||
try:
|
||||
subject = Subject.objects.get(id=subject_id)
|
||||
pdf_url = subject.get_pdf_url()
|
||||
|
||||
if not pdf_url:
|
||||
return JsonResponse(
|
||||
{"success": False, "message": "No syllabus available for this subject"}
|
||||
)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"success": True,
|
||||
"files": [
|
||||
{
|
||||
"title": f"{subject.name} Syllabus",
|
||||
"description": f"Syllabus PDF for {subject.name}",
|
||||
"file_url": pdf_url,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
except Subject.DoesNotExist:
|
||||
return JsonResponse({"success": False, "message": "Subject not found"})
|
||||
except Exception as e:
|
||||
return JsonResponse(
|
||||
{"success": False, "message": f"Error retrieving syllabus: {str(e)}"}
|
||||
)
|
||||
@@ -8,4 +8,31 @@ def get_range(value):
|
||||
Returns a range of numbers from 0 to value-1
|
||||
Used for creating star ratings
|
||||
"""
|
||||
return range(value)
|
||||
try:
|
||||
return range(int(value))
|
||||
except (ValueError, TypeError):
|
||||
return range(0)
|
||||
|
||||
@register.filter
|
||||
def multiply(value, arg):
|
||||
"""Multiply the value by the argument"""
|
||||
try:
|
||||
return int(value) * int(arg)
|
||||
except (ValueError, TypeError):
|
||||
return value
|
||||
|
||||
@register.filter
|
||||
def get_item(dictionary, key):
|
||||
"""
|
||||
Gets a value from a dictionary by key.
|
||||
Usage in template: {{ my_dict|get_item:key_variable }}
|
||||
"""
|
||||
if dictionary is None:
|
||||
return None
|
||||
|
||||
# Try with the key as is (likely integer ID)
|
||||
if key in dictionary:
|
||||
return dictionary.get(key)
|
||||
|
||||
# Try with the key as string
|
||||
return dictionary.get(str(key), None)
|
||||
+56
-5
@@ -1,8 +1,59 @@
|
||||
from django.urls import path
|
||||
from app import views
|
||||
from . import views
|
||||
from django.contrib import admin
|
||||
from . import auth
|
||||
from . import staffviews
|
||||
|
||||
urlpatterns = [
|
||||
path('dashboard/', views.dashboard, name='dashboard'),
|
||||
path('student-dashboard/', views.studentDashboard, name='studentDashboard'),
|
||||
path('teacher-dashboard/', views.teacherDashboard, name='teacherDashboard'),
|
||||
]
|
||||
path("dashboard/", views.dashboard, name="dashboard"),
|
||||
path("student-dashboard/", views.studentDashboard, name="studentDashboard"),
|
||||
path("teacher-dashboard/", views.teacherDashboard, name="teacherDashboard"),
|
||||
path("request-leave/", views.request_leave, name="request_leave"),
|
||||
path("request-staff-leave/", views.request_staff_leave, name="request_staff_leave"),
|
||||
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",
|
||||
),
|
||||
path("save-fcm-token/", views.saveFCMToken, name="saveFCMToken"),
|
||||
path("firebase-messaging-sw.js", views.showFirebaseJS, name="showFirebaseJS"),
|
||||
path("update-profile/", views.update_profile, name="update_profile"),
|
||||
path("change-password/", views.change_password, name="change_password"),
|
||||
path(
|
||||
"subject/<int:subject_id>/files/",
|
||||
views.get_subject_files,
|
||||
name="get_subject_files",
|
||||
),
|
||||
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('save-attendance/', views.save_attendance, name='save_attendance'),
|
||||
path('get-subject-schedule/', views.get_subject_schedule, name='get_subject_schedule'),
|
||||
path('get-attendance-form/', staffviews.get_attendance_form, name='get_attendance_form'),
|
||||
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'),
|
||||
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'),
|
||||
path('add-notice/', views.add_notice, name='add_notice'),
|
||||
path('delete-notice/<int:notice_id>/', views.delete_notice, name='delete_notice'),
|
||||
|
||||
# Password reset paths
|
||||
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'),
|
||||
path('password-reset/set/', auth.set_new_password, name='set_new_password'),
|
||||
path('get-students/', staffviews.get_students, name='get_students'),
|
||||
]
|
||||
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
import random
|
||||
import string
|
||||
from django.core.cache import cache
|
||||
import requests
|
||||
from firebase_admin import messaging, initialize_app, get_app, credentials
|
||||
import os
|
||||
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
|
||||
|
||||
# OTP expiration time in seconds (5 minutes)
|
||||
OTP_EXPIRY = 300
|
||||
|
||||
|
||||
def generate_otp(length=6):
|
||||
"""Generate a numeric OTP of specified length"""
|
||||
digits = string.digits
|
||||
return "".join(random.choice(digits) for _ in range(length))
|
||||
|
||||
|
||||
def store_otp(phone, otp):
|
||||
"""Store OTP in cache with phone as key"""
|
||||
cache_key = f"otp_{phone}"
|
||||
cache.set(cache_key, otp, OTP_EXPIRY)
|
||||
return True
|
||||
|
||||
|
||||
def verify_otp(phone, otp):
|
||||
"""Verify if the provided OTP matches the stored OTP for the phone"""
|
||||
cache_key = f"otp_{phone}"
|
||||
stored_otp = cache.get(cache_key)
|
||||
|
||||
if stored_otp and stored_otp == otp:
|
||||
# Delete the OTP after successful verification
|
||||
cache.delete(cache_key)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def send_otp_sms(phone, otp):
|
||||
"""Send OTP via SMS using a service like Twilio, MSG91, etc."""
|
||||
try:
|
||||
# This is a placeholder. You need to replace with actual SMS API implementation
|
||||
# Example using a generic SMS API
|
||||
# api_url = "https://api.example.com/send"
|
||||
# payload = {
|
||||
# "apikey": settings.SMS_API_KEY,
|
||||
# "to": phone,
|
||||
# "message": f"Your OTP for SMS login is: {otp}. Valid for 5 minutes."
|
||||
# }
|
||||
# response = requests.post(api_url, json=payload)
|
||||
# return response.status_code == 200
|
||||
|
||||
print(f"OTP for {phone}: {otp}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error sending OTP: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def send_push_notification(title, message, tokens):
|
||||
"""
|
||||
Send push notification using Firebase Cloud Messaging
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
app = get_app()
|
||||
except ValueError:
|
||||
# Initialize Firebase Admin SDK if not already initialized
|
||||
cred_path = os.path.join(
|
||||
os.path.dirname(os.path.dirname(__file__)),
|
||||
"firebase-key.json",
|
||||
)
|
||||
if not os.path.exists(cred_path):
|
||||
print(f"Firebase credentials file not found at {cred_path}")
|
||||
return
|
||||
|
||||
cred = credentials.Certificate(cred_path)
|
||||
app = initialize_app(cred)
|
||||
|
||||
if not tokens:
|
||||
print("No FCM tokens available to send notifications")
|
||||
return
|
||||
|
||||
# Create message
|
||||
message = messaging.MulticastMessage(
|
||||
notification=messaging.Notification(
|
||||
title=title,
|
||||
body=message,
|
||||
),
|
||||
tokens=list(tokens),
|
||||
)
|
||||
|
||||
# Send message
|
||||
response = messaging.send_multicast(message)
|
||||
|
||||
if response.failure_count > 0:
|
||||
failures = []
|
||||
for idx, resp in enumerate(response.responses):
|
||||
if not resp.success:
|
||||
failures.append(
|
||||
f"Failed to send message to token {tokens[idx]}: {resp.exception}"
|
||||
)
|
||||
print("\n".join(failures))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error sending notification: {e}")
|
||||
# Don't raise the exception to prevent save from failing
|
||||
pass
|
||||
|
||||
|
||||
def send_password_reset_email(user, request, email):
|
||||
"""
|
||||
Send password reset code to user's email using HTML template
|
||||
"""
|
||||
try:
|
||||
# Generate a reset code (OTP)
|
||||
reset_code = generate_otp(length=6)
|
||||
|
||||
# Store the reset code in cache with user's email as key
|
||||
cache_key = f"reset_code_{email}"
|
||||
cache.set(cache_key, reset_code, OTP_EXPIRY) # 5 minutes expiry
|
||||
|
||||
# Build email context
|
||||
context = {
|
||||
"user": user,
|
||||
"domain": request.META.get("HTTP_HOST", "localhost:8000"),
|
||||
"protocol": "https" if request.is_secure() else "http",
|
||||
"reset_code": reset_code,
|
||||
}
|
||||
|
||||
# Render email content using template
|
||||
subject = render_to_string("login/email_templates/password_reset_subject.txt")
|
||||
message = render_to_string(
|
||||
"login/email_templates/password_reset_email.html", context
|
||||
)
|
||||
|
||||
# Send email using console backend
|
||||
send_mail(
|
||||
subject=subject,
|
||||
message=message,
|
||||
from_email="noreply@studentmanagement.com",
|
||||
recipient_list=[email],
|
||||
html_message=message,
|
||||
fail_silently=False,
|
||||
)
|
||||
print(f"\nPassword reset code sent to {email}")
|
||||
print(f"Reset Code: {reset_code}\n")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Failed to send email: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def verify_reset_code(email, code):
|
||||
"""
|
||||
Verify if the provided reset code matches the stored code for the email
|
||||
"""
|
||||
cache_key = f"reset_code_{email}"
|
||||
stored_code = cache.get(cache_key)
|
||||
|
||||
if stored_code and stored_code == code:
|
||||
# Delete the code after successful verification
|
||||
cache.delete(cache_key)
|
||||
return True
|
||||
return False
|
||||
|
||||
+465
-199
@@ -16,10 +16,47 @@ from .models import (
|
||||
AttendanceRecord,
|
||||
StudentFeedback,
|
||||
Routine,
|
||||
StudentRoutine,
|
||||
Staff,
|
||||
Notice,
|
||||
Institute,
|
||||
InstituteFeedback,
|
||||
Subject,
|
||||
Batch,
|
||||
)
|
||||
from app.admin import custom_admin_site
|
||||
from django.views.decorators.http import require_http_methods, require_GET
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.files.base import ContentFile
|
||||
import json
|
||||
import os
|
||||
from django.db.models.base import transaction
|
||||
from .utils import generate_otp, store_otp, verify_otp, send_otp_sms
|
||||
from django.conf import settings
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Import views from studentviews.py
|
||||
from .studentviews import (
|
||||
studentDashboard,
|
||||
request_leave,
|
||||
submit_feedback,
|
||||
submit_institute_feedback,
|
||||
get_subject_files,
|
||||
)
|
||||
|
||||
# Import views from staffviews.py
|
||||
from .staffviews import (
|
||||
teacherDashboard,
|
||||
request_staff_leave,
|
||||
submit_staff_institute_feedback,
|
||||
save_attendance,
|
||||
get_subject_schedule,
|
||||
teacher_update_profile_picture,
|
||||
teacher_change_password,
|
||||
)
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
# Create your views here.
|
||||
|
||||
|
||||
@@ -33,48 +70,51 @@ def saveFCMToken(request, token):
|
||||
|
||||
|
||||
def showFirebaseJS(request):
|
||||
data = """
|
||||
data = f"""
|
||||
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');
|
||||
|
||||
firebase.initializeApp({
|
||||
apiKey: "",
|
||||
authDomain: "",
|
||||
projectId: "",
|
||||
storageBucket: "",
|
||||
messagingSenderId: "",
|
||||
appId: "",
|
||||
measurementId: ""
|
||||
});
|
||||
firebase.initializeApp({{
|
||||
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')}"
|
||||
}});
|
||||
|
||||
const messaging = firebase.messaging();
|
||||
|
||||
messaging.onBackgroundMessage((payload) => {
|
||||
|
||||
messaging.onBackgroundMessage((payload) => {{
|
||||
const notificationTitle = payload.notification.title;
|
||||
const notificationOptions = {
|
||||
const notificationOptions = {{
|
||||
body: payload.notification.body,
|
||||
icon: payload.notification.icon
|
||||
};
|
||||
}};
|
||||
|
||||
return self.registration.showNotification(notificationTitle, notificationOptions);
|
||||
});
|
||||
}});
|
||||
"""
|
||||
return HttpResponse(data, content_type="text/javascript")
|
||||
|
||||
|
||||
@login_required
|
||||
def dashboard(request):
|
||||
"""Main dashboard view that redirects to appropriate dashboard based on user role"""
|
||||
user = request.user
|
||||
user_groups = user.groups.values_list("name", flat=True)
|
||||
|
||||
# Redirect based on user role
|
||||
if "Student" in user_groups:
|
||||
return redirect("studentDashboard")
|
||||
elif "Teacher" in user_groups:
|
||||
return redirect("teacherDashboard")
|
||||
|
||||
# Get current date and date ranges
|
||||
today = timezone.now().date()
|
||||
last_week = today - timedelta(days=7)
|
||||
last_month = today - timedelta(days=30)
|
||||
last_week = timezone.now() - timedelta(days=7)
|
||||
last_month = timezone.now() - timedelta(days=30)
|
||||
|
||||
# Student Statistics
|
||||
total_students = Student.objects.count()
|
||||
@@ -82,6 +122,13 @@ def dashboard(request):
|
||||
completed_students = Student.objects.filter(status="Completed").count()
|
||||
on_leave_students = Student.objects.filter(status="Leave").count()
|
||||
|
||||
# Staff Statistics
|
||||
total_staff = Staff.objects.count()
|
||||
active_staff = Staff.objects.filter(is_active=True).count()
|
||||
staff_on_leave = (
|
||||
Staff_leave.objects.filter(status=1).values("staff").distinct().count()
|
||||
)
|
||||
|
||||
# Course Statistics
|
||||
total_courses = Course.objects.count()
|
||||
active_courses = (
|
||||
@@ -110,15 +157,123 @@ def dashboard(request):
|
||||
created_at__gte=last_week
|
||||
).order_by("-created_at")[:5]
|
||||
|
||||
# Pending Leave Requests
|
||||
pending_student_leaves = Student_Leave.objects.filter(status=0)
|
||||
pending_staff_leaves = Staff_leave.objects.filter(status=0).order_by("-created_at")
|
||||
|
||||
# Attendance Statistics
|
||||
today_attendance = Attendance.objects.filter(date=today).count()
|
||||
today_present = AttendanceRecord.objects.filter(
|
||||
attendance__date=today, student_attend=True
|
||||
today_classes = Attendance.objects.filter(date=today)
|
||||
total_classes = today_classes.count()
|
||||
total_students_today = AttendanceRecord.objects.filter(attendance__in=today_classes).count()
|
||||
students_present = AttendanceRecord.objects.filter(
|
||||
attendance__in=today_classes, student_attend=True
|
||||
).count()
|
||||
|
||||
# Recent Feedback
|
||||
recent_feedback = StudentFeedback.objects.order_by("-created_at")[:5]
|
||||
|
||||
# Recent Institute Feedback
|
||||
recent_institute_feedback = InstituteFeedback.objects.filter(
|
||||
is_public=True
|
||||
).order_by("-created_at")[:10]
|
||||
|
||||
# Get notices
|
||||
notices = Notice.objects.all().order_by("-created_at")
|
||||
|
||||
# Get recent activities
|
||||
recent_activities = []
|
||||
|
||||
# Add recent leaves
|
||||
for leave in recent_student_leaves:
|
||||
recent_activities.append(
|
||||
{
|
||||
"title": f"Student Leave Request",
|
||||
"description": f"{leave.student.name} requested leave for {leave.date}",
|
||||
"timestamp": leave.created_at,
|
||||
}
|
||||
)
|
||||
|
||||
for leave in recent_staff_leaves:
|
||||
recent_activities.append(
|
||||
{
|
||||
"title": f"Staff Leave Request",
|
||||
"description": f"{leave.staff.name} requested leave for {leave.date}",
|
||||
"timestamp": leave.created_at,
|
||||
}
|
||||
)
|
||||
|
||||
# Add recent feedback
|
||||
for feedback in recent_feedback:
|
||||
recent_activities.append(
|
||||
{
|
||||
"title": f"New Feedback",
|
||||
"description": f"Feedback received for {feedback.teacher.name if feedback.teacher else 'Institute'}",
|
||||
"timestamp": feedback.created_at,
|
||||
}
|
||||
)
|
||||
|
||||
# Add recent institute feedback
|
||||
for feedback in recent_institute_feedback:
|
||||
recent_activities.append(
|
||||
{
|
||||
"title": f"Institute Feedback",
|
||||
"description": f"New feedback from {feedback.display_name}",
|
||||
"timestamp": feedback.created_at,
|
||||
}
|
||||
)
|
||||
|
||||
# Add recent notices
|
||||
for notice in notices:
|
||||
recent_activities.append(
|
||||
{
|
||||
"title": f"New Notice",
|
||||
"description": notice.title,
|
||||
"timestamp": notice.created_at,
|
||||
}
|
||||
)
|
||||
|
||||
# Sort all activities by timestamp
|
||||
recent_activities.sort(key=lambda x: x["timestamp"], reverse=True)
|
||||
# Take only the most recent 10 activities
|
||||
recent_activities = recent_activities[:10]
|
||||
|
||||
# Teacher Ratings
|
||||
teacher_ratings = []
|
||||
teachers = Staff.objects.filter(
|
||||
Q(designation__icontains="Teacher")
|
||||
| Q(designation__icontains="Staff")
|
||||
| Q(designation__icontains="Principal")
|
||||
| Q(designation__isnull=True)
|
||||
| Q(designation="")
|
||||
)
|
||||
|
||||
for teacher in teachers:
|
||||
feedback = StudentFeedback.objects.filter(teacher=teacher)
|
||||
avg_rating = feedback.aggregate(Avg("rating"))["rating__avg"] or 0
|
||||
total_reviews = feedback.count()
|
||||
|
||||
# Get total students taught by this teacher
|
||||
total_students = (
|
||||
AttendanceRecord.objects.filter(attendance__routine__teacher=teacher)
|
||||
.values("student")
|
||||
.distinct()
|
||||
.count()
|
||||
)
|
||||
|
||||
teacher_ratings.append(
|
||||
{
|
||||
"name": teacher.name,
|
||||
"designation": teacher.designation or "Teacher",
|
||||
"image": teacher.image,
|
||||
"avg_rating": avg_rating,
|
||||
"total_reviews": total_reviews,
|
||||
"total_students": total_students,
|
||||
}
|
||||
)
|
||||
|
||||
# Sort teachers by average rating (highest first)
|
||||
teacher_ratings.sort(key=lambda x: x["avg_rating"], reverse=True)
|
||||
|
||||
# Course Progress Overview
|
||||
course_progress = []
|
||||
for course in Course.objects.all():
|
||||
@@ -142,25 +297,26 @@ def dashboard(request):
|
||||
# Monthly Statistics
|
||||
monthly_stats = {
|
||||
"new_students": Student.objects.filter(joining_date__gte=last_month).count(),
|
||||
"completed_courses": completed_trackings.filter(
|
||||
actual_end_date__gte=last_month
|
||||
).count(),
|
||||
"new_staff": Staff.objects.filter(joining_date__gte=last_month).count(),
|
||||
"attendance_rate": round(
|
||||
(
|
||||
AttendanceRecord.objects.filter(
|
||||
attendance__date__gte=last_month, student_attend=True
|
||||
attendance__date__gte=last_month.date(), student_attend=True
|
||||
).count()
|
||||
/ AttendanceRecord.objects.filter(
|
||||
attendance__date__gte=last_month
|
||||
attendance__date__gte=last_month.date()
|
||||
).count()
|
||||
* 100
|
||||
)
|
||||
if AttendanceRecord.objects.filter(
|
||||
attendance__date__gte=last_month
|
||||
attendance__date__gte=last_month.date()
|
||||
).exists()
|
||||
else 0,
|
||||
1,
|
||||
else 0
|
||||
),
|
||||
"leave_requests": Student_Leave.objects.filter(
|
||||
created_at__gte=last_month
|
||||
).count()
|
||||
+ Staff_leave.objects.filter(created_at__gte=last_month).count(),
|
||||
}
|
||||
|
||||
context = {
|
||||
@@ -172,6 +328,10 @@ def dashboard(request):
|
||||
"active_students": active_students,
|
||||
"completed_students": completed_students,
|
||||
"on_leave_students": on_leave_students,
|
||||
# Staff Statistics
|
||||
"total_staff": total_staff,
|
||||
"active_staff": active_staff,
|
||||
"staff_on_leave": staff_on_leave,
|
||||
# Course Statistics
|
||||
"total_courses": total_courses,
|
||||
"active_courses": active_courses,
|
||||
@@ -183,212 +343,318 @@ def dashboard(request):
|
||||
# Leave Requests
|
||||
"recent_student_leaves": recent_student_leaves,
|
||||
"recent_staff_leaves": recent_staff_leaves,
|
||||
"pending_student_leaves": pending_student_leaves,
|
||||
"pending_staff_leaves": pending_staff_leaves,
|
||||
# Attendance Statistics
|
||||
"today_attendance": today_attendance,
|
||||
"today_present": today_present,
|
||||
"total_classes_today": total_classes,
|
||||
"total_students_today": total_students_today,
|
||||
"students_present": students_present,
|
||||
"attendance_rate": round(
|
||||
(today_present / today_attendance * 100) if today_attendance > 0 else 0, 1
|
||||
(students_present / total_students_today * 100) if total_students_today > 0 else 0
|
||||
),
|
||||
# Recent Feedback
|
||||
# Feedback
|
||||
"recent_feedback": recent_feedback,
|
||||
"recent_institute_feedback": recent_institute_feedback,
|
||||
"teacher_ratings": teacher_ratings,
|
||||
# Course Progress
|
||||
"course_progress": course_progress,
|
||||
# Monthly Statistics
|
||||
"monthly_stats": monthly_stats,
|
||||
"notices": notices, # Add notices to context
|
||||
"recent_activities": recent_activities, # Add recent activities to context
|
||||
}
|
||||
|
||||
return render(request, "dashboard.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def studentDashboard(request):
|
||||
"""View to display the student dashboard with their personal information and progress"""
|
||||
student = request.user
|
||||
today = timezone.now().date()
|
||||
last_week = today - timedelta(days=7)
|
||||
last_month = today - timedelta(days=30)
|
||||
def update_profile(request):
|
||||
if request.method == "POST":
|
||||
user = request.user
|
||||
name = request.POST.get("name")
|
||||
email = request.POST.get("email")
|
||||
phone = request.POST.get("phone")
|
||||
address = request.POST.get("address")
|
||||
gender = request.POST.get("gender")
|
||||
dob = request.POST.get("dob")
|
||||
|
||||
# Course Progress
|
||||
course_trackings = CourseTracking.objects.filter(student=student)
|
||||
total_courses = course_trackings.count()
|
||||
completed_courses = course_trackings.filter(progress_status="Completed").count()
|
||||
in_progress_courses = course_trackings.filter(progress_status="In Progress").count()
|
||||
completion_percentage = (
|
||||
(completed_courses / total_courses * 100) if total_courses > 0 else 0
|
||||
)
|
||||
# Validate data
|
||||
if not all([name, email, phone]):
|
||||
messages.error(request, "Name, email and phone are required fields.")
|
||||
return redirect("studentDashboard")
|
||||
|
||||
# Course Details
|
||||
course_details = {}
|
||||
for tracking in course_trackings:
|
||||
course_details[tracking.course.name] = {
|
||||
"start_date": tracking.start_date,
|
||||
"end_date": tracking.end_date,
|
||||
"completion_percentage": tracking.completion_percentage,
|
||||
"progress_status": tracking.progress_status,
|
||||
"book": tracking.course.book if hasattr(tracking.course, "book") else None,
|
||||
"topics": [], # This will be populated if you have topic tracking
|
||||
}
|
||||
# Update profile based on user type
|
||||
try:
|
||||
if hasattr(user, "student"):
|
||||
student = user
|
||||
student.name = name
|
||||
student.email = email
|
||||
student.phone = phone
|
||||
student.address = address
|
||||
student.gender = gender
|
||||
if dob:
|
||||
student.dob = dob
|
||||
|
||||
# Attendance Statistics
|
||||
attendance_records = AttendanceRecord.objects.filter(student=student)
|
||||
total_classes = attendance_records.count()
|
||||
classes_attended = attendance_records.filter(student_attend=True).count()
|
||||
attendance_percentage = (
|
||||
(classes_attended / total_classes * 100) if total_classes > 0 else 0
|
||||
)
|
||||
# Handle profile image
|
||||
if "profile_image" in request.FILES:
|
||||
image = request.FILES["profile_image"]
|
||||
if student.image:
|
||||
# Delete old image if updating
|
||||
if os.path.isfile(student.image.path):
|
||||
os.remove(student.image.path)
|
||||
|
||||
# Calculate attendance streaks
|
||||
current_streak = 0
|
||||
longest_streak = 0
|
||||
temp_streak = 0
|
||||
# Save new image
|
||||
student.image = image
|
||||
|
||||
for record in attendance_records.order_by("-attendance__date"):
|
||||
if record.student_attend:
|
||||
temp_streak += 1
|
||||
if temp_streak > longest_streak:
|
||||
longest_streak = temp_streak
|
||||
if record.attendance.date >= last_week:
|
||||
current_streak = temp_streak
|
||||
student.save()
|
||||
messages.success(request, "Profile updated successfully.")
|
||||
|
||||
elif hasattr(user, "staff"):
|
||||
staff = user
|
||||
staff.name = name
|
||||
staff.email = email
|
||||
staff.phone = phone
|
||||
staff.address = address
|
||||
staff.gender = gender
|
||||
if dob:
|
||||
staff.dob = dob
|
||||
|
||||
# Handle profile image
|
||||
if "profile_image" in request.FILES:
|
||||
image = request.FILES["profile_image"]
|
||||
if staff.image:
|
||||
# Delete old image if updating
|
||||
if os.path.isfile(staff.image.path):
|
||||
os.remove(staff.image.path)
|
||||
|
||||
# Save new image
|
||||
staff.image = image
|
||||
|
||||
staff.save()
|
||||
messages.success(request, "Profile updated successfully.")
|
||||
|
||||
else:
|
||||
messages.error(request, "User profile not found.")
|
||||
|
||||
except Exception as e:
|
||||
messages.error(request, f"Error updating profile: {str(e)}")
|
||||
|
||||
# Redirect based on user type
|
||||
if hasattr(user, "student"):
|
||||
return redirect("studentDashboard")
|
||||
elif hasattr(user, "staff"):
|
||||
return redirect("teacherDashboard")
|
||||
else:
|
||||
temp_streak = 0
|
||||
return redirect("dashboard")
|
||||
|
||||
# Recent Leave Requests
|
||||
recent_leaves = Student_Leave.objects.filter(student=student).order_by(
|
||||
"-created_at"
|
||||
)[:5]
|
||||
|
||||
# Recent Feedback
|
||||
recent_feedback = StudentFeedback.objects.filter(student=student).order_by(
|
||||
"-created_at"
|
||||
)[:5]
|
||||
|
||||
context = {
|
||||
**custom_admin_site.each_context(
|
||||
request
|
||||
), # This adds the admin context, including the sidebar.
|
||||
"student": student,
|
||||
"title": "Student Dashboard",
|
||||
# Course Summary
|
||||
"course_summary": {
|
||||
"total_courses": total_courses,
|
||||
"completed_courses": completed_courses,
|
||||
"ongoing_courses": in_progress_courses,
|
||||
"completion_percentage": completion_percentage,
|
||||
"details": course_details,
|
||||
},
|
||||
# Attendance Summary
|
||||
"attendance_summary": {
|
||||
"total_classes": total_classes,
|
||||
"classes_attended": classes_attended,
|
||||
"attendance_percentage": attendance_percentage,
|
||||
"current_streak": current_streak,
|
||||
"longest_streak": longest_streak,
|
||||
},
|
||||
# Recent Activity
|
||||
"recent_leaves": recent_leaves,
|
||||
"recent_feedback": recent_feedback,
|
||||
}
|
||||
|
||||
return render(request, "student_dashboard.html", context)
|
||||
return redirect("dashboard")
|
||||
|
||||
|
||||
@login_required
|
||||
def teacherDashboard(request):
|
||||
"""View to display the teacher dashboard with their classes and student information"""
|
||||
teacher = Staff.objects.get(phone=request.user.phone) # Get the Staff object
|
||||
today = timezone.now().date()
|
||||
current_time = timezone.now().time()
|
||||
def change_password(request):
|
||||
if request.method == "POST":
|
||||
user = request.user
|
||||
current_password = request.POST.get("current_password")
|
||||
new_password = request.POST.get("new_password")
|
||||
confirm_password = request.POST.get("confirm_password")
|
||||
|
||||
# Get today's routines through attendance
|
||||
today_routines = Routine.objects.filter(
|
||||
teacher=teacher, attendances__date=today
|
||||
).order_by("start_time")
|
||||
# Validate input
|
||||
if not all([current_password, new_password, confirm_password]):
|
||||
messages.error(request, "All password fields are required.")
|
||||
return redirect("studentDashboard")
|
||||
|
||||
# Process routines to include status and attendance
|
||||
processed_routines = []
|
||||
for routine in today_routines:
|
||||
attendance = routine.attendances.filter(date=today).first()
|
||||
if new_password != confirm_password:
|
||||
messages.error(request, "New passwords do not match.")
|
||||
return redirect("studentDashboard")
|
||||
|
||||
total_students = StudentRoutine.objects.filter(routine=routine).count()
|
||||
attendance_count = (
|
||||
0
|
||||
if not attendance
|
||||
else AttendanceRecord.objects.filter(
|
||||
attendance=attendance, student_attend=True
|
||||
).count()
|
||||
)
|
||||
# Check current password
|
||||
if not user.check_password(current_password):
|
||||
messages.error(request, "Current password is incorrect.")
|
||||
return redirect("studentDashboard")
|
||||
|
||||
processed_routines.append(
|
||||
# Set new password
|
||||
try:
|
||||
user.set_password(new_password)
|
||||
user.save()
|
||||
messages.success(
|
||||
request, "Password changed successfully. Please login again."
|
||||
)
|
||||
return redirect("login")
|
||||
except Exception as e:
|
||||
messages.error(request, f"Error changing password: {str(e)}")
|
||||
|
||||
# Redirect based on user type
|
||||
if hasattr(user, "student"):
|
||||
return redirect("studentDashboard")
|
||||
elif hasattr(user, "staff"):
|
||||
return redirect("teacherDashboard")
|
||||
else:
|
||||
return redirect("dashboard")
|
||||
|
||||
return redirect("dashboard")
|
||||
|
||||
|
||||
@login_required
|
||||
@require_GET
|
||||
def get_subjects(request):
|
||||
"""Get subjects for a course and semester"""
|
||||
course_id = request.GET.get("course_id")
|
||||
semester = request.GET.get("semester", 1)
|
||||
|
||||
if not course_id:
|
||||
return JsonResponse({"success": False, "message": "Course ID is required"})
|
||||
|
||||
try:
|
||||
subjects = Subject.objects.filter(
|
||||
course_id=course_id, semester_or_year=semester
|
||||
).values("id", "name")
|
||||
|
||||
return JsonResponse({"success": True, "subjects": list(subjects)})
|
||||
except Exception as e:
|
||||
return JsonResponse({"success": False, "message": str(e)})
|
||||
|
||||
|
||||
@login_required
|
||||
@require_GET
|
||||
def get_teachers(request):
|
||||
"""Get teachers for the institute"""
|
||||
try:
|
||||
teachers = Staff.objects.filter(
|
||||
Q(designation__icontains="Teacher")
|
||||
| Q(designation__icontains="Staff")
|
||||
| Q(designation__icontains="Principal")
|
||||
| Q(designation__isnull=True)
|
||||
| Q(designation="")
|
||||
).values("id", "name")
|
||||
|
||||
return JsonResponse({"success": True, "teachers": list(teachers)})
|
||||
except Exception as e:
|
||||
return JsonResponse({"success": False, "message": str(e)})
|
||||
|
||||
|
||||
@login_required
|
||||
@require_GET
|
||||
def get_course_duration(request):
|
||||
"""Get course duration details"""
|
||||
course_id = request.GET.get("course_id")
|
||||
|
||||
if not course_id:
|
||||
return JsonResponse({"success": False, "message": "Course ID is required"})
|
||||
|
||||
try:
|
||||
course = Course.objects.get(id=course_id)
|
||||
|
||||
# Calculate number of semesters or years
|
||||
total_periods = course.duration
|
||||
if course.duration_type == "Semester":
|
||||
total_periods = course.duration * 2 # 2 semesters per year
|
||||
|
||||
# Create a list of periods
|
||||
periods = []
|
||||
for i in range(1, total_periods + 1):
|
||||
if course.duration_type == "Semester":
|
||||
periods.append({"value": i, "label": f"Semester {i}"})
|
||||
else:
|
||||
periods.append({"value": i, "label": f"Year {i}"})
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"name": routine.name,
|
||||
"start_time": routine.start_time,
|
||||
"end_time": routine.end_time,
|
||||
"is_completed": routine.end_time < current_time,
|
||||
"is_ongoing": routine.start_time <= current_time <= routine.end_time,
|
||||
"total_students": total_students,
|
||||
"attendance_count": attendance_count,
|
||||
"success": True,
|
||||
"course": {
|
||||
"id": course.id,
|
||||
"name": course.name,
|
||||
"duration": course.duration,
|
||||
"duration_type": course.duration_type,
|
||||
"total_periods": total_periods,
|
||||
"periods": periods,
|
||||
},
|
||||
}
|
||||
)
|
||||
except Course.DoesNotExist:
|
||||
return JsonResponse({"success": False, "message": "Course not found"})
|
||||
except Exception as e:
|
||||
return JsonResponse({"success": False, "message": str(e)})
|
||||
|
||||
# Calculate class statistics
|
||||
total_classes = today_routines.count()
|
||||
completed_classes = len([r for r in processed_routines if r["is_completed"]])
|
||||
|
||||
# Get student statistics
|
||||
total_students = (
|
||||
StudentRoutine.objects.filter(routine__teacher=teacher)
|
||||
.values("student")
|
||||
.distinct()
|
||||
.count()
|
||||
)
|
||||
@login_required
|
||||
def approve_student_leave(request, leave_id):
|
||||
"""View to approve a student leave request"""
|
||||
if request.method == "POST":
|
||||
leave = get_object_or_404(Student_Leave, id=leave_id)
|
||||
leave.status = 1 # Approved
|
||||
leave.save()
|
||||
messages.success(
|
||||
request, f"Leave request for {leave.student.name} has been approved."
|
||||
)
|
||||
return redirect("dashboard")
|
||||
|
||||
# Calculate present students for today
|
||||
present_students = AttendanceRecord.objects.filter(
|
||||
attendance__routine__teacher=teacher,
|
||||
attendance__date=today,
|
||||
student_attend=True,
|
||||
).count()
|
||||
|
||||
# Get teacher's feedback statistics
|
||||
feedback = StudentFeedback.objects.filter(teacher=teacher)
|
||||
avg_rating = feedback.aggregate(Avg("rating"))["rating__avg"] or 0
|
||||
avg_rating_rounded = int(avg_rating)
|
||||
remaining_stars = 5 - avg_rating_rounded
|
||||
@login_required
|
||||
def reject_student_leave(request, leave_id):
|
||||
"""View to reject a student leave request"""
|
||||
if request.method == "POST":
|
||||
leave = get_object_or_404(Student_Leave, id=leave_id)
|
||||
leave.status = 2 # Rejected
|
||||
leave.save()
|
||||
messages.warning(
|
||||
request, f"Leave request for {leave.student.name} has been rejected."
|
||||
)
|
||||
return redirect("dashboard")
|
||||
|
||||
# Get leave statistics
|
||||
leaves = Staff_leave.objects.filter(staff=teacher)
|
||||
pending_leaves = leaves.filter(status=0).count()
|
||||
approved_leaves = leaves.filter(status=1).count()
|
||||
rejected_leaves = leaves.filter(status=2).count()
|
||||
|
||||
# Get recent activity
|
||||
recent_leaves = leaves.order_by("-created_at")[:5]
|
||||
recent_feedback = feedback.order_by("-created_at")[:5]
|
||||
@login_required
|
||||
def approve_staff_leave(request, leave_id):
|
||||
"""View to approve a staff leave request"""
|
||||
if request.method == "POST":
|
||||
leave = get_object_or_404(Staff_leave, id=leave_id)
|
||||
leave.status = 1 # Approved
|
||||
leave.save()
|
||||
messages.success(
|
||||
request, f"Leave request for {leave.staff.name} has been approved."
|
||||
)
|
||||
return redirect("dashboard")
|
||||
|
||||
|
||||
@login_required
|
||||
def reject_staff_leave(request, leave_id):
|
||||
"""View to reject a staff leave request"""
|
||||
if request.method == "POST":
|
||||
leave = get_object_or_404(Staff_leave, id=leave_id)
|
||||
leave.status = 2 # Rejected
|
||||
leave.save()
|
||||
messages.warning(
|
||||
request, f"Leave request for {leave.staff.name} has been rejected."
|
||||
)
|
||||
return redirect("dashboard")
|
||||
|
||||
|
||||
@login_required
|
||||
def add_notice(request):
|
||||
if request.method == "POST":
|
||||
title = request.POST.get("title")
|
||||
message = request.POST.get("message")
|
||||
image = request.FILES.get("image")
|
||||
|
||||
if title and message:
|
||||
notice = Notice.objects.create(title=title, message=message, image=image)
|
||||
messages.success(request, "Notice added successfully.")
|
||||
else:
|
||||
messages.error(request, "Title and message are required.")
|
||||
|
||||
return redirect("dashboard")
|
||||
|
||||
|
||||
@login_required
|
||||
def delete_notice(request, notice_id):
|
||||
if request.method == "POST":
|
||||
try:
|
||||
notice = Notice.objects.get(id=notice_id)
|
||||
notice.delete()
|
||||
return JsonResponse({"success": True})
|
||||
except Notice.DoesNotExist:
|
||||
return JsonResponse({"success": False, "error": "Notice not found"})
|
||||
|
||||
return JsonResponse({"success": False, "error": "Invalid request method"})
|
||||
|
||||
|
||||
context = {
|
||||
**custom_admin_site.each_context(
|
||||
request
|
||||
), # This adds the admin context, including the sidebar.
|
||||
"teacher": teacher,
|
||||
"title": "Teacher Dashboard",
|
||||
# Class Statistics
|
||||
"today_classes": total_classes,
|
||||
"completed_classes": completed_classes,
|
||||
"today_routines": processed_routines,
|
||||
# Student Statistics
|
||||
"total_students": total_students,
|
||||
"present_students": present_students,
|
||||
# Feedback Statistics
|
||||
"avg_rating": avg_rating,
|
||||
"avg_rating_rounded": avg_rating_rounded,
|
||||
"remaining_stars": remaining_stars,
|
||||
# Leave Statistics
|
||||
"pending_leaves": pending_leaves,
|
||||
"approved_leaves": approved_leaves,
|
||||
"rejected_leaves": rejected_leaves,
|
||||
# Recent Activity
|
||||
"recent_leaves": recent_leaves,
|
||||
"recent_feedback": recent_feedback,
|
||||
}
|
||||
|
||||
return render(request, "teacher_dashboard.html", context)
|
||||
Reference in new issue
Block a user