diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b52d6c4 --- /dev/null +++ b/.env.example @@ -0,0 +1,24 @@ +# Django Settings +DJANGO_SECRET_KEY=your-secret-key-here +DJANGO_DEBUG=True +DJANGO_ALLOWED_HOSTS=* + +# Firebase Configuration +FIREBASE_API_KEY=your_firebase_api_key_here +FIREBASE_AUTH_DOMAIN=your_project_id.firebaseapp.com +FIREBASE_PROJECT_ID=your_project_id +FIREBASE_STORAGE_BUCKET=your_project_id.firebasestorage.app +FIREBASE_MESSAGING_SENDER_ID=your_sender_id +FIREBASE_APP_ID=your_app_id +FIREBASE_MEASUREMENT_ID=your_measurement_id + +# Email Configuration +EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend +DEFAULT_FROM_EMAIL=noreply@studentmanagementsystem.com + +# SMS Configuration +SMS_API_KEY=your_sms_api_key_here +SMS_SENDER_ID=SMSSYS + +# Cache Configuration +CACHE_LOCATION=unique-snowflake \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7ec4427..e5db256 100644 --- a/.gitignore +++ b/.gitignore @@ -150,3 +150,7 @@ media/result_sheet/* # Custom dump.rdb *.DS_Store + +# Firebase configuration +firebase-config.json +firebase.json diff --git a/README.md b/README.md index 860c0e1..9c9ec51 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,230 @@ -

- Logo -

+# ๐Ÿ“š Student Management System (SMS) -

Student Management System In Django

+
-

