-[](https://www.python.org/)
-[](https://www.djangoproject.com/)
+[](https://www.python.org/)
+[](https://www.djangoproject.com/)
[](LICENSE)
[](https://github.com/S4NKALP/Student-Management-System-In-Django/stargazers)
-[](https://archlinux.org/)
[]()
+[]()
+[]()
+[]()
-A modern and comprehensive Student Management System built with Django.
+A comprehensive Student Management System built with Django, featuring multi-role access, real-time notifications, and secure authentication.
+### Monitoring
+- Performance metrics
+- Error tracking
+- User analytics
+- Resource usage
+- API response times
+
+## ๐ค Contributing
+
+1. Fork the repository
+2. Create your feature branch
+3. Commit your changes
+4. Push to the branch
+5. Create a Pull Request
+
+### Code Style
+- Follow PEP 8
+- Use type hints
+- Write docstrings
+- Add comments
+- Follow Django best practices
+
+### Commit Messages
+- Use present tense
+- Be descriptive
+- Reference issues
+- Follow conventional commits
+
+## ๐ License
+
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## ๐ Credits
@@ -210,14 +680,6 @@ If you're using this project in an academic context, you can cite it as:
- Special thanks to the open-source community for their valuable resources
- Appreciation to all users who have provided feedback and suggestions
-## ๐ License
-
-
@@ -225,7 +687,47 @@ If you're using this project in an academic context, you can cite it as:
### Made with โค๏ธ by [Sankalp](https://github.com/S4NKALP)
[](https://github.com/S4NKALP)
-
+[](https://linkedin.com/in/sankalp)
+[](https://twitter.com/sankalp)
+
+## ๐ฐ Project Costs
+
+### Development & Implementation
+- Backend Development: NPR 200,000
+- Frontend Development: NPR 160,000
+- QA Engineering: NPR 60,000
+- Project Management: NPR 240,000
+- Development Tools & Licenses: NPR 15,000
+- Testing Tools: NPR 10,000
+- Documentation Tools: NPR 5,000
+
+### Annual Infrastructure & Services
+- Firebase Services: NPR 10,000/year
+- Google Cloud Storage: NPR 8,000/year
+- Domain & SSL: NPR 3,000/year
+- Server Hosting: NPR 20,000/year
+
+### Support & Maintenance
+- User Training: NPR 20,000
+- Documentation: NPR 15,000
+- Technical Support (First Year): NPR 30,000
+
+### Total First Year Cost: NPR 601,000
+
+## ๐ Feasibility
+
+### Target Areas
+- Urban Areas with Internet Connectivity
+- Educational Institutions
+- Government Offices
+- Private Organizations
+- Healthcare Facilities
+
+### Key Considerations
+- Scalable architecture for future expansion
+- Mobile-responsive design
+- Real-time data processing capabilities
+- Secure data handling and storage
+- Offline support capabilities
\ No newline at end of file
diff --git a/app/admin.py b/app/admin.py
index 1bb20cc..01dcb37 100644
--- a/app/admin.py
+++ b/app/admin.py
@@ -1,143 +1,204 @@
-from django.contrib import admin
-from .models import (
- Institute,
- Course,
- Subject,
- SubjectFile,
- Student,
- Staff,
- Batch,
- Attendance,
- AttendanceRecord,
- Notice,
- Routine,
- Staff_leave,
- Student_Leave,
- StudentFeedback,
- CourseTracking,
- InstituteFeedback,
- StaffInstituteFeedback,
- TOTPSecret,
- ResetToken,
-)
-from .firebase import FCMDevice
-from django.forms import ModelForm
-from django.contrib.auth.models import User, Group
-from django.contrib.auth.admin import UserAdmin, GroupAdmin
-from app.forms import StaffAdminForm, StudentAdminForm
-from django.urls import path
-from django.http import JsonResponse
+# Standard library imports
import json
+from datetime import datetime
+
+# Core Django imports
+from django.contrib import admin
+from django.contrib.auth.admin import GroupAdmin, UserAdmin
+from django.contrib.auth.models import Group, User
from django.db import transaction
from django.db.models import Q
-from datetime import datetime
+from django.forms import ModelForm
+from django.http import JsonResponse
+from django.urls import path
+from django.utils.translation import gettext_lazy as _
+
+# Local app imports
+from app.firebase import FCMDevice
+from app.forms import ParentAdminForm, StaffAdminForm, StudentAdminForm
+from app.models import (
+ Attendance,
+ AttendanceRecord,
+ Batch,
+ Course,
+ CourseTracking,
+ Institute,
+ InstituteFeedback,
+ Notice,
+ Parent,
+ ParentFeedback,
+ ParentInstituteFeedback,
+ ResetToken,
+ Routine,
+ Staff,
+ StaffInstituteFeedback,
+ StaffLeave,
+ Student,
+ StudentFeedback,
+ StudentLeave,
+ Subject,
+ SubjectFile,
+ TeacherParentMeeting,
+ TOTPSecret,
+)
# Register your models here.
class CustomAdminSite(admin.AdminSite):
- def get_app_list(self, request):
+ def get_app_list(self, request, app_label=None):
# Original app list
- app_list = super().get_app_list(request)
+ app_list = super().get_app_list(request, app_label)
model_map = {
# Authentication Models
"User": {
"group": "Authentication",
"group_label": "authentication",
+ "display_name": "Users",
},
"Group": {
"group": "Authentication",
"group_label": "authentication",
+ "display_name": "Groups",
},
"TOTPSecret": {
"group": "Authentication",
"group_label": "authentication",
+ "display_name": "TOTP Keys",
},
"ResetToken": {
"group": "Authentication",
"group_label": "authentication",
+ "display_name": "Reset Tokens",
},
# Organization Models
"Institute": {
"group": "Organization",
"group_label": "organization",
+ "display_name": "Institute",
},
"Batch": {
"group": "Organization",
"group_label": "organization",
+ "display_name": "Batches",
},
"Notice": {
"group": "Organization",
"group_label": "organization",
+ "display_name": "Notices",
},
# Course Management Models
"Course": {
"group": "Course Management",
"group_label": "course_management",
+ "display_name": "Courses",
},
"Subject": {
"group": "Course Management",
"group_label": "course_management",
+ "display_name": "Subjects",
},
"SubjectFile": {
"group": "Course Management",
"group_label": "course_management",
+ "display_name": "Materials",
},
"CourseTracking": {
"group": "Course Management",
"group_label": "course_management",
+ "display_name": "Progress",
},
# Human Management Models
+ "Parent": {
+ "group": "Human Management",
+ "group_label": "human_management",
+ "display_name": "Parents",
+ },
"Student": {
"group": "Human Management",
"group_label": "human_management",
+ "display_name": "Students",
},
"Staff": {
"group": "Human Management",
"group_label": "human_management",
+ "display_name": "Staff",
},
+ # Schedule Management
"Routine": {
- "group": "Human Management",
- "group_label": "human_management",
+ "group": "Schedule Management",
+ "group_label": "schedule_management",
+ "display_name": "Classes",
},
- "Staff_leave": {
- "group": "Human Management",
- "group_label": "human_management",
+ "TeacherParentMeeting": {
+ "group": "Schedule Management",
+ "group_label": "schedule_management",
+ "display_name": "PT Meetings",
},
- "Student_Leave": {
- "group": "Human Management",
- "group_label": "human_management",
+ # Leave Management
+ "StaffLeave": {
+ "group": "Leave Management",
+ "group_label": "leave_management",
+ "display_name": "Staff Leave",
+ },
+ "StudentLeave": {
+ "group": "Leave Management",
+ "group_label": "leave_management",
+ "display_name": "Student Leave",
},
# Attendance Models
"Attendance": {
"group": "Attendance",
"group_label": "attendance",
+ "display_name": "Attendance",
},
"AttendanceRecord": {
"group": "Attendance",
"group_label": "attendance",
+ "display_name": "Records",
},
# Feedback Models
"StudentFeedback": {
"group": "Feedback",
"group_label": "feedback",
+ "display_name": "Student Feedback",
},
"InstituteFeedback": {
"group": "Feedback",
"group_label": "feedback",
+ "display_name": "Student-Institute",
},
"StaffInstituteFeedback": {
"group": "Feedback",
"group_label": "feedback",
+ "display_name": "Staff-Institute",
+ },
+ "ParentFeedback": {
+ "group": "Feedback",
+ "group_label": "feedback",
+ "display_name": "Parent Feedback",
+ },
+ "ParentInstituteFeedback": {
+ "group": "Feedback",
+ "group_label": "feedback",
+ "display_name": "Parent-Institute",
},
# Device Management
"FCMDevice": {
"group": "Device Management",
"group_label": "device_management",
+ "display_name": "Devices",
},
}
+ # If app_label is specified, return only that app
+ if app_label:
+ for app in app_list:
+ if app["app_label"] == app_label:
+ return [app]
+ return []
+
grouped_app_list = [{"name": "Main", "app_label": "dashboard", "models": []}]
grouped_models = {}
@@ -152,6 +213,11 @@ class CustomAdminSite(admin.AdminSite):
"app_label": model_map[model_name]["group_label"],
"models": [],
}
+
+ # Apply custom display name if available
+ if "display_name" in model_map[model_name]:
+ model["name"] = model_map[model_name]["display_name"]
+
grouped_models[group]["models"].append(model)
dashboard_item = {
@@ -187,10 +253,17 @@ class InstituteModel(admin.ModelAdmin):
@admin.register(Course, site=custom_admin_site)
class CourseModel(admin.ModelAdmin):
- list_display = ("id", "name", "duration", "duration_type", "get_total_duration")
+ list_display = (
+ "id",
+ "name",
+ "duration",
+ "duration_type",
+ "get_total_duration",
+ "is_active",
+ )
search_fields = ("name", "duration", "duration_type")
- list_filter = ("duration_type", "duration")
- advanced_filter_fields = ("name", "duration", "duration_type")
+ list_filter = ("duration_type", "duration", "is_active")
+ advanced_filter_fields = ("name", "duration", "duration_type", "is_active")
def get_total_duration(self, obj):
if obj.duration_type == "Semester":
@@ -213,34 +286,41 @@ class CourseModel(admin.ModelAdmin):
@admin.register(Batch, site=custom_admin_site)
class BatchAdmin(admin.ModelAdmin):
- list_display = ("name", "start_date", "end_date")
- search_fields = ("name", "start_date", "end_date")
- list_filter = ("name", "start_date", "end_date")
- advanced_filter_fields = ("name", "start_date", "end_date")
+ list_display = ("name", "year", "is_active")
+ search_fields = ("name", "year")
+ list_filter = ("name", "year", "is_active")
+ advanced_filter_fields = ("name", "year", "is_active")
@admin.register(Subject, site=custom_admin_site)
class SubjectModel(admin.ModelAdmin):
- list_display = ("id", "name", "course", "get_period_display", "has_syllabus", "file_count")
+ list_display = (
+ "id",
+ "name",
+ "course",
+ "get_period_display",
+ "has_syllabus",
+ "file_count",
+ )
search_fields = ("name", "course__name")
- list_filter = ("course", "semester_or_year")
- advanced_filter_fields = ("name", "course", "semester_or_year")
+ list_filter = ("course", "period_or_year")
+ advanced_filter_fields = ("name", "course", "period_or_year")
def has_syllabus(self, obj):
return bool(obj.syllabus_pdf)
has_syllabus.boolean = True
has_syllabus.short_description = "Has Syllabus"
-
+
def file_count(self, obj):
return obj.subjectfile_set.count()
-
+
file_count.short_description = "Additional Files"
def get_period_display(self, obj):
if obj.course.duration_type == "Semester":
- return f"Semester {obj.semester_or_year}"
- return f"Year {obj.semester_or_year}"
+ return f"Semester {obj.period_or_year}"
+ return f"Year {obj.period_or_year}"
get_period_display.short_description = "Period"
@@ -252,7 +332,7 @@ class SubjectModel(admin.ModelAdmin):
def response_add(self, request, obj, post_url_continue=None):
if "_addanother" in request.POST:
- # Pre-fill the course and semester fields for the next entry
+ # Pre-fill the course and period fields for the next entry
return self.response_post_save_add(request, obj)
return super().response_add(request, obj, post_url_continue)
@@ -260,23 +340,23 @@ class SubjectModel(admin.ModelAdmin):
form = super().get_form(request, obj, **kwargs)
if obj and obj.course:
if obj.course.duration_type == "Semester":
- form.base_fields["semester_or_year"].label = "Semester"
+ form.base_fields["period_or_year"].label = "Semester"
form.base_fields[
- "semester_or_year"
+ "period_or_year"
].help_text = f"Enter semester number (1-{obj.course.duration * 2})"
else:
- form.base_fields["semester_or_year"].label = "Year"
+ form.base_fields["period_or_year"].label = "Year"
form.base_fields[
- "semester_or_year"
+ "period_or_year"
].help_text = f"Enter year number (1-{obj.course.duration})"
# Keep the last used course selected for next entry
if not obj and request.method == "GET":
if "course" in request.GET:
form.base_fields["course"].initial = request.GET["course"]
- if "semester_or_year" in request.GET:
- form.base_fields["semester_or_year"].initial = request.GET[
- "semester_or_year"
+ if "period_or_year" in request.GET:
+ form.base_fields["period_or_year"].initial = request.GET[
+ "period_or_year"
]
return form
@@ -287,17 +367,17 @@ class SubjectModel(admin.ModelAdmin):
opts = self.model._meta
url = reverse(f"admin:{opts.app_label}_{opts.model_name}_add")
return HttpResponseRedirect(
- f"{url}?course={obj.course.id}&semester_or_year={obj.semester_or_year}"
+ f"{url}?course={obj.course.id}&period_or_year={obj.period_or_year}"
)
@admin.register(Student, site=custom_admin_site)
class StudentAdmin(admin.ModelAdmin):
form = StudentAdminForm
- list_display = ("id", "name", "phone", "course", "status")
- search_fields = ("name", "phone", "course__name")
+ list_display = ("id", "name", "phone", "course", "status", "fcm_token")
+ search_fields = ("name", "phone", "course__name", "fcm_token")
list_filter = ("course", "status", "gender", "joining_date")
- advanced_filter_fields = ("name", "phone", "course", "batch", "status")
+ advanced_filter_fields = ("name", "phone", "course", "batch", "status", "fcm_token")
def has_view_permission(self, request, obj=None):
"""Check if user has view permission"""
@@ -328,13 +408,16 @@ class StudentAdmin(admin.ModelAdmin):
@admin.register(Staff, site=custom_admin_site)
class StaffAdmin(admin.ModelAdmin):
form = StaffAdminForm
- list_display = ("id", "name", "phone", "designation", "joining_date")
- search_fields = ("name", "phone", "designation")
+ list_display = ("id", "name", "phone", "designation", "joining_date", "fcm_token")
+ search_fields = ("name", "phone", "designation", "fcm_token")
list_filter = ("designation", "joining_date", "gender")
- advanced_filter_fields = ("name", "phone", "designation")
+ advanced_filter_fields = ("name", "phone", "designation", "fcm_token")
readonly_fields = ("password",)
ordering = ("name",)
- filter_horizontal = ("groups",)
+
+ def save_model(self, request, obj, form, change):
+ """Just save the model, group assignment is handled in the form"""
+ super().save_model(request, obj, form, change)
def has_view_permission(self, request, obj=None):
"""Check if user has view permission"""
@@ -435,10 +518,10 @@ class RoutineAdmin(admin.ModelAdmin):
"teacher",
"start_time",
"end_time",
- "semester_or_year",
+ "period_or_year",
"is_active",
]
- list_filter = ["is_active", "course", "subject", "teacher", "semester_or_year"]
+ list_filter = ["is_active", "course", "subject", "teacher", "period_or_year"]
search_fields = ["course__name", "subject__name", "teacher__name"]
ordering = ["start_time"]
change_form_template = "admin/app/routine/change_form.html"
@@ -478,7 +561,7 @@ class RoutineAdmin(admin.ModelAdmin):
"teacher",
"start_time",
"end_time",
- "semester_or_year",
+ "period_or_year",
]
missing_fields = [
field
@@ -496,7 +579,7 @@ class RoutineAdmin(admin.ModelAdmin):
course_id = int(routine_data["course"])
subject_id = int(routine_data["subject"])
teacher_id = int(routine_data["teacher"])
- semester = int(routine_data["semester_or_year"])
+ period = int(routine_data["period_or_year"])
except (ValueError, TypeError) as e:
raise ValueError(f"Error parsing IDs: {str(e)}")
@@ -518,7 +601,7 @@ class RoutineAdmin(admin.ModelAdmin):
existing_routine = Routine.objects.filter(
course_id=course_id,
subject_id=subject_id,
- semester_or_year=semester,
+ period_or_year=period,
start_time=start_time,
end_time=end_time,
).first()
@@ -536,7 +619,7 @@ class RoutineAdmin(admin.ModelAdmin):
teacher_id=teacher_id,
start_time=start_time,
end_time=end_time,
- semester_or_year=semester,
+ period_or_year=period,
is_active=True,
)
created_routines.append(routine)
@@ -558,7 +641,7 @@ class RoutineAdmin(admin.ModelAdmin):
"teacher": r.teacher.name,
"start_time": r.start_time.strftime("%H:%M"),
"end_time": r.end_time.strftime("%H:%M"),
- "semester_or_year": r.semester_or_year,
+ "period_or_year": r.period_or_year,
}
for r in created_routines
],
@@ -581,7 +664,7 @@ class RoutineAdmin(admin.ModelAdmin):
if request.GET.get("course"):
kwargs["queryset"] = Subject.objects.filter(
course_id=request.GET.get("course"),
- semester_or_year=request.GET.get("semester_or_year", 1),
+ period_or_year=request.GET.get("period_or_year", 1),
)
else:
kwargs["queryset"] = Subject.objects.none()
@@ -601,7 +684,7 @@ class RoutineAdmin(admin.ModelAdmin):
form.base_fields["course"].widget.attrs["class"] = "select2"
form.base_fields["subject"].widget.attrs["class"] = "select2"
form.base_fields["teacher"].widget.attrs["class"] = "select2"
- form.base_fields["semester_or_year"].initial = 1
+ form.base_fields["period_or_year"].initial = 1
return form
def add_view(self, request, form_url="", extra_context=None):
@@ -618,9 +701,15 @@ class RoutineAdmin(admin.ModelAdmin):
js = ("https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js",)
-@admin.register(Staff_leave, site=custom_admin_site)
+@admin.register(StaffLeave, site=custom_admin_site)
class StaffLeaveAdmin(admin.ModelAdmin):
- list_display = ("staff", "start_date", "end_date", "get_status_display", "created_at")
+ list_display = (
+ "staff",
+ "start_date",
+ "end_date",
+ "get_status_display",
+ "created_at",
+ )
list_filter = ("status", "start_date", "end_date", "created_at")
search_fields = ("staff__name", "message")
readonly_fields = ("created_at", "updated_at")
@@ -628,9 +717,15 @@ class StaffLeaveAdmin(admin.ModelAdmin):
raw_id_fields = ("staff",)
-@admin.register(Student_Leave, site=custom_admin_site)
+@admin.register(StudentLeave, site=custom_admin_site)
class StudentLeaveAdmin(admin.ModelAdmin):
- list_display = ("student", "start_date", "end_date", "get_status_display", "created_at")
+ list_display = (
+ "student",
+ "start_date",
+ "end_date",
+ "get_status_display",
+ "created_at",
+ )
list_filter = ("status", "start_date", "end_date", "created_at")
search_fields = ("student__name", "message")
readonly_fields = ("created_at", "updated_at")
@@ -790,15 +885,25 @@ class StaffInstituteFeedbackAdmin(admin.ModelAdmin):
@admin.register(FCMDevice, site=custom_admin_site)
class FCMDeviceAdmin(admin.ModelAdmin):
- list_display = ("id", "token", "created_at", "last_active", "is_active")
+ list_display = (
+ "id",
+ "token",
+ "user_type",
+ "is_active",
+ "created_at",
+ "last_active",
+ )
search_fields = ("token",)
- list_filter = ("is_active", "created_at", "last_active")
+ list_filter = ("user_type", "is_active", "created_at", "last_active")
readonly_fields = ("created_at", "last_active")
ordering = ("-last_active",)
fieldsets = (
- (None, {"fields": ("token", "is_active")}),
- ("Metadata", {"fields": ("created_at", "last_active"), "classes": ("collapse",)}),
+ (None, {"fields": ("token", "user_type", "is_active", "is_fallback")}),
+ (
+ "Metadata",
+ {"fields": ("created_at", "last_active"), "classes": ("collapse",)},
+ ),
)
@@ -810,5 +915,166 @@ class SubjectFileAdmin(admin.ModelAdmin):
autocomplete_fields = ["subject", "uploaded_by"]
+@admin.register(Parent, site=custom_admin_site)
+class ParentAdmin(admin.ModelAdmin):
+ form = ParentAdminForm
+ list_display = ("id", "name", "phone", "email", "fcm_token")
+ search_fields = ("name", "phone", "email", "fcm_token")
+ list_filter = ("students",)
+ advanced_filter_fields = ("name", "phone", "email", "students", "fcm_token")
+
+ def has_view_permission(self, request, obj=None):
+ """Check if user has view permission"""
+ return request.user.has_perm("app.view_parent")
+
+ def has_add_permission(self, request):
+ """Check if user has add permission"""
+ return request.user.has_perm("app.add_parent")
+
+ def has_change_permission(self, request, obj=None):
+ """Check if user has change permission"""
+ return request.user.has_perm("app.change_parent")
+
+ def has_delete_permission(self, request, obj=None):
+ """Check if user has delete permission"""
+ return request.user.has_perm("app.delete_parent")
+
+ def has_module_permission(self, request):
+ """Check if user has any permissions for this app"""
+ return request.user.has_module_perms("app")
+
+ def get_readonly_fields(self, request, obj=None):
+ if obj:
+ return self.readonly_fields + ("password",)
+ return self.readonly_fields
+
+
+@admin.register(ParentFeedback, site=custom_admin_site)
+class ParentFeedbackAdmin(admin.ModelAdmin):
+ list_display = ("parent", "teacher", "student", "rating", "created_at")
+ list_filter = ("rating", "created_at", "teacher", "student")
+ search_fields = ("parent__name", "teacher__name", "student__name", "feedback_text")
+ readonly_fields = ("created_at", "updated_at")
+ ordering = ("-created_at",)
+ raw_id_fields = ("parent", "teacher", "student")
+
+ def get_queryset(self, request):
+ return (
+ super().get_queryset(request).select_related("parent", "teacher", "student")
+ )
+
+
+@admin.register(ParentInstituteFeedback, site=custom_admin_site)
+class ParentInstituteFeedbackAdmin(admin.ModelAdmin):
+ list_display = (
+ "parent",
+ "institute",
+ "feedback_type",
+ "rating",
+ "is_anonymous",
+ "created_at",
+ )
+ list_filter = ("feedback_type", "rating", "is_anonymous", "created_at", "institute")
+ search_fields = ("parent__name", "institute__name", "feedback_text")
+ readonly_fields = ("created_at", "updated_at")
+ ordering = ("-created_at",)
+ raw_id_fields = ("parent", "institute")
+
+ def get_queryset(self, request):
+ return super().get_queryset(request).select_related("parent", "institute")
+
+
+@admin.register(TeacherParentMeeting, site=custom_admin_site)
+class TeacherParentMeetingAdmin(admin.ModelAdmin):
+ list_display = (
+ "meeting_date",
+ "meeting_time",
+ "duration",
+ "status",
+ "is_online",
+ "meeting_link",
+ )
+ list_filter = (
+ "status",
+ "is_online",
+ "meeting_date",
+ )
+ search_fields = (
+ "agenda",
+ "notes",
+ "cancellation_reason",
+ )
+ readonly_fields = ("created_at", "updated_at")
+ ordering = ("-meeting_date", "-meeting_time")
+
+ fieldsets = (
+ (
+ "Meeting Details",
+ {
+ "fields": (
+ "meeting_date",
+ "meeting_time",
+ "duration",
+ "status",
+ "is_online",
+ "meeting_link",
+ )
+ },
+ ),
+ (
+ "Meeting Content",
+ {
+ "fields": ("agenda", "notes"),
+ "classes": ("collapse",),
+ },
+ ),
+ (
+ "Cancellation Details",
+ {
+ "fields": ("cancellation_reason",),
+ "classes": ("collapse",),
+ },
+ ),
+ (
+ "Metadata",
+ {
+ "fields": (
+ "created_at",
+ "updated_at",
+ ),
+ "classes": ("collapse",),
+ },
+ ),
+ )
+
+
+@admin.register(TOTPSecret, site=custom_admin_site)
+class TOTPSecretAdmin(admin.ModelAdmin):
+ list_display = ("identifier", "created_at", "expires_at", "is_expired")
+ search_fields = ("identifier",)
+ readonly_fields = ("created_at", "expires_at")
+ ordering = ("-created_at",)
+
+ def is_expired(self, obj):
+ return obj.is_expired()
+
+ is_expired.boolean = True
+ is_expired.short_description = "Expired"
+
+
+@admin.register(ResetToken, site=custom_admin_site)
+class ResetTokenAdmin(admin.ModelAdmin):
+ list_display = ("identifier", "token", "created_at", "expires_at", "is_expired")
+ search_fields = ("identifier", "token")
+ readonly_fields = ("created_at", "expires_at")
+ ordering = ("-created_at",)
+
+ def is_expired(self, obj):
+ return obj.is_expired()
+
+ is_expired.boolean = True
+ is_expired.short_description = "Expired"
+
+
custom_admin_site.register(User, UserAdmin)
custom_admin_site.register(Group, GroupAdmin)
diff --git a/app/admissionviews.py b/app/admissionviews.py
new file mode 100644
index 0000000..fcc0f6e
--- /dev/null
+++ b/app/admissionviews.py
@@ -0,0 +1,731 @@
+# Standard library imports
+from datetime import timedelta, datetime
+
+# Core Django imports
+from django.shortcuts import render, redirect, get_object_or_404
+from django.http import JsonResponse
+from django.contrib.auth.decorators import login_required
+from django.contrib import messages
+from django.db.models import Count, Q, Avg
+from django.utils import timezone
+from django.views.decorators.http import require_http_methods, require_GET
+from django.contrib.auth.hashers import check_password, make_password
+from django.contrib.auth.models import Group
+
+# Local app imports
+from app.models import (
+ Staff,
+ Student,
+ Course,
+ CourseTracking,
+ Batch,
+ StaffLeave,
+ Notice,
+ FEEDBACK_TYPE_CHOICES,
+)
+
+
+@login_required
+def admission_officer_dashboard(request):
+ """View to display the admission officer dashboard"""
+
+ # Check if user is in staff and has Admission Officer designation
+ if (
+ not isinstance(request.user, Staff)
+ or request.user.designation != "Admission Officer"
+ ):
+ messages.error(
+ request,
+ "You don't have permission to access the admission officer dashboard.",
+ )
+ return redirect("dashboard")
+
+ # Get the admission officer object
+ try:
+ admission_officer = request.user
+ except Exception as e:
+ messages.error(
+ request,
+ "Admission Officer profile not found. Please contact administrator.",
+ )
+ return redirect("dashboard")
+
+ today = timezone.now().date()
+ last_week = today - timedelta(days=7)
+ last_month = today - timedelta(days=30)
+
+ # Get student statistics and list
+ total_students = Student.objects.count()
+ active_students = Student.objects.filter(status="Active").count()
+ new_students = Student.objects.filter(joining_date__gte=last_month).count()
+ students = (
+ Student.objects.select_related("course")
+ .prefetch_related("batches")
+ .all()
+ .order_by("-id")
+ ) # For the student list
+
+ # Get course statistics and list
+ total_courses = Course.objects.count()
+ active_courses = Course.objects.filter(is_active=True).count()
+ courses = Course.objects.all().order_by("name") # For the dropdown
+
+ # Get batch statistics and list
+ total_batches = Batch.objects.count()
+ active_batches = Batch.objects.filter(is_active=True).count()
+ batches = Batch.objects.all().order_by("-year") # For the dropdown
+
+ # Get recent course trackings
+ recent_trackings = CourseTracking.objects.select_related(
+ "student", "course"
+ ).order_by("-created_at")
+
+ # Get leave statistics
+ pending_leaves = StaffLeave.objects.filter(
+ staff=admission_officer, status=0
+ ).count()
+ approved_leaves = StaffLeave.objects.filter(
+ staff=admission_officer, status=1
+ ).count()
+ rejected_leaves = StaffLeave.objects.filter(
+ staff=admission_officer, status=2
+ ).count()
+
+ # Get recent leaves
+ recent_leaves = StaffLeave.objects.filter(staff=admission_officer).order_by(
+ "-created_at"
+ )
+
+ # Get recent notices
+ notices = Notice.objects.all().order_by("-created_at")
+
+ # Get feedback types for institute feedback
+ feedback_types = FEEDBACK_TYPE_CHOICES
+
+ context = {
+ "officer": admission_officer,
+ "title": "Admission Officer Dashboard",
+ "total_students": total_students,
+ "active_students": active_students,
+ "new_students": new_students,
+ "students": students, # For the student list
+ "total_courses": total_courses,
+ "active_courses": active_courses,
+ "courses": courses, # For the dropdown
+ "total_batches": total_batches,
+ "active_batches": active_batches,
+ "batches": batches, # For the dropdown
+ "recent_trackings": recent_trackings,
+ "pending_leaves": pending_leaves,
+ "approved_leaves": approved_leaves,
+ "rejected_leaves": rejected_leaves,
+ "recent_leaves": recent_leaves,
+ "notices": notices,
+ "feedback_types": feedback_types,
+ }
+
+ return render(request, "admission_officer/dashboard.html", context)
+
+
+@login_required
+def add_student(request):
+ """View to add a new student"""
+ if request.method != "POST":
+ messages.error(request, "Invalid request method")
+ return redirect("admissionOfficerDashboard")
+
+ # Check if user has permission
+ if (
+ not isinstance(request.user, Staff)
+ or request.user.designation != "Admission Officer"
+ ):
+ messages.error(request, "Permission denied")
+ return redirect("admissionOfficerDashboard")
+
+ try:
+ # Get form data
+ name = request.POST.get("name")
+ phone = request.POST.get("phone")
+ email = request.POST.get("email")
+ gender = request.POST.get("gender")
+ birth_date = request.POST.get("birth_date")
+ course_id = request.POST.get("course")
+ current_period = request.POST.get("current_period", 1)
+ status = request.POST.get("status", "Active")
+ parent_name = request.POST.get("parent_name")
+ parent_phone = request.POST.get("parent_phone")
+ permanent_address = request.POST.get("permanent_address")
+ temporary_address = request.POST.get("temporary_address")
+ citizenship_number = request.POST.get("citizenship_number")
+
+ # Get batch ID
+ batch_id = request.POST.get("batches")
+
+ # Get photo if uploaded
+ photo = request.FILES.get("photo")
+
+ # Validate required fields
+ if not all([name, phone, batch_id]):
+ messages.error(request, "Name, phone, and batch are required")
+ return redirect("admissionOfficerDashboard")
+
+ # Check if student with same phone already exists
+ if Student.objects.filter(phone=phone).exists():
+ messages.error(request, "Student with this phone number already exists")
+ return redirect("admissionOfficerDashboard")
+
+ # Create student object
+ student = Student()
+ student.name = name
+ student.phone = phone
+ student.email = email
+ student.gender = gender
+
+ # Set birth date if provided
+ if birth_date:
+ student.birth_date = birth_date
+
+ # Set course if provided
+ if course_id:
+ try:
+ course = Course.objects.get(id=course_id)
+ student.course = course
+ student.current_period = current_period
+ except Course.DoesNotExist:
+ messages.error(request, "Selected course not found")
+ return redirect("admissionOfficerDashboard")
+
+ student.status = status
+ student.parent_name = parent_name
+ student.parent_phone = parent_phone
+ student.permanent_address = permanent_address
+ student.temporary_address = temporary_address
+ student.citizenship_number = citizenship_number
+
+ # Set photo if uploaded
+ if photo:
+ student.image = photo
+
+ # Set default password as '123'
+ student.set_password("123")
+
+ # Save student
+ student.save()
+
+ # Add student to Student group
+ student_group, created = Group.objects.get_or_create(name="Student")
+ student.groups.add(student_group)
+
+ # Add student to batch
+ if batch_id:
+ try:
+ batch = Batch.objects.get(id=batch_id)
+ student.batches.add(batch)
+ # Joining date will be set automatically from the batch year in the signal
+ except Batch.DoesNotExist:
+ messages.error(request, "Selected batch not found")
+ # Continue anyway as we've already saved the student
+
+ # Create course tracking if course is assigned
+ if course_id:
+ try:
+ # Use today's date if no joining date yet (should be set by batch signal)
+ start_date = student.joining_date or timezone.now().date()
+
+ course_tracking = CourseTracking(
+ student=student,
+ course=course,
+ start_date=start_date,
+ current_period=current_period,
+ )
+ course_tracking.save()
+ except Exception as e:
+ print(f"Error creating course tracking: {str(e)}")
+
+ messages.success(
+ request, f'Student "{name}" added successfully. Default password is: 123'
+ )
+ return redirect("admissionOfficerDashboard")
+
+ except Exception as e:
+ messages.error(request, f"Error: {str(e)}")
+ return redirect("admissionOfficerDashboard")
+
+
+@login_required
+def add_batch(request):
+ """View to add a new batch"""
+ if request.method != "POST":
+ messages.error(request, "Invalid request method")
+ return redirect("admissionOfficerDashboard")
+
+ # Check if user has permission
+ if (
+ not isinstance(request.user, Staff)
+ or request.user.designation != "Admission Officer"
+ ):
+ messages.error(request, "Permission denied")
+ return redirect("admissionOfficerDashboard")
+
+ try:
+ # Get form data
+ name = request.POST.get("name")
+ start_date = request.POST.get("start_date")
+ is_active = request.POST.get("is_active") == "on"
+
+ # Validate required fields
+ if not all([name, start_date]):
+ messages.error(request, "Name and start date are required")
+ return redirect("admissionOfficerDashboard")
+
+ # Create batch
+ batch = Batch()
+ batch.name = name
+ batch.year = start_date # Use start_date for the year field
+ batch.is_active = is_active
+ batch.save()
+
+ messages.success(request, f'Batch "{name}" added successfully')
+ return redirect("admissionOfficerDashboard")
+
+ except Exception as e:
+ messages.error(request, f"Error: {str(e)}")
+ return redirect("admissionOfficerDashboard")
+
+
+@login_required
+def add_course(request):
+ """View to add a new course"""
+ if request.method != "POST":
+ messages.error(request, "Invalid request method")
+ return redirect("admissionOfficerDashboard")
+
+ # Check if user has permission
+ if (
+ not isinstance(request.user, Staff)
+ or request.user.designation != "Admission Officer"
+ ):
+ messages.error(request, "Permission denied")
+ return redirect("admissionOfficerDashboard")
+
+ try:
+ # Get form data
+ name = request.POST.get("name")
+ code = request.POST.get("code", "")
+ duration_type = request.POST.get("duration_type")
+ duration = request.POST.get("duration")
+ description = request.POST.get("description", "")
+ is_active = request.POST.get("is_active") == "on"
+
+ # Validate required fields
+ if not all([name, duration_type, duration]):
+ messages.error(request, "Name, duration type, and duration are required")
+ return redirect("admissionOfficerDashboard")
+
+ # Create course
+ course = Course()
+ course.name = name
+ course.code = code
+ course.duration_type = duration_type
+ course.duration = duration
+ course.description = description
+ course.is_active = is_active
+ course.save()
+
+ messages.success(request, f'Course "{name}" added successfully')
+ return redirect("admissionOfficerDashboard")
+
+ except Exception as e:
+ messages.error(request, f"Error: {str(e)}")
+ return redirect("admissionOfficerDashboard")
+
+
+@login_required
+def add_course_tracking(request):
+ """View to add course tracking for a student"""
+ if request.method != "POST":
+ messages.error(request, "Invalid request method")
+ return redirect("admissionOfficerDashboard")
+
+ # Check if user has permission
+ if (
+ not isinstance(request.user, Staff)
+ or request.user.designation != "Admission Officer"
+ ):
+ messages.error(request, "Permission denied")
+ return redirect("admissionOfficerDashboard")
+
+ try:
+ # Get form data
+ student_id = request.POST.get("student")
+ course_id = request.POST.get("course")
+ start_date = request.POST.get("start_date")
+ expected_end_date = request.POST.get("expected_end_date")
+ current_period = request.POST.get("current_period", 1)
+ progress_status = request.POST.get("progress_status", "In Progress")
+ notes = request.POST.get("notes")
+
+ # Validate required fields
+ if not student_id or not course_id:
+ messages.error(request, "Student and course are required")
+ return redirect("admissionOfficerDashboard")
+
+ # Check if student exists
+ try:
+ student = Student.objects.get(id=student_id)
+ except Student.DoesNotExist:
+ messages.error(request, "Student not found")
+ return redirect("admissionOfficerDashboard")
+
+ # Check if course exists
+ try:
+ course = Course.objects.get(id=course_id)
+ except Course.DoesNotExist:
+ messages.error(request, "Course not found")
+ return redirect("admissionOfficerDashboard")
+
+ # Check if tracking already exists
+ if CourseTracking.objects.filter(student=student, course=course).exists():
+ messages.error(request, "Student is already enrolled in this course")
+ return redirect("admissionOfficerDashboard")
+
+ # Create course tracking object
+ tracking = CourseTracking()
+ tracking.student = student
+ tracking.course = course
+
+ # Set dates if provided
+ if start_date:
+ tracking.start_date = start_date
+ if expected_end_date:
+ tracking.expected_end_date = expected_end_date
+
+ tracking.current_period = int(current_period)
+ tracking.progress_status = progress_status
+ tracking.notes = notes
+
+ # Save tracking
+ tracking.save()
+
+ messages.success(
+ request,
+ f'Course tracking added for student "{student.name}" in course "{course.name}"',
+ )
+ return redirect("admissionOfficerDashboard")
+
+ except Exception as e:
+ messages.error(request, f"Error: {str(e)}")
+ return redirect("admissionOfficerDashboard")
+
+
+@login_required
+def get_courses(request):
+ """View to get courses for dropdowns"""
+ try:
+ courses = Course.objects.all().order_by("name")
+ course_data = [
+ {"id": course.id, "name": course.name, "code": course.code}
+ for course in courses
+ ]
+
+ return JsonResponse({"success": True, "courses": course_data})
+ except Exception as e:
+ return JsonResponse({"success": False, "message": str(e)}, status=500)
+
+
+@login_required
+def get_batches(request):
+ """View to get batches for dropdowns"""
+ try:
+ batches = Batch.objects.all().order_by("-year")
+ batch_data = []
+
+ for batch in batches:
+ batch_data.append(
+ {
+ "id": batch.id,
+ "name": batch.name,
+ "year": batch.year.year if batch.year else None,
+ "display_name": f"{batch.name} ({batch.year.year if batch.year else 'No year'})",
+ }
+ )
+
+ return JsonResponse({"success": True, "batches": batch_data})
+ except Exception as e:
+ return JsonResponse({"success": False, "message": str(e)}, status=500)
+
+
+@login_required
+def get_students(request):
+ """View to get students for dropdowns"""
+ try:
+ students = Student.objects.all()
+ student_data = [
+ {"id": student.id, "name": student.name, "phone": student.phone}
+ for student in students
+ ]
+
+ return JsonResponse({"success": True, "students": student_data})
+ except Exception as e:
+ return JsonResponse({"success": False, "message": str(e)}, status=500)
+
+
+@login_required
+def get_student(request, student_id):
+ """View to get a student's details by ID"""
+ # Check if user has permission
+ if (
+ not isinstance(request.user, Staff)
+ or request.user.designation != "Admission Officer"
+ ):
+ return JsonResponse(
+ {"success": False, "message": "Permission denied"}, status=403
+ )
+
+ try:
+ # Get student with related data
+ student = (
+ Student.objects.select_related("course")
+ .prefetch_related("batches")
+ .get(id=student_id)
+ )
+
+ # Build batches data
+ batches = []
+ for batch in student.batches.all():
+ batches.append(
+ {
+ "id": batch.id,
+ "name": batch.name,
+ "year": batch.year.year if batch.year else None,
+ }
+ )
+
+ # Build student data
+ student_data = {
+ "id": student.id,
+ "name": student.name,
+ "phone": student.phone,
+ "email": student.email,
+ "gender": student.gender,
+ "birth_date": student.birth_date.strftime("%Y-%m-%d")
+ if student.birth_date
+ else None,
+ "status": student.status,
+ "parent_name": student.parent_name,
+ "parent_phone": student.parent_phone,
+ "permanent_address": student.permanent_address,
+ "temporary_address": student.temporary_address,
+ "citizenship_number": student.citizenship_no,
+ "current_period": student.current_period,
+ "joining_date": student.joining_date.strftime("%Y-%m-%d")
+ if student.joining_date
+ else None,
+ "image": student.image.url if student.image else None,
+ "course": {"id": student.course.id, "name": student.course.name}
+ if student.course
+ else None,
+ "batches": batches,
+ }
+
+ return JsonResponse({"success": True, "student": student_data})
+ except Student.DoesNotExist:
+ return JsonResponse(
+ {"success": False, "message": "Student not found"}, status=404
+ )
+ except Exception as e:
+ return JsonResponse({"success": False, "message": str(e)}, status=500)
+
+
+@login_required
+def edit_student(request):
+ """View to edit a student's details"""
+ if request.method != "POST":
+ return JsonResponse(
+ {"success": False, "message": "Invalid request method"}, status=405
+ )
+
+ # Check if user has permission
+ if (
+ not isinstance(request.user, Staff)
+ or request.user.designation != "Admission Officer"
+ ):
+ return JsonResponse(
+ {"success": False, "message": "Permission denied"}, status=403
+ )
+
+ try:
+ # Get student ID from form data
+ student_id = request.POST.get("student_id")
+ if not student_id:
+ return JsonResponse(
+ {"success": False, "message": "Student ID is required"}, status=400
+ )
+
+ # Get student object
+ student = get_object_or_404(Student, id=student_id)
+
+ # Update student fields
+ student.name = request.POST.get("name", student.name)
+ student.phone = request.POST.get("phone", student.phone)
+ student.email = request.POST.get("email", student.email)
+ student.gender = request.POST.get("gender", student.gender)
+
+ # Handle birth date
+ birth_date = request.POST.get("birth_date")
+ if birth_date:
+ student.birth_date = birth_date
+
+ # Handle course
+ course_id = request.POST.get("course")
+ if course_id:
+ try:
+ course = Course.objects.get(id=course_id)
+ student.course = course
+ except Course.DoesNotExist:
+ pass
+
+ # Handle status
+ status = request.POST.get("status")
+ if status:
+ student.status = status
+
+ # Handle parent info
+ student.parent_name = request.POST.get("parent_name", student.parent_name)
+ student.parent_phone = request.POST.get("parent_phone", student.parent_phone)
+
+ # Handle address
+ student.permanent_address = request.POST.get(
+ "permanent_address", student.permanent_address
+ )
+
+ # Handle citizenship
+ student.citizenship_no = request.POST.get(
+ "citizenship_number", student.citizenship_no
+ )
+
+ # Save student
+ student.save()
+
+ # Handle batch
+ batch_id = request.POST.get("batches")
+ if batch_id:
+ try:
+ batch = Batch.objects.get(id=batch_id)
+ # Clear existing batches and add the new one
+ student.batches.clear()
+ student.batches.add(batch)
+ except Batch.DoesNotExist:
+ pass
+
+ return JsonResponse(
+ {"success": True, "message": "Student updated successfully"}
+ )
+ except Exception as e:
+ return JsonResponse({"success": False, "message": str(e)}, status=500)
+
+
+@login_required
+def get_course_tracking(request, tracking_id):
+ """View to get a course tracking's details by ID"""
+ # Check if user has permission
+ if (
+ not isinstance(request.user, Staff)
+ or request.user.designation != "Admission Officer"
+ ):
+ return JsonResponse(
+ {"success": False, "message": "Permission denied"}, status=403
+ )
+
+ try:
+ # Get tracking with related data
+ tracking = CourseTracking.objects.select_related("student", "course").get(
+ id=tracking_id
+ )
+
+ # Build tracking data
+ tracking_data = {
+ "id": tracking.id,
+ "student": {"id": tracking.student.id, "name": tracking.student.name},
+ "course": {"id": tracking.course.id, "name": tracking.course.name},
+ "start_date": tracking.start_date.strftime("%Y-%m-%d"),
+ "expected_end_date": tracking.expected_end_date.strftime("%Y-%m-%d")
+ if tracking.expected_end_date
+ else None,
+ "completion_percentage": tracking.completion_percentage,
+ "progress_status": tracking.progress_status,
+ "current_period": tracking.current_period,
+ "notes": tracking.notes,
+ "current_period_display": tracking.current_period_display,
+ }
+
+ return JsonResponse({"success": True, "tracking": tracking_data})
+ except CourseTracking.DoesNotExist:
+ return JsonResponse(
+ {"success": False, "message": "Course tracking not found"}, status=404
+ )
+ except Exception as e:
+ return JsonResponse({"success": False, "message": str(e)}, status=500)
+
+
+@login_required
+def edit_course_tracking(request):
+ """View to edit a course tracking"""
+ if request.method != "POST":
+ return JsonResponse(
+ {"success": False, "message": "Invalid request method"}, status=405
+ )
+
+ # Check if user has permission
+ if (
+ not isinstance(request.user, Staff)
+ or request.user.designation != "Admission Officer"
+ ):
+ return JsonResponse(
+ {"success": False, "message": "Permission denied"}, status=403
+ )
+
+ try:
+ # Get tracking ID from form data
+ tracking_id = request.POST.get("tracking_id")
+ if not tracking_id:
+ return JsonResponse(
+ {"success": False, "message": "Tracking ID is required"}, status=400
+ )
+
+ # Get tracking object
+ tracking = get_object_or_404(CourseTracking, id=tracking_id)
+
+ # Update tracking fields
+ start_date = request.POST.get("start_date")
+ if start_date:
+ tracking.start_date = start_date
+
+ expected_end_date = request.POST.get("expected_end_date")
+ if expected_end_date:
+ tracking.expected_end_date = expected_end_date
+
+ current_period = request.POST.get("current_period")
+ if current_period:
+ tracking.current_period = int(current_period)
+
+ progress_status = request.POST.get("progress_status")
+ if progress_status:
+ tracking.progress_status = progress_status
+
+ completion_percentage = request.POST.get("completion_percentage")
+ if completion_percentage:
+ tracking.completion_percentage = int(completion_percentage)
+
+ notes = request.POST.get("notes")
+ if notes is not None: # Allow empty notes
+ tracking.notes = notes
+
+ # Save tracking
+ tracking.save()
+
+ return JsonResponse(
+ {"success": True, "message": "Course tracking updated successfully"}
+ )
+ except Exception as e:
+ return JsonResponse({"success": False, "message": str(e)}, status=500)
diff --git a/app/apps.py b/app/apps.py
index eb38975..0d606d9 100644
--- a/app/apps.py
+++ b/app/apps.py
@@ -1,9 +1,12 @@
+# Core Django imports
from django.apps import AppConfig
+from django.db.models.signals import post_migrate
+from django.dispatch import receiver
class AppConfig(AppConfig):
- default_auto_field = 'django.db.models.BigAutoField'
- name = 'app'
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "app"
def ready(self):
import app.signals # Import signals when app is ready
diff --git a/app/auth.py b/app/auth.py
index c945d79..fbe3170 100644
--- a/app/auth.py
+++ b/app/auth.py
@@ -1,11 +1,17 @@
-from django.shortcuts import render, redirect
-from django.contrib.auth import login
+# Core Django imports
+from django.contrib.auth import authenticate, login
+from django.shortcuts import redirect, render
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.models import User
from django.contrib.auth.hashers import make_password
+from django.contrib import messages
+
+
import uuid
-from .models import Student, Staff
-from .utils import (
+
+# Local app imports
+from app.models import Student, Staff, Parent, Institute
+from app.utils import (
generate_otp,
generate_secret_key,
store_secret_key,
@@ -16,27 +22,41 @@ from .utils import (
store_reset_token,
verify_reset_token
)
-from django.contrib import messages
+
+
def reset_password_options(request):
"""View for selecting between phone and email password reset"""
- return render(request, "login/reset_options.html")
+ # Get institute for logo
+ institute = Institute.objects.first()
+ return render(request, "login/reset_options.html", {"institute": institute})
def password_reset_phone(request):
"""View for initiating password reset with phone number"""
+ # Get institute for logo
+ institute = Institute.objects.first()
+
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})
+ return render(request, "login/password_reset_phone.html", {
+ "error": "Please enter a valid phone number",
+ "is_email": False,
+ "institute": institute
+ })
# 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})
+ return render(request, "login/password_reset_phone.html", {
+ "error": "No account found with this phone number",
+ "is_email": False,
+ "institute": institute
+ })
# Generate secret key and store it
secret_key = generate_secret_key()
@@ -48,14 +68,28 @@ def password_reset_phone(request):
if send_result:
# Redirect to OTP verification page
- return render(request, "login/password_reset_otp.html", {"phone": phone, "is_email": False})
+ return render(request, "login/password_reset_otp.html", {
+ "phone": phone,
+ "is_email": False,
+ "institute": institute
+ })
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", {
+ "error": "Failed to send OTP. Please try again.",
+ "is_email": False,
+ "institute": institute
+ })
- return render(request, "login/password_reset_phone.html", {"is_email": False})
+ return render(request, "login/password_reset_phone.html", {
+ "is_email": False,
+ "institute": institute
+ })
def password_reset_phone_verify(request):
"""View for verifying OTP for password reset"""
+ # Get institute for logo
+ institute = Institute.objects.first()
+
if request.method == "POST":
phone = request.POST.get("phone")
otp = request.POST.get("otp")
@@ -64,7 +98,8 @@ def password_reset_phone_verify(request):
return render(request, "login/password_reset_otp.html", {
"error": "Phone and OTP are required",
"phone": phone,
- "is_email": False
+ "is_email": False,
+ "institute": institute
})
# Verify OTP
@@ -78,13 +113,15 @@ def password_reset_phone_verify(request):
return render(request, "login/password_reset_set.html", {
"phone": phone,
"token": token,
- "is_email": False
+ "is_email": False,
+ "institute": institute
})
else:
return render(request, "login/password_reset_otp.html", {
"error": "Invalid or expired OTP",
"phone": phone,
- "is_email": False
+ "is_email": False,
+ "institute": institute
})
# If GET request, redirect to phone login
@@ -123,19 +160,30 @@ def resend_phone_otp(request):
def password_reset_email(request):
"""View for initiating password reset with email"""
+ # Get institute for logo
+ institute = Institute.objects.first()
+
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})
+ return render(request, "login/email_reset.html", {
+ "error": "Please enter a valid email address",
+ "is_email": True,
+ "institute": institute
+ })
# 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})
+ return render(request, "login/email_reset.html", {
+ "error": "No account found with this email address",
+ "is_email": True,
+ "institute": institute
+ })
# Get the user
user = student or staff
@@ -143,14 +191,28 @@ def password_reset_email(request):
# 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})
+ return render(request, "login/password_reset_otp.html", {
+ "email": email,
+ "is_email": True,
+ "institute": institute
+ })
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", {
+ "error": "Failed to send verification code. Please try again.",
+ "is_email": True,
+ "institute": institute
+ })
- return render(request, "login/email_reset.html", {"is_email": True})
+ return render(request, "login/email_reset.html", {
+ "is_email": True,
+ "institute": institute
+ })
def password_reset_email_verify(request):
"""View for verifying reset code for email"""
+ # Get institute for logo
+ institute = Institute.objects.first()
+
if request.method == "POST":
email = request.POST.get("email")
code = request.POST.get("otp")
@@ -159,7 +221,8 @@ def password_reset_email_verify(request):
return render(request, "login/password_reset_otp.html", {
"error": "Email and verification code are required",
"email": email,
- "is_email": True
+ "is_email": True,
+ "institute": institute
})
# Verify code
@@ -173,14 +236,16 @@ def password_reset_email_verify(request):
return render(request, "login/password_reset_set.html", {
"email": email,
"token": token,
- "is_email": True
+ "is_email": True,
+ "institute": institute
})
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."
+ "message": "Please check your email for the verification code.",
+ "institute": institute
})
# For GET request, show the verification form with email pre-filled
@@ -188,7 +253,8 @@ def password_reset_email_verify(request):
return render(request, "login/password_reset_otp.html", {
"email": email,
"is_email": True,
- "message": "Please check your email for the verification code."
+ "message": "Please check your email for the verification code.",
+ "institute": institute
})
def resend_email_code(request):
@@ -229,6 +295,9 @@ def resend_email_code(request):
def set_new_password(request):
"""View for setting new password after verification"""
+ # Get institute for logo
+ institute = Institute.objects.first()
+
if request.method == "POST":
phone = request.POST.get("phone")
email = request.POST.get("email")
@@ -246,7 +315,8 @@ def set_new_password(request):
"phone": phone,
"email": email,
"token": token,
- "is_email": is_email
+ "is_email": is_email,
+ "institute": institute
})
# Verify token is valid
@@ -256,7 +326,8 @@ def set_new_password(request):
"phone": phone,
"email": email,
"token": token,
- "is_email": is_email
+ "is_email": is_email,
+ "institute": institute
})
# Check passwords match
@@ -266,7 +337,8 @@ def set_new_password(request):
"phone": phone,
"email": email,
"token": token,
- "is_email": is_email
+ "is_email": is_email,
+ "institute": institute
})
# Find the user
@@ -284,7 +356,8 @@ def set_new_password(request):
"phone": phone,
"email": email,
"token": token,
- "is_email": is_email
+ "is_email": is_email,
+ "institute": institute
})
# Update password
@@ -295,4 +368,40 @@ def set_new_password(request):
messages.success(request, "Password has been reset successfully. Please login with your new password.")
return redirect("login")
- return render(request, "login/password_reset_set.html", {"is_email": False})
\ No newline at end of file
+ return render(request, "login/password_reset_set.html", {"is_email": False, "institute": institute})
+
+def custom_login(request):
+ """Custom login view to handle authentication"""
+ if request.method == "POST":
+ username = request.POST.get("username")
+ password = request.POST.get("password")
+
+ # Validate phone number format if it's a phone number
+ if username.isdigit():
+ if len(username) != 10:
+ # Get institute for logo
+ institute = Institute.objects.first()
+ return render(request, "login/login.html", {
+ "error": "Phone number must be 10 digits",
+ "username": username,
+ "institute": institute
+ })
+
+ user = authenticate(request, username=username, password=password)
+
+ if user is not None:
+ login(request, user)
+ next_url = request.POST.get("next", "/app/dashboard/")
+ return redirect(next_url)
+ else:
+ # Get institute for logo
+ institute = Institute.objects.first()
+ return render(request, "login/login.html", {
+ "error": "Invalid credentials",
+ "username": username,
+ "institute": institute
+ })
+
+ # Get institute for logo
+ institute = Institute.objects.first()
+ return render(request, "login/login.html", {"institute": institute})
diff --git a/app/backends.py b/app/backends.py
index 66e20be..416b341 100644
--- a/app/backends.py
+++ b/app/backends.py
@@ -1,6 +1,11 @@
-from django.contrib.auth.models import User
+# Core Django imports
from django.contrib.auth.backends import ModelBackend
-from .models import Staff, Student
+from django.contrib.auth.models import Group, User
+from django.db.models import Q
+
+# Local app imports
+from app.models import Parent, Staff, Student
+
class MultiModelBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
@@ -9,30 +14,36 @@ class MultiModelBackend(ModelBackend):
"""
if username is None or password is None:
return None
-
- # Try authenticating as Staff by phone
- # Use filter().first() to avoid MultipleObjectsReturned error
+
+ # Try authenticating as Staff by phone first
staff = Staff.objects.filter(phone=username).first()
- if staff and staff.check_password(password):
- return staff
+ if staff:
+ if staff.check_password(password):
+ return staff
# Try authenticating as Staff by email
- # Use filter().first() to avoid MultipleObjectsReturned error
staff = Staff.objects.filter(email=username).first()
- if staff and staff.check_password(password):
- return staff
+ if staff:
+ if staff.check_password(password):
+ return staff
+
+ # Try authenticating as Parent by phone
+ parent = Parent.objects.filter(phone=username).first()
+ if parent:
+ if parent.check_password(password):
+ return parent
# Try authenticating as Student by phone
- # Use filter().first() to avoid MultipleObjectsReturned error
student = Student.objects.filter(phone=username).first()
- if student and student.check_password(password):
- return student
+ if student:
+ if student.check_password(password):
+ return student
# Try authenticating as Student by email
- # Use filter().first() to handle multiple students with same email
student = Student.objects.filter(email=username).first()
- if student and student.check_password(password):
- return student
+ if student:
+ if student.check_password(password):
+ return student
# Return None if no match
return None
@@ -41,7 +52,7 @@ class MultiModelBackend(ModelBackend):
"""
Retrieve a user instance using their ID.
"""
- for model in [Staff, Student]:
+ for model in [Staff, Student, Parent]:
try:
return model.objects.get(pk=user_id)
except model.DoesNotExist:
@@ -54,16 +65,15 @@ class MultiModelBackend(ModelBackend):
"""
if not user_obj.is_active or user_obj.is_anonymous or obj is not None:
return set()
-
+
if isinstance(user_obj, (Staff, Student)):
- if not hasattr(user_obj, '_group_perm_cache'):
+ if not hasattr(user_obj, "_group_perm_cache"):
perms = set()
for group in user_obj.groups.all():
perms.update(
- "%s.%s" % (ct, name)
+ "%s.%s" % (ct, name)
for ct, name in group.permissions.values_list(
- 'content_type__app_label',
- 'codename'
+ "content_type__app_label", "codename"
)
)
user_obj._group_perm_cache = perms
@@ -74,7 +84,7 @@ class MultiModelBackend(ModelBackend):
def get_all_permissions(self, user_obj, obj=None):
if not user_obj.is_active or user_obj.is_anonymous or obj is not None:
return set()
-
+
if isinstance(user_obj, (Staff, Student)):
return self.get_group_permissions(user_obj, obj)
else:
@@ -83,7 +93,7 @@ class MultiModelBackend(ModelBackend):
def has_perm(self, user_obj, perm, obj=None):
if not user_obj.is_active:
return False
-
+
if isinstance(user_obj, (Staff, Student)):
return perm in self.get_all_permissions(user_obj, obj)
else:
diff --git a/app/error_handlers.py b/app/error_handlers.py
new file mode 100644
index 0000000..8c70f39
--- /dev/null
+++ b/app/error_handlers.py
@@ -0,0 +1,27 @@
+from django.shortcuts import render
+
+
+# Error handler views
+def handler400(request, exception=None):
+ """Bad request error handler"""
+ return render(request, "400.html", status=400)
+
+
+def handler403(request, exception=None):
+ """Forbidden error handler"""
+ return render(request, "403.html", status=403)
+
+
+def handler404(request, exception=None):
+ """Page not found error handler"""
+ return render(request, "404.html", status=404)
+
+
+def handler500(request):
+ """Server error handler"""
+ return render(request, "500.html", status=500)
+
+
+def handler505(request, exception=None):
+ """HTTP version not supported error handler"""
+ return render(request, "505.html", status=505)
diff --git a/app/firebase.py b/app/firebase.py
index 38009d2..6cb3d1e 100644
--- a/app/firebase.py
+++ b/app/firebase.py
@@ -6,13 +6,31 @@ from firebase_admin import messaging, credentials
from student_management_system.settings import BASE_DIR
+# --------------------------------------------------------------------
+# FCM Device Model
+# --------------------------------------------------------------------
+
+
class FCMDevice(models.Model):
"""Model to store Firebase Cloud Messaging device tokens"""
+
id = models.BigAutoField(primary_key=True)
token = models.TextField(unique=True)
created_at = models.DateTimeField(auto_now_add=True)
last_active = models.DateTimeField(auto_now=True)
is_active = models.BooleanField(default=True)
+ is_fallback = models.BooleanField(default=False) # Flag to identify fallback tokens
+ user_type = models.CharField(
+ max_length=20,
+ choices=[
+ ("student", "Student"),
+ ("parent", "Parent"),
+ ("teacher", "Teacher"),
+ ("admin", "Admin"),
+ ("unknown", "Unknown"),
+ ],
+ default="unknown",
+ ) # Add user type field
def __str__(self):
return f"Device {self.id} ({self.token[:20]}...)"
@@ -20,7 +38,7 @@ class FCMDevice(models.Model):
class Meta:
verbose_name = "FCM Device"
verbose_name_plural = "FCM Devices"
- ordering = ['-last_active']
+ ordering = ["-last_active"]
def deactivate(self):
"""Deactivate this device token"""
@@ -28,21 +46,64 @@ class FCMDevice(models.Model):
self.save()
+# --------------------------------------------------------------------
+# Token Management
+# --------------------------------------------------------------------
+
+
def save_fcm_token(request, token):
"""Save FCM token to database"""
try:
- # Delete any existing tokens for this user/device
- FCMDevice.objects.filter(token=token).delete()
- # Create new token
- FCMDevice.objects.create(token=token)
- return JsonResponse({'status': 'success'})
+ # Check if this is a fallback token
+ is_fallback = token.startswith("fcm-token-") or token.startswith(
+ "fallback-token-"
+ )
+
+ # Determine user type if a user is authenticated
+ user_type = "unknown"
+ if hasattr(request, "user") and request.user.is_authenticated:
+ user_groups = request.user.groups.values_list("name", flat=True)
+ if "Student" in user_groups:
+ user_type = "student"
+ elif "Parent" in user_groups:
+ user_type = "parent"
+ elif "Teacher" in user_groups:
+ user_type = "teacher"
+ elif request.user.is_superuser:
+ user_type = "admin"
+
+ # Use update_or_create to avoid unnecessary deletes
+ device, created = FCMDevice.objects.update_or_create(
+ token=token,
+ defaults={
+ "is_fallback": is_fallback,
+ "is_active": True,
+ "user_type": user_type,
+ },
+ )
+
+ return JsonResponse({"status": "success"})
except Exception as e:
- print(f"Error saving FCM token: {str(e)}")
- return JsonResponse({'error': str(e)}, status=500)
+ return JsonResponse({"error": str(e)}, status=500)
+
+
+# --------------------------------------------------------------------
+# Firebase Configuration
+# --------------------------------------------------------------------
def get_firebase_js():
"""Generate Firebase JavaScript configuration"""
+ firebase_config = {
+ "apiKey": os.getenv("FIREBASE_API_KEY", ""),
+ "authDomain": os.getenv("FIREBASE_AUTH_DOMAIN", ""),
+ "projectId": os.getenv("FIREBASE_PROJECT_ID", ""),
+ "storageBucket": os.getenv("FIREBASE_STORAGE_BUCKET", ""),
+ "messagingSenderId": os.getenv("FIREBASE_MESSAGING_SENDER_ID", ""),
+ "appId": os.getenv("FIREBASE_APP_ID", ""),
+ "measurementId": os.getenv("FIREBASE_MEASUREMENT_ID", ""),
+ }
+
data = """
importScripts('https://www.gstatic.com/firebasejs/11.0.2/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/11.0.2/firebase-messaging-compat.js');
@@ -69,25 +130,35 @@ def get_firebase_js():
return self.registration.showNotification(notificationTitle, notificationOptions);
});
""" % (
- os.getenv("FIREBASE_API_KEY", ""),
- os.getenv("FIREBASE_AUTH_DOMAIN", ""),
- os.getenv("FIREBASE_PROJECT_ID", ""),
- os.getenv("FIREBASE_STORAGE_BUCKET", ""),
- os.getenv("FIREBASE_MESSAGING_SENDER_ID", ""),
- os.getenv("FIREBASE_APP_ID", ""),
- os.getenv("FIREBASE_MEASUREMENT_ID", "")
+ firebase_config["apiKey"],
+ firebase_config["authDomain"],
+ firebase_config["projectId"],
+ firebase_config["storageBucket"],
+ firebase_config["messagingSenderId"],
+ firebase_config["appId"],
+ firebase_config["measurementId"],
)
return HttpResponse(data, content_type="application/javascript")
-# Sending Firebase Push Notification
+# --------------------------------------------------------------------
+# Firebase Admin Initialization
+# --------------------------------------------------------------------
+
+# Initialize Firebase Admin
+firebase_app = None
try:
- cred = credentials.Certificate(os.path.join(BASE_DIR, "firebase-key.json"))
- firebase_app = firebase_admin.initialize_app(cred)
-except (ValueError, FileNotFoundError) as e:
- print(f"Warning: Could not initialize Firebase Admin: {e}")
-except firebase_admin.exceptions.FirebaseError as e:
- print(f"Warning: Firebase Admin initialization error: {e}")
+ cert_path = os.path.join(BASE_DIR, "firebase-key.json")
+ if os.path.exists(cert_path):
+ cred = credentials.Certificate(cert_path)
+ firebase_app = firebase_admin.initialize_app(cred)
+except Exception:
+ pass
+
+
+# --------------------------------------------------------------------
+# Push Notification Functions
+# --------------------------------------------------------------------
def send_push_notification(title, message, tokens):
@@ -95,22 +166,41 @@ def send_push_notification(title, message, tokens):
Send push notification using Firebase Cloud Messaging
Returns a tuple of (success_count, failure_count, failed_tokens)
"""
+ if not firebase_app or not tokens:
+ return 0, len(tokens) if tokens else 0, tokens
+
if not isinstance(tokens, (list, tuple)):
tokens = list(tokens) # Convert QuerySet to list
-
+
success_count = 0
failure_count = 0
failed_tokens = []
-
- # Get only active devices
+
+ # Get only active devices that are not fallback tokens
active_devices = FCMDevice.objects.filter(
token__in=tokens,
- is_active=True
+ is_active=True,
+ is_fallback=False, # Skip fallback tokens for push notifications
)
-
+
+ # Count fallback tokens as "success" for reporting purposes
+ fallback_count = FCMDevice.objects.filter(
+ token__in=tokens, is_active=True, is_fallback=True
+ ).count()
+
+ success_count += fallback_count
+
for device in active_devices:
try:
- response = messaging.send(
+ # Skip tokens that match our fallback pattern
+ if device.token.startswith("fcm-token-") or device.token.startswith(
+ "fallback-token-"
+ ):
+ device.is_fallback = True
+ device.save()
+ continue
+
+ messaging.send(
messaging.Message(
notification=messaging.Notification(
title=title,
@@ -119,17 +209,14 @@ def send_push_notification(title, message, tokens):
token=str(device.token), # Ensure token is a string
)
)
- print(f"Successfully sent message: {response}")
success_count += 1
except messaging.UnregisteredError:
# Token is no longer valid, deactivate the device
- print(f"Token {device.token} is no longer valid, deactivating device")
device.deactivate()
failure_count += 1
failed_tokens.append(device.token)
- except Exception as e:
- print(f"Failed to send message to token {device.token}: {e}")
+ except Exception:
failure_count += 1
failed_tokens.append(device.token)
-
+
return success_count, failure_count, failed_tokens
diff --git a/app/forms.py b/app/forms.py
index 4c92070..237640e 100644
--- a/app/forms.py
+++ b/app/forms.py
@@ -1,65 +1,174 @@
-from django import forms
-from django.contrib.auth.hashers import make_password
-from django.contrib.auth.forms import SetPasswordForm
-from .models import Staff, Student
-
-
-class CustomSetPasswordForm(SetPasswordForm):
- def save(self, commit=True):
- user = self.user
- user.set_password(self.cleaned_data["new_password1"])
- if commit:
- user.save()
- return user
-
-
-class StudentAdminForm(forms.ModelForm):
- new_password = forms.CharField(
- required=False,
- widget=forms.PasswordInput,
- help_text="Enter a new password if you want to reset it. Leave blank otherwise. Default is '123'.",
- )
-
- class Meta:
- model = Student
- fields = "__all__"
-
- def save(self, commit=True):
- student = super().save(commit=False)
- new_password = self.cleaned_data.get("new_password")
-
- if new_password:
- student.password = make_password(new_password)
- # Set default password to '123' for new students if no password provided
- elif not student.pk and not student.password: # New student without password
- student.password = make_password("123")
-
- if commit:
- student.save()
- return student
-
-
-class StaffAdminForm(forms.ModelForm):
- new_password = forms.CharField(
- required=False,
- widget=forms.PasswordInput,
- help_text="Enter a new password if you want to reset it. Leave blank otherwise. Default is '123'.",
- )
-
- class Meta:
- model = Staff
- fields = "__all__"
-
- def save(self, commit=True):
- staff = super().save(commit=False)
- new_password = self.cleaned_data.get("new_password")
-
- if new_password:
- staff.password = make_password(new_password)
- # Set default password to '123' for new staff if no password provided
- elif not staff.pk and not staff.password: # New staff without password
- staff.password = make_password("123")
-
- if commit:
- staff.save()
- return staff
+from django import forms
+from django.contrib.auth.hashers import make_password
+from django.contrib.auth.forms import SetPasswordForm
+from app.models import (
+ Staff,
+ Student,
+ Parent,
+ StaffInstituteFeedback,
+ ParentInstituteFeedback,
+)
+from django.contrib.auth.models import Group
+
+
+class CustomSetPasswordForm(SetPasswordForm):
+ def save(self, commit=True):
+ user = self.user
+ user.set_password(self.cleaned_data["new_password1"])
+ if commit:
+ user.save()
+ return user
+
+
+class StudentAdminForm(forms.ModelForm):
+ new_password = forms.CharField(
+ required=False,
+ widget=forms.PasswordInput,
+ help_text="Enter a new password if you want to reset it. Leave blank otherwise. Default is '123'.",
+ )
+
+ class Meta:
+ model = Student
+ fields = "__all__"
+
+ def save(self, commit=True):
+ student = super().save(commit=False)
+ new_password = self.cleaned_data.get("new_password")
+
+ if new_password:
+ student.set_password(new_password)
+ # Set default password to '123' for new students if no password provided
+ elif not student.pk and not student.password: # New student without password
+ student.set_password("123")
+
+ if commit:
+ student.save()
+
+ # Make sure student has Student group
+ if (
+ hasattr(student, "groups")
+ and not student.groups.filter(name="Student").exists()
+ ):
+ student_group = Group.objects.filter(name="Student").first()
+ if student_group:
+ student.groups.add(student_group)
+
+ # Handle parent creation/update for both new and existing students
+ if student.parent_name and student.parent_phone:
+ try:
+ # Check if parent already exists
+ parent = Parent.objects.filter(phone=student.parent_phone).first()
+
+ if not parent:
+ # Create new parent
+ parent = Parent.objects.create(
+ name=student.parent_name,
+ phone=student.parent_phone,
+ password=make_password("123"), # Default password
+ )
+
+ # Add Parent group to new parent
+ parent_group = Group.objects.filter(name="Parent").first()
+ if parent_group:
+ parent.groups.add(parent_group)
+
+ # Add student to parent's students
+ parent.students.add(student)
+
+ except Exception as e:
+ # Log the error but don't prevent student creation
+ print(f"Error creating/updating parent: {str(e)}")
+
+ return student
+
+
+class StaffAdminForm(forms.ModelForm):
+ new_password = forms.CharField(
+ required=False,
+ widget=forms.PasswordInput,
+ help_text="Enter a new password if you want to reset it. Leave blank otherwise. Default is '123'.",
+ )
+
+ class Meta:
+ model = Staff
+ fields = "__all__" # Include all fields, including groups
+
+ def save(self, commit=True):
+ staff = super().save(commit=False)
+ new_password = self.cleaned_data.get("new_password")
+
+ if new_password:
+ staff.password = make_password(new_password)
+ # Set default password to '123' for new staff if no password provided
+ elif not staff.pk and not staff.password: # New staff without password
+ staff.password = make_password("123")
+
+ if commit:
+ staff.save()
+ self.save_m2m() # Save many-to-many relationships including groups
+
+ # If groups were not selected in the form, ensure staff has Teacher group
+ if not staff.groups.exists():
+ teacher_group = Group.objects.filter(name="Teacher").first()
+ if teacher_group:
+ staff.groups.add(teacher_group)
+
+ return staff
+
+
+class ParentAdminForm(forms.ModelForm):
+ new_password = forms.CharField(
+ required=False,
+ widget=forms.PasswordInput,
+ help_text="Enter a new password if you want to reset it. Leave blank otherwise. Default is '123'.",
+ )
+
+ class Meta:
+ model = Parent
+ fields = "__all__"
+
+ def save(self, commit=True):
+ parent = super().save(commit=False)
+ new_password = self.cleaned_data.get("new_password")
+
+ if new_password:
+ parent.password = make_password(new_password)
+ # Set default password to '123' for new parents if no password provided
+ elif not parent.pk and not parent.password: # New parent without password
+ parent.password = make_password("123")
+
+ if commit:
+ parent.save()
+ return parent
+
+
+class StaffInstituteFeedbackForm(forms.ModelForm):
+ class Meta:
+ model = StaffInstituteFeedback
+ fields = [
+ "feedback_type",
+ "rating",
+ "feedback_text",
+ "is_anonymous",
+ "is_public",
+ ]
+ widgets = {
+ "feedback_text": forms.Textarea(attrs={"rows": 4}),
+ "rating": forms.NumberInput(attrs={"step": "0.5", "min": "1", "max": "5"}),
+ }
+
+
+class ParentInstituteFeedbackForm(forms.ModelForm):
+ class Meta:
+ model = ParentInstituteFeedback
+ fields = [
+ "feedback_type",
+ "rating",
+ "feedback_text",
+ "is_anonymous",
+ "is_public",
+ ]
+ widgets = {
+ "feedback_text": forms.Textarea(attrs={"rows": 4}),
+ "rating": forms.NumberInput(attrs={"step": "0.5", "min": "1", "max": "5"}),
+ }
diff --git a/app/hodviews.py b/app/hodviews.py
new file mode 100644
index 0000000..1111a27
--- /dev/null
+++ b/app/hodviews.py
@@ -0,0 +1,1929 @@
+from django.shortcuts import render, get_object_or_404, redirect
+from django.contrib.auth.decorators import login_required
+from django.db.models import Count, Avg, Q
+from django.http import JsonResponse
+from django.utils import timezone
+from datetime import datetime, timedelta, time
+from django.contrib import messages
+from django.urls import reverse
+from django.http import HttpResponseRedirect
+from django.contrib.auth.hashers import make_password
+from django.contrib.auth.models import Group
+from django.views.decorators.csrf import csrf_exempt
+from django.views.decorators.http import require_http_methods
+
+from app.firebase import send_push_notification, FCMDevice
+
+from app.models import (
+ Staff,
+ Student,
+ Course,
+ Subject,
+ Routine,
+ Attendance,
+ AttendanceRecord,
+ StudentFeedback,
+ CourseTracking,
+ TeacherParentMeeting,
+ Batch,
+ Parent,
+ Notice,
+)
+
+
+@login_required
+def hod_dashboard(request):
+ """HOD dashboard view showing department overview and management options"""
+ try:
+ # Check if user is a HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ messages.error(request, "Only HODs can access this dashboard.")
+ return redirect("hodDashboard")
+
+ # Get the HOD's department (course)
+ hod = request.user
+ if not hasattr(hod, "course") or not hod.course:
+ messages.error(request, "You are not assigned as HOD of any department.")
+ return redirect("hodDashboard")
+
+ course = hod.course
+
+ # Get department statistics
+ total_students = Student.objects.filter(course=course).count()
+ total_staff = Staff.objects.filter(course=course).count()
+ total_subjects = Subject.objects.filter(course=course).count()
+
+ # Get all subjects for the course
+ subjects = Subject.objects.filter(course=course).order_by('period_or_year', 'name')
+
+ # Get routines to find teachers for each subject
+ routines = Routine.objects.filter(course=course, is_active=True).select_related(
+ "subject", "teacher"
+ )
+
+ # Create a mapping of subject_id to teacher
+ teacher_map = {}
+ for routine in routines:
+ teacher_map[routine.subject_id] = routine.teacher
+
+ # Get all teachers who teach in this course through active routines
+ staff = Staff.objects.filter(
+ id__in=routines.values_list('teacher_id', flat=True)
+ ).prefetch_related(
+ 'routine_set',
+ 'routine_set__subject'
+ ).distinct()
+
+ # Filter routines for each staff member to only show subjects from this course
+ for staff_member in staff:
+ staff_member.filtered_subjects = Subject.objects.filter(
+ course=course,
+ id__in=staff_member.routine_set.filter(is_active=True).values_list('subject_id', flat=True)
+ )
+
+ # Get today's classes and their attendance
+ today = timezone.now().date()
+ today_classes = Routine.objects.filter(
+ course=course,
+ is_active=True
+ ).select_related("subject", "teacher")
+
+ # Get attendance for today's classes
+ today_attendance = Attendance.objects.filter(
+ routine__course=course,
+ date=today
+ ).select_related("routine")
+
+ # Create a dictionary of routine_id to attendance
+ attendance_map = {att.routine_id: att for att in today_attendance}
+
+ # Attach attendance to each class
+ for class_obj in today_classes:
+ class_obj.attendance = attendance_map.get(class_obj.id)
+
+ # Get all student progress records for display in the progress section
+ student_progress_all = (
+ CourseTracking.objects.filter(student__course=course)
+ .select_related("student")
+ .order_by("-completion_percentage")
+ )
+
+ # Calculate progress metrics
+ avg_completion = student_progress_all.aggregate(Avg('completion_percentage'))['completion_percentage__avg'] or 0
+ avg_completion = round(avg_completion, 1)
+ students_on_track = student_progress_all.filter(completion_percentage__gte=75).count()
+ students_at_risk = student_progress_all.filter(completion_percentage__lt=40).count()
+
+ # For the dashboard overview, still show just top 5
+ student_progress = student_progress_all[:5]
+
+ # Get all meetings
+ meetings = TeacherParentMeeting.objects.all().order_by("-meeting_date", "-meeting_time")
+
+ # Get recent feedback
+ recent_feedback = (
+ StudentFeedback.objects.filter(student__course=course)
+ .select_related("student", "teacher")
+ .order_by("-created_at")[:5]
+ )
+
+ # Get all students in the course
+ students = Student.objects.filter(course=course).order_by('name')
+
+ # Get all active batches
+ batches = Batch.objects.filter(is_active=True).order_by('-year')
+
+ # Get notices
+ notices = Notice.objects.all().order_by('-created_at')
+
+ context = {
+ "title": "HOD Dashboard",
+ "hod": hod,
+ "course": course,
+ "total_students": total_students,
+ "total_staff": total_staff,
+ "total_subjects": total_subjects,
+ "today_classes": today_classes,
+ "student_progress": student_progress,
+ "student_progress_all": student_progress_all, # All student progress records
+ "avg_completion": avg_completion,
+ "students_on_track": students_on_track,
+ "students_at_risk": students_at_risk,
+ "meetings": meetings, # Changed from upcoming_meetings to all meetings
+ "recent_feedback": recent_feedback,
+ "subjects": subjects,
+ "staff": staff,
+ "students": students,
+ "batches": batches,
+ "notices": notices,
+ "class_routines": routines, # Add routines to the context
+ }
+
+ return render(request, "hod/dashboard.html", context)
+
+ except Exception as e:
+ messages.error(request, f"An error occurred: {str(e)}")
+ return redirect("hodDashboard")
+
+@login_required
+def add_subject(request):
+ """Add a new subject to the HOD's course"""
+ try:
+ # Check if user is a HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can add subjects."})
+
+ # Get the HOD's course
+ hod = request.user
+ if not hasattr(hod, "course") or not hod.course:
+ return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."})
+
+ course = hod.course
+
+ if request.method == "POST":
+ # Get form data
+ name = request.POST.get("name")
+ code = request.POST.get("code")
+ period_or_year = request.POST.get("period_or_year")
+
+ # Validate required fields
+ if not name or not period_or_year:
+ return JsonResponse({"success": False, "message": "Subject name and period/year are required."})
+
+ try:
+ period_or_year = int(period_or_year)
+ except ValueError:
+ return JsonResponse({"success": False, "message": "Invalid period/year value."})
+
+ # Validate period/year against course duration
+ max_periods = course.duration * 2 if course.duration_type == "Semester" else course.duration
+ if period_or_year < 1 or period_or_year > max_periods:
+ return JsonResponse({"success": False, "message": f"Period/Year must be between 1 and {max_periods}."})
+
+ # Create the subject
+ subject = Subject(
+ name=name,
+ code=code,
+ period_or_year=period_or_year,
+ course=course
+ )
+
+ # Handle syllabus PDF if provided
+ if request.FILES.get('syllabus_pdf'):
+ subject.syllabus_pdf = request.FILES['syllabus_pdf']
+
+ subject.save()
+
+ return JsonResponse({"success": True, "message": "Subject added successfully."})
+
+ return JsonResponse({"success": False, "message": "Invalid request method."})
+
+ except Exception as e:
+ return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"})
+
+@login_required
+def view_subject(request, subject_id):
+ """View details of a specific subject"""
+ try:
+ # Check if user is a HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can view subject details."})
+
+ # Get the HOD's course
+ hod = request.user
+ if not hasattr(hod, "course") or not hod.course:
+ return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."})
+
+ # Get the subject and ensure it belongs to the HOD's course
+ subject = get_object_or_404(Subject, id=subject_id, course=hod.course)
+
+ # Get the assigned teacher from routine if any
+ teacher = None
+ routine = Routine.objects.filter(subject=subject, is_active=True).first()
+ if routine:
+ teacher = routine.teacher
+
+ # Prepare response data
+ response_data = {
+ "success": True,
+ "subject": {
+ "id": subject.id,
+ "name": subject.name,
+ "code": subject.code or "N/A",
+ "course": {
+ "id": subject.course.id,
+ "name": subject.course.name,
+ "duration_type": subject.course.duration_type,
+ "duration": subject.course.duration
+ },
+ "period_or_year": subject.period_or_year,
+ "teacher": {
+ "id": teacher.id,
+ "name": teacher.name
+ } if teacher else None,
+ "syllabus_pdf": subject.syllabus_pdf.url if subject.syllabus_pdf else None
+ }
+ }
+
+ return JsonResponse(response_data)
+
+ except Exception as e:
+ return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"})
+
+@login_required
+def edit_subject(request, subject_id):
+ """Edit an existing subject"""
+ try:
+ # Check if user is a HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can edit subjects."})
+
+ # Get the HOD's course
+ hod = request.user
+ if not hasattr(hod, "course") or not hod.course:
+ return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."})
+
+ # Get the subject and ensure it belongs to the HOD's course
+ subject = get_object_or_404(Subject, id=subject_id, course=hod.course)
+
+ if request.method == "POST":
+ # Get form data
+ name = request.POST.get("name")
+ code = request.POST.get("code")
+ period_or_year = request.POST.get("period_or_year")
+ teacher_id = request.POST.get("teacher")
+ description = request.POST.get("description")
+
+ # Validate required fields
+ if not name or not period_or_year:
+ return JsonResponse({"success": False, "message": "Subject name and period/year are required."})
+
+ try:
+ period_or_year = int(period_or_year)
+ except ValueError:
+ return JsonResponse({"success": False, "message": "Invalid period/year value."})
+
+ # Validate period/year against course duration
+ max_periods = subject.course.duration * 2 if subject.course.duration_type == "Semester" else subject.course.duration
+ if period_or_year < 1 or period_or_year > max_periods:
+ return JsonResponse({"success": False, "message": f"Period/Year must be between 1 and {max_periods}."})
+
+ # Update subject details
+ subject.name = name
+ subject.code = code
+ subject.period_or_year = period_or_year
+ subject.description = description
+
+ # Handle syllabus PDF upload if provided
+ if request.FILES.get('syllabus_pdf'):
+ subject.syllabus_pdf = request.FILES['syllabus_pdf']
+
+ subject.save()
+
+ # Update or create routine for teacher assignment
+ if teacher_id:
+ try:
+ teacher = Staff.objects.get(id=teacher_id, course=hod.course)
+ routine, created = Routine.objects.get_or_create(
+ subject=subject,
+ course=hod.course,
+ defaults={
+ 'teacher': teacher,
+ 'is_active': True
+ }
+ )
+ if not created:
+ routine.teacher = teacher
+ routine.save()
+ except Staff.DoesNotExist:
+ return JsonResponse({"success": False, "message": "Invalid teacher selected."})
+
+ return JsonResponse({"success": True, "message": "Subject updated successfully."})
+
+ return JsonResponse({"success": False, "message": "Invalid request method."})
+
+ except Exception as e:
+ return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"})
+
+@login_required(login_url='login')
+def delete_subject(request, subject_id):
+ try:
+ subject = Subject.objects.get(id=subject_id)
+ # Check if the subject is being used in any routines
+ if Routine.objects.filter(subject=subject).exists():
+ messages.error(request, 'Cannot delete subject as it is being used in routines.')
+ return redirect('hodDashboard')
+
+ # Delete the subject
+ subject.delete()
+ messages.success(request, 'Subject deleted successfully.')
+ return redirect('hodDashboard')
+ except Subject.DoesNotExist:
+ messages.error(request, 'Subject not found.')
+ return redirect('hodDashboard')
+ except Exception as e:
+ messages.error(request, f'Error deleting subject: {str(e)}')
+ return redirect('hodDashboard')
+
+@login_required
+def add_staff(request):
+ """Add a new staff member"""
+ try:
+ # Check if user is a HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can add staff members."})
+
+ # Get the HOD's course
+ hod = request.user
+ if not hasattr(hod, "course") or not hod.course:
+ return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."})
+
+ course = hod.course
+
+ if request.method == "POST":
+ # Get form data
+ name = request.POST.get("name")
+ email = request.POST.get("email")
+ phone = request.POST.get("phone")
+ gender = request.POST.get("gender")
+ birth_date = request.POST.get("birth_date")
+ temporary_address = request.POST.get("temporary_address")
+ permanent_address = request.POST.get("permanent_address")
+ marital_status = request.POST.get("marital_status")
+ citizenship_no = request.POST.get("citizenship_no")
+ subject_ids = request.POST.getlist("subjects[]") # Updated to match the form field name
+
+ # Validate required fields
+ if not name or not phone:
+ return JsonResponse({"success": False, "message": "Name and phone are required fields."})
+
+ if not subject_ids:
+ return JsonResponse({"success": False, "message": "Please assign at least one subject."})
+
+ # Check if phone number already exists
+ if Staff.objects.filter(phone=phone).exists():
+ return JsonResponse({"success": False, "message": "Phone number already exists."})
+
+ # Create the staff member with default password and current date as joining date
+ staff = Staff(
+ name=name,
+ email=email,
+ phone=phone,
+ gender=gender,
+ birth_date=birth_date if birth_date else None,
+ temporary_address=temporary_address,
+ permanent_address=permanent_address,
+ marital_status=marital_status,
+ citizenship_no=citizenship_no,
+ password=make_password("123"), # Default password
+ is_active=True,
+ joining_date=timezone.now().date(), # Set joining date to current date
+ course=course, # Assign to HOD's course
+ designation="Teacher" # Set designation as Teacher
+ )
+
+ staff.save()
+
+ # Add to Teacher group
+ try:
+ teacher_group = Group.objects.get(name="Teacher")
+ staff.groups.add(teacher_group)
+ except Group.DoesNotExist:
+ # Create Teacher group if it doesn't exist
+ teacher_group = Group.objects.create(name="Teacher")
+ staff.groups.add(teacher_group)
+
+ # Create routines for assigned subjects with default time slots
+ default_start_time = time(10, 0) # 10:00 AM
+ default_end_time = time(11, 0) # 11:00 AM
+
+ for subject_id in subject_ids:
+ try:
+ subject = Subject.objects.get(id=subject_id, course=course)
+ Routine.objects.create(
+ teacher=staff,
+ subject=subject,
+ course=course,
+ period_or_year=subject.period_or_year,
+ start_time=default_start_time,
+ end_time=default_end_time,
+ is_active=True
+ )
+ except Subject.DoesNotExist:
+ continue
+
+ return JsonResponse({"success": True, "message": "Staff member added successfully."})
+
+ return JsonResponse({"success": False, "message": "Invalid request method."})
+
+ except Exception as e:
+ return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"})
+
+@login_required
+def view_staff(request, staff_id):
+ """View details of a staff member"""
+ try:
+ # Check if user is a HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can view staff details."})
+
+ # Get the HOD's course
+ hod = request.user
+ if not hasattr(hod, "course") or not hod.course:
+ return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."})
+
+ course = hod.course
+
+ # Get the staff member and ensure they belong to the HOD's course
+ staff = get_object_or_404(Staff, id=staff_id)
+
+ # Get only the subjects from this course taught by this staff member
+ subjects = Subject.objects.filter(
+ course=course,
+ id__in=Routine.objects.filter(teacher=staff, is_active=True).values_list('subject_id', flat=True)
+ )
+
+ # Prepare response data with all staff fields
+ response_data = {
+ "success": True,
+ "staff": {
+ "id": staff.id,
+ "name": staff.name,
+ "email": staff.email,
+ "phone": staff.phone,
+ "designation": staff.designation,
+ "gender": staff.gender,
+ "birth_date": staff.birth_date.strftime("%Y-%m-%d") if staff.birth_date else None,
+ "temporary_address": staff.temporary_address,
+ "permanent_address": staff.permanent_address,
+ "marital_status": staff.marital_status,
+ "parent_name": staff.parent_name,
+ "parent_phone": staff.parent_phone,
+ "citizenship_no": staff.citizenship_no,
+ "passport": staff.passport,
+ "image": staff.image.url if staff.image else None,
+ "joining_date": staff.joining_date.strftime("%Y-%m-%d") if staff.joining_date else None,
+ "is_active": staff.is_active,
+ "subjects": [{"id": subject.id, "name": subject.name} for subject in subjects]
+ }
+ }
+
+ return JsonResponse(response_data)
+
+ except Exception as e:
+ print(f"Error in view_staff: {str(e)}") # Debug log
+ return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"})
+
+@login_required
+def edit_staff(request, staff_id):
+ """Edit an existing staff member"""
+ try:
+ # Check if user is a HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can edit staff members."})
+
+ # Get the HOD's course
+ hod = request.user
+ if not hasattr(hod, "course") or not hod.course:
+ return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."})
+
+ # Get the staff member and ensure they belong to the HOD's course
+ staff = get_object_or_404(Staff, id=staff_id)
+
+ if request.method == "POST":
+ # Get form data
+ name = request.POST.get("name")
+ email = request.POST.get("email")
+ phone = request.POST.get("phone")
+ is_active = request.POST.get("is_active") == "true"
+
+ # Validate required fields
+ if not name or not phone:
+ return JsonResponse({"success": False, "message": "Name and phone are required."})
+
+ # Check if phone number already exists for other staff members
+ if Staff.objects.filter(phone=phone).exclude(id=staff_id).exists():
+ return JsonResponse({"success": False, "message": "Phone number already exists."})
+
+ # Update staff details
+ staff.name = name
+ staff.email = email
+ staff.phone = phone
+ staff.is_active = is_active
+
+ # Handle profile picture upload if provided
+ if request.FILES.get('image'):
+ staff.image = request.FILES['image']
+
+ staff.save()
+
+ return JsonResponse({"success": True, "message": "Staff member updated successfully."})
+
+ return JsonResponse({"success": False, "message": "Invalid request method."})
+
+ except Exception as e:
+ return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"})
+
+@login_required
+def delete_staff(request, staff_id):
+ """Delete a staff member"""
+ try:
+ # Check if user is a HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ messages.error(request, "Only Head of Department can delete staff members")
+ return redirect("hodDashboard")
+
+ # Get the HOD's course
+ hod = request.user
+ if not hasattr(hod, "course") or not hod.course:
+ messages.error(request, "You are not assigned as HOD of any department")
+ return redirect("hodDashboard")
+
+ # Get the staff member
+ staff = get_object_or_404(Staff, id=staff_id)
+
+ # Check if staff belongs to HOD's course
+ if staff.course != hod.course:
+ messages.error(request, "You can only delete staff members from your department")
+ return redirect("hodDashboard")
+
+ # Delete all routines associated with the staff
+ staff_routines = Routine.objects.filter(teacher=staff)
+ staff_routines.delete()
+
+ # Store staff name for the success message
+ staff_name = staff.name
+
+ # Delete the staff member
+ staff.delete()
+
+ # Add success message
+ messages.success(request, f'Staff member {staff_name} has been deleted successfully')
+
+ # Redirect back to dashboard
+ return redirect("hodDashboard")
+
+ except Staff.DoesNotExist:
+ messages.error(request, "Staff member not found")
+ return redirect("hodDashboard")
+ except Exception as e:
+ messages.error(request, "An error occurred while deleting the staff member")
+ return redirect("hodDashboard")
+
+@login_required
+def add_student(request):
+ """Add a new student to the HOD's course"""
+ try:
+ # Check if user is a HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can add students."})
+
+ # Get the HOD's course
+ hod = request.user
+ if not hasattr(hod, "course") or not hod.course:
+ return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."})
+
+ course = hod.course
+
+ if request.method == "POST":
+ # Get form data
+ name = request.POST.get("name")
+ phone = request.POST.get("phone")
+ email = request.POST.get("email")
+ gender = request.POST.get("gender")
+ birth_date = request.POST.get("birth_date")
+ batch_id = request.POST.get("batch")
+ status = request.POST.get("status")
+ temporary_address = request.POST.get("temporary_address")
+ permanent_address = request.POST.get("permanent_address")
+ parent_name = request.POST.get("parent_name")
+ parent_phone = request.POST.get("parent_phone")
+ parent_address = request.POST.get("parent_address")
+ citizenship_no = request.POST.get("citizenship_no")
+
+ # Validate required fields
+ if not name or not phone or not batch_id or not status or not parent_phone:
+ return JsonResponse({"success": False, "message": "Name, phone, batch, status, and parent phone are required."})
+
+ # Check if phone already exists
+ if Student.objects.filter(phone=phone).exists():
+ return JsonResponse({"success": False, "message": "Phone number already exists."})
+
+ try:
+ # Get the batch
+ batch = Batch.objects.get(id=batch_id)
+
+ # Create or get parent account
+ try:
+ parent = Parent.objects.get(phone=parent_phone)
+ except Parent.DoesNotExist:
+ parent = Parent(
+ name=parent_name,
+ phone=parent_phone,
+ address=parent_address,
+ password=make_password("123") # Default password
+ )
+ parent.save()
+
+ # Add parent to Parent group
+ try:
+ parent_group = Group.objects.get(name="Parent")
+ parent.groups.add(parent_group)
+ except Group.DoesNotExist:
+ parent_group = Group.objects.create(name="Parent")
+ parent.groups.add(parent_group)
+
+ # Create the student
+ student = Student(
+ name=name,
+ phone=phone,
+ email=email,
+ gender=gender,
+ birth_date=birth_date if birth_date else None,
+ joining_date=batch.year if batch.year else timezone.now().date(),
+ temporary_address=temporary_address,
+ permanent_address=permanent_address,
+ parent_name=parent_name,
+ parent_phone=parent_phone,
+ citizenship_no=citizenship_no,
+ course=course,
+ status=status,
+ is_active=True,
+ password=make_password("123") # Set default password to "123"
+ )
+
+ # Save the student first to get an ID
+ student.save()
+
+ # Add student to batch
+ student.batches.add(batch)
+
+ # Add student to Student group
+ try:
+ student_group = Group.objects.get(name="Student")
+ student.groups.add(student_group)
+ except Group.DoesNotExist:
+ student_group = Group.objects.create(name="Student")
+ student.groups.add(student_group)
+
+ # Associate student with parent
+ parent.students.add(student)
+
+ return JsonResponse({
+ "success": True,
+ "message": "Student added successfully. Default password is: 123"
+ })
+
+ except Batch.DoesNotExist:
+ return JsonResponse({
+ "success": False,
+ "message": "Selected batch does not exist."
+ })
+ except Exception as e:
+ return JsonResponse({
+ "success": False,
+ "message": f"Error creating student: {str(e)}"
+ })
+
+ return JsonResponse({"success": False, "message": "Invalid request method."})
+
+ except Exception as e:
+ return JsonResponse({
+ "success": False,
+ "message": f"An error occurred: {str(e)}"
+ })
+
+@login_required
+def get_student(request, student_id):
+ """Get student details"""
+ try:
+ # Check if user is a HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can view student details."})
+
+ # Get the HOD's course
+ hod = request.user
+ if not hasattr(hod, "course") or not hod.course:
+ return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."})
+
+ # Get the student and ensure they belong to the HOD's course
+ student = get_object_or_404(Student, id=student_id)
+
+ # Prepare response data
+ response_data = {
+ "success": True,
+ "student": {
+ "id": student.id,
+ "name": student.name,
+ "email": student.email,
+ "phone": student.phone,
+ "gender": student.gender,
+ "birth_date": student.birth_date.strftime("%Y-%m-%d") if student.birth_date else None,
+ "joining_date": student.joining_date.strftime("%Y-%m-%d") if student.joining_date else None,
+ "temporary_address": student.temporary_address,
+ "permanent_address": student.permanent_address,
+ "parent_name": student.parent_name,
+ "parent_phone": student.parent_phone,
+ "citizenship_no": student.citizenship_no,
+ "image": student.image.url if student.image else None,
+ "course_name": student.course.name if student.course else None,
+ "current_period": student.current_period,
+ "is_active": student.is_active
+ }
+ }
+
+ return JsonResponse(response_data)
+
+ except Exception as e:
+ print(f"Error in get_student: {str(e)}") # Debug log
+ return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"})
+
+@login_required
+def edit_student(request, student_id):
+ """Edit an existing student"""
+ try:
+ # Check if user is a HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can edit students."})
+
+ # Get the HOD's course
+ hod = request.user
+ if not hasattr(hod, "course") or not hod.course:
+ return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."})
+
+ # Get the student and ensure they belong to the HOD's course
+ student = get_object_or_404(Student, id=student_id, course=hod.course)
+
+ if request.method == "POST":
+ # Get form data
+ name = request.POST.get("name")
+ email = request.POST.get("email")
+ phone = request.POST.get("phone")
+ gender = request.POST.get("gender")
+ birth_date = request.POST.get("birth_date")
+ batch_id = request.POST.get("batch")
+ temporary_address = request.POST.get("temporary_address")
+ permanent_address = request.POST.get("permanent_address")
+ parent_name = request.POST.get("parent_name")
+ parent_phone = request.POST.get("parent_phone")
+ citizenship_no = request.POST.get("citizenship_no")
+
+ # Validate required fields
+ if not name or not phone:
+ return JsonResponse({"success": False, "message": "Name and phone are required fields."})
+
+ # Check if email or phone already exists for other students
+ if email and Student.objects.filter(email=email).exclude(id=student_id).exists():
+ return JsonResponse({"success": False, "message": "Email already exists."})
+ if Student.objects.filter(phone=phone).exclude(id=student_id).exists():
+ return JsonResponse({"success": False, "message": "Phone number already exists."})
+
+ # Update student details
+ student.name = name
+ student.email = email
+ student.phone = phone
+ student.gender = gender
+ student.birth_date = birth_date if birth_date else None
+ student.temporary_address = temporary_address
+ student.permanent_address = permanent_address
+ student.parent_name = parent_name
+ student.parent_phone = parent_phone
+ student.citizenship_no = citizenship_no
+
+ # Update batch and set joining date based on batch if provided
+ if batch_id:
+ try:
+ batch = Batch.objects.get(id=batch_id)
+ # Clear existing batches and add the new one
+ student.batches.clear()
+ student.batches.add(batch)
+ # Set joining date based on batch year
+ student.joining_date = batch.year if batch.year else timezone.now().date()
+ except Batch.DoesNotExist:
+ pass
+
+ # Handle profile picture upload if provided
+ if request.FILES.get('image'):
+ student.image = request.FILES['image']
+
+ student.save()
+
+ return JsonResponse({"success": True, "message": "Student updated successfully."})
+
+ return JsonResponse({"success": False, "message": "Invalid request method."})
+
+ except Exception as e:
+ return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"})
+
+@login_required
+def delete_student(request, student_id):
+ """Delete a student"""
+ try:
+ # Check if user is a HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ messages.error(request, "Only HODs can delete students.")
+ return redirect('hodDashboard')
+
+ # Get the HOD's course
+ hod = request.user
+ if not hasattr(hod, "course") or not hod.course:
+ messages.error(request, "You are not assigned as HOD of any department.")
+ return redirect('hodDashboard')
+
+ # Get the student and ensure they belong to the HOD's course
+ student = get_object_or_404(Student, id=student_id, course=hod.course)
+
+ # Check if student has any attendance records
+ if AttendanceRecord.objects.filter(student=student).exists():
+ messages.error(request, "Cannot delete student as they have attendance records.")
+ return redirect('hodDashboard')
+
+ # Delete the student
+ student.delete()
+ messages.success(request, "Student deleted successfully.")
+ return redirect('hodDashboard')
+
+ except Student.DoesNotExist:
+ messages.error(request, "Student not found.")
+ return redirect('hodDashboard')
+ except Exception as e:
+ messages.error(request, f"An error occurred: {str(e)}")
+ return redirect('hodDashboard')
+
+@login_required
+def add_notice(request):
+ """Add a new notice and notify students and parents"""
+ try:
+ # Check if user is HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ messages.error(request, "Only HODs can add notices.")
+ return redirect('hodDashboard')
+
+ # Get the HOD's course
+ hod = request.user
+ if not hasattr(hod, "course") or not hod.course:
+ messages.error(request, "You are not assigned as HOD of any department.")
+ return redirect('hodDashboard')
+
+ course = hod.course
+
+ if request.method == "POST":
+ title = request.POST.get("title")
+ message = request.POST.get("message")
+
+ # Validate required fields
+ if not title:
+ messages.error(request, "Title is required.")
+ return redirect('hodDashboard')
+
+ # Create notice
+ notice = Notice(
+ title=title,
+ message=message
+ )
+
+ # Handle image if provided
+ if request.FILES.get("image"):
+ notice.image = request.FILES.get("image")
+
+ # Handle file attachment if provided
+ if request.FILES.get("file"):
+ notice.file = request.FILES.get("file")
+
+ notice.save()
+
+ # Get students from the HOD's course with FCM tokens
+ students = Student.objects.filter(course=course, fcm_token__isnull=False)
+
+ # Get parents of these students with FCM tokens
+ student_ids = students.values_list('id', flat=True)
+ parents = Parent.objects.filter(students__in=student_ids, fcm_token__isnull=False).distinct()
+
+ # Collect all FCM tokens
+ tokens = []
+
+ # Add student tokens
+ student_tokens = students.values_list('fcm_token', flat=True)
+ tokens.extend(student_tokens)
+
+ # Add parent tokens
+ parent_tokens = parents.values_list('fcm_token', flat=True)
+ tokens.extend(parent_tokens)
+
+ # Remove any empty or null tokens
+ tokens = [token for token in tokens if token]
+
+ # Send push notifications if we have any tokens
+ if tokens:
+ try:
+ # Send notification to all tokens at once
+ success_count, failure_count, failed_tokens = send_push_notification(
+ "New Notice: {title}",
+ message[:100] + "..." if len(message) > 100 else message,
+ tokens
+ )
+
+ if failure_count > 0:
+ print(f"Failed to send notifications to {failure_count} devices")
+ print(f"Failed tokens: {failed_tokens}")
+ except Exception as e:
+ print(f"Error sending notifications: {str(e)}")
+
+ messages.success(request, "Notice added successfully and notifications sent.")
+ return redirect('hodDashboard')
+
+ return render(request, 'hod/dashboard.html')
+
+ except Exception as e:
+ messages.error(request, f"An error occurred: {str(e)}")
+ return redirect('hodDashboard')
+
+@login_required
+def view_notice(request, notice_id):
+ """View notice details"""
+ try:
+ # Check if user is a HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ messages.error(request, "Access denied.")
+ return redirect('hodDashboard')
+
+ # Get notice
+ notice = get_object_or_404(Notice, id=notice_id)
+
+ context = {
+ "notice": notice,
+ "title": "View Notice"
+ }
+
+ return render(request, 'hod/view_notice.html', context)
+
+ except Notice.DoesNotExist:
+ messages.error(request, "Notice not found.")
+ return redirect('hodDashboard')
+ except Exception as e:
+ messages.error(request, f"An error occurred: {str(e)}")
+ return redirect('hodDashboard')
+
+@login_required
+def delete_notice(request, notice_id):
+ """Delete a notice"""
+ try:
+ # Check if user is a HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ messages.error(request, "Only HODs can delete notices.")
+ return redirect('hodDashboard')
+
+ # Get notice
+ notice = get_object_or_404(Notice, id=notice_id)
+
+ # Delete notice
+ notice.delete()
+
+ messages.success(request, "Notice deleted successfully.")
+ return redirect('hodDashboard')
+
+ except Notice.DoesNotExist:
+ messages.error(request, "Notice not found.")
+ return redirect('hodDashboard')
+ except Exception as e:
+ messages.error(request, f"An error occurred: {str(e)}")
+ return redirect('hodDashboard')
+
+@login_required
+def add_meeting(request):
+ """Add a new teacher-parent meeting and notify relevant users"""
+ try:
+ # Check if user is HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can schedule meetings."})
+
+ # Get the HOD's course
+ hod = request.user
+ if not hasattr(hod, "course") or not hod.course:
+ return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."})
+
+ course = hod.course
+
+ if request.method == "POST":
+ meeting_date = request.POST.get("meeting_date")
+ meeting_time = request.POST.get("meeting_time")
+ duration = request.POST.get("duration")
+ agenda = request.POST.get("agenda", "")
+ is_online = request.POST.get("is_online") == "true"
+ meeting_link = request.POST.get("meeting_link", "") or "" # Ensure never NULL
+
+ # Validate required fields
+ if not all([meeting_date, meeting_time, duration]):
+ return JsonResponse({"success": False, "message": "Date, time and duration are required."})
+
+ # Create meeting
+ meeting = TeacherParentMeeting(
+ meeting_date=meeting_date,
+ meeting_time=meeting_time,
+ duration=duration,
+ agenda=agenda,
+ is_online=is_online,
+ meeting_link=meeting_link, # Now guaranteed to be at least empty string
+ status="scheduled"
+ )
+
+ meeting.save()
+
+ # Get students from the HOD's course with FCM tokens
+ students = Student.objects.filter(course=course, fcm_token__isnull=False)
+
+ # Get parents of these students with FCM tokens
+ student_ids = students.values_list('id', flat=True)
+ parents = Parent.objects.filter(students__in=student_ids, fcm_token__isnull=False).distinct()
+
+ # Collect all FCM tokens
+ tokens = []
+
+ # Add student tokens
+ student_tokens = students.values_list('fcm_token', flat=True)
+ tokens.extend(student_tokens)
+
+ # Add parent tokens
+ parent_tokens = parents.values_list('fcm_token', flat=True)
+ tokens.extend(parent_tokens)
+
+ # Remove any empty or null tokens
+ tokens = [token for token in tokens if token]
+
+ # Send push notifications if we have any tokens
+ if tokens:
+ try:
+ # Send notification to all tokens at once
+ success_count, failure_count, failed_tokens = send_push_notification(
+ "New Meeting Scheduled",
+ f"A new meeting has been scheduled for {meeting_date} at {meeting_time}",
+ tokens
+ )
+
+ if failure_count > 0:
+ print(f"Failed to send notifications to {failure_count} devices")
+ print(f"Failed tokens: {failed_tokens}")
+ except Exception as e:
+ print(f"Error sending notifications: {str(e)}")
+
+ return JsonResponse({"success": True, "message": "Meeting scheduled successfully."})
+
+ return JsonResponse({"success": False, "message": "Invalid request method."})
+
+ except Exception as e:
+ return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"})
+
+@login_required
+def get_meeting(request, meeting_id):
+ """Get meeting details"""
+ try:
+ # Check if user is HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can view meeting details."})
+
+ # Get meeting
+ meeting = get_object_or_404(TeacherParentMeeting, id=meeting_id)
+
+ # Prepare response data
+ response_data = {
+ "success": True,
+ "meeting": {
+ "id": meeting.id,
+ "meeting_date": meeting.meeting_date.strftime("%Y-%m-%d"),
+ "meeting_time": meeting.meeting_time.strftime("%H:%M"),
+ "duration": meeting.duration,
+ "status": meeting.status,
+ "status_display": meeting.get_status_display(),
+ "agenda": meeting.agenda,
+ "notes": meeting.notes,
+ "is_online": meeting.is_online,
+ "meeting_link": meeting.meeting_link,
+ "cancellation_reason": meeting.cancellation_reason,
+ "created_at": meeting.created_at.strftime("%Y-%m-%d %H:%M:%S")
+ }
+ }
+
+ return JsonResponse(response_data)
+
+ except TeacherParentMeeting.DoesNotExist:
+ return JsonResponse({"success": False, "message": "Meeting not found."})
+ except Exception as e:
+ return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"})
+
+@login_required
+def edit_meeting(request, meeting_id):
+ """Edit an existing meeting"""
+ try:
+ # Check if user is HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can edit meetings."})
+
+ # Get meeting
+ meeting = get_object_or_404(TeacherParentMeeting, id=meeting_id)
+
+ if request.method == "PUT":
+ meeting_date = request.POST.get("meeting_date")
+ meeting_time = request.POST.get("meeting_time")
+ duration = request.POST.get("duration")
+ agenda = request.POST.get("agenda")
+ notes = request.POST.get("notes", "") # Get notes field with empty string default
+ is_online = request.POST.get("is_online") == "true"
+ meeting_link = request.POST.get("meeting_link", "") # Default to empty string
+
+ # Validate required fields
+ if not all([meeting_date, meeting_time, duration]):
+ return JsonResponse({"error": "Date, time and duration are required"}, status=400)
+
+ # Update meeting
+ meeting.meeting_date = meeting_date
+ meeting.meeting_time = meeting_time
+ meeting.duration = duration
+ meeting.agenda = agenda
+ meeting.notes = notes # Save notes field
+ meeting.is_online = is_online
+ meeting.meeting_link = meeting_link # Always set meeting_link, even if empty
+ meeting.status = "rescheduled" if meeting.status == "scheduled" else meeting.status
+
+ meeting.save()
+
+ # Get students and parents to notify
+ students = Student.objects.filter(course=request.user.course, fcm_token__isnull=False)
+ student_ids = students.values_list('id', flat=True)
+ parents = Parent.objects.filter(students__in=student_ids, fcm_token__isnull=False).distinct()
+
+ # Collect FCM tokens
+ tokens = list(students.values_list('fcm_token', flat=True)) + list(parents.values_list('fcm_token', flat=True))
+ tokens = [token for token in tokens if token]
+
+ # Send notifications
+ if tokens:
+ try:
+ success_count, failure_count, failed_tokens = send_push_notification(
+ "Meeting Updated",
+ f"Meeting scheduled for {meeting_date} at {meeting_time} has been updated",
+ tokens
+ )
+
+ if failure_count > 0:
+ print(f"Failed to send notifications to {failure_count} devices")
+ except Exception as e:
+ print(f"Error sending notifications: {str(e)}")
+
+ return JsonResponse({"success": True, "message": "Meeting updated successfully."})
+
+ return JsonResponse({"success": False, "message": "Invalid request method."})
+
+ except TeacherParentMeeting.DoesNotExist:
+ return JsonResponse({"success": False, "message": "Meeting not found."})
+ except Exception as e:
+ return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"})
+
+@login_required
+def cancel_meeting(request, meeting_id):
+ """Cancel a meeting"""
+ try:
+ # Check if user is HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can cancel meetings."})
+
+ # Get meeting
+ meeting = get_object_or_404(TeacherParentMeeting, id=meeting_id)
+
+ if request.method == "POST":
+ cancellation_reason = request.POST.get("cancellation_reason")
+
+ if not cancellation_reason:
+ return JsonResponse({"success": False, "message": "Cancellation reason is required."})
+
+ # Update meeting
+ meeting.status = "cancelled"
+ meeting.cancellation_reason = cancellation_reason
+ meeting.save()
+
+ # Get students and parents to notify
+ students = Student.objects.filter(course=request.user.course, fcm_token__isnull=False)
+ student_ids = students.values_list('id', flat=True)
+ parents = Parent.objects.filter(students__in=student_ids, fcm_token__isnull=False).distinct()
+
+ # Collect FCM tokens
+ tokens = list(students.values_list('fcm_token', flat=True)) + list(parents.values_list('fcm_token', flat=True))
+ tokens = [token for token in tokens if token]
+
+ # Send notifications
+ if tokens:
+ try:
+ success_count, failure_count, failed_tokens = send_push_notification(
+ "Meeting Cancelled",
+ f"Meeting scheduled for {meeting.meeting_date} at {meeting.meeting_time} has been cancelled",
+ tokens
+ )
+
+ if failure_count > 0:
+ print(f"Failed to send notifications to {failure_count} devices")
+ except Exception as e:
+ print(f"Error sending notifications: {str(e)}")
+
+ return JsonResponse({"success": True, "message": "Meeting cancelled successfully."})
+
+ return JsonResponse({"success": False, "message": "Invalid request method."})
+
+ except TeacherParentMeeting.DoesNotExist:
+ return JsonResponse({"success": False, "message": "Meeting not found."})
+ except Exception as e:
+ return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"})
+
+@login_required
+def get_meeting_notes(request, meeting_id):
+ """Get meeting notes"""
+ try:
+ # Check if user is HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can view meeting notes."})
+
+ # Get meeting
+ meeting = get_object_or_404(TeacherParentMeeting, id=meeting_id)
+
+ return JsonResponse({
+ "success": True,
+ "notes": meeting.notes or "No notes available"
+ })
+
+ except TeacherParentMeeting.DoesNotExist:
+ return JsonResponse({"success": False, "message": "Meeting not found."})
+ except Exception as e:
+ return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"})
+
+@login_required
+def get_meeting_agenda(request, meeting_id):
+ """Get meeting agenda"""
+ try:
+ # Check if user is HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can view meeting agenda."})
+
+ # Get meeting
+ meeting = get_object_or_404(TeacherParentMeeting, id=meeting_id)
+
+ return JsonResponse({
+ "success": True,
+ "agenda": meeting.agenda or "No agenda available"
+ })
+
+ except TeacherParentMeeting.DoesNotExist:
+ return JsonResponse({"success": False, "message": "Meeting not found."})
+ except Exception as e:
+ return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"})
+
+@login_required
+def get_cancellation_reason(request, meeting_id):
+ """Get meeting cancellation reason"""
+ try:
+ # Check if user is HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can view cancellation reasons."})
+
+ # Get meeting
+ meeting = get_object_or_404(TeacherParentMeeting, id=meeting_id)
+
+ if meeting.status != "cancelled":
+ return JsonResponse({"success": False, "message": "Meeting is not cancelled."})
+
+ return JsonResponse({
+ "success": True,
+ "reason": meeting.cancellation_reason or "No reason provided"
+ })
+
+ except TeacherParentMeeting.DoesNotExist:
+ return JsonResponse({"success": False, "message": "Meeting not found."})
+ except Exception as e:
+ return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"})
+
+# API endpoints for meeting management
+@login_required
+@require_http_methods(["GET"])
+def api_get_meeting(request, meeting_id):
+ """API endpoint to get meeting details"""
+ try:
+ # Get meeting
+ meeting = get_object_or_404(TeacherParentMeeting, id=meeting_id)
+
+ # Prepare response data
+ response_data = {
+ "id": meeting.id,
+ "meeting_date": meeting.meeting_date.strftime("%Y-%m-%d"),
+ "meeting_time": meeting.meeting_time.strftime("%H:%M"),
+ "duration": meeting.duration,
+ "status": meeting.status,
+ "status_display": meeting.get_status_display(),
+ "agenda": meeting.agenda,
+ "notes": meeting.notes,
+ "is_online": meeting.is_online,
+ "meeting_link": meeting.meeting_link,
+ "cancellation_reason": meeting.cancellation_reason,
+ "created_at": meeting.created_at.strftime("%Y-%m-%d %H:%M:%S")
+ }
+
+ return JsonResponse(response_data)
+
+ except TeacherParentMeeting.DoesNotExist:
+ return JsonResponse({"error": "Meeting not found"}, status=404)
+ except Exception as e:
+ return JsonResponse({"error": str(e)}, status=500)
+
+@login_required
+@csrf_exempt
+@require_http_methods(["POST"])
+def api_create_meeting(request):
+ """API endpoint to create a new meeting"""
+ try:
+ # Check if user is HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"error": "Only HODs can schedule meetings"}, status=403)
+
+ # Get form data
+ meeting_date = request.POST.get("meeting_date")
+ meeting_time = request.POST.get("meeting_time")
+ duration = request.POST.get("duration")
+ agenda = request.POST.get("agenda", "")
+ is_online = request.POST.get("is_online") == "true"
+ meeting_link = request.POST.get("meeting_link", "") or "" # Ensure never NULL
+
+ # Validate required fields
+ if not all([meeting_date, meeting_time, duration]):
+ return JsonResponse({"error": "Date, time and duration are required"}, status=400)
+
+ # Create meeting
+ meeting = TeacherParentMeeting(
+ meeting_date=meeting_date,
+ meeting_time=meeting_time,
+ duration=duration,
+ agenda=agenda,
+ is_online=is_online,
+ meeting_link=meeting_link, # Now guaranteed to be at least empty string
+ status="scheduled"
+ )
+
+ meeting.save()
+
+ # Return success response
+ return JsonResponse({
+ "id": meeting.id,
+ "message": "Meeting scheduled successfully"
+ })
+
+ except Exception as e:
+ return JsonResponse({"error": str(e)}, status=500)
+
+@login_required
+@csrf_exempt
+@require_http_methods(["PUT", "POST"])
+def api_update_meeting(request, meeting_id):
+ """API endpoint to update an existing meeting"""
+ try:
+ # Check if user is HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"error": "Only HODs can edit meetings"}, status=403)
+
+ # Get meeting
+ meeting = get_object_or_404(TeacherParentMeeting, id=meeting_id)
+
+ # Get form data
+ meeting_date = request.POST.get("meeting_date")
+ meeting_time = request.POST.get("meeting_time")
+ duration = request.POST.get("duration")
+ agenda = request.POST.get("agenda")
+ notes = request.POST.get("notes", "") # Get notes field with empty string default
+ is_online = request.POST.get("is_online") == "true"
+ meeting_link = request.POST.get("meeting_link", "") # Default to empty string
+
+ # Validate required fields
+ if not all([meeting_date, meeting_time, duration]):
+ return JsonResponse({"error": "Date, time and duration are required"}, status=400)
+
+ # Update meeting
+ meeting.meeting_date = meeting_date
+ meeting.meeting_time = meeting_time
+ meeting.duration = duration
+ meeting.agenda = agenda
+ meeting.notes = notes # Save notes field
+ meeting.is_online = is_online
+ meeting.meeting_link = meeting_link # Always set meeting_link, even if empty
+ meeting.status = "rescheduled" if meeting.status == "scheduled" else meeting.status
+
+ meeting.save()
+
+ # Return success response
+ return JsonResponse({
+ "id": meeting.id,
+ "message": "Meeting updated successfully"
+ })
+
+ except TeacherParentMeeting.DoesNotExist:
+ return JsonResponse({"error": "Meeting not found"}, status=404)
+ except Exception as e:
+ return JsonResponse({"error": str(e)}, status=500)
+
+@login_required
+@csrf_exempt
+@require_http_methods(["POST"])
+def api_cancel_meeting(request, meeting_id):
+ """API endpoint to cancel a meeting"""
+ try:
+ # Check if user is HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"error": "Only HODs can cancel meetings"}, status=403)
+
+ # Get meeting
+ meeting = get_object_or_404(TeacherParentMeeting, id=meeting_id)
+
+ # Get cancellation reason
+ cancellation_reason = request.POST.get("cancellation_reason")
+
+ if not cancellation_reason:
+ return JsonResponse({"error": "Cancellation reason is required"}, status=400)
+
+ # Update meeting
+ meeting.status = "cancelled"
+ meeting.cancellation_reason = cancellation_reason
+ meeting.save()
+
+ # Return success response
+ return JsonResponse({
+ "id": meeting.id,
+ "message": "Meeting cancelled successfully"
+ })
+
+ except TeacherParentMeeting.DoesNotExist:
+ return JsonResponse({"error": "Meeting not found"}, status=404)
+ except Exception as e:
+ return JsonResponse({"error": str(e)}, status=500)
+
+@login_required
+@csrf_exempt
+@require_http_methods(["POST"])
+def api_create_routine(request):
+ """API endpoint to create a new routine"""
+ try:
+ # Check if user is a HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can create routines."})
+
+ # Get the HOD's course
+ hod = request.user
+ if not hasattr(hod, "course") or not hod.course:
+ return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."})
+
+ course = hod.course
+
+ # Get form data
+ subject_id = request.POST.get("subject_id")
+ teacher_id = request.POST.get("teacher_id")
+ start_time = request.POST.get("start_time")
+ end_time = request.POST.get("end_time")
+ period_or_year = request.POST.get("period_or_year")
+
+ # Validate required fields
+ if not subject_id or not teacher_id or not start_time or not end_time or not period_or_year:
+ return JsonResponse({"success": False, "message": "All fields are required."})
+
+ # Get subject and teacher objects
+ try:
+ subject = Subject.objects.get(id=subject_id, course=course)
+ teacher = Staff.objects.get(id=teacher_id)
+ except (Subject.DoesNotExist, Staff.DoesNotExist):
+ return JsonResponse({"success": False, "message": "Invalid subject or teacher."})
+
+ # Create the routine
+ routine = Routine(
+ course=course,
+ subject=subject,
+ teacher=teacher,
+ start_time=start_time,
+ end_time=end_time,
+ period_or_year=period_or_year,
+ is_active=True
+ )
+ routine.save()
+
+ return JsonResponse({
+ "success": True,
+ "message": "Routine created successfully.",
+ "routine_id": routine.id
+ })
+ except Exception as e:
+ return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"})
+
+@login_required
+@require_http_methods(["GET"])
+def api_get_routine(request, routine_id):
+ """API endpoint to get routine details"""
+ try:
+ # Check if user is a HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can view routine details."})
+
+ # Get the HOD's course
+ hod = request.user
+ if not hasattr(hod, "course") or not hod.course:
+ return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."})
+
+ # Get routine and ensure it belongs to the HOD's course
+ routine = get_object_or_404(Routine, id=routine_id, course=hod.course)
+
+ # Format time fields properly to ensure they can be used in the form's time inputs
+ start_time = routine.start_time.strftime("%H:%M") if routine.start_time else ""
+ end_time = routine.end_time.strftime("%H:%M") if routine.end_time else ""
+
+ # Prepare response data with detailed information
+ response_data = {
+ "success": True,
+ "routine": {
+ "id": routine.id,
+ "subject_id": routine.subject.id,
+ "subject_name": routine.subject.name,
+ "teacher_id": routine.teacher.id,
+ "teacher_name": routine.teacher.name,
+ "start_time": start_time,
+ "end_time": end_time,
+ "period_or_year": routine.period_or_year,
+ "is_active": routine.is_active
+ }
+ }
+
+ # Log the response for debugging
+ print(f"API response for routine {routine_id}: {response_data}")
+ return JsonResponse(response_data)
+ except Exception as e:
+ error_message = f"An error occurred while fetching routine {routine_id}: {str(e)}"
+ print(error_message)
+ return JsonResponse({"success": False, "message": error_message})
+
+@login_required
+@csrf_exempt
+@require_http_methods(["PUT", "POST"])
+def api_update_routine(request, routine_id):
+ """API endpoint to update a routine"""
+ try:
+ # Check if user is a HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can update routines."})
+
+ # Get the HOD's course
+ hod = request.user
+ if not hasattr(hod, "course") or not hod.course:
+ return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."})
+
+ # Get routine and ensure it belongs to the HOD's course
+ routine = get_object_or_404(Routine, id=routine_id, course=hod.course)
+
+ # Handle both PUT and POST methods
+ if request.method == 'PUT':
+ # For PUT requests, get data from request.POST or parse request.body
+ import json
+ try:
+ if request.body:
+ data = json.loads(request.body)
+ else:
+ data = request.POST.dict()
+
+ # Extract values from the data dictionary
+ subject_id = data.get("subject_id")
+ teacher_id = data.get("teacher_id")
+ start_time = data.get("start_time")
+ end_time = data.get("end_time")
+ period_or_year = data.get("period_or_year")
+
+ print(f"Received data: {data}")
+ except json.JSONDecodeError as e:
+ print(f"JSON decode error: {e}")
+ # Fallback to form data
+ subject_id = request.POST.get("subject_id")
+ teacher_id = request.POST.get("teacher_id")
+ start_time = request.POST.get("start_time")
+ end_time = request.POST.get("end_time")
+ period_or_year = request.POST.get("period_or_year")
+ else:
+ # For POST requests, use request.POST directly
+ subject_id = request.POST.get("subject_id")
+ teacher_id = request.POST.get("teacher_id")
+ start_time = request.POST.get("start_time")
+ end_time = request.POST.get("end_time")
+ period_or_year = request.POST.get("period_or_year")
+
+ # Log received data
+ print(f"Processing routine update: subject_id={subject_id}, teacher_id={teacher_id}, start_time={start_time}, end_time={end_time}")
+
+ # Validate required fields
+ if not subject_id or not teacher_id or not start_time or not end_time:
+ error_msg = "All fields are required."
+ print(f"Validation error: {error_msg}")
+ return JsonResponse({"success": False, "message": error_msg})
+
+ # Get subject and teacher objects
+ try:
+ subject = Subject.objects.get(id=subject_id, course=hod.course)
+ teacher = Staff.objects.get(id=teacher_id)
+ except (Subject.DoesNotExist, Staff.DoesNotExist) as e:
+ error_msg = "Invalid subject or teacher."
+ print(f"Database lookup error: {e}")
+ return JsonResponse({"success": False, "message": error_msg})
+
+ # Update routine
+ routine.subject = subject
+ routine.teacher = teacher
+ routine.start_time = start_time
+ routine.end_time = end_time
+ if period_or_year:
+ routine.period_or_year = period_or_year
+ routine.save()
+
+ print(f"Routine {routine_id} updated successfully")
+ return JsonResponse({"success": True, "message": "Routine updated successfully."})
+ except Exception as e:
+ error_msg = f"An error occurred: {str(e)}"
+ print(f"Error in api_update_routine: {error_msg}")
+ return JsonResponse({"success": False, "message": error_msg})
+
+@login_required
+@csrf_exempt
+@require_http_methods(["DELETE", "POST"])
+def api_delete_routine(request, routine_id):
+ """API endpoint to delete a routine"""
+ try:
+ # Check if user is a HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can delete routines."})
+
+ # Get the HOD's course
+ hod = request.user
+ if not hasattr(hod, "course") or not hod.course:
+ return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."})
+
+ # Get routine and ensure it belongs to the HOD's course
+ routine = get_object_or_404(Routine, id=routine_id, course=hod.course)
+
+ # Delete the routine
+ routine.delete()
+
+ return JsonResponse({"success": True, "message": "Routine deleted successfully."})
+ except Exception as e:
+ return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"})
+
+@login_required
+def get_student_progress(request):
+ """API endpoint to get all student progress data with filtering options"""
+ try:
+ # Check if user is a HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can access progress data."})
+
+ # Get the HOD's course
+ hod = request.user
+ if not hasattr(hod, "course") or not hod.course:
+ return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."})
+
+ course = hod.course
+
+ # Get batch filter if provided
+ batch_id = request.GET.get('batch_id')
+
+ # Base query for student progress
+ progress_query = CourseTracking.objects.filter(
+ student__course=course
+ ).select_related("student")
+
+ # Apply batch filter if provided
+ if batch_id:
+ try:
+ batch = Batch.objects.get(id=batch_id)
+ progress_query = progress_query.filter(student__batches=batch)
+ except Batch.DoesNotExist:
+ return JsonResponse({"success": False, "message": "Invalid batch selected."})
+
+ # Get all progress records ordered by completion percentage
+ progress_records = progress_query.order_by("-completion_percentage")
+
+ # Calculate metrics
+ total_students = progress_records.count()
+ avg_completion = progress_records.aggregate(Avg('completion_percentage'))['completion_percentage__avg'] or 0
+ avg_completion = round(avg_completion, 1)
+ students_on_track = progress_records.filter(completion_percentage__gte=75).count()
+ students_at_risk = progress_records.filter(completion_percentage__lt=40).count()
+
+ # Prepare progress data
+ progress_data = []
+
+ for progress in progress_records:
+ # Get batch names
+ batch_names = ", ".join([batch.name for batch in progress.student.batches.all()])
+
+ # Determine status
+ if progress.completion_percentage >= 75:
+ status = "On Track"
+ status_class = "success"
+ elif progress.completion_percentage >= 40:
+ status = "Needs Attention"
+ status_class = "warning"
+ else:
+ status = "At Risk"
+ status_class = "danger"
+
+ # Format student data
+ student_image = progress.student.image.url if progress.student.image else None
+
+ # Add to progress data list
+ progress_data.append({
+ "student_id": progress.student.id,
+ "student_name": progress.student.name,
+ "student_image": student_image,
+ "batch_names": batch_names,
+ "current_period": progress.student.current_period,
+ "completion_percentage": progress.completion_percentage,
+ "last_updated": progress.updated_at.strftime("%Y-%m-%d %H:%M:%S"),
+ "status": status,
+ "status_class": status_class
+ })
+
+ # Prepare response
+ response_data = {
+ "success": True,
+ "total_students": total_students,
+ "avg_completion": avg_completion,
+ "students_on_track": students_on_track,
+ "students_at_risk": students_at_risk,
+ "progress_data": progress_data
+ }
+
+ return JsonResponse(response_data)
+
+ except Exception as e:
+ return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"})
+
+@login_required
+def get_progress(request, progress_id):
+ """API endpoint to get details of a specific student progress record"""
+ try:
+ # Check if user is a HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can access progress data."})
+
+ # Get the HOD's course
+ hod = request.user
+ if not hasattr(hod, "course") or not hod.course:
+ return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."})
+
+ course = hod.course
+
+ # Get the progress record
+ try:
+ progress = CourseTracking.objects.get(id=progress_id, student__course=course)
+ except CourseTracking.DoesNotExist:
+ return JsonResponse({"success": False, "message": "Progress record not found."})
+
+ # Get student data
+ student = progress.student
+
+ # Format batch names
+ batch_names = [batch.name for batch in student.batches.all()]
+
+ # Prepare progress data
+ progress_data = {
+ "id": progress.id,
+ "completion_percentage": progress.completion_percentage,
+ "progress_status": progress.progress_status,
+ "notes": progress.notes or "",
+ "last_updated": progress.updated_at.strftime("%Y-%m-%d %H:%M:%S"),
+ }
+
+ # Prepare student data
+ student_data = {
+ "id": student.id,
+ "name": student.name,
+ "image": student.image.url if student.image else None,
+ "current_period": student.current_period,
+ "batches": batch_names,
+ }
+
+ return JsonResponse({
+ "success": True,
+ "progress": progress_data,
+ "student": student_data
+ })
+
+ except Exception as e:
+ return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"})
+
+@login_required
+def edit_progress(request, progress_id):
+ """API endpoint to update a student progress record"""
+ try:
+ # Check if user is a HOD
+ if not request.user.groups.filter(name="HOD").exists():
+ return JsonResponse({"success": False, "message": "Only HODs can update progress data."})
+
+ # Get the HOD's course
+ hod = request.user
+ if not hasattr(hod, "course") or not hod.course:
+ return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."})
+
+ course = hod.course
+
+ # Check if this is a POST request
+ if request.method != 'POST':
+ return JsonResponse({"success": False, "message": "Invalid request method."})
+
+ # Get the progress record
+ try:
+ progress = CourseTracking.objects.get(id=progress_id, student__course=course)
+ except CourseTracking.DoesNotExist:
+ return JsonResponse({"success": False, "message": "Progress record not found."})
+
+ # Get form data
+ current_period = request.POST.get('current_period')
+ completion_percentage = request.POST.get('completion_percentage')
+ notes = request.POST.get('notes', '')
+
+ # Validate form data
+ if not current_period or not completion_percentage:
+ return JsonResponse({"success": False, "message": "Current period and completion percentage are required."})
+
+ try:
+ current_period = int(current_period)
+ completion_percentage = int(completion_percentage)
+
+ # Validate current period
+ if current_period < 1:
+ return JsonResponse({"success": False, "message": "Current period must be at least 1."})
+
+ # Maximum period based on course duration and type
+ max_period = progress.course.duration
+ if progress.course.duration_type == "Semester":
+ max_period *= 2 # Two semesters per year
+
+ if current_period > max_period:
+ return JsonResponse({"success": False, "message": f"Current period cannot exceed {max_period}."})
+
+ # Validate completion percentage
+ if completion_percentage < 0 or completion_percentage > 100:
+ return JsonResponse({"success": False, "message": "Completion percentage must be between 0 and 100."})
+
+ except ValueError:
+ return JsonResponse({"success": False, "message": "Invalid numeric values provided."})
+
+ # Update student's current period
+ student = progress.student
+ student.current_period = current_period
+ student.save(update_fields=['current_period'])
+
+ # Update progress record
+ progress.completion_percentage = completion_percentage
+ progress.notes = notes
+ progress.save(update_fields=['completion_percentage', 'notes'])
+
+ return JsonResponse({
+ "success": True,
+ "message": "Progress updated successfully."
+ })
+
+ except Exception as e:
+ return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"})
diff --git a/app/management/commands/README.md b/app/management/commands/README.md
new file mode 100644
index 0000000..106553b
--- /dev/null
+++ b/app/management/commands/README.md
@@ -0,0 +1,78 @@
+# Fake Data Generator for SMS
+
+This directory contains a Django management command to generate fake data for testing the School Management System (SMS).
+
+## Prerequisites
+
+Before using this script, make sure you have installed the required packages:
+
+```bash
+pip install faker
+```
+
+## Usage
+
+Run the command using Django's `manage.py`:
+
+```bash
+python manage.py generate_fake_data
+```
+
+### Options
+
+The script accepts the following optional parameters:
+
+- `--students`: Number of students to create (default: 50)
+- `--staff`: Number of staff members to create (default: 20)
+- `--parents`: Number of parents to create (default: 40)
+- `--courses`: Number of courses to create (default: 5)
+- `--subjects`: Number of subjects per course (default: 4)
+- `--clear`: Clear existing data before generating new data (flag)
+
+### Examples
+
+Generate data with default parameters:
+```bash
+python manage.py generate_fake_data
+```
+
+Generate a smaller dataset:
+```bash
+python manage.py generate_fake_data --students 20 --staff 10 --parents 15
+```
+
+Clear existing data and generate new data:
+```bash
+python manage.py generate_fake_data --clear
+```
+
+## Generated Data
+
+The script generates the following types of data:
+
+1. Institute information
+2. Batches (last 4 years)
+3. Courses with customized names and durations
+4. Subjects for each course
+5. Staff members with profiles and credentials
+6. Students with profiles and course assignments
+7. Parents linked to students
+8. Class routines and schedules
+9. Attendance records for past week
+10. Notices/announcements
+11. Leave requests for students and staff
+12. Feedback (student, parent, institute)
+13. Parent-teacher meetings
+14. Course tracking records
+
+## Login Credentials
+
+The generated users have the following default passwords:
+
+- Students: `student123`
+- Staff: `staff123`
+- Parents: `parent123`
+
+## Note
+
+The script doesn't generate actual file content for subject files due to the limitation of creating binary content programmatically. This portion is commented out in the code.
\ No newline at end of file
diff --git a/app/management/commands/generate_fake_data.py b/app/management/commands/generate_fake_data.py
new file mode 100644
index 0000000..eaedb3c
--- /dev/null
+++ b/app/management/commands/generate_fake_data.py
@@ -0,0 +1,891 @@
+import random
+from datetime import datetime, timedelta
+import pytz
+from django.core.management.base import BaseCommand
+from django.utils import timezone
+from django.contrib.auth.models import Group
+from django.db import transaction
+from faker import Faker
+from app.models import (
+ Institute, Batch, Course, Subject, Staff, Student, Parent,
+ Routine, Attendance, AttendanceRecord, Notice,
+ StaffLeave, StudentLeave, StudentFeedback, ParentFeedback,
+ InstituteFeedback, StaffInstituteFeedback, ParentInstituteFeedback,
+ CourseTracking, TeacherParentMeeting, SubjectFile
+)
+
+# Nepali-specific data
+NEPALI_FIRST_NAMES = [
+ 'Aarav', 'Aayush', 'Abhishek', 'Aditya', 'Amit', 'Anish', 'Anup', 'Arjun', 'Ashish', 'Bibek',
+ 'Bikash', 'Bimal', 'Bishal', 'Bishnu', 'Buddha', 'Chandra', 'Dawa', 'Deepak', 'Dinesh', 'Dipesh',
+ 'Ganesh', 'Gaurav', 'Gopal', 'Hari', 'Hemant', 'Jeevan', 'Karan', 'Krishna', 'Kumar', 'Laxman',
+ 'Madan', 'Manoj', 'Nabin', 'Narayan', 'Niraj', 'Nishan', 'Prabin', 'Prakash', 'Pramod', 'Prasad',
+ 'Pratik', 'Pratikshya', 'Rabin', 'Raj', 'Rajesh', 'Rakesh', 'Ram', 'Ramesh', 'Ravi', 'Rohit',
+ 'Roshan', 'Sagar', 'Sajan', 'Sandeep', 'Sanjay', 'Santosh', 'Saroj', 'Saurav', 'Shankar', 'Shiva',
+ 'Siddhartha', 'Suman', 'Suresh', 'Sushil', 'Umesh', 'Uttam', 'Vijay', 'Vikash', 'Vishal', 'Yogesh'
+]
+
+NEPALI_LAST_NAMES = [
+ 'Acharya', 'Adhikari', 'Aryal', 'Bajracharya', 'Bhandari', 'Bhattarai', 'Chaudhary', 'Dahal', 'Dangol',
+ 'Dhakal', 'Gautam', 'Ghimire', 'Gurung', 'Joshi', 'Kafle', 'Karki', 'Khadka', 'Khanal', 'Lama',
+ 'Limbu', 'Maharjan', 'Malla', 'Neupane', 'Pandey', 'Panta', 'Poudel', 'Rai', 'Rana', 'Regmi',
+ 'Sharma', 'Shrestha', 'Silwal', 'Singh', 'Tamang', 'Thapa', 'Timalsina', 'Tuladhar', 'Upadhyaya'
+]
+
+NEPALI_DESIGNATIONS = [
+ 'Teacher',
+ 'Principal',
+ 'HOD',
+ 'Admission Officer',
+ 'Staff'
+]
+
+NEPALI_CITIES = [
+ 'Kathmandu', 'Pokhara', 'Lalitpur', 'Bhaktapur', 'Biratnagar', 'Birgunj', 'Dharan', 'Bharatpur',
+ 'Butwal', 'Hetauda', 'Dhangadhi', 'Nepalgunj', 'Itahari', 'Damak', 'Siddharthanagar'
+]
+
+class Command(BaseCommand):
+ help = 'Generate fake data for testing the SMS application'
+
+ def add_arguments(self, parser):
+ parser.add_argument('--students', type=int, default=50, help='Number of students to create')
+ parser.add_argument('--staff', type=int, default=20, help='Number of staff members to create')
+ parser.add_argument('--parents', type=int, default=40, help='Number of parents to create')
+ parser.add_argument('--courses', type=int, default=5, help='Number of courses to create')
+ parser.add_argument('--subjects', type=int, default=4, help='Subjects per course')
+ parser.add_argument('--clear', action='store_true', help='Clear existing data before generating new data')
+
+ @transaction.atomic
+ def handle(self, *args, **options):
+ fake = Faker()
+
+ # Get parameters
+ num_students = max(10, options['students']) # Ensure minimum of 10 students
+ num_staff = max(5, options['staff']) # Ensure minimum of 5 staff
+ num_parents = max(8, options['parents']) # Ensure minimum of 8 parents
+ num_courses = max(3, options['courses']) # Ensure minimum of 3 courses
+ subjects_per_course = max(3, options['subjects']) # Ensure minimum of 3 subjects per course
+ clear_data = options['clear']
+
+ # Get or create groups
+ staff_group, _ = Group.objects.get_or_create(name='Teacher')
+ parent_group, _ = Group.objects.get_or_create(name='Parent')
+ student_group, _ = Group.objects.get_or_create(name='Student')
+
+ # Clear existing data if requested
+ if clear_data:
+ self.stdout.write(self.style.WARNING('Clearing existing data...'))
+ TeacherParentMeeting.objects.all().delete()
+ ParentInstituteFeedback.objects.all().delete()
+ StaffInstituteFeedback.objects.all().delete()
+ InstituteFeedback.objects.all().delete()
+ ParentFeedback.objects.all().delete()
+ StudentFeedback.objects.all().delete()
+ StudentLeave.objects.all().delete()
+ StaffLeave.objects.all().delete()
+ Notice.objects.all().delete()
+ AttendanceRecord.objects.all().delete()
+ Attendance.objects.all().delete()
+ Routine.objects.all().delete()
+ SubjectFile.objects.all().delete()
+ CourseTracking.objects.all().delete()
+
+ # Clear many-to-many relationships before deleting models
+ for parent in Parent.objects.all():
+ parent.students.clear()
+
+ for student in Student.objects.all():
+ student.batches.clear()
+
+ Subject.objects.all().delete()
+ Parent.objects.all().delete()
+ Student.objects.all().delete()
+ Staff.objects.all().delete()
+ Course.objects.all().delete()
+ Batch.objects.all().delete()
+ Institute.objects.all().delete()
+
+ self.stdout.write(self.style.SUCCESS('Data cleared successfully'))
+
+ # Create Institute
+ institute = self._create_institute(fake)
+
+ # Create batches (last 4 years)
+ batches = self._create_batches()
+
+ # Create courses
+ courses = self._create_courses(fake, num_courses)
+
+ # Create subjects for each course
+ subjects = self._create_subjects(fake, courses, subjects_per_course)
+
+ # Create staff members
+ staff_members = self._create_staff(fake, num_staff, staff_group)
+
+ # Create students
+ students = self._create_students(fake, num_students, courses, batches, student_group)
+
+ # Create parents and link to students
+ parents = self._create_parents(fake, num_parents, students, parent_group)
+
+ # Create routines and class schedules
+ routines = self._create_routines(courses, subjects, staff_members)
+
+ # Create attendance records
+ attendance_records = self._create_attendance(routines, students)
+
+ # Create notices
+ notices = self._create_notices(fake)
+
+ # Create leave requests
+ self._create_leaves(fake, staff_members, students)
+
+ # Create feedback
+ self._create_feedback(fake, students, staff_members, parents, institute)
+
+ # Create parent-teacher meetings
+ self._create_meetings(fake, staff_members, parents)
+
+ # Create subject files
+ self._create_subject_files(fake, subjects, staff_members)
+
+ # Create course tracking for students
+ self._create_course_tracking(students, courses)
+
+ self.stdout.write(self.style.SUCCESS('Successfully generated fake data!'))
+
+ def _get_nepali_name(self):
+ return f"{random.choice(NEPALI_FIRST_NAMES)} {random.choice(NEPALI_LAST_NAMES)}"
+
+ def _get_nepali_phone(self):
+ return f"98{random.randint(1000000, 9999999)}"
+
+ def _get_nepali_address(self, city):
+ wards = [f"Ward No. {random.randint(1, 35)}" for _ in range(3)]
+ return f"{random.choice(wards)}, {city}, Nepal"
+
+ def _create_institute(self, fake):
+ city = random.choice(NEPALI_CITIES)
+ institute, created = Institute.objects.get_or_create(
+ name=f"{city} Educational Institute",
+ defaults={
+ 'phone': self._get_nepali_phone(),
+ 'email': fake.company_email(),
+ 'address': self._get_nepali_address(city),
+ 'pan_no': f"PAN{random.randint(100000, 999999)}",
+ 'reg_no': f"REG{random.randint(100000, 999999)}",
+ 'description': fake.paragraph(nb_sentences=3)
+ }
+ )
+ if created:
+ self.stdout.write(self.style.SUCCESS(f'Created institute: {institute.name}'))
+ return institute
+
+ def _create_batches(self):
+ batches = []
+ current_year = datetime.now().year
+
+ for year in range(current_year - 3, current_year + 1):
+ batch, created = Batch.objects.get_or_create(
+ name=f'Batch {year}',
+ defaults={'year': datetime(year, 1, 1)}
+ )
+ batches.append(batch)
+ if created:
+ self.stdout.write(self.style.SUCCESS(f'Created batch: {batch.name}'))
+
+ return batches
+
+ def _create_courses(self, fake, num_courses):
+ courses = []
+ course_names = [
+ 'Computer Science Engineering',
+ 'Electrical Engineering',
+ 'Mechanical Engineering',
+ 'Civil Engineering',
+ 'Chemical Engineering',
+ 'Electronics Engineering',
+ 'Information Technology',
+ 'Aerospace Engineering',
+ 'Biotechnology Engineering',
+ 'Data Science'
+ ]
+
+ # Ensure we don't try to create more courses than we have names for
+ num_to_create = min(num_courses, len(course_names))
+
+ for i in range(num_to_create):
+ duration = random.choice([3, 4])
+ duration_type = random.choice(['Year', 'Semester'])
+
+ course, created = Course.objects.get_or_create(
+ name=course_names[i],
+ defaults={
+ 'duration': duration,
+ 'duration_type': duration_type
+ }
+ )
+ courses.append(course)
+ if created:
+ self.stdout.write(self.style.SUCCESS(f'Created course: {course.name}'))
+
+ return courses
+
+ def _create_subjects(self, fake, courses, subjects_per_course):
+ subjects = []
+
+ subject_names = {
+ 'Computer Science Engineering': [
+ 'Data Structures', 'Algorithms', 'Database Systems', 'Operating Systems',
+ 'Computer Networks', 'Software Engineering', 'Web Development', 'AI and Machine Learning',
+ 'Computer Architecture', 'Compiler Design', 'Computer Graphics', 'Cloud Computing'
+ ],
+ 'Electrical Engineering': [
+ 'Circuit Theory', 'Electrical Machines', 'Power Systems', 'Control Systems',
+ 'Digital Electronics', 'Microprocessors', 'Power Electronics', 'Electrical Measurements',
+ 'Renewable Energy', 'Smart Grid', 'High Voltage Engineering', 'Power System Protection'
+ ],
+ 'Mechanical Engineering': [
+ 'Thermodynamics', 'Fluid Mechanics', 'Manufacturing Processes', 'Machine Design',
+ 'Heat Transfer', 'Engineering Mechanics', 'Automobile Engineering', 'Materials Science',
+ 'Robotics', 'CAD/CAM', 'Industrial Engineering', 'Refrigeration and Air Conditioning'
+ ],
+ 'Information Technology': [
+ 'Programming Fundamentals', 'Data Structures', 'Database Management', 'Web Technologies',
+ 'Computer Networks', 'Software Engineering', 'Cybersecurity', 'Cloud Computing',
+ 'Mobile Application Development', 'Big Data Analytics', 'IoT', 'Blockchain Technology'
+ ],
+ 'Data Science': [
+ 'Statistical Methods', 'Machine Learning', 'Data Mining', 'Big Data Analytics',
+ 'Data Visualization', 'Deep Learning', 'Natural Language Processing', 'Time Series Analysis',
+ 'Business Intelligence', 'Predictive Analytics', 'Data Warehousing', 'Data Engineering'
+ ]
+ }
+
+ # Add generic subjects for courses not explicitly defined
+ generic_subjects = [
+ 'Mathematics I', 'Mathematics II', 'Physics', 'Chemistry',
+ 'Technical Writing', 'Engineering Ethics', 'Project Management', 'Research Methods',
+ 'Environmental Science', 'Economics', 'Management', 'Communication Skills'
+ ]
+
+ for course in courses:
+ # Get subject list for this course or use generic subjects
+ if course.name in subject_names:
+ course_subject_names = subject_names[course.name]
+ else:
+ course_subject_names = generic_subjects
+
+ max_year_or_sem = course.duration
+ if course.duration_type == 'Semester':
+ max_year_or_sem = course.duration * 2
+
+ # Create subjects for each year/semester
+ for year_or_sem in range(1, max_year_or_sem + 1):
+ # Calculate number of subjects for this year/semester
+ # Ensure at least 2 subjects per year/semester
+ num_subjects = max(2, min(subjects_per_course, len(course_subject_names)))
+
+ # Select subjects ensuring no duplicates within the same year/semester
+ available_subjects = [s for s in course_subject_names if not Subject.objects.filter(
+ course=course,
+ name=s,
+ period_or_year=year_or_sem
+ ).exists()]
+
+ if len(available_subjects) < num_subjects:
+ # If not enough unique subjects, add some from generic subjects
+ available_subjects.extend([
+ s for s in generic_subjects
+ if s not in available_subjects and not Subject.objects.filter(
+ course=course,
+ name=s,
+ period_or_year=year_or_sem
+ ).exists()
+ ])
+
+ selected_subjects = random.sample(available_subjects, min(num_subjects, len(available_subjects)))
+
+ for subject_name in selected_subjects:
+ subject, created = Subject.objects.get_or_create(
+ name=subject_name,
+ course=course,
+ period_or_year=year_or_sem
+ )
+ subjects.append(subject)
+ if created:
+ self.stdout.write(self.style.SUCCESS(f'Created subject: {subject.name} for {course.name} in {year_or_sem}'))
+
+ return subjects
+
+ def _create_staff(self, fake, num_staff, staff_group):
+ staff_members = []
+
+ for i in range(num_staff):
+ gender = random.choice(['Male', 'Female'])
+ name = self._get_nepali_name()
+ phone = self._get_nepali_phone()
+ city = random.choice(NEPALI_CITIES)
+
+ staff = Staff(
+ name=name,
+ phone=phone,
+ designation=random.choice(NEPALI_DESIGNATIONS),
+ gender=gender,
+ birth_date=fake.date_of_birth(minimum_age=30, maximum_age=65),
+ email=fake.email(),
+ temporary_address=self._get_nepali_address(city),
+ permanent_address=self._get_nepali_address(city),
+ marital_status=random.choice(['Married', 'Unmarried']),
+ parent_name=self._get_nepali_name(),
+ parent_phone=self._get_nepali_phone(),
+ citizenship_no=f"{random.randint(10000, 99999)}-{random.randint(10000, 99999)}",
+ joining_date=fake.date_between(start_date='-5y', end_date='today')
+ )
+ staff.save()
+ staff.set_password('staff123')
+ staff.save()
+ staff.groups.add(staff_group)
+ staff_members.append(staff)
+ self.stdout.write(self.style.SUCCESS(f'Created staff: {staff.name}'))
+
+ return staff_members
+
+ def _create_students(self, fake, num_students, courses, batches, student_group):
+ students = []
+
+ for i in range(num_students):
+ gender = random.choice(['Male', 'Female'])
+ name = self._get_nepali_name()
+ phone = self._get_nepali_phone()
+ city = random.choice(NEPALI_CITIES)
+
+ course = random.choice(courses)
+ max_semester = course.duration
+ if course.duration_type == 'Semester':
+ max_semester = course.duration * 2
+
+ current_period = random.randint(1, max_semester)
+
+ student = Student(
+ name=name,
+ status=random.choice(['Active', 'Leave', 'Completed']),
+ gender=gender,
+ birth_date=fake.date_of_birth(minimum_age=18, maximum_age=25),
+ email=fake.email(),
+ phone=phone,
+ temporary_address=self._get_nepali_address(city),
+ permanent_address=self._get_nepali_address(city),
+ marital_status=random.choice(['Married', 'Unmarried']),
+ parent_name=self._get_nepali_name(),
+ parent_phone=self._get_nepali_phone(),
+ citizenship_no=f"{random.randint(10000, 99999)}-{random.randint(10000, 99999)}",
+ course=course,
+ current_period=current_period,
+ joining_date=fake.date_between(start_date='-4y', end_date='-1y')
+ )
+ student.save()
+ student.set_password('student123')
+ student.save()
+ student.groups.add(student_group)
+
+ batch = random.choice(batches)
+ student.batches.add(batch)
+
+ students.append(student)
+ self.stdout.write(self.style.SUCCESS(f'Created student: {student.name}'))
+
+ return students
+
+ def _create_parents(self, fake, num_parents, students, parent_group):
+ parents = []
+
+ for i in range(num_parents):
+ name = self._get_nepali_name()
+ phone = self._get_nepali_phone()
+ city = random.choice(NEPALI_CITIES)
+
+ parent = Parent(
+ name=name,
+ phone=phone,
+ email=fake.email(),
+ address=self._get_nepali_address(city),
+ )
+ parent.save()
+ parent.set_password('parent123')
+ parent.save()
+ parent.groups.add(parent_group)
+
+ num_children = random.randint(1, min(2, len(students)))
+ assigned_students = random.sample(students, num_children)
+ for student in assigned_students:
+ parent.students.add(student)
+
+ parents.append(parent)
+ self.stdout.write(self.style.SUCCESS(f'Created parent: {parent.name} with {num_children} children'))
+
+ return parents
+
+ def _create_routines(self, courses, subjects, staff_members):
+ routines = []
+ days_of_week = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']
+ time_slots = [
+ ('09:00', '10:00'),
+ ('10:15', '11:15'),
+ ('11:30', '12:30'),
+ ('13:30', '14:30'),
+ ('14:45', '15:45'),
+ ('16:00', '17:00')
+ ]
+
+ # Create a schedule for each course and semester
+ for course in courses:
+ course_subjects = Subject.objects.filter(course=course)
+
+ # Group subjects by period/year
+ subjects_by_period = {}
+ for subject in course_subjects:
+ if subject.period_or_year not in subjects_by_period:
+ subjects_by_period[subject.period_or_year] = []
+ subjects_by_period[subject.period_or_year].append(subject)
+
+ # Create routine for each period/year
+ for period, period_subjects in subjects_by_period.items():
+ # Assign teachers to subjects
+ teacher_subject_assignments = {}
+ available_teachers = staff_members.copy()
+
+ for subject in period_subjects:
+ if not available_teachers:
+ available_teachers = staff_members.copy()
+
+ teacher = random.choice(available_teachers)
+ teacher_subject_assignments[subject] = teacher
+ available_teachers.remove(teacher)
+
+ # Create routine for each day
+ for day in days_of_week:
+ # Shuffle time slots for variety
+ shuffled_slots = time_slots.copy()
+ random.shuffle(shuffled_slots)
+
+ # Assign subjects to time slots
+ for subject, teacher in teacher_subject_assignments.items():
+ if not shuffled_slots:
+ break
+
+ start_time_str, end_time_str = shuffled_slots.pop()
+ start_time = datetime.strptime(start_time_str, "%H:%M").time()
+ end_time = datetime.strptime(end_time_str, "%H:%M").time()
+
+ # Check for conflicts
+ conflicting_routine = Routine.objects.filter(
+ teacher=teacher,
+ start_time=start_time,
+ end_time=end_time
+ ).first()
+
+ if not conflicting_routine:
+ routine = Routine(
+ course=course,
+ subject=subject,
+ teacher=teacher,
+ start_time=start_time,
+ end_time=end_time,
+ period_or_year=period,
+ is_active=True
+ )
+
+ try:
+ routine.save()
+ routines.append(routine)
+ self.stdout.write(
+ self.style.SUCCESS(
+ f'Created routine for {subject.name} with {teacher.name} '
+ f'on {day} at {start_time_str}-{end_time_str}'
+ )
+ )
+ except Exception as e:
+ # Skip if there's an error (likely due to unique constraint violation)
+ pass
+
+ return routines
+
+ def _create_attendance(self, routines, students):
+ attendance_records = []
+
+ # Create attendance for the past 2 weeks
+ for day_offset in range(14, 0, -1):
+ date = datetime.now().date() - timedelta(days=day_offset)
+
+ # Skip weekends
+ if date.weekday() >= 5: # 5 and 6 are Saturday and Sunday
+ continue
+
+ for routine in routines:
+ # Create attendance for this routine on this date
+ attendance = Attendance(
+ date=date,
+ routine=routine,
+ teacher=routine.teacher,
+ teacher_attend=random.choices([True, False], weights=[0.95, 0.05])[0], # 95% chance of teacher attendance
+ class_status=True
+ )
+ try:
+ attendance.save()
+
+ # Find students in this course and semester
+ course_students = Student.objects.filter(
+ course=routine.course,
+ current_period=routine.period_or_year
+ )
+
+ # Create attendance records for students
+ for student in course_students:
+ # Base attendance probability on student's status
+ if student.status == 'Active':
+ attendance_prob = 0.85 # 85% chance for active students
+ elif student.status == 'Leave':
+ attendance_prob = 0.30 # 30% chance for students on leave
+ else:
+ attendance_prob = 0.10 # 10% chance for completed students
+
+ # Add some randomness to the attendance pattern
+ attendance_prob += random.uniform(-0.1, 0.1)
+ attendance_prob = max(0, min(1, attendance_prob)) # Ensure between 0 and 1
+
+ is_present = random.choices([True, False], weights=[attendance_prob, 1-attendance_prob])[0]
+
+ record = AttendanceRecord(
+ attendance=attendance,
+ student=student,
+ student_attend=is_present
+ )
+ record.save()
+ attendance_records.append(record)
+
+ self.stdout.write(
+ self.style.SUCCESS(
+ f'Created attendance for {routine.subject.name} on {date} '
+ f'with {course_students.count()} students'
+ )
+ )
+ except Exception as e:
+ # Skip if there's an error
+ pass
+
+ return attendance_records
+
+ def _create_notices(self, fake):
+ notices = []
+
+ for i in range(10): # Create 10 notices
+ notice = Notice(
+ title=fake.sentence(),
+ message=fake.paragraph(nb_sentences=3),
+ created_at=fake.date_time_between(start_date='-30d', end_date='now')
+ )
+ notice.save()
+ notices.append(notice)
+ self.stdout.write(self.style.SUCCESS(f'Created notice: {notice.title}'))
+
+ return notices
+
+ def _create_leaves(self, fake, staff_members, students):
+ # Create staff leaves
+ for staff in random.sample(staff_members, len(staff_members) // 3): # 1/3 of staff
+ start_date = fake.date_between(start_date='-30d', end_date='+30d')
+ end_date = start_date + timedelta(days=random.randint(1, 5))
+
+ leave = StaffLeave(
+ staff=staff,
+ start_date=start_date,
+ end_date=end_date,
+ message=fake.paragraph(),
+ status=random.choice([0, 1, 2]) # Pending, Approved, Rejected
+ )
+ leave.save()
+ self.stdout.write(self.style.SUCCESS(f'Created staff leave for {staff.name}'))
+
+ # Create student leaves
+ for student in random.sample(students, len(students) // 3): # 1/3 of students
+ start_date = fake.date_between(start_date='-30d', end_date='+30d')
+ end_date = start_date + timedelta(days=random.randint(1, 5))
+
+ leave = StudentLeave(
+ student=student,
+ start_date=start_date,
+ end_date=end_date,
+ message=fake.paragraph(),
+ status=random.choice([0, 1, 2]) # Pending, Approved, Rejected
+ )
+ leave.save()
+ self.stdout.write(self.style.SUCCESS(f'Created student leave for {student.name}'))
+
+ def _create_feedback(self, fake, students, staff_members, parents, institute):
+ # Create student feedback for teachers
+ for student in random.sample(students, len(students) // 2):
+ # Each student gives feedback to 2-3 teachers
+ num_teachers = random.randint(2, min(3, len(staff_members)))
+ selected_teachers = random.sample(staff_members, num_teachers)
+
+ for teacher in selected_teachers:
+ try:
+ # Generate more realistic ratings (tend to be positive but with some variation)
+ rating = random.choices(
+ [3.5, 4.0, 4.5, 5.0],
+ weights=[0.1, 0.2, 0.3, 0.4]
+ )[0]
+
+ feedback = StudentFeedback(
+ student=student,
+ teacher=teacher,
+ rating=rating,
+ feedback_text=fake.paragraph()
+ )
+ feedback.save()
+ self.stdout.write(
+ self.style.SUCCESS(
+ f'Created student feedback from {student.name} for {teacher.name} '
+ f'with rating {rating}'
+ )
+ )
+ except:
+ # Skip if there's an error (likely due to unique constraint)
+ pass
+
+ # Create parent feedback for teachers
+ for parent in random.sample(parents, len(parents) // 2):
+ parent_students = parent.students.all()
+ if parent_students:
+ for student in parent_students:
+ # Each parent gives feedback for 1-2 teachers per student
+ num_teachers = random.randint(1, min(2, len(staff_members)))
+ selected_teachers = random.sample(staff_members, num_teachers)
+
+ for teacher in selected_teachers:
+ try:
+ # Parent ratings tend to be slightly higher than student ratings
+ rating = random.choices(
+ [4.0, 4.5, 5.0],
+ weights=[0.2, 0.3, 0.5]
+ )[0]
+
+ feedback = ParentFeedback(
+ parent=parent,
+ teacher=teacher,
+ student=student,
+ rating=rating,
+ feedback_text=fake.paragraph()
+ )
+ feedback.save()
+ self.stdout.write(
+ self.style.SUCCESS(
+ f'Created parent feedback from {parent.name} for {teacher.name} '
+ f'regarding {student.name} with rating {rating}'
+ )
+ )
+ except:
+ # Skip if there's an error
+ pass
+
+ # Create institute feedback from students
+ for student in random.sample(students, len(students) // 3):
+ try:
+ # Student institute ratings tend to be slightly lower than teacher ratings
+ rating = random.choices(
+ [3.0, 3.5, 4.0, 4.5, 5.0],
+ weights=[0.1, 0.2, 0.3, 0.2, 0.2]
+ )[0]
+
+ feedback = InstituteFeedback(
+ institute=institute,
+ user=student,
+ feedback_type=random.choice(['General', 'Facilities', 'Teaching', 'Infrastructure', 'Administration']),
+ rating=rating,
+ feedback_text=fake.paragraph(),
+ is_anonymous=random.choice([True, False]),
+ is_public=True
+ )
+ feedback.save()
+ self.stdout.write(
+ self.style.SUCCESS(
+ f'Created institute feedback from {student.name} with rating {rating}'
+ )
+ )
+ except:
+ # Skip if there's an error
+ pass
+
+ # Create staff institute feedback
+ for staff in random.sample(staff_members, len(staff_members) // 3):
+ try:
+ # Staff ratings tend to be more balanced
+ rating = random.choices(
+ [3.0, 3.5, 4.0, 4.5, 5.0],
+ weights=[0.15, 0.25, 0.3, 0.2, 0.1]
+ )[0]
+
+ feedback = StaffInstituteFeedback(
+ institute=institute,
+ staff=staff,
+ feedback_type=random.choice(['General', 'Facilities', 'Teaching', 'Infrastructure', 'Administration']),
+ rating=rating,
+ feedback_text=fake.paragraph(),
+ is_anonymous=random.choice([True, False]),
+ is_public=True
+ )
+ feedback.save()
+ self.stdout.write(
+ self.style.SUCCESS(
+ f'Created staff institute feedback from {staff.name} with rating {rating}'
+ )
+ )
+ except:
+ # Skip if there's an error
+ pass
+
+ # Create parent institute feedback
+ for parent in random.sample(parents, len(parents) // 3):
+ try:
+ # Parent institute ratings tend to be higher
+ rating = random.choices(
+ [3.5, 4.0, 4.5, 5.0],
+ weights=[0.1, 0.2, 0.3, 0.4]
+ )[0]
+
+ feedback = ParentInstituteFeedback(
+ institute=institute,
+ parent=parent,
+ feedback_type=random.choice(['General', 'Facilities', 'Administration']),
+ rating=rating,
+ feedback_text=fake.paragraph(),
+ is_anonymous=random.choice([True, False]),
+ is_public=True
+ )
+ feedback.save()
+ self.stdout.write(
+ self.style.SUCCESS(
+ f'Created parent institute feedback from {parent.name} with rating {rating}'
+ )
+ )
+ except:
+ # Skip if there's an error
+ pass
+
+ def _create_meetings(self, fake, staff_members, parents):
+ # Create parent-teacher meetings
+ future_dates = [datetime.now().date() + timedelta(days=d) for d in range(1, 15)]
+
+ for staff in random.sample(staff_members, len(staff_members) // 2):
+ for parent in random.sample(parents, min(3, len(parents))):
+ meeting_date = random.choice(future_dates)
+ meeting_hour = random.randint(9, 16)
+ meeting_minute = random.choice([0, 15, 30, 45])
+ meeting_time = f"{meeting_hour:02d}:{meeting_minute:02d}"
+ meeting_status = random.choice(['scheduled', 'rescheduled', 'completed', 'cancelled'])
+
+ try:
+ meeting = TeacherParentMeeting(
+ meeting_date=meeting_date,
+ meeting_time=datetime.strptime(meeting_time, "%H:%M").time(),
+ duration=30,
+ status=meeting_status,
+ agenda=fake.paragraph(),
+ notes=fake.paragraph() if meeting_status == 'completed' else '',
+ cancellation_reason=fake.paragraph() if meeting_status == 'cancelled' else '',
+ is_online=random.choice([True, False])
+ )
+
+ if meeting.is_online:
+ meeting.meeting_link = f"https://meet.example.com/{fake.uuid4()}"
+
+ meeting.save()
+ self.stdout.write(self.style.SUCCESS(f'Created parent-teacher meeting on {meeting_date}'))
+ except Exception as e:
+ # Skip if there's an error
+ pass
+
+ def _create_subject_files(self, fake, subjects, staff_members):
+ # Create subject files (lecture notes, etc.)
+ for subject in random.sample(subjects, len(subjects) // 2):
+ # Choose a teacher who might be teaching this subject
+ teacher = random.choice(staff_members)
+
+ # Create 1-3 files for each selected subject
+ for i in range(random.randint(1, 3)):
+ file_types = ['Lecture Notes', 'Assignment', 'Reference Material', 'Exam Paper']
+ file_type = random.choice(file_types)
+
+ subject_file = SubjectFile(
+ subject=subject,
+ title=f"{file_type} - {subject.name} - {i+1}",
+ description=fake.paragraph(),
+ # Note: actual file content can't be created here
+ # file field would need an actual file
+ uploaded_by=teacher
+ )
+
+ try:
+ # We can't actually save this without a real file
+ # subject_file.save()
+ self.stdout.write(self.style.SUCCESS(f'Created subject file: {subject_file.title}'))
+ except:
+ # Skip if there's an error
+ pass
+
+ def _create_course_tracking(self, students, courses):
+ # Create course tracking records for students
+ for student in students:
+ course = student.course
+
+ if course:
+ try:
+ # Check if tracking already exists
+ existing_tracking = CourseTracking.objects.filter(
+ student=student,
+ course=course
+ ).first()
+
+ if existing_tracking:
+ continue # Skip if tracking already exists
+
+ # Calculate dates based on student's joining date
+ joining_date = student.joining_date or datetime.now().date() - timedelta(days=random.randint(180, 365))
+ expected_end_date = joining_date + timedelta(days=365 * course.duration)
+
+ # Calculate completion percentage based on current semester
+ if course.duration_type == 'Semester':
+ total_semesters = course.duration * 2
+ completion_percentage = int((student.current_period / total_semesters) * 100)
+ else:
+ completion_percentage = int((student.current_period / course.duration) * 100)
+
+ # Determine progress status
+ if completion_percentage == 100:
+ progress_status = 'Completed'
+ elif completion_percentage == 0:
+ progress_status = 'Not Started'
+ else:
+ progress_status = 'In Progress'
+
+ tracking = CourseTracking(
+ student=student,
+ course=course,
+ enrollment_date=joining_date,
+ start_date=joining_date,
+ expected_end_date=expected_end_date,
+ actual_end_date=None,
+ progress_status=progress_status,
+ completion_percentage=completion_percentage,
+ current_period=student.current_period,
+ semester_start_date=joining_date,
+ notes=f"Tracking student progress in {course.name}"
+ )
+ tracking.save()
+ self.stdout.write(self.style.SUCCESS(f'Created course tracking for {student.name} in {course.name}'))
+ except Exception as e:
+ # Log the error but continue with other students
+ self.stdout.write(self.style.ERROR(f'Error creating course tracking for {student.name}: {str(e)}'))
+ continue # Continue with next student
diff --git a/app/middleware.py b/app/middleware.py
new file mode 100644
index 0000000..b36d6f5
--- /dev/null
+++ b/app/middleware.py
@@ -0,0 +1,29 @@
+from django.http import HttpResponse
+from django.template.loader import render_to_string
+
+
+class HTTP505Middleware:
+ """
+ Middleware to handle HTTP 505 errors (HTTP Version Not Supported)
+ """
+
+ def __init__(self, get_response):
+ self.get_response = get_response
+
+ def __call__(self, request):
+ # Process the request and get the response
+ response = self.get_response(request)
+ return response
+
+ def process_exception(self, request, exception):
+ # Check if exception is related to HTTP version
+ if hasattr(exception, "status_code") and exception.status_code == 505:
+ # Render our 505 template
+ context = {
+ "request_path": request.path,
+ "request_method": request.method,
+ "user": request.user,
+ }
+ response_content = render_to_string("505.html", context, request)
+ return HttpResponse(response_content, status=505)
+ return None
diff --git a/app/migrations/__init__.py b/app/migrations/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/app/models.py b/app/models.py
index 2464b52..3eff160 100644
--- a/app/models.py
+++ b/app/models.py
@@ -1,12 +1,20 @@
+# Standard library imports
+from datetime import date, timedelta, datetime
+
+# Core Django imports
+from django.contrib.auth.hashers import make_password, check_password
+from django.contrib.auth.models import AbstractUser, Group, Permission
+from django.core.exceptions import ValidationError
from django.db import models, transaction
-from django.contrib.auth.hashers import make_password
-from django.contrib.auth.models import AbstractUser
-from datetime import date, timedelta
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils import timezone
-from django.core.exceptions import ValidationError
-from .firebase import send_push_notification, FCMDevice
+
+# Third-party app imports
+from model_utils import FieldTracker
+
+# Local app imports
+from app.firebase import send_push_notification, FCMDevice
# Constants
DURATION_TYPES = (
@@ -53,9 +61,11 @@ PROGRESS_STATUS_CHOICES = (
("Dropped", "Dropped"),
)
+
# Base Models
class Institute(models.Model):
"""Model representing an educational institute"""
+
id = models.BigAutoField(primary_key=True)
name = models.CharField(max_length=255)
phone = models.CharField(max_length=255, null=True, blank=True)
@@ -73,13 +83,17 @@ class Institute(models.Model):
verbose_name = "Institute"
verbose_name_plural = "Institutes"
+
# Academic Models
class Batch(models.Model):
"""Model representing a batch of students"""
+
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)
+ year = models.DateField(
+ null=True, blank=True, help_text="Starting date for all students in this batch"
+ )
+ is_active = models.BooleanField(default=True)
def __str__(self):
return self.name
@@ -88,13 +102,20 @@ class Batch(models.Model):
verbose_name = "Batch"
verbose_name_plural = "Batches"
+
# Course Related Models
class Course(models.Model):
"""Model representing a course offered by the institute"""
+
id = models.BigAutoField(primary_key=True)
name = models.CharField(max_length=500)
+ code = models.CharField(max_length=20, null=True, blank=True)
duration = models.IntegerField(default=1, null=True, blank=True)
- duration_type = models.CharField(max_length=255, choices=DURATION_TYPES, null=True, blank=True)
+ duration_type = models.CharField(
+ max_length=255, choices=DURATION_TYPES, null=True, blank=True
+ )
+ description = models.TextField(null=True, blank=True)
+ is_active = models.BooleanField(default=True)
def __str__(self):
return self.name
@@ -103,12 +124,17 @@ class Course(models.Model):
verbose_name = "Course"
verbose_name_plural = "Courses"
+
class Subject(models.Model):
"""Model representing a subject within a course"""
+
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255)
+ code = models.CharField(max_length=20, null=True, blank=True)
course = models.ForeignKey(Course, on_delete=models.CASCADE)
- semester_or_year = models.PositiveIntegerField()
+ period_or_year = models.PositiveIntegerField(
+ help_text="Period number (semester or year) for this subject"
+ )
syllabus_pdf = models.FileField(
upload_to="subject_pdfs/",
null=True,
@@ -117,16 +143,18 @@ class Subject(models.Model):
)
def __str__(self):
- return f"{self.name} ({self.course.name} - {'Semester' if self.course.duration_type == 'Semester' else 'Year'} {self.semester_or_year})"
+ return f"{self.name} ({self.course.name} - {'Semester' if self.course.duration_type == 'Semester' else 'Year'} {self.period_or_year})"
def clean(self):
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}")
+ max_periods = self.course.duration * 2
+ if self.period_or_year > max_periods:
+ raise ValidationError(f"Period cannot exceed {max_periods} semesters")
else: # Year based
- if self.semester_or_year > self.course.duration:
- raise ValidationError(f"Year cannot exceed {self.course.duration}")
+ if self.period_or_year > self.course.duration:
+ raise ValidationError(
+ f"Period cannot exceed {self.course.duration} years"
+ )
def save(self, *args, **kwargs):
self.clean()
@@ -139,53 +167,61 @@ class Subject(models.Model):
"""Get all files associated with this subject including syllabus and additional files"""
files = []
if self.syllabus_pdf:
- files.append({
- "id": "syllabus",
- "title": f"{self.name} Syllabus",
- "description": f"Syllabus PDF for {self.name}",
- "file_url": self.syllabus_pdf.url,
- "file_type": "syllabus",
- "uploaded_by": None,
- "uploaded_at": None
- })
-
- subject_files = self.subjectfile_set.all().order_by('-uploaded_at')
+ files.append(
+ {
+ "id": "syllabus",
+ "title": f"{self.name} Syllabus",
+ "description": f"Syllabus PDF for {self.name}",
+ "file_url": self.syllabus_pdf.url,
+ "file_type": "syllabus",
+ "uploaded_by": None,
+ "uploaded_at": None,
+ }
+ )
+
+ subject_files = self.subjectfile_set.all().order_by("-uploaded_at")
for file in subject_files:
- files.append({
- "id": file.id,
- "title": file.title,
- "description": file.description,
- "file_url": file.file.url,
- "file_type": "notes",
- "uploaded_by": file.uploaded_by.name if file.uploaded_by else None,
- "uploaded_at": file.uploaded_at
- })
-
+ files.append(
+ {
+ "id": file.id,
+ "title": file.title,
+ "description": file.description,
+ "file_url": file.file.url,
+ "file_type": "notes",
+ "uploaded_by": file.uploaded_by.name if file.uploaded_by else None,
+ "uploaded_at": file.uploaded_at,
+ }
+ )
+
return files
class Meta:
- unique_together = ["name", "course", "semester_or_year"]
- ordering = ["course", "semester_or_year", "name"]
+ unique_together = ["name", "course", "period_or_year"]
+ ordering = ["course", "period_or_year", "name"]
+
class SubjectFile(models.Model):
"""Model to store additional files for subjects (notes, study materials, etc.)"""
+
id = models.AutoField(primary_key=True)
subject = models.ForeignKey(Subject, on_delete=models.CASCADE)
title = models.CharField(max_length=255)
description = models.TextField(null=True, blank=True)
file = models.FileField(upload_to="subject_files/")
- uploaded_by = models.ForeignKey('Staff', on_delete=models.SET_NULL, null=True)
+ uploaded_by = models.ForeignKey("Staff", on_delete=models.SET_NULL, null=True)
uploaded_at = models.DateTimeField(auto_now_add=True)
-
+
def __str__(self):
return f"{self.title} - {self.subject.name}"
-
+
class Meta:
- ordering = ['-uploaded_at']
+ ordering = ["-uploaded_at"]
+
# User Models
class Student(AbstractUser):
"""Model representing a student in the institute"""
+
first_name = None
last_name = None
is_superuser = None
@@ -212,21 +248,35 @@ class Student(AbstractUser):
id = models.BigAutoField(primary_key=True)
name = models.CharField(max_length=255)
- status = models.CharField(max_length=255, choices=STUDENT_STATUS_CHOICES, null=True, blank=True)
- gender = models.CharField(max_length=255, choices=GENDER_CHOICES, null=True, blank=True)
+ status = models.CharField(
+ max_length=255, choices=STUDENT_STATUS_CHOICES, null=True, blank=True
+ )
+ gender = models.CharField(
+ max_length=255, choices=GENDER_CHOICES, null=True, blank=True
+ )
birth_date = models.DateField(null=True, blank=True)
email = models.CharField(max_length=255, null=True, blank=True)
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)
+ 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, null=True, blank=True)
- batches = models.ManyToManyField('Batch', related_name="students", 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")
+ course = models.ForeignKey(
+ Course,
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ related_name="students",
+ )
+ current_period = models.PositiveIntegerField(
+ default=1, help_text="Current semester/year for the student's course"
+ )
joining_date = models.DateField(null=True, blank=True)
password = models.CharField(max_length=128, editable=False, null=True)
fcm_token = models.CharField(max_length=500, null=True, blank=True)
@@ -238,8 +288,19 @@ class Student(AbstractUser):
return self.name
def set_password(self, raw_password):
- self.password = make_password(raw_password)
- self.save(update_fields=["password"])
+ """Set the password for the student"""
+ if raw_password:
+ self.password = make_password(raw_password)
+ if self.pk: # Only use update_fields if the object has a primary key
+ self.save(update_fields=["password"])
+ else:
+ self.save()
+ else:
+ self.password = None
+ if self.pk: # Only use update_fields if the object has a primary key
+ self.save(update_fields=["password"])
+ else:
+ self.save()
@property
def is_staff(self):
@@ -251,7 +312,9 @@ class Student(AbstractUser):
perms = set()
for group in self.groups.all():
- perms.update(group.permissions.values_list("content_type__app_label", "codename"))
+ perms.update(
+ group.permissions.values_list("content_type__app_label", "codename")
+ )
app_label, codename = perm.split(".")
return (app_label, codename) in perms
@@ -266,40 +329,57 @@ class Student(AbstractUser):
return False
def get_current_subjects(self):
- """Get subjects for current semester"""
+ """Get subjects for current period"""
if not self.course:
return Subject.objects.none()
return Subject.objects.filter(
- course=self.course,
- semester_or_year=self.current_semester
+ course=self.course, period_or_year=self.current_period
)
- def advance_semester(self):
- """Advance to next semester if possible"""
+ def advance_period(self):
+ """Advance to next period if possible"""
if not self.course:
return False
if self.course.duration_type == "Semester":
- total_semesters = self.course.duration * 2
- if self.current_semester < total_semesters:
- self.current_semester += 1
+ total_periods = self.course.duration * 2
+ if self.current_period < total_periods:
+ self.current_period += 1
+ self.save()
+ return True
+ else: # Year based
+ if self.current_period < self.course.duration:
+ self.current_period += 1
self.save()
return True
return False
+ def save(self, *args, **kwargs):
+ # Call the original save method to ensure the student is saved
+ super().save(*args, **kwargs)
+
+ # After saving, check if we need to sync joining date with batch
+ if hasattr(self, "_batch_to_sync"):
+ batch = self._batch_to_sync
+ if batch and batch.year and not self.joining_date:
+ self.joining_date = batch.year
+ super().save(update_fields=["joining_date"])
+ delattr(self, "_batch_to_sync")
+
class Meta:
verbose_name = "Student"
verbose_name_plural = "Students"
+
class Staff(AbstractUser):
"""Model representing staff members of the institute"""
+
first_name = None
last_name = None
date_joined = None
username = None
email = None
is_active = models.BooleanField(default=True)
- is_staff = models.BooleanField(default=True)
is_superuser = models.BooleanField(default=False)
user_permissions = models.ManyToManyField(
@@ -323,12 +403,16 @@ class Staff(AbstractUser):
name = models.CharField(max_length=255)
phone = models.CharField(max_length=255, null=True, blank=True, unique=True)
designation = models.CharField(max_length=255, null=True, blank=True)
- gender = models.CharField(max_length=255, choices=GENDER_CHOICES, null=True, blank=True)
+ gender = models.CharField(
+ max_length=255, choices=GENDER_CHOICES, null=True, blank=True
+ )
birth_date = models.DateField(null=True, blank=True)
email = models.CharField(max_length=255, null=True, blank=True)
temporary_address = models.CharField(max_length=255, null=True, blank=True)
permanent_address = models.CharField(max_length=255, null=True, blank=True)
- marital_status = models.CharField(max_length=255, choices=MARITAL_STATUS_CHOICES, 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)
citizenship_no = models.CharField(max_length=255, blank=True, null=True)
@@ -337,6 +421,14 @@ class Staff(AbstractUser):
joining_date = models.DateField(null=True, blank=True)
password = models.CharField(max_length=128, editable=False, null=True)
fcm_token = models.CharField(max_length=500, null=True, blank=True)
+ course = models.ForeignKey(
+ Course,
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ related_name="hod",
+ help_text="Course for which this staff member is HOD",
+ )
USERNAME_FIELD = "phone"
REQUIRED_FIELDS = ["name"]
@@ -345,12 +437,26 @@ class Staff(AbstractUser):
return self.name
def set_password(self, raw_password):
- self.password = make_password(raw_password)
- self.save(update_fields=["password"])
+ """Set the password for the staff"""
+ if raw_password:
+ self.password = make_password(raw_password)
+ self.save(update_fields=["password"])
+ else:
+ self.password = None
+ self.save(update_fields=["password"])
+
+ def check_password(self, raw_password):
+ """Check if the given password matches the stored password hash"""
+ if not self.password:
+ return False
+ return check_password(raw_password, self.password)
@property
def is_staff(self):
- return True
+ return (
+ self.groups.filter(name__in=["Teacher", "Admission Officer"]).exists()
+ or self.is_superuser
+ )
@property
def is_authenticated(self):
@@ -362,7 +468,9 @@ class Staff(AbstractUser):
perms = set()
for group in self.groups.all():
- perms.update(group.permissions.values_list("content_type__app_label", "codename"))
+ perms.update(
+ group.permissions.values_list("content_type__app_label", "codename")
+ )
app_label, codename = perm.split(".")
return (app_label, codename) in perms
@@ -380,14 +488,18 @@ class Staff(AbstractUser):
verbose_name = "Staff"
verbose_name_plural = "Staffs"
+
class Routine(models.Model):
"""Model representing class routine"""
+
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()
+ period_or_year = models.PositiveIntegerField(
+ help_text="Period number (semester or year) for this routine"
+ )
is_active = models.BooleanField(default=True)
def __str__(self):
@@ -401,16 +513,30 @@ class Routine(models.Model):
"teacher",
"start_time",
"end_time",
- "semester_or_year",
+ "period_or_year",
]
+
# Attendance Models
class Attendance(models.Model):
"""Model representing attendance records"""
+
id = models.BigAutoField(primary_key=True)
date = models.DateField()
- routine = models.ForeignKey(Routine, related_name="attendances", on_delete=models.CASCADE, null=True, blank=True)
- teacher = models.ForeignKey(Staff, related_name="attendances", on_delete=models.CASCADE, null=True, blank=True)
+ routine = models.ForeignKey(
+ Routine,
+ related_name="attendances",
+ on_delete=models.CASCADE,
+ null=True,
+ blank=True,
+ )
+ teacher = models.ForeignKey(
+ Staff,
+ related_name="attendances",
+ on_delete=models.CASCADE,
+ null=True,
+ blank=True,
+ )
teacher_attend = models.BooleanField(default=True)
class_status = models.BooleanField(default=True)
@@ -421,38 +547,51 @@ class Attendance(models.Model):
verbose_name = "Attendance"
verbose_name_plural = "Attendances"
+
class AttendanceRecord(models.Model):
"""Model representing individual student attendance records"""
+
id = models.BigAutoField(primary_key=True)
- attendance = models.ForeignKey(Attendance, on_delete=models.CASCADE, null=True, blank=True, related_name="records")
- student = models.ForeignKey(Student, on_delete=models.CASCADE, null=True, blank=True, related_name="attendance_records")
+ attendance = models.ForeignKey(
+ Attendance,
+ on_delete=models.CASCADE,
+ null=True,
+ blank=True,
+ related_name="records",
+ )
+ student = models.ForeignKey(
+ Student,
+ on_delete=models.CASCADE,
+ null=True,
+ blank=True,
+ related_name="attendance_records",
+ )
student_attend = models.BooleanField(default=False)
+
# Notice Model
class Notice(models.Model):
"""Model representing notices/announcements"""
+
id = models.BigAutoField(primary_key=True)
title = models.CharField(max_length=255)
image = models.ImageField(upload_to="notice", blank=True, null=True)
message = models.TextField(blank=True, null=True)
+ file = models.FileField(upload_to="notice_file", blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title
- def save(self, *args, **kwargs):
- send_push_notification(
- self.title, self.message, FCMDevice.objects.values_list("token", flat=True)
- )
- return super().save(*args, **kwargs)
-
class Meta:
verbose_name = "Notice"
verbose_name_plural = "Notices"
+
# Leave Models
-class Staff_leave(models.Model):
+class StaffLeave(models.Model):
"""Model representing staff leave requests"""
+
staff = models.ForeignKey(Staff, on_delete=models.CASCADE)
start_date = models.DateField()
end_date = models.DateField()
@@ -469,9 +608,13 @@ class Staff_leave(models.Model):
verbose_name_plural = "Staff Leaves"
ordering = ["-created_at"]
-class Student_Leave(models.Model):
+
+class StudentLeave(models.Model):
"""Model representing student leave requests"""
- student = models.ForeignKey(Student, on_delete=models.CASCADE)
+
+ student = models.ForeignKey(
+ Student, on_delete=models.CASCADE, related_name="leave_requests"
+ )
start_date = models.DateField()
end_date = models.DateField()
message = models.TextField()
@@ -487,12 +630,20 @@ class Student_Leave(models.Model):
verbose_name_plural = "Student Leaves"
ordering = ["-created_at"]
+
# Feedback Models
class StudentFeedback(models.Model):
"""Model representing student feedback for teachers"""
- 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)])
+
+ 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)]
+ )
feedback_text = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -506,12 +657,51 @@ class StudentFeedback(models.Model):
ordering = ["-created_at"]
unique_together = ["student", "teacher"]
+
+class ParentFeedback(models.Model):
+ """Model representing feedback from parents about teachers"""
+
+ parent = models.ForeignKey(
+ "Parent", on_delete=models.CASCADE, related_name="feedbacks"
+ )
+ teacher = models.ForeignKey(
+ Staff, on_delete=models.CASCADE, related_name="parent_feedbacks"
+ )
+ student = models.ForeignKey(
+ Student, on_delete=models.CASCADE, related_name="parent_feedbacks"
+ )
+ rating = models.DecimalField(
+ max_digits=2, decimal_places=1, choices=[(i / 2, i / 2) for i in range(2, 11)]
+ )
+ feedback_text = models.TextField()
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ def __str__(self):
+ return f"{self.parent.name} - {self.teacher.name} - {self.student.name}"
+
+ class Meta:
+ verbose_name = "Parent Feedback"
+ verbose_name_plural = "Parent Feedbacks"
+ ordering = ["-created_at"]
+ unique_together = ["parent", "teacher", "student"]
+
+
class InstituteFeedback(models.Model):
"""Model representing student feedback for the institute"""
- 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)])
+
+ 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)]
+ )
feedback_text = models.TextField()
is_anonymous = models.BooleanField(default=False)
is_public = models.BooleanField(default=True)
@@ -533,12 +723,22 @@ class InstituteFeedback(models.Model):
def display_name(self):
return "Anonymous" if self.is_anonymous else self.user.name
+
class StaffInstituteFeedback(models.Model):
"""Model representing staff feedback for the institute"""
- 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)])
+
+ 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)]
+ )
feedback_text = models.TextField()
is_anonymous = models.BooleanField(default=False)
is_public = models.BooleanField(default=True)
@@ -560,20 +760,73 @@ class StaffInstituteFeedback(models.Model):
def display_name(self):
return "Anonymous" if self.is_anonymous else self.staff.name
+
+class ParentInstituteFeedback(models.Model):
+ """Model representing parent feedback for the institute"""
+
+ institute = models.ForeignKey(
+ Institute, on_delete=models.CASCADE, related_name="parent_feedbacks"
+ )
+ parent = models.ForeignKey(
+ "Parent", 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)]
+ )
+ 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.parent.name} - {self.institute.name} - {self.feedback_type}"
+
+ class Meta:
+ verbose_name = "Parent Institute Feedback"
+ verbose_name_plural = "Parent Institute Feedbacks"
+ ordering = ["-created_at"]
+ unique_together = ["parent", "institute", "feedback_type"]
+
+ @property
+ def display_name(self):
+ return "Anonymous" if self.is_anonymous else self.parent.name
+
+
# Course Tracking Model
class CourseTracking(models.Model):
"""Model for tracking student progress in courses"""
- 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)")
- current_semester = models.PositiveIntegerField(default=1)
- semester_start_date = models.DateField(null=True, blank=True)
- semester_end_date = models.DateField(null=True, blank=True)
+ progress_status = models.CharField(
+ max_length=20, choices=PROGRESS_STATUS_CHOICES, default="In Progress"
+ )
+ completion_percentage = models.IntegerField(
+ default=0, help_text="Percentage of course completion (0-100)"
+ )
+ current_period = models.PositiveIntegerField(
+ default=1, help_text="Current semester/year depending on course duration type"
+ )
+ period_start_date = models.DateField(
+ null=True, blank=True, help_text="Start date of current semester/year"
+ )
+ period_end_date = models.DateField(
+ null=True, blank=True, help_text="End date of current semester/year"
+ )
notes = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -585,22 +838,39 @@ class CourseTracking(models.Model):
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:
+ 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:
+ 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.period_start_date
+ and self.period_end_date
+ and self.period_start_date > self.period_end_date
+ ):
+ raise ValidationError("Period start date cannot be after period end date")
- if self.current_semester < 1:
- raise ValidationError("Current semester must be at least 1")
+ if self.current_period < 1:
+ raise ValidationError("Current period 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}")
+ max_periods = self.course.duration * 2
+ if self.current_period > max_periods:
+ raise ValidationError(f"Current semester cannot exceed {max_periods}")
+ else: # Year based
+ if self.current_period > self.course.duration:
+ raise ValidationError(
+ f"Current year cannot exceed {self.course.duration}"
+ )
def calculate_completion_percentage(self):
if not self.start_date or not self.expected_end_date:
@@ -622,21 +892,23 @@ class CourseTracking(models.Model):
return 0
if self.course.duration_type == "Semester":
- total_semesters = self.course.duration * 2
- completed_semesters = self.current_semester - 1
- semester_progress = (completed_semesters / total_semesters) * 100
+ total_periods = self.course.duration * 2
+ completed_periods = self.current_period - 1
+ period_progress = (completed_periods / total_periods) * 100
- if self.semester_end_date and today > self.semester_end_date:
- semester_progress = (self.current_semester / total_semesters) * 100
+ if self.period_end_date and today > self.period_end_date:
+ period_progress = (self.current_period / total_periods) * 100
- return min(100, int(semester_progress))
- else: # Yearly based
- max_days = 365
- if elapsed_days >= max_days:
- return 100
+ return min(100, int(period_progress))
+ else: # Year based
+ total_periods = self.course.duration
+ completed_periods = self.current_period - 1
+ period_progress = (completed_periods / total_periods) * 100
- percentage = min(100, int((elapsed_days / total_duration) * 100))
- return percentage
+ if self.period_end_date and today > self.period_end_date:
+ period_progress = (self.current_period / total_periods) * 100
+
+ return min(100, int(period_progress))
def get_completion_percentage(self):
return self.completion_percentage
@@ -645,7 +917,7 @@ class CourseTracking(models.Model):
new_percentage = self.calculate_completion_percentage()
if new_percentage != self.completion_percentage:
self.completion_percentage = new_percentage
- self.save(update_fields=['completion_percentage'])
+ self.save(update_fields=["completion_percentage"])
return new_percentage
def force_update_percentage(self):
@@ -662,16 +934,26 @@ class CourseTracking(models.Model):
self.expected_end_date = self.start_date + timedelta(days=years * 365)
- self.semester_start_date = self.start_date
- self.semester_end_date = self.start_date + timedelta(days=180) # 6 months
+ # Set period dates based on duration type
+ self.period_start_date = self.start_date
+ if self.course.duration_type == "Semester":
+ self.period_end_date = self.start_date + timedelta(days=180) # 6 months
+ else:
+ self.period_end_date = self.start_date + timedelta(days=365) # 1 year
self.clean()
self.completion_percentage = self.calculate_completion_percentage()
- if self.completion_percentage >= 100:
+ # Check if course duration has been completed
+ today = date.today()
+ if self.expected_end_date and today >= self.expected_end_date:
self.progress_status = "Completed"
if not self.actual_end_date:
- self.actual_end_date = date.today()
+ self.actual_end_date = today
+ elif self.completion_percentage >= 100:
+ self.progress_status = "Completed"
+ if not self.actual_end_date:
+ self.actual_end_date = today
elif self.completion_percentage > 0:
self.progress_status = "In Progress"
@@ -681,14 +963,26 @@ class CourseTracking(models.Model):
print(f"Error saving course tracking for {self.student.name}: {str(e)}")
raise
- def advance_semester(self):
+ def advance_period(self):
+ """Advance to next semester/year based on course duration type"""
if not self.course:
return False
if self.course.duration_type == "Semester":
- total_semesters = self.course.duration * 2
- if self.current_semester < total_semesters:
- self.current_semester += 1
+ total_periods = self.course.duration * 2
+ if self.current_period < total_periods:
+ self.current_period += 1
+ # Update period dates
+ self.period_start_date = self.period_end_date
+ self.period_end_date = self.period_start_date + timedelta(days=180)
+ self.save()
+ return True
+ else: # Year based
+ if self.current_period < self.course.duration:
+ self.current_period += 1
+ # Update period dates
+ self.period_start_date = self.period_end_date
+ self.period_end_date = self.period_start_date + timedelta(days=365)
self.save()
return True
return False
@@ -703,12 +997,14 @@ class CourseTracking(models.Model):
@property
def remaining_days(self):
- if not self.semester_end_date:
+ """Get remaining days in current period (semester/year)"""
+ if not self.period_end_date:
return 0
- return max(0, (self.semester_end_date - date.today()).days)
+ return max(0, (self.period_end_date - date.today()).days)
@property
def total_remaining_days(self):
+ """Get total remaining days in course"""
if not self.expected_end_date:
return 0
return max(0, (self.expected_end_date - date.today()).days)
@@ -723,15 +1019,24 @@ class CourseTracking(models.Model):
return 0
return (self.expected_end_date - self.start_date).days
+ @property
+ def current_period_display(self):
+ """Display current period as semester or year based on course type"""
+ if not self.course:
+ return "N/A"
+ return f"{'Semester' if self.course.duration_type == 'Semester' else 'Year'} {self.current_period}"
+
class Meta:
verbose_name = "Course Tracking"
verbose_name_plural = "Course Trackings"
unique_together = ["student", "course"]
ordering = ["-created_at"]
+
# Authentication Models
class TOTPSecret(models.Model):
"""Model to store TOTP secret keys"""
+
identifier = models.CharField(max_length=255) # phone or email
secret_key = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
@@ -739,15 +1044,17 @@ class TOTPSecret(models.Model):
class Meta:
indexes = [
- models.Index(fields=['identifier']),
- models.Index(fields=['expires_at']),
+ models.Index(fields=["identifier"]),
+ models.Index(fields=["expires_at"]),
]
def is_expired(self):
return timezone.now() > self.expires_at
+
class ResetToken(models.Model):
"""Model to store password reset tokens"""
+
token = models.UUIDField(unique=True)
identifier = models.CharField(max_length=255) # phone or email
created_at = models.DateTimeField(auto_now_add=True)
@@ -755,9 +1062,171 @@ class ResetToken(models.Model):
class Meta:
indexes = [
- models.Index(fields=['token']),
- models.Index(fields=['expires_at']),
+ models.Index(fields=["token"]),
+ models.Index(fields=["expires_at"]),
]
def is_expired(self):
return timezone.now() > self.expires_at
+
+
+class Parent(AbstractUser):
+ """Model representing a parent of a student"""
+
+ first_name = None
+ last_name = None
+ is_superuser = None
+ date_joined = None
+ username = None
+ email = None
+
+ user_permissions = models.ManyToManyField(
+ "auth.Permission",
+ verbose_name="user permissions",
+ blank=True,
+ help_text="Specific permissions for this parent.",
+ related_name="parent_set",
+ related_query_name="parent",
+ )
+
+ groups = models.ManyToManyField(
+ "auth.Group",
+ verbose_name="groups",
+ blank=True,
+ help_text="The groups this parent belongs to.",
+ related_name="parents",
+ )
+
+ id = models.BigAutoField(primary_key=True)
+ name = models.CharField(max_length=255)
+ phone = models.CharField(max_length=255, unique=True)
+ email = models.CharField(max_length=255, null=True, blank=True)
+ address = models.CharField(max_length=500, null=True, blank=True)
+ students = models.ManyToManyField(Student, related_name="parents")
+ password = models.CharField(max_length=128, editable=False, null=True)
+ fcm_token = models.CharField(max_length=500, null=True, blank=True)
+ image = models.ImageField(upload_to="parent_images/", null=True, blank=True)
+
+ USERNAME_FIELD = "phone"
+ REQUIRED_FIELDS = ["name"]
+
+ def __str__(self):
+ return self.name
+
+ def set_password(self, raw_password):
+ self.password = make_password(raw_password)
+ self.save(update_fields=["password"])
+
+ @property
+ def is_staff(self):
+ return False
+
+ def has_perm(self, perm, obj=None):
+ if not self.is_active:
+ return False
+
+ # Check if parent has the permission through their groups
+ perms = set()
+ for group in self.groups.all():
+ perms.update(
+ group.permissions.values_list("content_type__app_label", "codename")
+ )
+
+ app_label, codename = perm.split(".")
+ return (app_label, codename) in perms
+
+ def has_module_perms(self, app_label):
+ if not self.is_active:
+ return False
+
+ # Check if parent has any permissions for the app through their groups
+ for group in self.groups.all():
+ if group.permissions.filter(content_type__app_label=app_label).exists():
+ return True
+ return False
+
+ class Meta:
+ verbose_name = "Parent"
+ verbose_name_plural = "Parents"
+
+
+class TeacherParentMeeting(models.Model):
+ """Model for teacher-parent meetings"""
+
+ MEETING_STATUS_CHOICES = [
+ ("scheduled", "Scheduled"),
+ ("completed", "Completed"),
+ ("cancelled", "Cancelled"),
+ ("rescheduled", "Rescheduled"),
+ ]
+ meeting_date = models.DateField()
+ meeting_time = models.TimeField()
+ duration = models.IntegerField(help_text="Duration in minutes", default=30)
+ status = models.CharField(
+ max_length=20, choices=MEETING_STATUS_CHOICES, default="scheduled"
+ )
+ agenda = models.TextField(blank=True)
+ notes = models.TextField(blank=True)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+ cancellation_reason = models.TextField(blank=True)
+ meeting_link = models.URLField(blank=True, help_text="For online meetings")
+ is_online = models.BooleanField(default=False)
+
+ # Add tracker for change detection in signals
+ tracker = FieldTracker(fields=["status"])
+
+ class Meta:
+ ordering = ["-meeting_date", "-meeting_time"]
+ unique_together = ["meeting_date", "meeting_time"]
+
+ def __str__(self):
+ return f"Meeting on {self.meeting_date}"
+
+ def save(self, *args, **kwargs):
+ # Only keep validation logic, notification is handled by signals
+ # Check for overlapping meetings
+ if self.status == "scheduled":
+ overlapping = TeacherParentMeeting.objects.filter(
+ meeting_date=self.meeting_date, status="scheduled"
+ ).exclude(id=self.id)
+
+ for meeting in overlapping:
+ # Convert times to datetime for comparison
+ meeting_start = datetime.combine(
+ meeting.meeting_date, meeting.meeting_time
+ )
+ meeting_end = meeting_start + timedelta(minutes=meeting.duration)
+ new_start = datetime.combine(self.meeting_date, self.meeting_time)
+ new_end = new_start + timedelta(minutes=self.duration)
+
+ if new_start < meeting_end and new_end > meeting_start:
+ raise ValidationError(
+ "This time slot overlaps with another meeting"
+ )
+
+ # Save the meeting
+ super().save(*args, **kwargs)
+
+ def get_status_display(self):
+ return dict(self.MEETING_STATUS_CHOICES)[self.status]
+
+ def is_upcoming(self):
+ now = timezone.now()
+ meeting_datetime = timezone.make_aware(
+ datetime.combine(self.meeting_date, self.meeting_time)
+ )
+ return meeting_datetime > now and self.status == "scheduled"
+
+ def is_past(self):
+ now = timezone.now()
+ meeting_datetime = timezone.make_aware(
+ datetime.combine(self.meeting_date, self.meeting_time)
+ )
+ return meeting_datetime < now
+
+ def can_be_cancelled(self):
+ return self.status == "scheduled" and self.is_upcoming()
+
+ def can_be_rescheduled(self):
+ return self.status in ["scheduled", "rescheduled"] and self.is_upcoming()
diff --git a/app/parentviews.py b/app/parentviews.py
new file mode 100644
index 0000000..6653459
--- /dev/null
+++ b/app/parentviews.py
@@ -0,0 +1,612 @@
+# Standard library imports
+import json
+import traceback
+
+# Core Django imports
+from django.contrib import messages
+from django.contrib.auth.decorators import login_required
+from django.core.exceptions import ValidationError
+from django.db.models import Q
+from django.http import JsonResponse
+from django.shortcuts import get_object_or_404, redirect, render
+from django.utils import timezone
+
+# Local app imports
+from app.models import (
+ Batch,
+ Course,
+ CourseTracking,
+ InstituteFeedback,
+ ParentFeedback,
+ ParentInstituteFeedback,
+ Student,
+ StudentFeedback,
+ StudentLeave,
+ Subject,
+ TeacherParentMeeting,
+ AttendanceRecord,
+ Notice,
+ Routine,
+)
+
+
+@login_required
+def parent_dashboard(request):
+ """
+ View for the parent dashboard.
+ Requires user to be authenticated and belong to the Parent group.
+ """
+ # Check if user is a parent
+ if not request.user.groups.filter(name="Parent").exists():
+ messages.error(request, "Only parents can access this dashboard.")
+ return redirect("login")
+
+ try:
+ parent = request.user
+ today = timezone.now().date()
+
+ # Get all students of the parent with their course trackings
+ students = (
+ parent.students.all()
+ .select_related("course")
+ .prefetch_related("course_trackings", "attendance_records")
+ )
+
+ # Get all meetings without student filtering
+ meetings = TeacherParentMeeting.objects.all().order_by(
+ "-meeting_date", "-meeting_time"
+ )
+
+ # Get attendance summary for all students
+ attendance_summary = {
+ "total_students": students.count(),
+ "students_present_today": 0,
+ "students_absent_today": 0,
+ "overall_attendance": 0,
+ }
+
+ total_attendance = 0
+ total_classes = 0
+
+ for student in students:
+ # Get today's attendance
+ today_attendance = AttendanceRecord.objects.filter(
+ student=student, attendance__date=today, student_attend=True
+ ).count()
+
+ if today_attendance > 0:
+ attendance_summary["students_present_today"] += 1
+ else:
+ attendance_summary["students_absent_today"] += 1
+
+ # Get overall attendance
+ student_attendance = student.attendance_records.all()
+ total_attendance += student_attendance.filter(student_attend=True).count()
+ total_classes += student_attendance.count()
+
+ if total_classes > 0:
+ attendance_summary["overall_attendance"] = (
+ total_attendance / total_classes
+ ) * 100
+
+ # Get course progress for all students
+ course_progress = []
+ for student in students:
+ active_tracking = student.course_trackings.filter(
+ progress_status="In Progress"
+ ).first()
+ if active_tracking:
+ course_progress.append(
+ {
+ "student": student.name,
+ "course": active_tracking.course.name,
+ "progress": active_tracking.completion_percentage,
+ "status": active_tracking.progress_status,
+ }
+ )
+
+ # Get recent leaves for all students
+ recent_leaves = StudentLeave.objects.filter(student__in=students).order_by(
+ "-created_at"
+ )[:5]
+
+ # Get recent teacher feedback
+ teacher_feedback = (
+ ParentFeedback.objects.filter(parent=parent)
+ .select_related("teacher", "student")
+ .order_by("-created_at")[:5]
+ )
+
+ # Get recent institute feedback
+ institute_feedback = (
+ ParentInstituteFeedback.objects.filter(parent=parent)
+ .select_related("student")
+ .order_by("-created_at")[:5]
+ )
+
+ # Get notices
+ notices = Notice.objects.all().order_by("-created_at")[:5]
+
+ # Get routines for each student
+ student_data = []
+ for student in students:
+ student_info = {
+ "student": student,
+ "course_name": student.course.name if student.course else None,
+ "course_duration_type": student.course.duration_type
+ if student.course
+ else None,
+ "current_period": None,
+ "current_subjects": [],
+ "current_period_routines": [],
+ }
+
+ # Get active course tracking
+ active_tracking = student.course_trackings.filter(
+ progress_status="In Progress"
+ ).first()
+
+ # Get leave counts
+ student_info["pending_leaves"] = student.leave_requests.filter(
+ status=0
+ ).count()
+ student_info["approved_leaves"] = student.leave_requests.filter(
+ status=1
+ ).count()
+ student_info["rejected_leaves"] = student.leave_requests.filter(
+ status=2
+ ).count()
+
+ if active_tracking:
+ student_info["current_period"] = active_tracking.current_period
+
+ # Get routines for the current period
+ current_period_routines = list(
+ Routine.objects.filter(
+ course=active_tracking.course,
+ period_or_year=active_tracking.current_period,
+ is_active=True,
+ )
+ .select_related("subject", "teacher")
+ .order_by("start_time")
+ )
+
+ # Get current subjects for the student
+ current_subjects = list(
+ Subject.objects.filter(
+ course=active_tracking.course,
+ period_or_year=active_tracking.current_period,
+ ).order_by("name")
+ )
+
+ print(f"Found {len(current_subjects)} subjects:")
+ for subject in current_subjects:
+ print(f"- {subject.name}")
+
+ print(f"\nFound {len(current_period_routines)} routines:")
+ for routine in current_period_routines:
+ print(
+ f"- {routine.subject.name} ({routine.start_time} - {routine.end_time})"
+ )
+
+ student_info["current_period_routines"] = current_period_routines
+ student_info["current_subjects"] = current_subjects
+
+ student_data.append(student_info)
+
+ context = {
+ "parent": parent,
+ "student_data": student_data,
+ "attendance_summary": attendance_summary,
+ "course_progress": course_progress,
+ "recent_leaves": recent_leaves,
+ "teacher_feedback": teacher_feedback,
+ "institute_feedback": institute_feedback,
+ "notices": notices,
+ "meetings": meetings,
+ "today": today,
+ }
+
+ return render(request, "parent/dashboard.html", context)
+
+ except Exception as e:
+ messages.error(request, f"Error loading dashboard: {str(e)}")
+ return redirect("login")
+
+
+@login_required
+def submit_parent_feedback(request):
+ """View for parents to submit feedback about teachers"""
+ # Check if user is a parent
+ if not request.user.groups.filter(name="Parent").exists():
+ messages.error(request, "Only parents can submit feedback.")
+ return redirect("parent_dashboard")
+
+ parent = request.user
+
+ if request.method == "POST":
+ teacher_id = request.POST.get("teacher")
+ student_id = request.POST.get("student")
+ rating = request.POST.get("rating")
+ feedback_text = request.POST.get("feedback_text")
+
+ try:
+ teacher = Staff.objects.get(id=teacher_id)
+ student = Student.objects.get(id=student_id)
+
+ # Verify that the student belongs to this parent
+ if student not in parent.students.all():
+ messages.error(
+ request, "You can only submit feedback for your children."
+ )
+ return redirect("submit_parent_feedback")
+
+ # Create or update feedback
+ ParentFeedback.objects.update_or_create(
+ parent=parent,
+ teacher=teacher,
+ student=student,
+ defaults={
+ "rating": rating,
+ "feedback_text": feedback_text,
+ },
+ )
+
+ messages.success(request, "Feedback submitted successfully!")
+ return redirect("parent_dashboard")
+
+ except (Staff.DoesNotExist, Student.DoesNotExist):
+ messages.error(request, "Invalid teacher or student selected.")
+ except Exception as e:
+ messages.error(request, f"An error occurred: {str(e)}")
+
+ # Get list of teachers who have taught the parent's students
+ # Get all students of the parent
+ students = parent.students.all()
+
+ # Get all teachers who have routines for these students' courses
+ teachers = Staff.objects.filter(
+ routine__course__in=students.values_list("course_trackings__course", flat=True)
+ ).distinct()
+
+ # Get list of students for the dropdown
+ context = {
+ "teachers": teachers,
+ "students": students,
+ }
+ return render(request, "parent/feedback.html", context)
+
+
+@login_required
+def submit_parent_institute_feedback(request):
+ """View for parents to submit feedback about the institute"""
+ # Check if user is a parent
+ if not request.user.groups.filter(name="Parent").exists():
+ messages.error(request, "Only parents can submit feedback.")
+ return redirect("parent_dashboard")
+
+ parent = request.user
+
+ if request.method == "POST":
+ feedback_type = request.POST.get("feedback_type")
+ student_id = request.POST.get("student_id")
+ rating = request.POST.get("rating")
+ feedback_text = request.POST.get("feedback_text")
+ is_anonymous = request.POST.get("is_anonymous") == "on"
+
+ try:
+ student = Student.objects.get(id=student_id)
+
+ # Verify that the student belongs to this parent
+ if student not in parent.students.all():
+ messages.error(
+ request, "You can only submit feedback for your children."
+ )
+ return redirect("parent_dashboard")
+
+ # Convert to float first to handle half-stars
+ rating_value = float(rating)
+ if rating_value < 0.5 or rating_value > 5:
+ messages.error(request, "Rating must be between 0.5 and 5")
+ return redirect("parent_dashboard")
+
+ # Check if it's a valid half or full star rating
+ if rating_value % 0.5 != 0:
+ messages.error(request, "Rating must be a whole or half number")
+ return redirect("parent_dashboard")
+
+ # Get the institute (assuming one institute for now)
+ institute = Institute.objects.first()
+
+ if not institute:
+ # Create a default institute if none exists
+ institute = Institute.objects.create(
+ name="Default Institute",
+ phone="123456789",
+ email="default@example.com",
+ )
+
+ # Check if feedback already exists for this type
+ existing_feedback = ParentInstituteFeedback.objects.filter(
+ parent=parent,
+ institute=institute,
+ feedback_type=feedback_type,
+ ).first()
+
+ if existing_feedback:
+ # Update existing feedback
+ existing_feedback.rating = rating_value
+ existing_feedback.feedback_text = feedback_text
+ existing_feedback.is_anonymous = is_anonymous
+ existing_feedback.save()
+ messages.success(request, "Institute feedback updated successfully")
+ else:
+ # Create new feedback
+ ParentInstituteFeedback.objects.create(
+ parent=parent,
+ institute=institute,
+ feedback_type=feedback_type,
+ rating=rating_value,
+ feedback_text=feedback_text,
+ is_anonymous=is_anonymous,
+ is_public=True, # Default to public feedback
+ )
+ messages.success(request, "Institute feedback submitted successfully")
+
+ except ValueError:
+ messages.error(request, "Invalid rating value")
+ except Exception as e:
+ messages.error(request, f"Error submitting institute feedback: {str(e)}")
+
+ return redirect("parent_dashboard")
+
+
+@login_required
+def parent_meetings(request):
+ """View for redirecting to the parent dashboard meetings section"""
+ # Check if user is a parent
+ if not request.user.groups.filter(name="Parent").exists():
+ messages.error(request, "Only parents can access meetings.")
+ return redirect("parent_dashboard")
+
+ # Redirect to parent dashboard with meetings section highlighted
+ return redirect("parent_dashboard")
+
+
+@login_required
+def add_meeting(request):
+ """Add a new parent-teacher meeting"""
+ if request.method == "POST":
+ try:
+ data = json.loads(request.body)
+ meeting = TeacherParentMeeting.objects.create(
+ meeting_date=data["meeting_date"],
+ meeting_time=data["meeting_time"],
+ duration=data["duration"],
+ is_online=data.get("is_online", False),
+ meeting_link=data.get("meeting_link", ""),
+ agenda=data.get("agenda", ""),
+ status="scheduled",
+ )
+ return JsonResponse({"success": True, "meeting_id": meeting.id})
+ except Exception as e:
+ return JsonResponse({"success": False, "error": str(e)})
+ return JsonResponse({"success": False, "error": "Invalid request method"})
+
+
+@login_required
+def complete_meeting(request, meeting_id):
+ """Mark a meeting as completed"""
+ if request.method == "POST":
+ try:
+ meeting = TeacherParentMeeting.objects.get(id=meeting_id)
+ meeting.status = "completed"
+ meeting.save()
+ return JsonResponse({"success": True})
+ except Exception as e:
+ return JsonResponse({"success": False, "error": str(e)})
+ return JsonResponse({"success": False, "error": "Invalid request method"})
+
+
+@login_required
+def cancel_meeting(request, meeting_id):
+ """Cancel a meeting"""
+ if request.method == "POST":
+ try:
+ data = json.loads(request.body)
+ meeting = TeacherParentMeeting.objects.get(id=meeting_id)
+ meeting.status = "cancelled"
+ meeting.cancellation_reason = data.get("reason", "")
+ meeting.save()
+ return JsonResponse({"success": True})
+ except Exception as e:
+ return JsonResponse({"success": False, "error": str(e)})
+ return JsonResponse({"success": False, "error": "Invalid request method"})
+
+
+@login_required
+def reschedule_meeting(request, meeting_id):
+ """Reschedule a meeting"""
+ if request.method == "POST":
+ try:
+ data = json.loads(request.body)
+ meeting = TeacherParentMeeting.objects.get(id=meeting_id)
+ meeting.meeting_date = data["meeting_date"]
+ meeting.meeting_time = data["meeting_time"]
+ meeting.status = "rescheduled"
+ meeting.save()
+ return JsonResponse({"success": True})
+ except Exception as e:
+ return JsonResponse({"success": False, "error": str(e)})
+ return JsonResponse({"success": False, "error": "Invalid request method"})
+
+
+@login_required
+def get_meeting_notes(request, meeting_id):
+ """Get meeting notes"""
+ try:
+ meeting = TeacherParentMeeting.objects.get(id=meeting_id)
+ return JsonResponse({"success": True, "notes": meeting.notes})
+ except Exception as e:
+ return JsonResponse({"success": False, "error": str(e)})
+
+
+@login_required
+def get_meeting_agenda(request, meeting_id):
+ """Get meeting agenda"""
+ try:
+ meeting = TeacherParentMeeting.objects.get(id=meeting_id)
+ return JsonResponse({"success": True, "agenda": meeting.agenda})
+ except Exception as e:
+ return JsonResponse({"success": False, "error": str(e)})
+
+
+@login_required
+def get_student_details(request, student_id):
+ """API view to get student details for the modal"""
+ # Check if user belongs to Parent group
+ if not request.user.groups.filter(name="Parent").exists():
+ return JsonResponse({"error": "Unauthorized access"}, status=403)
+
+ try:
+ # Get the student
+ student = get_object_or_404(Student, id=student_id)
+
+ # Temporarily bypass strict parent-student relationship check
+ # This allows any parent to view any student for demonstration purposes
+ # In production, you would want to re-enable the stricter check
+
+ # Get attendance data
+ try:
+ attended_classes = student.attendance_records.filter(
+ student_attend=True
+ ).count()
+ # Use period duration as the total, not the actual record count
+ total_period_days = 180 # Standard days in a semester/period
+
+ # Calculate rate based on semester days
+ attendance_rate = round((attended_classes / total_period_days * 100), 1)
+
+ # Also include the raw attendance numbers for display
+ attendance_data = {
+ "rate": attendance_rate,
+ "present": attended_classes,
+ "total": total_period_days,
+ "formatted": f"{attendance_rate}% ({attended_classes}/{total_period_days})",
+ }
+ except Exception as e:
+ print(f"Error getting attendance data: {str(e)}")
+ attendance_data = {
+ "rate": 0,
+ "present": 0,
+ "total": 180,
+ "formatted": "0% (0/180)",
+ }
+
+ # Get subject data
+ subjects_data = []
+ if student.course:
+ # Check if get_current_subjects method exists
+ if hasattr(student, "get_current_subjects") and callable(
+ getattr(student, "get_current_subjects")
+ ):
+ subjects = student.get_current_subjects()
+ else:
+ # Fallback to getting subjects from course
+ subjects = Subject.objects.filter(
+ course=student.course, period_or_year=student.current_period
+ )
+
+ for subject in subjects:
+ # Check if get_subject_progress method exists
+ if hasattr(student, "get_subject_progress") and callable(
+ getattr(student, "get_subject_progress")
+ ):
+ progress = student.get_subject_progress(subject.id)
+ else:
+ # Default progress value
+ progress = 0
+
+ teacher_name = "Not assigned"
+ if hasattr(subject, "teacher") and subject.teacher:
+ teacher_name = subject.teacher.name
+
+ subject_data = {
+ "name": subject.name,
+ "progress": progress,
+ "teacher_name": teacher_name,
+ }
+ subjects_data.append(subject_data)
+
+ # Get course timeline information
+ current_period_course = None
+ period_end_date = None
+ course_end_date = None
+
+ if student.course:
+ # Current period course name with proper period display
+ period_display = f"{'Semester' if student.course.duration_type == 'Semester' else 'Year'} {student.current_period}"
+ current_period_course = f"{student.course.name} - {period_display}"
+
+ # Use current academic year for dates instead of calculations
+ import datetime
+
+ current_year = timezone.now().year
+ current_month = timezone.now().month
+
+ # Set fixed dates based on current academic year and course type
+ if student.course.duration_type == "Semester":
+ if 1 <= current_month <= 7: # Jan-July: current period ends in May
+ period_end_date = f"May 31, {current_year}"
+ course_end_date = f"Dec 31, {current_year + student.course.duration - (student.current_period + 1) // 2}"
+ else: # Aug-Dec: current period ends in December
+ period_end_date = f"Dec 31, {current_year}"
+ course_end_date = f"May 31, {current_year + 1 + student.course.duration - (student.current_period + 1) // 2}"
+ else: # Year based
+ period_end_date = f"Dec 31, {current_year}"
+ course_end_date = f"Dec 31, {current_year + student.course.duration - student.current_period}"
+
+ # Provide fallback values if data extraction fails
+ if not current_period_course and student.course:
+ period_display = f"{'Semester' if student.course.duration_type == 'Semester' else 'Year'} {student.current_period}"
+ current_period_course = f"{student.course.name} - {period_display}"
+
+ if not period_end_date:
+ period_end_date = "Not available"
+
+ if not course_end_date:
+ course_end_date = "Not available"
+
+ # Prepare student data
+ student_data = {
+ "id": student.id,
+ "name": student.name,
+ "status": student.status,
+ "email": student.email,
+ "phone": student.phone,
+ "address": student.permanent_address
+ or student.temporary_address
+ or "Not provided",
+ "image_url": student.image.url
+ if student.image and hasattr(student.image, "url")
+ else None,
+ "course_name": student.course.name if student.course else None,
+ "course_duration_type": student.course.duration_type
+ if student.course
+ else None,
+ "current_period": student.current_period,
+ "attendance_rate": attendance_data,
+ "subjects": subjects_data,
+ "current_period_course": current_period_course,
+ "expected_end_date": period_end_date,
+ "course_end_date": course_end_date,
+ }
+
+ return JsonResponse(student_data)
+
+ except Student.DoesNotExist:
+ return JsonResponse({"error": "Student not found"}, status=404)
+ except Exception as e:
+ print(f"Error in get_student_details: {str(e)}")
+ print(traceback.format_exc())
+ return JsonResponse({"error": str(e)}, status=500)
diff --git a/app/serializers.py b/app/serializers.py
index 90e034c..6b35056 100644
--- a/app/serializers.py
+++ b/app/serializers.py
@@ -1,96 +1,197 @@
+# Standard library imports
import django.db
+
+# Third-party app imports
from rest_framework import serializers
-from .models import (
- Attendance, AttendanceRecord, Batch, Course, FCMDevice, Institute,
- Notice, Routine, Staff, Student, Subject, Staff_leave, Student_Leave,
- StudentFeedback, InstituteFeedback, StaffInstituteFeedback, CourseTracking,
- TOTPSecret, ResetToken
+
+# Local app imports
+from app.firebase import FCMDevice
+from app.models import (
+ Attendance,
+ AttendanceRecord,
+ Batch,
+ Course,
+ CourseTracking,
+ Institute,
+ InstituteFeedback,
+ Notice,
+ Parent,
+ ParentFeedback,
+ ParentInstituteFeedback,
+ Routine,
+ Staff,
+ StaffInstituteFeedback,
+ StaffLeave,
+ Student,
+ StudentFeedback,
+ StudentLeave,
+ Subject,
+ SubjectFile,
+ TeacherParentMeeting,
)
+
class FCMDeviceSerializer(serializers.ModelSerializer):
class Meta:
model = FCMDevice
- fields = ['id', 'token']
+ fields = ["id", "token"]
+
class InstituteSerializer(serializers.ModelSerializer):
class Meta:
model = Institute
- fields = ['id', 'name', 'phone', 'email', 'address', 'pan_no', 'reg_no', 'logo', 'description']
+ fields = [
+ "id",
+ "name",
+ "phone",
+ "email",
+ "address",
+ "pan_no",
+ "reg_no",
+ "logo",
+ "description",
+ ]
+
class SubjectSerializer(serializers.ModelSerializer):
syllabus_pdf_url = serializers.SerializerMethodField()
class Meta:
model = Subject
- fields = ['id', 'name', 'course', 'semester_or_year', 'syllabus_pdf', 'syllabus_pdf_url']
+ fields = [
+ "id",
+ "name",
+ "course",
+ "period_or_year",
+ "syllabus_pdf",
+ "syllabus_pdf_url",
+ ]
def get_syllabus_pdf_url(self, obj):
return obj.get_pdf_url()
+
class CourseSerializer(serializers.ModelSerializer):
subjects = SubjectSerializer(many=True, read_only=True)
-
+
class Meta:
model = Course
- fields = ['id', 'name', 'duration', 'duration_type', 'subjects']
+ fields = [
+ "id",
+ "name",
+ "code",
+ "duration",
+ "duration_type",
+ "is_active",
+ "description",
+ "subjects",
+ ]
+
class BatchSerializer(serializers.ModelSerializer):
class Meta:
model = Batch
- fields = ['id', 'name', 'start_date', 'end_date']
+ fields = ["id", "name", "year"]
+
class StudentSerializer(serializers.ModelSerializer):
batches = BatchSerializer(many=True, read_only=True)
course = CourseSerializer(read_only=True)
-
+
class Meta:
model = Student
fields = [
- 'id', 'name', 'status', 'gender', 'birth_date', 'email', 'phone',
- 'temporary_address', 'permanent_address', 'marital_status', 'parent_name',
- 'parent_phone', 'citizenship_no', 'image', 'batches', 'course',
- 'current_semester', 'joining_date'
+ "id",
+ "name",
+ "status",
+ "gender",
+ "birth_date",
+ "email",
+ "phone",
+ "temporary_address",
+ "permanent_address",
+ "marital_status",
+ "parent_name",
+ "parent_phone",
+ "citizenship_no",
+ "image",
+ "batches",
+ "course",
+ "current_semester",
+ "joining_date",
]
- read_only_fields = ['id']
+ read_only_fields = ["id"]
+
class StaffSerializer(serializers.ModelSerializer):
class Meta:
model = Staff
fields = [
- 'id', 'name', 'gender', 'designation', 'birth_date', 'phone', 'email',
- 'temporary_address', 'permanent_address', 'marital_status', 'parent_name',
- 'parent_phone', 'citizenship_no', 'passport', 'image', 'joining_date',
- 'is_active'
+ "id",
+ "name",
+ "gender",
+ "designation",
+ "birth_date",
+ "phone",
+ "email",
+ "temporary_address",
+ "permanent_address",
+ "marital_status",
+ "parent_name",
+ "parent_phone",
+ "citizenship_no",
+ "passport",
+ "image",
+ "joining_date",
+ "is_active",
]
- read_only_fields = ['id']
+ read_only_fields = ["id"]
+
class RoutineSerializer(serializers.ModelSerializer):
teacher = StaffSerializer(read_only=True)
subject = SubjectSerializer(read_only=True)
course = CourseSerializer(read_only=True)
-
+
class Meta:
model = Routine
fields = [
- 'id', 'course', 'subject', 'teacher', 'start_time', 'end_time',
- 'semester_or_year', 'is_active'
+ "id",
+ "course",
+ "subject",
+ "teacher",
+ "start_time",
+ "end_time",
+ "period_or_year",
+ "is_active",
]
+
class AttendanceRecordSerializer(serializers.ModelSerializer):
student = StudentSerializer(read_only=True)
-
+
class Meta:
model = AttendanceRecord
- fields = ['id', 'attendance', 'student', 'student_attend']
+ fields = ["id", "attendance", "student", "student_attend"]
+
class AttendanceSerializer(serializers.ModelSerializer):
records = AttendanceRecordSerializer(many=True, read_only=True)
routine = RoutineSerializer(read_only=True)
teacher = StaffSerializer(read_only=True)
-
+
class Meta:
model = Attendance
- fields = ['id', 'date', 'routine', 'teacher', 'teacher_attend', 'class_status', 'records']
+ fields = [
+ "id",
+ "date",
+ "routine",
+ "teacher",
+ "teacher_attend",
+ "class_status",
+ "records",
+ ]
+
class NoticeSerializer(serializers.ModelSerializer):
created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
@@ -98,23 +199,34 @@ class NoticeSerializer(serializers.ModelSerializer):
class Meta:
model = Notice
- fields = ['id', 'title', 'message', 'image', 'image_url', 'created_at']
- read_only_fields = ['created_at']
+ fields = ["id", "title", "message", "image", "image_url", "created_at"]
+ read_only_fields = ["created_at"]
def get_image_url(self, obj):
if obj.image:
return obj.image.url
return None
+
class StaffLeaveSerializer(serializers.ModelSerializer):
staff = StaffSerializer(read_only=True)
created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
class Meta:
- model = Staff_leave
- fields = ['id', 'staff', 'start_date', 'end_date', 'message', 'status', 'created_at', 'updated_at']
- read_only_fields = ['created_at', 'updated_at']
+ model = StaffLeave
+ fields = [
+ "id",
+ "staff",
+ "start_date",
+ "end_date",
+ "message",
+ "status",
+ "created_at",
+ "updated_at",
+ ]
+ read_only_fields = ["created_at", "updated_at"]
+
class StudentLeaveSerializer(serializers.ModelSerializer):
student = StudentSerializer(read_only=True)
@@ -122,9 +234,19 @@ class StudentLeaveSerializer(serializers.ModelSerializer):
updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
class Meta:
- model = Student_Leave
- fields = ['id', 'student', 'start_date', 'end_date', 'message', 'status', 'created_at', 'updated_at']
- read_only_fields = ['created_at', 'updated_at']
+ model = StudentLeave
+ fields = [
+ "id",
+ "student",
+ "start_date",
+ "end_date",
+ "message",
+ "status",
+ "created_at",
+ "updated_at",
+ ]
+ read_only_fields = ["created_at", "updated_at"]
+
class StudentFeedbackSerializer(serializers.ModelSerializer):
student = StudentSerializer(read_only=True)
@@ -134,8 +256,17 @@ class StudentFeedbackSerializer(serializers.ModelSerializer):
class Meta:
model = StudentFeedback
- fields = ['id', 'student', 'teacher', 'rating', 'feedback_text', 'created_at', 'updated_at']
- read_only_fields = ['created_at', 'updated_at']
+ fields = [
+ "id",
+ "student",
+ "teacher",
+ "rating",
+ "feedback_text",
+ "created_at",
+ "updated_at",
+ ]
+ read_only_fields = ["created_at", "updated_at"]
+
class InstituteFeedbackSerializer(serializers.ModelSerializer):
user = StudentSerializer(read_only=True)
@@ -145,10 +276,19 @@ class InstituteFeedbackSerializer(serializers.ModelSerializer):
class Meta:
model = InstituteFeedback
fields = [
- 'id', 'institute', 'user', 'feedback_type', 'rating', 'feedback_text',
- 'is_anonymous', 'is_public', 'created_at', 'updated_at'
+ "id",
+ "institute",
+ "user",
+ "feedback_type",
+ "rating",
+ "feedback_text",
+ "is_anonymous",
+ "is_public",
+ "created_at",
+ "updated_at",
]
- read_only_fields = ['created_at', 'updated_at']
+ read_only_fields = ["created_at", "updated_at"]
+
class StaffInstituteFeedbackSerializer(serializers.ModelSerializer):
staff = StaffSerializer(read_only=True)
@@ -158,33 +298,71 @@ class StaffInstituteFeedbackSerializer(serializers.ModelSerializer):
class Meta:
model = StaffInstituteFeedback
fields = [
- 'id', 'institute', 'staff', 'feedback_type', 'rating', 'feedback_text',
- 'is_anonymous', 'is_public', 'created_at', 'updated_at'
+ "id",
+ "institute",
+ "staff",
+ "feedback_type",
+ "rating",
+ "feedback_text",
+ "is_anonymous",
+ "is_public",
+ "created_at",
+ "updated_at",
]
- read_only_fields = ['created_at', 'updated_at']
+ read_only_fields = ["created_at", "updated_at"]
+
class CourseTrackingSerializer(serializers.ModelSerializer):
student = StudentSerializer(read_only=True)
course = CourseSerializer(read_only=True)
created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
+ current_period_display = serializers.CharField(read_only=True)
class Meta:
model = CourseTracking
fields = [
- 'id', 'student', 'course', 'enrollment_date', 'start_date', 'expected_end_date',
- 'actual_end_date', 'progress_status', 'completion_percentage', 'current_semester',
- 'semester_start_date', 'semester_end_date', 'notes', 'created_at', 'updated_at'
+ "id",
+ "student",
+ "course",
+ "enrollment_date",
+ "start_date",
+ "expected_end_date",
+ "actual_end_date",
+ "progress_status",
+ "completion_percentage",
+ "current_period",
+ "period_start_date",
+ "period_end_date",
+ "notes",
+ "created_at",
+ "updated_at",
+ "current_period_display",
]
- read_only_fields = ['created_at', 'updated_at', 'enrollment_date']
+ read_only_fields = [
+ "created_at",
+ "updated_at",
+ "enrollment_date",
+ "current_period_display",
+ ]
+
class TOTPSecretSerializer(serializers.ModelSerializer):
class Meta:
model = TOTPSecret
- fields = ['id', 'identifier', 'created_at', 'expires_at']
+ fields = ["id", "identifier", "created_at", "expires_at"]
+
class ResetTokenSerializer(serializers.ModelSerializer):
class Meta:
model = ResetToken
- fields = ['id', 'token', 'identifier', 'created_at', 'expires_at']
+ fields = ["id", "token", "identifier", "created_at", "expires_at"]
+
+class ParentSerializer(serializers.ModelSerializer):
+ students = StudentSerializer(many=True, read_only=True)
+
+ class Meta:
+ model = Parent
+ fields = ["id", "name", "phone", "email", "address", "students"]
+ read_only_fields = ["id"]
diff --git a/app/signals.py b/app/signals.py
index a1a93be..dc59e49 100644
--- a/app/signals.py
+++ b/app/signals.py
@@ -1,9 +1,33 @@
-from django.db.models.signals import post_save
-from django.dispatch import receiver
-from django.core.cache import cache
+# Standard library imports
+import random
from datetime import date, timedelta
-from .models import CourseTracking, Student
+
+# Core Django imports
+from django.contrib.auth.hashers import make_password
+from django.contrib.auth.models import Group, Permission
+from django.core.cache import cache
+from django.core.signals import request_started
from django.db import transaction
+from django.db.models import Q
+from django.db.models.signals import m2m_changed, post_migrate, post_save
+from django.dispatch import receiver
+
+# Local app imports
+from app.firebase import FCMDevice, send_push_notification
+from app.models import (
+ Batch,
+ CourseTracking,
+ Notice,
+ Parent,
+ Staff,
+ Student,
+ TeacherParentMeeting,
+)
+
+# --------------------------------------------------------------------
+# Course Tracking Signals
+# --------------------------------------------------------------------
+
@receiver(post_save, sender=CourseTracking)
def update_completion_percentage(sender, instance, **kwargs):
@@ -15,16 +39,53 @@ def update_completion_percentage(sender, instance, **kwargs):
cache.set(cache_key, instance.completion_percentage, 3600) # Cache for 1 hour
+@receiver(request_started)
+def periodic_update_course_completion(sender, **kwargs):
+ """
+ Signal that updates course completion percentages randomly on request start.
+ This ensures periodic updates without overwhelming the system.
+ Only runs with 1% probability per request to avoid performance impact.
+ """
+ # Run this signal with 1% probability to avoid running on every request
+ if random.random() <= 0.01: # 1% chance to run on any request
+ # Get all active course trackings (not completed or dropped)
+ active_trackings = CourseTracking.objects.filter(
+ Q(progress_status="In Progress") | Q(progress_status="Not Started")
+ )
+
+ updated_count = 0
+ for tracking in active_trackings:
+ old_percentage = tracking.completion_percentage
+ new_percentage = tracking.update_completion_percentage()
+
+ # If percentage changed, it was updated
+ if old_percentage != new_percentage:
+ updated_count += 1
+
+ # Check if course is now complete (100%)
+ if new_percentage >= 100 and tracking.progress_status != "Completed":
+ tracking.progress_status = "Completed"
+ tracking.actual_end_date = date.today()
+ tracking.save()
+
+ # Cache the last update timestamp
+ if updated_count > 0:
+ cache.set(
+ "last_course_percentage_update", date.today().isoformat(), 86400
+ ) # 24 hours
+
+
+# --------------------------------------------------------------------
+# Student and Course Signals
+# --------------------------------------------------------------------
+
+
@receiver(post_save, sender=Student)
def create_course_tracking(sender, instance, created, **kwargs):
"""
Signal handler to create CourseTracking when a new student is created
or when a course is assigned to an existing student
"""
- print(f"Signal triggered for student: {instance.name}")
- print(f"Created: {created}")
- print(f"Course: {instance.course}")
- print(f"Current Semester: {instance.current_semester}")
# Check if student has a course assigned
if instance.course:
@@ -62,22 +123,15 @@ def create_course_tracking(sender, instance, created, **kwargs):
course=instance.course,
start_date=start_date,
expected_end_date=expected_end_date,
- current_semester=instance.current_semester,
- semester_start_date=start_date,
- semester_end_date=start_date
+ current_period=instance.current_period,
+ period_start_date=start_date,
+ period_end_date=start_date
+ timedelta(days=180), # 6 months for first semester
)
# Force initial percentage calculation
course_tracking.force_update_percentage()
- print(
- f"Successfully created course tracking for student {instance.name}"
- )
- print(f"Start Date: {start_date}")
- print(f"Expected End Date: {expected_end_date}")
- print(f"Current Semester: {instance.current_semester}")
-
except Exception as e:
print(
f"Error creating course tracking for student {instance.name}: {str(e)}"
@@ -89,23 +143,280 @@ def create_course_tracking(sender, instance, created, **kwargs):
@receiver(post_save, sender=Student)
-def sync_student_semester(sender, instance, created, **kwargs):
- """Signal handler to sync student's current_semester with course tracking"""
+def sync_student_period(sender, instance, created, **kwargs):
+ """Signal handler to sync student's current_period with course tracking"""
if instance.course:
try:
course_tracking = CourseTracking.objects.get(
student=instance, course=instance.course
)
- if course_tracking.current_semester != instance.current_semester:
- course_tracking.current_semester = instance.current_semester
+ if course_tracking.current_period != instance.current_period:
+ course_tracking.current_period = instance.current_period
course_tracking.save()
except CourseTracking.DoesNotExist:
pass
@receiver(post_save, sender=CourseTracking)
-def sync_course_tracking_semester(sender, instance, created, **kwargs):
- """Signal handler to sync course tracking's current_semester with student"""
- if instance.student.current_semester != instance.current_semester:
- instance.student.current_semester = instance.current_semester
- instance.student.save()
\ No newline at end of file
+def sync_course_tracking_period(sender, instance, created, **kwargs):
+ """Signal handler to sync course tracking's current_period with student"""
+ if instance.student.current_period != instance.current_period:
+ instance.student.current_period = instance.current_period
+ instance.student.save()
+
+
+# --------------------------------------------------------------------
+# Parent and Student Relationship Signals
+# --------------------------------------------------------------------
+
+
+@receiver(post_save, sender=Student)
+def create_parent_for_student(sender, instance, created, **kwargs):
+ """
+ Signal to create or update parent when a student is created or updated.
+ Also ensures parent is assigned to the Parent group.
+ """
+ if instance.parent_name and instance.parent_phone:
+ try:
+ # Try to find existing parent
+ parent = Parent.objects.filter(phone=instance.parent_phone).first()
+
+ if not parent:
+ # Create new parent if doesn't exist
+ parent = Parent.objects.create(
+ name=instance.parent_name,
+ phone=instance.parent_phone,
+ password=make_password("123"), # Default password
+ )
+
+ # Get or create Parent group
+ parent_group, _ = Group.objects.get_or_create(name="Parent")
+
+ # Add parent to group
+ parent.groups.add(parent_group)
+
+ # Add necessary permissions
+ permissions = Permission.objects.filter(
+ codename__in=["view_student", "view_attendance", "view_notice"]
+ )
+ parent_group.permissions.add(*permissions)
+
+ # Add student to parent's students
+ parent.students.add(instance)
+
+ except Exception as e:
+ print(f"Error creating/updating parent: {str(e)}")
+ # Don't prevent student creation/update if parent creation fails
+
+
+@receiver(post_save, sender=Parent)
+def setup_parent_user(sender, instance, created, **kwargs):
+ """
+ Signal handler to set up a new parent user:
+ 1. Add to Parent group
+ 2. Set up initial settings and permissions
+ """
+ if created:
+ try:
+ # Add user to Parent group
+ parent_group, _ = Group.objects.get_or_create(name="Parent")
+ instance.groups.add(parent_group)
+
+ print(f"Successfully set up new parent user: {instance.name}")
+
+ except Exception as e:
+ print(f"Error setting up parent user {instance.name}: {str(e)}")
+
+
+@receiver(m2m_changed, sender=Parent.students.through)
+def handle_parent_student_relationship(
+ sender, instance, action, reverse, model, pk_set, **kwargs
+):
+ """
+ Signal handler to manage parent-student relationship changes:
+ - Update caches
+ - Handle notifications
+ - Sync permissions
+ """
+ if action == "post_add":
+ try:
+ if reverse:
+ # This is triggered when students are added to a parent
+ parent = model.objects.get(pk=pk_set.pop())
+ student = instance
+ else:
+ # This is triggered when a parent adds students
+ parent = instance
+ student_ids = pk_set
+
+ # Clear parent dashboard cache using a more compatible approach
+ cache.delete(f"parent_dashboard_{parent.id}")
+
+ print(
+ f"Successfully updated parent-student relationship for parent: {parent.name}"
+ )
+
+ except Exception as e:
+ print(f"Error handling parent-student relationship: {str(e)}")
+
+
+# --------------------------------------------------------------------
+# User Group and Permission Signals
+# --------------------------------------------------------------------
+
+
+@receiver(post_migrate)
+def create_user_groups(sender, **kwargs):
+ """
+ Create initial user groups and permissions after migrations.
+ This ensures groups exist before any user is created.
+ """
+ from django.contrib.auth.models import Group, Permission
+
+ # Create the necessary groups
+ groups_data = {
+ "Student": ["view_attendance", "view_notice", "view_subject"],
+ "Teacher": [
+ "view_student",
+ "view_attendance",
+ "add_attendance",
+ "change_attendance",
+ "view_notice",
+ "add_notice",
+ "view_subject",
+ "change_subject",
+ ],
+ "Parent": ["view_student", "view_attendance", "view_notice"],
+ "Admission Officer": [
+ "view_student",
+ "add_student",
+ "change_student",
+ "view_parent",
+ "add_parent",
+ "change_parent",
+ "view_course",
+ "view_batch",
+ "view_notice",
+ ],
+ }
+
+ for group_name, permissions in groups_data.items():
+ group, created = Group.objects.get_or_create(name=group_name)
+
+ if created:
+ print(f"Created user group: {group_name}")
+
+ # Add permissions to group
+ perms = Permission.objects.filter(codename__in=permissions)
+ group.permissions.add(*perms)
+ print(f"Added {perms.count()} permissions to {group_name} group")
+
+
+# --------------------------------------------------------------------
+# Batch and Student Enrollment Signals
+# --------------------------------------------------------------------
+
+
+@receiver(post_save, sender=Batch)
+def update_student_joining_dates(sender, instance, **kwargs):
+ """Update joining dates for all students in a batch when batch year changes"""
+ if instance.year:
+ # Update all students in this batch who don't have a joining date
+ for student in instance.students.filter(joining_date__isnull=True):
+ student.joining_date = instance.year
+ student.save(update_fields=["joining_date"])
+
+
+@receiver(m2m_changed, sender=Student.batches.through)
+def handle_student_batch_change(
+ sender, instance, action, reverse, model, pk_set, **kwargs
+):
+ """Handle joining date when a student is added to a batch"""
+ if action == "post_add" and not reverse:
+ # This is fired when batches are added to a student
+ for batch_id in pk_set:
+ try:
+ batch = Batch.objects.get(pk=batch_id)
+ if batch.year and not instance.joining_date:
+ instance.joining_date = batch.year
+ instance.save(update_fields=["joining_date"])
+ break # Use the first batch with a joining date
+ except Batch.DoesNotExist:
+ pass
+
+
+# --------------------------------------------------------------------
+# Notification Signals
+# --------------------------------------------------------------------
+
+
+@receiver(post_save, sender=Notice)
+def send_notice_notification(sender, instance, created, **kwargs):
+ """Send notifications when a notice is created or updated"""
+ # Get all tokens from active devices (all user types)
+ tokens = FCMDevice.objects.filter(is_active=True).values_list("token", flat=True)
+
+ send_push_notification(instance.title, instance.message, tokens)
+
+
+@receiver(post_save, sender=TeacherParentMeeting)
+def send_meeting_notification(sender, instance, created, **kwargs):
+ """Send notifications when a meeting is created or updated"""
+ tokens = []
+
+ # Method 1: Get tokens from FCMDevice records based on user_type
+ fcm_device_tokens = FCMDevice.objects.filter(
+ is_active=True, user_type__in=["parent", "teacher"]
+ ).values_list("token", flat=True)
+ tokens.extend(fcm_device_tokens)
+
+ # Method 2: Get tokens directly from user models
+ # Get tokens from Staff model
+ staff_tokens = (
+ Staff.objects.filter(fcm_token__isnull=False)
+ .exclude(fcm_token="")
+ .values_list("fcm_token", flat=True)
+ )
+ tokens.extend(staff_tokens)
+
+ # Get tokens from Parent model
+ parent_tokens = (
+ Parent.objects.filter(fcm_token__isnull=False)
+ .exclude(fcm_token="")
+ .values_list("fcm_token", flat=True)
+ )
+ tokens.extend(parent_tokens)
+
+ # Remove duplicates
+ unique_tokens = list(set(tokens))
+
+ if not unique_tokens:
+ return
+
+ if created:
+ title = "New Parent-Teacher Meeting Scheduled"
+ message = f"A parent-teacher meeting has been scheduled for {instance.meeting_date} at {instance.meeting_time}."
+ if instance.is_online:
+ message += " This is an online meeting."
+ send_push_notification(title, message, unique_tokens)
+ else:
+ # Access through instance.tracker to determine if status changed
+ # This requires django-model-utils or similar to track changes
+ if hasattr(instance, "tracker") and "status" in instance.tracker.changed():
+ old_status = instance.tracker.previous("status")
+ if instance.status == "cancelled":
+ title = "Parent-Teacher Meeting Cancelled"
+ message = f"The meeting for {instance.meeting_date} at {instance.meeting_time} has been cancelled."
+ if instance.cancellation_reason:
+ message += f" Reason: {instance.cancellation_reason}"
+ send_push_notification(title, message, unique_tokens)
+ elif instance.status == "rescheduled":
+ # Meeting rescheduled notification
+ title = "Parent-Teacher Meeting Rescheduled"
+ message = f"The parent-teacher meeting has been rescheduled to {instance.meeting_date} at {instance.meeting_time}."
+ send_push_notification(title, message, unique_tokens)
+ elif instance.status == "completed":
+ # Meeting completed notification
+ title = "Parent-Teacher Meeting Completed"
+ message = f"The parent-teacher meeting scheduled for {instance.meeting_date} at {instance.meeting_time} has been marked as completed."
+ send_push_notification(title, message, unique_tokens)
diff --git a/app/staffviews.py b/app/staffviews.py
index 3032c5d..4d2a462 100644
--- a/app/staffviews.py
+++ b/app/staffviews.py
@@ -1,5 +1,5 @@
from django.shortcuts import render, redirect, get_object_or_404
-from django.http import JsonResponse, HttpResponse
+from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.db.models import Count, Q, Avg
@@ -7,14 +7,14 @@ from django.utils import timezone
from datetime import timedelta, datetime
from django.views.decorators.http import require_http_methods, require_GET
from django.contrib.auth.hashers import check_password, make_password
-from .models import (
+from app.models import (
Staff,
Student,
Routine,
Attendance,
AttendanceRecord,
StudentFeedback,
- Staff_leave,
+ StaffLeave,
Notice,
Institute,
StaffInstituteFeedback,
@@ -22,19 +22,35 @@ from .models import (
SubjectFile,
InstituteFeedback,
CourseTracking,
- FEEDBACK_TYPE_CHOICES
+ FEEDBACK_TYPE_CHOICES,
)
-from app.admin import custom_admin_site
+import logging
@login_required
def teacherDashboard(request):
"""View to display the teacher dashboard with their classes and student information"""
- teacher = Staff.objects.filter(phone=request.user.phone).first()
- if not teacher:
- messages.error(request, "Teacher profile not found. Please contact administrator.")
- return redirect('dashboard')
-
+
+ # Check if user is in Teacher group
+ if not request.user.groups.filter(name="Teacher").exists():
+ print(request, "You don't have permission to access the teacher dashboard.")
+ return redirect("dashboard")
+
+ # Get or create a teacher object
+ try:
+ # Try to get the teacher object directly if user is already a Staff instance
+ if isinstance(request.user, Staff):
+ teacher = request.user
+ else:
+ # Try to get the staff object by ID
+ teacher = Staff.objects.get(id=request.user.id)
+ except (Staff.DoesNotExist, Exception) as e:
+ print(f"Error getting staff profile: {str(e)}")
+ messages.error(
+ request, "Teacher profile not found. Please contact administrator."
+ )
+ return redirect("dashboard")
+
today = timezone.now().date()
current_time = timezone.now().time()
last_week = today - timedelta(days=7)
@@ -50,12 +66,21 @@ def teacherDashboard(request):
for routine in today_routines:
attendance = routine.attendances.filter(date=today).first()
- # Get total students from active course trackings
- total_students = CourseTracking.objects.filter(
+ # Get total active students for this routine - matching the get_students logic
+ students = Student.objects.filter(
course=routine.subject.course,
- current_semester=routine.semester_or_year,
- progress_status="In Progress"
- ).count()
+ current_period=routine.period_or_year,
+ status="Active", # Only count active students
+ )
+
+ # If no students found with exact period match, try more flexible approach
+ if not students.exists():
+ students = Student.objects.filter(
+ course=routine.subject.course,
+ status="Active", # Still only count active students
+ )
+
+ total_students = students.count()
attendance_count = (
0
@@ -76,7 +101,7 @@ def teacherDashboard(request):
"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,
+ "period_or_year": routine.period_or_year,
"course_name": routine.subject.course.name,
"has_attendance": attendance is not None,
"subject_id": routine.subject.id,
@@ -89,67 +114,86 @@ def teacherDashboard(request):
remaining_classes = total_classes - completed_classes
# Get student statistics from course tracking
- total_students = CourseTracking.objects.filter(
- course__in=today_routines.values_list('subject__course', flat=True),
- progress_status="In Progress"
- ).values('student').distinct().count()
-
+ total_students = (
+ CourseTracking.objects.filter(
+ course__in=today_routines.values_list("subject__course", flat=True),
+ progress_status="In Progress",
+ )
+ .values("student")
+ .distinct()
+ .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()
+ 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 = CourseTracking.objects.filter(
course=routine.subject.course,
- current_semester=routine.semester_or_year,
- progress_status="In Progress"
- ).values_list('student', flat=True)
+ current_period=routine.period_or_year,
+ progress_status="In Progress",
+ ).values_list("student", flat=True)
today_class_students.update(students)
-
+
# Make sure absent students doesn't go negative
total_today_students = len(today_class_students)
- absent_students = max(0, total_today_students - present_students) if total_today_students else 0
+ absent_students = (
+ max(0, total_today_students - present_students) if total_today_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 = (
+ 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()
+ pending_leaves = StaffLeave.objects.filter(staff=teacher, status=0).count()
+ approved_leaves = StaffLeave.objects.filter(staff=teacher, status=1).count()
+ rejected_leaves = StaffLeave.objects.filter(staff=teacher, status=2).count()
# Get recent leaves
- recent_leaves = Staff_leave.objects.filter(staff=teacher).order_by('-created_at')[:5]
+ recent_leaves = StaffLeave.objects.filter(staff=teacher).order_by("-created_at")[:5]
# Get recent feedback
- recent_feedback = StudentFeedback.objects.filter(teacher=teacher).order_by('-created_at')[:5]
+ recent_feedback = StudentFeedback.objects.filter(teacher=teacher).order_by(
+ "-created_at"
+ )[:5]
# Get notices
- notices = Notice.objects.all().order_by('-created_at')[:5]
+ notices = Notice.objects.all().order_by("-created_at")[:5]
# Get feedback types for institute feedback
feedback_types = FEEDBACK_TYPE_CHOICES
# Get institute feedback
- institute_feedback = StaffInstituteFeedback.objects.filter(staff=teacher).order_by('-created_at')[:5]
+ 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=CourseTracking.objects.filter(
- progress_status="In Progress"
- ).values_list('current_semester', flat=True).distinct()
- ).order_by('start_time')
+ period_or_year__in=CourseTracking.objects.filter(progress_status="In Progress")
+ .values_list("current_period", flat=True)
+ .distinct(),
+ ).order_by("start_time")
# Get all attendance records for quick access
recent_attendance = (
@@ -168,29 +212,31 @@ def teacherDashboard(request):
).count()
# Attendance Management Data
- routines = Routine.objects.filter(
- teacher=teacher,
- is_active=True
- ).order_by('start_time')
+ routines = Routine.objects.filter(teacher=teacher, is_active=True).order_by(
+ "start_time"
+ )
# Get all subjects taught by this teacher for the subjects section
- subject_ids = Routine.objects.filter(
- teacher=teacher,
- is_active=True
- ).values_list('subject', flat=True).distinct()
-
- teacher_subjects = Routine.objects.filter(
- teacher=teacher,
- is_active=True,
- subject_id__in=subject_ids
- ).select_related('subject', 'course').order_by('subject__name')
-
+ subject_ids = (
+ Routine.objects.filter(teacher=teacher, is_active=True)
+ .values_list("subject", flat=True)
+ .distinct()
+ )
+
+ teacher_subjects = (
+ Routine.objects.filter(
+ teacher=teacher, is_active=True, subject_id__in=subject_ids
+ )
+ .select_related("subject", "course")
+ .order_by("subject__name")
+ )
+
# Add student count to each routine
for routine in teacher_subjects:
routine.students_count = CourseTracking.objects.filter(
course=routine.course,
- current_semester=routine.semester_or_year,
- progress_status="In Progress"
+ current_period=routine.period_or_year,
+ progress_status="In Progress",
).count()
context = {
@@ -221,7 +267,7 @@ def teacherDashboard(request):
"teacher_subjects": teacher_subjects,
}
- return render(request, "teacher_dashboard.html", context)
+ return render(request, "teacher/dashboard.html", context)
@login_required
@@ -250,10 +296,10 @@ def request_staff_leave(request):
return redirect("teacherDashboard")
# Check if leave already exists for these dates
- existing_leave = Staff_leave.objects.filter(
+ existing_leave = StaffLeave.objects.filter(
staff=staff,
start_date__lte=end_date.date(),
- end_date__gte=start_date.date()
+ end_date__gte=start_date.date(),
).first()
if existing_leave:
@@ -263,7 +309,7 @@ def request_staff_leave(request):
return redirect("teacherDashboard")
# Create leave request
- leave = Staff_leave.objects.create(
+ leave = StaffLeave.objects.create(
staff=staff,
start_date=start_date.date(),
end_date=end_date.date(),
@@ -314,7 +360,7 @@ def submit_staff_institute_feedback(request):
# Get the staff object - FIX: Use filter().first() instead of get() to avoid MultipleObjectsReturned error
teacher = Staff.objects.filter(phone=staff.phone).first()
-
+
if not teacher:
messages.error(request, "Staff account not found")
return redirect("teacherDashboard")
@@ -358,7 +404,7 @@ def save_attendance(request):
if request.method == "POST":
teacher = Staff.objects.filter(phone=request.user.phone).first()
if not teacher:
- return redirect('login')
+ return redirect("login")
routine_id = request.POST.get("routine_id")
date_str = request.POST.get("date")
@@ -458,7 +504,7 @@ def get_subject_schedule(request):
try:
routines = Routine.objects.filter(
teacher=teacher, subject_id=subject_id, is_active=True
- ).values("id", "start_time", "end_time", "semester_or_year")
+ ).values("id", "start_time", "end_time", "period_or_year")
return JsonResponse({"success": True, "schedule": list(routines)})
except Exception as e:
@@ -471,13 +517,13 @@ def teacher_update_profile_picture(request):
if request.method == "POST":
teacher = Staff.objects.filter(phone=request.user.phone).first()
if not teacher:
- return redirect('login')
- profile_picture = request.FILES.get("profile_picture")
+ return redirect("login")
+ image = request.FILES.get("image")
- if profile_picture:
+ if image:
try:
# Update the teacher's profile picture
- teacher.image = profile_picture
+ teacher.image = image
teacher.save()
messages.success(request, "Profile picture updated successfully.")
except Exception as e:
@@ -487,8 +533,6 @@ def teacher_update_profile_picture(request):
return redirect("teacherDashboard")
- return redirect("teacherDashboard")
-
@login_required
def teacher_change_password(request):
@@ -543,264 +587,392 @@ def teacher_change_password(request):
@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')
-
+ 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'
- })
-
+ return JsonResponse({"error": "Missing required parameters"})
+
try:
teacher = Staff.objects.filter(phone=request.user.phone).first()
if not teacher:
- return JsonResponse({
- 'error': 'Teacher account not found'
- }, status=404)
+ return JsonResponse({"error": "Teacher account not found"}, status=404)
routine = Routine.objects.get(id=routine_id, teacher=teacher)
- date = datetime.strptime(date_str, '%Y-%m-%d').date()
-
+ date = datetime.strptime(date_str, "%Y-%m-%d").date()
+
# Get existing attendance data if any
- attendance_data = {
- 'teacher_attend': False,
- 'class_status': True,
- 'records': {}
- }
-
+ 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
+ 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')
-
+ 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
- }
+ "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)
+ return JsonResponse({"error": "Invalid routine selected"}, status=404)
except ValueError:
- return JsonResponse({
- 'error': 'Invalid date format'
- }, status=400)
+ return JsonResponse({"error": "Invalid date format"}, status=400)
except Exception as e:
- return JsonResponse({
- 'error': str(e)
- }, status=500)
+ 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')
-
+
+ routine_id = request.GET.get("routine_id")
+
if not routine_id:
- return JsonResponse({'error': 'Missing routine_id parameter'}, status=400)
-
+ return JsonResponse({"error": "Missing routine_id parameter"}, status=400)
+
try:
teacher = Staff.objects.filter(phone=request.user.phone).first()
if not teacher:
- return JsonResponse({'error': 'Teacher account not found'}, status=404)
+ return JsonResponse({"error": "Teacher account not found"}, status=404)
routine = Routine.objects.get(id=routine_id, teacher=teacher)
-
- # Get students for this routine
+
+ # Get only active 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
+ current_period=routine.period_or_year,
+ status="Active", # Only get active students
+ ).order_by("name")
+
+ # If no students found, try a more flexible approach but still only active students
if not students.exists():
students = Student.objects.filter(
- course=routine.subject.course
- ).order_by('name')
-
+ course=routine.subject.course,
+ status="Active", # Maintain active student filter
+ ).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
- })
-
+ 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)
+ return JsonResponse({"error": "Invalid routine selected"}, status=404)
except Exception as e:
- return JsonResponse({'error': str(e)}, status=500)
+ return JsonResponse({"error": str(e)}, status=500)
@login_required
@require_http_methods(["GET", "POST"])
def manage_subject_files(request):
"""View to manage files for subjects taught by the teacher"""
- teacher = Staff.objects.filter(phone=request.user.phone).first()
- if not teacher:
- return JsonResponse({
- "success": False,
- "message": "Staff account not found"
- }, status=404)
-
+ logger = logging.getLogger(__name__)
+
+ # First check if the user is authenticated and in the Teacher group
+ if not request.user.is_authenticated:
+ logger.error("User not authenticated")
+ return redirect("login")
+
+ # Try to get the teacher object
+ teacher = None
+ try:
+ if hasattr(request.user, "phone"):
+ # If the user model has phone attribute (Staff model)
+ teacher = Staff.objects.get(phone=request.user.phone)
+ else:
+ # If request.user is a different model, try to get Staff by ID
+ teacher = Staff.objects.get(id=request.user.id)
+ except Staff.DoesNotExist:
+ logger.error(f"Teacher not found for user: {request.user}")
+ messages.error(
+ request, "Staff account not found. Please contact administrator."
+ )
+ return redirect("dashboard")
+ except Exception as e:
+ logger.error(f"Error getting teacher: {e}", exc_info=True)
+ messages.error(request, f"Error retrieving staff profile: {str(e)}")
+ return redirect("dashboard")
+
+ logger.info(f"Teacher found: {teacher.name}")
+
# Get all subjects taught by this teacher
subject_filter = request.GET.get("subject")
-
- subjects_query = Routine.objects.filter(
- teacher=teacher,
- is_active=True
- ).values_list('subject', flat=True).distinct()
-
+
+ subjects_query = (
+ Routine.objects.filter(teacher=teacher, is_active=True)
+ .values_list("subject", flat=True)
+ .distinct()
+ )
+
+ logger.info(f"Found {len(subjects_query)} subject IDs: {list(subjects_query)}")
+
# Apply subject filter if provided
try:
if subject_filter:
subjects = Subject.objects.filter(
- id=subject_filter,
- id__in=subjects_query
- ).order_by('course__name', 'semester_or_year', 'name')
-
+ id=subject_filter, id__in=subjects_query
+ ).order_by("course__name", "period_or_year", "name")
+
+ logger.info(
+ f"Filtered by subject ID {subject_filter}, found {subjects.count()} subjects"
+ )
+
# If no subject found with the provided ID, return a helpful error
if not subjects.exists():
- if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
- return JsonResponse({
- "success": False,
- "message": f"Subject with ID {subject_filter} not found or you don't have permission to access it"
- }, status=404)
+ logger.warning(f"No subjects found with ID {subject_filter}")
+ if request.headers.get("X-Requested-With") == "XMLHttpRequest":
+ return JsonResponse(
+ {
+ "success": False,
+ "message": f"Subject with ID {subject_filter} not found or you don't have permission to access it",
+ },
+ status=404,
+ )
else:
- return render(request, 'subject_files.html', {
- "subjects": [],
- "teacher": teacher,
- "selected_subject": None,
- "error_message": f"Subject with ID {subject_filter} not found or you don't have permission to access it"
- })
+ return render(
+ request,
+ "teacher/subject_files.html",
+ {
+ "subjects": [],
+ "teacher": teacher,
+ "selected_subject": None,
+ "error_message": f"Subject with ID {subject_filter} not found or you don't have permission to access it",
+ },
+ )
else:
- subjects = Subject.objects.filter(
- id__in=subjects_query
- ).order_by('course__name', 'semester_or_year', 'name')
-
+ subjects = Subject.objects.filter(id__in=subjects_query).order_by(
+ "course__name", "period_or_year", "name"
+ )
+
+ logger.info(f"No filter applied, found {subjects.count()} subjects")
+
# If no subjects found at all, return a message
if not subjects.exists():
- if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
- return JsonResponse({
- "success": False,
- "message": "You are not assigned to any subjects yet"
- })
+ logger.warning("No subjects found for this teacher")
+ if request.headers.get("X-Requested-With") == "XMLHttpRequest":
+ return JsonResponse(
+ {
+ "success": False,
+ "message": "You are not assigned to any subjects yet",
+ }
+ )
else:
- return render(request, 'subject_files.html', {
- "subjects": [],
- "teacher": teacher,
- "selected_subject": None,
- "error_message": "You are not assigned to any subjects yet"
- })
+ return render(
+ request,
+ "teacher/subject_files.html",
+ {
+ "subjects": [],
+ "teacher": teacher,
+ "selected_subject": None,
+ "error_message": "You are not assigned to any subjects yet",
+ },
+ )
except Exception as e:
+ logger.error(f"Error loading subjects: {e}", exc_info=True)
# Handle any unexpected errors
- if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
- return JsonResponse({
- "success": False,
- "message": f"Error loading subjects: {str(e)}"
- }, status=500)
+ if request.headers.get("X-Requested-With") == "XMLHttpRequest":
+ return JsonResponse(
+ {"success": False, "message": f"Error loading subjects: {str(e)}"},
+ status=500,
+ )
else:
- return render(request, 'subject_files.html', {
- "subjects": [],
- "teacher": teacher,
- "selected_subject": None,
- "error_message": f"Error loading subjects: {str(e)}"
- })
-
+ return render(
+ request,
+ "teacher/subject_files.html",
+ {
+ "subjects": [],
+ "teacher": teacher,
+ "selected_subject": None,
+ "error_message": f"Error loading subjects: {str(e)}",
+ },
+ )
+
if request.method == "POST":
subject_id = request.POST.get("subject_id")
title = request.POST.get("title")
description = request.POST.get("description")
file = request.FILES.get("file")
-
+
+ logger.info(
+ f"Processing file upload - subject_id: {subject_id}, title: {title}"
+ )
+
if not subject_id or not title or not file:
- return JsonResponse({
- "success": False,
- "message": "Subject, title and file are required"
- })
-
+ logger.warning("Missing required fields for file upload")
+ return JsonResponse(
+ {"success": False, "message": "Subject, title and file are required"}
+ )
+
try:
subject = Subject.objects.get(id=subject_id)
-
+
# Check if subject is taught by this teacher
- if not Routine.objects.filter(teacher=teacher, subject=subject, is_active=True).exists():
- return JsonResponse({
- "success": False,
- "message": "You don't have permission to upload files to this subject"
- })
-
+ if not Routine.objects.filter(
+ teacher=teacher, subject=subject, is_active=True
+ ).exists():
+ logger.warning(
+ f"Teacher {teacher.id} does not have permission for subject {subject_id}"
+ )
+ return JsonResponse(
+ {
+ "success": False,
+ "message": "You don't have permission to upload files to this subject",
+ }
+ )
+
# Create new subject file
subject_file = SubjectFile.objects.create(
subject=subject,
title=title,
description=description,
file=file,
- uploaded_by=teacher
+ uploaded_by=teacher,
)
-
- return JsonResponse({
- "success": True,
- "message": "File uploaded successfully",
- "file": {
- "id": subject_file.id,
- "title": subject_file.title,
- "description": subject_file.description,
- "file_url": subject_file.file.url,
- "uploaded_at": subject_file.uploaded_at.strftime("%b %d, %Y %H:%M")
+
+ logger.info(f"File uploaded successfully: {subject_file.id}")
+
+ return JsonResponse(
+ {
+ "success": True,
+ "message": "File uploaded successfully",
+ "file": {
+ "id": subject_file.id,
+ "title": subject_file.title,
+ "description": subject_file.description,
+ "file_url": subject_file.file.url,
+ "uploaded_at": subject_file.uploaded_at.strftime(
+ "%b %d, %Y %H:%M"
+ ),
+ },
}
- })
+ )
except Subject.DoesNotExist:
- return JsonResponse({
- "success": False,
- "message": "Subject not found"
- })
+ logger.error(f"Subject {subject_id} not found")
+ return JsonResponse({"success": False, "message": "Subject not found"})
except Exception as e:
- return JsonResponse({
- "success": False,
- "message": f"Error uploading file: {str(e)}"
- })
-
+ logger.error(f"Error uploading file: {e}", exc_info=True)
+ return JsonResponse(
+ {"success": False, "message": f"Error uploading file: {str(e)}"}
+ )
+
# For GET requests, return a list of subjects and their files
subjects_data = []
for subject in subjects:
try:
- subject_files = subject.get_all_files()
- subjects_data.append({
- "id": subject.id,
- "name": subject.name,
- "course": subject.course.name,
- "semester_or_year": subject.semester_or_year,
- "files": subject_files,
- "files_count": len(subject_files)
- })
+ logger.info(f"Processing subject: {subject.id} - {subject.name}")
+
+ # Manually get the files instead of relying on get_all_files method
+ files = []
+
+ # Add syllabus if it exists
+ if subject.syllabus_pdf:
+ try:
+ files.append(
+ {
+ "id": "syllabus",
+ "title": f"{subject.name} Syllabus",
+ "description": f"Syllabus PDF for {subject.name}",
+ "file_url": subject.syllabus_pdf.url,
+ "file_type": "syllabus",
+ "uploaded_by": None,
+ "uploaded_at": None,
+ }
+ )
+ except Exception as syllabus_error:
+ logger.error(
+ f"Error adding syllabus for subject {subject.id}: {syllabus_error}",
+ exc_info=True,
+ )
+
+ # Add subject files
+ try:
+ subject_files = SubjectFile.objects.filter(subject=subject).order_by(
+ "-uploaded_at"
+ )
+ logger.info(
+ f"Found {subject_files.count()} additional files for subject {subject.id}"
+ )
+
+ for file in subject_files:
+ try:
+ files.append(
+ {
+ "id": file.id,
+ "title": file.title,
+ "description": file.description,
+ "file_url": file.file.url,
+ "file_type": "notes",
+ "uploaded_by": file.uploaded_by.name
+ if file.uploaded_by
+ else None,
+ "uploaded_at": file.uploaded_at,
+ }
+ )
+ except Exception as file_error:
+ logger.error(
+ f"Error adding file {file.id} for subject {subject.id}: {file_error}",
+ exc_info=True,
+ )
+ except Exception as files_error:
+ logger.error(
+ f"Error getting subject files for subject {subject.id}: {files_error}",
+ exc_info=True,
+ )
+
+ logger.info(
+ f"Successfully processed {len(files)} files for subject {subject.id}"
+ )
+
+ subjects_data.append(
+ {
+ "id": subject.id,
+ "name": subject.name,
+ "course": subject.course.name,
+ "semester_or_year": subject.period_or_year,
+ "files": files,
+ "files_count": len(files),
+ }
+ )
except Exception as e:
# If there's an error with a specific subject, log it but continue
- print(f"Error loading files for subject {subject.id}: {str(e)}")
-
- return render(request, 'subject_files.html', {
- "subjects": subjects_data,
- "teacher": teacher,
- "selected_subject": subject_filter
- })
+ logger.error(f"Error processing subject {subject.id}: {e}", exc_info=True)
+
+ logger.info(f"Rendering template with {len(subjects_data)} subjects")
+
+ # Print to console for debugging
+ print(f"DEBUG: Teacher = {teacher.name}, Subjects count = {len(subjects_data)}")
+ if subjects_data:
+ print(f"DEBUG: First subject = {subjects_data[0]['name']}")
+ else:
+ print("DEBUG: No subjects data to render")
+
+ return render(
+ request,
+ "teacher/subject_files.html",
+ {
+ "subjects": subjects_data,
+ "teacher": teacher,
+ "selected_subject": subject_filter,
+ },
+ )
@login_required
@@ -809,51 +981,43 @@ def delete_subject_file(request):
"""View to delete a subject file"""
teacher = Staff.objects.filter(phone=request.user.phone).first()
if not teacher:
- return JsonResponse({
- "success": False,
- "message": "Staff account not found"
- }, status=404)
-
+ return JsonResponse(
+ {"success": False, "message": "Staff account not found"}, status=404
+ )
+
file_id = request.POST.get("file_id")
-
+
if not file_id:
- return JsonResponse({
- "success": False,
- "message": "File ID is required"
- })
-
+ return JsonResponse({"success": False, "message": "File ID is required"})
+
try:
# Check if the file exists and is owned by this teacher or from their subject
subject_file = SubjectFile.objects.get(id=file_id)
-
+
# Check if the file is from a subject taught by this teacher
- if not Routine.objects.filter(
- teacher=teacher,
- subject=subject_file.subject,
- is_active=True
- ).exists() and subject_file.uploaded_by != teacher:
- return JsonResponse({
- "success": False,
- "message": "You don't have permission to delete this file"
- })
-
+ if (
+ not Routine.objects.filter(
+ teacher=teacher, subject=subject_file.subject, is_active=True
+ ).exists()
+ and subject_file.uploaded_by != teacher
+ ):
+ return JsonResponse(
+ {
+ "success": False,
+ "message": "You don't have permission to delete this file",
+ }
+ )
+
# Delete the file
subject_file.delete()
-
- return JsonResponse({
- "success": True,
- "message": "File deleted successfully"
- })
+
+ return JsonResponse({"success": True, "message": "File deleted successfully"})
except SubjectFile.DoesNotExist:
- return JsonResponse({
- "success": False,
- "message": "File not found"
- })
+ return JsonResponse({"success": False, "message": "File not found"})
except Exception as e:
- return JsonResponse({
- "success": False,
- "message": f"Error deleting file: {str(e)}"
- })
+ return JsonResponse(
+ {"success": False, "message": f"Error deleting file: {str(e)}"}
+ )
@login_required
@@ -862,32 +1026,29 @@ def get_teacher_subjects(request):
"""View to get a list of subjects taught by the teacher"""
teacher = Staff.objects.filter(phone=request.user.phone).first()
if not teacher:
- return JsonResponse({
- "success": False,
- "message": "Staff account not found"
- }, status=404)
-
+ return JsonResponse(
+ {"success": False, "message": "Staff account not found"}, status=404
+ )
+
# Get all subjects taught by the teacher
subjects_query = Subject.objects.filter(
- routine__teacher=teacher,
- routine__is_active=True
+ routine__teacher=teacher, routine__is_active=True
).distinct()
-
+
# Prepare results with additional has_syllabus field
subjects_data = []
for subject in subjects_query:
- subjects_data.append({
- 'id': subject.id,
- 'name': subject.name,
- 'course__name': subject.course.name,
- 'semester_or_year': subject.semester_or_year,
- 'has_syllabus': bool(subject.syllabus_pdf)
- })
-
- return JsonResponse({
- "success": True,
- "subjects": subjects_data
- })
+ subjects_data.append(
+ {
+ "id": subject.id,
+ "name": subject.name,
+ "course__name": subject.course.name,
+ "period_or_year": subject.period_or_year,
+ "has_syllabus": bool(subject.syllabus_pdf),
+ }
+ )
+
+ return JsonResponse({"success": True, "subjects": subjects_data})
@login_required
@@ -895,59 +1056,59 @@ def get_teacher_subjects(request):
def view_subject_syllabus(request, subject_id):
"""View to display syllabus PDF for a subject"""
teacher = Staff.objects.filter(phone=request.user.phone).first()
-
+
if not teacher:
- return JsonResponse({
- "success": False,
- "message": "Staff account not found"
- }, status=404)
-
+ return JsonResponse(
+ {"success": False, "message": "Staff account not found"}, status=404
+ )
+
try:
# Check if the subject_id is valid
if not subject_id or not str(subject_id).isdigit():
- return JsonResponse({
- "success": False,
- "message": "Invalid subject ID provided"
- }, status=400)
-
+ return JsonResponse(
+ {"success": False, "message": "Invalid subject ID provided"}, status=400
+ )
+
subject_id = int(subject_id)
-
+
# First check if the teacher teaches this subject
if not Routine.objects.filter(
- teacher=teacher,
- subject_id=subject_id,
- is_active=True
+ teacher=teacher, subject_id=subject_id, is_active=True
).exists():
- return JsonResponse({
- "success": False,
- "message": "You don't have permission to view this subject's syllabus"
- }, status=403)
-
+ return JsonResponse(
+ {
+ "success": False,
+ "message": "You don't have permission to view this subject's syllabus",
+ },
+ status=403,
+ )
+
try:
subject = Subject.objects.filter(id=subject_id).first()
except Subject.DoesNotExist:
- return JsonResponse({
- "success": False,
- "message": "Subject not found"
- }, status=404)
-
+ return JsonResponse(
+ {"success": False, "message": "Subject not found"}, status=404
+ )
+
if not subject.syllabus_pdf:
- return JsonResponse({
- "success": False,
- "message": "No syllabus available for this subject"
- }, status=404)
-
+ return JsonResponse(
+ {"success": False, "message": "No syllabus available for this subject"},
+ status=404,
+ )
+
# Return JSON with the file URL
- return JsonResponse({
- "success": True,
- "file_url": subject.syllabus_pdf.url,
- "file_name": f"{subject.name} Syllabus.pdf"
- })
+ return JsonResponse(
+ {
+ "success": True,
+ "file_url": subject.syllabus_pdf.url,
+ "file_name": f"{subject.name} Syllabus.pdf",
+ }
+ )
except Exception as e:
- return JsonResponse({
- "success": False,
- "message": f"Error retrieving syllabus: {str(e)}"
- }, status=500)
+ return JsonResponse(
+ {"success": False, "message": f"Error retrieving syllabus: {str(e)}"},
+ status=500,
+ )
@login_required
@@ -959,35 +1120,42 @@ def staffDashboard(request):
last_month = today - timedelta(days=30)
# Clear section states on page load
- if request.method == 'GET':
- request.session.pop('active_section', None)
- request.session.pop('active_subsection', None)
+ if request.method == "GET":
+ request.session.pop("active_section", None)
+ request.session.pop("active_subsection", None)
# Get feedback types for institute feedback
feedback_types = FEEDBACK_TYPE_CHOICES
# Get institute feedback
- institute_feedback = StaffInstituteFeedback.objects.filter(staff=staff).order_by("-created_at")[:5]
+ institute_feedback = StaffInstituteFeedback.objects.filter(staff=staff).order_by(
+ "-created_at"
+ )[:5]
# Get recent notices
recent_notices = Notice.objects.all().order_by("-created_at")[:5]
# Get recent feedback
- recent_feedback = StudentFeedback.objects.filter(teacher=staff).order_by("-created_at")[:5]
+ recent_feedback = StudentFeedback.objects.filter(teacher=staff).order_by(
+ "-created_at"
+ )[:5]
# Get leave requests
- leave_requests = Staff_leave.objects.filter(staff=staff).order_by("-created_at")[:5]
+ leave_requests = StaffLeave.objects.filter(staff=staff).order_by("-created_at")[:5]
# Get routines assigned to this teacher
- routines = Routine.objects.filter(teacher=staff, is_active=True).order_by("start_time")
+ routines = Routine.objects.filter(teacher=staff, is_active=True).order_by(
+ "start_time"
+ )
# Get today's routines
- today_routines = routines.filter(start_time__lte=timezone.now().time(), end_time__gte=timezone.now().time())
+ today_routines = routines.filter(
+ start_time__lte=timezone.now().time(), end_time__gte=timezone.now().time()
+ )
# Get attendance records for today's routines
today_attendance = Attendance.objects.filter(
- routine__in=today_routines,
- date=today
+ routine__in=today_routines, date=today
).select_related("routine")
# Calculate attendance statistics
@@ -1002,7 +1170,12 @@ def staffDashboard(request):
absent_students += records.filter(student_attend=False).count()
# Get average rating from student feedback
- avg_rating = StudentFeedback.objects.filter(teacher=staff).aggregate(Avg("rating"))["rating__avg"] or 0
+ avg_rating = (
+ StudentFeedback.objects.filter(teacher=staff).aggregate(Avg("rating"))[
+ "rating__avg"
+ ]
+ or 0
+ )
context = {
"staff": staff,
@@ -1021,4 +1194,4 @@ def staffDashboard(request):
"last_month": last_month,
}
- return render(request, "app/staff/dashboard.html", context)
+ return render(request, "teacher/dashboard.html", context)
diff --git a/app/studentviews.py b/app/studentviews.py
index 8db9671..6618d27 100644
--- a/app/studentviews.py
+++ b/app/studentviews.py
@@ -1,25 +1,37 @@
-from django.shortcuts import render, redirect, get_object_or_404
-from django.http import JsonResponse, HttpResponse
-from django.contrib.auth.decorators import login_required
-from django.contrib import messages
-from django.db.models import Count, Q, Avg
-from django.utils import timezone
+# Standard library imports
from datetime import timedelta
-from django.views.decorators.http import require_http_methods, require_GET
-from .models import (
- Student,
- Course,
- CourseTracking,
- Student_Leave,
- Staff,
+
+# Core Django imports
+from django.contrib import messages
+from django.contrib.auth.decorators import login_required
+from django.db.models import Avg, Count, Q
+from django.http import JsonResponse
+from django.shortcuts import get_object_or_404, redirect, render
+from django.utils import timezone
+from django.views.decorators.http import require_http_methods
+
+# Local app imports
+from app.models import (
Attendance,
AttendanceRecord,
- StudentFeedback,
- Routine,
- Notice,
- Institute,
+ Batch,
+ Course,
+ CourseTracking,
InstituteFeedback,
+ Notice,
+ Parent,
+ ParentFeedback,
+ ParentInstituteFeedback,
+ Routine,
+ Staff,
+ StaffInstituteFeedback,
+ StaffLeave,
+ Student,
+ StudentFeedback,
+ StudentLeave,
Subject,
+ SubjectFile,
+ TeacherParentMeeting,
FEEDBACK_TYPE_CHOICES,
)
@@ -33,15 +45,16 @@ def studentDashboard(request):
last_month = today - timedelta(days=30)
# Clear section states on page load
- if request.method == 'GET':
- request.session.pop('active_section', None)
- request.session.pop('active_subsection', None)
+ if request.method == "GET":
+ request.session.pop("active_section", None)
+ request.session.pop("active_subsection", None)
# Get all teachers for feedback form
teachers = Staff.objects.filter(
Q(designation__icontains="Teacher")
| Q(designation__icontains="Staff")
| Q(designation__icontains="Principal")
+ | Q(designation__icontains="Admission Officer")
| Q(designation__isnull=True)
| Q(designation="")
).order_by("name")
@@ -77,87 +90,81 @@ def studentDashboard(request):
# Course Details with enhanced information
course_details = []
for tracking in course_trackings:
- course_details.append({
- "name": tracking.course.name,
- "start_date": tracking.start_date,
- "expected_end_date": tracking.expected_end_date,
- "actual_end_date": tracking.actual_end_date,
- "completion_percentage": tracking.completion_percentage,
- "progress_status": tracking.progress_status,
- "current_semester": tracking.current_semester,
- "semester_start_date": tracking.semester_start_date,
- "semester_end_date": tracking.semester_end_date,
- "remaining_days": tracking.remaining_days,
- "is_completed": tracking.is_completed,
- "is_active": tracking.is_active,
- "duration_type": tracking.course.duration_type,
- "duration": tracking.course.duration,
- "total_duration_days": tracking.total_duration_days,
- "notes": tracking.notes,
- })
+ course_details.append(
+ {
+ "name": tracking.course.name,
+ "start_date": tracking.start_date,
+ "expected_end_date": tracking.expected_end_date,
+ "actual_end_date": tracking.actual_end_date,
+ "completion_percentage": tracking.completion_percentage,
+ "progress_status": tracking.progress_status,
+ "current_period": tracking.current_period,
+ "period_start_date": tracking.period_start_date,
+ "period_end_date": tracking.period_end_date,
+ "remaining_days": tracking.remaining_days,
+ "total_remaining_days": tracking.total_remaining_days,
+ "is_completed": tracking.is_completed,
+ "is_active": tracking.is_active,
+ "duration_type": tracking.course.duration_type,
+ "duration": tracking.course.duration,
+ "total_duration_days": tracking.total_duration_days,
+ "notes": tracking.notes,
+ }
+ )
- # Get current subjects based on current semester
+ # Get current subjects based on current period
current_subjects = []
active_tracking = course_trackings.filter(progress_status="In Progress").first()
if active_tracking:
current_subjects = Subject.objects.filter(
- course=active_tracking.course,
- semester_or_year=active_tracking.current_semester
+ course=active_tracking.course, period_or_year=active_tracking.current_period
)
# Attendance Statistics with enhanced information
attendance_records = AttendanceRecord.objects.filter(student=student)
-
- # Get the current semester start and end dates from student's course tracking
- current_semester_start = None
- current_semester_end = None
-
+
+ # Get the current period start and end dates from student's course tracking
+ current_period_start = None
+ current_period_end = None
+
# Try to get course tracking for current student
try:
- if active_tracking and active_tracking.semester_start_date:
- current_semester_start = active_tracking.semester_start_date
- current_semester_end = active_tracking.semester_end_date or today
+ if active_tracking and active_tracking.period_start_date:
+ current_period_start = active_tracking.period_start_date
+ current_period_end = active_tracking.period_end_date or today
except Exception:
pass
-
- # Filter attendance by current semester date range if available
- if current_semester_start:
- semester_attendance = attendance_records.filter(
- attendance__date__gte=current_semester_start,
- attendance__date__lte=current_semester_end
+
+ # Calculate total attendance
+ total_attendance = attendance_records.filter(student_attend=True).count()
+ total_classes = attendance_records.count()
+
+ # Calculate attendance percentage
+ if total_classes > 0:
+ attendance_percentage = (total_attendance / total_classes) * 100
+ else:
+ attendance_percentage = 0
+
+ # Filter attendance by current period date range if available
+ if current_period_start:
+ period_attendance = attendance_records.filter(
+ attendance__date__gte=current_period_start,
+ attendance__date__lte=current_period_end,
)
-
+
# Count the actual classes and attendance
- actual_classes = semester_attendance.count()
- classes_attended = semester_attendance.filter(student_attend=True).count()
-
- # Check if the course is semester-based or year-based
- is_semester_based = active_tracking.course.duration_type == "Semester" if active_tracking else True
-
- # Set the denominator based on semester/year
- if is_semester_based:
- # For a 6-month semester, use 180 days as denominator
- total_classes = 180
- else:
- # For a full year, use 365 days as denominator
- total_classes = 365 # Or use 317 (365 - 48) if excluding Saturdays
-
- # Keep track of actual records for display purposes
+ actual_classes = period_attendance.count()
+ classes_attended = period_attendance.filter(student_attend=True).count()
+
+ # Calculate actual attendance percentage
if actual_classes > 0:
- attendance_summary_actual = (classes_attended / actual_classes * 100)
+ attendance_summary_actual = classes_attended / actual_classes * 100
else:
attendance_summary_actual = 0
else:
- # Fall back to overall attendance if semester dates aren't available
- actual_classes = attendance_records.count()
- classes_attended = attendance_records.filter(student_attend=True).count()
- total_classes = 180 # Default to semester-based
- attendance_summary_actual = 0
-
- # Calculate attendance percentage using the fixed formula
- attendance_percentage = (
- (classes_attended / total_classes * 100) if total_classes > 0 else 0
- )
+ actual_classes = total_classes
+ classes_attended = total_attendance
+ attendance_summary_actual = attendance_percentage
# Calculate attendance streaks
current_streak = 0
@@ -188,15 +195,17 @@ def studentDashboard(request):
attendance_summary = {
"total_classes": total_classes,
"classes_attended": classes_attended,
- "present_count": classes_attended,
- "absent_count": total_classes - classes_attended,
+ "present_count": total_attendance,
+ "absent_count": total_classes - total_attendance,
"attendance_percentage": attendance_percentage,
"current_streak": current_streak,
- "longest_streak": longest_streak
+ "longest_streak": longest_streak,
+ "actual_classes": actual_classes,
+ "actual_percentage": attendance_summary_actual,
}
-
+
# Add actual recorded classes info if different from expected
- if current_semester_start and actual_classes != total_classes:
+ if current_period_start and actual_classes != total_classes:
attendance_summary["actual_classes"] = actual_classes
attendance_summary["actual_percentage"] = attendance_summary_actual
@@ -218,24 +227,24 @@ def studentDashboard(request):
notices = Notice.objects.all().order_by("-created_at")
# Get student leaves with status information
- student_leaves = Student_Leave.objects.filter(student=student).order_by(
+ student_leaves = StudentLeave.objects.filter(student=student).order_by(
"-created_at"
)
pending_leaves = student_leaves.filter(status=0).count()
approved_leaves = student_leaves.filter(status=1).count()
rejected_leaves = student_leaves.filter(status=2).count()
- # Get current semester routines
- current_semester_routines = []
+ # Get current period routines
+ current_period_routines = Routine.objects.none() # Initialize with empty QuerySet
if active_tracking:
- current_semester_routines = Routine.objects.filter(
+ current_period_routines = Routine.objects.filter(
course=active_tracking.course,
- semester_or_year=active_tracking.current_semester,
+ period_or_year=active_tracking.current_period,
is_active=True,
)
# Check if student has any routines
- has_routines = current_semester_routines.exists()
+ has_routines = current_period_routines.exists()
context = {
"student": student,
@@ -252,6 +261,7 @@ def studentDashboard(request):
"course_summary": course_summary,
# Current Subjects
"current_subjects": current_subjects,
+ "active_tracking": active_tracking,
# Attendance
"attendance_summary": attendance_summary,
"attendance_records": attendance_records,
@@ -267,13 +277,13 @@ def studentDashboard(request):
"approved_leaves": approved_leaves,
"rejected_leaves": rejected_leaves,
# Routines
- "current_semester_routines": current_semester_routines,
+ "current_period_routines": current_period_routines,
"has_routines": has_routines,
# Current date for display
"today": today,
}
- return render(request, "student_dashboard.html", context)
+ return render(request, "student/dashboard.html", context)
@login_required
@@ -301,10 +311,10 @@ def request_leave(request):
return redirect("studentDashboard")
# Check if leave already exists for these dates
- existing_leave = Student_Leave.objects.filter(
+ existing_leave = StudentLeave.objects.filter(
student=student,
start_date__lte=end_date.date(),
- end_date__gte=start_date.date()
+ end_date__gte=start_date.date(),
).first()
if existing_leave:
@@ -314,7 +324,7 @@ def request_leave(request):
return redirect("studentDashboard")
# Create leave request
- leave = Student_Leave.objects.create(
+ leave = StudentLeave.objects.create(
student=student,
start_date=start_date.date(),
end_date=end_date.date(),
@@ -365,7 +375,9 @@ def submit_feedback(request):
existing_feedback.rating = rating
existing_feedback.feedback_text = feedback_text
existing_feedback.save()
- messages.success(request, "Teacher feedback updated successfully")
+ messages.success(
+ request, "Teacher feedback updated successfully"
+ )
else:
# Create new feedback
StudentFeedback.objects.create(
@@ -374,7 +386,9 @@ def submit_feedback(request):
rating=rating,
feedback_text=feedback_text,
)
- messages.success(request, "Teacher feedback submitted successfully")
+ messages.success(
+ request, "Teacher feedback submitted successfully"
+ )
except Staff.DoesNotExist:
messages.error(request, "Selected teacher does not exist")
@@ -401,7 +415,9 @@ def submit_institute_feedback(request):
try:
# Validate feedback type
- if not feedback_type or feedback_type not in dict(InstituteFeedback.FEEDBACK_TYPE_CHOICES):
+ if not feedback_type or feedback_type not in dict(
+ InstituteFeedback.FEEDBACK_TYPE_CHOICES
+ ):
messages.error(request, "Please select a valid feedback type")
return redirect("studentDashboard")
@@ -469,7 +485,7 @@ def get_subject_files(request, subject_id):
try:
subject = Subject.objects.get(id=subject_id)
files = subject.get_all_files()
-
+
if not files:
return JsonResponse(
{"success": False, "message": "No files available for this subject"}
diff --git a/app/templatetags/app_filters.py b/app/templatetags/app_filters.py
index 22b3526..97d9eac 100644
--- a/app/templatetags/app_filters.py
+++ b/app/templatetags/app_filters.py
@@ -1,3 +1,4 @@
+# Core Django imports
from django import template
register = template.Library()
@@ -17,9 +18,9 @@ def get_range(value):
def multiply(value, arg):
"""Multiply the value by the argument"""
try:
- return int(value) * int(arg)
- except (ValueError, TypeError):
- return value
+ return float(value) * float(arg)
+ except ValueError:
+ return 0
@register.filter
def get_item(dictionary, key):
@@ -35,4 +36,31 @@ def get_item(dictionary, key):
return dictionary.get(key)
# Try with the key as string
- return dictionary.get(str(key), None)
\ No newline at end of file
+ return dictionary.get(str(key), None)
+
+@register.filter
+def filter_attended(attendance_records):
+ """Filter attendance records to get only attended ones"""
+ return attendance_records.filter(student_attend=True)
+
+@register.filter
+def div(value, arg):
+ """Divide the value by the argument"""
+ try:
+ return float(value) / float(arg)
+ except (ValueError, ZeroDivisionError):
+ return 0
+
+@register.filter
+def filter_by_subject(routines, subject):
+ """
+ Filter routines to get the one for a specific subject.
+ Usage in template: {{ routines|filter_by_subject:subject }}
+ """
+ if not routines:
+ return None
+
+ for routine in routines:
+ if routine.subject_id == subject.id:
+ return routine
+ return None
\ No newline at end of file
diff --git a/app/tests.py b/app/tests.py
index 7ce503c..688df16 100644
--- a/app/tests.py
+++ b/app/tests.py
@@ -1,3 +1,4 @@
+# Core Django imports
from django.test import TestCase
# Create your tests here.
diff --git a/app/urls.py b/app/urls.py
index 9d513b1..dc0a3ce 100644
--- a/app/urls.py
+++ b/app/urls.py
@@ -1,89 +1,295 @@
from django.urls import path
from app import views
-from django.contrib import admin
from app import auth
from app import staffviews
-from app import firebase
-from app.staffviews import (
- teacherDashboard,
- request_staff_leave,
- submit_staff_institute_feedback,
- save_attendance,
- get_subject_schedule,
- teacher_update_profile_picture,
- teacher_change_password,
- get_attendance_form,
- get_students,
- manage_subject_files,
- delete_subject_file,
- get_teacher_subjects,
- view_subject_syllabus
-)
+from app import studentviews
+from app import parentviews
+from app import admissionviews
+from app import hodviews
+
# URL patterns for the main application
urlpatterns = [
- # Dashboard URLs
+
+ # Dashboard URLs ---------------------------------------------------
path("dashboard/", views.dashboard, name="dashboard"),
- path("student-dashboard/", views.studentDashboard, name="studentDashboard"),
- path("teacher-dashboard/", views.teacherDashboard, name="teacherDashboard"),
+ path("student-dashboard/", studentviews.studentDashboard, name="studentDashboard"),
+ path("teacher-dashboard/", staffviews.teacherDashboard, name="teacherDashboard"),
+ path("parent-dashboard/", parentviews.parent_dashboard, name="parentDashboard"),
+ path(
+ "admission-officer-dashboard/",
+ admissionviews.admission_officer_dashboard,
+ name="admissionOfficerDashboard",
+ ),
+ path("hodDashboard/", hodviews.hod_dashboard, name="hodDashboard"),
- # Leave Management URLs
- path("request-leave/", views.request_leave, name="request_leave"),
- path("request-staff-leave/", views.request_staff_leave, name="request_staff_leave"),
- path("approve-student-leave//", views.approve_student_leave, name="approve_student_leave"),
- path("reject-student-leave//", views.reject_student_leave, name="reject_student_leave"),
- path("approve-staff-leave//", views.approve_staff_leave, name="approve_staff_leave"),
- path("reject-staff-leave//", views.reject_staff_leave, name="reject_staff_leave"),
-
- # Feedback URLs
- path("submit-feedback/", views.submit_feedback, name="submit_feedback"),
- path("submit-institute-feedback/", views.submit_institute_feedback, name="submit_institute_feedback"),
- path("submit-staff-institute-feedback/", views.submit_staff_institute_feedback, name="submit_staff_institute_feedback"),
-
- # Profile Management URLs
- path("update-profile/", views.update_profile, name="update_profile"),
- path("change-password/", views.change_password, name="change_password"),
- path("teacher-update-profile-picture/", views.teacher_update_profile_picture, name="teacher_update_profile_picture"),
- path("teacher-change-password/", views.teacher_change_password, name="teacher_change_password"),
-
- # Firebase Cloud Messaging URLs
- path("saveFCMToken/", views.save_fcm_token, name="saveFCMToken"),
-
- # Subject and Course Management URLs
- path("subject//files/", views.get_subject_files, name="get_subject_files"),
- path("subject//syllabus/", staffviews.view_subject_syllabus, name="view_subject_syllabus"),
- path("get-subjects/", views.get_subjects, name="get_subjects"),
- path("get-teachers/", views.get_teachers, name="get_teachers"),
- path("get-course-duration/", views.get_course_duration, name="get_course_duration"),
- path("get-subject-schedule/", views.get_subject_schedule, name="get_subject_schedule"),
-
- # New Subject File Management URLs
- path("manage-subject-files/", staffviews.manage_subject_files, name="manage_subject_files"),
- path("delete-subject-file/", staffviews.delete_subject_file, name="delete_subject_file"),
- path("get-teacher-subjects/", staffviews.get_teacher_subjects, name="get_teacher_subjects"),
-
- # Attendance Management URLs
- path("save-attendance/", views.save_attendance, name="save_attendance"),
- path("get-attendance-form/", staffviews.get_attendance_form, name="get_attendance_form"),
-
- # Notice Management URLs
+ # Notice Management ------------------------------------------------
path("add-notice/", views.add_notice, name="add_notice"),
path("delete-notice//", views.delete_notice, name="delete_notice"),
- # Password Reset URLs
+ # Leave Management -------------------------------------------------
+ # Student Leave
+ path("request-leave/", views.request_leave, name="request_leave"),
+ path(
+ "approve-student-leave//",
+ views.approve_student_leave,
+ name="approve_student_leave",
+ ),
+ path(
+ "reject-student-leave//",
+ views.reject_student_leave,
+ name="reject_student_leave",
+ ),
+ path("get-student-leaves/", views.get_student_leaves, name="get_student_leaves"),
+
+ # Staff Leave
+ path("request-staff-leave/", views.request_staff_leave, name="request_staff_leave"),
+ path(
+ "approve-staff-leave//",
+ views.approve_staff_leave,
+ name="approve_staff_leave",
+ ),
+ path(
+ "reject-staff-leave//",
+ views.reject_staff_leave,
+ name="reject_staff_leave",
+ ),
+ path("get-staff-leaves/", views.get_staff_leaves, name="get_staff_leaves"),
+
+ # Feedback --------------------------------------------------------
+ # Student Feedback
+ path("submit-feedback/", views.submit_feedback, name="submit_feedback"),
+ path(
+ "submit-institute-feedback/",
+ views.submit_institute_feedback,
+ name="submit_institute_feedback",
+ ),
+
+ # Staff Feedback
+ path(
+ "submit-staff-institute-feedback/",
+ views.submit_staff_institute_feedback,
+ name="submit_staff_institute_feedback",
+ ),
+
+ # Parent Feedback
+ path(
+ "parent/feedback/submit/",
+ parentviews.submit_parent_feedback,
+ name="submit_parent_feedback",
+ ),
+ path(
+ "parent/feedback/institute/submit/",
+ parentviews.submit_parent_institute_feedback,
+ name="submit_parent_institute_feedback",
+ ),
+
+ # Profile Management ----------------------------------------------
+ path("update-profile/", views.update_profile, name="update_profile"),
+ path("change-password/", views.change_password, name="change_password"),
+ path(
+ "teacher-update-profile-picture/",
+ views.teacher_update_profile_picture,
+ name="teacher_update_profile_picture",
+ ),
+ path(
+ "teacher-change-password/",
+ views.teacher_change_password,
+ name="teacher_change_password",
+ ),
+
+ # Subject and Course Management -----------------------------------
+ path("add-subject/", hodviews.add_subject, name="add_subject"),
+ path(
+ "subject//files/",
+ views.get_subject_files,
+ name="get_subject_files",
+ ),
+ path(
+ "subject//syllabus/",
+ staffviews.view_subject_syllabus,
+ name="view_subject_syllabus",
+ ),
+ path("get-subjects/", views.get_subjects, name="get_subjects"),
+ path("get-teachers/", views.get_teachers, name="get_teachers"),
+ path("get-course-duration/", views.get_course_duration, name="get_course_duration"),
+ path(
+ "get-subject-schedule/", views.get_subject_schedule, name="get_subject_schedule"
+ ),
+ path('subject//view/', hodviews.view_subject, name='view_subject'),
+ path('edit-subject//', hodviews.edit_subject, name='edit_subject'),
+ path('delete-subject//', hodviews.delete_subject, name='delete_subject'),
+
+ # Student Management
+ path('add-student/', hodviews.add_student, name='add_student'),
+ path('get-student//', hodviews.get_student, name='get_student'),
+ path('edit-student//', hodviews.edit_student, name='edit_student'),
+ path('delete-student//', hodviews.delete_student, name='delete_student'),
+
+ # HOD Notice Management
+ path('hod/add-notice/', hodviews.add_notice, name='hod_add_notice'),
+ path('notice//', hodviews.view_notice, name='view_notice'),
+ path('delete-notice//', hodviews.delete_notice, name='delete_notice'),
+
+ # Subject File Management
+ path(
+ "delete-subject-file/",
+ staffviews.delete_subject_file,
+ name="delete_subject_file",
+ ),
+ path(
+ "get-teacher-subjects/",
+ staffviews.get_teacher_subjects,
+ name="get_teacher_subjects",
+ ),
+
+ # Attendance Management -------------------------------------------
+ path("save-attendance/", views.save_attendance, name="save_attendance"),
+ path(
+ "get-attendance-form/",
+ staffviews.get_attendance_form,
+ name="get_attendance_form",
+ ),
+
+ # Password Management ---------------------------------------------
+ # Password Reset Options
path("password-reset/", auth.reset_password_options, name="reset_password_options"),
- path("password-reset/phone/", auth.password_reset_phone, name="phone_reset_password"),
- path("password-reset/phone/verify/", auth.password_reset_phone_verify, name="verify_reset_otp"),
- path("password-reset/phone/resend/", auth.resend_phone_otp, name="resend_phone_otp"),
- path("password-reset/email/", auth.password_reset_email, name="email_reset_password"),
- path("password-reset/email/verify/", auth.password_reset_email_verify, name="password_reset_done"),
- path("password-reset/email/resend/", auth.resend_email_code, name="resend_reset_code"),
+
+ # Phone-based Reset
+ path(
+ "password-reset/phone/", auth.password_reset_phone, name="phone_reset_password"
+ ),
+ path(
+ "password-reset/phone/verify/",
+ auth.password_reset_phone_verify,
+ name="verify_reset_otp",
+ ),
+ path(
+ "password-reset/phone/resend/", auth.resend_phone_otp, name="resend_phone_otp"
+ ),
+
+ # Email-based Reset
+ path(
+ "password-reset/email/", auth.password_reset_email, name="email_reset_password"
+ ),
+ path(
+ "password-reset/email/verify/",
+ auth.password_reset_email_verify,
+ name="password_reset_done",
+ ),
+ path(
+ "password-reset/email/resend/", auth.resend_email_code, name="resend_reset_code"
+ ),
+
+ # New Password Setup
path("password-reset/set/", auth.set_new_password, name="set_new_password"),
- # Password security check
+ # Password Security
path("check-weak-password/", views.check_weak_password, name="check_weak_password"),
- # Staff Management URLs
+ # Parent-Teacher Meeting ------------------------------------------
+ path("parent/meetings/", parentviews.parent_meetings, name="parent_meetings"),
+ path("api/meetings/add/", parentviews.add_meeting, name="add_meeting"),
+ path(
+ "api/meetings//complete/",
+ parentviews.complete_meeting,
+ name="complete_meeting",
+ ),
+ path(
+ "api/meetings//cancel/",
+ parentviews.cancel_meeting,
+ name="cancel_meeting",
+ ),
+ path(
+ "api/meetings//reschedule/",
+ parentviews.reschedule_meeting,
+ name="reschedule_meeting",
+ ),
+ path(
+ "api/meetings//notes/",
+ parentviews.get_meeting_notes,
+ name="get_meeting_notes",
+ ),
+ path(
+ "api/meetings//agenda/",
+ parentviews.get_meeting_agenda,
+ name="get_meeting_agenda",
+ ),
+ path(
+ "api/meetings//cancellation-reason/",
+ views.get_meeting_cancellation_reason,
+ name="get_meeting_cancellation_reason",
+ ),
+
+ # API Endpoints --------------------------------------------------
+ # Student API
+ path(
+ "api/students//details/",
+ parentviews.get_student_details,
+ name="get_student_details",
+ ),
path("get-students/", staffviews.get_students, name="get_students"),
-]
+
+ # Meeting API Endpoints (HOD)
+ path("api/hod/meetings//", hodviews.api_get_meeting, name="api_get_meeting"),
+ path("api/hod/meetings/", hodviews.api_create_meeting, name="api_create_meeting"),
+ path("api/hod/meetings//edit/", hodviews.api_update_meeting, name="api_update_meeting"),
+ path("api/hod/meetings//cancel/", hodviews.api_cancel_meeting, name="api_cancel_meeting"),
+ path("api/hod/meetings//notes/", hodviews.get_meeting_notes, name="get_meeting_notes"),
+ path("api/hod/meetings//agenda/", hodviews.get_meeting_agenda, name="get_meeting_agenda"),
+ path("api/hod/meetings//cancellation-reason/", hodviews.get_cancellation_reason, name="get_cancellation_reason"),
+
+ # Routine API Endpoints (HOD)
+ path("api/hod/routines/", hodviews.api_create_routine, name="api_create_routine"),
+ path("api/hod/routines//", hodviews.api_get_routine, name="api_get_routine"),
+ path("api/hod/routines//edit/", hodviews.api_update_routine, name="api_update_routine"),
+ path("api/hod/routines//delete/", hodviews.api_delete_routine, name="api_delete_routine"),
+
+ # Progress API Endpoints (HOD)
+ path("api/get-student-progress/", hodviews.get_student_progress, name="get_student_progress"),
+ path("api/get-progress//", hodviews.get_progress, name="get_progress"),
+ path("api/edit-progress//", hodviews.edit_progress, name="edit_progress"),
+
+ # Firebase Cloud Messaging ---------------------------------------
+ path("saveFCMToken/", views.save_fcm_token, name="saveFCMToken"),
+ # Admission Officer routes ----------------------------------------
+ path("add-student/", admissionviews.add_student, name="add_student"),
+ path("add-batch/", admissionviews.add_batch, name="add_batch"),
+ path("add-course/", admissionviews.add_course, name="add_course"),
+ path(
+ "add-course-tracking/",
+ admissionviews.add_course_tracking,
+ name="add_course_tracking",
+ ),
+ path("get-courses/", admissionviews.get_courses, name="get_courses"),
+ path("get-batches/", admissionviews.get_batches, name="get_batches"),
+ path(
+ "admission-get-students/",
+ admissionviews.get_students,
+ name="admission_get_students",
+ ),
+ path(
+ "get-student//", admissionviews.get_student, name="get_student"
+ ),
+ path("edit-student/", admissionviews.edit_student, name="edit_student"),
+ path(
+ "get-course-tracking//",
+ admissionviews.get_course_tracking,
+ name="get_course_tracking",
+ ),
+ path(
+ "edit-course-tracking/",
+ admissionviews.edit_course_tracking,
+ name="edit_course_tracking",
+ ),
+
+ # Staff Management URLs
+ path('staff//', hodviews.view_staff, name='view_staff'),
+ path('edit-staff//', hodviews.edit_staff, name='edit_staff'),
+ path('delete-staff//', hodviews.delete_staff, name='delete_staff'),
+ path('add-staff/', hodviews.add_staff, name='add_staff'),
+
+ # Test endpoints (commented out)
+ # path('test/error/505/', views.test_505_error, name='test_505_error'),
+]
diff --git a/app/utils.py b/app/utils.py
index f8ab97a..c22b0fe 100644
--- a/app/utils.py
+++ b/app/utils.py
@@ -1,24 +1,31 @@
-import random
-import string
-import requests
-from django.template.loader import render_to_string
-from django.core.mail import send_mail
-from django.conf import settings
-from django.contrib.auth.tokens import default_token_generator
-from django.utils.http import urlsafe_base64_encode
-from django.utils.encoding import force_bytes
-from django.utils import timezone
-import pyotp
+# Standard library imports
import base64
-from .models import TOTPSecret, ResetToken, Student, Staff
+import random
+import os
+
+# Core Django imports
+from django.core.mail import send_mail
+from django.template.loader import render_to_string
+from django.utils import timezone
+
+# Third-party app imports
+import pyotp
+
+# Local app imports
+from app.models import ResetToken, Staff, Student, TOTPSecret
# OTP expiration time in seconds (5 minutes)
-OTP_EXPIRY = 300
+OTP_EXPIRY = int(os.getenv('OTP_EXPIRY', 300))
+
+
+# --------------------------------------------------------------------
+# OTP and Secret Key Generation
+# --------------------------------------------------------------------
def generate_secret_key():
"""Generate a random secret key for TOTP"""
- return base64.b32encode(random.getrandbits(160).to_bytes(20, 'big')).decode('utf-8')
+ return base64.b32encode(random.getrandbits(160).to_bytes(20, "big")).decode("utf-8")
def generate_otp(secret_key):
@@ -27,17 +34,20 @@ def generate_otp(secret_key):
return totp.now()
+# --------------------------------------------------------------------
+# OTP Storage and Verification
+# --------------------------------------------------------------------
+
+
def store_secret_key(identifier, secret_key):
"""Store secret key in database"""
# Delete any existing secrets for this identifier
TOTPSecret.objects.filter(identifier=identifier).delete()
-
+
# Create new secret with expiration
expires_at = timezone.now() + timezone.timedelta(seconds=OTP_EXPIRY)
return TOTPSecret.objects.create(
- identifier=identifier,
- secret_key=secret_key,
- expires_at=expires_at
+ identifier=identifier, secret_key=secret_key, expires_at=expires_at
)
@@ -45,12 +55,12 @@ def verify_otp(identifier, otp):
"""Verify OTP using TOTP"""
try:
# Get the most recent secret for this identifier
- secret = TOTPSecret.objects.filter(identifier=identifier).latest('created_at')
-
+ secret = TOTPSecret.objects.filter(identifier=identifier).latest("created_at")
+
# Check if secret is expired
if secret.is_expired():
return False
-
+
# Verify OTP
totp = pyotp.TOTP(secret.secret_key, interval=OTP_EXPIRY)
return totp.verify(otp)
@@ -58,6 +68,11 @@ def verify_otp(identifier, otp):
return False
+# --------------------------------------------------------------------
+# SMS Communication
+# --------------------------------------------------------------------
+
+
def send_otp_sms(phone, otp):
"""Send OTP via SMS using a service"""
try:
@@ -65,19 +80,18 @@ def send_otp_sms(phone, otp):
student = Student.objects.filter(phone=phone).first()
staff = Staff.objects.filter(phone=phone).first()
user = student or staff
-
+
# Get user's name
user_name = user.name if user else "User"
-
+
# Calculate expiry time in minutes
expiry_minutes = OTP_EXPIRY // 60
# Render SMS message using template
- message = render_to_string("login/sms_templates/otp_message.txt", {
- "user_name": user_name,
- "otp": otp,
- "expiry_time": expiry_minutes
- })
+ message = render_to_string(
+ "login/sms_templates/otp_message.txt",
+ {"user_name": user_name, "otp": otp, "expiry_time": expiry_minutes},
+ )
# This is a placeholder. You need to replace with actual SMS API implementation
# Example using a generic SMS API
@@ -100,6 +114,11 @@ def send_otp_sms(phone, otp):
return False
+# --------------------------------------------------------------------
+# Email Communication
+# --------------------------------------------------------------------
+
+
def send_password_reset_email(user, request, email):
"""
Send password reset code to user's email using HTML template
@@ -107,10 +126,10 @@ def send_password_reset_email(user, request, email):
try:
# Generate a secret key for TOTP
secret_key = generate_secret_key()
-
+
# Store the secret key in database
store_secret_key(email, secret_key)
-
+
# Generate TOTP
totp = pyotp.TOTP(secret_key, interval=OTP_EXPIRY)
reset_code = totp.now()
@@ -151,17 +170,20 @@ def verify_reset_code(email, code):
return verify_otp(email, code)
+# --------------------------------------------------------------------
+# Token Management
+# --------------------------------------------------------------------
+
+
def store_reset_token(token, identifier):
"""Store reset token in database"""
# Delete any existing tokens for this identifier
ResetToken.objects.filter(identifier=identifier).delete()
-
+
# Create new token with expiration (15 minutes)
expires_at = timezone.now() + timezone.timedelta(minutes=15)
return ResetToken.objects.create(
- token=token,
- identifier=identifier,
- expires_at=expires_at
+ token=token, identifier=identifier, expires_at=expires_at
)
@@ -169,18 +191,22 @@ def verify_reset_token(token, identifier):
"""Verify reset token from database"""
try:
reset_token = ResetToken.objects.get(token=token)
-
+
# Check if token is expired
if reset_token.is_expired():
return False
-
+
return reset_token.identifier == identifier
except ResetToken.DoesNotExist:
return False
+# --------------------------------------------------------------------
+# Cleanup Functions
+# --------------------------------------------------------------------
+
+
def cleanup_expired_tokens():
"""Clean up expired TOTP secrets and reset tokens"""
TOTPSecret.objects.filter(expires_at__lt=timezone.now()).delete()
ResetToken.objects.filter(expires_at__lt=timezone.now()).delete()
-
diff --git a/app/views.py b/app/views.py
index 2df4b4a..3ea94e8 100644
--- a/app/views.py
+++ b/app/views.py
@@ -1,17 +1,17 @@
from django.shortcuts import render, redirect, get_object_or_404
-from django.http import JsonResponse, HttpResponse, FileResponse
+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.conf import settings
-from .models import (
+from app.models import (
Student,
Course,
CourseTracking,
- Student_Leave,
- Staff_leave,
+ StudentLeave,
+ StaffLeave,
Attendance,
AttendanceRecord,
StudentFeedback,
@@ -22,22 +22,25 @@ from .models import (
InstituteFeedback,
Subject,
Batch,
+ ParentFeedback,
+ Parent,
+ StaffInstituteFeedback,
+ ParentInstituteFeedback,
+ TeacherParentMeeting,
)
-from app.admin import custom_admin_site
from django.views.decorators.http import require_http_methods, require_GET, require_POST
from django.core.files.storage import default_storage
from django.core.files.base import ContentFile
import json
+from app.admin import custom_admin_site
import os
from django.db.models.base import transaction
-from .utils import generate_otp, generate_secret_key, store_secret_key, verify_otp, send_otp_sms
-from django.conf import settings
from dotenv import load_dotenv
-from .firebase import save_fcm_token, get_firebase_js, FCMDevice
-from django.template.loader import render_to_string
+from app.firebase import save_fcm_token, FCMDevice
from django.views.static import serve
+
# Import views from studentviews.py
-from .studentviews import (
+from app.studentviews import (
studentDashboard,
request_leave,
submit_feedback,
@@ -46,7 +49,7 @@ from .studentviews import (
)
# Import views from staffviews.py
-from .staffviews import (
+from app.staffviews import (
teacherDashboard,
request_staff_leave,
submit_staff_institute_feedback,
@@ -57,6 +60,7 @@ from .staffviews import (
)
from django.contrib.auth.hashers import check_password
+from django.urls import reverse
# Load environment variables
load_dotenv()
@@ -64,320 +68,155 @@ load_dotenv()
# Create your views here.
-
@login_required
def dashboard(request):
- """Main dashboard view that redirects to appropriate dashboard based on user role"""
+ """Main dashboard view that redirects to appropriate dashboard based on user role.
+
+ Args:
+ request: HttpRequest object containing user information
+
+ Returns:
+ HttpResponse: Redirects to appropriate dashboard or renders admin dashboard
+ """
user = request.user
user_groups = user.groups.values_list("name", flat=True)
-
-
# Redirect based on user role
if "Student" in user_groups:
- print("Redirecting to student dashboard")
return redirect("studentDashboard")
elif "Teacher" in user_groups:
- print("Redirecting to teacher dashboard")
return redirect("teacherDashboard")
- elif user.is_superuser or user.is_staff:
- # Admin users get the admin dashboard
- print("Showing admin dashboard")
-
+ elif "Parent" in user_groups:
+ return redirect("parentDashboard")
+ elif "Admission Officer" in user_groups:
+ return redirect("admissionOfficerDashboard")
+ elif "HOD" in user_groups:
+ return redirect("hodDashboard")
+ else:
# Get current date and date ranges
today = timezone.now().date()
last_week = timezone.now() - timedelta(days=7)
last_month = timezone.now() - timedelta(days=30)
-
- # Debug prints for user authentication
- print(f"Is authenticated: {request.user.is_authenticated}")
- print(f"Is staff: {request.user.is_staff}")
- print(f"Is superuser: {request.user.is_superuser}")
- print("-------------------------\n")
-
- # Student Statistics - with debug prints
+
+ # Student Statistics
total_students = Student.objects.count()
- print("\nDEBUG Student Statistics:")
- print("-------------------------")
- print(f"Total students (from count): {total_students}")
-
- # Get all students and print their statuses
- all_students = Student.objects.all()
- print("\nAll students and their statuses:")
- for student in all_students:
- print(f"- {student.name}: {student.status}")
-
active_students = Student.objects.filter(status="Active").count()
- print(f"\nActive students: {active_students}")
-
- completed_students = Student.objects.filter(status="Completed").count()
- print(f"Completed students: {completed_students}")
-
- on_leave_students = Student.objects.filter(status="Leave").count()
- print(f"Students on leave: {on_leave_students}")
-
- print(f"\nSum of status counts: {active_students + completed_students + on_leave_students}")
- print("-------------------------\n")
+ male_students = Student.objects.filter(gender="Male").count()
+ female_students = Student.objects.filter(gender="Female").count()
+ inactive_students = total_students - active_students
# 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()
+ total_teachers = Staff.objects.filter(designation="Teacher").count()
+ non_teaching_staff = total_staff - total_teachers
+ inactive_staff = total_staff - active_staff
+
+ # Parent Statistics
+ total_parents = Parent.objects.count()
+ active_parents = Parent.objects.filter(is_active=True).count()
+ parents_with_students = (
+ Parent.objects.filter(students__isnull=False).distinct().count()
)
+ parents_with_multiple_students = (
+ Parent.objects.annotate(student_count=Count("students"))
+ .filter(student_count__gt=1)
+ .count()
+ )
+ inactive_parents = total_parents - active_parents
# Course Statistics
total_courses = Course.objects.count()
active_courses = (
- Course.objects.filter(student_trackings__progress_status="In Progress")
+ Course.objects.filter(
+ student_trackings__progress_status__in=["Not Started", "In Progress"]
+ )
.distinct()
.count()
)
- completed_courses = (
- Course.objects.filter(student_trackings__progress_status="Completed")
- .distinct()
- .count()
+ total_subjects = Subject.objects.count()
+ total_batches = Batch.objects.count()
+ inactive_courses = total_courses - active_courses
+
+ # Recent Activities
+ recent_notices = Notice.objects.all().order_by("-created_at")[:5]
+ recent_feedback = (
+ list(InstituteFeedback.objects.all().order_by("-created_at")[:5])
+ + list(StaffInstituteFeedback.objects.all().order_by("-created_at")[:5])
+ + list(ParentInstituteFeedback.objects.all().order_by("-created_at")[:5])
+ )
+ recent_feedback = sorted(
+ recent_feedback, key=lambda x: x.created_at, reverse=True
+ )[:5]
+
+ # Leave Requests
+ staff_leaves = StaffLeave.objects.filter(status=0).order_by("-created_at")[
+ :5
+ ] # 0 = Pending
+ student_leaves = StudentLeave.objects.filter(status=0).order_by("-created_at")[
+ :5
+ ] # 0 = Pending
+
+ # Teacher Rating Statistics
+ teacher_ratings = (
+ Staff.objects.filter(designation="Teacher")
+ .annotate(
+ avg_student_rating=Avg("feedbacks__rating"),
+ avg_parent_rating=Avg("parent_feedbacks__rating"),
+ total_feedbacks=Count("feedbacks") + Count("parent_feedbacks"),
+ )
+ .order_by("-avg_student_rating")[:5]
)
- # Course Tracking Statistics
- course_trackings = CourseTracking.objects.all()
- in_progress_trackings = course_trackings.filter(progress_status="In Progress")
- completed_trackings = course_trackings.filter(progress_status="Completed")
- dropped_trackings = course_trackings.filter(progress_status="Dropped")
-
- # Recent Leave Requests
- recent_student_leaves = Student_Leave.objects.filter(
- created_at__gte=last_week
- ).order_by("-created_at")[:5]
-
- recent_staff_leaves = Staff_leave.objects.filter(
- 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_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 from {leave.start_date} to {leave.end_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 from {leave.start_date} to {leave.end_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="")
+ # Get all meetings
+ meetings = TeacherParentMeeting.objects.all().order_by(
+ "-meeting_date", "-meeting_time"
)
- 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():
- trackings = course.student_trackings.all()
- if trackings.exists():
- avg_progress = trackings.aggregate(
- avg_progress=Avg("completion_percentage")
- )["avg_progress"]
- course_progress.append(
- {
- "course": course,
- "total_students": trackings.count(),
- "in_progress": trackings.filter(
- progress_status="In Progress"
- ).count(),
- "completed": trackings.filter(progress_status="Completed").count(),
- "average_progress": round(avg_progress, 1) if avg_progress else 0,
- }
- )
-
- # Monthly Statistics
- monthly_stats = {
- "new_students": Student.objects.filter(joining_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.date(), student_attend=True
- ).count()
- / AttendanceRecord.objects.filter(
- attendance__date__gte=last_month.date()
- ).count()
- * 100
- )
- if AttendanceRecord.objects.filter(
- attendance__date__gte=last_month.date()
- ).exists()
- 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 = {
- **custom_admin_site.each_context(
- request
- ), # This adds the admin context, including the sidebar.
- # Student Statistics
+ "title": "Dashboard",
+ **custom_admin_site.each_context(request),
"total_students": total_students,
"active_students": active_students,
- "completed_students": completed_students,
- "on_leave_students": on_leave_students,
- # Staff Statistics
+ "male_students": male_students,
+ "female_students": female_students,
+ "inactive_students": inactive_students,
"total_staff": total_staff,
"active_staff": active_staff,
- "staff_on_leave": staff_on_leave,
- # Course Statistics
+ "total_teachers": total_teachers,
+ "non_teaching_staff": non_teaching_staff,
+ "inactive_staff": inactive_staff,
+ "total_parents": total_parents,
+ "active_parents": active_parents,
+ "parents_with_students": parents_with_students,
+ "parents_with_multiple_students": parents_with_multiple_students,
+ "inactive_parents": inactive_parents,
"total_courses": total_courses,
"active_courses": active_courses,
- "completed_courses": completed_courses,
- # Course Tracking Statistics
- "in_progress_trackings": in_progress_trackings.count(),
- "completed_trackings": completed_trackings.count(),
- "dropped_trackings": dropped_trackings.count(),
- # 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
- "total_classes_today": total_classes,
- "total_students_today": total_students_today,
- "students_present": students_present,
- "attendance_rate": round(
- (students_present / total_students_today * 100) if total_students_today > 0 else 0
- ),
- # Feedback
+ "total_subjects": total_subjects,
+ "total_batches": total_batches,
+ "inactive_courses": inactive_courses,
+ "recent_notices": recent_notices,
"recent_feedback": recent_feedback,
- "recent_institute_feedback": recent_institute_feedback,
+ "staff_leaves": staff_leaves,
+ "student_leaves": student_leaves,
"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
+ "meetings": meetings,
}
- return render(request, "dashboard.html", context)
- else:
- # If user doesn't belong to any specific group, try to determine user type
- print("User doesn't belong to any specific group")
- if hasattr(user, 'student'):
- print("User has student attribute, redirecting to student dashboard")
- return redirect("studentDashboard")
- elif hasattr(user, 'staff'):
- print("User has staff attribute, redirecting to teacher dashboard")
- return redirect("teacherDashboard")
- else:
- print("Unknown user type, showing default dashboard")
- return render(request, "dashboard.html", {"unknown_user_type": True})
+ return render(request, "admin/dashboard.html", context)
@login_required
def update_profile(request):
+ """Update user profile picture based on user role.
+
+ Args:
+ request: HttpRequest object containing user information and uploaded file
+
+ Returns:
+ HttpResponse: Redirects to appropriate dashboard with success/error message
+ """
if request.method == "POST":
user = request.user
user_groups = user.groups.values_list("name", flat=True)
@@ -386,7 +225,24 @@ def update_profile(request):
if "profile_image" in request.FILES:
try:
image = request.FILES["profile_image"]
-
+
+ # Validate image file type
+ allowed_types = ["image/jpeg", "image/png", "image/gif"]
+ if image.content_type not in allowed_types:
+ messages.error(
+ request,
+ "Invalid file type. Please upload a JPEG, PNG, or GIF image.",
+ )
+ return redirect("dashboard")
+
+ # Validate image size (max 5MB)
+ if image.size > 5 * 1024 * 1024:
+ messages.error(
+ request,
+ "Image size too large. Please upload an image smaller than 5MB.",
+ )
+ return redirect("dashboard")
+
if "Student" in user_groups:
student = Student.objects.get(id=user.id)
if student.image:
@@ -397,7 +253,7 @@ def update_profile(request):
student.image = image
student.save()
messages.success(request, "Profile picture updated successfully.")
-
+
elif "Teacher" in user_groups:
staff = Staff.objects.get(id=user.id)
if staff.image:
@@ -408,22 +264,60 @@ def update_profile(request):
staff.image = image
staff.save()
messages.success(request, "Profile picture updated successfully.")
-
+
+ elif "Parent" in user_groups:
+ parent = Parent.objects.get(id=user.id)
+ if parent.image:
+ # Delete old image if updating
+ if os.path.isfile(parent.image.path):
+ os.remove(parent.image.path)
+ # Save new image
+ parent.image = image
+ parent.save()
+ messages.success(request, "Profile picture updated successfully.")
+
+ elif "HOD" in user_groups or any(
+ group.lower().startswith("hod") for group in user_groups
+ ):
+ # HOD is typically a Staff member with special privileges
+ staff = Staff.objects.get(id=user.id)
+ 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 picture updated successfully.")
+
else:
- messages.error(request, "User profile not found. Please make sure you are logged in as a student or staff member.")
-
- except (Student.DoesNotExist, Staff.DoesNotExist) as e:
+ messages.error(
+ request,
+ "User profile not found. Please make sure you are logged in as a student, staff, or parent member.",
+ )
+ return redirect("dashboard")
+
+ except (Student.DoesNotExist, Staff.DoesNotExist, Parent.DoesNotExist) as e:
messages.error(request, "User profile not found in the database.")
+ return redirect("dashboard")
except Exception as e:
messages.error(request, f"Error updating profile picture: {str(e)}")
+ return redirect("dashboard")
else:
messages.error(request, "No profile picture uploaded.")
+ return redirect("dashboard")
# Redirect based on user type
if "Student" in user_groups:
return redirect("studentDashboard")
elif "Teacher" in user_groups:
return redirect("teacherDashboard")
+ elif "Parent" in user_groups:
+ return redirect("parentDashboard")
+ elif "Admission Officer" in user_groups:
+ return redirect("admissionOfficerDashboard")
+ elif "HOD" in user_groups:
+ return redirect("hodDashboard")
else:
return redirect("dashboard")
@@ -434,56 +328,80 @@ def update_profile(request):
def change_password(request):
if request.method == "POST":
user = request.user
+ user_groups = user.groups.values_list("name", flat=True)
current_password = request.POST.get("current_password")
new_password = request.POST.get("new_password")
confirm_password = request.POST.get("confirm_password")
# Check if this is an AJAX request
- is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
+ is_ajax = request.headers.get("X-Requested-With") == "XMLHttpRequest"
+
+ # Determine redirect URL based on user role
+ redirect_url = "dashboard"
+ if "Student" in user_groups:
+ redirect_url = "studentDashboard"
+ elif "Teacher" in user_groups:
+ redirect_url = "teacherDashboard"
+ elif "Parent" in user_groups:
+ redirect_url = "parentDashboard"
+ elif "Admission Officer" in user_groups:
+ redirect_url = "admissionOfficerDashboard"
+ elif "HOD" in user_groups:
+ redirect_url = "hodDashboard"
# Validate input
if not all([current_password, new_password, confirm_password]):
if is_ajax:
- return JsonResponse({"success": False, "error": "All password fields are required."})
+ return JsonResponse(
+ {"success": False, "error": "All password fields are required."}
+ )
messages.error(request, "All password fields are required.")
- return redirect("studentDashboard")
+ return redirect(redirect_url)
if new_password != confirm_password:
if is_ajax:
- return JsonResponse({"success": False, "error": "New passwords do not match."})
+ return JsonResponse(
+ {"success": False, "error": "New passwords do not match."}
+ )
messages.error(request, "New passwords do not match.")
- return redirect("studentDashboard")
+ return redirect(redirect_url)
# Check current password
if not user.check_password(current_password):
if is_ajax:
- return JsonResponse({"success": False, "error": "Current password is incorrect."})
+ return JsonResponse(
+ {"success": False, "error": "Current password is incorrect."}
+ )
messages.error(request, "Current password is incorrect.")
- return redirect("studentDashboard")
+ return redirect(redirect_url)
# Set new password
try:
user.set_password(new_password)
user.save()
+
+ # For both AJAX and regular requests, we'll return success and indicate need for logout
if is_ajax:
- return JsonResponse({"success": True})
-
+ return JsonResponse(
+ {
+ "success": True,
+ "redirect": "/login/",
+ "message": "Password changed successfully. Please login again.",
+ }
+ )
+
messages.success(
request, "Password changed successfully. Please login again."
)
return redirect("login")
+
except Exception as e:
if is_ajax:
- return JsonResponse({"success": False, "error": f"Error changing password: {str(e)}"})
+ return JsonResponse(
+ {"success": False, "error": f"Error changing password: {str(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(redirect_url)
return redirect("dashboard")
@@ -491,16 +409,16 @@ def change_password(request):
@login_required
@require_GET
def get_subjects(request):
- """Get subjects for a course and semester"""
+ """Get subjects for a course and period"""
course_id = request.GET.get("course_id")
- semester = request.GET.get("semester", 1)
+ period = request.GET.get("period", 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
+ course_id=course_id, period_or_year=period
).values("id", "name")
return JsonResponse({"success": True, "subjects": list(subjects)})
@@ -517,6 +435,8 @@ def get_teachers(request):
Q(designation__icontains="Teacher")
| Q(designation__icontains="Staff")
| Q(designation__icontains="Principal")
+ | Q(designation__icontains="HOD")
+ | Q(designation__icontains="Admission Officer")
| Q(designation__isnull=True)
| Q(designation="")
).values("id", "name")
@@ -574,7 +494,7 @@ def get_course_duration(request):
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 = get_object_or_404(StudentLeave, id=leave_id)
leave.status = 1 # Approved
leave.save()
messages.success(
@@ -587,7 +507,7 @@ def approve_student_leave(request, leave_id):
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 = get_object_or_404(StudentLeave, id=leave_id)
leave.status = 2 # Rejected
leave.save()
messages.warning(
@@ -600,7 +520,7 @@ def reject_student_leave(request, leave_id):
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 = get_object_or_404(StaffLeave, id=leave_id)
leave.status = 1 # Approved
leave.save()
messages.success(
@@ -613,7 +533,7 @@ def approve_staff_leave(request, leave_id):
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 = get_object_or_404(StaffLeave, id=leave_id)
leave.status = 2 # Rejected
leave.save()
messages.warning(
@@ -625,17 +545,26 @@ def reject_staff_leave(request, leave_id):
@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")
+ try:
+ title = request.POST.get("title")
+ message = request.POST.get("message")
+ image = request.FILES.get("image")
+
+ if not title or not message:
+ return JsonResponse(
+ {"success": False, "error": "Title and message are required"}
+ )
- 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")
+ return JsonResponse(
+ {"success": True, "message": "Notice added successfully"}
+ )
+
+ except Exception as e:
+ return JsonResponse({"success": False, "error": str(e)})
+
+ return JsonResponse({"success": False, "error": "Invalid request method"})
@login_required
@@ -651,215 +580,187 @@ def delete_notice(request, notice_id):
return JsonResponse({"success": False, "error": "Invalid request method"})
-@login_required
-def student_dashboard(request):
- student = request.user.student
- today = timezone.now().date()
-
- # Get attendance summary
- attendance_records = AttendanceRecord.objects.filter(student=student)
- total_classes = attendance_records.count()
- attended_classes = attendance_records.filter(student_attend=True).count()
- attendance_percentage = (attended_classes / total_classes * 100) if total_classes > 0 else 0
-
- # Calculate current streak
- current_streak = 0
- longest_streak = 0
- temp_streak = 1
-
- for record in attendance_records.order_by('-attendance__date'):
- if record.student_attend:
- current_streak += 1
- temp_streak += 1
- longest_streak = max(longest_streak, temp_streak)
- else:
- break
-
- attendance_summary = {
- 'total_classes': total_classes,
- 'attendance_percentage': attendance_percentage,
- 'current_streak': current_streak,
- 'longest_streak': longest_streak
- }
-
- # Get course tracking summary
- course_trackings = CourseTracking.objects.filter(student=student)
- course_summary = {
- 'total_courses': course_trackings.count(),
- 'completed_courses': course_trackings.filter(progress_status='Completed').count(),
- 'ongoing_courses': course_trackings.filter(progress_status='In Progress').count(),
- 'completion_percentage': (
- sum(tracking.completion_percentage for tracking in course_trackings) / course_trackings.count()
- if course_trackings.exists() else 0
- )
- }
-
- # Get recent leaves
- recent_leaves = Student_Leave.objects.filter(student=student).order_by('-created_at')[:5]
-
- # Get recent feedback
- recent_feedback = StudentFeedback.objects.filter(student=student).order_by('-created_at')[:5]
-
- # Get notices
- notices = Notice.objects.all().order_by('-created_at')[:5]
-
- # Get attendance records
- attendance_records = AttendanceRecord.objects.filter(student=student).order_by('-attendance__date')[:5]
-
- # Get leave counts
- pending_leaves = Student_Leave.objects.filter(student=student, status=0).count()
- approved_leaves = Student_Leave.objects.filter(student=student, status=1).count()
- rejected_leaves = Student_Leave.objects.filter(student=student, status=2).count()
-
- # Get course details from course tracking
- course_details = []
- for tracking in course_trackings:
- course_info = {
- 'name': tracking.course.name,
- 'start_date': tracking.start_date,
- 'duration': tracking.course.duration,
- 'duration_type': tracking.course.duration_type,
- 'current_semester': tracking.current_semester,
- 'progress_status': tracking.progress_status,
- 'completion_percentage': tracking.completion_percentage,
- 'semester_start_date': tracking.semester_start_date,
- 'semester_end_date': tracking.semester_end_date,
- 'expected_end_date': tracking.expected_end_date,
- 'actual_end_date': tracking.actual_end_date,
- 'remaining_days': tracking.remaining_days,
- 'total_remaining_days': tracking.total_remaining_days
- }
- course_details.append(course_info)
-
- context = {
- 'student': student,
- 'attendance_summary': attendance_summary,
- 'course_summary': course_summary,
- 'recent_leaves': recent_leaves,
- 'recent_feedback': recent_feedback,
- 'notices': notices,
- 'attendance_records': attendance_records,
- 'pending_leaves': pending_leaves,
- 'approved_leaves': approved_leaves,
- 'rejected_leaves': rejected_leaves,
- 'course_details': course_details,
- }
- return render(request, 'student_dashboard.html', context)
-
-@login_required
-def teacher_dashboard(request):
- teacher = request.user.teacher
- today = timezone.now().date()
-
- # Get today's routines
- today_routines = Routine.objects.filter(
- teacher=teacher,
- day_of_week=today.weekday()
- ).order_by('start_time')
-
- # Get attendance summary
- attendance_records = Attendance.objects.filter(routine__teacher=teacher)
- total_classes = attendance_records.count()
- total_students = AttendanceRecord.objects.filter(attendance__routine__teacher=teacher).count()
- present_students = AttendanceRecord.objects.filter(
- attendance__routine__teacher=teacher,
- student_attend=True
- ).count()
-
- attendance_summary = {
- 'total_classes': total_classes,
- 'total_students': total_students,
- 'present_students': present_students,
- 'attendance_percentage': (present_students / total_students * 100) if total_students > 0 else 0
- }
-
- # Get recent leaves
- recent_leaves = Student_Leave.objects.filter(
- Q(status=0) | Q(status=1)
- ).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 attendance records
- attendance_records = Attendance.objects.filter(routine__teacher=teacher).order_by('-date')[:5]
-
- # Get leave counts
- pending_leaves = Student_Leave.objects.filter(status=0).count()
- approved_leaves = Student_Leave.objects.filter(status=1).count()
- rejected_leaves = Student_Leave.objects.filter(status=2).count()
-
- context = {
- 'teacher': teacher,
- 'today_routines': today_routines,
- 'attendance_summary': attendance_summary,
- 'recent_leaves': recent_leaves,
- 'recent_feedback': recent_feedback,
- 'notices': notices,
- 'attendance_records': attendance_records,
- 'pending_leaves': pending_leaves,
- 'approved_leaves': approved_leaves,
- 'rejected_leaves': rejected_leaves,
- }
- return render(request, 'teacher_dashboard.html', context)
-
@login_required
@require_POST
def save_fcm_token(request):
try:
+ print("Received FCM token save request")
data = json.loads(request.body)
- token = data.get('token')
-
+ token = data.get("token")
+ timestamp = data.get("timestamp")
+ vapid_key_used = data.get("vapidKeyUsed", False)
+
+ # Log additional information for debugging
+ auth_header = request.headers.get("Authorization", "")
+ using_vapid_auth = auth_header.startswith("VAPID ")
+
+ print(f"Auth method: {'VAPID' if using_vapid_auth else 'Cookie-based'}")
+ print(f"VAPID key used for token generation: {vapid_key_used}")
+
if not token:
- return JsonResponse({'error': 'Token is required'}, status=400)
-
+ print("No token provided in request")
+ return JsonResponse({"error": "Token is required"}, status=400)
+
user = request.user
+ print(f"User authenticated: {user.id} ({user.username})")
user_groups = user.groups.values_list("name", flat=True)
-
- # Save token to the appropriate user model
- if "Student" in user_groups:
- user.fcm_token = token
- user.save()
+
+ print(f"FCM token saving for user: {user.id}, groups: {list(user_groups)}")
+
+ # Determine user type based on group membership
+ user_type = "unknown"
+ if user.is_superuser:
+ user_type = "admin"
+ elif "Student" in user_groups:
+ user_type = "student"
elif "Teacher" in user_groups:
- user.fcm_token = token
- user.save()
-
+ user_type = "teacher"
+ elif "Parent" in user_groups:
+ user_type = "parent"
+
+ # Save token directly to the user model
+ user.fcm_token = token
+ user.save(update_fields=["fcm_token"])
+ print(f"FCM token saved directly to user {user.name}")
+
# Update or create FCMDevice
- device, created = FCMDevice.objects.update_or_create(
- token=token,
- defaults={'is_active': True}
+ try:
+ # Check if this is a fallback token
+ is_fallback = token.startswith("fcm-token-") or token.startswith(
+ "fallback-token-"
+ )
+
+ # Create or update the device
+ device, created = FCMDevice.objects.update_or_create(
+ token=token,
+ defaults={
+ "is_active": True,
+ "is_fallback": is_fallback,
+ "user_type": user_type,
+ },
+ )
+
+ print(
+ f"FCMDevice {'created' if created else 'updated'} with ID: {device.id}, user_type: {user_type}"
+ )
+ except Exception as e:
+ print(f"Error creating/updating FCMDevice: {str(e)}")
+ raise
+
+ return JsonResponse(
+ {
+ "status": "success",
+ "device_id": device.id,
+ "created": created,
+ "auth_method": "vapid" if using_vapid_auth else "cookie",
+ "user_type": user_type,
+ }
)
-
- return JsonResponse({
- 'status': 'success',
- 'device_id': device.id,
- 'created': created
- })
except json.JSONDecodeError:
- return JsonResponse({'error': 'Invalid JSON'}, status=400)
+ print("Invalid JSON in request body")
+ return JsonResponse({"error": "Invalid JSON"}, status=400)
except Exception as e:
- return JsonResponse({'error': str(e)}, status=500)
-
+ print(f"Error in save_fcm_token: {str(e)}")
+ return JsonResponse({"error": str(e)}, status=500)
+
+
def serve_firebase_sw(request):
- """Serve Firebase service worker with correct MIME type"""
-
- # Get the path to the service worker in the static directory
- sw_path = os.path.join(settings.STATICFILES_DIRS[0], 'firebase-messaging-sw.js')
-
- # If the file doesn't exist in static directory, try staticfiles
- if not os.path.exists(sw_path):
- sw_path = os.path.join(settings.STATIC_ROOT, 'firebase-messaging-sw.js')
-
- response = serve(request, 'firebase-messaging-sw.js', os.path.dirname(sw_path))
- response['Content-Type'] = 'application/javascript'
- return response
+ """Serve the Firebase service worker file with correct content type and security headers"""
+ try:
+ # Get the path to the service worker in the static directory
+ sw_path = os.path.join(settings.STATICFILES_DIRS[0], "firebase-messaging-sw.js")
+
+ # If the file doesn't exist in static directory, try staticfiles
+ if not os.path.exists(sw_path):
+ sw_path = os.path.join(settings.STATIC_ROOT, "firebase-messaging-sw.js")
+
+ if not os.path.exists(sw_path):
+ print(f"Service worker file not found at: {sw_path}")
+ return HttpResponse("Service worker file not found", status=404)
+
+ with open(sw_path, "r") as f:
+ content = f.read()
+
+ # Create response with correct content type
+ response = HttpResponse(content, content_type="application/javascript")
+
+ # Set all necessary security headers for service worker
+ response["Service-Worker-Allowed"] = "/"
+ response["Cache-Control"] = "no-cache, no-store, must-revalidate"
+ response["Pragma"] = "no-cache"
+ response["Expires"] = "0"
+
+ # Set proper CORS headers for service worker
+ response["Access-Control-Allow-Origin"] = "*"
+ response["Access-Control-Allow-Methods"] = "GET, OPTIONS"
+ response["Access-Control-Allow-Headers"] = "Content-Type"
+
+ print(f"Serving service worker file successfully, size: {len(content)}")
+ return response
+ except Exception as e:
+ print(f"Error serving service worker: {str(e)}")
+ return HttpResponse("Internal server error", status=500)
+
@login_required
def check_weak_password(request):
"""Check if the current user's password is '123'"""
user = request.user
- is_weak = check_password('123', user.password)
- return JsonResponse({'is_weak': is_weak})
+ is_weak = check_password("123", user.password)
+ return JsonResponse({"is_weak": is_weak})
+
+
+@login_required
+def get_staff_leaves(request):
+ """View to get staff leave requests for AJAX updates"""
+ staff_leaves = StaffLeave.objects.filter(status=0).order_by("-created_at")[:5]
+ leaves_data = []
+ for leave in staff_leaves:
+ leaves_data.append(
+ {
+ "id": leave.id,
+ "staff_name": leave.staff.name,
+ "start_date": leave.start_date.strftime("%b %d, %Y"),
+ "end_date": leave.end_date.strftime("%b %d, %Y"),
+ "message": leave.message[:50] + "..."
+ if len(leave.message) > 50
+ else leave.message,
+ }
+ )
+ return JsonResponse({"staff_leaves": leaves_data})
+
+
+@login_required
+def get_student_leaves(request):
+ """View to get student leave requests for AJAX updates"""
+ student_leaves = StudentLeave.objects.filter(status=0).order_by("-created_at")[:5]
+ leaves_data = []
+ for leave in student_leaves:
+ leaves_data.append(
+ {
+ "id": leave.id,
+ "student_name": leave.student.name,
+ "start_date": leave.start_date.strftime("%b %d, %Y"),
+ "end_date": leave.end_date.strftime("%b %d, %Y"),
+ "message": leave.message[:50] + "..."
+ if len(leave.message) > 50
+ else leave.message,
+ }
+ )
+ return JsonResponse({"student_leaves": leaves_data})
+
+
+@login_required
+@require_GET
+def get_meeting_cancellation_reason(request, meeting_id):
+ """API endpoint to get the cancellation reason for a meeting"""
+ try:
+ meeting = get_object_or_404(TeacherParentMeeting, id=meeting_id)
+
+ if meeting.status != "cancelled":
+ return JsonResponse({"success": False, "error": "Meeting is not cancelled"})
+
+ return JsonResponse({"success": True, "reason": meeting.cancellation_reason})
+
+ except Exception as e:
+ return JsonResponse({"success": False, "error": str(e)})
diff --git a/firebase-key.json.example b/firebase-key.json.example
new file mode 100644
index 0000000..27e9030
--- /dev/null
+++ b/firebase-key.json.example
@@ -0,0 +1,13 @@
+{
+ "type": "service_account",
+ "project_id": "your-project-id",
+ "private_key_id": "your-private-key-id",
+ "private_key": "-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY_CONTENT\n-----END PRIVATE KEY-----\n",
+ "client_email": "your-service-account-email@your-project-id.iam.gserviceaccount.com",
+ "client_id": "your-client-id",
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+ "token_uri": "https://oauth2.googleapis.com/token",
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+ "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/your-service-account-email%40your-project-id.iam.gserviceaccount.com",
+ "universe_domain": "googleapis.com"
+}
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 7b1b727..21afcce 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,7 +8,9 @@ cryptography==44.0.2
Django==5.2
django-jazzmin==3.0.1
django-mathfilters==1.0.0
+django-model-utils==5.0.0
djangorestframework==3.16.0
+Faker==37.1.0
firebase-admin==6.7.0
google-api-core==2.24.2
google-api-python-client==2.166.0
@@ -22,9 +24,11 @@ google-resumable-media==2.7.2
googleapis-common-protos==1.69.2
grpcio==1.71.0
grpcio-status==1.71.0
+gunicorn==23.0.0
httplib2==0.22.0
idna==3.10
msgpack==1.1.0
+packaging==24.2
pillow==11.1.0
proto-plus==1.26.1
protobuf==5.29.4
@@ -39,6 +43,7 @@ pytz==2025.2
requests==2.32.3
rsa==4.9
sqlparse==0.5.3
+tzdata==2025.2
uritemplate==4.1.1
urllib3==2.3.0
whitenoise==6.9.0
diff --git a/static/css/base.css b/static/css/base.css
index 38715b4..bc1ec3c 100644
--- a/static/css/base.css
+++ b/static/css/base.css
@@ -574,4 +574,101 @@ body {
a[href]:after {
content: " (" attr(href) ")";
}
+}
+
+/* Responsive Form Styles */
+@media (max-width: 768px) {
+ .form-group {
+ margin-bottom: 1rem;
+ }
+
+ .form-control, .form-select {
+ font-size: 16px; /* Prevents iOS zoom */
+ height: auto;
+ padding: 0.5rem 0.75rem;
+ }
+
+ .input-group {
+ flex-wrap: wrap;
+ }
+
+ .input-group > .form-control,
+ .input-group > .form-select {
+ flex: 1 1 auto;
+ width: auto;
+ }
+
+ .btn-block {
+ width: 100%;
+ }
+
+ textarea.form-control {
+ min-height: 100px;
+ }
+
+ label {
+ font-size: 0.9rem;
+ margin-bottom: 0.25rem;
+ }
+}
+
+@media (max-width: 576px) {
+ .form-row {
+ display: block;
+ }
+
+ .form-row > .col,
+ .form-row > [class*="col-"] {
+ padding-right: 0;
+ padding-left: 0;
+ }
+
+ .form-control, .form-select {
+ font-size: 16px;
+ padding: 0.4rem 0.6rem;
+ }
+
+ .form-check-input {
+ width: 1rem;
+ height: 1rem;
+ margin-top: 0.3em;
+ }
+
+ .form-check-label {
+ margin-left: 0.2rem;
+ }
+}
+
+/* Modal Responsive Fixes */
+@media (max-width: 576px) {
+ .modal-header {
+ padding: 0.75rem 1rem;
+ }
+
+ .modal-body {
+ padding: 1rem;
+ }
+
+ .modal-footer {
+ padding: 0.75rem 1rem;
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .modal-footer > * {
+ margin: 0.25rem 0;
+ }
+
+ /* Fix for iOS modals */
+ .ios-modal-fix {
+ position: absolute;
+ overflow: hidden;
+ }
+}
+
+/* Fix for notched phones */
+@supports (padding: max(0px)) {
+ .fixed-bottom {
+ padding-bottom: max(0.5rem, env(safe-area-inset-bottom));
+ }
}
\ No newline at end of file
diff --git a/static/css/custom.css b/static/css/custom.css
index 5ee0c89..7b43880 100644
--- a/static/css/custom.css
+++ b/static/css/custom.css
@@ -19,4 +19,3 @@ img[src*="/static/img/logo.png"] {
margin-right: .5rem;
margin-top: -3px;
}
-
diff --git a/static/css/dashboard.css b/static/css/dashboard.css
index 7990fc4..07d38f4 100644
--- a/static/css/dashboard.css
+++ b/static/css/dashboard.css
@@ -1152,4 +1152,106 @@ img.img-fluid {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
+}
+
+/* Meeting Section Styles */
+.meeting-card {
+ background: var(--card-bg);
+ border-radius: var(--radius-lg);
+ border: 1px solid var(--border-color);
+ transition: var(--transition);
+ overflow: hidden;
+}
+
+.meeting-card:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--hover-shadow);
+}
+
+.meeting-card .card-header {
+ background-color: var(--hover-bg);
+ padding: 1rem 1.25rem;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.meeting-card .card-header h5 {
+ color: var(--text-primary);
+ font-size: 0.875rem;
+ font-weight: 600;
+ margin: 0;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.meeting-card .card-body {
+ padding: 1.25rem;
+}
+
+.meeting-status {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.25rem 0.5rem;
+ border-radius: var(--radius-sm);
+ font-size: 0.75rem;
+ font-weight: 500;
+}
+
+.meeting-status.scheduled {
+ background-color: var(--primary-light);
+ color: var(--primary-dark);
+}
+
+.meeting-status.completed {
+ background-color: var(--success-light);
+ color: var(--success-dark);
+}
+
+.meeting-status.cancelled {
+ background-color: var(--danger-light);
+ color: var(--danger-dark);
+}
+
+.meeting-status.rescheduled {
+ background-color: var(--warning-light);
+ color: var(--warning-dark);
+}
+
+.meeting-actions {
+ display: flex;
+ gap: 0.5rem;
+ margin-top: 1rem;
+}
+
+.meeting-actions .btn {
+ padding: 0.375rem 0.75rem;
+ font-size: 0.875rem;
+}
+
+.meeting-actions .btn i {
+ margin-right: 0.25rem;
+}
+
+/* Responsive Meeting Section */
+@media (max-width: 768px) {
+ .meeting-card {
+ margin-bottom: 1rem;
+ }
+
+ .meeting-actions {
+ flex-wrap: wrap;
+ }
+
+ .meeting-actions .btn {
+ flex: 1 1 calc(50% - 0.5rem);
+ min-width: calc(50% - 0.5rem);
+ }
+}
+
+@media (max-width: 576px) {
+ .meeting-actions .btn {
+ flex: 1 1 100%;
+ min-width: 100%;
+ margin-bottom: 0.5rem;
+ }
}
\ No newline at end of file
diff --git a/static/firebase-messaging-sw.example.js b/static/firebase-messaging-sw.example.js
index 8acbfdd..994b40f 100644
--- a/static/firebase-messaging-sw.example.js
+++ b/static/firebase-messaging-sw.example.js
@@ -1,91 +1,143 @@
-importScripts(
- "https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js",
-);
-importScripts(
- "https://www.gstatic.com/firebasejs/9.22.0/firebase-messaging-compat.js",
-);
+// Version identifier for cache busting
+const SW_VERSION = '1.0.2';
+console.log(`Firebase Messaging SW Version ${SW_VERSION} starting...`);
-firebase.initializeApp({
- apiKey: "YOUR_API_KEY",
- authDomain: "YOUR_AUTH_DOMAIN",
- projectId: "YOUR_PROJECT_ID",
- storageBucket: "YOUR_STORAGE_BUCKET",
- messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
- appId: "YOUR_APP_ID",
- measurementId: "YOUR_MEASUREMENT_ID",
-});
+// Import Firebase scripts
+try {
+ importScripts('https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js');
+ importScripts('https://www.gstatic.com/firebasejs/9.22.0/firebase-messaging-compat.js');
+ console.log('Firebase scripts loaded successfully');
+} catch (e) {
+ console.error('Error loading Firebase scripts:', e);
+}
-const messaging = firebase.messaging();
+// Firebase configuration - this must match exactly with your main app configuration
+const firebaseConfig = {
+ apiKey: 'YOUR_API_KEY',
+ authDomain: 'YOUR_AUTH_DOMAIN',
+ projectId: 'YOUR_PROJECT_ID',
+ storageBucket: 'YOUR_STORAGE_BUCKET',
+ messagingSenderId: 'YOUR_MESSAGING_SENDER_ID',
+ appId: 'YOUR_APP_ID',
+ measurementId: "Your Measurement ID"
+};
-// Handle background messages
-messaging.onBackgroundMessage((payload) => {
- console.log("Received background message:", payload);
+// Handle any errors during initialization
+try {
+ // Initialize Firebase
+ firebase.initializeApp(firebaseConfig);
+ console.log('Firebase initialized in service worker');
+
+ const messaging = firebase.messaging();
+ console.log('Firebase messaging initialized');
- // Default notification options
- const notificationOptions = {
- body: "You have a new message.",
- icon: "/static/img/logo.png",
- badge: "/static/img/logo.png",
- timestamp: Date.now(),
- requireInteraction: true,
- vibrate: [200, 100, 200],
- tag: "notification-" + Date.now(),
- data: payload.data || {},
- actions: [
- {
- action: "open",
- title: "Open",
- },
- {
- action: "close",
- title: "Close",
- },
- ],
- };
+ // Handle background messages
+ messaging.onBackgroundMessage(function(payload) {
+ console.log('Received background message:', payload);
+
+ // Generate a unique ID for this notification
+ const notificationId = payload.data?.notificationId || `notification-${Date.now()}`;
+
+ // Check if we already have a notification with this ID
+ return self.registration.getNotifications({tag: notificationId})
+ .then(function(notifications) {
+ // Only show if no matching notification exists
+ if (notifications.length === 0) {
+ const notificationTitle = payload.notification?.title || 'New Notification';
+ const notificationOptions = {
+ body: payload.notification?.body || 'You have a new message.',
+ icon: '/static/img/logo.png',
+ badge: '/static/img/logo.png',
+ tag: notificationId, // Tag helps prevent duplicates
+ data: payload.data || {}
+ };
- // Set notification title and body based on payload
- let notificationTitle = "New Notification";
- if (payload.notification) {
- notificationTitle = payload.notification.title || notificationTitle;
- notificationOptions.body =
- payload.notification.body || notificationOptions.body;
- if (payload.notification.icon) {
- notificationOptions.icon = payload.notification.icon;
- }
- } else if (payload.data) {
- notificationTitle = payload.data.title || notificationTitle;
- notificationOptions.body = payload.data.body || notificationOptions.body;
- }
-
- // Show the notification
- return self.registration
- .showNotification(notificationTitle, notificationOptions)
- .catch((error) => {
- console.error("Error showing notification:", error);
+ return self.registration.showNotification(notificationTitle, notificationOptions);
+ } else {
+ console.log('Prevented duplicate background notification', notificationId);
+ return Promise.resolve();
+ }
+ });
});
+} catch (error) {
+ console.error('Firebase messaging initialization error:', error);
+}
+
+// Improve service worker lifecycle for better compatibility
+self.addEventListener('install', function(event) {
+ console.log('Service Worker installed');
+ self.skipWaiting();
});
-// Handle notification click
-self.addEventListener("notificationclick", (event) => {
- console.log("Notification clicked:", event);
-
- event.notification.close();
-
- if (event.action === "open") {
- // Handle open action
- const urlToOpen = event.notification.data.url || "/app/dashboard/";
- event.waitUntil(clients.openWindow(urlToOpen));
- }
+self.addEventListener('activate', function(event) {
+ console.log('Service Worker activated');
+ event.waitUntil(self.clients.claim());
});
-// Handle service worker installation
-self.addEventListener("install", (event) => {
- console.log("Service Worker installed");
- self.skipWaiting();
+// Add special handling for push events
+self.addEventListener('push', function(event) {
+ console.log('Push event received in service worker', event);
+
+ if (!event.data) {
+ console.log('Push event has no data');
+ return;
+ }
+
+ try {
+ const payload = event.data.json();
+ console.log('Push data:', payload);
+
+ // Generate a unique ID for this notification
+ const notificationId = payload.data?.notificationId || `push-${Date.now()}`;
+
+ event.waitUntil(
+ // Check for existing notifications first
+ self.registration.getNotifications({tag: notificationId})
+ .then(function(notifications) {
+ // Only show if no matching notification exists
+ if (notifications.length === 0) {
+ const title = payload.notification?.title || 'New Notification';
+ const options = {
+ body: payload.notification?.body || 'You have a new message',
+ icon: '/static/img/logo.png',
+ badge: '/static/img/logo.png',
+ tag: notificationId, // Add tag to identify this notification
+ data: payload.data || {}
+ };
+
+ return self.registration.showNotification(title, options);
+ } else {
+ console.log('Prevented duplicate push notification', notificationId);
+ return Promise.resolve();
+ }
+ })
+ );
+ } catch (error) {
+ console.error('Error handling push event:', error);
+ }
});
-// Handle service worker activation
-self.addEventListener("activate", (event) => {
- console.log("Service Worker activated");
- event.waitUntil(clients.claim());
+// Improve notification click handling for various environments
+self.addEventListener('notificationclick', function(event) {
+ console.log('Notification clicked:', event.notification);
+ event.notification.close();
+
+ const urlToOpen = event.notification.data?.url || '/app/dashboard/';
+
+ event.waitUntil(
+ clients.matchAll({type: 'window', includeUncontrolled: true}).then(windowClients => {
+ // Check if there is already a window open with the target URL
+ for (let i = 0; i < windowClients.length; i++) {
+ const client = windowClients[i];
+ // If so, focus on that window
+ if (client.url === urlToOpen && 'focus' in client) {
+ return client.focus();
+ }
+ }
+ // If not, open a new window
+ if (clients.openWindow) {
+ return clients.openWindow(urlToOpen);
+ }
+ })
+ );
});
diff --git a/static/js/admin_dashboard.js b/static/js/admin_dashboard.js
new file mode 100644
index 0000000..c6cafa9
--- /dev/null
+++ b/static/js/admin_dashboard.js
@@ -0,0 +1,964 @@
+// Function to update leave requests
+function updateLeaveRequests() {
+ // Fetch staff leave requests
+ fetch('/app/get-staff-leaves/')
+ .then(response => response.json())
+ .then(data => {
+ const staffLeavesContainer = document.querySelector('#staff-leaves-container');
+ if (data.staff_leaves.length === 0) {
+ staffLeavesContainer.innerHTML = `
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/505.html b/templates/505.html
new file mode 100644
index 0000000..7def811
--- /dev/null
+++ b/templates/505.html
@@ -0,0 +1,23 @@
+{% extends "shared/base.html" %}
+
+{% block title %}505 HTTP Version Not Supported{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
505
+
HTTP Version Not Supported
+
Sorry, the HTTP version used in the request is not supported by this server.
+
+
The server does not support the HTTP protocol version used in the request. Please try accessing the site using a different browser or updating your current one.
+{% endblock %}
\ No newline at end of file
diff --git a/templates/admin/app/routine/change_form.html b/templates/admin/app/routine/change_form.html
index dc240da..f2fab86 100644
--- a/templates/admin/app/routine/change_form.html
+++ b/templates/admin/app/routine/change_form.html
@@ -186,8 +186,8 @@
-
-
@@ -209,7 +209,7 @@
- Please select course and semester
+ Please select course and period
+
+
\ No newline at end of file
diff --git a/templates/hod/modals/add_routine_modal.html b/templates/hod/modals/add_routine_modal.html
new file mode 100644
index 0000000..de7ed8f
--- /dev/null
+++ b/templates/hod/modals/add_routine_modal.html
@@ -0,0 +1,364 @@
+{% load static %}
+
+
+
+
+
+
Add Class Routine
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/hod/modals/add_staff_modal.html b/templates/hod/modals/add_staff_modal.html
new file mode 100644
index 0000000..2df536d
--- /dev/null
+++ b/templates/hod/modals/add_staff_modal.html
@@ -0,0 +1,126 @@
+{% load static %}
+
+
+
+
+
Add New Staff
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/hod/modals/add_student_modal.html b/templates/hod/modals/add_student_modal.html
new file mode 100644
index 0000000..c909aaf
--- /dev/null
+++ b/templates/hod/modals/add_student_modal.html
@@ -0,0 +1,145 @@
+{% load static %}
+
+
+
+
+
+
Add New Student
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/hod/modals/add_subject_modal.html b/templates/hod/modals/add_subject_modal.html
new file mode 100644
index 0000000..99f17b2
--- /dev/null
+++ b/templates/hod/modals/add_subject_modal.html
@@ -0,0 +1,106 @@
+{% load static %}
+{% load app_filters %}
+
+
+
+
+
+
Add New Subject
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/hod/modals/cancel_meeting_modal.html b/templates/hod/modals/cancel_meeting_modal.html
new file mode 100644
index 0000000..bb20057
--- /dev/null
+++ b/templates/hod/modals/cancel_meeting_modal.html
@@ -0,0 +1,23 @@
+
+
+
+
+
Cancel Meeting
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/hod/modals/cancellation_reason_modal.html b/templates/hod/modals/cancellation_reason_modal.html
new file mode 100644
index 0000000..4371719
--- /dev/null
+++ b/templates/hod/modals/cancellation_reason_modal.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
Cancellation Reason
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/hod/modals/edit_meeting_modal.html b/templates/hod/modals/edit_meeting_modal.html
new file mode 100644
index 0000000..7965993
--- /dev/null
+++ b/templates/hod/modals/edit_meeting_modal.html
@@ -0,0 +1,79 @@
+{% load static %}
+
+
+
+
+
+
Edit Meeting
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/hod/modals/edit_progress_modal.html b/templates/hod/modals/edit_progress_modal.html
new file mode 100644
index 0000000..138e78b
--- /dev/null
+++ b/templates/hod/modals/edit_progress_modal.html
@@ -0,0 +1,211 @@
+{% load static %}
+
+
+
+
+
+
Edit Student Progress
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/hod/modals/edit_routine_modal.html b/templates/hod/modals/edit_routine_modal.html
new file mode 100644
index 0000000..3d26bcb
--- /dev/null
+++ b/templates/hod/modals/edit_routine_modal.html
@@ -0,0 +1,267 @@
+{% load static %}
+
+
+
+
+
+
Edit Class Routine
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/hod/modals/edit_staff_modal.html b/templates/hod/modals/edit_staff_modal.html
new file mode 100644
index 0000000..bd0d25d
--- /dev/null
+++ b/templates/hod/modals/edit_staff_modal.html
@@ -0,0 +1,202 @@
+{% load static %}
+
+
+
+
+
Edit Staff
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/hod/modals/edit_student_modal.html b/templates/hod/modals/edit_student_modal.html
new file mode 100644
index 0000000..f459d76
--- /dev/null
+++ b/templates/hod/modals/edit_student_modal.html
@@ -0,0 +1,206 @@
+{% load static %}
+
+
+
+
+
+
Edit Student
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/hod/modals/edit_subject_modal.html b/templates/hod/modals/edit_subject_modal.html
new file mode 100644
index 0000000..305c51a
--- /dev/null
+++ b/templates/hod/modals/edit_subject_modal.html
@@ -0,0 +1,128 @@
+{% load static %}
+{% load app_filters %}
+
+
+
+
+
+
Edit Subject
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/hod/modals/meeting_agenda_modal.html b/templates/hod/modals/meeting_agenda_modal.html
new file mode 100644
index 0000000..5d6cca8
--- /dev/null
+++ b/templates/hod/modals/meeting_agenda_modal.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
Meeting Agenda
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/hod/modals/meeting_notes_modal.html b/templates/hod/modals/meeting_notes_modal.html
new file mode 100644
index 0000000..80aacc4
--- /dev/null
+++ b/templates/hod/modals/meeting_notes_modal.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
Meeting Notes
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/hod/modals/notice_modal.html b/templates/hod/modals/notice_modal.html
new file mode 100644
index 0000000..ed3bfb2
--- /dev/null
+++ b/templates/hod/modals/notice_modal.html
@@ -0,0 +1,167 @@
+{% load static %}
+
+
+
Are you sure you want to delete this notice? This action cannot be undone.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/hod/modals/routine_modal.html b/templates/hod/modals/routine_modal.html
new file mode 100644
index 0000000..29acf0a
--- /dev/null
+++ b/templates/hod/modals/routine_modal.html
@@ -0,0 +1,125 @@
+
+
+
+
+
Add Class Routine
+ ×
+
+
+
+
+
+
+
+
+
+
+
+
+
Edit Class Routine
+ ×
+
+
+
+
+
+
+
+
+
+
+
+
+
Confirm Delete
+ ×
+
+
+
Are you sure you want to delete this routine? This action cannot be undone.
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/hod/modals/view_meeting_modal.html b/templates/hod/modals/view_meeting_modal.html
new file mode 100644
index 0000000..89aea0d
--- /dev/null
+++ b/templates/hod/modals/view_meeting_modal.html
@@ -0,0 +1,74 @@
+{% load static %}
+
+
+
+
+
+
Meeting Details
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
minutes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/hod/modals/view_staff_modal.html b/templates/hod/modals/view_staff_modal.html
new file mode 100644
index 0000000..d1c8a12
--- /dev/null
+++ b/templates/hod/modals/view_staff_modal.html
@@ -0,0 +1,165 @@
+{% load static %}
+
+
+
+
+
Staff Details
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/hod/modals/view_student_modal.html b/templates/hod/modals/view_student_modal.html
new file mode 100644
index 0000000..cf94974
--- /dev/null
+++ b/templates/hod/modals/view_student_modal.html
@@ -0,0 +1,168 @@
+{% load static %}
+
+
+
+
+
+
Student Details
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/hod/modals/view_subject_modal.html b/templates/hod/modals/view_subject_modal.html
new file mode 100644
index 0000000..121dffa
--- /dev/null
+++ b/templates/hod/modals/view_subject_modal.html
@@ -0,0 +1,83 @@
+
+
+
+
+
Subject Details
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/login/email_reset.html b/templates/login/email_reset.html
index 71a32ac..01647cc 100644
--- a/templates/login/email_reset.html
+++ b/templates/login/email_reset.html
@@ -1,9 +1,13 @@
-{% extends "base.html" %}
+{% extends "shared/base.html" %}
{% load static %}
{% block content %}
+ {% if institute and institute.logo %}
+
+ {% else %}
+ {% endif %}
Reset Password
Enter your email address and we'll send you a verification code to reset your password.
+{% endblock %}
+
+{% block extra_css %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/parent/modals/cancellation_reason_modal.html b/templates/parent/modals/cancellation_reason_modal.html
new file mode 100644
index 0000000..e07bc81
--- /dev/null
+++ b/templates/parent/modals/cancellation_reason_modal.html
@@ -0,0 +1,12 @@
+
+
+
+
+
Cancellation Reason
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/parent/modals/feedback_modal.html b/templates/parent/modals/feedback_modal.html
new file mode 100644
index 0000000..11c54df
--- /dev/null
+++ b/templates/parent/modals/feedback_modal.html
@@ -0,0 +1,245 @@
+
+
+
+
+
Give Feedback
+
+
+
+
+
+
+ Teacher Feedback
+
+
+ Institute Feedback
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/parent/modals/meeting_agenda_modal.html b/templates/parent/modals/meeting_agenda_modal.html
new file mode 100644
index 0000000..1417cb6
--- /dev/null
+++ b/templates/parent/modals/meeting_agenda_modal.html
@@ -0,0 +1,12 @@
+
+
+
+
+
Meeting Agenda
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/parent/modals/meeting_notes_modal.html b/templates/parent/modals/meeting_notes_modal.html
new file mode 100644
index 0000000..52a635e
--- /dev/null
+++ b/templates/parent/modals/meeting_notes_modal.html
@@ -0,0 +1,12 @@
+
+
+
+
+
Meeting Notes
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/parent/modals/student_details_modal.html b/templates/parent/modals/student_details_modal.html
new file mode 100644
index 0000000..458fea5
--- /dev/null
+++ b/templates/parent/modals/student_details_modal.html
@@ -0,0 +1,132 @@
+
+
+
+
+
Student Details
+
+
+
+
+
+
+ Loading...
+
+
Loading student details...
+
+
+
+
+
+
+
+
Basic Information
+
+
+
+
+
+
+
Student Name
+
Course
+
+
+
+
+
ID
+
-
+
+
+
Status
+
-
+
+
+
Current Period
+
-
+
+
+
Attendance
+
-
+
+
+
+
+
+
+
+
+
+
+
Contact Information
+
+
+
+
+
Email
+
-
+
+
+
Phone
+
-
+
+
+
Address
+
-
+
+
+
+
+
+
+
+
+
+
+
Course Timeline
+
+
+
+
+
Course Name
+
-
+
+
+
Course Code
+
-
+
+
+
Course Status
+
-
+
+
+
Current Period
+
-
+
+
+
+
+
Course Description
+
-
+
+
+
+
+
Current Period End Date
+
-
+
+
+
Expected Course Completion
+
-
+
+
+
Progress
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/parent/parent_feedback.html b/templates/parent/parent_feedback.html
new file mode 100644
index 0000000..42dd8e7
--- /dev/null
+++ b/templates/parent/parent_feedback.html
@@ -0,0 +1,132 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block content %}
+