+[](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.
-
-
+
+
+| Feature | Description |
+|---------|-------------|
+| ๐ฅ Multi-User Roles | Admin, Staff, and Student interfaces |
+| ๐ Course Management | Comprehensive course and subject tracking |
+| ๐ Attendance System | Real-time attendance monitoring |
+| ๐ Leave Management | Streamlined leave application process |
+| ๐ข Notice Board | Instant announcements and updates |
+| ๐ฌ Feedback System | Student and staff feedback collection |
+| ๐ OTP Verification | Secure authentication system |
+| โ๏ธ Cloud Storage | Google Cloud integration |
+| ๐ฅ Firebase | Real-time notifications and auth |
+
+
+
+## ๐ Quick Start
+
+### Linux Users
+```bash
+# Clone & Setup
+git clone https://github.com/S4NKALP/Student-Management-System-In-Django.git
+cd Student-Management-System-In-Django
+python -m venv venv && source venv/bin/activate
+pip install -r requirements.txt
+
+# Configure
+cp .env.example .env # Edit with your settings
+
+# Run
+python manage.py migrate
+python manage.py createsuperuser
+python manage.py runserver
+```
+
+### Windows Users
+```powershell
+# Prerequisites
+# Install Python from https://www.python.org/downloads/
+# Install Git from https://git-scm.com/download/win
+# Install Visual C++ Build Tools: https://visualstudio.microsoft.com/visual-cpp-build-tools/
+
+# Clone & Setup
+git clone https://github.com/S4NKALP/Student-Management-System-In-Django.git
+cd Student-Management-System-In-Django
+python -m venv venv
+.\venv\Scripts\activate
+pip install -r requirements.txt
+
+# Configure
+copy .env.example .env
+# Edit .env file with your settings
+
+# Run
+python manage.py migrate
+python manage.py createsuperuser
+python manage.py runserver
+```
+
+### โ ๏ธ Common Issues (Windows)
+- If `pip install` fails, make sure you have Visual C++ Build Tools installed
+- If `python` command not found, add Python to your PATH environment variable
+- If permission errors occur, run Command Prompt as Administrator
+- If virtualenv fails, try: `python -m pip install --upgrade virtualenv`
+
+### ๐ Environment Variables (.env)
+```env
+# Django Settings
+DEBUG=True
+SECRET_KEY=your-secret-key-here
+ALLOWED_HOSTS=localhost,127.0.0.1
+
+# Database Configuration
+DB_NAME=sms_db
+DB_USER=your_db_user
+DB_PASSWORD=your_db_password
+DB_HOST=localhost
+DB_PORT=3306
+
+# Email Configuration (for OTP)
+EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
+EMAIL_HOST=smtp.gmail.com
+EMAIL_PORT=587
+EMAIL_USE_TLS=True
+EMAIL_HOST_USER=your-email@gmail.com
+EMAIL_HOST_PASSWORD=your-app-specific-password
+
+# Firebase Configuration
+FIREBASE_DATABASE_URL=your-firebase-url
+FIREBASE_TYPE=service_account
+FIREBASE_PROJECT_ID=your-project-id
+FIREBASE_PRIVATE_KEY_ID=your-private-key-id
+FIREBASE_PRIVATE_KEY=your-private-key
+FIREBASE_CLIENT_EMAIL=your-client-email
+FIREBASE_CLIENT_ID=your-client-id
+FIREBASE_AUTH_URI=https://accounts.google.com/o/oauth2/auth
+FIREBASE_TOKEN_URI=https://oauth2.googleapis.com/token
+FIREBASE_AUTH_PROVIDER_X509_CERT_URL=https://www.googleapis.com/oauth2/v1/certs
+FIREBASE_CLIENT_X509_CERT_URL=your-cert-url
+
+# Google Cloud Storage
+GS_BUCKET_NAME=your-bucket-name
+GS_CREDENTIALS=path/to/credentials.json
+```
+
+> **Note**: Never commit your `.env` file to version control. Keep your credentials secure!
+
+## ๐ฏ Project Roadmap
+
+
+
+### โ ๏ธ Important Notice
+```
+This is a Minor Project created for educational purposes.
+Features listed in the roadmap may or may not be implemented.
+```
+
+
+
+## ๐ Credits
+
+If you use this project in your work, please give credit where credit is due:
+
+```markdown
+Student Management System
+Created by Sankalp
+GitHub: https://github.com/S4NKALP
+```
+
+### ๐ Citation
+If you're using this project in an academic context, you can cite it as:
+
+```bibtex
+@software{StudentManagementSystem2024,
+ author = {Sankalp},
+ title = {Student Management System},
+ year = {2024},
+ publisher = {GitHub},
+ url = {https://github.com/S4NKALP/Student-Management-System-In-Django}
+}
+```
+
+### ๐ค Acknowledgments
+- Thanks to all contributors who have helped improve this project
+- Special thanks to the open-source community for their valuable resources
+- Appreciation to all users who have provided feedback and suggestions
+
+## ๐ License
+
+
+
+### Made with โค๏ธ by [Sankalp](https://github.com/S4NKALP)
+
+[](https://github.com/S4NKALP)
+
+
+
diff --git a/app/admin.py b/app/admin.py
index 4448edb..9de4070 100644
--- a/app/admin.py
+++ b/app/admin.py
@@ -10,17 +10,25 @@ from app.models import (
AttendanceRecord,
Notice,
Routine,
- StudentRoutine,
Staff_leave,
Student_Leave,
StudentFeedback,
CourseTracking,
InstituteFeedback,
+ StaffInstituteFeedback,
+ FCMDevice,
)
from django.forms import ModelForm
from django.contrib.auth.models import User, Group
from django.contrib.auth.admin import UserAdmin, GroupAdmin
from app.forms import StaffAdminForm, StudentAdminForm
+from django.urls import path
+from django.http import JsonResponse
+import json
+from django.db import transaction
+from django.db.models import Q
+from datetime import datetime
+
# Register your models here.
@@ -39,7 +47,6 @@ class CustomAdminSite(admin.AdminSite):
"group": "Authentication",
"group_label": "authentication",
},
-
# Organization Models
"Institute": {
"group": "Organization",
@@ -61,7 +68,6 @@ class CustomAdminSite(admin.AdminSite):
"group": "Organization",
"group_label": "organization",
},
-
# Human Management Models
"Student": {
"group": "Human Management",
@@ -99,6 +105,10 @@ class CustomAdminSite(admin.AdminSite):
"group": "Human Management",
"group_label": "human_management",
},
+ "StaffInstituteFeedback": {
+ "group": "Human Management",
+ "group_label": "human_management",
+ },
}
grouped_app_list = [{"name": "Main", "app_label": "dashboard", "models": []}]
@@ -117,7 +127,21 @@ class CustomAdminSite(admin.AdminSite):
}
grouped_models[group]["models"].append(model)
- grouped_app_list = list(grouped_models.values())
+ dashboard_item = {
+ "name": "Dashboard",
+ "app_label": "dashboard",
+ "models": [
+ {
+ "name": "Dashboard",
+ "object_name": "Dashboard",
+ "perms": {"has_module_perms": True},
+ "admin_url": "/app/dashboard",
+ "view_only": True,
+ }
+ ],
+ }
+
+ grouped_app_list = [dashboard_item] + list(grouped_models.values())
return grouped_app_list
@@ -136,11 +160,29 @@ class InstituteModel(admin.ModelAdmin):
@admin.register(Course, site=custom_admin_site)
class CourseModel(admin.ModelAdmin):
- list_display = ("id", "name", "duration", "duration_type")
+ list_display = ("id", "name", "duration", "duration_type", "get_total_duration")
search_fields = ("name", "duration", "duration_type")
- list_filter = ("name", "duration", "duration_type")
+ list_filter = ("duration_type", "duration")
advanced_filter_fields = ("name", "duration", "duration_type")
+ def get_total_duration(self, obj):
+ if obj.duration_type == "Semester":
+ return f"{obj.duration} years ({obj.duration * 2} semesters)"
+ return f"{obj.duration} year{'s' if obj.duration > 1 else ''}"
+
+ get_total_duration.short_description = "Total Duration"
+
+ fieldsets = (
+ (None, {"fields": ("name",)}),
+ (
+ "Duration Settings",
+ {
+ "fields": ("duration", "duration_type"),
+ "description": "For yearly courses, duration is in years. For semester-based courses, duration is in years (2 semesters per year).",
+ },
+ ),
+ )
+
@admin.register(Batch, site=custom_admin_site)
class BatchAdmin(admin.ModelAdmin):
@@ -152,41 +194,78 @@ class BatchAdmin(admin.ModelAdmin):
@admin.register(Subject, site=custom_admin_site)
class SubjectModel(admin.ModelAdmin):
- list_display = ("id", "name", "course", "semester_or_year")
- search_fields = ("name", "course_id", "semester_or_year")
- list_filter = ("name", "course_id", "semester_or_year")
+ list_display = ("id", "name", "course", "get_period_display", "has_syllabus")
+ search_fields = ("name", "course__name")
+ list_filter = ("course", "semester_or_year")
+ advanced_filter_fields = ("name", "course", "semester_or_year")
+
+ def has_syllabus(self, obj):
+ return bool(obj.syllabus_pdf)
+
+ has_syllabus.boolean = True
+ has_syllabus.short_description = "Has Syllabus"
+
+ def get_period_display(self, obj):
+ if obj.course.duration_type == "Semester":
+ return f"Semester {obj.semester_or_year}"
+ return f"Year {obj.semester_or_year}"
+
+ get_period_display.short_description = "Period"
+
+ def add_view(self, request, form_url="", extra_context=None):
+ extra_context = extra_context or {}
+ extra_context["show_save_and_add_another"] = True
+ extra_context["show_save"] = False
+ return super().add_view(request, form_url, extra_context)
+
+ def response_add(self, request, obj, post_url_continue=None):
+ if "_addanother" in request.POST:
+ # Pre-fill the course and semester fields for the next entry
+ return self.response_post_save_add(request, obj)
+ return super().response_add(request, obj, post_url_continue)
+
+ def get_form(self, request, obj=None, **kwargs):
+ form = super().get_form(request, obj, **kwargs)
+ if obj and obj.course:
+ if obj.course.duration_type == "Semester":
+ form.base_fields["semester_or_year"].label = "Semester"
+ form.base_fields[
+ "semester_or_year"
+ ].help_text = f"Enter semester number (1-{obj.course.duration * 2})"
+ else:
+ form.base_fields["semester_or_year"].label = "Year"
+ form.base_fields[
+ "semester_or_year"
+ ].help_text = f"Enter year number (1-{obj.course.duration})"
+
+ # Keep the last used course selected for next entry
+ if not obj and request.method == "GET":
+ if "course" in request.GET:
+ form.base_fields["course"].initial = request.GET["course"]
+ if "semester_or_year" in request.GET:
+ form.base_fields["semester_or_year"].initial = request.GET[
+ "semester_or_year"
+ ]
+ return form
+
+ def response_post_save_add(self, request, obj):
+ from django.http import HttpResponseRedirect
+ from django.urls import reverse
+
+ opts = self.model._meta
+ url = reverse(f"admin:{opts.app_label}_{opts.model_name}_add")
+ return HttpResponseRedirect(
+ f"{url}?course={obj.course.id}&semester_or_year={obj.semester_or_year}"
+ )
@admin.register(Student, site=custom_admin_site)
class StudentAdmin(admin.ModelAdmin):
- list_display = ("id", "name", "status", "phone")
form = StudentAdminForm
- search_fields = (
- "name",
- "phone",
- "courses",
- "gender",
- "joining_date",
- )
- list_filter = (
- "name",
- "phone",
- "courses",
- "gender",
- "joining_date",
- "status",
- )
- advanced_filter_fields = (
- "name",
- "phone",
- "courses",
- "gender",
- "joining_date",
- "status",
- )
- readonly_fields = ("password",)
- ordering = ("name",)
- filter_horizontal = ("groups",)
+ list_display = ("id", "name", "phone", "course", "status")
+ search_fields = ("name", "phone", "course__name")
+ list_filter = ("course", "status", "gender", "joining_date")
+ advanced_filter_fields = ("name", "phone", "course", "batch", "status")
def has_view_permission(self, request, obj=None):
"""Check if user has view permission"""
@@ -213,38 +292,14 @@ class StudentAdmin(admin.ModelAdmin):
return self.readonly_fields + ("password",)
return self.readonly_fields
- class Media:
- js = ("js/student_admin.js",)
-
@admin.register(Staff, site=custom_admin_site)
class StaffAdmin(admin.ModelAdmin):
- list_display = ["name", "phone", "designation", "joining_date"]
form = StaffAdminForm
- search_fields = (
- "name",
- "phone",
- "monthly_salary",
- "designation",
- "joining_date",
- "gender",
- )
- list_filter = (
- "name",
- "phone",
- "designation",
- "joining_date",
- "gender",
- "groups",
- )
- advanced_filter_fields = (
- "name",
- "phone",
- "designation",
- "joining_date",
- "gender",
- "groups",
- )
+ list_display = ("id", "name", "phone", "designation", "joining_date")
+ search_fields = ("name", "phone", "designation")
+ list_filter = ("designation", "joining_date", "gender")
+ advanced_filter_fields = ("name", "phone", "designation")
readonly_fields = ("password",)
ordering = ("name",)
filter_horizontal = ("groups",)
@@ -274,9 +329,6 @@ class StaffAdmin(admin.ModelAdmin):
return self.readonly_fields + ("password",)
return self.readonly_fields
- class Media:
- js = ("js/staff_admin.js",)
-
class AttendanceRecordForm(ModelForm):
def clean(self):
@@ -334,9 +386,6 @@ class AttendanceAdmin(admin.ModelAdmin):
formset.save_m2m()
- class Media:
- js = ("js/attendance_admin.js",)
-
@admin.register(Notice, site=custom_admin_site)
class NoticeAdmin(admin.ModelAdmin):
@@ -345,64 +394,242 @@ class NoticeAdmin(admin.ModelAdmin):
list_filter = ("title", "message")
advanced_filter_fields = ("title", "message")
- class Media:
- js = ("js/notice_admin.js",)
-
-
-class StudentRoutineStackInline(admin.StackedInline):
- model = StudentRoutine
- extra = 0
-
@admin.register(Routine, site=custom_admin_site)
class RoutineAdmin(admin.ModelAdmin):
- inlines = [StudentRoutineStackInline]
- list_display = ["name", "start_time", "end_time", "teacher"]
- search_fields = ("name", "start_time", "end_time", "teacher__name")
- list_filter = ("name", "start_time", "end_time", "teacher")
- advanced_filter_fields = ("name", "start_time", "end_time", "teacher")
+ list_display = [
+ "course",
+ "subject",
+ "teacher",
+ "start_time",
+ "end_time",
+ "semester_or_year",
+ "is_active",
+ ]
+ list_filter = ["is_active", "course", "subject", "teacher", "semester_or_year"]
+ search_fields = ["course__name", "subject__name", "teacher__name"]
+ ordering = ["start_time"]
+ change_form_template = "admin/app/routine/change_form.html"
+
+ def get_urls(self):
+ urls = super().get_urls()
+ custom_urls = [
+ path(
+ "add-multiple/",
+ self.admin_site.admin_view(self.save_routines),
+ name="routine-add-multiple",
+ ),
+ ]
+ return custom_urls + urls
+
+ def save_routines(self, request):
+ if request.method == "POST":
+ try:
+ data = json.loads(request.body)
+ routines = data.get("routines", [])
+
+ if not routines:
+ return JsonResponse(
+ {"success": False, "message": "No routines data provided"}
+ )
+
+ with (
+ transaction.atomic()
+ ): # Use transaction to ensure all routines are created or none
+ created_routines = []
+ for index, routine_data in enumerate(routines):
+ try:
+ # Validate required fields
+ required_fields = [
+ "course",
+ "subject",
+ "teacher",
+ "start_time",
+ "end_time",
+ "semester_or_year",
+ ]
+ missing_fields = [
+ field
+ for field in required_fields
+ if field not in routine_data
+ or not routine_data.get(field)
+ ]
+
+ if missing_fields:
+ error_msg = f"Missing required fields: {', '.join(missing_fields)}"
+ raise ValueError(error_msg)
+
+ # Convert IDs to integers
+ try:
+ course_id = int(routine_data["course"])
+ subject_id = int(routine_data["subject"])
+ teacher_id = int(routine_data["teacher"])
+ semester = int(routine_data["semester_or_year"])
+
+ except (ValueError, TypeError) as e:
+ raise ValueError(f"Error parsing IDs: {str(e)}")
+
+ # Validate time format
+ try:
+ start_time = datetime.strptime(
+ routine_data["start_time"], "%H:%M"
+ ).time()
+ end_time = datetime.strptime(
+ routine_data["end_time"], "%H:%M"
+ ).time()
+ except ValueError as e:
+ raise ValueError(
+ "Invalid time format. Please use HH:MM format."
+ )
+
+ # Check for existing routine
+ existing_routine = Routine.objects.filter(
+ course_id=course_id,
+ subject_id=subject_id,
+ semester_or_year=semester,
+ start_time=start_time,
+ end_time=end_time,
+ ).first()
+
+ if existing_routine:
+ # Update existing routine
+ existing_routine.teacher_id = teacher_id
+ existing_routine.save()
+ created_routines.append(existing_routine)
+ else:
+ # Create new routine
+ routine = Routine.objects.create(
+ course_id=course_id,
+ subject_id=subject_id,
+ teacher_id=teacher_id,
+ start_time=start_time,
+ end_time=end_time,
+ semester_or_year=semester,
+ is_active=True,
+ )
+ created_routines.append(routine)
+
+ except ValueError as e:
+ raise # Re-raise to trigger rollback
+ except Exception as e:
+ raise # Re-raise to trigger rollback
+
+ return JsonResponse(
+ {
+ "success": True,
+ "message": f"Successfully saved {len(created_routines)} routines",
+ "routines": [
+ {
+ "id": r.id,
+ "course": r.course.name,
+ "subject": r.subject.name,
+ "teacher": r.teacher.name,
+ "start_time": r.start_time.strftime("%H:%M"),
+ "end_time": r.end_time.strftime("%H:%M"),
+ "semester_or_year": r.semester_or_year,
+ }
+ for r in created_routines
+ ],
+ }
+ )
+
+ except ValueError as e:
+ return JsonResponse({"success": False, "message": str(e)})
+ except json.JSONDecodeError as e:
+ return JsonResponse(
+ {"success": False, "message": f"Invalid JSON data: {str(e)}"}
+ )
+ except Exception as e:
+ return JsonResponse({"success": False, "message": str(e)})
+
+ return JsonResponse({"success": False, "message": "Invalid request method"})
+
+ def formfield_for_foreignkey(self, db_field, request, **kwargs):
+ if db_field.name == "subject":
+ if request.GET.get("course"):
+ kwargs["queryset"] = Subject.objects.filter(
+ course_id=request.GET.get("course"),
+ semester_or_year=request.GET.get("semester_or_year", 1),
+ )
+ else:
+ kwargs["queryset"] = Subject.objects.none()
+ elif db_field.name == "teacher":
+ kwargs["queryset"] = Staff.objects.filter(
+ Q(designation__icontains="Teacher")
+ | Q(designation__icontains="Staff")
+ | Q(designation__icontains="Principal")
+ | Q(designation__isnull=True)
+ | Q(designation="")
+ ).order_by("name")
+ return super().formfield_for_foreignkey(db_field, request, **kwargs)
+
+ def get_form(self, request, obj=None, **kwargs):
+ form = super().get_form(request, obj, **kwargs)
+ if not obj: # Only for adding new routines
+ form.base_fields["course"].widget.attrs["class"] = "select2"
+ form.base_fields["subject"].widget.attrs["class"] = "select2"
+ form.base_fields["teacher"].widget.attrs["class"] = "select2"
+ form.base_fields["semester_or_year"].initial = 1
+ return form
+
+ def add_view(self, request, form_url="", extra_context=None):
+ extra_context = extra_context or {}
+ extra_context["show_save_and_add_another"] = True
+ return super().add_view(request, form_url, extra_context)
class Media:
- js = ("js/multiple_student_hover.js", "js/routine_admin.js")
+ css = {
+ "all": (
+ "https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css",
+ )
+ }
+ js = ("https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js",)
@admin.register(Staff_leave, site=custom_admin_site)
class StaffLeaveAdmin(admin.ModelAdmin):
- list_display = ('staff', 'date', 'get_status_display', 'created_at')
- list_filter = ('status', 'date', 'created_at')
- search_fields = ('staff__name', 'message')
- readonly_fields = ('created_at', 'updated_at')
- ordering = ('-created_at',)
- raw_id_fields = ('staff',)
+ list_display = ("staff", "date", "get_status_display", "created_at")
+ list_filter = ("status", "date", "created_at")
+ search_fields = ("staff__name", "message")
+ readonly_fields = ("created_at", "updated_at")
+ ordering = ("-created_at",)
+ raw_id_fields = ("staff",)
@admin.register(Student_Leave, site=custom_admin_site)
class StudentLeaveAdmin(admin.ModelAdmin):
- list_display = ('student', 'date', 'get_status_display', 'created_at')
- list_filter = ('status', 'date', 'created_at')
- search_fields = ('student__name', 'message')
- readonly_fields = ('created_at', 'updated_at')
- ordering = ('-created_at',)
- raw_id_fields = ('student',)
+ list_display = ("student", "date", "get_status_display", "created_at")
+ list_filter = ("status", "date", "created_at")
+ search_fields = ("student__name", "message")
+ readonly_fields = ("created_at", "updated_at")
+ ordering = ("-created_at",)
+ raw_id_fields = ("student",)
@admin.register(StudentFeedback, site=custom_admin_site)
class StudentFeedbackAdmin(admin.ModelAdmin):
- list_display = ('student', 'rating', 'created_at')
- list_filter = ('rating', 'created_at')
- search_fields = ('student__name', 'feedback_text')
- readonly_fields = ('created_at', 'updated_at')
- ordering = ('-created_at',)
+ list_display = ("student", "rating", "created_at")
+ list_filter = ("rating", "created_at")
+ search_fields = ("student__name", "feedback_text")
+ readonly_fields = ("created_at", "updated_at")
+ ordering = ("-created_at",)
@admin.register(CourseTracking, site=custom_admin_site)
class CourseTrackingAdmin(admin.ModelAdmin):
- list_display = ('student', 'course', 'progress_status', 'completion_percentage', 'start_date', 'expected_end_date')
- list_filter = ('progress_status', 'course', 'start_date', 'expected_end_date')
- search_fields = ('student__name', 'course__name', 'notes')
- readonly_fields = ('enrollment_date', 'created_at', 'updated_at')
- ordering = ('-created_at',)
- raw_id_fields = ('student', 'course')
+ list_display = (
+ "student",
+ "course",
+ "progress_status",
+ "completion_percentage",
+ "start_date",
+ "expected_end_date",
+ )
+ list_filter = ("progress_status", "course", "start_date", "expected_end_date")
+ search_fields = ("student__name", "course__name", "notes")
+ readonly_fields = ("enrollment_date", "created_at", "updated_at")
+ ordering = ("-created_at",)
+ raw_id_fields = ("student", "course")
list_per_page = 25
def has_view_permission(self, request, obj=None):
@@ -420,44 +647,121 @@ class CourseTrackingAdmin(admin.ModelAdmin):
def has_module_permission(self, request):
return request.user.has_module_perms("app")
- class Media:
- js = ("js/course_tracking_admin.js",)
-
@admin.register(InstituteFeedback, site=custom_admin_site)
class InstituteFeedbackAdmin(admin.ModelAdmin):
- list_display = ('display_name', 'institute', 'feedback_type', 'rating', 'is_anonymous', 'is_public', 'created_at')
- list_filter = ('feedback_type', 'rating', 'is_anonymous', 'is_public', 'created_at', 'institute')
- search_fields = ('user__name', 'institute__name', 'feedback_text')
- readonly_fields = ('created_at', 'updated_at')
- date_hierarchy = 'created_at'
-
+ list_display = (
+ "display_name",
+ "institute",
+ "feedback_type",
+ "rating",
+ "is_anonymous",
+ "is_public",
+ "created_at",
+ )
+ list_filter = (
+ "feedback_type",
+ "rating",
+ "is_anonymous",
+ "is_public",
+ "created_at",
+ "institute",
+ )
+ search_fields = ("user__name", "institute__name", "feedback_text")
+ readonly_fields = ("created_at", "updated_at")
+ date_hierarchy = "created_at"
+
def display_name(self, obj):
return obj.display_name
- display_name.short_description = 'Student Name'
-
+
+ display_name.short_description = "Student Name"
+
fieldsets = (
- ('Feedback Information', {
- 'fields': ('institute', 'user', 'feedback_type', 'rating', 'feedback_text')
- }),
- ('Privacy Settings', {
- 'fields': ('is_anonymous', 'is_public'),
- 'classes': ('collapse',)
- }),
- ('Timestamps', {
- 'fields': ('created_at', 'updated_at'),
- 'classes': ('collapse',)
- }),
+ (None, {"fields": ("institute", "user")}),
+ (
+ "Feedback Details",
+ {
+ "fields": (
+ "feedback_type",
+ "rating",
+ "feedback_text",
+ "is_anonymous",
+ "is_public",
+ )
+ },
+ ),
+ (
+ "Metadata",
+ {"fields": ("created_at", "updated_at"), "classes": ("collapse",)},
+ ),
)
def get_queryset(self, request):
- return super().get_queryset(request).select_related('user', 'institute')
+ return super().get_queryset(request).select_related("user", "institute")
class Media:
- css = {
- 'all': ('admin/css/forms.css',)
- }
- js = ('admin/js/collapse.js',)
+ css = {"all": ("admin/css/forms.css",)}
+ js = ("admin/js/collapse.js",)
+
+
+@admin.register(StaffInstituteFeedback, site=custom_admin_site)
+class StaffInstituteFeedbackAdmin(admin.ModelAdmin):
+ list_display = (
+ "display_name",
+ "institute",
+ "feedback_type",
+ "rating",
+ "is_anonymous",
+ "is_public",
+ "created_at",
+ )
+ list_filter = (
+ "feedback_type",
+ "rating",
+ "is_anonymous",
+ "is_public",
+ "created_at",
+ "institute",
+ )
+ search_fields = ("staff__name", "institute__name", "feedback_text")
+ readonly_fields = ("created_at", "updated_at")
+ date_hierarchy = "created_at"
+
+ def display_name(self, obj):
+ return obj.display_name
+
+ display_name.short_description = "Staff Name"
+
+ fieldsets = (
+ (None, {"fields": ("institute", "staff")}),
+ (
+ "Feedback Details",
+ {
+ "fields": (
+ "feedback_type",
+ "rating",
+ "feedback_text",
+ "is_anonymous",
+ "is_public",
+ )
+ },
+ ),
+ (
+ "Metadata",
+ {"fields": ("created_at", "updated_at"), "classes": ("collapse",)},
+ ),
+ )
+
+ def get_queryset(self, request):
+ return super().get_queryset(request).select_related("staff", "institute")
+
+
+@admin.register(FCMDevice)
+class FCMDeviceAdmin(admin.ModelAdmin):
+ list_display = ("token",)
+ search_fields = ("token",)
+ list_filter = ("token",)
+ advanced_filter_fields = ("token",)
custom_admin_site.register(User, UserAdmin)
diff --git a/app/api_urls.py b/app/api_urls.py
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/app/api_urls.py
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/api_views.py b/app/api_views.py
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/app/api_views.py
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/auth.py b/app/auth.py
new file mode 100644
index 0000000..0c0bc34
--- /dev/null
+++ b/app/auth.py
@@ -0,0 +1,298 @@
+from django.shortcuts import render, redirect
+from django.contrib.auth import login
+from django.views.decorators.csrf import csrf_exempt
+from django.contrib.auth.models import User
+from django.contrib.auth.hashers import make_password
+from django.core.cache import cache
+import uuid
+from .models import Student, Staff
+from .utils import (
+ generate_otp,
+ store_otp,
+ verify_otp,
+ send_otp_sms,
+ send_password_reset_email,
+ verify_reset_code
+)
+
+def reset_password_options(request):
+ """View for selecting between phone and email password reset"""
+ return render(request, "login/reset_options.html")
+
+def password_reset_phone(request):
+ """View for initiating password reset with phone number"""
+ if request.method == "POST":
+ phone = request.POST.get("phone")
+
+ # Validate phone number
+ if not phone or not phone.isdigit():
+ return render(request, "login/password_reset_phone.html", {"error": "Please enter a valid phone number", "is_email": False})
+
+ # Check if a user with this phone exists
+ student = Student.objects.filter(phone=phone).first()
+ staff = Staff.objects.filter(phone=phone).first()
+
+ if not student and not staff:
+ return render(request, "login/password_reset_phone.html", {"error": "No account found with this phone number", "is_email": False})
+
+ # Generate and store OTP
+ otp = generate_otp()
+ store_otp(phone, otp)
+
+ # Send OTP via SMS
+ send_result = send_otp_sms(phone, otp)
+
+ if send_result:
+ # Redirect to OTP verification page
+ return render(request, "login/password_reset_otp.html", {"phone": phone, "is_email": False})
+ else:
+ return render(request, "login/password_reset_phone.html", {"error": "Failed to send OTP. Please try again.", "is_email": False})
+
+ return render(request, "login/password_reset_phone.html", {"is_email": False})
+
+def password_reset_phone_verify(request):
+ """View for verifying OTP for password reset"""
+ if request.method == "POST":
+ phone = request.POST.get("phone")
+ otp = request.POST.get("otp")
+
+ if not phone or not otp:
+ return render(request, "login/password_reset_otp.html", {
+ "error": "Phone and OTP are required",
+ "phone": phone,
+ "is_email": False
+ })
+
+ # Verify OTP
+ if verify_otp(phone, otp):
+ # Generate a secure token for this reset
+ token = str(uuid.uuid4())
+ # Store token with phone in cache for 15 minutes
+ cache.set(f"reset_token_{token}", phone, 60 * 15)
+
+ # Redirect to set new password
+ return render(request, "login/password_reset_set.html", {
+ "phone": phone,
+ "token": token,
+ "is_email": False
+ })
+ else:
+ return render(request, "login/password_reset_otp.html", {
+ "error": "Invalid or expired OTP",
+ "phone": phone,
+ "is_email": False
+ })
+
+ # If GET request, redirect to phone login
+ return redirect("phone_reset_password")
+
+def resend_phone_otp(request):
+ """View for resending OTP for password reset"""
+ if request.method == "POST":
+ phone = request.POST.get("phone")
+
+ if not phone:
+ return redirect("phone_reset_password")
+
+ # Generate and store new OTP
+ otp = generate_otp()
+ store_otp(phone, otp)
+
+ # Send OTP via SMS
+ send_result = send_otp_sms(phone, otp)
+
+ if send_result:
+ return render(request, "login/password_reset_otp.html", {
+ "phone": phone,
+ "message": "OTP resent successfully",
+ "is_email": False
+ })
+ else:
+ return render(request, "login/password_reset_otp.html", {
+ "phone": phone,
+ "error": "Failed to resend OTP",
+ "is_email": False
+ })
+
+ return redirect("phone_reset_password")
+
+def password_reset_email(request):
+ """View for initiating password reset with email"""
+ if request.method == "POST":
+ email = request.POST.get("email")
+
+ # Validate email
+ if not email:
+ return render(request, "login/email_reset.html", {"error": "Please enter a valid email address", "is_email": True})
+
+ # Check if a user with this email exists
+ student = Student.objects.filter(email=email).first()
+ staff = Staff.objects.filter(email=email).first()
+
+ if not student and not staff:
+ return render(request, "login/email_reset.html", {"error": "No account found with this email address", "is_email": True})
+
+ # Get the user
+ user = student or staff
+
+ # Send password reset email
+ if send_password_reset_email(user, request, email):
+ # Redirect to verification page
+ return render(request, "login/password_reset_otp.html", {"email": email, "is_email": True})
+ else:
+ return render(request, "login/email_reset.html", {"error": "Failed to send verification code. Please try again.", "is_email": True})
+
+ return render(request, "login/email_reset.html", {"is_email": True})
+
+def password_reset_email_verify(request):
+ """View for verifying reset code for email"""
+ if request.method == "POST":
+ email = request.POST.get("email")
+ code = request.POST.get("otp")
+
+ if not email or not code:
+ return render(request, "login/password_reset_otp.html", {
+ "error": "Email and verification code are required",
+ "email": email,
+ "is_email": True
+ })
+
+ # Verify code
+ if verify_reset_code(email, code):
+ # Generate a secure token for this reset
+ token = str(uuid.uuid4())
+ # Store token with email in cache for 15 minutes
+ cache.set(f"reset_token_{token}", email, 60 * 15)
+
+ # Redirect to set new password
+ return render(request, "login/password_reset_set.html", {
+ "email": email,
+ "token": token,
+ "is_email": True
+ })
+ else:
+ return render(request, "login/password_reset_otp.html", {
+ "error": "Invalid or expired code",
+ "email": email,
+ "is_email": True,
+ "message": "Please check your email for the verification code."
+ })
+
+ # For GET request, show the verification form with email pre-filled
+ email = request.GET.get("email")
+ return render(request, "login/password_reset_otp.html", {
+ "email": email,
+ "is_email": True,
+ "message": "Please check your email for the verification code."
+ })
+
+def resend_email_code(request):
+ """View for resending verification code for password reset"""
+ if request.method == "POST":
+ email = request.POST.get("email")
+
+ if not email:
+ return redirect("email_reset_password")
+
+ # Find the user
+ student = Student.objects.filter(email=email).first()
+ staff = Staff.objects.filter(email=email).first()
+ user = student or staff
+
+ if not user:
+ return render(request, "login/password_reset_otp.html", {
+ "email": email,
+ "is_email": True,
+ "error": "User not found"
+ })
+
+ # Send new verification code
+ if send_password_reset_email(user, request, email):
+ return render(request, "login/password_reset_otp.html", {
+ "email": email,
+ "is_email": True,
+ "message": "Verification code resent successfully"
+ })
+ else:
+ return render(request, "login/password_reset_otp.html", {
+ "email": email,
+ "is_email": True,
+ "error": "Failed to resend verification code"
+ })
+
+ return redirect("email_reset_password")
+
+def set_new_password(request):
+ """View for setting new password after verification"""
+ if request.method == "POST":
+ phone = request.POST.get("phone")
+ email = request.POST.get("email")
+ token = request.POST.get("token")
+ new_password1 = request.POST.get("new_password1")
+ new_password2 = request.POST.get("new_password2")
+
+ identifier = phone or email
+ is_email = bool(email)
+
+ # Validate inputs
+ if not all([identifier, token, new_password1, new_password2]):
+ return render(request, "login/password_reset_set.html", {
+ "error": "All fields are required",
+ "phone": phone,
+ "email": email,
+ "token": token,
+ "is_email": is_email
+ })
+
+ # Verify token is valid
+ stored_identifier = cache.get(f"reset_token_{token}")
+ if not stored_identifier or stored_identifier != identifier:
+ return render(request, "login/password_reset_set.html", {
+ "error": "Invalid or expired reset session",
+ "phone": phone,
+ "email": email,
+ "token": token,
+ "is_email": is_email
+ })
+
+ # Check passwords match
+ if new_password1 != new_password2:
+ return render(request, "login/password_reset_set.html", {
+ "error": "Passwords do not match",
+ "phone": phone,
+ "email": email,
+ "token": token,
+ "is_email": is_email
+ })
+
+ # Find the user
+ if is_email:
+ student = Student.objects.filter(email=email).first()
+ staff = Staff.objects.filter(email=email).first()
+ else:
+ student = Student.objects.filter(phone=phone).first()
+ staff = Staff.objects.filter(phone=phone).first()
+
+ user = student or staff
+
+ if user:
+ # Set new password
+ user.password = make_password(new_password1)
+ user.save()
+
+ # Invalidate reset token
+ cache.delete(f"reset_token_{token}")
+
+ # Show success message
+ return render(request, "login/password_reset_complete.html")
+ else:
+ return render(request, "login/password_reset_set.html", {
+ "error": "User not found",
+ "phone": phone,
+ "email": email,
+ "token": token,
+ "is_email": is_email
+ })
+
+ # If GET request, redirect to options page
+ return redirect("reset_password_options")
\ No newline at end of file
diff --git a/app/backends.py b/app/backends.py
index 7097874..1aa92f5 100644
--- a/app/backends.py
+++ b/app/backends.py
@@ -5,12 +5,12 @@ from .models import Staff, Student
class MultiModelBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
"""
- Authenticate the user based on username (phone for Student and Staff) and password.
+ Authenticate the user based on username (email or phone) and password.
"""
if username is None or password is None:
return None
- # Try authenticating as Staff
+ # Try authenticating as Staff by phone
try:
staff = Staff.objects.get(phone=username)
if staff.check_password(password):
@@ -18,7 +18,15 @@ class MultiModelBackend(ModelBackend):
except Staff.DoesNotExist:
pass
- # Try authenticating as Student
+ # Try authenticating as Staff by email
+ try:
+ staff = Staff.objects.get(email=username)
+ if staff.check_password(password):
+ return staff
+ except Staff.DoesNotExist:
+ pass
+
+ # Try authenticating as Student by phone
try:
student = Student.objects.get(phone=username)
if student.check_password(password):
@@ -26,6 +34,12 @@ class MultiModelBackend(ModelBackend):
except Student.DoesNotExist:
pass
+ # Try authenticating as Student by email
+ # Use filter().first() to handle multiple students with same email
+ student = Student.objects.filter(email=username).first()
+ if student and student.check_password(password):
+ return student
+
# Return None if no match
return None
diff --git a/app/forms.py b/app/forms.py
index 4faadfb..deba05d 100644
--- a/app/forms.py
+++ b/app/forms.py
@@ -1,8 +1,18 @@
from django import forms
from django.contrib.auth.hashers import make_password
+from django.contrib.auth.forms import SetPasswordForm
from .models import Staff, Student
+class CustomSetPasswordForm(SetPasswordForm):
+ def save(self, commit=True):
+ user = self.user
+ user.set_password(self.cleaned_data["new_password1"])
+ if commit:
+ user.save()
+ return user
+
+
class StudentAdminForm(forms.ModelForm):
new_password = forms.CharField(
required=False,
@@ -36,10 +46,10 @@ class StaffAdminForm(forms.ModelForm):
fields = "__all__"
def save(self, commit=True):
- student = super().save(commit=False)
+ staff = super().save(commit=False)
new_password = self.cleaned_data.get("new_password")
if new_password:
- student.password = make_password(new_password)
+ staff.password = make_password(new_password)
if commit:
- student.save()
- return student
+ staff.save()
+ return staff
diff --git a/app/migrations/0001_initial.py b/app/migrations/0001_initial.py
index 5f3fe09..bb2e0b7 100644
--- a/app/migrations/0001_initial.py
+++ b/app/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.1.7 on 2025-03-25 17:13
+# Generated by Django 5.1.7 on 2025-03-27 01:54
import django.contrib.auth.models
import django.db.models.deletion
@@ -95,6 +95,11 @@ class Migration(migrations.Migration):
('temporary_address', models.CharField(blank=True, max_length=255, null=True)),
('permanent_address', models.CharField(blank=True, max_length=255, null=True)),
('marital_status', models.CharField(blank=True, choices=[('Married', 'Married'), ('Unmarried', 'Unmarried'), ('Divorced', 'Divorced')], max_length=255, null=True)),
+ ('parent_name', models.CharField(blank=True, max_length=255, null=True)),
+ ('parent_phone', models.CharField(blank=True, max_length=255, null=True)),
+ ('citizenship_no', models.CharField(blank=True, max_length=255, null=True)),
+ ('passport', models.CharField(blank=True, max_length=255, null=True)),
+ ('image', models.ImageField(blank=True, null=True, upload_to='staff_image')),
('joining_date', models.DateField(blank=True, null=True)),
('password', models.CharField(editable=False, max_length=128, null=True)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this staff belongs to.', related_name='staffs', to='auth.group', verbose_name='groups')),
@@ -111,15 +116,16 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Routine',
fields=[
- ('id', models.BigAutoField(primary_key=True, serialize=False)),
- ('name', models.CharField(blank=True, max_length=255, null=True)),
- ('start_time', models.TimeField(blank=True, null=True)),
- ('end_time', models.TimeField(blank=True, null=True)),
- ('teacher', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='routines', to='app.staff')),
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('start_time', models.TimeField()),
+ ('end_time', models.TimeField()),
+ ('semester_or_year', models.PositiveIntegerField()),
+ ('is_active', models.BooleanField(default=True)),
+ ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.course')),
+ ('teacher', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.staff')),
],
options={
- 'verbose_name': 'Routine',
- 'verbose_name_plural': 'Routines',
+ 'ordering': ['start_time'],
},
),
migrations.CreateModel(
@@ -168,11 +174,16 @@ class Migration(migrations.Migration):
('phone', models.CharField(blank=True, max_length=255, null=True, unique=True)),
('temporary_address', models.CharField(blank=True, max_length=500, null=True)),
('permanent_address', models.CharField(blank=True, max_length=500, null=True)),
+ ('marital_status', models.CharField(blank=True, choices=[('Married', 'Married'), ('Unmarried', 'Unmarried'), ('Divorced', 'Divorced')], max_length=255, null=True)),
('parent_name', models.CharField(blank=True, max_length=255, null=True)),
('parent_phone', models.CharField(blank=True, max_length=255, null=True)),
+ ('citizenship_no', models.CharField(blank=True, max_length=255, null=True)),
+ ('image', models.ImageField(blank=True, null=True, upload_to='student_image')),
+ ('current_semester', models.PositiveIntegerField(default=1, help_text='Current semester for semester-based courses')),
('joining_date', models.DateField(blank=True, null=True)),
('password', models.CharField(editable=False, max_length=128, null=True)),
- ('courses', models.ManyToManyField(blank=True, related_name='students', to='app.course')),
+ ('batches', models.ManyToManyField(blank=True, related_name='students', to='app.batch')),
+ ('course', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='students', to='app.course')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this student belongs to.', related_name='students', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this student.', related_name='student_set', related_query_name='student', to='auth.permission', verbose_name='user permissions')),
],
@@ -210,56 +221,113 @@ class Migration(migrations.Migration):
'ordering': ['-created_at'],
},
),
- migrations.CreateModel(
- name='StudentRoutine',
- fields=[
- ('id', models.BigAutoField(primary_key=True, serialize=False)),
- ('course', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='app.course')),
- ('routine', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='students', to='app.routine')),
- ('student', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='routines', to='app.student')),
- ],
- ),
migrations.CreateModel(
name='Subject',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('semester_or_year', models.PositiveIntegerField()),
- ('course', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='app.course')),
+ ('syllabus_pdf', models.FileField(blank=True, help_text='Upload syllabus or study material PDF', null=True, upload_to='subject_pdfs/')),
+ ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.course')),
],
+ options={
+ 'ordering': ['course', 'semester_or_year', 'name'],
+ 'unique_together': {('name', 'course', 'semester_or_year')},
+ },
+ ),
+ migrations.AddField(
+ model_name='routine',
+ name='subject',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.subject'),
+ ),
+ migrations.CreateModel(
+ name='StaffInstituteFeedback',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('feedback_type', models.CharField(choices=[('General', 'General'), ('Facilities', 'Facilities'), ('Teaching', 'Teaching'), ('Infrastructure', 'Infrastructure'), ('Administration', 'Administration')], default='General', max_length=20)),
+ ('rating', models.DecimalField(choices=[(1.0, 1.0), (1.5, 1.5), (2.0, 2.0), (2.5, 2.5), (3.0, 3.0), (3.5, 3.5), (4.0, 4.0), (4.5, 4.5), (5.0, 5.0)], decimal_places=1, max_digits=2)),
+ ('feedback_text', models.TextField()),
+ ('is_anonymous', models.BooleanField(default=False)),
+ ('is_public', models.BooleanField(default=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('institute', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='staff_feedbacks', to='app.institute')),
+ ('staff', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='institute_feedbacks', to='app.staff')),
+ ],
+ options={
+ 'verbose_name': 'Staff Institute Feedback',
+ 'verbose_name_plural': 'Staff Institute Feedbacks',
+ 'ordering': ['-created_at'],
+ 'unique_together': {('staff', 'institute', 'feedback_type')},
+ },
+ ),
+ migrations.CreateModel(
+ name='InstituteFeedback',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('feedback_type', models.CharField(choices=[('General', 'General'), ('Facilities', 'Facilities'), ('Teaching', 'Teaching'), ('Infrastructure', 'Infrastructure'), ('Administration', 'Administration')], default='General', max_length=20)),
+ ('rating', models.DecimalField(choices=[(1.0, 1.0), (1.5, 1.5), (2.0, 2.0), (2.5, 2.5), (3.0, 3.0), (3.5, 3.5), (4.0, 4.0), (4.5, 4.5), (5.0, 5.0)], decimal_places=1, max_digits=2)),
+ ('feedback_text', models.TextField()),
+ ('is_anonymous', models.BooleanField(default=False)),
+ ('is_public', models.BooleanField(default=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('institute', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to='app.institute')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='institute_feedbacks', to='app.student')),
+ ],
+ options={
+ 'verbose_name': 'Student Institute Feedback',
+ 'verbose_name_plural': 'Student Institute Feedbacks',
+ 'ordering': ['-created_at'],
+ 'unique_together': {('user', 'institute', 'feedback_type')},
+ },
+ ),
+ migrations.CreateModel(
+ name='CourseTracking',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('enrollment_date', models.DateField(auto_now_add=True)),
+ ('start_date', models.DateField(blank=True, null=True)),
+ ('expected_end_date', models.DateField(blank=True, null=True)),
+ ('actual_end_date', models.DateField(blank=True, null=True)),
+ ('progress_status', models.CharField(choices=[('Not Started', 'Not Started'), ('In Progress', 'In Progress'), ('Completed', 'Completed'), ('Dropped', 'Dropped')], default='Not Started', max_length=20)),
+ ('completion_percentage', models.IntegerField(default=0, help_text='Percentage of course completion (0-100)')),
+ ('current_semester', models.PositiveIntegerField(default=1)),
+ ('semester_start_date', models.DateField(blank=True, null=True)),
+ ('semester_end_date', models.DateField(blank=True, null=True)),
+ ('notes', models.TextField(blank=True, null=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='student_trackings', to='app.course')),
+ ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='course_trackings', to='app.student')),
+ ],
+ options={
+ 'verbose_name': 'Course Tracking',
+ 'verbose_name_plural': 'Course Trackings',
+ 'ordering': ['-created_at'],
+ 'unique_together': {('student', 'course')},
+ },
),
migrations.CreateModel(
name='StudentFeedback',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('rating', models.IntegerField(choices=[(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)])),
+ ('rating', models.DecimalField(choices=[(1.0, 1.0), (1.5, 1.5), (2.0, 2.0), (2.5, 2.5), (3.0, 3.0), (3.5, 3.5), (4.0, 4.0), (4.5, 4.5), (5.0, 5.0)], decimal_places=1, max_digits=2)),
('feedback_text', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to='app.student')),
- ('teacher', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.staff')),
- ('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.subject')),
+ ('teacher', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to='app.staff')),
],
options={
'verbose_name': 'Student Feedback',
'verbose_name_plural': 'Student Feedbacks',
- 'unique_together': {('student', 'subject', 'teacher')},
+ 'ordering': ['-created_at'],
+ 'unique_together': {('student', 'teacher')},
},
),
- migrations.CreateModel(
- name='TeacherFeedback',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('feedback_text', models.TextField()),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ('subject', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app.subject')),
- ('teacher', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to='app.staff')),
- ],
- options={
- 'verbose_name': 'Teacher Feedback',
- 'verbose_name_plural': 'Teacher Feedbacks',
- 'unique_together': {('teacher', 'subject')},
- },
+ migrations.AlterUniqueTogether(
+ name='routine',
+ unique_together={('course', 'subject', 'teacher', 'start_time', 'end_time', 'semester_or_year')},
),
]
diff --git a/app/migrations/0002_alter_studentfeedback_unique_together_and_more.py b/app/migrations/0002_alter_studentfeedback_unique_together_and_more.py
deleted file mode 100644
index c7b952a..0000000
--- a/app/migrations/0002_alter_studentfeedback_unique_together_and_more.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# Generated by Django 5.1.7 on 2025-03-25 17:18
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('app', '0001_initial'),
- ]
-
- operations = [
- migrations.AlterUniqueTogether(
- name='studentfeedback',
- unique_together=set(),
- ),
- migrations.DeleteModel(
- name='TeacherFeedback',
- ),
- migrations.RemoveField(
- model_name='studentfeedback',
- name='subject',
- ),
- migrations.RemoveField(
- model_name='studentfeedback',
- name='teacher',
- ),
- ]
diff --git a/app/migrations/0002_create_staffinstitutefeedback_table.py b/app/migrations/0002_create_staffinstitutefeedback_table.py
new file mode 100644
index 0000000..9601f31
--- /dev/null
+++ b/app/migrations/0002_create_staffinstitutefeedback_table.py
@@ -0,0 +1,24 @@
+from django.db import migrations
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('app', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RunSQL(
+ """
+ CREATE TABLE IF NOT EXISTS "app_staffinstitutefeedback" (
+ "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "feedback_type" integer NOT NULL,
+ "feedback_text" text NOT NULL,
+ "rating" integer NOT NULL,
+ "is_anonymous" bool NOT NULL,
+ "created_at" datetime NOT NULL,
+ "staff_id" integer NOT NULL REFERENCES "app_staff" ("id") DEFERRABLE INITIALLY DEFERRED
+ );
+ """,
+ "DROP TABLE IF EXISTS app_staffinstitutefeedback;"
+ ),
+ ]
\ No newline at end of file
diff --git a/app/migrations/0003_coursetracking.py b/app/migrations/0003_coursetracking.py
deleted file mode 100644
index 735e412..0000000
--- a/app/migrations/0003_coursetracking.py
+++ /dev/null
@@ -1,37 +0,0 @@
-# Generated by Django 5.1.7 on 2025-03-25 18:18
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('app', '0002_alter_studentfeedback_unique_together_and_more'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='CourseTracking',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('enrollment_date', models.DateField(auto_now_add=True)),
- ('start_date', models.DateField(blank=True, null=True)),
- ('expected_end_date', models.DateField(blank=True, null=True)),
- ('actual_end_date', models.DateField(blank=True, null=True)),
- ('progress_status', models.CharField(choices=[('Not Started', 'Not Started'), ('In Progress', 'In Progress'), ('Completed', 'Completed'), ('Dropped', 'Dropped')], default='Not Started', max_length=20)),
- ('completion_percentage', models.IntegerField(default=0, help_text='Percentage of course completion (0-100)')),
- ('notes', models.TextField(blank=True, null=True)),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='student_trackings', to='app.course')),
- ('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='course_trackings', to='app.student')),
- ],
- options={
- 'verbose_name': 'Course Tracking',
- 'verbose_name_plural': 'Course Trackings',
- 'ordering': ['-created_at'],
- 'unique_together': {('student', 'course')},
- },
- ),
- ]
diff --git a/app/migrations/0004_alter_studentfeedback_options_and_more.py b/app/migrations/0004_alter_studentfeedback_options_and_more.py
deleted file mode 100644
index d67251e..0000000
--- a/app/migrations/0004_alter_studentfeedback_options_and_more.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# Generated by Django 5.1.7 on 2025-03-25 18:27
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('app', '0003_coursetracking'),
- ]
-
- operations = [
- migrations.AlterModelOptions(
- name='studentfeedback',
- options={'ordering': ['-created_at'], 'verbose_name': 'Student Feedback', 'verbose_name_plural': 'Student Feedbacks'},
- ),
- migrations.AddField(
- model_name='studentfeedback',
- name='subject',
- field=models.ForeignKey(default='1', on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to='app.subject'),
- preserve_default=False,
- ),
- migrations.AddField(
- model_name='studentfeedback',
- name='teacher',
- field=models.ForeignKey(default='1', on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to='app.staff'),
- preserve_default=False,
- ),
- migrations.AlterUniqueTogether(
- name='studentfeedback',
- unique_together={('student', 'subject', 'teacher')},
- ),
- ]
diff --git a/app/migrations/0005_alter_studentfeedback_unique_together_and_more.py b/app/migrations/0005_alter_studentfeedback_unique_together_and_more.py
deleted file mode 100644
index 826b095..0000000
--- a/app/migrations/0005_alter_studentfeedback_unique_together_and_more.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# Generated by Django 5.1.7 on 2025-03-25 19:41
-
-from django.db import migrations
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('app', '0004_alter_studentfeedback_options_and_more'),
- ]
-
- operations = [
- migrations.AlterUniqueTogether(
- name='studentfeedback',
- unique_together={('student', 'teacher')},
- ),
- migrations.RemoveField(
- model_name='studentfeedback',
- name='subject',
- ),
- ]
diff --git a/app/migrations/0006_institutefeedback.py b/app/migrations/0006_institutefeedback.py
deleted file mode 100644
index b3cf33e..0000000
--- a/app/migrations/0006_institutefeedback.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# Generated by Django 5.1.7 on 2025-03-25 19:44
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('app', '0005_alter_studentfeedback_unique_together_and_more'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='InstituteFeedback',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('feedback_type', models.CharField(choices=[('General', 'General'), ('Facilities', 'Facilities'), ('Teaching', 'Teaching'), ('Infrastructure', 'Infrastructure'), ('Administration', 'Administration')], default='General', max_length=20)),
- ('rating', models.IntegerField(choices=[(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)])),
- ('feedback_text', models.TextField()),
- ('is_anonymous', models.BooleanField(default=False)),
- ('is_public', models.BooleanField(default=True)),
- ('created_at', models.DateTimeField(auto_now_add=True)),
- ('updated_at', models.DateTimeField(auto_now=True)),
- ('institute', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feedbacks', to='app.institute')),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='institute_feedbacks', to='app.student')),
- ],
- options={
- 'verbose_name': 'Institute Feedback',
- 'verbose_name_plural': 'Institute Feedbacks',
- 'ordering': ['-created_at'],
- 'unique_together': {('user', 'institute', 'feedback_type')},
- },
- ),
- ]
diff --git a/app/migrations/0007_student_is_superuser_alter_student_is_active.py b/app/migrations/0007_student_is_superuser_alter_student_is_active.py
deleted file mode 100644
index 06b0dd2..0000000
--- a/app/migrations/0007_student_is_superuser_alter_student_is_active.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# Generated by Django 5.1.7 on 2025-03-25 20:12
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('app', '0006_institutefeedback'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='student',
- name='is_superuser',
- field=models.BooleanField(default=False),
- ),
- migrations.AlterField(
- model_name='student',
- name='is_active',
- field=models.BooleanField(default=True),
- ),
- ]
diff --git a/app/models.py b/app/models.py
index 4425b95..d1ddf96 100644
--- a/app/models.py
+++ b/app/models.py
@@ -1,12 +1,11 @@
-from django.db import models
+from django.db import models, transaction
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import AbstractUser
-from django.utils.safestring import mark_safe
-from django.utils import timezone
-from datetime import date
+from datetime import date, timedelta
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.core.cache import cache
+from .utils import send_push_notification
# Create your models here.
duration_types_choices = (("Year", "Yearl Based"), ("Semester", "Semester Based"))
@@ -61,12 +60,52 @@ class Course(models.Model):
class Subject(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255)
- course = models.ForeignKey(Course, on_delete=models.CASCADE, default=1)
+ course = models.ForeignKey(Course, on_delete=models.CASCADE)
semester_or_year = models.PositiveIntegerField()
+ syllabus_pdf = models.FileField(upload_to='subject_pdfs/', null=True, blank=True, help_text="Upload syllabus or study material PDF")
+
+ def __str__(self):
+ return f"{self.name} ({self.course.name} - {'Semester' if self.course.duration_type == 'Semester' else 'Year'} {self.semester_or_year})"
+
+ def clean(self):
+ from django.core.exceptions import ValidationError
+
+ # Validate semester/year based on course duration type
+ if self.course.duration_type == "Semester":
+ max_semesters = self.course.duration * 2
+ if self.semester_or_year > max_semesters:
+ raise ValidationError(f"Semester cannot exceed {max_semesters}")
+ else: # Year based
+ if self.semester_or_year > self.course.duration:
+ raise ValidationError(f"Year cannot exceed {self.course.duration}")
+
+ def save(self, *args, **kwargs):
+ self.clean()
+ super().save(*args, **kwargs)
+
+ def get_pdf_url(self):
+ if self.syllabus_pdf:
+ return self.syllabus_pdf.url
+ return None
+
+ class Meta:
+ unique_together = ['name', 'course', 'semester_or_year']
+ ordering = ['course', 'semester_or_year', 'name']
+
+
+class Batch(models.Model):
+ id = models.BigAutoField(primary_key=True)
+ name = models.CharField(max_length=255)
+ start_date = models.DateField(null=True, blank=True)
+ end_date = models.DateField(null=True, blank=True)
def __str__(self):
return self.name
+ class Meta:
+ verbose_name = "Batch"
+ verbose_name_plural = "Batches"
+
class Student(AbstractUser):
first_name = None
@@ -76,27 +115,21 @@ class Student(AbstractUser):
username = None
email = None
- # Add these fields
- is_active = models.BooleanField(default=True)
- is_staff = models.BooleanField(default=True) # This ensures admin access
- is_superuser = models.BooleanField(default=False)
-
user_permissions = models.ManyToManyField(
"auth.Permission",
verbose_name="user permissions",
blank=True,
help_text="Specific permissions for this student.",
- related_name="student_set", # This fixes the clash
+ related_name="student_set",
related_query_name="student",
)
- # Similarly, let's also update groups to be explicit
groups = models.ManyToManyField(
"auth.Group",
verbose_name="groups",
blank=True,
help_text="The groups this student belongs to.",
- related_name="students", # This matches your existing related_name
+ related_name="students",
)
id = models.BigAutoField(primary_key=True)
@@ -112,16 +145,29 @@ class Student(AbstractUser):
phone = models.CharField(max_length=255, null=True, blank=True, unique=True)
temporary_address = models.CharField(max_length=500, null=True, blank=True)
permanent_address = models.CharField(max_length=500, null=True, blank=True)
+ marital_status = models.CharField(
+ max_length=255, choices=marital_status_choices, null=True, blank=True
+ )
parent_name = models.CharField(max_length=255, null=True, blank=True)
parent_phone = models.CharField(max_length=255, null=True, blank=True)
- courses = models.ManyToManyField(Course, related_name="students", blank=True)
+ citizenship_no = models.CharField(max_length=255, null=True, blank=True)
+ batches = models.ManyToManyField(Batch, related_name="students", blank=True)
+ image = models.ImageField(upload_to="student_image", null=True, blank=True)
+ course = models.ForeignKey(
+ Course,
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ related_name="students",
+ )
+ current_semester = models.PositiveIntegerField(
+ default=1, help_text="Current semester for semester-based courses"
+ )
joining_date = models.DateField(null=True, blank=True)
- password = models.CharField(
- max_length=128, editable=False, null=True
- ) # Editable is set to False to prevent direct editing
+ password = models.CharField(max_length=128, editable=False, null=True)
USERNAME_FIELD = "phone"
- REQUIRED_FIELDS = ["name"] # Add required fields
+ REQUIRED_FIELDS = ["name"]
def __str__(self):
return self.name
@@ -132,41 +178,55 @@ class Student(AbstractUser):
@property
def is_staff(self):
- """Check if student has any groups (permissions)"""
return self.groups.exists()
- @property
- def is_authenticated(self):
- """Required by Django's authentication system"""
- return True
-
def has_perm(self, perm, obj=None):
- """Check if student has a specific permission"""
if not self.is_active:
return False
- # Get permissions from groups
perms = set()
for group in self.groups.all():
perms.update(
group.permissions.values_list("content_type__app_label", "codename")
)
- # Convert permission string to app_label and codename
app_label, codename = perm.split(".")
return (app_label, codename) in perms
def has_module_perms(self, app_label):
- """Check if student has any permissions in the given app"""
if not self.is_active:
return False
- # Check if any group permissions match the app_label
for group in self.groups.all():
if group.permissions.filter(content_type__app_label=app_label).exists():
return True
return False
+ def get_current_subjects(self):
+ """Get subjects for current semester"""
+ if not self.course:
+ return Subject.objects.none()
+ return Subject.objects.filter(
+ course=self.course, semester_or_year=self.current_semester
+ )
+
+ def advance_semester(self):
+ """Advance to next semester if possible"""
+ if not self.course:
+ return False
+
+ if self.course.duration_type == "Semester":
+ total_semesters = self.course.duration * 2 # 2 semesters per year
+ if self.current_semester < total_semesters:
+ self.current_semester += 1
+ self.save()
+ return True
+ return False
+
+ def save(self, *args, **kwargs):
+ # Remove the course tracking creation from here
+ super().save(*args, **kwargs)
+
class Meta:
verbose_name = "Student"
verbose_name_plural = "Students"
@@ -213,6 +273,11 @@ class Staff(AbstractUser):
marital_status = models.CharField(
max_length=255, choices=marital_status_choices, null=True, blank=True
)
+ parent_name = models.CharField(max_length=255, null=True, blank=True)
+ parent_phone = models.CharField(max_length=255, null=True, blank=True)
+ citizenship_no = models.CharField(max_length=255, blank=True, null=True)
+ passport = models.CharField(max_length=255, blank=True, null=True)
+ image = models.ImageField(upload_to="staff_image", null=True, blank=True)
joining_date = models.DateField(null=True, blank=True)
password = models.CharField(
max_length=128, editable=False, null=True
@@ -269,49 +334,21 @@ class Staff(AbstractUser):
verbose_name_plural = "Staffs"
-class Batch(models.Model):
- id = models.BigAutoField(primary_key=True)
- name = models.CharField(max_length=255)
- start_date = models.DateField(null=True, blank=True)
- end_date = models.DateField(null=True, blank=True)
-
- def __str__(self):
- return self.name
-
- class Meta:
- verbose_name = "Batch"
- verbose_name_plural = "Batches"
-
-
class Routine(models.Model):
- id = models.BigAutoField(primary_key=True)
- name = models.CharField(max_length=255, null=True, blank=True)
- start_time = models.TimeField(null=True, blank=True)
- end_time = models.TimeField(null=True, blank=True)
- teacher = models.ForeignKey(
- Staff, related_name="routines", on_delete=models.CASCADE, null=True, blank=True
- )
+ course = models.ForeignKey(Course, on_delete=models.CASCADE)
+ subject = models.ForeignKey(Subject, on_delete=models.CASCADE)
+ teacher = models.ForeignKey(Staff, on_delete=models.CASCADE)
+ start_time = models.TimeField()
+ end_time = models.TimeField()
+ semester_or_year = models.PositiveIntegerField()
+ is_active = models.BooleanField(default=True)
def __str__(self):
- return self.name
+ return f"{self.subject.name} - {self.teacher.name} ({self.start_time} - {self.end_time})"
class Meta:
- verbose_name = "Routine"
- verbose_name_plural = "Routines"
-
-
-class StudentRoutine(models.Model):
- id = models.BigAutoField(primary_key=True)
- student = models.ForeignKey(
- Student, related_name="routines", on_delete=models.CASCADE, null=True
- )
- routine = models.ForeignKey(
- Routine, related_name="students", on_delete=models.CASCADE, null=True
- )
- course = models.ForeignKey(Course, on_delete=models.CASCADE, null=True, blank=True)
-
- def __str__(self):
- return self.student.name
+ ordering = ['start_time']
+ unique_together = ['course', 'subject', 'teacher', 'start_time', 'end_time', 'semester_or_year']
class Attendance(models.Model):
@@ -395,11 +432,11 @@ class Notice(models.Model):
class Staff_leave(models.Model):
LEAVE_STATUS_CHOICES = (
- (0, 'Pending'),
- (1, 'Approved'),
- (2, 'Rejected'),
+ (0, "Pending"),
+ (1, "Approved"),
+ (2, "Rejected"),
)
-
+
staff = models.ForeignKey(Staff, on_delete=models.CASCADE)
date = models.DateField()
message = models.TextField()
@@ -413,16 +450,16 @@ class Staff_leave(models.Model):
class Meta:
verbose_name = "Staff Leave"
verbose_name_plural = "Staff Leaves"
- ordering = ['-created_at']
+ ordering = ["-created_at"]
class Student_Leave(models.Model):
LEAVE_STATUS_CHOICES = (
- (0, 'Pending'),
- (1, 'Approved'),
- (2, 'Rejected'),
+ (0, "Pending"),
+ (1, "Approved"),
+ (2, "Rejected"),
)
-
+
student = models.ForeignKey(Student, on_delete=models.CASCADE)
date = models.DateField()
message = models.TextField()
@@ -436,13 +473,19 @@ class Student_Leave(models.Model):
class Meta:
verbose_name = "Student Leave"
verbose_name_plural = "Student Leaves"
- ordering = ['-created_at']
+ ordering = ["-created_at"]
class StudentFeedback(models.Model):
- student = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='feedbacks')
- teacher = models.ForeignKey(Staff, on_delete=models.CASCADE, related_name='feedbacks')
- rating = models.IntegerField(choices=[(i, i) for i in range(1, 6)]) # 1-5 rating
+ student = models.ForeignKey(
+ Student, on_delete=models.CASCADE, related_name="feedbacks"
+ )
+ teacher = models.ForeignKey(
+ Staff, on_delete=models.CASCADE, related_name="feedbacks"
+ )
+ rating = models.DecimalField(
+ max_digits=2, decimal_places=1, choices=[(i / 2, i / 2) for i in range(2, 11)]
+ ) # 1 to 5 rating
feedback_text = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -453,23 +496,31 @@ class StudentFeedback(models.Model):
class Meta:
verbose_name = "Student Feedback"
verbose_name_plural = "Student Feedbacks"
- ordering = ['-created_at']
- unique_together = ['student', 'teacher'] # Updated unique constraint
+ ordering = ["-created_at"]
+ unique_together = ["student", "teacher"]
class InstituteFeedback(models.Model):
FEEDBACK_TYPE_CHOICES = (
- ('General', 'General'),
- ('Facilities', 'Facilities'),
- ('Teaching', 'Teaching'),
- ('Infrastructure', 'Infrastructure'),
- ('Administration', 'Administration'),
+ ("General", "General"),
+ ("Facilities", "Facilities"),
+ ("Teaching", "Teaching"),
+ ("Infrastructure", "Infrastructure"),
+ ("Administration", "Administration"),
)
- institute = models.ForeignKey(Institute, on_delete=models.CASCADE, related_name='feedbacks')
- user = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='institute_feedbacks')
- feedback_type = models.CharField(max_length=20, choices=FEEDBACK_TYPE_CHOICES, default='General')
- rating = models.IntegerField(choices=[(i, i) for i in range(1, 6)]) # 1-5 rating
+ institute = models.ForeignKey(
+ Institute, on_delete=models.CASCADE, related_name="feedbacks"
+ )
+ user = models.ForeignKey(
+ Student, on_delete=models.CASCADE, related_name="institute_feedbacks"
+ )
+ feedback_type = models.CharField(
+ max_length=20, choices=FEEDBACK_TYPE_CHOICES, default="General"
+ )
+ rating = models.DecimalField(
+ max_digits=2, decimal_places=1, choices=[(i / 2, i / 2) for i in range(2, 11)]
+ ) # 1 to 5 rating
feedback_text = models.TextField()
is_anonymous = models.BooleanField(default=False)
is_public = models.BooleanField(default=True)
@@ -482,33 +533,97 @@ class InstituteFeedback(models.Model):
return f"{self.user.name} - {self.institute.name} - {self.feedback_type}"
class Meta:
- verbose_name = "Institute Feedback"
- verbose_name_plural = "Institute Feedbacks"
- ordering = ['-created_at']
- unique_together = ['user', 'institute', 'feedback_type'] # One feedback per type per user
+ verbose_name = "Student Institute Feedback"
+ verbose_name_plural = "Student Institute Feedbacks"
+ ordering = ["-created_at"]
+ unique_together = [
+ "user",
+ "institute",
+ "feedback_type",
+ ] # One feedback per type per user
@property
def display_name(self):
"""Return anonymous if feedback is anonymous, otherwise return user's name"""
- return 'Anonymous' if self.is_anonymous else self.user.name
+ return "Anonymous" if self.is_anonymous else self.user.name
+
+
+class StaffInstituteFeedback(models.Model):
+ """Model for tracking feedback from staff members to the institute"""
+ FEEDBACK_TYPE_CHOICES = (
+ ("General", "General"),
+ ("Facilities", "Facilities"),
+ ("Teaching", "Teaching"),
+ ("Infrastructure", "Infrastructure"),
+ ("Administration", "Administration"),
+ )
+
+ institute = models.ForeignKey(
+ Institute, on_delete=models.CASCADE, related_name="staff_feedbacks"
+ )
+ staff = models.ForeignKey(
+ Staff, on_delete=models.CASCADE, related_name="institute_feedbacks"
+ )
+ feedback_type = models.CharField(
+ max_length=20, choices=FEEDBACK_TYPE_CHOICES, default="General"
+ )
+ rating = models.DecimalField(
+ max_digits=2, decimal_places=1, choices=[(i / 2, i / 2) for i in range(2, 11)]
+ ) # 1 to 5 rating
+ feedback_text = models.TextField()
+ is_anonymous = models.BooleanField(default=False)
+ is_public = models.BooleanField(default=True)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ def __str__(self):
+ if self.is_anonymous:
+ return f"Anonymous - {self.institute.name} - {self.feedback_type}"
+ return f"{self.staff.name} - {self.institute.name} - {self.feedback_type}"
+
+ class Meta:
+ verbose_name = "Staff Institute Feedback"
+ verbose_name_plural = "Staff Institute Feedbacks"
+ ordering = ["-created_at"]
+ unique_together = [
+ "staff",
+ "institute",
+ "feedback_type",
+ ] # One feedback per type per staff
+
+ @property
+ def display_name(self):
+ """Return anonymous if feedback is anonymous, otherwise return staff's name"""
+ return "Anonymous" if self.is_anonymous else self.staff.name
class CourseTracking(models.Model):
PROGRESS_STATUS_CHOICES = (
- ('Not Started', 'Not Started'),
- ('In Progress', 'In Progress'),
- ('Completed', 'Completed'),
- ('Dropped', 'Dropped'),
+ ("Not Started", "Not Started"),
+ ("In Progress", "In Progress"),
+ ("Completed", "Completed"),
+ ("Dropped", "Dropped"),
)
- student = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='course_trackings')
- course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='student_trackings')
+ student = models.ForeignKey(
+ Student, on_delete=models.CASCADE, related_name="course_trackings"
+ )
+ course = models.ForeignKey(
+ Course, on_delete=models.CASCADE, related_name="student_trackings"
+ )
enrollment_date = models.DateField(auto_now_add=True)
start_date = models.DateField(null=True, blank=True)
expected_end_date = models.DateField(null=True, blank=True)
actual_end_date = models.DateField(null=True, blank=True)
- progress_status = models.CharField(max_length=20, choices=PROGRESS_STATUS_CHOICES, default='Not Started')
- completion_percentage = models.IntegerField(default=0, help_text="Percentage of course completion (0-100)")
+ progress_status = models.CharField(
+ max_length=20, choices=PROGRESS_STATUS_CHOICES, default="Not Started"
+ )
+ completion_percentage = models.IntegerField(
+ default=0, help_text="Percentage of course completion (0-100)"
+ )
+ current_semester = models.PositiveIntegerField(default=1)
+ semester_start_date = models.DateField(null=True, blank=True)
+ semester_end_date = models.DateField(null=True, blank=True)
notes = models.TextField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -516,18 +631,34 @@ class CourseTracking(models.Model):
class Meta:
verbose_name = "Course Tracking"
verbose_name_plural = "Course Trackings"
- unique_together = ['student', 'course'] # Ensure one tracking per student per course
- ordering = ['-created_at']
+ unique_together = ["student", "course"]
+ ordering = ["-created_at"]
def __str__(self):
return f"{self.student.name} - {self.course.name}"
def clean(self):
from django.core.exceptions import ValidationError
+
+ if not self.student or not self.course:
+ raise ValidationError("Both student and course are required")
+
if self.start_date and self.expected_end_date and self.start_date > self.expected_end_date:
raise ValidationError("Start date cannot be after expected end date")
+
if self.actual_end_date and self.start_date and self.actual_end_date < self.start_date:
raise ValidationError("Actual end date cannot be before start date")
+
+ if self.semester_start_date and self.semester_end_date and self.semester_start_date > self.semester_end_date:
+ raise ValidationError("Semester start date cannot be after semester end date")
+
+ if self.current_semester < 1:
+ raise ValidationError("Current semester must be at least 1")
+
+ if self.course.duration_type == "Semester":
+ max_semesters = self.course.duration * 2
+ if self.current_semester > max_semesters:
+ raise ValidationError(f"Current semester cannot exceed {max_semesters}")
def calculate_completion_percentage(self):
if not self.start_date or not self.expected_end_date:
@@ -536,9 +667,9 @@ class CourseTracking(models.Model):
today = date.today()
# If course is completed, return 100%
- if self.progress_status == 'Completed':
+ if self.progress_status == "Completed":
return 100
- elif self.progress_status == 'Dropped':
+ elif self.progress_status == "Dropped":
return 0
# Calculate total duration in days
@@ -552,11 +683,18 @@ class CourseTracking(models.Model):
return 0
# Calculate percentage based on course duration type
- if self.course.duration_type == 'Semester':
- # For semester-based courses (6 months)
- max_days = 180 # 6 months = 180 days
- if elapsed_days >= max_days:
- return 100
+ if self.course.duration_type == "Semester":
+ # For semester-based courses
+ semester_duration = 180 # 6 months = 180 days
+ total_semesters = self.course.duration * 2 # 2 semesters per year
+ completed_semesters = self.current_semester - 1
+ semester_progress = (completed_semesters / total_semesters) * 100
+
+ # If current semester is completed
+ if self.semester_end_date and today > self.semester_end_date:
+ semester_progress = (self.current_semester / total_semesters) * 100
+
+ return min(100, int(semester_progress))
else: # Yearly based
max_days = 365 # 1 year = 365 days
if elapsed_days >= max_days:
@@ -567,32 +705,81 @@ class CourseTracking(models.Model):
return percentage
def save(self, *args, **kwargs):
- # Validate dates
+ # Calculate dates if this is a new instance
+ if not self.pk:
+ self.start_date = self.start_date or date.today()
+
+ # Calculate expected end date based on course duration
+ if self.course.duration_type == "Year":
+ years = self.course.duration
+ else: # Semester based
+ years = self.course.duration # Duration is already in years
+
+ self.expected_end_date = self.start_date + timedelta(days=years * 365)
+
+ # Set initial semester dates
+ self.semester_start_date = self.start_date
+ self.semester_end_date = self.start_date + timedelta(days=180) # 6 months
+
+ # Validate dates and other fields
self.clean()
-
+
# Calculate completion percentage
self.completion_percentage = self.calculate_completion_percentage()
-
+
# Update progress status based on completion percentage
if self.completion_percentage >= 100:
- self.progress_status = 'Completed'
+ self.progress_status = "Completed"
if not self.actual_end_date:
self.actual_end_date = date.today()
elif self.completion_percentage > 0:
- self.progress_status = 'In Progress'
-
- super().save(*args, **kwargs)
+ self.progress_status = "In Progress"
+
+ try:
+ super().save(*args, **kwargs)
+ except Exception as e:
+ print(f"Error saving course tracking for {self.student.name}: {str(e)}")
+ raise
+
+ def advance_semester(self):
+ """Advance to next semester if possible"""
+ if not self.course or self.course.duration_type != "Semester":
+ return False
+
+ total_semesters = self.course.duration * 2
+ if self.current_semester < total_semesters:
+ self.current_semester += 1
+ self.semester_start_date = date.today()
+ self.semester_end_date = self.semester_start_date + timedelta(
+ days=180
+ ) # 6 months
+ # Update student's current semester as well
+ self.student.current_semester = self.current_semester
+ self.student.save()
+ self.save()
+ return True
+ return False
@property
def is_completed(self):
- return self.progress_status == 'Completed'
+ return self.progress_status == "Completed"
@property
def is_active(self):
- return self.progress_status == 'In Progress'
+ return self.progress_status == "In Progress"
@property
def remaining_days(self):
+ """Returns remaining days in current semester"""
+ if not self.semester_end_date:
+ return None
+ today = date.today()
+ remaining = (self.semester_end_date - today).days
+ return max(0, remaining)
+
+ @property
+ def total_remaining_days(self):
+ """Returns remaining days for entire course"""
if not self.expected_end_date:
return None
today = date.today()
@@ -610,25 +797,19 @@ class CourseTracking(models.Model):
return (self.expected_end_date - self.start_date).days
def get_completion_percentage(self):
- """
- Get completion percentage with caching
- """
return get_cached_percentage(self.id)
def force_update_percentage(self):
- """
- Force update the completion percentage and clear cache
- """
- cache_key = f'course_tracking_percentage_{self.id}'
+ cache_key = f"course_tracking_percentage_{self.id}"
cache.delete(cache_key)
- self.save() # This will recalculate and update the percentage
+ self.save()
def get_cached_percentage(tracking_id):
"""
Get cached completion percentage for a course tracking
"""
- cache_key = f'course_tracking_percentage_{tracking_id}'
+ cache_key = f"course_tracking_percentage_{tracking_id}"
percentage = cache.get(cache_key)
if percentage is None:
try:
@@ -646,8 +827,95 @@ def update_completion_percentage(sender, instance, **kwargs):
Signal to update completion percentage after save
Also caches the percentage for 1 hour to avoid frequent recalculations
"""
- cache_key = f'course_tracking_percentage_{instance.id}'
+ cache_key = f"course_tracking_percentage_{instance.id}"
cache.set(cache_key, instance.completion_percentage, 3600) # Cache for 1 hour
+@receiver(post_save, sender=Student)
+def create_course_tracking(sender, instance, created, **kwargs):
+ """
+ Signal handler to create CourseTracking when a new student is created
+ or when a course is assigned to an existing student
+ """
+ print(f"Signal triggered for student: {instance.name}")
+ print(f"Created: {created}")
+ print(f"Course: {instance.course}")
+ print(f"Current Semester: {instance.current_semester}")
+ # Check if student has a course assigned
+ if instance.course:
+ try:
+ # Check if course tracking already exists
+ existing_tracking = CourseTracking.objects.filter(
+ student=instance,
+ course=instance.course
+ ).first()
+
+ if existing_tracking:
+ print(f"Course tracking already exists for {instance.name}")
+ return
+
+ # Calculate dates based on course duration
+ start_date = instance.joining_date or date.today()
+
+ # Calculate expected end date based on course duration type
+ if instance.course.duration_type == "Year":
+ # For year-based courses, each year is 365 days
+ years = instance.course.duration
+ duration_days = years * 365
+ else: # Semester based
+ # For semester-based courses:
+ # - Each year has 2 semesters
+ # - Total years is the same as course duration
+ years = instance.course.duration
+ duration_days = years * 365 # Use full years for total duration
+
+ expected_end_date = start_date + timedelta(days=duration_days)
+
+ # Create course tracking with transaction
+ with transaction.atomic():
+ course_tracking = CourseTracking.objects.create(
+ student=instance,
+ course=instance.course,
+ start_date=start_date,
+ expected_end_date=expected_end_date,
+ current_semester=instance.current_semester,
+ semester_start_date=start_date,
+ semester_end_date=start_date + timedelta(days=180) # 6 months for first semester
+ )
+
+ # Force initial percentage calculation
+ course_tracking.force_update_percentage()
+
+ print(f"Successfully created course tracking for student {instance.name}")
+ print(f"Start Date: {start_date}")
+ print(f"Expected End Date: {expected_end_date}")
+ print(f"Current Semester: {instance.current_semester}")
+
+ except Exception as e:
+ print(f"Error creating course tracking for student {instance.name}: {str(e)}")
+ # Log the error but don't prevent student creation
+ # You might want to add proper logging here instead of print statements
+ else:
+ print(f"No course assigned to student {instance.name}")
+
+
+@receiver(post_save, sender=Student)
+def sync_student_semester(sender, instance, created, **kwargs):
+ """Signal handler to sync student's current_semester with course tracking"""
+ if instance.course:
+ try:
+ course_tracking = CourseTracking.objects.get(student=instance, course=instance.course)
+ if course_tracking.current_semester != instance.current_semester:
+ course_tracking.current_semester = instance.current_semester
+ course_tracking.save()
+ except CourseTracking.DoesNotExist:
+ pass
+
+
+@receiver(post_save, sender=CourseTracking)
+def sync_course_tracking_semester(sender, instance, created, **kwargs):
+ """Signal handler to sync course tracking's current_semester with student"""
+ if instance.student.current_semester != instance.current_semester:
+ instance.student.current_semester = instance.current_semester
+ instance.student.save()
diff --git a/app/serializers.py b/app/serializers.py
new file mode 100644
index 0000000..8dfd092
--- /dev/null
+++ b/app/serializers.py
@@ -0,0 +1,179 @@
+import django.db
+from rest_framework import serializers
+from .models import (
+ Attendance, AttendanceRecord, Batch, Course, FCMDevice, Institute,
+ Notice, Routine, Staff, Student, Subject, Staff_leave, Student_Leave,
+ StudentFeedback, InstituteFeedback, StaffInstituteFeedback, CourseTracking
+)
+
+class FCMDeviceSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = FCMDevice
+ fields = ['id', 'token']
+
+class InstituteSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Institute
+ fields = ['id', 'name', 'phone', 'email', 'address', 'pan_no', 'reg_no', 'logo', 'description']
+
+class SubjectSerializer(serializers.ModelSerializer):
+ syllabus_pdf_url = serializers.SerializerMethodField()
+
+ class Meta:
+ model = Subject
+ fields = ['id', 'name', 'course', 'semester_or_year', 'syllabus_pdf', 'syllabus_pdf_url']
+
+ def get_syllabus_pdf_url(self, obj):
+ return obj.get_pdf_url()
+
+class CourseSerializer(serializers.ModelSerializer):
+ subjects = SubjectSerializer(many=True, read_only=True)
+
+ class Meta:
+ model = Course
+ fields = ['id', 'name', 'duration', 'duration_type', 'subjects']
+
+class BatchSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Batch
+ fields = ['id', 'name', 'start_date', 'end_date']
+
+class StudentSerializer(serializers.ModelSerializer):
+ batches = BatchSerializer(many=True, read_only=True)
+ course = CourseSerializer(read_only=True)
+
+ class Meta:
+ model = Student
+ fields = [
+ 'id', 'name', 'status', 'gender', 'birth_date', 'email', 'phone',
+ 'temporary_address', 'permanent_address', 'marital_status', 'parent_name',
+ 'parent_phone', 'citizenship_no', 'image', 'batches', 'course',
+ 'current_semester', 'joining_date'
+ ]
+ read_only_fields = ['id']
+
+class StaffSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Staff
+ fields = [
+ 'id', 'name', 'gender', 'designation', 'birth_date', 'phone', 'email',
+ 'temporary_address', 'permanent_address', 'marital_status', 'parent_name',
+ 'parent_phone', 'citizenship_no', 'passport', 'image', 'joining_date',
+ 'is_active'
+ ]
+ read_only_fields = ['id']
+
+class RoutineSerializer(serializers.ModelSerializer):
+ teacher = StaffSerializer(read_only=True)
+ subject = SubjectSerializer(read_only=True)
+ course = CourseSerializer(read_only=True)
+
+ class Meta:
+ model = Routine
+ fields = [
+ 'id', 'course', 'subject', 'teacher', 'start_time', 'end_time',
+ 'semester_or_year', 'is_active'
+ ]
+
+class AttendanceRecordSerializer(serializers.ModelSerializer):
+ student = StudentSerializer(read_only=True)
+
+ class Meta:
+ model = AttendanceRecord
+ fields = ['id', 'attendance', 'student', 'student_attend']
+
+class AttendanceSerializer(serializers.ModelSerializer):
+ records = AttendanceRecordSerializer(many=True, read_only=True)
+ routine = RoutineSerializer(read_only=True)
+ teacher = StaffSerializer(read_only=True)
+
+ class Meta:
+ model = Attendance
+ fields = ['id', 'date', 'routine', 'teacher', 'teacher_attend', 'class_status', 'records']
+
+class NoticeSerializer(serializers.ModelSerializer):
+ created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
+ image_url = serializers.SerializerMethodField()
+
+ class Meta:
+ model = Notice
+ fields = ['id', 'title', 'message', 'image', 'image_url', 'created_at']
+ read_only_fields = ['created_at']
+
+ def get_image_url(self, obj):
+ if obj.image:
+ return obj.image.url
+ return None
+
+class StaffLeaveSerializer(serializers.ModelSerializer):
+ staff = StaffSerializer(read_only=True)
+ created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
+ updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
+
+ class Meta:
+ model = Staff_leave
+ fields = ['id', 'staff', 'date', 'message', 'status', 'created_at', 'updated_at']
+ read_only_fields = ['created_at', 'updated_at']
+
+class StudentLeaveSerializer(serializers.ModelSerializer):
+ student = StudentSerializer(read_only=True)
+ created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
+ updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
+
+ class Meta:
+ model = Student_Leave
+ fields = ['id', 'student', 'date', 'message', 'status', 'created_at', 'updated_at']
+ read_only_fields = ['created_at', 'updated_at']
+
+class StudentFeedbackSerializer(serializers.ModelSerializer):
+ student = StudentSerializer(read_only=True)
+ teacher = StaffSerializer(read_only=True)
+ created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
+ updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
+
+ class Meta:
+ model = StudentFeedback
+ fields = ['id', 'student', 'teacher', 'rating', 'feedback_text', 'created_at', 'updated_at']
+ read_only_fields = ['created_at', 'updated_at']
+
+class InstituteFeedbackSerializer(serializers.ModelSerializer):
+ user = StudentSerializer(read_only=True)
+ created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
+ updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
+
+ class Meta:
+ model = InstituteFeedback
+ fields = [
+ 'id', 'institute', 'user', 'feedback_type', 'rating', 'feedback_text',
+ 'is_anonymous', 'is_public', 'created_at', 'updated_at'
+ ]
+ read_only_fields = ['created_at', 'updated_at']
+
+class StaffInstituteFeedbackSerializer(serializers.ModelSerializer):
+ staff = StaffSerializer(read_only=True)
+ created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
+ updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
+
+ class Meta:
+ model = StaffInstituteFeedback
+ fields = [
+ 'id', 'institute', 'staff', 'feedback_type', 'rating', 'feedback_text',
+ 'is_anonymous', 'is_public', 'created_at', 'updated_at'
+ ]
+ read_only_fields = ['created_at', 'updated_at']
+
+class CourseTrackingSerializer(serializers.ModelSerializer):
+ student = StudentSerializer(read_only=True)
+ course = CourseSerializer(read_only=True)
+ created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
+ updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
+
+ class Meta:
+ model = CourseTracking
+ fields = [
+ 'id', 'student', 'course', 'enrollment_date', 'start_date', 'expected_end_date',
+ 'actual_end_date', 'progress_status', 'completion_percentage', 'current_semester',
+ 'semester_start_date', 'semester_end_date', 'notes', 'created_at', 'updated_at'
+ ]
+ read_only_fields = ['created_at', 'updated_at', 'enrollment_date']
+
diff --git a/app/staffviews.py b/app/staffviews.py
new file mode 100644
index 0000000..5096594
--- /dev/null
+++ b/app/staffviews.py
@@ -0,0 +1,585 @@
+from django.shortcuts import render, redirect
+from django.http import JsonResponse
+from django.contrib.auth.decorators import login_required
+from django.db.models import Avg, Q
+from django.utils import timezone
+from datetime import timedelta, datetime
+from django.views.decorators.http import require_http_methods, require_GET
+from django.contrib import messages
+from django.contrib.auth.hashers import check_password, make_password
+from .models import (
+ Staff,
+ Student,
+ Routine,
+ Attendance,
+ AttendanceRecord,
+ StudentFeedback,
+ Staff_leave,
+ Notice,
+ Institute,
+ StaffInstituteFeedback,
+ Subject,
+ InstituteFeedback
+)
+from app.admin import custom_admin_site
+
+
+@login_required
+def teacherDashboard(request):
+ """View to display the teacher dashboard with their classes and student information"""
+ teacher = Staff.objects.get(phone=request.user.phone) # Get the Staff object
+ today = timezone.now().date()
+ current_time = timezone.now().time()
+ last_week = today - timedelta(days=7)
+
+ # Get only the active routines assigned to this teacher
+ today_routines = Routine.objects.filter(
+ teacher=teacher, # Filter by the current teacher
+ is_active=True, # Only include active routines
+ ).order_by("start_time")
+
+ # Process routines to include status and attendance
+ processed_routines = []
+ for routine in today_routines:
+ attendance = routine.attendances.filter(date=today).first()
+
+ # Get total students from the course associated with the subject
+ total_students = Student.objects.filter(course=routine.subject.course).count()
+ attendance_count = (
+ 0
+ if not attendance
+ else AttendanceRecord.objects.filter(
+ attendance=attendance, student_attend=True
+ ).count()
+ )
+
+ # Add routine to processed_routines
+ processed_routines.append(
+ {
+ "id": routine.id, # Add the routine ID for use in the template
+ "name": routine.subject.name,
+ "start_time": routine.start_time,
+ "end_time": routine.end_time,
+ "is_completed": attendance is not None, # Only mark as completed if attendance exists
+ "is_ongoing": routine.start_time <= current_time <= routine.end_time,
+ "total_students": total_students,
+ "attendance_count": attendance_count,
+ "semester_or_year": routine.semester_or_year,
+ "course_name": routine.subject.course.name,
+ "has_attendance": attendance is not None, # Flag to check if attendance has been taken
+ }
+ )
+
+ # Calculate class statistics
+ total_classes = today_routines.count()
+ completed_classes = len([r for r in processed_routines if r["is_completed"]])
+ remaining_classes = total_classes - completed_classes
+
+ # Get student statistics - count unique students from teacher's assigned subjects and semesters
+ total_students = Student.objects.filter(status="Active").count()
+
+ # Get present and absent students for today
+ present_students = AttendanceRecord.objects.filter(
+ attendance__date=today,
+ student_attend=True,
+ attendance__routine__teacher=teacher
+ ).values('student').distinct().count()
+
+ # Calculate absent students based on teacher's classes today
+ today_class_students = set()
+ for routine in today_routines:
+ students = Student.objects.filter(
+ course=routine.subject.course,
+ current_semester=routine.semester_or_year,
+ status="Active"
+ )
+ today_class_students.update(students.values_list('id', flat=True))
+
+ absent_students = len(today_class_students) - present_students if today_class_students else 0
+
+ # Get average rating from student feedback
+ avg_rating = StudentFeedback.objects.filter(
+ teacher=teacher
+ ).aggregate(Avg('rating'))['rating__avg'] or 0
+ avg_rating_rounded = round(avg_rating)
+ remaining_stars = 5 - avg_rating_rounded
+
+ # Get leave statistics
+ pending_leaves = Staff_leave.objects.filter(staff=teacher, status=0).count()
+ approved_leaves = Staff_leave.objects.filter(staff=teacher, status=1).count()
+ rejected_leaves = Staff_leave.objects.filter(staff=teacher, status=2).count()
+
+ # Get recent leaves
+ recent_leaves = Staff_leave.objects.filter(staff=teacher).order_by('-created_at')[:5]
+
+ # Get recent feedback
+ recent_feedback = StudentFeedback.objects.filter(teacher=teacher).order_by('-created_at')[:5]
+
+ # Get notices
+ notices = Notice.objects.all().order_by('-created_at')[:5]
+
+ # Get feedback types for institute feedback
+ feedback_types = InstituteFeedback.FEEDBACK_TYPE_CHOICES
+
+ # Get institute feedback
+ institute_feedback = StaffInstituteFeedback.objects.filter(staff=teacher).order_by('-created_at')[:5]
+
+ # Get all routines for the teacher (for the schedule)
+ all_routines = Routine.objects.filter(
+ teacher=teacher,
+ is_active=True,
+ semester_or_year__in=Student.objects.filter(status="Active")
+ .values_list('current_semester', flat=True)
+ .distinct()
+ ).order_by('start_time')
+
+ # Get all attendance records for quick access
+ recent_attendance = (
+ Attendance.objects.filter(teacher=teacher, date__gte=last_week)
+ .select_related("routine")
+ .order_by("-date")
+ )
+
+ # Add present/total counts to each attendance record
+ for attendance in recent_attendance:
+ attendance.present_count = AttendanceRecord.objects.filter(
+ attendance=attendance, student_attend=True
+ ).count()
+ attendance.total_count = AttendanceRecord.objects.filter(
+ attendance=attendance
+ ).count()
+
+ # Attendance Management Data
+ routines = Routine.objects.filter(
+ teacher=teacher,
+ is_active=True
+ ).order_by('start_time')
+
+ context = {
+ "teacher": teacher,
+ "title": "Teacher Dashboard",
+ "today_routines": processed_routines,
+ "total_classes": total_classes,
+ "completed_classes": completed_classes,
+ "remaining_classes": remaining_classes,
+ "total_students": total_students,
+ "present_students": present_students,
+ "absent_students": absent_students,
+ "avg_rating": avg_rating,
+ "avg_rating_rounded": avg_rating_rounded,
+ "remaining_stars": remaining_stars,
+ "pending_leaves": pending_leaves,
+ "approved_leaves": approved_leaves,
+ "rejected_leaves": rejected_leaves,
+ "recent_leaves": recent_leaves,
+ "recent_feedback": recent_feedback,
+ "notices": notices,
+ "feedback_types": feedback_types,
+ "institute_feedback": institute_feedback,
+ "all_routines": all_routines,
+ "recent_attendance": recent_attendance,
+ # Attendance Management Context
+ "routines": routines,
+ "today": today,
+ }
+
+ return render(request, "teacher_dashboard.html", context)
+
+
+@login_required
+def request_staff_leave(request):
+ """View to handle leave requests from staff"""
+ if request.method == "POST":
+ staff = request.user
+ date = request.POST.get("date")
+ message = request.POST.get("message")
+
+ try:
+ # Convert date string to timezone-aware datetime
+ leave_date = timezone.datetime.strptime(date, "%Y-%m-%d")
+ leave_date = timezone.make_aware(leave_date)
+ today = timezone.now()
+
+ if leave_date.date() < today.date():
+ messages.error(request, "Leave date cannot be in the past.")
+ return redirect("teacherDashboard")
+
+ # Check if leave already exists for this date
+ existing_leave = Staff_leave.objects.filter(
+ staff=staff, date=leave_date.date()
+ ).first()
+
+ if existing_leave:
+ messages.error(
+ request, "You have already requested leave for this date."
+ )
+ return redirect("teacherDashboard")
+
+ # Create leave request
+ leave = Staff_leave.objects.create(
+ staff=staff,
+ date=leave_date.date(),
+ message=message,
+ status=0, # Pending status
+ )
+ messages.success(request, "Leave request submitted successfully.")
+ except Exception as e:
+ messages.error(request, f"Error submitting leave request: {str(e)}")
+
+ return redirect("teacherDashboard")
+
+ return redirect("teacherDashboard")
+
+
+@login_required
+def submit_staff_institute_feedback(request):
+ """View to handle feedback submission from staff to institute"""
+ if request.method == "POST":
+ staff = request.user
+ feedback_type = request.POST.get("feedback_type")
+ rating = request.POST.get("rating")
+ feedback_text = request.POST.get("feedback_text")
+ is_anonymous = request.POST.get("is_anonymous") == "on"
+
+ try:
+ # Convert to float first to handle half-stars
+ rating_value = float(rating)
+ if rating_value < 0.5 or rating_value > 5:
+ messages.error(request, "Rating must be between 0.5 and 5")
+ return redirect("teacherDashboard")
+
+ # Check if it's a valid half or full star rating
+ if rating_value % 0.5 != 0:
+ messages.error(request, "Rating must be a whole or half number")
+ return redirect("teacherDashboard")
+
+ # Get the institute (assuming one institute for now)
+ institute = Institute.objects.first()
+
+ if not institute:
+ # Create a default institute if none exists
+ institute = Institute.objects.create(
+ name="Default Institute",
+ phone="123456789",
+ email="default@example.com",
+ )
+
+ # Get the staff object
+ teacher = Staff.objects.get(phone=staff.phone)
+
+ # Check if feedback already exists for this type
+ existing_feedback = StaffInstituteFeedback.objects.filter(
+ staff=teacher, institute=institute, feedback_type=feedback_type
+ ).first()
+
+ if existing_feedback:
+ # Update existing feedback
+ existing_feedback.rating = rating_value
+ existing_feedback.feedback_text = feedback_text
+ existing_feedback.is_anonymous = is_anonymous
+ existing_feedback.save()
+ messages.success(request, "Institute feedback updated successfully.")
+ else:
+ # Create new feedback
+ feedback = StaffInstituteFeedback.objects.create(
+ staff=teacher,
+ institute=institute,
+ feedback_type=feedback_type,
+ rating=rating_value,
+ feedback_text=feedback_text,
+ is_anonymous=is_anonymous,
+ )
+ messages.success(request, "Institute feedback submitted successfully.")
+ except ValueError:
+ messages.error(request, "Invalid rating value")
+ except Exception as e:
+ messages.error(request, f"Error submitting institute feedback: {str(e)}")
+
+ return redirect("teacherDashboard")
+
+ return redirect("teacherDashboard")
+
+
+@login_required
+def save_attendance(request):
+ """View to save attendance data"""
+ if request.method == "POST":
+ teacher = Staff.objects.get(phone=request.user.phone)
+ routine_id = request.POST.get("routine_id")
+ date_str = request.POST.get("date")
+
+ # Convert class_status to boolean - "Conducted" is True, others are False
+ class_status_str = request.POST.get("class_status", "True")
+ class_status = class_status_str == "True"
+
+ teacher_attend = request.POST.get("teacher_attend") == "on"
+
+ try:
+ # Validate routine - strict check to ensure teacher can only access their assigned routines
+ routine = Routine.objects.get(
+ id=routine_id,
+ teacher=teacher, # Ensure this routine is assigned to this teacher
+ is_active=True, # Only allow active routines
+ )
+
+ # Validate date
+ attendance_date = timezone.datetime.strptime(date_str, "%Y-%m-%d").date()
+
+ # Get or create attendance record
+ attendance, created = Attendance.objects.get_or_create(
+ routine=routine,
+ date=attendance_date,
+ defaults={
+ "teacher": teacher,
+ "class_status": class_status,
+ "teacher_attend": teacher_attend,
+ },
+ )
+
+ if not created:
+ # Update existing attendance
+ attendance.class_status = class_status
+ attendance.teacher_attend = teacher_attend
+ attendance.save()
+
+ # Process student attendance
+ for key, value in request.POST.items():
+ if key.startswith("student_"):
+ student_id = key.replace("student_", "")
+ student_attend = value == "on"
+
+ try:
+ # Convert student_id to integer to ensure proper lookup
+ student_id_int = int(student_id)
+ student = Student.objects.get(id=student_id_int)
+
+ # Get or create attendance record for this student
+ record, _ = AttendanceRecord.objects.get_or_create(
+ attendance=attendance,
+ student=student,
+ defaults={"student_attend": student_attend},
+ )
+
+ # Update if record already exists
+ if record.student_attend != student_attend:
+ record.student_attend = student_attend
+ record.save()
+
+ except Student.DoesNotExist:
+ continue
+
+ messages.success(
+ request,
+ f"Attendance for {attendance_date.strftime('%d %b, %Y')} saved successfully.",
+ )
+
+ except Routine.DoesNotExist:
+ messages.error(
+ request,
+ "You do not have permission to take attendance for this class. Only teachers assigned to this class can manage its attendance.",
+ )
+ except ValueError:
+ messages.error(request, "Invalid date format.")
+ except Exception as e:
+ messages.error(request, f"Error saving attendance: {str(e)}")
+
+ # Redirect to teacher dashboard after saving attendance
+ return redirect("teacherDashboard")
+
+ return redirect("teacherDashboard")
+
+
+@login_required
+@require_GET
+def get_subject_schedule(request):
+ """View to get schedule for a subject"""
+ teacher = Staff.objects.get(phone=request.user.phone)
+ subject_id = request.GET.get("subject_id")
+
+ if not subject_id:
+ return JsonResponse({"success": False, "message": "Subject ID is required"})
+
+ try:
+ routines = Routine.objects.filter(
+ teacher=teacher, subject_id=subject_id, is_active=True
+ ).values("id", "start_time", "end_time", "semester_or_year")
+
+ return JsonResponse({"success": True, "schedule": list(routines)})
+ except Exception as e:
+ return JsonResponse({"success": False, "message": str(e)})
+
+
+@login_required
+def teacher_update_profile_picture(request):
+ """View to handle profile picture update for teachers"""
+ if request.method == "POST":
+ teacher = Staff.objects.get(phone=request.user.phone)
+ profile_picture = request.FILES.get("profile_picture")
+
+ if profile_picture:
+ try:
+ # Update the teacher's profile picture
+ teacher.image = profile_picture
+ teacher.save()
+ messages.success(request, "Profile picture updated successfully.")
+ except Exception as e:
+ messages.error(request, f"Error updating profile picture: {str(e)}")
+ else:
+ messages.error(request, "No profile picture uploaded.")
+
+ return redirect("teacherDashboard")
+
+ return redirect("teacherDashboard")
+
+
+@login_required
+def teacher_change_password(request):
+ """View to handle password change for teachers"""
+ if request.method == "POST":
+ current_password = request.POST.get("current_password")
+ new_password = request.POST.get("new_password")
+ confirm_password = request.POST.get("confirm_password")
+
+ # Get the current user
+ user = request.user
+
+ # Validate inputs
+ if not current_password or not new_password or not confirm_password:
+ messages.error(request, "All password fields are required.")
+ return redirect("teacherDashboard")
+
+ # Check if current password is correct
+ if not check_password(current_password, user.password):
+ messages.error(request, "Current password is incorrect.")
+ return redirect("teacherDashboard")
+
+ # Check if new passwords match
+ if new_password != confirm_password:
+ messages.error(request, "New passwords do not match.")
+ return redirect("teacherDashboard")
+
+ # Check if new password is strong enough (add more criteria as needed)
+ if len(new_password) < 8:
+ messages.error(request, "New password must be at least 8 characters long.")
+ return redirect("teacherDashboard")
+
+ try:
+ # Update the password
+ user.password = make_password(new_password)
+ user.save()
+ messages.success(
+ request,
+ "Password changed successfully. Please log in again with your new password.",
+ )
+ return redirect(
+ "login"
+ ) # Redirect to login page to log in with new password
+ except Exception as e:
+ messages.error(request, f"Error changing password: {str(e)}")
+
+ return redirect("teacherDashboard")
+
+ return redirect("teacherDashboard")
+
+
+@login_required
+def get_attendance_form(request):
+ """View to handle AJAX request for getting attendance form"""
+ routine_id = request.GET.get('routine_id')
+ date_str = request.GET.get('date')
+
+ if not routine_id or not date_str:
+ return JsonResponse({
+ 'error': 'Missing required parameters'
+ })
+
+ try:
+ routine = Routine.objects.get(id=routine_id, teacher=request.user)
+ date = datetime.strptime(date_str, '%Y-%m-%d').date()
+
+ # Get existing attendance data if any
+ attendance_data = {
+ 'teacher_attend': False,
+ 'class_status': True,
+ 'records': {}
+ }
+
+ try:
+ # Get attendance record with all related data in one query
+ attendance = Attendance.objects.select_related('routine').get(
+ routine=routine,
+ date=date
+ )
+
+ # Get all attendance records for this attendance in one query
+ records = AttendanceRecord.objects.filter(attendance=attendance).select_related('student')
+
+ attendance_data = {
+ 'id': attendance.id,
+ 'date': attendance.date.strftime('%Y-%m-%d'),
+ 'teacher_attend': attendance.teacher_attend,
+ 'class_status': attendance.class_status,
+ 'records': {
+ str(record.student.id): record.student_attend
+ for record in records
+ }
+ }
+ except Attendance.DoesNotExist:
+ # No existing attendance record found - return default values
+ pass
+
+ return JsonResponse(attendance_data)
+
+ except Routine.DoesNotExist:
+ return JsonResponse({
+ 'error': 'Invalid routine selected'
+ }, status=404)
+ except ValueError:
+ return JsonResponse({
+ 'error': 'Invalid date format'
+ }, status=400)
+ except Exception as e:
+ return JsonResponse({
+ 'error': str(e)
+ }, status=500)
+
+
+@login_required
+def get_students(request):
+ """View to handle AJAX request for getting students list"""
+ from django.http import JsonResponse
+
+ routine_id = request.GET.get('routine_id')
+
+ if not routine_id:
+ return JsonResponse({'error': 'Missing routine_id parameter'}, status=400)
+
+ try:
+ routine = Routine.objects.get(id=routine_id, teacher=request.user)
+
+ # Get students for this routine
+ students = Student.objects.filter(
+ course=routine.subject.course,
+ current_semester=routine.semester_or_year
+ ).order_by('name')
+
+ # If no students found, try a more flexible approach
+ if not students.exists():
+ students = Student.objects.filter(
+ course=routine.subject.course
+ ).order_by('name')
+
+ # Format student data for JSON response
+ student_data = [{
+ 'id': student.id,
+ 'name': student.name
+ } for student in students]
+
+ return JsonResponse({
+ 'students': student_data
+ })
+
+ except Routine.DoesNotExist:
+ return JsonResponse({'error': 'Invalid routine selected'}, status=404)
+ except Exception as e:
+ return JsonResponse({'error': str(e)}, status=500)
diff --git a/app/studentviews.py b/app/studentviews.py
new file mode 100644
index 0000000..33e2500
--- /dev/null
+++ b/app/studentviews.py
@@ -0,0 +1,511 @@
+from django.shortcuts import render, redirect, get_object_or_404
+from django.http import JsonResponse, HttpResponse
+from django.contrib.auth.decorators import login_required
+from django.contrib import messages
+from django.db.models import Count, Q, Avg
+from django.utils import timezone
+from datetime import timedelta
+from django.views.decorators.http import require_http_methods, require_GET
+from .models import (
+ Student,
+ Course,
+ CourseTracking,
+ Student_Leave,
+ Staff,
+ Attendance,
+ AttendanceRecord,
+ StudentFeedback,
+ Routine,
+ Notice,
+ Institute,
+ InstituteFeedback,
+ Subject,
+)
+
+
+@login_required
+def studentDashboard(request):
+ """View to display the student dashboard with their personal information and progress"""
+ student = Student.objects.get(phone=request.user.phone) # Get the Student object
+ today = timezone.now().date()
+ last_week = today - timedelta(days=7)
+ last_month = today - timedelta(days=30)
+
+ # Get all teachers for feedback form
+ teachers = Staff.objects.filter(
+ Q(designation__icontains="Teacher")
+ | Q(designation__icontains="Staff")
+ | Q(designation__icontains="Principal")
+ | Q(designation__isnull=True)
+ | Q(designation="")
+ ).order_by("name")
+
+ # Get feedback types for institute feedback
+ feedback_types = InstituteFeedback.FEEDBACK_TYPE_CHOICES
+
+ # Course Progress and Tracking
+ course_trackings = CourseTracking.objects.filter(student=student).select_related(
+ "course"
+ )
+ total_courses = course_trackings.count()
+ completed_courses = course_trackings.filter(progress_status="Completed").count()
+ in_progress_courses = course_trackings.filter(progress_status="In Progress").count()
+ dropped_courses = course_trackings.filter(progress_status="Dropped").count()
+
+ # Calculate overall completion percentage
+ overall_completion = 0
+ if total_courses > 0:
+ total_percentage = sum(
+ tracking.completion_percentage for tracking in course_trackings
+ )
+ overall_completion = total_percentage / total_courses
+
+ # Course Summary
+ course_summary = {
+ "total_courses": total_courses,
+ "completed_courses": completed_courses,
+ "ongoing_courses": in_progress_courses,
+ "completion_percentage": overall_completion,
+ }
+
+ # Course Details with enhanced information
+ course_details = []
+ for tracking in course_trackings:
+ # Calculate expected end date based on course duration
+ if tracking.start_date:
+ if tracking.course.duration_type == "Year":
+ duration_days = tracking.course.duration * 365 # Years to days
+ else: # Semester based
+ duration_days = (
+ tracking.course.duration * 365
+ ) # Use full years for total duration
+
+ expected_end_date = tracking.start_date + timedelta(days=duration_days)
+ else:
+ expected_end_date = None
+
+ course_details.append(
+ {
+ "name": tracking.course.name,
+ "start_date": tracking.start_date,
+ "expected_end_date": expected_end_date,
+ "actual_end_date": tracking.actual_end_date,
+ "completion_percentage": tracking.completion_percentage,
+ "progress_status": tracking.progress_status,
+ "current_semester": tracking.current_semester,
+ "semester_start_date": tracking.semester_start_date,
+ "semester_end_date": tracking.semester_end_date,
+ "remaining_days": tracking.remaining_days,
+ "is_completed": tracking.is_completed,
+ "is_active": tracking.is_active,
+ "duration_type": tracking.course.duration_type,
+ "duration": tracking.course.duration,
+ "total_duration_days": tracking.total_duration_days,
+ "notes": tracking.notes,
+ "book": tracking.course.book
+ if hasattr(tracking.course, "book")
+ else None,
+ "topics": [], # This will be populated if you have topic tracking
+ }
+ )
+
+ # Get current subjects based on current semester
+ current_subjects = student.get_current_subjects()
+
+ # Attendance Statistics with enhanced information
+ attendance_records = AttendanceRecord.objects.filter(student=student)
+ total_classes = attendance_records.count()
+ classes_attended = attendance_records.filter(student_attend=True).count()
+ attendance_percentage = (
+ (classes_attended / total_classes * 100) if total_classes > 0 else 0
+ )
+
+ # Calculate attendance streaks
+ current_streak = 0
+ longest_streak = 0
+ temp_streak = 0
+ last_attendance_date = None
+
+ for record in attendance_records.order_by("-attendance__date"):
+ if record.student_attend:
+ if last_attendance_date is None:
+ last_attendance_date = record.attendance.date
+ current_streak = 1
+ temp_streak = 1
+ elif (last_attendance_date - record.attendance.date).days == 1:
+ temp_streak += 1
+ if temp_streak > longest_streak:
+ longest_streak = temp_streak
+ if record.attendance.date == today - timedelta(days=current_streak):
+ current_streak += 1
+ else:
+ temp_streak = 1
+ else:
+ temp_streak = 0
+ if record.attendance.date == today:
+ current_streak = 0
+
+ # Attendance Summary
+ attendance_summary = {
+ "total_classes": total_classes,
+ "classes_attended": classes_attended,
+ "attendance_percentage": attendance_percentage,
+ "current_streak": current_streak,
+ "longest_streak": longest_streak,
+ }
+
+ # Get attendance records with related information
+ attendance_records = attendance_records.select_related(
+ "attendance", "attendance__routine", "attendance__teacher"
+ ).order_by("-attendance__date")
+
+ # Get recent attendance (last 7 days)
+ recent_attendance = attendance_records.filter(
+ attendance__date__gte=last_week
+ ).order_by("-attendance__date")
+
+ # Get feedback
+ recent_feedback = student.feedbacks.all().order_by("-created_at")[:5]
+ institute_feedback = student.institute_feedbacks.all().order_by("-created_at")[:5]
+
+ # Get notices
+ notices = Notice.objects.all().order_by("-created_at")
+
+ # Get student leaves with status information
+ student_leaves = Student_Leave.objects.filter(student=student).order_by(
+ "-created_at"
+ )
+ pending_leaves = student_leaves.filter(status=0).count()
+ approved_leaves = student_leaves.filter(status=1).count()
+ rejected_leaves = student_leaves.filter(status=2).count()
+
+ # Get current semester routines
+ try:
+ # Get routines that match the student's course and current semester directly
+ current_semester_routines = Routine.objects.filter(
+ course=student.course,
+ semester_or_year=student.current_semester,
+ is_active=True,
+ )
+ except Exception as e:
+ current_semester_routines = []
+
+ # Check if student has any routines for any semester
+ has_routines = False
+ try:
+ if student.course:
+ has_routines = Routine.objects.filter(course=student.course).exists()
+ except Exception as e:
+ has_routines = False
+
+ context = {
+ "student": student,
+ "title": "Student Dashboard",
+ "teachers": teachers,
+ "feedback_types": feedback_types,
+ # Course Progress
+ "total_courses": total_courses,
+ "completed_courses": completed_courses,
+ "in_progress_courses": in_progress_courses,
+ "dropped_courses": dropped_courses,
+ "overall_completion": round(overall_completion, 1),
+ "course_details": course_details,
+ "course_summary": course_summary,
+ # Current Subjects
+ "current_subjects": current_subjects,
+ # Attendance
+ "attendance_summary": attendance_summary,
+ "attendance_records": attendance_records,
+ "recent_attendance": recent_attendance,
+ # Feedback
+ "recent_feedback": recent_feedback,
+ "institute_feedback": institute_feedback,
+ # Notices
+ "notices": notices,
+ # Leaves
+ "student_leaves": student_leaves,
+ "pending_leaves": pending_leaves,
+ "approved_leaves": approved_leaves,
+ "rejected_leaves": rejected_leaves,
+ # Routines
+ "current_semester_routines": current_semester_routines,
+ "has_routines": has_routines,
+ # Current date for display
+ "today": today,
+ }
+
+ return render(request, "student_dashboard.html", context)
+
+
+@login_required
+def request_leave(request):
+ if request.method == "POST":
+ student = request.user
+ date = request.POST.get("date")
+ message = request.POST.get("message")
+
+ try:
+ # Convert date string to timezone-aware datetime
+ leave_date = timezone.datetime.strptime(date, "%Y-%m-%d")
+ leave_date = timezone.make_aware(leave_date)
+ today = timezone.now()
+
+ if leave_date.date() < today.date():
+ messages.error(request, "Leave date cannot be in the past.")
+ return redirect("studentDashboard")
+
+ # Check if leave already exists for this date
+ existing_leave = Student_Leave.objects.filter(
+ student=student, date=leave_date.date()
+ ).first()
+
+ if existing_leave:
+ messages.error(
+ request, "You have already requested leave for this date."
+ )
+ return redirect("studentDashboard")
+
+ # Create leave request
+ leave = Student_Leave.objects.create(
+ student=student,
+ date=leave_date.date(),
+ message=message,
+ status=0, # Pending status
+ )
+ messages.success(request, "Leave request submitted successfully.")
+ except Exception as e:
+ messages.error(request, f"Error submitting leave request: {str(e)}")
+
+ return redirect("studentDashboard")
+
+ return redirect("studentDashboard")
+
+
+@login_required
+def submit_feedback(request):
+ if request.method == "POST":
+ try:
+ feedback_type = request.POST.get("feedback_type")
+ rating = float(request.POST.get("rating", 0))
+ feedback_text = request.POST.get("feedback_text", "").strip()
+
+ # Validate rating range (1 to 5)
+ if not (1 <= rating <= 5):
+ return JsonResponse(
+ {"success": False, "message": "Rating must be between 1 and 5"}
+ )
+
+ if not feedback_text:
+ return JsonResponse(
+ {"success": False, "message": "Please provide feedback text"}
+ )
+
+ if feedback_type == "teacher":
+ teacher_id = request.POST.get("teacher_id")
+ if not teacher_id:
+ return JsonResponse(
+ {"success": False, "message": "Please select a teacher"}
+ )
+
+ try:
+ teacher = Staff.objects.get(id=teacher_id)
+ # Check if feedback already exists
+ existing_feedback = StudentFeedback.objects.filter(
+ student=request.user, teacher=teacher
+ ).first()
+
+ if existing_feedback:
+ # Update existing feedback
+ existing_feedback.rating = rating
+ existing_feedback.feedback_text = feedback_text
+ existing_feedback.save()
+ else:
+ # Create new feedback
+ StudentFeedback.objects.create(
+ student=request.user,
+ teacher=teacher,
+ rating=rating,
+ feedback_text=feedback_text,
+ )
+
+ return JsonResponse(
+ {
+ "success": True,
+ "message": "Teacher feedback submitted successfully",
+ }
+ )
+
+ except Staff.DoesNotExist:
+ return JsonResponse(
+ {"success": False, "message": "Selected teacher does not exist"}
+ )
+
+ elif feedback_type == "institute":
+ category = request.POST.get("category")
+ is_anonymous = request.POST.get("is_anonymous") == "on"
+
+ if not category:
+ return JsonResponse(
+ {
+ "success": False,
+ "message": "Please select a feedback category",
+ }
+ )
+
+ try:
+ institute = Institute.objects.first() # Assuming single institute
+ # Check if feedback already exists for this category - use new structure
+ existing_feedback = InstituteFeedback.objects.filter(
+ user_type="student",
+ content_object_id=request.user.id,
+ institute=institute,
+ feedback_type=category,
+ ).first()
+
+ if existing_feedback:
+ # Update existing feedback
+ existing_feedback.rating = rating
+ existing_feedback.feedback_text = feedback_text
+ existing_feedback.is_anonymous = is_anonymous
+ existing_feedback.save()
+ else:
+ # Create new feedback - use new structure
+ InstituteFeedback.objects.create(
+ user_type="student",
+ content_object_id=request.user.id,
+ user=request.user,
+ institute=institute,
+ feedback_type=category,
+ rating=rating,
+ feedback_text=feedback_text,
+ is_anonymous=is_anonymous,
+ )
+
+ return JsonResponse(
+ {
+ "success": True,
+ "message": "Institute feedback submitted successfully",
+ }
+ )
+
+ except Institute.DoesNotExist:
+ return JsonResponse(
+ {"success": False, "message": "Institute not found"}
+ )
+
+ else:
+ return JsonResponse(
+ {"success": False, "message": "Invalid feedback type"}
+ )
+
+ except ValueError:
+ return JsonResponse({"success": False, "message": "Invalid rating value"})
+ except Exception as e:
+ return JsonResponse(
+ {"success": False, "message": f"An error occurred: {str(e)}"}
+ )
+
+ return JsonResponse({"success": False, "message": "Invalid request method"})
+
+
+@login_required
+def submit_institute_feedback(request):
+ if request.method == "POST":
+ student = request.user
+ feedback_type = request.POST.get("feedback_type")
+ rating = request.POST.get("rating")
+ feedback_text = request.POST.get("feedback_text")
+ is_anonymous = request.POST.get("is_anonymous") == "on"
+
+ try:
+ # Convert to float first to handle half-stars
+ rating_value = float(rating)
+ if rating_value < 0.5 or rating_value > 5:
+ messages.error(request, "Rating must be between 0.5 and 5")
+ return redirect("studentDashboard")
+
+ # Check if it's a valid half or full star rating
+ if rating_value % 0.5 != 0:
+ messages.error(request, "Rating must be a whole or half number")
+ return redirect("studentDashboard")
+
+ # Get the institute (assuming one institute for now)
+ institute = Institute.objects.first()
+
+ if not institute:
+ # Create a default institute if none exists
+ institute = Institute.objects.create(
+ name="Default Institute",
+ phone="123456789",
+ email="default@example.com",
+ )
+
+ # Check if feedback already exists for this type - using new structure
+ existing_feedback = InstituteFeedback.objects.filter(
+ user_type="student",
+ content_object_id=student.id,
+ institute=institute,
+ feedback_type=feedback_type,
+ ).first()
+
+ if existing_feedback:
+ # Update existing feedback
+ existing_feedback.rating = rating_value
+ existing_feedback.feedback_text = feedback_text
+ existing_feedback.is_anonymous = is_anonymous
+ existing_feedback.save()
+ messages.success(request, "Institute feedback updated successfully.")
+ else:
+ # Create new feedback with new structure
+ feedback = InstituteFeedback.objects.create(
+ user_type="student",
+ content_object_id=student.id,
+ user=student,
+ institute=institute,
+ feedback_type=feedback_type,
+ rating=rating_value,
+ feedback_text=feedback_text,
+ is_anonymous=is_anonymous,
+ )
+ messages.success(request, "Institute feedback submitted successfully.")
+ except ValueError:
+ messages.error(request, "Invalid rating value")
+ except Exception as e:
+ messages.error(request, f"Error submitting institute feedback: {str(e)}")
+
+ return redirect("studentDashboard")
+
+ return redirect("studentDashboard")
+
+
+@login_required
+@require_http_methods(["GET"])
+def get_subject_files(request, subject_id):
+ """View to get syllabus PDF for a subject"""
+ try:
+ subject = Subject.objects.get(id=subject_id)
+ pdf_url = subject.get_pdf_url()
+
+ if not pdf_url:
+ return JsonResponse(
+ {"success": False, "message": "No syllabus available for this subject"}
+ )
+
+ return JsonResponse(
+ {
+ "success": True,
+ "files": [
+ {
+ "title": f"{subject.name} Syllabus",
+ "description": f"Syllabus PDF for {subject.name}",
+ "file_url": pdf_url,
+ }
+ ],
+ }
+ )
+ except Subject.DoesNotExist:
+ return JsonResponse({"success": False, "message": "Subject not found"})
+ except Exception as e:
+ return JsonResponse(
+ {"success": False, "message": f"Error retrieving syllabus: {str(e)}"}
+ )
diff --git a/app/templatetags/app_filters.py b/app/templatetags/app_filters.py
index 58b88c4..22b3526 100644
--- a/app/templatetags/app_filters.py
+++ b/app/templatetags/app_filters.py
@@ -8,4 +8,31 @@ def get_range(value):
Returns a range of numbers from 0 to value-1
Used for creating star ratings
"""
- return range(value)
\ No newline at end of file
+ try:
+ return range(int(value))
+ except (ValueError, TypeError):
+ return range(0)
+
+@register.filter
+def multiply(value, arg):
+ """Multiply the value by the argument"""
+ try:
+ return int(value) * int(arg)
+ except (ValueError, TypeError):
+ return value
+
+@register.filter
+def get_item(dictionary, key):
+ """
+ Gets a value from a dictionary by key.
+ Usage in template: {{ my_dict|get_item:key_variable }}
+ """
+ if dictionary is None:
+ return None
+
+ # Try with the key as is (likely integer ID)
+ if key in dictionary:
+ return dictionary.get(key)
+
+ # Try with the key as string
+ return dictionary.get(str(key), None)
\ No newline at end of file
diff --git a/app/urls.py b/app/urls.py
index 582fe4f..840a3b0 100644
--- a/app/urls.py
+++ b/app/urls.py
@@ -1,8 +1,59 @@
from django.urls import path
-from app import views
+from . import views
+from django.contrib import admin
+from . import auth
+from . import staffviews
urlpatterns = [
- path('dashboard/', views.dashboard, name='dashboard'),
- path('student-dashboard/', views.studentDashboard, name='studentDashboard'),
- path('teacher-dashboard/', views.teacherDashboard, name='teacherDashboard'),
-]
\ No newline at end of file
+ path("dashboard/", views.dashboard, name="dashboard"),
+ path("student-dashboard/", views.studentDashboard, name="studentDashboard"),
+ path("teacher-dashboard/", views.teacherDashboard, name="teacherDashboard"),
+ path("request-leave/", views.request_leave, name="request_leave"),
+ path("request-staff-leave/", views.request_staff_leave, name="request_staff_leave"),
+ path("submit-feedback/", views.submit_feedback, name="submit_feedback"),
+ path(
+ "submit-institute-feedback/",
+ views.submit_institute_feedback,
+ name="submit_institute_feedback",
+ ),
+ path(
+ "submit-staff-institute-feedback/",
+ views.submit_staff_institute_feedback,
+ name="submit_staff_institute_feedback",
+ ),
+ path("save-fcm-token/", views.saveFCMToken, name="saveFCMToken"),
+ path("firebase-messaging-sw.js", views.showFirebaseJS, name="showFirebaseJS"),
+ path("update-profile/", views.update_profile, name="update_profile"),
+ path("change-password/", views.change_password, name="change_password"),
+ path(
+ "subject//files/",
+ views.get_subject_files,
+ name="get_subject_files",
+ ),
+ path("get-subjects/", views.get_subjects, name="get_subjects"),
+ path('get-teachers/', views.get_teachers, name='get_teachers'),
+ path('get-course-duration/', views.get_course_duration, name='get_course_duration'),
+ path('save-attendance/', views.save_attendance, name='save_attendance'),
+ path('get-subject-schedule/', views.get_subject_schedule, name='get_subject_schedule'),
+ path('get-attendance-form/', staffviews.get_attendance_form, name='get_attendance_form'),
+ path('teacher-update-profile-picture/', views.teacher_update_profile_picture, name='teacher_update_profile_picture'),
+ path('teacher-change-password/', views.teacher_change_password, name='teacher_change_password'),
+ path('approve-student-leave//', 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'),
+ path('add-notice/', views.add_notice, name='add_notice'),
+ path('delete-notice//', views.delete_notice, name='delete_notice'),
+
+ # Password reset paths
+ path('password-reset/', auth.reset_password_options, name='reset_password_options'),
+ path('password-reset/phone/', auth.password_reset_phone, name='phone_reset_password'),
+ path('password-reset/phone/verify/', auth.password_reset_phone_verify, name='verify_reset_otp'),
+ path('password-reset/phone/resend/', auth.resend_phone_otp, name='resend_phone_otp'),
+ path('password-reset/email/', auth.password_reset_email, name='email_reset_password'),
+ path('password-reset/email/verify/', auth.password_reset_email_verify, name='password_reset_done'),
+ path('password-reset/email/resend/', auth.resend_email_code, name='resend_reset_code'),
+ path('password-reset/set/', auth.set_new_password, name='set_new_password'),
+ path('get-students/', staffviews.get_students, name='get_students'),
+]
+
diff --git a/app/utils.py b/app/utils.py
new file mode 100644
index 0000000..34d8b68
--- /dev/null
+++ b/app/utils.py
@@ -0,0 +1,170 @@
+import random
+import string
+from django.core.cache import cache
+import requests
+from firebase_admin import messaging, initialize_app, get_app, credentials
+import os
+from django.template.loader import render_to_string
+from django.core.mail import send_mail
+from django.conf import settings
+from django.contrib.auth.tokens import default_token_generator
+from django.utils.http import urlsafe_base64_encode
+from django.utils.encoding import force_bytes
+
+# OTP expiration time in seconds (5 minutes)
+OTP_EXPIRY = 300
+
+
+def generate_otp(length=6):
+ """Generate a numeric OTP of specified length"""
+ digits = string.digits
+ return "".join(random.choice(digits) for _ in range(length))
+
+
+def store_otp(phone, otp):
+ """Store OTP in cache with phone as key"""
+ cache_key = f"otp_{phone}"
+ cache.set(cache_key, otp, OTP_EXPIRY)
+ return True
+
+
+def verify_otp(phone, otp):
+ """Verify if the provided OTP matches the stored OTP for the phone"""
+ cache_key = f"otp_{phone}"
+ stored_otp = cache.get(cache_key)
+
+ if stored_otp and stored_otp == otp:
+ # Delete the OTP after successful verification
+ cache.delete(cache_key)
+ return True
+ return False
+
+
+def send_otp_sms(phone, otp):
+ """Send OTP via SMS using a service like Twilio, MSG91, etc."""
+ try:
+ # This is a placeholder. You need to replace with actual SMS API implementation
+ # Example using a generic SMS API
+ # api_url = "https://api.example.com/send"
+ # payload = {
+ # "apikey": settings.SMS_API_KEY,
+ # "to": phone,
+ # "message": f"Your OTP for SMS login is: {otp}. Valid for 5 minutes."
+ # }
+ # response = requests.post(api_url, json=payload)
+ # return response.status_code == 200
+
+ print(f"OTP for {phone}: {otp}")
+ return True
+ except Exception as e:
+ print(f"Error sending OTP: {str(e)}")
+ return False
+
+
+def send_push_notification(title, message, tokens):
+ """
+ Send push notification using Firebase Cloud Messaging
+ """
+ try:
+ try:
+ app = get_app()
+ except ValueError:
+ # Initialize Firebase Admin SDK if not already initialized
+ cred_path = os.path.join(
+ os.path.dirname(os.path.dirname(__file__)),
+ "firebase-key.json",
+ )
+ if not os.path.exists(cred_path):
+ print(f"Firebase credentials file not found at {cred_path}")
+ return
+
+ cred = credentials.Certificate(cred_path)
+ app = initialize_app(cred)
+
+ if not tokens:
+ print("No FCM tokens available to send notifications")
+ return
+
+ # Create message
+ message = messaging.MulticastMessage(
+ notification=messaging.Notification(
+ title=title,
+ body=message,
+ ),
+ tokens=list(tokens),
+ )
+
+ # Send message
+ response = messaging.send_multicast(message)
+
+ if response.failure_count > 0:
+ failures = []
+ for idx, resp in enumerate(response.responses):
+ if not resp.success:
+ failures.append(
+ f"Failed to send message to token {tokens[idx]}: {resp.exception}"
+ )
+ print("\n".join(failures))
+
+ except Exception as e:
+ print(f"Error sending notification: {e}")
+ # Don't raise the exception to prevent save from failing
+ pass
+
+
+def send_password_reset_email(user, request, email):
+ """
+ Send password reset code to user's email using HTML template
+ """
+ try:
+ # Generate a reset code (OTP)
+ reset_code = generate_otp(length=6)
+
+ # Store the reset code in cache with user's email as key
+ cache_key = f"reset_code_{email}"
+ cache.set(cache_key, reset_code, OTP_EXPIRY) # 5 minutes expiry
+
+ # Build email context
+ context = {
+ "user": user,
+ "domain": request.META.get("HTTP_HOST", "localhost:8000"),
+ "protocol": "https" if request.is_secure() else "http",
+ "reset_code": reset_code,
+ }
+
+ # Render email content using template
+ subject = render_to_string("login/email_templates/password_reset_subject.txt")
+ message = render_to_string(
+ "login/email_templates/password_reset_email.html", context
+ )
+
+ # Send email using console backend
+ send_mail(
+ subject=subject,
+ message=message,
+ from_email="noreply@studentmanagement.com",
+ recipient_list=[email],
+ html_message=message,
+ fail_silently=False,
+ )
+ print(f"\nPassword reset code sent to {email}")
+ print(f"Reset Code: {reset_code}\n")
+ return True
+ except Exception as e:
+ print(f"Failed to send email: {str(e)}")
+ return False
+
+
+def verify_reset_code(email, code):
+ """
+ Verify if the provided reset code matches the stored code for the email
+ """
+ cache_key = f"reset_code_{email}"
+ stored_code = cache.get(cache_key)
+
+ if stored_code and stored_code == code:
+ # Delete the code after successful verification
+ cache.delete(cache_key)
+ return True
+ return False
+
diff --git a/app/views.py b/app/views.py
index 21721a8..6e537d0 100644
--- a/app/views.py
+++ b/app/views.py
@@ -16,10 +16,47 @@ from .models import (
AttendanceRecord,
StudentFeedback,
Routine,
- StudentRoutine,
Staff,
+ Notice,
+ Institute,
+ InstituteFeedback,
+ Subject,
+ Batch,
)
from app.admin import custom_admin_site
+from django.views.decorators.http import require_http_methods, require_GET
+from django.core.files.storage import default_storage
+from django.core.files.base import ContentFile
+import json
+import os
+from django.db.models.base import transaction
+from .utils import generate_otp, store_otp, verify_otp, send_otp_sms
+from django.conf import settings
+from dotenv import load_dotenv
+
+# Import views from studentviews.py
+from .studentviews import (
+ studentDashboard,
+ request_leave,
+ submit_feedback,
+ submit_institute_feedback,
+ get_subject_files,
+)
+
+# Import views from staffviews.py
+from .staffviews import (
+ teacherDashboard,
+ request_staff_leave,
+ submit_staff_institute_feedback,
+ save_attendance,
+ get_subject_schedule,
+ teacher_update_profile_picture,
+ teacher_change_password,
+)
+
+# Load environment variables
+load_dotenv()
+
# Create your views here.
@@ -33,48 +70,51 @@ def saveFCMToken(request, token):
def showFirebaseJS(request):
- data = """
+ data = f"""
importScripts('https://www.gstatic.com/firebasejs/11.0.2/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/11.0.2/firebase-messaging-compat.js');
- firebase.initializeApp({
- apiKey: "",
- authDomain: "",
- projectId: "",
- storageBucket: "",
- messagingSenderId: "",
- appId: "",
- measurementId: ""
- });
+ firebase.initializeApp({{
+ apiKey: "{os.getenv('FIREBASE_API_KEY')}",
+ authDomain: "{os.getenv('FIREBASE_AUTH_DOMAIN')}",
+ projectId: "{os.getenv('FIREBASE_PROJECT_ID')}",
+ storageBucket: "{os.getenv('FIREBASE_STORAGE_BUCKET')}",
+ messagingSenderId: "{os.getenv('FIREBASE_MESSAGING_SENDER_ID')}",
+ appId: "{os.getenv('FIREBASE_APP_ID')}",
+ measurementId: "{os.getenv('FIREBASE_MEASUREMENT_ID')}"
+ }});
const messaging = firebase.messaging();
- messaging.onBackgroundMessage((payload) => {
-
+ messaging.onBackgroundMessage((payload) => {{
const notificationTitle = payload.notification.title;
- const notificationOptions = {
+ const notificationOptions = {{
body: payload.notification.body,
icon: payload.notification.icon
- };
+ }};
return self.registration.showNotification(notificationTitle, notificationOptions);
- });
+ }});
"""
return HttpResponse(data, content_type="text/javascript")
@login_required
def dashboard(request):
+ """Main dashboard view that redirects to appropriate dashboard based on user role"""
user = request.user
user_groups = user.groups.values_list("name", flat=True)
+
+ # Redirect based on user role
if "Student" in user_groups:
return redirect("studentDashboard")
elif "Teacher" in user_groups:
return redirect("teacherDashboard")
+
# Get current date and date ranges
today = timezone.now().date()
- last_week = today - timedelta(days=7)
- last_month = today - timedelta(days=30)
+ last_week = timezone.now() - timedelta(days=7)
+ last_month = timezone.now() - timedelta(days=30)
# Student Statistics
total_students = Student.objects.count()
@@ -82,6 +122,13 @@ def dashboard(request):
completed_students = Student.objects.filter(status="Completed").count()
on_leave_students = Student.objects.filter(status="Leave").count()
+ # Staff Statistics
+ total_staff = Staff.objects.count()
+ active_staff = Staff.objects.filter(is_active=True).count()
+ staff_on_leave = (
+ Staff_leave.objects.filter(status=1).values("staff").distinct().count()
+ )
+
# Course Statistics
total_courses = Course.objects.count()
active_courses = (
@@ -110,15 +157,123 @@ def dashboard(request):
created_at__gte=last_week
).order_by("-created_at")[:5]
+ # Pending Leave Requests
+ pending_student_leaves = Student_Leave.objects.filter(status=0)
+ pending_staff_leaves = Staff_leave.objects.filter(status=0).order_by("-created_at")
+
# Attendance Statistics
- today_attendance = Attendance.objects.filter(date=today).count()
- today_present = AttendanceRecord.objects.filter(
- attendance__date=today, student_attend=True
+ today_classes = Attendance.objects.filter(date=today)
+ total_classes = today_classes.count()
+ total_students_today = AttendanceRecord.objects.filter(attendance__in=today_classes).count()
+ students_present = AttendanceRecord.objects.filter(
+ attendance__in=today_classes, student_attend=True
).count()
# Recent Feedback
recent_feedback = StudentFeedback.objects.order_by("-created_at")[:5]
+ # Recent Institute Feedback
+ recent_institute_feedback = InstituteFeedback.objects.filter(
+ is_public=True
+ ).order_by("-created_at")[:10]
+
+ # Get notices
+ notices = Notice.objects.all().order_by("-created_at")
+
+ # Get recent activities
+ recent_activities = []
+
+ # Add recent leaves
+ for leave in recent_student_leaves:
+ recent_activities.append(
+ {
+ "title": f"Student Leave Request",
+ "description": f"{leave.student.name} requested leave for {leave.date}",
+ "timestamp": leave.created_at,
+ }
+ )
+
+ for leave in recent_staff_leaves:
+ recent_activities.append(
+ {
+ "title": f"Staff Leave Request",
+ "description": f"{leave.staff.name} requested leave for {leave.date}",
+ "timestamp": leave.created_at,
+ }
+ )
+
+ # Add recent feedback
+ for feedback in recent_feedback:
+ recent_activities.append(
+ {
+ "title": f"New Feedback",
+ "description": f"Feedback received for {feedback.teacher.name if feedback.teacher else 'Institute'}",
+ "timestamp": feedback.created_at,
+ }
+ )
+
+ # Add recent institute feedback
+ for feedback in recent_institute_feedback:
+ recent_activities.append(
+ {
+ "title": f"Institute Feedback",
+ "description": f"New feedback from {feedback.display_name}",
+ "timestamp": feedback.created_at,
+ }
+ )
+
+ # Add recent notices
+ for notice in notices:
+ recent_activities.append(
+ {
+ "title": f"New Notice",
+ "description": notice.title,
+ "timestamp": notice.created_at,
+ }
+ )
+
+ # Sort all activities by timestamp
+ recent_activities.sort(key=lambda x: x["timestamp"], reverse=True)
+ # Take only the most recent 10 activities
+ recent_activities = recent_activities[:10]
+
+ # Teacher Ratings
+ teacher_ratings = []
+ teachers = Staff.objects.filter(
+ Q(designation__icontains="Teacher")
+ | Q(designation__icontains="Staff")
+ | Q(designation__icontains="Principal")
+ | Q(designation__isnull=True)
+ | Q(designation="")
+ )
+
+ for teacher in teachers:
+ feedback = StudentFeedback.objects.filter(teacher=teacher)
+ avg_rating = feedback.aggregate(Avg("rating"))["rating__avg"] or 0
+ total_reviews = feedback.count()
+
+ # Get total students taught by this teacher
+ total_students = (
+ AttendanceRecord.objects.filter(attendance__routine__teacher=teacher)
+ .values("student")
+ .distinct()
+ .count()
+ )
+
+ teacher_ratings.append(
+ {
+ "name": teacher.name,
+ "designation": teacher.designation or "Teacher",
+ "image": teacher.image,
+ "avg_rating": avg_rating,
+ "total_reviews": total_reviews,
+ "total_students": total_students,
+ }
+ )
+
+ # Sort teachers by average rating (highest first)
+ teacher_ratings.sort(key=lambda x: x["avg_rating"], reverse=True)
+
# Course Progress Overview
course_progress = []
for course in Course.objects.all():
@@ -142,25 +297,26 @@ def dashboard(request):
# Monthly Statistics
monthly_stats = {
"new_students": Student.objects.filter(joining_date__gte=last_month).count(),
- "completed_courses": completed_trackings.filter(
- actual_end_date__gte=last_month
- ).count(),
+ "new_staff": Staff.objects.filter(joining_date__gte=last_month).count(),
"attendance_rate": round(
(
AttendanceRecord.objects.filter(
- attendance__date__gte=last_month, student_attend=True
+ attendance__date__gte=last_month.date(), student_attend=True
).count()
/ AttendanceRecord.objects.filter(
- attendance__date__gte=last_month
+ attendance__date__gte=last_month.date()
).count()
* 100
)
if AttendanceRecord.objects.filter(
- attendance__date__gte=last_month
+ attendance__date__gte=last_month.date()
).exists()
- else 0,
- 1,
+ else 0
),
+ "leave_requests": Student_Leave.objects.filter(
+ created_at__gte=last_month
+ ).count()
+ + Staff_leave.objects.filter(created_at__gte=last_month).count(),
}
context = {
@@ -172,6 +328,10 @@ def dashboard(request):
"active_students": active_students,
"completed_students": completed_students,
"on_leave_students": on_leave_students,
+ # Staff Statistics
+ "total_staff": total_staff,
+ "active_staff": active_staff,
+ "staff_on_leave": staff_on_leave,
# Course Statistics
"total_courses": total_courses,
"active_courses": active_courses,
@@ -183,212 +343,318 @@ def dashboard(request):
# Leave Requests
"recent_student_leaves": recent_student_leaves,
"recent_staff_leaves": recent_staff_leaves,
+ "pending_student_leaves": pending_student_leaves,
+ "pending_staff_leaves": pending_staff_leaves,
# Attendance Statistics
- "today_attendance": today_attendance,
- "today_present": today_present,
+ "total_classes_today": total_classes,
+ "total_students_today": total_students_today,
+ "students_present": students_present,
"attendance_rate": round(
- (today_present / today_attendance * 100) if today_attendance > 0 else 0, 1
+ (students_present / total_students_today * 100) if total_students_today > 0 else 0
),
- # Recent Feedback
+ # Feedback
"recent_feedback": recent_feedback,
+ "recent_institute_feedback": recent_institute_feedback,
+ "teacher_ratings": teacher_ratings,
# Course Progress
"course_progress": course_progress,
# Monthly Statistics
"monthly_stats": monthly_stats,
+ "notices": notices, # Add notices to context
+ "recent_activities": recent_activities, # Add recent activities to context
}
return render(request, "dashboard.html", context)
@login_required
-def studentDashboard(request):
- """View to display the student dashboard with their personal information and progress"""
- student = request.user
- today = timezone.now().date()
- last_week = today - timedelta(days=7)
- last_month = today - timedelta(days=30)
+def update_profile(request):
+ if request.method == "POST":
+ user = request.user
+ name = request.POST.get("name")
+ email = request.POST.get("email")
+ phone = request.POST.get("phone")
+ address = request.POST.get("address")
+ gender = request.POST.get("gender")
+ dob = request.POST.get("dob")
- # Course Progress
- course_trackings = CourseTracking.objects.filter(student=student)
- total_courses = course_trackings.count()
- completed_courses = course_trackings.filter(progress_status="Completed").count()
- in_progress_courses = course_trackings.filter(progress_status="In Progress").count()
- completion_percentage = (
- (completed_courses / total_courses * 100) if total_courses > 0 else 0
- )
+ # Validate data
+ if not all([name, email, phone]):
+ messages.error(request, "Name, email and phone are required fields.")
+ return redirect("studentDashboard")
- # Course Details
- course_details = {}
- for tracking in course_trackings:
- course_details[tracking.course.name] = {
- "start_date": tracking.start_date,
- "end_date": tracking.end_date,
- "completion_percentage": tracking.completion_percentage,
- "progress_status": tracking.progress_status,
- "book": tracking.course.book if hasattr(tracking.course, "book") else None,
- "topics": [], # This will be populated if you have topic tracking
- }
+ # Update profile based on user type
+ try:
+ if hasattr(user, "student"):
+ student = user
+ student.name = name
+ student.email = email
+ student.phone = phone
+ student.address = address
+ student.gender = gender
+ if dob:
+ student.dob = dob
- # Attendance Statistics
- attendance_records = AttendanceRecord.objects.filter(student=student)
- total_classes = attendance_records.count()
- classes_attended = attendance_records.filter(student_attend=True).count()
- attendance_percentage = (
- (classes_attended / total_classes * 100) if total_classes > 0 else 0
- )
+ # Handle profile image
+ if "profile_image" in request.FILES:
+ image = request.FILES["profile_image"]
+ if student.image:
+ # Delete old image if updating
+ if os.path.isfile(student.image.path):
+ os.remove(student.image.path)
- # Calculate attendance streaks
- current_streak = 0
- longest_streak = 0
- temp_streak = 0
+ # Save new image
+ student.image = image
- for record in attendance_records.order_by("-attendance__date"):
- if record.student_attend:
- temp_streak += 1
- if temp_streak > longest_streak:
- longest_streak = temp_streak
- if record.attendance.date >= last_week:
- current_streak = temp_streak
+ student.save()
+ messages.success(request, "Profile updated successfully.")
+
+ elif hasattr(user, "staff"):
+ staff = user
+ staff.name = name
+ staff.email = email
+ staff.phone = phone
+ staff.address = address
+ staff.gender = gender
+ if dob:
+ staff.dob = dob
+
+ # Handle profile image
+ if "profile_image" in request.FILES:
+ image = request.FILES["profile_image"]
+ if staff.image:
+ # Delete old image if updating
+ if os.path.isfile(staff.image.path):
+ os.remove(staff.image.path)
+
+ # Save new image
+ staff.image = image
+
+ staff.save()
+ messages.success(request, "Profile updated successfully.")
+
+ else:
+ messages.error(request, "User profile not found.")
+
+ except Exception as e:
+ messages.error(request, f"Error updating profile: {str(e)}")
+
+ # Redirect based on user type
+ if hasattr(user, "student"):
+ return redirect("studentDashboard")
+ elif hasattr(user, "staff"):
+ return redirect("teacherDashboard")
else:
- temp_streak = 0
+ return redirect("dashboard")
- # Recent Leave Requests
- recent_leaves = Student_Leave.objects.filter(student=student).order_by(
- "-created_at"
- )[:5]
-
- # Recent Feedback
- recent_feedback = StudentFeedback.objects.filter(student=student).order_by(
- "-created_at"
- )[:5]
-
- context = {
- **custom_admin_site.each_context(
- request
- ), # This adds the admin context, including the sidebar.
- "student": student,
- "title": "Student Dashboard",
- # Course Summary
- "course_summary": {
- "total_courses": total_courses,
- "completed_courses": completed_courses,
- "ongoing_courses": in_progress_courses,
- "completion_percentage": completion_percentage,
- "details": course_details,
- },
- # Attendance Summary
- "attendance_summary": {
- "total_classes": total_classes,
- "classes_attended": classes_attended,
- "attendance_percentage": attendance_percentage,
- "current_streak": current_streak,
- "longest_streak": longest_streak,
- },
- # Recent Activity
- "recent_leaves": recent_leaves,
- "recent_feedback": recent_feedback,
- }
-
- return render(request, "student_dashboard.html", context)
+ return redirect("dashboard")
@login_required
-def teacherDashboard(request):
- """View to display the teacher dashboard with their classes and student information"""
- teacher = Staff.objects.get(phone=request.user.phone) # Get the Staff object
- today = timezone.now().date()
- current_time = timezone.now().time()
+def change_password(request):
+ if request.method == "POST":
+ user = request.user
+ current_password = request.POST.get("current_password")
+ new_password = request.POST.get("new_password")
+ confirm_password = request.POST.get("confirm_password")
- # Get today's routines through attendance
- today_routines = Routine.objects.filter(
- teacher=teacher, attendances__date=today
- ).order_by("start_time")
+ # Validate input
+ if not all([current_password, new_password, confirm_password]):
+ messages.error(request, "All password fields are required.")
+ return redirect("studentDashboard")
- # Process routines to include status and attendance
- processed_routines = []
- for routine in today_routines:
- attendance = routine.attendances.filter(date=today).first()
+ if new_password != confirm_password:
+ messages.error(request, "New passwords do not match.")
+ return redirect("studentDashboard")
- total_students = StudentRoutine.objects.filter(routine=routine).count()
- attendance_count = (
- 0
- if not attendance
- else AttendanceRecord.objects.filter(
- attendance=attendance, student_attend=True
- ).count()
- )
+ # Check current password
+ if not user.check_password(current_password):
+ messages.error(request, "Current password is incorrect.")
+ return redirect("studentDashboard")
- processed_routines.append(
+ # Set new password
+ try:
+ user.set_password(new_password)
+ user.save()
+ messages.success(
+ request, "Password changed successfully. Please login again."
+ )
+ return redirect("login")
+ except Exception as e:
+ messages.error(request, f"Error changing password: {str(e)}")
+
+ # Redirect based on user type
+ if hasattr(user, "student"):
+ return redirect("studentDashboard")
+ elif hasattr(user, "staff"):
+ return redirect("teacherDashboard")
+ else:
+ return redirect("dashboard")
+
+ return redirect("dashboard")
+
+
+@login_required
+@require_GET
+def get_subjects(request):
+ """Get subjects for a course and semester"""
+ course_id = request.GET.get("course_id")
+ semester = request.GET.get("semester", 1)
+
+ if not course_id:
+ return JsonResponse({"success": False, "message": "Course ID is required"})
+
+ try:
+ subjects = Subject.objects.filter(
+ course_id=course_id, semester_or_year=semester
+ ).values("id", "name")
+
+ return JsonResponse({"success": True, "subjects": list(subjects)})
+ except Exception as e:
+ return JsonResponse({"success": False, "message": str(e)})
+
+
+@login_required
+@require_GET
+def get_teachers(request):
+ """Get teachers for the institute"""
+ try:
+ teachers = Staff.objects.filter(
+ Q(designation__icontains="Teacher")
+ | Q(designation__icontains="Staff")
+ | Q(designation__icontains="Principal")
+ | Q(designation__isnull=True)
+ | Q(designation="")
+ ).values("id", "name")
+
+ return JsonResponse({"success": True, "teachers": list(teachers)})
+ except Exception as e:
+ return JsonResponse({"success": False, "message": str(e)})
+
+
+@login_required
+@require_GET
+def get_course_duration(request):
+ """Get course duration details"""
+ course_id = request.GET.get("course_id")
+
+ if not course_id:
+ return JsonResponse({"success": False, "message": "Course ID is required"})
+
+ try:
+ course = Course.objects.get(id=course_id)
+
+ # Calculate number of semesters or years
+ total_periods = course.duration
+ if course.duration_type == "Semester":
+ total_periods = course.duration * 2 # 2 semesters per year
+
+ # Create a list of periods
+ periods = []
+ for i in range(1, total_periods + 1):
+ if course.duration_type == "Semester":
+ periods.append({"value": i, "label": f"Semester {i}"})
+ else:
+ periods.append({"value": i, "label": f"Year {i}"})
+
+ return JsonResponse(
{
- "name": routine.name,
- "start_time": routine.start_time,
- "end_time": routine.end_time,
- "is_completed": routine.end_time < current_time,
- "is_ongoing": routine.start_time <= current_time <= routine.end_time,
- "total_students": total_students,
- "attendance_count": attendance_count,
+ "success": True,
+ "course": {
+ "id": course.id,
+ "name": course.name,
+ "duration": course.duration,
+ "duration_type": course.duration_type,
+ "total_periods": total_periods,
+ "periods": periods,
+ },
}
)
+ except Course.DoesNotExist:
+ return JsonResponse({"success": False, "message": "Course not found"})
+ except Exception as e:
+ return JsonResponse({"success": False, "message": str(e)})
- # Calculate class statistics
- total_classes = today_routines.count()
- completed_classes = len([r for r in processed_routines if r["is_completed"]])
- # Get student statistics
- total_students = (
- StudentRoutine.objects.filter(routine__teacher=teacher)
- .values("student")
- .distinct()
- .count()
- )
+@login_required
+def approve_student_leave(request, leave_id):
+ """View to approve a student leave request"""
+ if request.method == "POST":
+ leave = get_object_or_404(Student_Leave, id=leave_id)
+ leave.status = 1 # Approved
+ leave.save()
+ messages.success(
+ request, f"Leave request for {leave.student.name} has been approved."
+ )
+ return redirect("dashboard")
- # Calculate present students for today
- present_students = AttendanceRecord.objects.filter(
- attendance__routine__teacher=teacher,
- attendance__date=today,
- student_attend=True,
- ).count()
- # Get teacher's feedback statistics
- feedback = StudentFeedback.objects.filter(teacher=teacher)
- avg_rating = feedback.aggregate(Avg("rating"))["rating__avg"] or 0
- avg_rating_rounded = int(avg_rating)
- remaining_stars = 5 - avg_rating_rounded
+@login_required
+def reject_student_leave(request, leave_id):
+ """View to reject a student leave request"""
+ if request.method == "POST":
+ leave = get_object_or_404(Student_Leave, id=leave_id)
+ leave.status = 2 # Rejected
+ leave.save()
+ messages.warning(
+ request, f"Leave request for {leave.student.name} has been rejected."
+ )
+ return redirect("dashboard")
- # Get leave statistics
- leaves = Staff_leave.objects.filter(staff=teacher)
- pending_leaves = leaves.filter(status=0).count()
- approved_leaves = leaves.filter(status=1).count()
- rejected_leaves = leaves.filter(status=2).count()
- # Get recent activity
- recent_leaves = leaves.order_by("-created_at")[:5]
- recent_feedback = feedback.order_by("-created_at")[:5]
+@login_required
+def approve_staff_leave(request, leave_id):
+ """View to approve a staff leave request"""
+ if request.method == "POST":
+ leave = get_object_or_404(Staff_leave, id=leave_id)
+ leave.status = 1 # Approved
+ leave.save()
+ messages.success(
+ request, f"Leave request for {leave.staff.name} has been approved."
+ )
+ return redirect("dashboard")
+
+
+@login_required
+def reject_staff_leave(request, leave_id):
+ """View to reject a staff leave request"""
+ if request.method == "POST":
+ leave = get_object_or_404(Staff_leave, id=leave_id)
+ leave.status = 2 # Rejected
+ leave.save()
+ messages.warning(
+ request, f"Leave request for {leave.staff.name} has been rejected."
+ )
+ return redirect("dashboard")
+
+
+@login_required
+def add_notice(request):
+ if request.method == "POST":
+ title = request.POST.get("title")
+ message = request.POST.get("message")
+ image = request.FILES.get("image")
+
+ if title and message:
+ notice = Notice.objects.create(title=title, message=message, image=image)
+ messages.success(request, "Notice added successfully.")
+ else:
+ messages.error(request, "Title and message are required.")
+
+ return redirect("dashboard")
+
+
+@login_required
+def delete_notice(request, notice_id):
+ if request.method == "POST":
+ try:
+ notice = Notice.objects.get(id=notice_id)
+ notice.delete()
+ return JsonResponse({"success": True})
+ except Notice.DoesNotExist:
+ return JsonResponse({"success": False, "error": "Notice not found"})
+
+ return JsonResponse({"success": False, "error": "Invalid request method"})
+
- context = {
- **custom_admin_site.each_context(
- request
- ), # This adds the admin context, including the sidebar.
- "teacher": teacher,
- "title": "Teacher Dashboard",
- # Class Statistics
- "today_classes": total_classes,
- "completed_classes": completed_classes,
- "today_routines": processed_routines,
- # Student Statistics
- "total_students": total_students,
- "present_students": present_students,
- # Feedback Statistics
- "avg_rating": avg_rating,
- "avg_rating_rounded": avg_rating_rounded,
- "remaining_stars": remaining_stars,
- # Leave Statistics
- "pending_leaves": pending_leaves,
- "approved_leaves": approved_leaves,
- "rejected_leaves": rejected_leaves,
- # Recent Activity
- "recent_leaves": recent_leaves,
- "recent_feedback": recent_feedback,
- }
- return render(request, "teacher_dashboard.html", context)
diff --git a/assets/logo.png b/assets/logo.png
deleted file mode 100644
index 05e9f69..0000000
Binary files a/assets/logo.png and /dev/null differ
diff --git a/requirements.txt b/requirements.txt
index 1aea859..03275c1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,13 +1,43 @@
asgiref==3.8.1
+CacheControl==0.14.2
+cachetools==5.5.2
certifi==2025.1.31
+cffi==1.17.1
charset-normalizer==3.4.1
+cryptography==44.0.2
Django==5.1.7
django-jazzmin==3.0.1
+django-mathfilters==1.0.0
django-rest-framework==0.1.0
djangorestframework==3.15.2
+firebase-admin==6.7.0
+google-api-core==2.24.2
+google-api-python-client==2.165.0
+google-auth==2.38.0
+google-auth-httplib2==0.2.0
+google-cloud-core==2.4.3
+google-cloud-firestore==2.20.1
+google-cloud-storage==3.1.0
+google-crc32c==1.7.1
+google-resumable-media==2.7.2
+googleapis-common-protos==1.69.2
+grpcio==1.71.0
+grpcio-status==1.71.0
+httplib2==0.22.0
idna==3.10
+msgpack==1.1.0
pillow==11.1.0
+proto-plus==1.26.1
+protobuf==5.29.4
+pyasn1==0.6.1
+pyasn1_modules==0.4.1
+pycparser==2.22
+PyJWT==2.10.1
+pyparsing==3.2.3
pytz==2025.2
requests==2.32.3
+rsa==4.9
sqlparse==0.5.3
+uritemplate==4.1.1
urllib3==2.3.0
+python-dotenv==1.0.1
diff --git a/static/css/custom.css b/static/css/custom.css
new file mode 100644
index 0000000..5ee0c89
--- /dev/null
+++ b/static/css/custom.css
@@ -0,0 +1,22 @@
+/* Target the specific logo image */
+img[src*="/static/img/logo.png"] {
+ max-height: 100px !important;
+ width: auto !important;
+}
+
+/* Target login page logo */
+.jazzmin-login-page .login-logo img {
+ max-height: 100px !important;
+ width: auto !important;
+}
+
+/* Target admin interface logo */
+.brand-link img.img-circle {
+ max-height: 33px !important;
+ width: auto !important;
+ opacity: .8;
+ margin-left: .8rem;
+ margin-right: .5rem;
+ margin-top: -3px;
+}
+
diff --git a/static/css/dashboard.css b/static/css/dashboard.css
new file mode 100644
index 0000000..9dc9685
--- /dev/null
+++ b/static/css/dashboard.css
@@ -0,0 +1,712 @@
+/* Reset body padding since we removed the sidebar */
+body {
+ padding-left: 0 !important;
+ padding-top: 0 !important;
+}
+
+/* Material Design Color System */
+:root {
+ --primary: #3F51B5; /* Indigo */
+ --primary-light: #7986CB;
+ --secondary: #9E9E9E; /* Grey */
+ --success: #4CAF50; /* Green */
+ --warning: #FFC107; /* Amber */
+ --danger: #F44336; /* Red */
+ --info: #2196F3; /* Blue */
+ --background: #F5F5F5; /* Grey 100 */
+ --card-bg: #FFFFFF;
+ --text-primary: #212121; /* Grey 900 */
+ --text-secondary: #757575; /* Grey 600 */
+ --border-color: #BDBDBD; /* Grey 400 */
+ --hover-bg: #E8EAF6; /* Indigo 50 */
+ --card-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+ --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+/* Base Layout Styles */
+body {
+ background-color: var(--background);
+ min-height: 100vh;
+ padding-bottom: 80px;
+ position: relative;
+ color: var(--text-primary);
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+}
+
+/* Modern Card Styles */
+.content-card {
+ background-color: var(--card-bg);
+ border-radius: 8px;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
+ padding: 20px;
+ margin-bottom: 20px;
+ transition: var(--transition);
+}
+
+.content-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
+}
+
+/* Bottom Navigation */
+.bottom-nav {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background-color: white;
+ box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
+ z-index: 1000;
+}
+
+.nav-container {
+ display: flex;
+ overflow-x: auto;
+ padding: 10px;
+ justify-content: space-around;
+}
+
+.nav-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ color: var(--secondary);
+ text-decoration: none;
+ padding: 8px;
+ font-size: 0.8rem;
+ transition: all 0.2s;
+ border-radius: 5px;
+ min-width: 60px;
+}
+
+.nav-item i {
+ font-size: 1.2rem;
+ margin-bottom: 4px;
+}
+
+.nav-item.active {
+ color: var(--primary);
+ background-color: var(--hover-bg);
+ transform: scale(1.1);
+}
+
+.nav-item:hover:not(.active) {
+ background-color: rgba(0, 0, 0, 0.04);
+}
+
+/* Content Section Styles */
+.content-section {
+ display: none;
+ opacity: 0;
+ transform: translateY(20px);
+ transition: opacity 0.3s ease, transform 0.3s ease;
+}
+
+.content-section.active {
+ display: block;
+ opacity: 1;
+ transform: translateY(0);
+}
+
+/* Summary Cards */
+.summary-card {
+ background-color: var(--card-bg);
+ border-radius: 8px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+}
+
+.summary-card .card-header {
+ background-color: var(--hover-bg);
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.card-body {
+ padding: 1.5rem;
+}
+
+/* Progress Bars */
+.progress {
+ height: 12px;
+ background-color: #E8EAF6;
+ border-radius: 24px;
+ overflow: hidden;
+}
+
+.progress-bar {
+ border-radius: 24px;
+ transition: var(--transition);
+}
+
+/* Add this to replace inline style attributes */
+.progress-bar[style] {
+ width: var(--progress-width, 0%);
+}
+
+/* Tables */
+.table {
+ background: #FFFFFF;
+ border-radius: 16px;
+ overflow: hidden;
+ box-shadow: var(--card-shadow);
+ border-collapse: separate;
+ border-spacing: 0;
+}
+
+.table thead th {
+ background: var(--hover-bg);
+ border: none;
+ padding: 1rem 1.5rem;
+ font-weight: 600;
+ color: var(--primary);
+}
+
+.table tbody td {
+ padding: 1rem 1.5rem;
+ border-top: 1px solid var(--border-color);
+ transition: var(--transition);
+}
+
+.table tbody tr:hover td {
+ background-color: var(--hover-bg);
+}
+
+/* Status Badges */
+.status-badge {
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 0.75rem;
+ font-weight: 500;
+ display: inline-block;
+}
+
+.bg-success, .completed, .leave-approved {
+ background-color: var(--success) !important;
+ color: white;
+}
+
+.bg-warning, .ongoing, .leave-pending {
+ background-color: var(--warning) !important;
+ color: #212121; /* Material design suggests dark text on light colors like Amber */
+}
+
+.bg-danger, .leave-rejected {
+ background-color: var(--danger) !important;
+ color: white;
+}
+
+.bg-info, .upcoming {
+ background-color: var(--info) !important;
+ color: white;
+}
+
+/* Animations */
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(20px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .container-fluid {
+ padding: 0.5rem;
+ }
+
+ .content-card {
+ padding: 16px;
+ }
+
+ .summary-card {
+ margin-bottom: 1rem;
+ }
+
+ .card-header {
+ padding: 1rem;
+ }
+
+ .card-body {
+ padding: 1rem;
+ }
+
+ .table {
+ font-size: 0.875rem;
+ }
+
+ .table th,
+ .table td {
+ padding: 0.75rem;
+ white-space: nowrap;
+ }
+
+ .table-responsive {
+ margin: 0 -0.5rem;
+ }
+
+ .row {
+ margin-left: -0.5rem;
+ margin-right: -0.5rem;
+ }
+
+ .col-12, .col-md-6 {
+ padding-left: 0.5rem;
+ padding-right: 0.5rem;
+ }
+
+ .bottom-nav {
+ padding: 0.5rem;
+ }
+
+ .nav-item {
+ padding: 0.375rem;
+ }
+
+ .nav-item i {
+ font-size: 1.25rem;
+ }
+
+ .nav-item span {
+ display: none;
+ }
+
+ .nav-item.active::after {
+ bottom: -0.5rem;
+ }
+
+ /* Welcome Section */
+ .welcome-section img {
+ width: 60px !important;
+ height: 60px !important;
+ }
+
+ .welcome-section h2 {
+ font-size: 1.5rem;
+ }
+
+ /* Course Cards */
+ .course-card {
+ margin-bottom: 1rem;
+ }
+
+ .course-card .card-title {
+ font-size: 1.1rem;
+ }
+
+ /* Subject Cards */
+ .subject-card {
+ margin-bottom: 1rem;
+ }
+
+ /* Feedback Cards */
+ .feedback-card {
+ margin-bottom: 1rem;
+ }
+
+ /* Notice Cards */
+ .notice-card {
+ margin-bottom: 1rem;
+ }
+
+ /* Profile Section */
+ .profile-image {
+ width: 120px !important;
+ height: 120px !important;
+ }
+
+ /* Modal Adjustments */
+ .modal-dialog {
+ margin: 0.5rem;
+ }
+
+ .modal-body {
+ padding: 1rem;
+ }
+
+ /* Form Elements */
+ .form-control,
+ .form-select {
+ font-size: 0.875rem;
+ }
+
+ .btn {
+ padding: 0.375rem 0.75rem;
+ font-size: 0.875rem;
+ }
+
+ /* Rating Stars */
+ .rating-container .fa-star {
+ font-size: 1.25rem;
+ }
+
+ .nav-item {
+ min-width: auto;
+ }
+}
+
+/* Small Mobile Devices */
+@media (max-width: 480px) {
+ .nav-container {
+ padding: 0 0.5rem;
+ }
+
+ .nav-item {
+ padding: 0.25rem;
+ }
+
+ .nav-item i {
+ font-size: 1.1rem;
+ }
+
+ .nav-item span {
+ font-size: 0.65rem;
+ }
+
+ .content-card {
+ padding: 0.75rem;
+ }
+
+ .card-header {
+ padding: 0.75rem;
+ }
+
+ .card-body {
+ padding: 0.75rem;
+ }
+
+ .table th,
+ .table td {
+ padding: 0.5rem;
+ }
+
+ .status-badge {
+ padding: 0.2rem 0.5rem;
+ font-size: 0.75rem;
+ }
+
+ .btn {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.75rem;
+ }
+}
+
+/* Landscape Mode */
+@media (max-height: 500px) and (orientation: landscape) {
+ .bottom-nav {
+ position: static;
+ margin-top: 1rem;
+ }
+
+ body {
+ padding-bottom: 0;
+ }
+
+ .content-card {
+ margin-bottom: 1rem;
+ }
+}
+
+/* High-DPI Screens */
+@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
+ .content-card {
+ box-shadow: 0 5px 10px -3px rgba(0, 0, 0, 0.1);
+ }
+}
+
+/* Print Styles */
+@media print {
+ .bottom-nav,
+ .btn-primary,
+ .modal,
+ .modal-backdrop {
+ display: none !important;
+ }
+
+ .content-card {
+ box-shadow: none;
+ border: 1px solid #ddd;
+ }
+
+ .table {
+ box-shadow: none;
+ }
+}
+
+/* Modal styles */
+.modal {
+ z-index: 1050 !important;
+}
+
+.modal-backdrop {
+ z-index: 1040 !important;
+}
+
+.modal-content {
+ border-radius: 8px !important;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1) !important;
+}
+
+.modal-header {
+ border-bottom: 1px solid #e5e7eb;
+ padding: 1rem 1.5rem;
+}
+
+.modal-body {
+ padding: 1.5rem;
+}
+
+.modal-footer {
+ border-top: 1px solid #e5e7eb;
+ padding: 1rem 1.5rem;
+}
+
+/* Navigation enhanced styles */
+.nav-item {
+ transition: transform 0.2s ease-in-out;
+}
+
+.content-section {
+ transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
+ opacity: 0;
+ transform: translateY(20px);
+}
+
+.content-section.active {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+/* Improve active state visibility */
+.nav-item.active {
+ color: var(--primary);
+ font-weight: 600;
+}
+
+.nav-item.active i {
+ transform: scale(1.1);
+}
+
+/* Add ripple effect for touch devices */
+.nav-item {
+ position: relative;
+ overflow: hidden;
+}
+
+.nav-item::after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 5px;
+ height: 5px;
+ background: rgba(79, 70, 229, 0.3);
+ opacity: 0;
+ border-radius: 100%;
+ transform: scale(1, 1) translate(-50%);
+ transform-origin: 50% 50%;
+}
+
+.nav-item:active::after {
+ animation: ripple 0.6s ease-out;
+}
+
+@keyframes ripple {
+ 0% {
+ transform: scale(0, 0);
+ opacity: 0.5;
+ }
+ 100% {
+ transform: scale(100, 100);
+ opacity: 0;
+ }
+}
+
+/* Star Rating Styles */
+.rating-container .form-check-label {
+ cursor: pointer;
+ padding: 0.25rem;
+}
+
+.rating-container .fa-star,
+.rating-container .fa-star-half-alt {
+ color: #f8d74c;
+}
+
+/* Profile Images */
+.profile-image-small {
+ width: 80px;
+ height: 80px;
+ object-fit: cover;
+}
+
+.profile-image-large {
+ width: 200px;
+ height: 200px;
+ object-fit: cover;
+}
+
+/* Add notification toast styles if they don't already exist */
+.notification-toast {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ z-index: 9999;
+ min-width: 250px;
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
+ border-radius: 4px;
+}
+
+/* Welcome Section */
+.welcome-section {
+ background: linear-gradient(to right, #4338ca, #6366f1);
+ color: white;
+ padding: 20px;
+}
+
+/* Schedule styling */
+.schedule-item {
+ padding: 12px;
+ border-radius: 6px;
+ margin-bottom: 10px;
+ background-color: #f8f9fa;
+ border-left: 4px solid var(--primary);
+}
+
+/* Rating stars styling */
+.rating-stars {
+ color: #FFD700;
+}
+
+/* Custom tabs styling */
+.feedback-tabs {
+ border-bottom: 2px solid #e9ecef;
+ margin-bottom: 1.5rem;
+}
+
+.feedback-tab-btn {
+ padding: 10px 20px;
+ cursor: pointer;
+ border-radius: 4px;
+ transition: var(--transition);
+ border: 1px solid var(--border-color);
+ background-color: white;
+ color: var(--text-primary);
+ text-align: center;
+ flex: 1;
+}
+
+.feedback-tab-btn.active {
+ background-color: var(--primary);
+ color: white;
+ border-color: var(--primary);
+}
+
+.feedback-tab-btn:hover:not(.active) {
+ background-color: var(--hover-bg);
+}
+
+.tab-pane {
+ display: none;
+}
+
+.tab-pane.active {
+ display: block;
+}
+
+/* Star rating styling */
+.rating-container .form-check-label i.fas {
+ color: #FFD700;
+}
+
+/* Custom smaller scrollbar styling */
+.activity-scroll::-webkit-scrollbar {
+ width: 4px;
+}
+
+.activity-scroll::-webkit-scrollbar-track {
+ background: #f1f1f1;
+ border-radius: 10px;
+}
+
+.activity-scroll::-webkit-scrollbar-thumb {
+ background: #ccc;
+ border-radius: 10px;
+}
+
+.activity-scroll::-webkit-scrollbar-thumb:hover {
+ background: #999;
+}
+
+/* Firefox scrollbar */
+.activity-scroll {
+ scrollbar-width: thin;
+ scrollbar-color: #ccc #f1f1f1;
+}
+
+@media (min-width: 992px) {
+ .container-fluid {
+ padding-bottom: 60px;
+ }
+}
+
+/* Custom Tab System */
+.custom-tabs {
+ display: flex;
+ border-bottom: 1px solid #dee2e6;
+ margin-bottom: 1rem;
+}
+
+.custom-tab-btn {
+ padding: 0.5rem 1rem;
+ margin-bottom: -1px;
+ background: none;
+ border: 1px solid transparent;
+ border-top-left-radius: 0.25rem;
+ border-top-right-radius: 0.25rem;
+ cursor: pointer;
+ color: var(--secondary);
+}
+
+.custom-tab-btn.active {
+ color: var(--primary);
+ background-color: #fff;
+ border-color: #dee2e6 #dee2e6 #fff;
+}
+
+.custom-tab-content {
+ display: none;
+}
+
+.custom-tab-content.active {
+ display: block;
+}
+
+/* Fix for profile tab visibility */
+#profileSection.active .tab-pane.active {
+ display: block !important;
+}
+
+#profileSection.active .nav-tabs .nav-link.active {
+ color: var(--primary);
+ border-color: var(--primary);
+ background-color: #fff;
+}
+
+/* Action buttons enhanced */
+.action-button {
+ border-radius: 50px;
+ padding: 10px 20px;
+ font-weight: 500;
+ transition: var(--transition);
+ display: inline-flex;
+ align-items: center;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
+}
+
+.action-button:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+}
+
+.action-button i {
+ margin-right: 8px;
+}
\ No newline at end of file
diff --git a/static/img/logo.png b/static/img/logo.png
new file mode 100644
index 0000000..1f51c1a
Binary files /dev/null and b/static/img/logo.png differ
diff --git a/static/img/user.png b/static/img/user.png
new file mode 100644
index 0000000..7127aa8
Binary files /dev/null and b/static/img/user.png differ
diff --git a/static/js/dashboard.js b/static/js/dashboard.js
new file mode 100644
index 0000000..c724eb3
--- /dev/null
+++ b/static/js/dashboard.js
@@ -0,0 +1,563 @@
+document.addEventListener('DOMContentLoaded', function() {
+ console.log("Dashboard script loaded");
+
+ // Set default date to today for leave requests
+ const dateInput = document.getElementById('customLeaveDate');
+ if (dateInput) {
+ const today = new Date();
+ const year = today.getFullYear();
+ let month = today.getMonth() + 1;
+ let day = today.getDate();
+
+ month = month < 10 ? '0' + month : month;
+ day = day < 10 ? '0' + day : day;
+
+ dateInput.min = `${year}-${month}-${day}`;
+ dateInput.value = `${year}-${month}-${day}`;
+ }
+
+ // Close modals with Escape key
+ document.addEventListener('keydown', function(event) {
+ if (event.key === 'Escape') {
+ // Check each modal and close if visible
+ const modalIds = [
+ { id: 'customLeaveModal', closeFn: closeLeaveModal },
+ { id: 'profileModal', closeFn: closeProfileModal },
+ { id: 'passwordModal', closeFn: closePasswordModal },
+ { id: 'feedbackModal', closeFn: closeFeedbackModal },
+ { id: 'pdfViewerModal', closeFn: closePdfViewerModal },
+ { id: 'subjectFilesModal', closeFn: closeSubjectFilesModal }
+ ];
+
+ modalIds.forEach(modal => {
+ const modalElement = document.getElementById(modal.id);
+ if (modalElement && modalElement.style.display === 'block') {
+ modal.closeFn();
+ }
+ });
+ }
+ });
+
+ // Navigation
+ const navItems = document.querySelectorAll('.nav-item');
+ const contentSections = document.querySelectorAll('.content-section');
+
+ // Function to scroll to section with smooth animation
+ function scrollToSection(sectionId) {
+ const section = document.getElementById(sectionId);
+ if (section) {
+ const offset = 20; // Offset from top
+ const sectionPosition = section.getBoundingClientRect().top + window.pageYOffset - offset;
+
+ window.scrollTo({
+ top: sectionPosition,
+ behavior: 'smooth'
+ });
+ }
+ }
+
+ // Function to update active state
+ function updateActiveState(clickedItem) {
+ // Remove active class from all nav items
+ navItems.forEach(nav => {
+ nav.classList.remove('active');
+ nav.style.transform = 'scale(1)';
+ });
+
+ // Add active class to clicked item
+ clickedItem.classList.add('active');
+ clickedItem.style.transform = 'scale(1.1)';
+
+ // Remove active class from all sections
+ contentSections.forEach(section => {
+ section.classList.remove('active');
+ section.style.opacity = '0';
+ section.style.transform = 'translateY(20px)';
+ });
+
+ // Add active class to target section
+ const sectionId = clickedItem.dataset.section + 'Section';
+ const targetSection = document.getElementById(sectionId);
+ if (targetSection) {
+ targetSection.classList.add('active');
+ targetSection.style.opacity = '1';
+ targetSection.style.transform = 'translateY(0)';
+ }
+ }
+
+ // Add click event listeners to nav items
+ navItems.forEach(item => {
+ item.addEventListener('click', function(e) {
+ e.preventDefault();
+ const sectionId = this.dataset.section + 'Section';
+
+ // Update active state
+ updateActiveState(this);
+
+ // Scroll to section
+ scrollToSection(sectionId);
+ });
+ });
+
+ // Add scroll event listener to update active state based on scroll position
+ let scrollTimeout;
+ window.addEventListener('scroll', function() {
+ clearTimeout(scrollTimeout);
+ scrollTimeout = setTimeout(function() {
+ const scrollPosition = window.scrollY + 100; // Offset for better detection
+
+ contentSections.forEach(section => {
+ const sectionTop = section.offsetTop - 100;
+ const sectionBottom = sectionTop + section.offsetHeight;
+
+ if (scrollPosition >= sectionTop && scrollPosition < sectionBottom) {
+ const sectionId = section.id;
+ const navItem = document.querySelector(`.nav-item[data-section="${sectionId.replace('Section', '')}"]`);
+ if (navItem) {
+ updateActiveState(navItem);
+ }
+ }
+ });
+ }, 100);
+ });
+
+ // Add touch feedback
+ navItems.forEach(item => {
+ item.addEventListener('touchstart', function() {
+ this.style.transform = 'scale(0.95)';
+ });
+
+ item.addEventListener('touchend', function() {
+ this.style.transform = this.classList.contains('active') ? 'scale(1.1)' : 'scale(1)';
+ });
+ });
+
+ // Initialize first section as active
+ const firstNavItem = navItems[0];
+ if (firstNavItem) {
+ updateActiveState(firstNavItem);
+ }
+
+ // Initialize rating functionality
+ setupStarRating();
+
+ // Set up subject files buttons
+ document.querySelectorAll('.view-subject-files').forEach(button => {
+ button.addEventListener('click', function() {
+ const subjectId = this.getAttribute('data-subject-id');
+ viewSubjectFiles(subjectId);
+ });
+ });
+
+ // Handle image loading errors
+ document.querySelectorAll('img[src*="student.image.url"]').forEach(img => {
+ img.addEventListener('error', function() {
+ this.onerror = null;
+ this.src = '/static/img/user.png';
+ });
+ });
+
+ // Handle feedback form tabs
+ setupFeedbackTabs();
+
+ // Initialize Bootstrap tooltip and popover components
+ initializeBootstrapComponents();
+
+ // Setup custom tabs
+ setupCustomTabs();
+
+ // Add form validation for rating form
+ setupRatingFormValidation();
+});
+
+// Modal Functions
+function openLeaveModal() {
+ document.getElementById('customLeaveModal').style.display = 'block';
+ document.body.style.overflow = 'hidden';
+}
+
+function closeLeaveModal() {
+ document.getElementById('customLeaveModal').style.display = 'none';
+ document.body.style.overflow = '';
+}
+
+function openProfileModal() {
+ document.getElementById('profileModal').style.display = 'block';
+ document.body.style.overflow = 'hidden';
+}
+
+function closeProfileModal() {
+ document.getElementById('profileModal').style.display = 'none';
+ document.body.style.overflow = '';
+}
+
+function openPasswordModal() {
+ document.getElementById('passwordModal').style.display = 'block';
+ document.body.style.overflow = 'hidden';
+}
+
+function closePasswordModal() {
+ document.getElementById('passwordModal').style.display = 'none';
+ document.body.style.overflow = '';
+}
+
+function openFeedbackModal() {
+ document.getElementById('feedbackModal').style.display = 'block';
+ document.body.style.overflow = 'hidden';
+}
+
+function closeFeedbackModal() {
+ document.getElementById('feedbackModal').style.display = 'none';
+ document.body.style.overflow = '';
+}
+
+function openPdfViewerModal(url) {
+ document.getElementById('pdfViewer').src = url;
+ document.getElementById('pdfViewerModal').style.display = 'block';
+ document.body.style.overflow = 'hidden';
+}
+
+function closePdfViewerModal() {
+ document.getElementById('pdfViewerModal').style.display = 'none';
+ document.body.style.overflow = '';
+}
+
+function openSubjectFilesModal(subjectId) {
+ // Use the viewSubjectFiles function which fetches the files from the server
+ viewSubjectFiles(subjectId);
+}
+
+function closeSubjectFilesModal() {
+ document.getElementById('subjectFilesModal').style.display = 'none';
+ document.body.style.overflow = '';
+}
+
+// Setup custom tabs
+function setupCustomTabs() {
+ const tabButtons = document.querySelectorAll('.custom-tab-btn');
+ if (tabButtons.length > 0) {
+ tabButtons.forEach(button => {
+ button.addEventListener('click', function() {
+ // Remove active class from all buttons
+ tabButtons.forEach(btn => btn.classList.remove('active'));
+
+ // Add active class to clicked button
+ this.classList.add('active');
+
+ // Hide all content
+ document.querySelectorAll('.custom-tab-content').forEach(content => {
+ content.classList.remove('active');
+ });
+
+ // Show target content
+ const target = this.getAttribute('data-target');
+ document.getElementById(target).classList.add('active');
+ });
+ });
+ }
+}
+
+// Setup feedback form validation
+function setupRatingFormValidation() {
+ const ratingForm = document.querySelector('.feedback-form');
+ const ratingInput = document.getElementById('selected_rating');
+ const ratingError = document.getElementById('rating-error');
+
+ if (ratingForm && ratingInput && ratingError) {
+ // Add form validation before submission
+ ratingForm.addEventListener('submit', function(e) {
+ // Check if rating is selected
+ if (!ratingInput.value) {
+ e.preventDefault(); // Stop form submission
+ if (ratingError) {
+ ratingError.style.display = 'block';
+ ratingError.scrollIntoView({ behavior: 'smooth' });
+ }
+ return false;
+ }
+ return true;
+ });
+ }
+}
+
+// Implementation for subject files
+function viewSubjectFiles(subjectId) {
+ // Show loading state
+ const filesList = document.getElementById('subjectFilesList');
+ if (!filesList) return;
+
+ filesList.innerHTML = `
+
+
+ Loading...
+
+
Loading syllabus...
+
+ `;
+
+ // Show modal
+ document.getElementById('subjectFilesModal').style.display = 'block';
+ document.body.style.overflow = 'hidden';
+
+ // Fetch syllabus
+ fetch(`/app/subject/${subjectId}/files/`)
+ .then(response => {
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+ return response.json();
+ })
+ .then(data => {
+ if (data.success) {
+ if (!data.files || data.files.length === 0) {
+ filesList.innerHTML = `
+
+
+
No syllabus available for this subject
+
+ `;
+ } else {
+ let html = '';
+ data.files.forEach(file => {
+ html += `
+
-
-
-{% endblock %}
\ No newline at end of file
+
+ // Run on load and resize
+ adjustLayout();
+ window.addEventListener('resize', adjustLayout);
+ });
+
+
+
+
- {% with ''|center:avg_rating_rounded as range %}
- {% for _ in range %}
-
- {% endfor %}
- {% endwith %}
- {% with ''|center:remaining_stars as range %}
- {% for _ in range %}
-
- {% endfor %}
- {% endwith %}
-