huge updates i donot track what updates i have made

This commit is contained in:
sankalp committed 2025-03-29 03:22:47 +05:45
1 parent d02b470ca0
commit 84dd5d0b2f
55 files changed
+11072 -1415

No files matched your search

+438 -134
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+298
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;"
),
]
-37
View File
@@ -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',
),
]
-35
View File
@@ -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
View File
@@ -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()
+179
View File
@@ -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']
+585
View File
@@ -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)
+511
View File
@@ -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)}"}
)
+28 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)