+[![Python](https://img.shields.io/badge/python-3.x-blue.svg)](https://www.python.org/) +[![Django](https://img.shields.io/badge/django-5.1.7-green.svg)](https://www.djangoproject.com/) +[![License](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE) +[![Stars](https://img.shields.io/github/stars/S4NKALP/Student-Management-System-In-Django)](https://github.com/S4NKALP/Student-Management-System-In-Django/stargazers) +[![Arch Linux](https://img.shields.io/badge/Arch%20Linux-1793D1?logo=arch-linux&logoColor=white)](https://archlinux.org/) +[![Status](https://img.shields.io/badge/status-active-success.svg)]() - -People +A modern and comprehensive Student Management System built with Django. - -Stars +

- -Forks +## ๐Ÿ’ป Tech Stack - -Watches +
- -Last Updated -

+### ๐Ÿ”ฅ Core Technologies +| Python | Django | JavaScript | MySQL | REST API | +|:---:|:---:|:---:|:---:|:---:| +| Python | Django | JavaScript | MySQL | REST API | -
+### โšก Additional Technologies +| Redis | Nginx | Bootstrap | HTML5 | CSS3 | Git | +|:---:|:---:|:---:|:---:|:---:|:---:| +| Redis | Nginx | Bootstrap | HTML5 | CSS3 | Git | -# WIP +
+ +## ๐Ÿ–ฅ๏ธ Development Environment + +- **Processor**: Intel Core i5-7300U +- **Operating System**: Arch Linux +- **Window Manager**: [Hyprland](https://github.com/S4NKALP/hyprland) +- **Status Bar**: [Modus](https://github.com/S4NKALP/Modus) +- **Editor**: [Neovim](https://github.com/S4NKALP/nvim) + +## โœจ Key Features + +
+ +| 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. +``` + +
+ +### ๐ŸŽฏ Current (v1.0) +- โœ… Core SMS functionality +- โœ… User management +- โœ… Course tracking +- โœ… Basic features + +### ๐Ÿ”œ Upcoming (v1.1+) +- ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Parents dashboard +- ๐Ÿ“š Teacher study materials +- ๐Ÿ“– Library management +- ๐Ÿ“Š Enhanced analytics +- ๐Ÿค Parent-teacher meetings + +## ๐Ÿ“ฎ Support & Links + +
+ +[![GitHub Issues](https://img.shields.io/github/issues/S4NKALP/Student-Management-System-In-Django)](https://github.com/S4NKALP/Student-Management-System-In-Django/issues) +[![GitHub Pull Requests](https://img.shields.io/github/issues-pr/S4NKALP/Student-Management-System-In-Django)](https://github.com/S4NKALP/Student-Management-System-In-Django/pulls) +[![Documentation](https://img.shields.io/badge/docs-wiki-blue)](https://github.com/S4NKALP/Student-Management-System-In-Django/wiki) + +
+ +## ๐Ÿ™ 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 + +
+ +[![License](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE) + +
+ +--- + +
+ +### Made with โค๏ธ by [Sankalp](https://github.com/S4NKALP) + +[![GitHub](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white)](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 += ` +
+
+
+
+ + ${file.title} +
+

${file.description || 'No description'}

+
+
+ ${file.file_url ? ` + + Download + + ` : ` + + File not available + + `} +
+
+
+ `; + }); + filesList.innerHTML = html; + } + } else { + throw new Error(data.message || 'Failed to load syllabus'); + } + }) + .catch(error => { + console.error('Error:', error); + filesList.innerHTML = ` +
+ +

Unable to load syllabus. Please try again later.

+ ${error.message} +
+ `; + }); +} + +// Setup feedback tabs +function setupFeedbackTabs() { + const teacherTabBtn = document.getElementById('teacher-tab-btn'); + const instituteTabBtn = document.getElementById('institute-tab-btn'); + + if (teacherTabBtn && instituteTabBtn) { + teacherTabBtn.addEventListener('click', function() { + switchFeedbackTab('teacher'); + }); + + instituteTabBtn.addEventListener('click', function() { + switchFeedbackTab('institute'); + }); + } +} + +// Tab switching function +function switchFeedbackTab(tabId) { + // Hide all tab contents + document.querySelectorAll('.tab-pane').forEach(tab => { + tab.classList.remove('active'); + }); + + // Deactivate all tab buttons + document.querySelectorAll('.feedback-tab-btn').forEach(btn => { + btn.classList.remove('active'); + }); + + // Show selected tab content and activate button + document.getElementById(`${tabId}-tab-content`).classList.add('active'); + document.getElementById(`${tabId}-tab-btn`).classList.add('active'); +} + +// Initialize Bootstrap components +function initializeBootstrapComponents() { + // Initialize tooltips if available + if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) { + const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); + tooltipTriggerList.map(function (tooltipTriggerEl) { + return new bootstrap.Tooltip(tooltipTriggerEl); + }); + } + + // Initialize popovers if available + if (typeof bootstrap !== 'undefined' && bootstrap.Popover) { + const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')); + popoverTriggerList.map(function (popoverTriggerEl) { + return new bootstrap.Popover(popoverTriggerEl); + }); + } +} + +// Function to show notification toast +function showNotification(title, message, type = 'success') { + const toast = document.getElementById('notificationToast'); + if (!toast) return; + + const toastTitle = document.getElementById('toastTitle'); + const toastMessage = document.getElementById('toastMessage'); + + toastTitle.innerText = title; + toastMessage.innerText = message; + + // Set toast color based on type + toast.className = 'toast'; + if (type === 'success') { + toast.classList.add('bg-success', 'text-white'); + } else if (type === 'error') { + toast.classList.add('bg-danger', 'text-white'); + } else if (type === 'warning') { + toast.classList.add('bg-warning'); + } else if (type === 'info') { + toast.classList.add('bg-info', 'text-white'); + } + + const bsToast = new bootstrap.Toast(toast); + bsToast.show(); +} + +// Function to set up star rating +function setupStarRating() { + // Teacher feedback + setupStarRatingComponent('teacher-rating', 'teacherStar'); + // Institute feedback + setupStarRatingComponent('institute-rating', 'instituteStar'); + // Generic rating stars + setupGenericRatingStars(); +} + +function setupStarRatingComponent(containerId, inputPrefix) { + const container = document.getElementById(containerId); + if (!container) return; + + const stars = container.querySelectorAll('.form-check-label'); + const inputs = Array.from(container.querySelectorAll('input[type="radio"]')); + + stars.forEach((star, index) => { + // Click event + star.addEventListener('click', function() { + // Update visual state + updateStars(stars, index); + + // Update form input + inputs[index].checked = true; + }); + + // Hover effects + star.addEventListener('mouseenter', function() { + highlightStars(stars, index); + }); + + container.addEventListener('mouseleave', function() { + resetStars(stars, inputs); + }); + }); +} + +function setupGenericRatingStars() { + const stars = document.querySelectorAll('.rating-star'); + const ratingInput = document.getElementById('selected_rating'); + const ratingError = document.getElementById('rating-error'); + + if (stars.length > 0 && ratingInput) { + stars.forEach(star => { + // Click event + star.addEventListener('click', function() { + const value = parseInt(this.getAttribute('data-value')); + ratingInput.value = value; + + // Update the stars + stars.forEach((s, index) => { + if (index < value) { + s.className = 'fas fa-star fs-4 text-warning rating-star'; + } else { + s.className = 'far fa-star fs-4 text-warning rating-star'; + } + }); + + // Hide error message if displayed + if (ratingError) ratingError.style.display = 'none'; + }); + + // Hover events + star.addEventListener('mouseenter', function() { + const value = parseInt(this.getAttribute('data-value')); + + stars.forEach((s, index) => { + if (index < value) { + s.className = 'fas fa-star fs-4 text-warning rating-star'; + } + }); + }); + + star.addEventListener('mouseleave', function() { + const selectedValue = parseInt(ratingInput.value) || 0; + + stars.forEach((s, index) => { + if (index < selectedValue) { + s.className = 'fas fa-star fs-4 text-warning rating-star'; + } else { + s.className = 'far fa-star fs-4 text-warning rating-star'; + } + }); + }); + }); + } +} + +function updateStars(stars, selectedIndex) { + stars.forEach((star, i) => { + const starIcon = star.querySelector('i'); + if (i <= selectedIndex) { + starIcon.className = 'fas fa-star fs-4'; + } else { + starIcon.className = 'far fa-star fs-4'; + } + }); +} + +function highlightStars(stars, hoverIndex) { + stars.forEach((star, i) => { + const starIcon = star.querySelector('i'); + if (i <= hoverIndex) { + starIcon.className = 'fas fa-star fs-4'; + } + }); +} + +function resetStars(stars, inputs) { + const selectedIndex = inputs.findIndex(input => input.checked); + stars.forEach((star, i) => { + const starIcon = star.querySelector('i'); + if (selectedIndex >= 0 && i <= selectedIndex) { + starIcon.className = 'fas fa-star fs-4'; + } else { + starIcon.className = 'far fa-star fs-4'; + } + }); +} \ No newline at end of file diff --git a/static/js/feedback_rating.js b/static/js/feedback_rating.js new file mode 100644 index 0000000..7e7b505 --- /dev/null +++ b/static/js/feedback_rating.js @@ -0,0 +1,113 @@ +document.addEventListener('DOMContentLoaded', function() { + // Handle star rating selection for teacher feedback + const teacherStars = document.querySelectorAll('[id^="teacherStar"]'); + const teacherLabels = document.querySelectorAll('label[for^="teacherStar"]'); + + teacherLabels.forEach(label => { + label.addEventListener('click', function() { + const starId = this.getAttribute('for'); + const starNumber = parseInt(starId.replace('teacherStar', '')); + + // Update visual appearance + teacherLabels.forEach((lbl, index) => { + const star = lbl.querySelector('i'); + if (index < starNumber) { + star.className = 'fas fa-star fs-4 text-warning'; + } else { + star.className = 'far fa-star fs-4 text-warning'; + } + }); + + // Set the correct radio button + document.getElementById(starId).checked = true; + }); + + // Add hover effects + label.addEventListener('mouseenter', function() { + const starId = this.getAttribute('for'); + const starNumber = parseInt(starId.replace('teacherStar', '')); + + teacherLabels.forEach((lbl, index) => { + const star = lbl.querySelector('i'); + if (index < starNumber) { + star.className = 'fas fa-star fs-4 text-warning'; + } + }); + }); + + label.addEventListener('mouseleave', function() { + // Reset to selected state + let selectedValue = 0; + teacherStars.forEach((radio, index) => { + if (radio.checked) { + selectedValue = index + 1; + } + }); + + teacherLabels.forEach((lbl, index) => { + const star = lbl.querySelector('i'); + if (index < selectedValue) { + star.className = 'fas fa-star fs-4 text-warning'; + } else { + star.className = 'far fa-star fs-4 text-warning'; + } + }); + }); + }); + + // Handle star rating selection for institute feedback + const instituteStars = document.querySelectorAll('[id^="instituteStar"]'); + const instituteLabels = document.querySelectorAll('label[for^="instituteStar"]'); + + instituteLabels.forEach(label => { + label.addEventListener('click', function() { + const starId = this.getAttribute('for'); + const starNumber = parseInt(starId.replace('instituteStar', '')); + + // Update visual appearance + instituteLabels.forEach((lbl, index) => { + const star = lbl.querySelector('i'); + if (index < starNumber) { + star.className = 'fas fa-star fs-4 text-warning'; + } else { + star.className = 'far fa-star fs-4 text-warning'; + } + }); + + // Set the correct radio button + document.getElementById(starId).checked = true; + }); + + // Add hover effects + label.addEventListener('mouseenter', function() { + const starId = this.getAttribute('for'); + const starNumber = parseInt(starId.replace('instituteStar', '')); + + instituteLabels.forEach((lbl, index) => { + const star = lbl.querySelector('i'); + if (index < starNumber) { + star.className = 'fas fa-star fs-4 text-warning'; + } + }); + }); + + label.addEventListener('mouseleave', function() { + // Reset to selected state + let selectedValue = 0; + instituteStars.forEach((radio, index) => { + if (radio.checked) { + selectedValue = index + 1; + } + }); + + instituteLabels.forEach((lbl, index) => { + const star = lbl.querySelector('i'); + if (index < selectedValue) { + star.className = 'fas fa-star fs-4 text-warning'; + } else { + star.className = 'far fa-star fs-4 text-warning'; + } + }); + }); + }); +}); \ No newline at end of file diff --git a/student_management_system/jazzmin.py b/student_management_system/jazzmin.py new file mode 100644 index 0000000..8a3e150 --- /dev/null +++ b/student_management_system/jazzmin.py @@ -0,0 +1,98 @@ +JAZZMIN_SETTINGS = { + "site_title": "Student Management System", + "site_header": "Student Management", + "site_brand": "SMS", + "site_logo": "img/logo.png", + "login_logo": "img/logo.png", + "login_logo_dark": None, + "site_logo_classes": "img-circle", + "login_logo_classes": "", + "custom_css": "css/custom.css", + "welcome_sign": "Welcome to Student Management System", + "copyright": "Student Management System", + "search_model": ["app.Student", "app.Staff", "app.Course"], + "topmenu_links": [ + {"name": "Dashboard", "url": "admin:index", "permissions": ["auth.view_user"]}, + {"model": "auth.User"}, + ], + "usermenu_links": [{"model": "auth.user"}], + "show_sidebar": True, + "navigation_expanded": True, + "hide_apps": [], + "hide_models": [], + "order_with_respect_to": [ + "dashboard", + "authentication", + "organization", + "human_managementorganization.institute", + "organization.batch", + "organization.course", + "human_management.student", + "human_management.staff", + "human_management.routine", + "human_management.staff_leave", + "human_management.student_leave", + "human_management.studentfeedback", + "human_management.coursetracking", + "human_management.attendance", + "human_management.institutefeedback", + ], + "icons": { + "auth": "fas fa-users-cog", + "auth.user": "fas fa-user", + "auth.Group": "fas fa-users", + "app.student": "fas fa-user-graduate", + "app.staff": "fas fa-chalkboard-teacher", + "app.course": "fas fa-book", + "app.subject": "fas fa-book-open", + "app.attendance": "fas fa-calendar-check", + "app.studentfeedback": "fas fa-comment", + "app.institutefeedback": "fas fa-comments", + "app.student_leave": "fas fa-calendar-times", + "app.staff_leave": "fas fa-calendar-minus", + "app.notice": "fas fa-bullhorn", + }, + "default_icon_parents": "fas fa-chevron-circle-right", + "default_icon_children": "fas fa-circle", + "related_modal_active": False, + "custom_js": None, + "show_ui_builder": False, + "changeform_format": "horizontal_tabs", + "changeform_format_overrides": { + "auth.user": "collapsible", + "auth.group": "vertical_tabs", + }, + "language_chooser": False, +} + +JAZZMIN_UI_TWEAKS = { + "navbar_small_text": False, + "footer_small_text": False, + "body_small_text": False, + "brand_small_text": False, + "brand_colour": False, + "accent": "accent-primary", + "navbar": "navbar-dark", + "no_navbar_border": False, + "navbar_fixed": False, + "layout_boxed": False, + "footer_fixed": False, + "sidebar_fixed": False, + "sidebar": "sidebar-dark-primary", + "sidebar_nav_small_text": False, + "sidebar_disable_expand": False, + "sidebar_nav_child_indent": False, + "sidebar_nav_compact_style": False, + "sidebar_nav_legacy_style": False, + "sidebar_nav_flat_style": False, + "theme": "default", + "dark_mode_theme": None, + "button_classes": { + "primary": "btn-primary", + "secondary": "btn-secondary", + "info": "btn-info", + "warning": "btn-warning", + "danger": "btn-danger", + "success": "btn-success" + } +} diff --git a/student_management_system/settings.py b/student_management_system/settings.py index 64b90af..c12213d 100644 --- a/student_management_system/settings.py +++ b/student_management_system/settings.py @@ -11,21 +11,26 @@ https://docs.djangoproject.com/en/5.1/ref/settings/ """ import os +from pathlib import Path +from dotenv import load_dotenv +from student_management_system.jazzmin import JAZZMIN_SETTINGS, JAZZMIN_UI_TWEAKS + +# Load environment variables +load_dotenv() # Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "django-insecure-r))8$j-a-kc641we86oiyxr&x$1e43c!7y4#w-r1s@$$q8dik8" +SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] +DEBUG = os.getenv('DJANGO_DEBUG', 'False') == 'True' +ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', '*').split(',') # Application definition @@ -38,6 +43,7 @@ INSTALLED_APPS = [ "django.contrib.messages", "django.contrib.staticfiles", "rest_framework", + "mathfilters", "app", ] @@ -139,96 +145,37 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" MEDIA_URL = "/media/" MEDIA_ROOT = os.path.join(BASE_DIR, "media") -JAZZMIN_SETTINGS = { - "site_title": "SMS", - "site_header": "SMS", - "site_brand": "SMS", - "site_logo_classes": "img-circle", - "welcome_sign": "WELCOME TO SMS", - "copyright": "S4NKALP: SMS", - "search_model": ["app.Student", "app.Course"], - "topmenu_links": [ - {"name": "Home", "url": "admin:index", "permissions": ["auth.view_user"]}, - {"name": "Dashboard", "url": "/app/dashboard", "permissions": []}, - {"model": "app.Student"}, - {"app": "app"}, - ], - "usermenu_links": [ - {"model": "auth.user"} - ], - "show_sidebar": True, - "navigation_expanded": True, - "hide_apps": [], - "hide_models": [], - "order_with_respect_to": [ - "dashboard", - "authentication","organization","human_management" - "organization.institute", "organization.batch", "organization.course", - "human_management.student", "human_management.staff", "human_management.routine", - "human_management.staff_leave", "human_management.student_leave", - "human_management.studentfeedback", "human_management.coursetracking", - "human_management.attendance", "human_management.institutefeedback" - ], - "icons": { - "auth": "fas fa-users-cog", - "auth.user": "fas fa-user", - "auth.Group": "fas fa-users", - "app.Student": "fas fa-user-graduate", - "app.Course": "fas fa-book", - "app.Staff": "fas fa-chalkboard-teacher", - "app.Attendance": "fas fa-calendar-check", - "app.CourseTracking": "fas fa-chart-line", - "app.StudentFeedback": "fas fa-comment", - }, - "default_icon_parents": "fas fa-chevron-circle-right", - "default_icon_children": "fas fa-circle", - "related_modal_active": False, - "custom_css": None, - "custom_js": None, - "show_ui_builder": False, - "changeform_format": "horizontal_tabs", - "changeform_format_overrides": {"auth.user": "collapsible", "auth.group": "vertical_tabs"}, - "language_chooser": False, - "custom_links": { - "app": [ - { - "name": "Dashboard", - "url": "/app/dashboard", - "icon": "fas fa-tachometer-alt", - "permissions": [] - } - ] - }, -} -JAZZMIN_UI_TWEAKS = { - "navbar_small_text": False, - "footer_small_text": False, - "body_small_text": False, - "brand_small_text": False, - "brand_colour": False, - "accent": "accent-primary", - "navbar": "navbar-dark", - "no_navbar_border": False, - "navbar_fixed": True, - "layout_boxed": False, - "footer_fixed": False, - "sidebar_fixed": True, - "sidebar": "sidebar-dark-primary", - "sidebar_nav_small_text": False, - "sidebar_disable_expand": False, - "sidebar_nav_child_indent": False, - "sidebar_nav_compact_style": False, - "sidebar_nav_legacy_style": False, - "sidebar_nav_flat_style": False, - "theme": "default", - "dark_mode_theme": None, - "button_classes": { - "primary": "btn-primary", - "secondary": "btn-secondary", - "info": "btn-info", - "warning": "btn-warning", - "danger": "btn-danger", - "success": "btn-success" +# Jazmin Settings +JAZZMIN_SETTINGS = JAZZMIN_SETTINGS +JAZZMIN_UI_TWEAKS = JAZZMIN_UI_TWEAKS + +# Login redirect URL +LOGIN_REDIRECT_URL = '/app/dashboard/' +LOGIN_URL = '/login/' + +# Email Configuration +EMAIL_BACKEND = os.getenv('EMAIL_BACKEND', 'django.core.mail.backends.console.EmailBackend') +DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'noreply@studentmanagementsystem.com') + +# Cache configuration for OTP storage +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': os.getenv('CACHE_LOCATION', 'unique-snowflake'), } } + +# SMS API configuration +SMS_API_KEY = os.getenv('SMS_API_KEY', '') +SMS_SENDER_ID = os.getenv('SMS_SENDER_ID', 'SMSSYS') + + +CSRF_TRUSTED_ORIGINS = [ + "https://ab4c-2405-acc0-1504-65ce-00-2.ngrok-free.app" +] + +CSRF_TRUSTED_ORIGINS = [ + "https://*.ngrok-free.app", + "https://*.ngrok.io" +] diff --git a/student_management_system/urls.py b/student_management_system/urls.py index ac6627c..260b3ac 100644 --- a/student_management_system/urls.py +++ b/student_management_system/urls.py @@ -19,9 +19,28 @@ from django.contrib import admin from django.urls import path, include from django.conf import settings from django.conf.urls.static import static +from django.contrib.auth import views as auth_views from app.admin import custom_admin_site +from django.views.generic import RedirectView +from app import views +from app import auth -urlpatterns = [ +# Serve media files without requiring authentication +urlpatterns = static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + +# Add other URL patterns +urlpatterns += [ path("app/", include("app.urls")), - path("", custom_admin_site.urls), -] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + path("admin/", custom_admin_site.urls), # Admin site at /admin/ + path("", RedirectView.as_view(url="/app/dashboard/", permanent=False)), # Redirect root to dashboard + + # Custom login and logout views + path('login/', auth_views.LoginView.as_view(template_name='login/login.html'), name='login'), + path('logout/', auth_views.LogoutView.as_view(next_page='/login/'), name='logout'), + + # Password reset views - main entry point + path('password-reset/', auth.reset_password_options, name='password_reset'), +] + +if settings.DEBUG: + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/templates/admin/app/routine/change_form.html b/templates/admin/app/routine/change_form.html new file mode 100644 index 0000000..dc240da --- /dev/null +++ b/templates/admin/app/routine/change_form.html @@ -0,0 +1,555 @@ +{% extends "admin/change_form.html" %} +{% load static %} +{% load mathfilters %} + +{% block extrahead %} +{{ block.super }} + + + +{% endblock %} + +{% block content %} +
+
+

Add Routines

+
+
+
+ {% csrf_token %} + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+ + + + + + + + + + + + + + + +
SubjectTeacherStart TimeEnd TimeAction
+ Please select course and semester +
+
+ +
+
+ +
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..6acb351 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,22 @@ +{% load static %} + + + + + + {% block title %}Student Management System{% endblock %} + + + + + + + + {% block extrastyle %}{% endblock %} + + + {% block content %}{% endblock %} + + {% block extrahead %}{% endblock %} + + \ No newline at end of file diff --git a/templates/dashboard.html b/templates/dashboard.html index 8a0e7c8..29deed2 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -1,225 +1,818 @@ {% extends "admin/base_site.html" %} {% load static %} {% load app_filters %} +{% block extrahead %} + {{ block.super }} + +{% endblock %} +{% block extrastyle %} + {{ block.super }} + + + + + +{% endblock %} {% block content %} -
- -
- -
-
-
-

{{ total_students }}

-

Total Students

-
- Active: {{ active_students }} | - Completed: {{ completed_students }} | - On Leave: {{ on_leave_students }} -
-
-
- -
+
+ +
+
+
+
+
+

Welcome, Admin!

+

Student Management Dashboard

+
+
+
+
+
+ +
+ +
+
+
+
+ Students +
-
- - -
-
-
-

{{ total_courses }}

-

Course Statistics

-
- Active: {{ active_courses }} | - Completed: {{ completed_courses }} +
+
+
+
{{ total_students }}
+
Total Students
-
- -
-
-
- - -
-
-
-

{{ in_progress_trackings }}

-

Course Progress

-
- In Progress | - Completed: {{ completed_trackings }} | - Dropped: {{ dropped_trackings }} +
+
+ Active + {{ active_students }}
-
-
- -
-
-
- - -
-
-
-

{{ today_present }}/{{ today_attendance }}

-

Today's Attendance

-
- Rate: {{ attendance_rate }}% +
+ Completed + {{ completed_students }} +
+
+ On Leave + {{ on_leave_students }}
-
-
-
- -
- -
-
-
-

Course Progress Overview

-
-
-
- - - - - - - - - - - - {% for progress in course_progress %} - - - - - - - - {% endfor %} - -
CourseTotal StudentsIn ProgressCompletedAvg Progress
{{ progress.course.name }}{{ progress.total_students }}{{ progress.in_progress }}{{ progress.completed }} -
-
- {{ progress.average_progress }}% -
-
-
-
-
+ +
+
+
+
+ Courses +
-
- - -
-
-
-

Recent Activity

+
+
+
+
{{ total_courses }}
+
Total Courses
+
-
- -
Recent Leave Requests
-
-
Students
- {% for leave in recent_student_leaves %} -
- {{ leave.created_at|date:"M d, Y" }} -
{{ leave.student.name }} - {{ leave.get_status_display }}
-
- {% empty %} -

No recent student leave requests

- {% endfor %} +
+
+ Active + {{ active_courses }}
- -
Staff
- {% for leave in recent_staff_leaves %} -
- {{ leave.created_at|date:"M d, Y" }} -
{{ leave.staff.name }} - {{ leave.get_status_display }}
+
+ Completed + {{ completed_courses }}
- {% empty %} -

No recent staff leave requests

- {% endfor %} - - -
Recent Feedback
- {% for feedback in recent_feedback %} -
- {{ feedback.created_at|date:"M d, Y" }} -
{{ feedback.student.name }} - {{ feedback.subject.name }}
-
- {% for i in feedback.rating|get_range %} - - {% endfor %} -
-
- {% empty %} -

No recent feedback

- {% endfor %}
- -
-
-
-
-

Monthly Statistics

+ +
+
+
+
+ Staff +
+
+
+
+
+
{{ total_staff }}
+
Total Staff
+
-
-
-
-
- - - -
- New Students - {{ monthly_stats.new_students }} -
-
-
-
-
- - - -
- Completed Courses - {{ monthly_stats.completed_courses }} -
-
-
-
-
- - - -
- Monthly Attendance Rate - {{ monthly_stats.attendance_rate }}% -
-
-
+
+
+ Active + {{ active_staff }} +
+
+ On Leave + {{ staff_on_leave }} +
+
+
+
+
+ + +
+
+
+
+ Attendance +
+
+
+
+
+
{{ total_classes_today }}
+
Classes Today
+
+
+
+
+ Students Present + {{ students_present }}/{{ total_students_today }} +
+
+ Attendance Rate + + {{ attendance_rate }}% +
-{% endblock %} \ No newline at end of file +
+ +
+
+ +
+
+ +
+ +
+
+
+Course Progress +
+
+ + + + + + + + + + +{% for progress in course_progress %} + + + + + + +{% empty %} + + + +{% endfor %} + +
CourseStudentsProgressStatus
{{ progress.course.name }}{{ progress.total_students }} +
+
+
+
+{{ progress.average_progress }}% +
+ +{% if progress.average_progress == 100 %} +Completed +{% elif progress.average_progress > 0 %} +In Progress +{% else %} +Not Started +{% endif %} + +
No course progress data available
+
+
+
+ +
+
+
+Recent Activity +
+
+{% for activity in recent_activities %} +
+
+
{{ activity.title }}
+

{{ activity.description }}

+{{ activity.timestamp|timesince }} ago +
+
+{% empty %} +

No recent activity

+{% endfor %} +
+
+
+
+ +
+
+
+
+Pending Leave Requests +
+
+ + + + + + + + + + + +{% for leave in pending_student_leaves %} + + + + + + + +{% endfor %} +{% for leave in pending_staff_leaves %} + + + + + + + +{% endfor %} +{% if not pending_student_leaves and not pending_staff_leaves %} + + + +{% endif %} + +
NameTypeDateReasonActions
{{ leave.student.name }} +Student +{{ leave.date|date:"M d, Y" }}{{ leave.message }} +
+ {% csrf_token %} + +
+
+ {% csrf_token %} + +
+
{{ leave.staff.name }} +Staff +{{ leave.date|date:"M d, Y" }}{{ leave.message }} +
+ {% csrf_token %} + +
+
+ {% csrf_token %} + +
+
No pending leave requests
+
+
+
+
+ +
+
+
+
+Notice Board +
+
+{% csrf_token %} +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+{% for notice in notices %} +
+
+
+
{{ notice.title }}
+{{ notice.created_at|date:"M d, Y" }} +
+{% if notice.image %} +Notice Image +{% endif %} +

{{ notice.message }}

+ +
+
+{% empty %} +

No notices available

+{% endfor %} +
+
+
+
+ +
+
+
+
+Teacher Ratings +
+
+{% for teacher in teacher_ratings %} +
+
+
+{% if teacher.image %} +{{ teacher.name }} +{% else %} +
+ +
+{% endif %} +
+
{{ teacher.name }}
+{{ teacher.designation }} +
+
+
+
+Average Rating +{{ teacher.avg_rating|floatformat:1 }}/5.0 +
+
+{% for i in "12345"|make_list %} + +{% endfor %} +
+
+
+
+
+
+
+
+Total Reviews +{{ teacher.total_reviews }} +
+
+Students Taught +{{ teacher.total_students }} +
+
+
+
+{% empty %} +
+

No teacher ratings available

+
+{% endfor %} +
+
+
+
+ +
+
+
+
+Monthly Statistics +
+
+
+
+ +

{{ monthly_stats.new_students }}

+

New Students

+
+
+
+
+ +

{{ monthly_stats.new_staff }}

+

New Staff

+
+
+
+
+ +

{{ monthly_stats.attendance_rate }}%

+

Attendance Rate

+
+
+
+
+ +

{{ monthly_stats.leave_requests }}

+

Leave Requests

+
+
+
+
+
+
+ +
+
+
+
+Institute Feedback +
+
+ + + + + + + + + + + + +{% for feedback in recent_institute_feedback %} + + + + + + + + +{% empty %} + + + +{% endfor %} + +
TypeCategoryNameRatingFeedbackDate
+ +{{ feedback.user_type|title }} + +{{ feedback.feedback_type }}{{ feedback.display_name }} +
+{% for i in "12345"|make_list %} + +{% endfor %} +
+
{{ feedback.feedback_text|truncatechars:100 }}{{ feedback.created_at|date:"M d, Y" }}
No institute feedback available
+
+
+
+
+
+ + +{% endblock %} +{% block extrajs %} +{{ block.super }} + + +{% endblock %} diff --git a/templates/login/email_reset.html b/templates/login/email_reset.html new file mode 100644 index 0000000..71a32ac --- /dev/null +++ b/templates/login/email_reset.html @@ -0,0 +1,170 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+ +

Reset Password

+ +

Enter your email address and we'll send you a verification code to reset your password.

+ + {% if error %} +
+ {{ error }} +
+ {% endif %} + + {% if success %} +
+ {{ success }} +
+ {% endif %} + + {% if messages %} +
+ {% for message in messages %} + {{ message }} + {% endfor %} +
+ {% endif %} + +
+ {% csrf_token %} +
+ + +
+ + +
+ + โ† Back to Reset Options +
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/login/email_templates/password_reset_email.html b/templates/login/email_templates/password_reset_email.html new file mode 100644 index 0000000..63db663 --- /dev/null +++ b/templates/login/email_templates/password_reset_email.html @@ -0,0 +1,83 @@ +{% autoescape off %} + + + + + + Password Reset + + + +
+
+

Password Reset

+
+ +
+

Hello,

+

We received a request to reset your password for your Student Management System account. If you did not make this request, please ignore this email.

+

To reset your password, use the following code:

+
{{ reset_code }}
+

This code will expire in 5 minutes.

+

If you have any questions, please contact the system administrator.

+

Thank you,
Student Management System Team

+
+ + +
+ + +{% endautoescape %} \ No newline at end of file diff --git a/templates/login/email_templates/password_reset_subject.txt b/templates/login/email_templates/password_reset_subject.txt new file mode 100644 index 0000000..db28747 --- /dev/null +++ b/templates/login/email_templates/password_reset_subject.txt @@ -0,0 +1 @@ +Password Reset for Student Management System \ No newline at end of file diff --git a/templates/login/login.html b/templates/login/login.html new file mode 100644 index 0000000..830d2b6 --- /dev/null +++ b/templates/login/login.html @@ -0,0 +1,168 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} + + + + + + Login - Student Management System + + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/login/password_reset_complete.html b/templates/login/password_reset_complete.html new file mode 100644 index 0000000..9fea7ce --- /dev/null +++ b/templates/login/password_reset_complete.html @@ -0,0 +1,113 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} + + + + + + Password Reset Complete - Student Management System + + + +
+ +

Password Reset Complete

+ +
โœ“
+ +
+

Your password has been successfully reset.

+
+ +

You can now use your new password to log in to your account.

+ + +
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/login/password_reset_confirm.html b/templates/login/password_reset_confirm.html new file mode 100644 index 0000000..788ba0f --- /dev/null +++ b/templates/login/password_reset_confirm.html @@ -0,0 +1,178 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} + + + + + + Set New Password - Student Management System + + + +
+ +

Set New Password

+ + {% if validlink %} +

Please enter your new password below.

+ + {% if form.errors %} +
+ {% for field in form %} + {% for error in field.errors %} + {{ error }}
+ {% endfor %} + {% endfor %} +
+ {% endif %} + +
+ {% csrf_token %} +
+ + {{ form.new_password1 }} +
+ +
+ + {{ form.new_password2 }} +
+ + +
+ {% else %} +
+

The password reset link was invalid, possibly because it has already been used or has expired.

+
+

Please request a new password reset link.

+ Request New Reset Link + {% endif %} +
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/login/password_reset_done.html b/templates/login/password_reset_done.html new file mode 100644 index 0000000..4546ca0 --- /dev/null +++ b/templates/login/password_reset_done.html @@ -0,0 +1,107 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} + + + + + + Password Reset Email Sent - Student Management System + + + +
+ +

Email Sent

+ +
โœ“
+ +
+

We've emailed you instructions for setting your password. You should receive them shortly.

+
+ +

If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder.

+ + โ† Return to Login +
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/login/password_reset_otp.html b/templates/login/password_reset_otp.html new file mode 100644 index 0000000..53e1117 --- /dev/null +++ b/templates/login/password_reset_otp.html @@ -0,0 +1,270 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} + + + + + + Verify OTP - Student Management System + + + +
+ +

Verify OTP

+ + {% if is_email %} +

+ Code sent to: {{ email|stringformat:"s"|slice:":3" }}***@{{ email|stringformat:"s"|slice:"-10:" }} + Edit Email +

+ {% else %} +

+ Code sent to: +91 ******{{ phone|stringformat:"s"|slice:"-4:" }} + Edit Number +

+ {% endif %} + + {% if error %} +
+ {{ error }} +
+ {% endif %} + + {% if messages %} +
+ {% for message in messages %} + {{ message }} + {% endfor %} +
+ {% endif %} + + {% if is_email %} +
+ {% else %} + + {% endif %} + {% csrf_token %} +
+ + + {% if is_email %} + + {% else %} + + {% endif %} +
+ + +
+ +
+ Resend code in 60s + {% if is_email %} +
+ {% else %} + + {% endif %} + {% csrf_token %} + {% if is_email %} + + {% else %} + + {% endif %} + +
+
+ + +
+ + + + +{% endblock %} \ No newline at end of file diff --git a/templates/login/password_reset_phone.html b/templates/login/password_reset_phone.html new file mode 100644 index 0000000..4b02282 --- /dev/null +++ b/templates/login/password_reset_phone.html @@ -0,0 +1,197 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} + + + + + + Reset Password - Student Management System + + + +
+ +

Reset Password

+ + {% if is_email %} +

Enter your email address and we'll send you a verification code to reset your password.

+ {% else %} +

Enter your phone number and we'll send you a verification code to reset your password.

+ {% endif %} + + {% if error %} +
+ {{ error }} +
+ {% endif %} + + {% if success %} +
+ {{ success }} +
+ {% endif %} + + {% if messages %} +
+ {% for message in messages %} + {{ message }} + {% endfor %} +
+ {% endif %} + +
+ {% csrf_token %} +
+ {% if is_email %} + + + {% else %} + + + {% endif %} +
+ + +
+ + โ† Back to Reset Options +
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/login/password_reset_set.html b/templates/login/password_reset_set.html new file mode 100644 index 0000000..6fd33e3 --- /dev/null +++ b/templates/login/password_reset_set.html @@ -0,0 +1,160 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} + + + + + + Set New Password - Student Management System + + + +
+ +

Set New Password

+ +

Please enter your new password below.

+ + {% if error %} +
+ {{ error }} +
+ {% endif %} + + {% if messages %} +
+ {% for message in messages %} + {{ message }} + {% endfor %} +
+ {% endif %} + +
+ {% csrf_token %} +
+ + +
+ +
+ + +
+ + {% if is_email %} + + {% else %} + + {% endif %} + + + +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/login/password_reset_verify.html b/templates/login/password_reset_verify.html new file mode 100644 index 0000000..a5b9c1f --- /dev/null +++ b/templates/login/password_reset_verify.html @@ -0,0 +1,219 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} + + + + + + Verify OTP - Student Management System + + + +
+ +

Verify OTP

+

OTP sent to: {{ phone }}

+ + {% if error %} +
+ {{ error }} +
+ {% endif %} + + {% if messages %} +
+ {% for message in messages %} + {{ message }} + {% endfor %} +
+ {% endif %} + +
+ {% csrf_token %} +
+ + + +
+ + + +
+ Resend OTP in 60s + + {% csrf_token %} + + + +
+ + + +
+ + + + +{% endblock %} \ No newline at end of file diff --git a/templates/login/reset_options.html b/templates/login/reset_options.html new file mode 100644 index 0000000..f365771 --- /dev/null +++ b/templates/login/reset_options.html @@ -0,0 +1,156 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} + + + + + + Reset Password - Student Management System + + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/login/verify_reset_code.html b/templates/login/verify_reset_code.html new file mode 100644 index 0000000..a2fa42c --- /dev/null +++ b/templates/login/verify_reset_code.html @@ -0,0 +1,61 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+
+
+

Reset Password

+
+
+ {% if message %} +
{{ message }}
+ {% endif %} + + {% if error %} +
{{ error }}
+ {% endif %} + +
+ {% csrf_token %} +
+ + +
+ +
+ + + + Enter the 6-digit code sent to your email. + +
+ +
+ + + + Your password must contain at least 8 characters. + +
+ +
+ + +
+ +
+ +
+
+ +
+

Didn't receive the code? Request a new one

+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/student_dashboard.html b/templates/student_dashboard.html index 22c7a1c..676ddb0 100644 --- a/templates/student_dashboard.html +++ b/templates/student_dashboard.html @@ -1,217 +1,1097 @@ -{% extends 'admin/base_site.html' %} +{% extends "base.html" %} {% load static %} +{% load tz %} +{% load app_filters %} + +{% block extrastyle %} +{{ block.super }} + + + + + + + + + + +{% endblock %} + +{% block extrahead %} +{{ block.super }} + + + + + + +{% endblock %} {% block content %} +
- + +
+
+
+
+ {% if student.image %} + {{ student.name }} + {% else %} + Default User + {% endif %} +
+

Welcome, {{ student.name }}!

+

Student ID: {{ student.id }}

+
+
+
+
+
+ + +
+
-
-
-
- {% if student.image %} - {{ student.name }} + +
+
+
+
Attendance Summary
+
+
+
+
+ Total Classes +

{{ attendance_summary.total_classes }}

+
+
+ Attendance Rate +

{{ attendance_summary.attendance_percentage|floatformat:1 }}%

+
+
+
+ Current: {{ attendance_summary.current_streak }} days + Best: {{ attendance_summary.longest_streak }} days +
+
+
+
+ + +
+
+
+
Course Progress
+
+
+
+
+ Total Courses +

{{ course_summary.total_courses }}

+
+
+ Completion Rate +

{{ course_summary.completion_percentage|floatformat:1 }}%

+
+
+
+
+ Completed + {{ course_summary.completed_courses }} +
+
+ Ongoing + {{ course_summary.ongoing_courses }} +
+
+
+
+
+ + +
+
+
+
Recent Activity
+
+
+
+ {% for leave in recent_leaves %} +
+ +
+ Leave Request ({{ leave.date|date:"M d" }}) + {{ leave.get_status_display }} + {{ leave.created_at|timesince }} ago +
+
+ {% endfor %} + + {% for feedback in recent_feedback %} +
+ +
+ Gave feedback {% if feedback.teacher %}to {{ feedback.teacher.name }}{% endif %} +
+ {% for i in "12345"|make_list %} + {% if forloop.counter <= feedback.rating %} + + {% else %} + {% endif %} -
-

Welcome, {{ student.name }}!

-

Student ID: {{ student.id }}

-
+ {% endfor %} + {{ feedback.created_at|timesince }} ago +
-
-
-
+
+ {% endfor %} - + {% for notice in notices %} +
+ +
+ {{ notice.title }} + {{ notice.created_at|timesince }} ago +
+
+ {% endfor %} + + {% for record in attendance_records %} +
+ +
+ {% if record.student_attend %}Attended{% else %}Missed{% endif %} {{ record.attendance.routine.subject.name|truncatechars:15 }} + {{ record.attendance.date|date:"M d" }} +
+
+ {% endfor %} + + {% if not recent_leaves and not recent_feedback and not notices and not attendance_records %} +
+ No recent activity +
+ {% endif %} +
+
+
+
+ + +
+
+
+
Leave Status
+
+
+
+ Pending + {{ pending_leaves }} +
+
+ Approved + {{ approved_leaves }} +
+
+ Rejected + {{ rejected_leaves }} +
+ +
+
+
+
+ +
- -
-
-
-
-
-
Course Progress
-
{{ course_summary.completion_percentage|floatformat:1 }}%
-
-
-
-
-
-
- Completed: {{ course_summary.completed_courses }}/{{ course_summary.total_courses }} - Ongoing: {{ course_summary.ongoing_courses }} -
-
-
- -
-
-
-
-
+
+
+

+ Course Details +

+ + {% if course_details %} + {% for course in course_details %} +
+
+
{{ course.name }}
+ + {{ course.progress_status }} + +
- -
-
-
-
-
-
Attendance Rate
-
{{ attendance_summary.attendance_percentage|floatformat:1 }}%
-
-
-
-
-
-
- Present: {{ attendance_summary.classes_attended }}/{{ attendance_summary.total_classes }} -
-
-
- -
-
+ +
+
+ {{ course.completion_percentage }}%
-
-
+
- -
-
-
-
-
-
Attendance Streaks
-
- Current: {{ attendance_summary.current_streak }} days -
-
- Best Streak: {{ attendance_summary.longest_streak }} days -
-
-
- -
-
+ +
+
+ + + + + + + + + + + + + + + + + + + {% if course.actual_end_date %} + + + + + {% endif %} + +
Start Date{{ course.start_date|date:"M d, Y" }}
Duration Type{{ course.duration_type }}
Total Duration + {% if course.duration_type == "Semester" %} + {{ course.duration }} years ({{ course.duration|multiply:2 }} semesters) + {% else %} + {{ course.duration }} years + {% endif %} +
Expected End Date{{ course.expected_end_date|date:"M d, Y" }}
Actual End Date{{ course.actual_end_date|date:"M d, Y" }}
+
+ + + + + + + + + + + + + + + {% if course.remaining_days is not None %} + + + + + {% endif %} + {% if course.total_remaining_days is not None %} + + + + + {% endif %} + +
Current Semester{{ course.current_semester }}
Semester Start{{ course.semester_start_date|date:"M d, Y" }}
Semester End{{ course.semester_end_date|date:"M d, Y" }}
Semester Days Remaining{{ course.remaining_days }} days
Total Course Days Remaining{{ course.total_remaining_days }} days
+
+
+ {% endfor %} + {% else %} +
+ No courses assigned yet. +
+ {% endif %}
+
+
- -
- -
-
-
-
Course Details
-
+
+
+

+ Current Semester Subjects +

+ {% if current_subjects %} +
+ {% for subject in current_subjects %} +
+
- {% for course_name, details in course_summary.details.items %} -
-
{{ course_name }}
-
-
-

Start Date: {{ details.start_date|default:'-' }}

-

End Date: {{ details.end_date|default:'-' }}

-

Status: {{ details.progress_status }}

-
-
-

Progress:

-
-
{{ details.completion_percentage }}%
-
- {% if details.book %} - - {% endif %} -
-
-
-
- {% empty %} -

No courses enrolled yet.

- {% endfor %} +
{{ subject.name }}
+

+ Semester: {{ subject.semester_or_year }} +

+
+ +
+
+ {% endfor %}
- - -
- -
-
-
Recent Leave Requests
-
-
- {% for leave in recent_leaves %} -
-
- {{ leave.date }} - - {{ leave.get_status_display }} - -
-

{{ leave.message|truncatechars:100 }}

-
-
- {% empty %} -

No recent leave requests.

- {% endfor %} -
-
- - -
-
-
Recent Feedback
-
-
- {% for feedback in recent_feedback %} - - {% empty %} -

No recent feedback.

- {% endfor %} -
-
+ {% else %} +
+ No subjects assigned for the current semester.
+ {% endif %}
+
+ +
+
+

+ Class Routine +

+
+ + + + + + + + + + {% if current_semester_routines %} + {% for routine in current_semester_routines %} + + + + + + {% endfor %} + {% elif has_routines %} + + + + {% else %} + + + + {% endif %} + +
TimeSubjectTeacher
{{ routine.start_time|time:"H:i" }} - {{ routine.end_time|time:"H:i" }}{{ routine.subject.name }}{{ routine.teacher.name }}
No routines for current semester (Semester {{ student.current_semester }}).
No routines assigned yet.
+
+ + +
+

+ Attendance Record +

+
+ + + + + + + + + + + {% for record in attendance_records %} + + + + + + + {% empty %} + + + + {% endfor %} + +
DateSubjectTeacherStatus
{{ record.attendance.date|date:"M d, Y" }}{{ record.attendance.routine.name }}{{ record.attendance.teacher.name }} + + {% if record.student_attend %}Present{% else %}Absent{% endif %} + +
No attendance records found.
+
+
+
+
+ +
+
+

+ Leave Management +

+
+
+ +
+
+
+ + + + + + + + + + + {% for leave in student_leaves %} + + + + + + + {% empty %} + + + + {% endfor %} + +
DateMessageStatusCreated At
{{ leave.date|date:"M d, Y" }}{{ leave.message }} + + {{ leave.get_status_display }} + + {{ leave.created_at|date:"M d, Y H:i" }}
No leave requests found.
+
+
+
+ +
+
+

+ Feedback +

+
+ +
+ +
+
+

Teacher Feedback

+ {% for feedback in student.feedbacks.all %} +
+
+
+
{{ feedback.teacher.name }}
+
+ {% for i in "12345"|make_list %} + {% with i_float=forloop.counter|floatformat:0|add:"0" %} + {% if feedback.rating >= i_float %} + + {% elif feedback.rating >= i_float|add:"-0.5" %} + + {% else %} + + {% endif %} + {% endwith %} + {% endfor %} +
+
+ +
+ {{ feedback.created_at|timesince }} ago + {{ feedback.rating }}/5 +
+
+
+ {% empty %} +
+ No feedback given to teachers yet. +
+ {% endfor %} +
+
+

Institute Feedback

+ {% for feedback in student.institute_feedbacks.all %} +
+
+
+
{{ feedback.feedback_type }}
+
+ {% for i in "12345"|make_list %} + {% with i_float=forloop.counter|floatformat:0|add:"0" %} + {% if feedback.rating >= i_float %} + + {% elif feedback.rating >= i_float|add:"-0.5" %} + + {% else %} + + {% endif %} + {% endwith %} + {% endfor %} +
+
+ +
+ {{ feedback.created_at|timesince }} ago + {{ feedback.rating }}/5 +
+
+
+ {% empty %} +
+ No institute feedback given yet. +
+ {% endfor %} +
+
+
+
+ +
+
+

+ Notices +

+
+ {% for notice in notices %} +
+
+ {% if notice.image %} + {{ notice.title }} + {% endif %} +
+
{{ notice.title }}
+

{{ notice.message }}

+
+ {{ notice.created_at|date:"M d, Y" }} + {% if notice.file %} + + Download + + {% endif %} +
+
+
+
+ {% empty %} +
+

No notices available.

+
+ {% endfor %} +
+
+
+ +
+
+

+ Profile Information +

+
+ +
+
+
+ {% if student.image %} + {{ student.name }} + {% else %} + Default User + {% endif %} +
+

{{ student.name }}

+

+ {{ student.id }} + {{ student.email }} + {{ student.phone }} +

+
+ + {{ student.status }} + + {% if student.course %} + {{ student.course.name }} + {% endif %} + Semester {{ student.current_semester }} +
+
+
+
+ + +
+
+
+ + +
+
+
+
Personal Information
+
+
+
+
+ Full Name +

{{ student.name }}

+
+
+ Gender +

{{ student.gender }}

+
+
+ Birth Date +

{{ student.birth_date|date:"M d, Y" }}

+
+
+ Marital Status +

{{ student.marital_status }}

+
+
+ Citizenship No. +

{{ student.citizenship_no|default:"Not provided" }}

+
+
+ Joining Date +

{{ student.joining_date|date:"M d, Y" }}

+
+
+
+
+
+ + +
+
+
+
Contact Information
+
+
+
+
+ Email Address +

{{ student.email }}

+
+
+ Phone Number +

{{ student.phone }}

+
+
+ Parent Name +

{{ student.parent_name|default:"Not provided" }}

+
+
+ Parent Phone +

{{ student.parent_phone|default:"Not provided" }}

+
+
+ Temporary Address +

{{ student.temporary_address|default:"Not provided" }}

+
+
+ Permanent Address +

{{ student.permanent_address|default:"Not provided" }}

+
+
+
+
+
+ + +
+
+
+
Academic Information
+
+
+
+
+ Course +

{{ student.course.name|default:"Not assigned" }}

+
+
+ Current Semester +

{{ student.current_semester }}

+
+
+ Joining Date +

{{ student.joining_date|date:"M d, Y" }}

+
+
+ Batches +

+ {% for batch in student.batches.all %} + {{ batch.name }} + {% empty %} + No batches assigned + {% endfor %} +

+
+
+
+
+
+
+
+
+ + + + + +
- - -{% endblock %} \ No newline at end of file + + // Run on load and resize + adjustLayout(); + window.addEventListener('resize', adjustLayout); + }); + + + + + + +
+ +
+{% endblock %} diff --git a/templates/teacher_attendance.html b/templates/teacher_attendance.html new file mode 100644 index 0000000..848b204 --- /dev/null +++ b/templates/teacher_attendance.html @@ -0,0 +1,254 @@ +{% extends 'base.html' %} +{% load static %} +{% load app_filters %} +{% block extrastyle %} + + + + + +{% endblock %} +{% block content %} +
+ +
+

Manage Attendance

+ + Back to Dashboard + +
+
+ +
+
+
Your Routines
+ {% if routines %} + + {% else %} +
+ You don't have any active routines. +
+ {% endif %} +
+
+ +
+
+ {% if selected_routine %} +
+
+
{{ selected_routine.subject.name }}
+

{{ selected_routine.course.name }} - Semester {{ selected_routine.semester_or_year }}

+
+
+
+ + + +
+
+
+
+ +
+ {% csrf_token %} + + +
+
+ + +
+
+
+ + +
+ +
Student Attendance
+ {% if students %} +
+ + + + + + + + + + + {% for student in students %} + + + + + + + {% endfor %} + +
#NameRoll/IDPresent
{{ forloop.counter }}{{ student.name }}{{ student.roll_no|default:student.id }} +
+ +
+
+
+
+
+ + +
+ +
+ {% else %} +
+ No students found for this course and semester. +
+ {% endif %} +
+ {% else %} +
+ +
Select a routine to manage attendance
+

Choose a class from the list on the left to take attendance.

+
+ {% endif %} +
+
+
+
+{% endblock %} +{% block extrahead %} + + +{% endblock %} diff --git a/templates/teacher_dashboard.html b/templates/teacher_dashboard.html index e449724..b23a32f 100644 --- a/templates/teacher_dashboard.html +++ b/templates/teacher_dashboard.html @@ -1,250 +1,1386 @@ -{% extends 'admin/base_site.html' %} +{% extends 'base.html' %} {% load static %} +{% load tz %} +{% load app_filters %} + +{% block extrastyle %} + + + + + + + + + +{% endblock %} {% block content %}
- -
-
-
-
- {% if teacher.image %} - {{ teacher.name }} - {% endif %} -
-

Welcome, {{ teacher.name }}!

-

{{ teacher.designation }}

-
-
-
+ +
+
+
+
+ {% if teacher.image %} + {{ teacher.name }} + {% else %} + Default User + {% endif %} +
+

Welcome, {{ teacher.name }}!

+

{{ teacher.designation }} ID: {{ teacher.id }}

+
+
+
+
+ + + + + +
+ +
+
+
+
+
Students
+
+
+
+

{{ total_students }}

+

Total Students

+
+
+
+
+

{{ present_students }}

+

Present Today

+
+
+

{{ absent_students }}

+

Absent Today

+
+
+
+
+
+
+ +
+
+
+
Today's Classes
+
+
+
+

{{ today_classes }}

+

Total Classes

+
+
+ {% if today_routines %} +
+ +
+ {% else %} +

No classes scheduled for today

+ {% endif %} +
+
+
+
+ +
+
+
+
Feedback Rating
+
+
+

{{ avg_rating|floatformat:1 }}/5.0

+
+ {% for i in "12345"|make_list %} + {% if forloop.counter <= avg_rating_rounded %} + + {% else %} + + {% endif %} + {% endfor %} +
+

Based on {{ recent_feedback.count }} student feedbacks

+
+
+
+ +
+
+
+
Leave Status
+
+
+
+

Pending

+ {{ pending_leaves }} +
+
+

Approved

+ {{ approved_leaves }} +
+
+

Rejected

+ {{ rejected_leaves }} +
+ +
+
+
- +
- -
-
-
-
-
-
Today's Classes
-
{{ today_classes }}
-
- Completed: {{ completed_classes }} -
-
-
- -
-
-
+
+
+
Today's Classes
+ + {% if today_routines %} + +
+ + + + + + + + + + + + + + {% for routine in today_routines %} + + + + + + + + + + {% endfor %} + +
SubjectCourseSemester/YearTimeAttendanceStatusAction
{{ routine.name }}{{ routine.course_name }}{{ routine.semester_or_year }}{{ routine.start_time|time:"h:i A" }} - {{ routine.end_time|time:"h:i A" }}{{ routine.attendance_count }}/{{ routine.total_students }} + {% if routine.is_completed %} + Completed + {% elif routine.is_ongoing %} + In Progress + {% else %} + Upcoming + {% endif %} + + {% if routine.has_attendance %} + + {% else %} + + {% endif %} +
-
- - -
-
-
-
-
-
Total Students
-
{{ total_students }}
-
- Present Today: {{ present_students }} -
-
-
- -
-
-
-
-
- - -
-
-
-
-
-
Average Rating
-
- {{ avg_rating|floatformat:1 }}/5.0 -
-
-
- {% with ''|center:avg_rating_rounded as range %} - {% for _ in range %} - - {% endfor %} - {% endwith %} - {% with ''|center:remaining_stars as range %} - {% for _ in range %} - - {% endfor %} - {% endwith %} -
-
-
-
- -
-
-
-
-
- - -
-
-
-
-
-
Leave Requests
-
{{ pending_leaves }}
-
- Approved: {{ approved_leaves }} | - Rejected: {{ rejected_leaves }} -
-
-
- -
-
-
+ {% else %} +
+ No classes scheduled for today.
+ {% endif %}
+
- +
- -
-
-
-
Today's Schedule
-
-
- {% if today_routines %} -
- - - - - - - - - - - {% for routine in today_routines %} - - - - - - - {% endfor %} - -
TimeCourseStatusAttendance
{{ routine.start_time|time:"g:i A" }} - {{ routine.end_time|time:"g:i A" }}{{ routine.name }} - {% if routine.is_completed %} - Completed - {% elif routine.is_ongoing %} - Ongoing - {% else %} - Upcoming - {% endif %} - {{ routine.attendance_count }}/{{ routine.total_students }}
-
- {% else %} -

No classes scheduled for today.

- {% endif %} -
-
-
- - -
- -
-
-
Recent Leave Requests
-
-
- {% for leave in recent_leaves %} -
-
- {{ leave.date }} - - {{ leave.get_status_display }} - -
-

{{ leave.message|truncatechars:100 }}

-
-
- {% empty %} -

No recent leave requests.

+ +
+
+
Recent Feedback
+ {% if recent_feedback %} +
+ {% for feedback in recent_feedback %} +
+
+
{{ feedback.student.name }}
+ {{ feedback.created_at|timesince }} ago +
+
+ {% for i in "12345"|make_list %} + {% if forloop.counter <= feedback.rating %} + + {% else %} + + {% endif %} {% endfor %} +
+

{{ feedback.feedback_text }}

+ {% endfor %}
- - -
-
-
Recent Feedback
-
-
- {% for feedback in recent_feedback %} - - {% empty %} -

No recent feedback.

- {% endfor %} -
+ {% else %} +
+ No recent feedback.
+ {% endif %}
+
+ + +
+
+
Recent Notices
+ {% if notices %} +
+ {% for notice in notices %} +
+
+
{{ notice.title }}
+ {{ notice.created_at|timesince }} ago +
+

{{ notice.message }}

+
+ {% endfor %} +
+ {% else %} +
+ No recent notices. +
+ {% endif %} +
+
+
+ + +
+
+
Your Class Schedule
+ + {% if all_routines %} +
+
+ {% for routine in all_routines %} +
+
+
{{ routine.subject.name }}
+ {{ routine.start_time|time:"h:i A" }} - {{ routine.end_time|time:"h:i A" }} +
+

{{ routine.course.name }} - Semester {{ routine.semester_or_year }}

+
+ {% endfor %} +
+
+ {% else %} +
+ No class schedules found. +
+ {% endif %} +
+
+ + +
+
+ +
+
+
Attendance Management
+ +
+ + +
+ {% if recent_attendance %} +
+ + + + + + + + + + + + + {% for attendance in recent_attendance %} + + + + + + + + + {% endfor %} + +
DateSubjectTimeStatusPresent/TotalAction
{{ attendance.date|date:"M d, Y" }}{{ attendance.routine.subject.name }}{{ attendance.routine.start_time|time:"h:i A" }} - {{ attendance.routine.end_time|time:"h:i A" }} + + {{ attendance.class_status }} + + + {{ attendance.present_count|default:"0" }}/{{ attendance.total_count|default:"0" }} + + +
+
+ {% else %} +
+ No recent attendance records found. +
+ {% endif %} +
+
+ + + +
+
+ + +
+
+
Notices & Announcements
+ + {% if notices %} +
+ {% for notice in notices %} +
+
+
{{ notice.title }}
+ {{ notice.created_at|date:"M d, Y" }} +
+

{{ notice.message }}

+ Posted {{ notice.created_at|timesince }} ago +
+ {% endfor %} +
+ {% else %} +
+ No notices available. +
+ {% endif %} +
+
+ + +
+
+ +
+
+
Give Feedback to Institute
+ +
+
+ + +
+
+
Your Recent Feedback
+ + {% if institute_feedback %} +
+ {% for feedback in institute_feedback %} +
+
+
{{ feedback.get_feedback_type_display }}
+ {{ feedback.created_at|timesince }} ago +
+
+ {% for i in "12345"|make_list %} + {% if forloop.counter <= feedback.rating %} + + {% else %} + + {% endif %} + {% endfor %} +
+

{{ feedback.feedback_text }}

+ + {% if feedback.is_anonymous %} + Anonymous + {% else %} + Public + {% endif %} + +
+ {% endfor %} +
+ {% else %} +
+ You haven't submitted any feedback yet. +
+ {% endif %} +
+
+
+
+ + +
+
+

+ Profile Information +

+
+ +
+
+
+ {% if teacher.image %} + {{ teacher.name }} + {% else %} + Default User + {% endif %} +
+

{{ teacher.name }}

+

+ {{ teacher.id }} + {{ teacher.email }} + {{ teacher.phone }} +

+
+ + {{ teacher.is_active|yesno:"Active,Inactive" }} + + {{ teacher.designation }} +
+
+
+
+ + +
+
+
+ + +
+
+
+
Personal Information
+
+
+
+
+ Full Name +

{{ teacher.name }}

+
+
+ Gender +

{{ teacher.gender|default:"Not provided" }}

+
+
+ Birth Date +

{{ teacher.birth_date|date:"M d, Y"|default:"Not provided" }}

+
+
+ Marital Status +

{{ teacher.marital_status|default:"Not provided" }}

+
+
+ Citizenship No. +

{{ teacher.citizenship_no|default:"Not provided" }}

+
+
+ Passport +

{{ teacher.passport|default:"Not provided" }}

+
+
+
+
+
+ + +
+
+
+
Contact Information
+
+
+
+
+ Email Address +

{{ teacher.email|default:"Not provided" }}

+
+
+ Phone Number +

{{ teacher.phone|default:"Not provided" }}

+
+
+ Parent Name +

{{ teacher.parent_name|default:"Not provided" }}

+
+
+ Parent Phone +

{{ teacher.parent_phone|default:"Not provided" }}

+
+
+ Temporary Address +

{{ teacher.temporary_address|default:"Not provided" }}

+
+
+ Permanent Address +

{{ teacher.permanent_address|default:"Not provided" }}

+
+
+
+
+
+ + +
+
+
+
Employment Information
+
+
+
+
+ Employee ID +

{{ teacher.id }}

+
+
+ Designation +

{{ teacher.designation|default:"Not provided" }}

+
+
+ Joining Date +

{{ teacher.joining_date|date:"M d, Y"|default:"Not provided" }}

+
+
+ Status +

+ + {{ teacher.is_active|yesno:"Active,Inactive" }} + +

+
+
+ Permissions +

+ {% for group in teacher.groups.all %} + {{ group.name }} + {% empty %} + No specific permissions + {% endfor %} +

+
+
+
+
+
+ + +
+
+
+
Qualifications
+
+
+
+
+ Educational Background + {% if teacher.education_set.all %} + {% for education in teacher.education_set.all %} +
+

{{ education.degree }}, {{ education.institution }} ({{ education.year }})

+ {{ education.specialization }} +
+ {% endfor %} + {% else %} +

No educational qualifications recorded

+ {% endif %} + + {% if teacher.qualification %} +
+ Additional Qualifications +

{{ teacher.qualification }}

+
+ {% endif %} +
+ + {% if teacher.skills %} +
+ Skills & Specialization +

{{ teacher.specialization|default:"No specialization information available" }}

+ +
+ {% for skill in teacher.skills %} + {{ skill.strip }} + {% endfor %} +
+
+ {% endif %} +
+
+
+
+
+
+
+ + +
+
+

+ Leave Management +

+
+
+ +
+
+ +
+
+
+
+ +

{{ pending_leaves }}

+

Pending Requests

+
+
+
+
+
+
+ +

{{ approved_leaves }}

+

Approved Requests

+
+
+
+
+
+
+ +

{{ rejected_leaves }}

+

Rejected Requests

+
+
+
+
+ +
+
Leave History
+
+ + + + + + + + + + + {% if recent_leaves %} + {% for leave in recent_leaves %} + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
DateReasonStatusApplied On
{{ leave.date|date:"M d, Y" }}{{ leave.message }} + + {{ leave.get_status_display }} + + {{ leave.created_at|date:"M d, Y" }}
No leave history available
+
+
+
+
- - -{% endblock %} \ No newline at end of file + }); + +{% endblock %}