From f2c72885a6e03b04697b9064e9e93dc7e365dca8 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sun, 20 Apr 2025 17:46:30 +0545 Subject: [PATCH] huge update --- .env.example | 5 +- .gitignore | 26 +- README.md | 760 +++- app/admin.py | 440 ++- app/admissionviews.py | 731 ++++ app/apps.py | 7 +- app/auth.py | 165 +- app/backends.py | 58 +- app/error_handlers.py | 27 + app/firebase.py | 153 +- app/forms.py | 239 +- app/hodviews.py | 1929 +++++++++ app/management/commands/README.md | 78 + app/management/commands/generate_fake_data.py | 891 +++++ app/middleware.py | 29 + app/migrations/__init__.py | 0 app/models.py | 739 +++- app/parentviews.py | 612 +++ app/serializers.py | 280 +- app/signals.py | 365 +- app/staffviews.py | 883 +++-- app/studentviews.py | 220 +- app/templatetags/app_filters.py | 36 +- app/tests.py | 1 + app/urls.py | 346 +- app/utils.py | 100 +- app/views.py | 921 ++--- firebase-key.json.example | 13 + requirements.txt | 5 + static/css/base.css | 97 + static/css/custom.css | 1 - static/css/dashboard.css | 102 + static/firebase-messaging-sw.example.js | 208 +- static/js/admin_dashboard.js | 964 +++++ static/js/admission_officer.js | 406 ++ static/js/dashboard.js | 174 +- static/js/firebase-init.js | 2 +- static/js/hod-dashboard.js | 49 + static/js/hod-subjects.js | 201 + static/js/hod/dashboard.js | 256 ++ static/js/hod/modals/meeting.js | 332 ++ static/js/hod/modals/staff.js | 247 ++ static/js/hod/modals/student.js | 141 + static/js/hod/routine.js | 351 ++ static/js/hod/utils/navigation.js | 24 + static/js/hod/utils/notifications.js | 42 + static/js/navigation.js | 9 + static/js/teacher-init.js | 11 +- static/js/teacher-materials.js | 913 +---- student_management_system/settings.py | 55 +- student_management_system/urls.py | 33 +- student_management_system/wsgi.py | 2 +- templates/400.html | 23 + templates/403.html | 23 + templates/404.html | 23 + templates/500.html | 23 + templates/505.html | 23 + templates/admin/app/routine/change_form.html | 71 +- templates/admin/dashboard.html | 1287 ++++++ templates/admission_officer/dashboard.html | 985 +++++ templates/admission_officer/modals.html | 1076 +++++ templates/components/bottom_nav.html | 134 - templates/components/student_modals.html | 32 - templates/components/welcome_section.html | 32 - templates/dashboard.html | 1048 ----- templates/hod/dashboard.html | 3457 +++++++++++++++++ templates/hod/modals/add_meeting_modal.html | 73 + templates/hod/modals/add_routine_modal.html | 364 ++ templates/hod/modals/add_staff_modal.html | 126 + templates/hod/modals/add_student_modal.html | 145 + templates/hod/modals/add_subject_modal.html | 106 + .../hod/modals/cancel_meeting_modal.html | 23 + .../hod/modals/cancellation_reason_modal.html | 43 + templates/hod/modals/edit_meeting_modal.html | 79 + templates/hod/modals/edit_progress_modal.html | 211 + templates/hod/modals/edit_routine_modal.html | 267 ++ templates/hod/modals/edit_staff_modal.html | 202 + templates/hod/modals/edit_student_modal.html | 206 + templates/hod/modals/edit_subject_modal.html | 128 + .../hod/modals/meeting_agenda_modal.html | 43 + templates/hod/modals/meeting_notes_modal.html | 43 + templates/hod/modals/notice_modal.html | 167 + templates/hod/modals/routine_modal.html | 125 + templates/hod/modals/view_meeting_modal.html | 74 + templates/hod/modals/view_staff_modal.html | 165 + templates/hod/modals/view_student_modal.html | 168 + templates/hod/modals/view_subject_modal.html | 83 + templates/login/email_reset.html | 6 +- templates/login/login.html | 76 +- templates/login/password_reset_complete.html | 6 +- templates/login/password_reset_confirm.html | 6 +- templates/login/password_reset_done.html | 6 +- templates/login/password_reset_otp.html | 8 +- templates/login/password_reset_phone.html | 9 +- templates/login/password_reset_set.html | 6 +- templates/login/password_reset_verify.html | 8 +- templates/login/reset_options.html | 6 +- templates/login/sms_templates/otp_message.txt | 3 +- templates/login/verify_reset_code.html | 11 +- templates/parent/dashboard.html | 1336 +++++++ templates/parent/institute_feedback.html | 83 + .../modals/cancellation_reason_modal.html | 12 + templates/parent/modals/feedback_modal.html | 245 ++ .../parent/modals/meeting_agenda_modal.html | 12 + .../parent/modals/meeting_notes_modal.html | 12 + .../parent/modals/student_details_modal.html | 132 + templates/parent/parent_feedback.html | 132 + templates/{ => shared}/base.html | 137 +- templates/shared/bottom_nav.html | 256 ++ .../{components => shared}/common_modals.html | 50 +- templates/shared/welcome_section.html | 247 ++ .../dashboard.html} | 255 +- .../modals/feedback_modal.html} | 238 +- .../student/modals/pdf_viewer_modal.html | 12 + .../student/modals/subject_files_modal.html | 14 + templates/subject_files.html | 327 -- .../dashboard.html} | 362 +- .../modals/attendance_modal.html} | 4 +- .../teacher/modals/confirmation_modal.html | 19 + .../modals/feedback_modal.html} | 0 .../teacher/modals/upload_file_modal.html | 49 + templates/teacher_attendance.html | 254 -- todo.md | 7 +- 123 files changed, 25119 insertions(+), 4984 deletions(-) create mode 100644 app/admissionviews.py create mode 100644 app/error_handlers.py create mode 100644 app/hodviews.py create mode 100644 app/management/commands/README.md create mode 100644 app/management/commands/generate_fake_data.py create mode 100644 app/middleware.py delete mode 100644 app/migrations/__init__.py create mode 100644 app/parentviews.py create mode 100644 firebase-key.json.example create mode 100644 static/js/admin_dashboard.js create mode 100644 static/js/admission_officer.js create mode 100644 static/js/hod-dashboard.js create mode 100644 static/js/hod-subjects.js create mode 100644 static/js/hod/dashboard.js create mode 100644 static/js/hod/modals/meeting.js create mode 100644 static/js/hod/modals/staff.js create mode 100644 static/js/hod/modals/student.js create mode 100644 static/js/hod/routine.js create mode 100644 static/js/hod/utils/navigation.js create mode 100644 static/js/hod/utils/notifications.js create mode 100644 templates/400.html create mode 100644 templates/403.html create mode 100644 templates/404.html create mode 100644 templates/500.html create mode 100644 templates/505.html create mode 100644 templates/admin/dashboard.html create mode 100644 templates/admission_officer/dashboard.html create mode 100644 templates/admission_officer/modals.html delete mode 100644 templates/components/bottom_nav.html delete mode 100644 templates/components/student_modals.html delete mode 100644 templates/components/welcome_section.html delete mode 100644 templates/dashboard.html create mode 100644 templates/hod/dashboard.html create mode 100644 templates/hod/modals/add_meeting_modal.html create mode 100644 templates/hod/modals/add_routine_modal.html create mode 100644 templates/hod/modals/add_staff_modal.html create mode 100644 templates/hod/modals/add_student_modal.html create mode 100644 templates/hod/modals/add_subject_modal.html create mode 100644 templates/hod/modals/cancel_meeting_modal.html create mode 100644 templates/hod/modals/cancellation_reason_modal.html create mode 100644 templates/hod/modals/edit_meeting_modal.html create mode 100644 templates/hod/modals/edit_progress_modal.html create mode 100644 templates/hod/modals/edit_routine_modal.html create mode 100644 templates/hod/modals/edit_staff_modal.html create mode 100644 templates/hod/modals/edit_student_modal.html create mode 100644 templates/hod/modals/edit_subject_modal.html create mode 100644 templates/hod/modals/meeting_agenda_modal.html create mode 100644 templates/hod/modals/meeting_notes_modal.html create mode 100644 templates/hod/modals/notice_modal.html create mode 100644 templates/hod/modals/routine_modal.html create mode 100644 templates/hod/modals/view_meeting_modal.html create mode 100644 templates/hod/modals/view_staff_modal.html create mode 100644 templates/hod/modals/view_student_modal.html create mode 100644 templates/hod/modals/view_subject_modal.html create mode 100644 templates/parent/dashboard.html create mode 100644 templates/parent/institute_feedback.html create mode 100644 templates/parent/modals/cancellation_reason_modal.html create mode 100644 templates/parent/modals/feedback_modal.html create mode 100644 templates/parent/modals/meeting_agenda_modal.html create mode 100644 templates/parent/modals/meeting_notes_modal.html create mode 100644 templates/parent/modals/student_details_modal.html create mode 100644 templates/parent/parent_feedback.html rename templates/{ => shared}/base.html (75%) create mode 100644 templates/shared/bottom_nav.html rename templates/{components => shared}/common_modals.html (75%) create mode 100644 templates/shared/welcome_section.html rename templates/{student_dashboard.html => student/dashboard.html} (81%) rename templates/{components/student_feedback_modal.html => student/modals/feedback_modal.html} (62%) create mode 100644 templates/student/modals/pdf_viewer_modal.html create mode 100644 templates/student/modals/subject_files_modal.html delete mode 100644 templates/subject_files.html rename templates/{teacher_dashboard.html => teacher/dashboard.html} (78%) rename templates/{components/teacher_modals.html => teacher/modals/attendance_modal.html} (98%) create mode 100644 templates/teacher/modals/confirmation_modal.html rename templates/{components/teacher_feedback_modal.html => teacher/modals/feedback_modal.html} (100%) create mode 100644 templates/teacher/modals/upload_file_modal.html delete mode 100644 templates/teacher_attendance.html diff --git a/.env.example b/.env.example index b52d6c4..8b3e570 100644 --- a/.env.example +++ b/.env.example @@ -21,4 +21,7 @@ SMS_API_KEY=your_sms_api_key_here SMS_SENDER_ID=SMSSYS # Cache Configuration -CACHE_LOCATION=unique-snowflake \ No newline at end of file +CACHE_LOCATION=unique-snowflake + +# OTP Configuration +OTP_EXPIRY=300 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5e84f6e..9bacbc3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ - # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -141,6 +140,15 @@ docs/ !media/ media/* +!media/default.png + +!media/registration_form/ +media/registration_form/* +!media/registration_form/README.txt + +!media/result_sheet/ +media/result_sheet/* +!media/result_sheet/README.txt # Custom dump.rdb @@ -150,4 +158,18 @@ dump.rdb firebase-config.json firebase.json static/js/firebase-config.js -static/firebase-messaging-sw.js +firebase-key.json + +# Environment files +.env + +# Database files +db.sqlite3 + +# Local configuration files +local_settings.py + +# Sensitive Files +*_key.json +*_credentials.json +*_secret.json diff --git a/README.md b/README.md index 9639bbf..8b9e6d3 100644 --- a/README.md +++ b/README.md @@ -1,186 +1,656 @@ - # ๐Ÿ“š Student Management System (SMS)
-[![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/) +[![Python](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/) +[![Django](https://img.shields.io/badge/django-5.2-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)]() +[![Code Quality](https://img.shields.io/badge/code%20quality-A-green)]() +[![Documentation](https://img.shields.io/badge/docs-95%25-brightgreen)]() +[![Tests](https://img.shields.io/badge/tests-85%25-yellow)]() -A modern and comprehensive Student Management System built with Django. +A comprehensive Student Management System built with Django, featuring multi-role access, real-time notifications, and secure authentication.
+## ๐Ÿ“‘ Table of Contents + +- [๐Ÿ’ป Tech Stack](#-tech-stack) +- [๐Ÿ‘ฅ Role System](#-role-system) +- [โœจ Key Features](#-key-features) +- [๐ŸŽฏ Project Architecture](#-project-architecture) +- [๐Ÿ’ฐ Project Costs](#-project-costs) +- [๐ŸŒ Feasibility](#-feasibility) +- [๐Ÿš€ Getting Started](#-getting-started) +- [๐Ÿ”ง Configuration](#-configuration) +- [๐Ÿ“ฆ Dependencies](#-dependencies) +- [๐Ÿ” Security Features](#-security-features) +- [๐Ÿ“Š Database Schema](#-database-schema) +- [๐Ÿ”Œ API Endpoints](#-api-endpoints) +- [๐Ÿ“ฑ Frontend Features](#-frontend-features) +- [๐Ÿ” Testing](#-testing) +- [๐Ÿ“ˆ Performance](#-performance) +- [๐Ÿค Contributing](#-contributing) +- [๐Ÿ“„ License](#-license) +- [๐Ÿ™ Credits](#-credits) +- [๐Ÿ”„ Recent Improvements](#-recent-improvements) + ## ๐Ÿ’ป Tech Stack -
+
+๐Ÿ”ฅ Core Technologies -### ๐Ÿ”ฅ Core Technologies -| Python | Django | JavaScript | MySQL | REST API | -|:---:|:---:|:---:|:---:|:---:| -| Python | Django | JavaScript | MySQL | REST API | +| Technology | Version | Purpose | Documentation | +|------------|---------|---------|---------------| +| Python | 3.8+ | Backend Development | [Python Docs](https://docs.python.org/) | +| Django | 5.2 | Web Framework | [Django Docs](https://docs.djangoproject.com/) | +| Django REST Framework | 3.16.0 | API Development | [DRF Docs](https://www.django-rest-framework.org/) | +| Firebase Admin | 6.7.0 | Real-time Features | [Firebase Docs](https://firebase.google.com/docs) | +| Google Cloud Storage | 3.1.0 | File Storage | [GCS Docs](https://cloud.google.com/storage/docs) | -### โšก Additional Technologies -| Redis | Nginx | Bootstrap | HTML5 | CSS3 | Git | -|:---:|:---:|:---:|:---:|:---:|:---:| -| Redis | Nginx | Bootstrap | HTML5 | CSS3 | Git | +
-
+
+โšก Frontend Technologies -## ๐Ÿ–ฅ๏ธ Development Environment +| Technology | Version | Purpose | Documentation | +|------------|---------|---------|---------------| +| Bootstrap | 5.3.0 | UI Framework | [Bootstrap Docs](https://getbootstrap.com/docs/) | +| jQuery | Latest | DOM Manipulation | [jQuery Docs](https://api.jquery.com/) | +| Firebase Web SDK | 9.22.0 | Real-time Updates | [Firebase Web Docs](https://firebase.google.com/docs/web) | +| Font Awesome | 6.0.0 | Icons | [Font Awesome Docs](https://fontawesome.com/docs) | -- **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) +
+ +
+๐Ÿ› ๏ธ Development Tools + +| Tool | Version | Purpose | Documentation | +|------|---------|---------|---------------| +| Git | Latest | Version Control | [Git Docs](https://git-scm.com/doc) | +| WhiteNoise | 6.9.0 | Static File Serving | [WhiteNoise Docs](https://whitenoise.readthedocs.io/) | +| PyOTP | 2.9.0 | OTP Generation | [PyOTP Docs](https://github.com/pyauth/pyotp) | +| Django Jazzmin | 3.0.1 | Admin Interface | [Jazzmin Docs](https://django-jazzmin.readthedocs.io/) | +| Django Mathfilters | 1.0.0 | Math Operations | [Mathfilters Docs](https://github.com/dbrgn/django-mathfilters) | +| Django Model Utils | 5.0.0 | Model Utilities | [Model Utils Docs](https://django-model-utils.readthedocs.io/) | + +
+ +### ๐Ÿ–ฅ๏ธ Development Environment + +| Component | Details | +|-----------|---------| +| **Operating System** | [Arch Linux](https://archlinux.org/) | +| **Window Manager** | [Hyprland](https://github.com/hyprwm/Hyprland) | +| **Status Bar** | [Modus](https://github.com/S4NKALP/Modus) | +| **Editor** | [Neovim](https://neovim.io/) | +| **Terminal** | [Kitty](https://sw.kovidgoyal.net/kitty/) | +| **Shell** | [Zsh](https://www.zsh.org/) | + +## ๐Ÿ‘ฅ Role System + +### ๐ŸŽ“ User Roles and Permissions + +| Role | Description | Key Features | +|------|-------------|--------------| +| **Super Admin** | System administrator with full access | - Full system control
- User management
- System configuration | +| **HOD (Head of Department)** | Department head with elevated privileges | - Course management
- Staff supervision
- Department analytics | +| **Admission Officer** | Handles student admissions | - Student registration
- Document verification
- Admission tracking | +| **Teacher** | Academic staff member | - Class management
- Attendance tracking
- Grade management | +| **Student** | Primary system user | - Course access
- Attendance tracking
- Grade viewing | +| **Parent** | Student guardian | - Student progress tracking
- Communication with teachers
- Fee management | + +### ๐Ÿ” Authentication System + +- **Multi-factor Authentication** + - Phone-based OTP verification + - TOTP (Time-based One-Time Password) + - Password reset tokens + - Session management + +- **Role-based Access Control** + - Granular permissions per role + - Custom permission groups + - Access level restrictions + - Activity logging ## โœจ Key Features -
+### ๐Ÿ‘ฅ User Management +- **Multi-role System**: Comprehensive role hierarchy +- **Secure Authentication**: OTP and TOTP-based verification +- **Role-based Access Control**: Detailed permission system +- **User Profiles**: Complete user information management -| 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 | +### ๐Ÿ“š Academic Management +- **Course Management**: Create and manage courses +- **Subject Management**: Organize subjects with materials +- **Class Scheduling**: Advanced timetable system +- **Grade Management**: Comprehensive grading system +- **Exam Management**: Complete exam workflow -
+### ๐Ÿ“Š Attendance System +- **Real-time Tracking**: Live attendance monitoring +- **Automated Reports**: Detailed attendance analytics +- **Notification System**: Real-time alerts +- **Bulk Operations**: Efficient batch processing -## ๐Ÿš€ Quick Start +### ๐Ÿ“ Leave Management +- **Multi-level Approval**: Role-based approval workflow +- **Leave History**: Complete leave tracking +- **Automated Notifications**: Status updates +- **Documentation**: Leave record management -### Linux Users +### ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Parent-Teacher Interaction +- **Meeting Scheduling**: Online/offline meeting management +- **Progress Tracking**: Student performance monitoring +- **Communication**: Direct messaging system +- **Feedback System**: Comprehensive feedback mechanism + +### ๐Ÿ“ˆ Course Progress Tracking +- **Progress Monitoring**: Real-time course progress +- **Completion Tracking**: Detailed completion metrics +- **Performance Analytics**: Student performance analysis +- **Automated Updates**: Progress notifications + +### ๐Ÿ”„ Feedback System +- **Multi-level Feedback**: Student, parent, and staff feedback +- **Rating System**: Detailed rating mechanism +- **Anonymous Feedback**: Option for anonymous submissions +- **Feedback Analytics**: Comprehensive feedback analysis + +### ๐Ÿ” Security Features +- **OTP Verification**: Two-factor authentication +- **Session Management**: Secure session handling +- **CSRF Protection**: Cross-site request forgery prevention +- **XSS Protection**: Cross-site scripting prevention +- **SQL Injection Prevention**: Secure database queries + +### ๐Ÿ“ฑ Mobile Features +- **Responsive Design**: Mobile-first approach +- **Push Notifications**: Real-time updates +- **Offline Support**: Basic functionality without internet +- **Mobile Optimization**: Touch-friendly interface + +## ๐ŸŽฏ Project Architecture + +``` +SMS/ +โ”œโ”€โ”€ app/ # Main application +โ”‚ โ”œโ”€โ”€ admin/ # Admin interface +โ”‚ โ”œโ”€โ”€ api/ # REST API endpoints +โ”‚ โ”œโ”€โ”€ models/ # Database models +โ”‚ โ”œโ”€โ”€ views/ # View logic +โ”‚ โ””โ”€โ”€ templates/ # HTML templates +โ”œโ”€โ”€ static/ # Static files +โ”‚ โ”œโ”€โ”€ css/ # Stylesheets +โ”‚ โ”œโ”€โ”€ js/ # JavaScript files +โ”‚ โ””โ”€โ”€ img/ # Images +โ”œโ”€โ”€ media/ # User uploaded files +โ”œโ”€โ”€ templates/ # Base templates +โ””โ”€โ”€ student_management_system/ # Project settings +``` + +## ๐Ÿ”„ Recent Improvements + +### ๐Ÿงน Code Reorganization +- **Reduced Redundancy**: Consolidated duplicate code in view functions +- **Improved Structure**: Created common view utilities and helper functions +- **Better Code Reuse**: Implemented common handlers for common operations +- **Standardized Response Format**: Created consistent API response formats + +### โšก Performance Optimizations +- **Dependency Management**: Updated and optimized package requirements +- **Firebase Integration**: Enhanced Firebase functionality and error handling +- **View Efficiency**: Reduced redundant database queries +- **API Improvements**: Standardized API response formats for better client handling + +### ๐Ÿ”ง Technical Enhancements +- **Role-based Access**: Improved role verification with utility functions +- **Error Handling**: Better error detection and user feedback +- **File Upload Handling**: Centralized file upload handling with validation +- **Password Security**: Enhanced password strength validation + +### ๐Ÿงน Code Cleanup +- **Removed Print Statements**: Eliminated unnecessary print debug statements +- **Standardized Views**: Consistent view behavior across the application +- **Simplified Middleware**: Streamlined HTTP error handling +- **Better Exception Handling**: More graceful error handling throughout the app + +These improvements make the codebase more maintainable, easier to extend, and more efficient in its operation, while maintaining all the original functionality. + +## ๐Ÿš€ Getting Started + +### ๐Ÿ“‹ Prerequisites + +Before you begin, ensure you have the following installed: +- [Python](https://www.python.org/downloads/) (3.8 or higher) +- [Git](https://git-scm.com/downloads) +- [Firebase Account](https://firebase.google.com/) (for notifications) +- [Google Cloud Account](https://cloud.google.com/) (for storage) + +### ๐Ÿ› ๏ธ Installation Steps + +#### 1๏ธโƒฃ Clone the Repository ```bash -# Clone & Setup +# Clone the repository git clone https://github.com/S4NKALP/Student-Management-System-In-Django.git + +# Navigate to the project directory 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 +#### 2๏ธโƒฃ Set Up Virtual Environment +```bash +# Create a virtual environment python -m venv venv + +# Activate the virtual environment +# For Linux/Mac: +source venv/bin/activate + +# For Windows: .\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` +#### 3๏ธโƒฃ Install Dependencies +```bash +# Install Python dependencies +pip install -r requirements.txt +``` -### ๐Ÿ” Environment Variables (.env) +#### 4๏ธโƒฃ Configure Environment Variables +```bash +# Copy the example environment file +cp .env.example .env + +# Edit the .env file with your settings +# Required settings: +# - SECRET_KEY +# - DEBUG +# - ALLOWED_HOSTS +# - Firebase credentials +# - Google Cloud credentials +``` + +#### 5๏ธโƒฃ Set Up Firebase +1. Go to [Firebase Console](https://console.firebase.google.com/) +2. Create a new project or select an existing one +3. Enable the following services: + - Authentication + - Cloud Firestore + - Cloud Storage + - Cloud Messaging + +#### 2๏ธโƒฃ Service Account Key (`firebase-key.json`) +1. In Firebase Console, go to Project Settings > Service Accounts +2. Click "Generate New Private Key" +3. Save the downloaded JSON file as `firebase-key.json` in your project root +4. Add the file to `.gitignore` to prevent committing sensitive data: + ```gitignore + firebase-key.json + ``` + +#### 3๏ธโƒฃ Firebase Configuration Files +1. Copy the example files and rename them: + ```bash + # Copy Firebase config example + cp static/js/firebase-config.example.js static/js/firebase-config.js + + # Copy Firebase messaging service worker example + cp static/firebase-messaging-sw.example.js static/firebase-messaging-sw.js + ``` + +2. Update the configuration in both files with your Firebase project details: + - Replace `YOUR_API_KEY` with your Firebase API key + - Replace `YOUR_PROJECT_ID` with your Firebase project ID + - Replace other placeholder values with your actual Firebase configuration + +#### 4๏ธโƒฃ Firebase Cloud Messaging (FCM) Setup +1. In Firebase Console, go to Project Settings > Cloud Messaging +2. Generate a new Server Key +3. Add the key to your `.env` file: + ```env + FIREBASE_SERVER_KEY=your_server_key_here + ``` + +#### 5๏ธโƒฃ Testing Firebase Setup +1. Run the development server +2. Open the browser console +3. Check for any Firebase-related errors +4. Test push notifications using the Firebase Console + +### ๐Ÿ“ฑ Accessing the Application + +Once everything is set up, you can access: +- **Admin Panel**: http://localhost:8000/admin +- **Main Application**: http://localhost:8000 + +### ๐Ÿšจ Troubleshooting Guide + +#### ๐Ÿ”ง Common Issues +- **Firebase Setup**: Ensure proper configuration in `firebase-config.js` and `firebase-key.json` +- **Static Files**: Run `python manage.py collectstatic` if static files are not loading +- **Database Issues**: Check database settings in `.env` file +- **OTP Not Working**: Verify email settings in `.env` file + +## ๐Ÿ”ง Configuration + +### Environment Variables +Create a `.env` file in the project root: ```env # Django Settings DEBUG=True -SECRET_KEY=your-secret-key-here +SECRET_KEY=your-secret-key ALLOWED_HOSTS=localhost,127.0.0.1 -# Database Configuration +# Database 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 +# Firebase Settings +FIREBASE_CREDENTIALS=path/to/firebase-key.json +FIREBASE_DATABASE_URL=your-firebase-database-url + +# Google Cloud Settings +GOOGLE_CLOUD_PROJECT=your-project-id +GOOGLE_CLOUD_CREDENTIALS=path/to/google-cloud-key.json + +# Email Settings 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 +EMAIL_HOST_PASSWORD=your-email-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 +# SMS Configuration +SMS_API_KEY=your_sms_api_key_here +SMS_SENDER_ID=SMSSYS -# Google Cloud Storage -GS_BUCKET_NAME=your-bucket-name -GS_CREDENTIALS=path/to/credentials.json +# OTP Settings +OTP_EXPIRY=300 # OTP expiry time in seconds ``` -> **Note**: Never commit your `.env` file to version control. Keep your credentials secure! +## ๐Ÿ“ฆ Dependencies -## ๐ŸŽฏ Project Roadmap +### Backend +- Django 5.2 +- Django REST Framework 3.16.0 +- Firebase Admin 6.7.0 +- Google Cloud Storage 3.1.0 +- PyOTP 2.9.0 +- WhiteNoise 6.9.0 +- Django Jazzmin 3.0.1 +- Django Mathfilters 1.0.0 +- Django Model Utils 5.0.0 -
+### Frontend +- Bootstrap 5.3.0 +- jQuery Latest +- Firebase Web SDK 9.22.0 +- Font Awesome 6.0.0 -### โš ๏ธ Important Notice -``` -This is a Minor Project created for educational purposes. -Features listed in the roadmap may or may not be implemented. +## ๐Ÿ” Security Features + +### Authentication +- OTP and TOTP-based authentication +- Session management +- Password hashing +- Password reset tokens + +### Data Protection +- CSRF protection +- XSS prevention +- SQL injection prevention +- Input validation +- File upload security + +## ๐Ÿ“Š Database Schema + +### Core Models + +#### Institute +- `name`, `phone`, `email`, `address`, `pan_no`, `reg_no`, `logo`, `description` + +#### Course +- `name`, `code`, `duration`, `duration_type`, `description`, `is_active` + +#### Subject +- `name`, `code`, `course`, `period_or_year`, `syllabus_pdf`, `files` + +#### Student +- `name`, `status`, `gender`, `birth_date`, `email`, `phone`, `addresses` +- `marital_status`, `parent_name`, `parent_phone`, `citizenship_no` +- `batches`, `image`, `course`, `current_period`, `joining_date`, `fcm_token` + +#### Staff +- `name`, `phone`, `designation`, `gender`, `birth_date`, `email`, `addresses` +- `marital_status`, `parent_name`, `parent_phone`, `citizenship_no`, `passport` +- `image`, `joining_date`, `fcm_token`, `course` + +#### Parent +- `name`, `phone`, `email`, `address`, `students`, `image`, `fcm_token` + +#### Routine +- `course`, `subject`, `teacher`, `start_time`, `end_time`, `period_or_year`, `is_active` + +#### Attendance +- `date`, `routine`, `teacher`, `teacher_attend`, `class_status` + +#### Leave Management +- `StaffLeave`: `staff`, `start_date`, `end_date`, `message`, `status` +- `StudentLeave`: `student`, `start_date`, `end_date`, `message`, `status` + +#### Feedback System +- `StudentFeedback`: Student feedback for teachers +- `ParentFeedback`: Parent feedback for teachers +- `InstituteFeedback`: Student feedback for institute +- `StaffInstituteFeedback`: Staff feedback for institute +- `ParentInstituteFeedback`: Parent feedback for institute + +#### Course Tracking +- `student`, `course`, `enrollment_date`, `start_date`, `expected_end_date` +- `actual_end_date`, `progress_status`, `completion_percentage`, `current_period` +- `period_start_date`, `period_end_date`, `notes` + +#### Teacher-Parent Meeting +- `meeting_date`, `meeting_time`, `duration`, `status`, `agenda`, `notes` +- `cancellation_reason`, `meeting_link`, `is_online` + +### Security Models +- `TOTPSecret`: OTP secret keys +- `ResetToken`: Password reset tokens + +## ๐Ÿ”Œ API Endpoints + +### Authentication & Security +- `POST /password-reset/` - Password reset options +- `POST /password-reset/phone/` - Phone-based password reset +- `POST /password-reset/phone/verify/` - Verify phone OTP +- `POST /password-reset/email/` - Email-based password reset +- `POST /password-reset/email/verify/` - Verify email code +- `POST /password-reset/set/` - Set new password +- `POST /check-weak-password/` - Password strength check + +### Dashboard & Profile +- `GET /dashboard/` - Main dashboard +- `GET /student-dashboard/` - Student dashboard +- `GET /teacher-dashboard/` - Teacher dashboard +- `GET /parent-dashboard/` - Parent dashboard +- `GET /hod-dashboard/` - HOD dashboard +- `GET /admission-officer-dashboard/` - Admission officer dashboard +- `POST /update-profile/` - Update user profile +- `POST /change-password/` - Change password + +### Leave Management +- `POST /request-leave/` - Request student leave +- `POST /request-staff-leave/` - Request staff leave +- `POST /approve-student-leave//` - Approve student leave +- `POST /reject-student-leave//` - Reject student leave +- `POST /approve-staff-leave//` - Approve staff leave +- `POST /reject-staff-leave//` - Reject staff leave + +### Feedback System +- `POST /submit-feedback/` - Submit general feedback +- `POST /submit-institute-feedback/` - Submit institute feedback +- `POST /submit-staff-institute-feedback/` - Submit staff institute feedback +- `POST /submit-parent-feedback/` - Submit parent feedback +- `POST /submit-parent-institute-feedback/` - Submit parent institute feedback + +### Subject & Course Management +- `GET /subject//files/` - Get subject files +- `GET /subject//syllabus/` - View subject syllabus +- `GET /get-subjects/` - Get all subjects +- `GET /get-teachers/` - Get all teachers +- `GET /get-course-duration/` - Get course duration +- `GET /get-subject-schedule/` - Get subject schedule +- `POST /manage-subject-files/` - Manage subject files +- `POST /delete-subject-file/` - Delete subject file +- `GET /get-teacher-subjects/` - Get teacher's subjects + +### Attendance Management +- `POST /save-attendance/` - Save attendance +- `GET /get-attendance-form/` - Get attendance form +- `GET /get-students/` - Get student list + +### Notice Management +- `POST /add-notice/` - Add new notice +- `POST /delete-notice//` - Delete notice + +### Parent-Teacher Meeting +- `POST /schedule-meeting/` - Schedule a meeting +- `POST /update-meeting//` - Update meeting details +- `POST /cancel-meeting//` - Cancel a meeting +- `GET /get-meetings/` - Get all meetings +- `GET /get-upcoming-meetings/` - Get upcoming meetings + +### Course Progress +- `GET /get-course-progress//` - Get student's course progress +- `POST /update-course-progress/` - Update course progress +- `GET /get-progress-analytics/` - Get progress analytics + +### Firebase Integration +- `POST /saveFCMToken/` - Save Firebase Cloud Messaging token + +### ๐Ÿ” Authentication System + +#### OTP Verification +- Phone-based OTP verification +- OTP expiry management +- OTP resend functionality +- Secure OTP storage + + +## ๐Ÿ“ฑ Frontend Features + +### User Interface +- Responsive design +- Dark/Light mode +- Custom themes +- Accessibility support +- Mobile-first approach + +### Interactive Features +- Real-time updates +- File uploads +- Data visualization +- Interactive forms +- Dynamic content loading + +### Mobile Optimization +- Touch-friendly interface +- Offline support +- Push notifications +- Mobile-specific layouts +- Gesture support + +## ๐Ÿ” Testing + +### Current Test Coverage +- Unit Tests: Basic test structure in place +- API Tests: Endpoint testing framework available +- Integration Tests: Setup for comprehensive testing + +### Running Tests +```bash +# Run all tests +python manage.py test + +# Run specific app tests +python manage.py test app + +# Run with coverage +coverage run --source='.' manage.py test +coverage report ``` -
+### Test Areas +1. Authentication & Authorization + - OTP verification + - Password reset + - Role-based access -### ๐ŸŽฏ Current (v1.0) -- โœ… Core SMS functionality -- โœ… User management -- โœ… Course tracking -- โœ… Basic features +2. Data Management + - CRUD operations + - Data validation + - File handling -### ๐Ÿ”œ Upcoming (v1.1+) -- ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Parents dashboard -- ๐Ÿ“š Teacher study materials -- ๐Ÿ“– Library management -- ๐Ÿ“Š Enhanced analytics -- ๐Ÿค Parent-teacher meetings +3. Business Logic + - Attendance calculation + - Leave approval workflow + - Course progress tracking -## ๐Ÿ“ฎ Support & Links +4. Integration + - Firebase messaging + - File storage + - Email/SMS notifications -
+## ๐Ÿ“ˆ Performance -[![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) +### Optimization Techniques +- Database indexing +- Query optimization +- Caching +- Lazy loading +- Code minification +- Asset compression +- CDN integration -
+### Monitoring +- Performance metrics +- Error tracking +- User analytics +- Resource usage +- API response times + +## ๐Ÿค Contributing + +1. Fork the repository +2. Create your feature branch +3. Commit your changes +4. Push to the branch +5. Create a Pull Request + +### Code Style +- Follow PEP 8 +- Use type hints +- Write docstrings +- Add comments +- Follow Django best practices + +### Commit Messages +- Use present tense +- Be descriptive +- Reference issues +- Follow conventional commits + +## ๐Ÿ“„ License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ## ๐Ÿ™ Credits @@ -210,14 +680,6 @@ If you're using this project in an academic context, you can cite it as: - Special thanks to the open-source community for their valuable resources - Appreciation to all users who have provided feedback and suggestions -## ๐Ÿ“„ License - -
- -[![License](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE) - -
- ---
@@ -225,7 +687,47 @@ If you're using this project in an academic context, you can cite it as: ### Made with โค๏ธ by [Sankalp](https://github.com/S4NKALP) [![GitHub](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/S4NKALP) - +[![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white)](https://linkedin.com/in/sankalp) +[![Twitter](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://twitter.com/sankalp)
+ +## ๐Ÿ’ฐ Project Costs + +### Development & Implementation +- Backend Development: NPR 200,000 +- Frontend Development: NPR 160,000 +- QA Engineering: NPR 60,000 +- Project Management: NPR 240,000 +- Development Tools & Licenses: NPR 15,000 +- Testing Tools: NPR 10,000 +- Documentation Tools: NPR 5,000 + +### Annual Infrastructure & Services +- Firebase Services: NPR 10,000/year +- Google Cloud Storage: NPR 8,000/year +- Domain & SSL: NPR 3,000/year +- Server Hosting: NPR 20,000/year + +### Support & Maintenance +- User Training: NPR 20,000 +- Documentation: NPR 15,000 +- Technical Support (First Year): NPR 30,000 + +### Total First Year Cost: NPR 601,000 + +## ๐ŸŒ Feasibility + +### Target Areas +- Urban Areas with Internet Connectivity +- Educational Institutions +- Government Offices +- Private Organizations +- Healthcare Facilities + +### Key Considerations +- Scalable architecture for future expansion +- Mobile-responsive design +- Real-time data processing capabilities +- Secure data handling and storage +- Offline support capabilities \ No newline at end of file diff --git a/app/admin.py b/app/admin.py index 1bb20cc..01dcb37 100644 --- a/app/admin.py +++ b/app/admin.py @@ -1,143 +1,204 @@ -from django.contrib import admin -from .models import ( - Institute, - Course, - Subject, - SubjectFile, - Student, - Staff, - Batch, - Attendance, - AttendanceRecord, - Notice, - Routine, - Staff_leave, - Student_Leave, - StudentFeedback, - CourseTracking, - InstituteFeedback, - StaffInstituteFeedback, - TOTPSecret, - ResetToken, -) -from .firebase import FCMDevice -from django.forms import ModelForm -from django.contrib.auth.models import User, Group -from django.contrib.auth.admin import UserAdmin, GroupAdmin -from app.forms import StaffAdminForm, StudentAdminForm -from django.urls import path -from django.http import JsonResponse +# Standard library imports import json +from datetime import datetime + +# Core Django imports +from django.contrib import admin +from django.contrib.auth.admin import GroupAdmin, UserAdmin +from django.contrib.auth.models import Group, User from django.db import transaction from django.db.models import Q -from datetime import datetime +from django.forms import ModelForm +from django.http import JsonResponse +from django.urls import path +from django.utils.translation import gettext_lazy as _ + +# Local app imports +from app.firebase import FCMDevice +from app.forms import ParentAdminForm, StaffAdminForm, StudentAdminForm +from app.models import ( + Attendance, + AttendanceRecord, + Batch, + Course, + CourseTracking, + Institute, + InstituteFeedback, + Notice, + Parent, + ParentFeedback, + ParentInstituteFeedback, + ResetToken, + Routine, + Staff, + StaffInstituteFeedback, + StaffLeave, + Student, + StudentFeedback, + StudentLeave, + Subject, + SubjectFile, + TeacherParentMeeting, + TOTPSecret, +) # Register your models here. class CustomAdminSite(admin.AdminSite): - def get_app_list(self, request): + def get_app_list(self, request, app_label=None): # Original app list - app_list = super().get_app_list(request) + app_list = super().get_app_list(request, app_label) model_map = { # Authentication Models "User": { "group": "Authentication", "group_label": "authentication", + "display_name": "Users", }, "Group": { "group": "Authentication", "group_label": "authentication", + "display_name": "Groups", }, "TOTPSecret": { "group": "Authentication", "group_label": "authentication", + "display_name": "TOTP Keys", }, "ResetToken": { "group": "Authentication", "group_label": "authentication", + "display_name": "Reset Tokens", }, # Organization Models "Institute": { "group": "Organization", "group_label": "organization", + "display_name": "Institute", }, "Batch": { "group": "Organization", "group_label": "organization", + "display_name": "Batches", }, "Notice": { "group": "Organization", "group_label": "organization", + "display_name": "Notices", }, # Course Management Models "Course": { "group": "Course Management", "group_label": "course_management", + "display_name": "Courses", }, "Subject": { "group": "Course Management", "group_label": "course_management", + "display_name": "Subjects", }, "SubjectFile": { "group": "Course Management", "group_label": "course_management", + "display_name": "Materials", }, "CourseTracking": { "group": "Course Management", "group_label": "course_management", + "display_name": "Progress", }, # Human Management Models + "Parent": { + "group": "Human Management", + "group_label": "human_management", + "display_name": "Parents", + }, "Student": { "group": "Human Management", "group_label": "human_management", + "display_name": "Students", }, "Staff": { "group": "Human Management", "group_label": "human_management", + "display_name": "Staff", }, + # Schedule Management "Routine": { - "group": "Human Management", - "group_label": "human_management", + "group": "Schedule Management", + "group_label": "schedule_management", + "display_name": "Classes", }, - "Staff_leave": { - "group": "Human Management", - "group_label": "human_management", + "TeacherParentMeeting": { + "group": "Schedule Management", + "group_label": "schedule_management", + "display_name": "PT Meetings", }, - "Student_Leave": { - "group": "Human Management", - "group_label": "human_management", + # Leave Management + "StaffLeave": { + "group": "Leave Management", + "group_label": "leave_management", + "display_name": "Staff Leave", + }, + "StudentLeave": { + "group": "Leave Management", + "group_label": "leave_management", + "display_name": "Student Leave", }, # Attendance Models "Attendance": { "group": "Attendance", "group_label": "attendance", + "display_name": "Attendance", }, "AttendanceRecord": { "group": "Attendance", "group_label": "attendance", + "display_name": "Records", }, # Feedback Models "StudentFeedback": { "group": "Feedback", "group_label": "feedback", + "display_name": "Student Feedback", }, "InstituteFeedback": { "group": "Feedback", "group_label": "feedback", + "display_name": "Student-Institute", }, "StaffInstituteFeedback": { "group": "Feedback", "group_label": "feedback", + "display_name": "Staff-Institute", + }, + "ParentFeedback": { + "group": "Feedback", + "group_label": "feedback", + "display_name": "Parent Feedback", + }, + "ParentInstituteFeedback": { + "group": "Feedback", + "group_label": "feedback", + "display_name": "Parent-Institute", }, # Device Management "FCMDevice": { "group": "Device Management", "group_label": "device_management", + "display_name": "Devices", }, } + # If app_label is specified, return only that app + if app_label: + for app in app_list: + if app["app_label"] == app_label: + return [app] + return [] + grouped_app_list = [{"name": "Main", "app_label": "dashboard", "models": []}] grouped_models = {} @@ -152,6 +213,11 @@ class CustomAdminSite(admin.AdminSite): "app_label": model_map[model_name]["group_label"], "models": [], } + + # Apply custom display name if available + if "display_name" in model_map[model_name]: + model["name"] = model_map[model_name]["display_name"] + grouped_models[group]["models"].append(model) dashboard_item = { @@ -187,10 +253,17 @@ class InstituteModel(admin.ModelAdmin): @admin.register(Course, site=custom_admin_site) class CourseModel(admin.ModelAdmin): - list_display = ("id", "name", "duration", "duration_type", "get_total_duration") + list_display = ( + "id", + "name", + "duration", + "duration_type", + "get_total_duration", + "is_active", + ) search_fields = ("name", "duration", "duration_type") - list_filter = ("duration_type", "duration") - advanced_filter_fields = ("name", "duration", "duration_type") + list_filter = ("duration_type", "duration", "is_active") + advanced_filter_fields = ("name", "duration", "duration_type", "is_active") def get_total_duration(self, obj): if obj.duration_type == "Semester": @@ -213,34 +286,41 @@ class CourseModel(admin.ModelAdmin): @admin.register(Batch, site=custom_admin_site) class BatchAdmin(admin.ModelAdmin): - list_display = ("name", "start_date", "end_date") - search_fields = ("name", "start_date", "end_date") - list_filter = ("name", "start_date", "end_date") - advanced_filter_fields = ("name", "start_date", "end_date") + list_display = ("name", "year", "is_active") + search_fields = ("name", "year") + list_filter = ("name", "year", "is_active") + advanced_filter_fields = ("name", "year", "is_active") @admin.register(Subject, site=custom_admin_site) class SubjectModel(admin.ModelAdmin): - list_display = ("id", "name", "course", "get_period_display", "has_syllabus", "file_count") + list_display = ( + "id", + "name", + "course", + "get_period_display", + "has_syllabus", + "file_count", + ) search_fields = ("name", "course__name") - list_filter = ("course", "semester_or_year") - advanced_filter_fields = ("name", "course", "semester_or_year") + list_filter = ("course", "period_or_year") + advanced_filter_fields = ("name", "course", "period_or_year") def has_syllabus(self, obj): return bool(obj.syllabus_pdf) has_syllabus.boolean = True has_syllabus.short_description = "Has Syllabus" - + def file_count(self, obj): return obj.subjectfile_set.count() - + file_count.short_description = "Additional Files" def get_period_display(self, obj): if obj.course.duration_type == "Semester": - return f"Semester {obj.semester_or_year}" - return f"Year {obj.semester_or_year}" + return f"Semester {obj.period_or_year}" + return f"Year {obj.period_or_year}" get_period_display.short_description = "Period" @@ -252,7 +332,7 @@ class SubjectModel(admin.ModelAdmin): def response_add(self, request, obj, post_url_continue=None): if "_addanother" in request.POST: - # Pre-fill the course and semester fields for the next entry + # Pre-fill the course and period fields for the next entry return self.response_post_save_add(request, obj) return super().response_add(request, obj, post_url_continue) @@ -260,23 +340,23 @@ class SubjectModel(admin.ModelAdmin): form = super().get_form(request, obj, **kwargs) if obj and obj.course: if obj.course.duration_type == "Semester": - form.base_fields["semester_or_year"].label = "Semester" + form.base_fields["period_or_year"].label = "Semester" form.base_fields[ - "semester_or_year" + "period_or_year" ].help_text = f"Enter semester number (1-{obj.course.duration * 2})" else: - form.base_fields["semester_or_year"].label = "Year" + form.base_fields["period_or_year"].label = "Year" form.base_fields[ - "semester_or_year" + "period_or_year" ].help_text = f"Enter year number (1-{obj.course.duration})" # Keep the last used course selected for next entry if not obj and request.method == "GET": if "course" in request.GET: form.base_fields["course"].initial = request.GET["course"] - if "semester_or_year" in request.GET: - form.base_fields["semester_or_year"].initial = request.GET[ - "semester_or_year" + if "period_or_year" in request.GET: + form.base_fields["period_or_year"].initial = request.GET[ + "period_or_year" ] return form @@ -287,17 +367,17 @@ class SubjectModel(admin.ModelAdmin): opts = self.model._meta url = reverse(f"admin:{opts.app_label}_{opts.model_name}_add") return HttpResponseRedirect( - f"{url}?course={obj.course.id}&semester_or_year={obj.semester_or_year}" + f"{url}?course={obj.course.id}&period_or_year={obj.period_or_year}" ) @admin.register(Student, site=custom_admin_site) class StudentAdmin(admin.ModelAdmin): form = StudentAdminForm - list_display = ("id", "name", "phone", "course", "status") - search_fields = ("name", "phone", "course__name") + list_display = ("id", "name", "phone", "course", "status", "fcm_token") + search_fields = ("name", "phone", "course__name", "fcm_token") list_filter = ("course", "status", "gender", "joining_date") - advanced_filter_fields = ("name", "phone", "course", "batch", "status") + advanced_filter_fields = ("name", "phone", "course", "batch", "status", "fcm_token") def has_view_permission(self, request, obj=None): """Check if user has view permission""" @@ -328,13 +408,16 @@ class StudentAdmin(admin.ModelAdmin): @admin.register(Staff, site=custom_admin_site) class StaffAdmin(admin.ModelAdmin): form = StaffAdminForm - list_display = ("id", "name", "phone", "designation", "joining_date") - search_fields = ("name", "phone", "designation") + list_display = ("id", "name", "phone", "designation", "joining_date", "fcm_token") + search_fields = ("name", "phone", "designation", "fcm_token") list_filter = ("designation", "joining_date", "gender") - advanced_filter_fields = ("name", "phone", "designation") + advanced_filter_fields = ("name", "phone", "designation", "fcm_token") readonly_fields = ("password",) ordering = ("name",) - filter_horizontal = ("groups",) + + def save_model(self, request, obj, form, change): + """Just save the model, group assignment is handled in the form""" + super().save_model(request, obj, form, change) def has_view_permission(self, request, obj=None): """Check if user has view permission""" @@ -435,10 +518,10 @@ class RoutineAdmin(admin.ModelAdmin): "teacher", "start_time", "end_time", - "semester_or_year", + "period_or_year", "is_active", ] - list_filter = ["is_active", "course", "subject", "teacher", "semester_or_year"] + list_filter = ["is_active", "course", "subject", "teacher", "period_or_year"] search_fields = ["course__name", "subject__name", "teacher__name"] ordering = ["start_time"] change_form_template = "admin/app/routine/change_form.html" @@ -478,7 +561,7 @@ class RoutineAdmin(admin.ModelAdmin): "teacher", "start_time", "end_time", - "semester_or_year", + "period_or_year", ] missing_fields = [ field @@ -496,7 +579,7 @@ class RoutineAdmin(admin.ModelAdmin): course_id = int(routine_data["course"]) subject_id = int(routine_data["subject"]) teacher_id = int(routine_data["teacher"]) - semester = int(routine_data["semester_or_year"]) + period = int(routine_data["period_or_year"]) except (ValueError, TypeError) as e: raise ValueError(f"Error parsing IDs: {str(e)}") @@ -518,7 +601,7 @@ class RoutineAdmin(admin.ModelAdmin): existing_routine = Routine.objects.filter( course_id=course_id, subject_id=subject_id, - semester_or_year=semester, + period_or_year=period, start_time=start_time, end_time=end_time, ).first() @@ -536,7 +619,7 @@ class RoutineAdmin(admin.ModelAdmin): teacher_id=teacher_id, start_time=start_time, end_time=end_time, - semester_or_year=semester, + period_or_year=period, is_active=True, ) created_routines.append(routine) @@ -558,7 +641,7 @@ class RoutineAdmin(admin.ModelAdmin): "teacher": r.teacher.name, "start_time": r.start_time.strftime("%H:%M"), "end_time": r.end_time.strftime("%H:%M"), - "semester_or_year": r.semester_or_year, + "period_or_year": r.period_or_year, } for r in created_routines ], @@ -581,7 +664,7 @@ class RoutineAdmin(admin.ModelAdmin): if request.GET.get("course"): kwargs["queryset"] = Subject.objects.filter( course_id=request.GET.get("course"), - semester_or_year=request.GET.get("semester_or_year", 1), + period_or_year=request.GET.get("period_or_year", 1), ) else: kwargs["queryset"] = Subject.objects.none() @@ -601,7 +684,7 @@ class RoutineAdmin(admin.ModelAdmin): form.base_fields["course"].widget.attrs["class"] = "select2" form.base_fields["subject"].widget.attrs["class"] = "select2" form.base_fields["teacher"].widget.attrs["class"] = "select2" - form.base_fields["semester_or_year"].initial = 1 + form.base_fields["period_or_year"].initial = 1 return form def add_view(self, request, form_url="", extra_context=None): @@ -618,9 +701,15 @@ class RoutineAdmin(admin.ModelAdmin): js = ("https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js",) -@admin.register(Staff_leave, site=custom_admin_site) +@admin.register(StaffLeave, site=custom_admin_site) class StaffLeaveAdmin(admin.ModelAdmin): - list_display = ("staff", "start_date", "end_date", "get_status_display", "created_at") + list_display = ( + "staff", + "start_date", + "end_date", + "get_status_display", + "created_at", + ) list_filter = ("status", "start_date", "end_date", "created_at") search_fields = ("staff__name", "message") readonly_fields = ("created_at", "updated_at") @@ -628,9 +717,15 @@ class StaffLeaveAdmin(admin.ModelAdmin): raw_id_fields = ("staff",) -@admin.register(Student_Leave, site=custom_admin_site) +@admin.register(StudentLeave, site=custom_admin_site) class StudentLeaveAdmin(admin.ModelAdmin): - list_display = ("student", "start_date", "end_date", "get_status_display", "created_at") + list_display = ( + "student", + "start_date", + "end_date", + "get_status_display", + "created_at", + ) list_filter = ("status", "start_date", "end_date", "created_at") search_fields = ("student__name", "message") readonly_fields = ("created_at", "updated_at") @@ -790,15 +885,25 @@ class StaffInstituteFeedbackAdmin(admin.ModelAdmin): @admin.register(FCMDevice, site=custom_admin_site) class FCMDeviceAdmin(admin.ModelAdmin): - list_display = ("id", "token", "created_at", "last_active", "is_active") + list_display = ( + "id", + "token", + "user_type", + "is_active", + "created_at", + "last_active", + ) search_fields = ("token",) - list_filter = ("is_active", "created_at", "last_active") + list_filter = ("user_type", "is_active", "created_at", "last_active") readonly_fields = ("created_at", "last_active") ordering = ("-last_active",) fieldsets = ( - (None, {"fields": ("token", "is_active")}), - ("Metadata", {"fields": ("created_at", "last_active"), "classes": ("collapse",)}), + (None, {"fields": ("token", "user_type", "is_active", "is_fallback")}), + ( + "Metadata", + {"fields": ("created_at", "last_active"), "classes": ("collapse",)}, + ), ) @@ -810,5 +915,166 @@ class SubjectFileAdmin(admin.ModelAdmin): autocomplete_fields = ["subject", "uploaded_by"] +@admin.register(Parent, site=custom_admin_site) +class ParentAdmin(admin.ModelAdmin): + form = ParentAdminForm + list_display = ("id", "name", "phone", "email", "fcm_token") + search_fields = ("name", "phone", "email", "fcm_token") + list_filter = ("students",) + advanced_filter_fields = ("name", "phone", "email", "students", "fcm_token") + + def has_view_permission(self, request, obj=None): + """Check if user has view permission""" + return request.user.has_perm("app.view_parent") + + def has_add_permission(self, request): + """Check if user has add permission""" + return request.user.has_perm("app.add_parent") + + def has_change_permission(self, request, obj=None): + """Check if user has change permission""" + return request.user.has_perm("app.change_parent") + + def has_delete_permission(self, request, obj=None): + """Check if user has delete permission""" + return request.user.has_perm("app.delete_parent") + + def has_module_permission(self, request): + """Check if user has any permissions for this app""" + return request.user.has_module_perms("app") + + def get_readonly_fields(self, request, obj=None): + if obj: + return self.readonly_fields + ("password",) + return self.readonly_fields + + +@admin.register(ParentFeedback, site=custom_admin_site) +class ParentFeedbackAdmin(admin.ModelAdmin): + list_display = ("parent", "teacher", "student", "rating", "created_at") + list_filter = ("rating", "created_at", "teacher", "student") + search_fields = ("parent__name", "teacher__name", "student__name", "feedback_text") + readonly_fields = ("created_at", "updated_at") + ordering = ("-created_at",) + raw_id_fields = ("parent", "teacher", "student") + + def get_queryset(self, request): + return ( + super().get_queryset(request).select_related("parent", "teacher", "student") + ) + + +@admin.register(ParentInstituteFeedback, site=custom_admin_site) +class ParentInstituteFeedbackAdmin(admin.ModelAdmin): + list_display = ( + "parent", + "institute", + "feedback_type", + "rating", + "is_anonymous", + "created_at", + ) + list_filter = ("feedback_type", "rating", "is_anonymous", "created_at", "institute") + search_fields = ("parent__name", "institute__name", "feedback_text") + readonly_fields = ("created_at", "updated_at") + ordering = ("-created_at",) + raw_id_fields = ("parent", "institute") + + def get_queryset(self, request): + return super().get_queryset(request).select_related("parent", "institute") + + +@admin.register(TeacherParentMeeting, site=custom_admin_site) +class TeacherParentMeetingAdmin(admin.ModelAdmin): + list_display = ( + "meeting_date", + "meeting_time", + "duration", + "status", + "is_online", + "meeting_link", + ) + list_filter = ( + "status", + "is_online", + "meeting_date", + ) + search_fields = ( + "agenda", + "notes", + "cancellation_reason", + ) + readonly_fields = ("created_at", "updated_at") + ordering = ("-meeting_date", "-meeting_time") + + fieldsets = ( + ( + "Meeting Details", + { + "fields": ( + "meeting_date", + "meeting_time", + "duration", + "status", + "is_online", + "meeting_link", + ) + }, + ), + ( + "Meeting Content", + { + "fields": ("agenda", "notes"), + "classes": ("collapse",), + }, + ), + ( + "Cancellation Details", + { + "fields": ("cancellation_reason",), + "classes": ("collapse",), + }, + ), + ( + "Metadata", + { + "fields": ( + "created_at", + "updated_at", + ), + "classes": ("collapse",), + }, + ), + ) + + +@admin.register(TOTPSecret, site=custom_admin_site) +class TOTPSecretAdmin(admin.ModelAdmin): + list_display = ("identifier", "created_at", "expires_at", "is_expired") + search_fields = ("identifier",) + readonly_fields = ("created_at", "expires_at") + ordering = ("-created_at",) + + def is_expired(self, obj): + return obj.is_expired() + + is_expired.boolean = True + is_expired.short_description = "Expired" + + +@admin.register(ResetToken, site=custom_admin_site) +class ResetTokenAdmin(admin.ModelAdmin): + list_display = ("identifier", "token", "created_at", "expires_at", "is_expired") + search_fields = ("identifier", "token") + readonly_fields = ("created_at", "expires_at") + ordering = ("-created_at",) + + def is_expired(self, obj): + return obj.is_expired() + + is_expired.boolean = True + is_expired.short_description = "Expired" + + custom_admin_site.register(User, UserAdmin) custom_admin_site.register(Group, GroupAdmin) diff --git a/app/admissionviews.py b/app/admissionviews.py new file mode 100644 index 0000000..fcc0f6e --- /dev/null +++ b/app/admissionviews.py @@ -0,0 +1,731 @@ +# Standard library imports +from datetime import timedelta, datetime + +# Core Django imports +from django.shortcuts import render, redirect, get_object_or_404 +from django.http import JsonResponse +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.db.models import Count, Q, Avg +from django.utils import timezone +from django.views.decorators.http import require_http_methods, require_GET +from django.contrib.auth.hashers import check_password, make_password +from django.contrib.auth.models import Group + +# Local app imports +from app.models import ( + Staff, + Student, + Course, + CourseTracking, + Batch, + StaffLeave, + Notice, + FEEDBACK_TYPE_CHOICES, +) + + +@login_required +def admission_officer_dashboard(request): + """View to display the admission officer dashboard""" + + # Check if user is in staff and has Admission Officer designation + if ( + not isinstance(request.user, Staff) + or request.user.designation != "Admission Officer" + ): + messages.error( + request, + "You don't have permission to access the admission officer dashboard.", + ) + return redirect("dashboard") + + # Get the admission officer object + try: + admission_officer = request.user + except Exception as e: + messages.error( + request, + "Admission Officer profile not found. Please contact administrator.", + ) + return redirect("dashboard") + + today = timezone.now().date() + last_week = today - timedelta(days=7) + last_month = today - timedelta(days=30) + + # Get student statistics and list + total_students = Student.objects.count() + active_students = Student.objects.filter(status="Active").count() + new_students = Student.objects.filter(joining_date__gte=last_month).count() + students = ( + Student.objects.select_related("course") + .prefetch_related("batches") + .all() + .order_by("-id") + ) # For the student list + + # Get course statistics and list + total_courses = Course.objects.count() + active_courses = Course.objects.filter(is_active=True).count() + courses = Course.objects.all().order_by("name") # For the dropdown + + # Get batch statistics and list + total_batches = Batch.objects.count() + active_batches = Batch.objects.filter(is_active=True).count() + batches = Batch.objects.all().order_by("-year") # For the dropdown + + # Get recent course trackings + recent_trackings = CourseTracking.objects.select_related( + "student", "course" + ).order_by("-created_at") + + # Get leave statistics + pending_leaves = StaffLeave.objects.filter( + staff=admission_officer, status=0 + ).count() + approved_leaves = StaffLeave.objects.filter( + staff=admission_officer, status=1 + ).count() + rejected_leaves = StaffLeave.objects.filter( + staff=admission_officer, status=2 + ).count() + + # Get recent leaves + recent_leaves = StaffLeave.objects.filter(staff=admission_officer).order_by( + "-created_at" + ) + + # Get recent notices + notices = Notice.objects.all().order_by("-created_at") + + # Get feedback types for institute feedback + feedback_types = FEEDBACK_TYPE_CHOICES + + context = { + "officer": admission_officer, + "title": "Admission Officer Dashboard", + "total_students": total_students, + "active_students": active_students, + "new_students": new_students, + "students": students, # For the student list + "total_courses": total_courses, + "active_courses": active_courses, + "courses": courses, # For the dropdown + "total_batches": total_batches, + "active_batches": active_batches, + "batches": batches, # For the dropdown + "recent_trackings": recent_trackings, + "pending_leaves": pending_leaves, + "approved_leaves": approved_leaves, + "rejected_leaves": rejected_leaves, + "recent_leaves": recent_leaves, + "notices": notices, + "feedback_types": feedback_types, + } + + return render(request, "admission_officer/dashboard.html", context) + + +@login_required +def add_student(request): + """View to add a new student""" + if request.method != "POST": + messages.error(request, "Invalid request method") + return redirect("admissionOfficerDashboard") + + # Check if user has permission + if ( + not isinstance(request.user, Staff) + or request.user.designation != "Admission Officer" + ): + messages.error(request, "Permission denied") + return redirect("admissionOfficerDashboard") + + try: + # Get form data + name = request.POST.get("name") + phone = request.POST.get("phone") + email = request.POST.get("email") + gender = request.POST.get("gender") + birth_date = request.POST.get("birth_date") + course_id = request.POST.get("course") + current_period = request.POST.get("current_period", 1) + status = request.POST.get("status", "Active") + parent_name = request.POST.get("parent_name") + parent_phone = request.POST.get("parent_phone") + permanent_address = request.POST.get("permanent_address") + temporary_address = request.POST.get("temporary_address") + citizenship_number = request.POST.get("citizenship_number") + + # Get batch ID + batch_id = request.POST.get("batches") + + # Get photo if uploaded + photo = request.FILES.get("photo") + + # Validate required fields + if not all([name, phone, batch_id]): + messages.error(request, "Name, phone, and batch are required") + return redirect("admissionOfficerDashboard") + + # Check if student with same phone already exists + if Student.objects.filter(phone=phone).exists(): + messages.error(request, "Student with this phone number already exists") + return redirect("admissionOfficerDashboard") + + # Create student object + student = Student() + student.name = name + student.phone = phone + student.email = email + student.gender = gender + + # Set birth date if provided + if birth_date: + student.birth_date = birth_date + + # Set course if provided + if course_id: + try: + course = Course.objects.get(id=course_id) + student.course = course + student.current_period = current_period + except Course.DoesNotExist: + messages.error(request, "Selected course not found") + return redirect("admissionOfficerDashboard") + + student.status = status + student.parent_name = parent_name + student.parent_phone = parent_phone + student.permanent_address = permanent_address + student.temporary_address = temporary_address + student.citizenship_number = citizenship_number + + # Set photo if uploaded + if photo: + student.image = photo + + # Set default password as '123' + student.set_password("123") + + # Save student + student.save() + + # Add student to Student group + student_group, created = Group.objects.get_or_create(name="Student") + student.groups.add(student_group) + + # Add student to batch + if batch_id: + try: + batch = Batch.objects.get(id=batch_id) + student.batches.add(batch) + # Joining date will be set automatically from the batch year in the signal + except Batch.DoesNotExist: + messages.error(request, "Selected batch not found") + # Continue anyway as we've already saved the student + + # Create course tracking if course is assigned + if course_id: + try: + # Use today's date if no joining date yet (should be set by batch signal) + start_date = student.joining_date or timezone.now().date() + + course_tracking = CourseTracking( + student=student, + course=course, + start_date=start_date, + current_period=current_period, + ) + course_tracking.save() + except Exception as e: + print(f"Error creating course tracking: {str(e)}") + + messages.success( + request, f'Student "{name}" added successfully. Default password is: 123' + ) + return redirect("admissionOfficerDashboard") + + except Exception as e: + messages.error(request, f"Error: {str(e)}") + return redirect("admissionOfficerDashboard") + + +@login_required +def add_batch(request): + """View to add a new batch""" + if request.method != "POST": + messages.error(request, "Invalid request method") + return redirect("admissionOfficerDashboard") + + # Check if user has permission + if ( + not isinstance(request.user, Staff) + or request.user.designation != "Admission Officer" + ): + messages.error(request, "Permission denied") + return redirect("admissionOfficerDashboard") + + try: + # Get form data + name = request.POST.get("name") + start_date = request.POST.get("start_date") + is_active = request.POST.get("is_active") == "on" + + # Validate required fields + if not all([name, start_date]): + messages.error(request, "Name and start date are required") + return redirect("admissionOfficerDashboard") + + # Create batch + batch = Batch() + batch.name = name + batch.year = start_date # Use start_date for the year field + batch.is_active = is_active + batch.save() + + messages.success(request, f'Batch "{name}" added successfully') + return redirect("admissionOfficerDashboard") + + except Exception as e: + messages.error(request, f"Error: {str(e)}") + return redirect("admissionOfficerDashboard") + + +@login_required +def add_course(request): + """View to add a new course""" + if request.method != "POST": + messages.error(request, "Invalid request method") + return redirect("admissionOfficerDashboard") + + # Check if user has permission + if ( + not isinstance(request.user, Staff) + or request.user.designation != "Admission Officer" + ): + messages.error(request, "Permission denied") + return redirect("admissionOfficerDashboard") + + try: + # Get form data + name = request.POST.get("name") + code = request.POST.get("code", "") + duration_type = request.POST.get("duration_type") + duration = request.POST.get("duration") + description = request.POST.get("description", "") + is_active = request.POST.get("is_active") == "on" + + # Validate required fields + if not all([name, duration_type, duration]): + messages.error(request, "Name, duration type, and duration are required") + return redirect("admissionOfficerDashboard") + + # Create course + course = Course() + course.name = name + course.code = code + course.duration_type = duration_type + course.duration = duration + course.description = description + course.is_active = is_active + course.save() + + messages.success(request, f'Course "{name}" added successfully') + return redirect("admissionOfficerDashboard") + + except Exception as e: + messages.error(request, f"Error: {str(e)}") + return redirect("admissionOfficerDashboard") + + +@login_required +def add_course_tracking(request): + """View to add course tracking for a student""" + if request.method != "POST": + messages.error(request, "Invalid request method") + return redirect("admissionOfficerDashboard") + + # Check if user has permission + if ( + not isinstance(request.user, Staff) + or request.user.designation != "Admission Officer" + ): + messages.error(request, "Permission denied") + return redirect("admissionOfficerDashboard") + + try: + # Get form data + student_id = request.POST.get("student") + course_id = request.POST.get("course") + start_date = request.POST.get("start_date") + expected_end_date = request.POST.get("expected_end_date") + current_period = request.POST.get("current_period", 1) + progress_status = request.POST.get("progress_status", "In Progress") + notes = request.POST.get("notes") + + # Validate required fields + if not student_id or not course_id: + messages.error(request, "Student and course are required") + return redirect("admissionOfficerDashboard") + + # Check if student exists + try: + student = Student.objects.get(id=student_id) + except Student.DoesNotExist: + messages.error(request, "Student not found") + return redirect("admissionOfficerDashboard") + + # Check if course exists + try: + course = Course.objects.get(id=course_id) + except Course.DoesNotExist: + messages.error(request, "Course not found") + return redirect("admissionOfficerDashboard") + + # Check if tracking already exists + if CourseTracking.objects.filter(student=student, course=course).exists(): + messages.error(request, "Student is already enrolled in this course") + return redirect("admissionOfficerDashboard") + + # Create course tracking object + tracking = CourseTracking() + tracking.student = student + tracking.course = course + + # Set dates if provided + if start_date: + tracking.start_date = start_date + if expected_end_date: + tracking.expected_end_date = expected_end_date + + tracking.current_period = int(current_period) + tracking.progress_status = progress_status + tracking.notes = notes + + # Save tracking + tracking.save() + + messages.success( + request, + f'Course tracking added for student "{student.name}" in course "{course.name}"', + ) + return redirect("admissionOfficerDashboard") + + except Exception as e: + messages.error(request, f"Error: {str(e)}") + return redirect("admissionOfficerDashboard") + + +@login_required +def get_courses(request): + """View to get courses for dropdowns""" + try: + courses = Course.objects.all().order_by("name") + course_data = [ + {"id": course.id, "name": course.name, "code": course.code} + for course in courses + ] + + return JsonResponse({"success": True, "courses": course_data}) + except Exception as e: + return JsonResponse({"success": False, "message": str(e)}, status=500) + + +@login_required +def get_batches(request): + """View to get batches for dropdowns""" + try: + batches = Batch.objects.all().order_by("-year") + batch_data = [] + + for batch in batches: + batch_data.append( + { + "id": batch.id, + "name": batch.name, + "year": batch.year.year if batch.year else None, + "display_name": f"{batch.name} ({batch.year.year if batch.year else 'No year'})", + } + ) + + return JsonResponse({"success": True, "batches": batch_data}) + except Exception as e: + return JsonResponse({"success": False, "message": str(e)}, status=500) + + +@login_required +def get_students(request): + """View to get students for dropdowns""" + try: + students = Student.objects.all() + student_data = [ + {"id": student.id, "name": student.name, "phone": student.phone} + for student in students + ] + + return JsonResponse({"success": True, "students": student_data}) + except Exception as e: + return JsonResponse({"success": False, "message": str(e)}, status=500) + + +@login_required +def get_student(request, student_id): + """View to get a student's details by ID""" + # Check if user has permission + if ( + not isinstance(request.user, Staff) + or request.user.designation != "Admission Officer" + ): + return JsonResponse( + {"success": False, "message": "Permission denied"}, status=403 + ) + + try: + # Get student with related data + student = ( + Student.objects.select_related("course") + .prefetch_related("batches") + .get(id=student_id) + ) + + # Build batches data + batches = [] + for batch in student.batches.all(): + batches.append( + { + "id": batch.id, + "name": batch.name, + "year": batch.year.year if batch.year else None, + } + ) + + # Build student data + student_data = { + "id": student.id, + "name": student.name, + "phone": student.phone, + "email": student.email, + "gender": student.gender, + "birth_date": student.birth_date.strftime("%Y-%m-%d") + if student.birth_date + else None, + "status": student.status, + "parent_name": student.parent_name, + "parent_phone": student.parent_phone, + "permanent_address": student.permanent_address, + "temporary_address": student.temporary_address, + "citizenship_number": student.citizenship_no, + "current_period": student.current_period, + "joining_date": student.joining_date.strftime("%Y-%m-%d") + if student.joining_date + else None, + "image": student.image.url if student.image else None, + "course": {"id": student.course.id, "name": student.course.name} + if student.course + else None, + "batches": batches, + } + + return JsonResponse({"success": True, "student": student_data}) + except Student.DoesNotExist: + return JsonResponse( + {"success": False, "message": "Student not found"}, status=404 + ) + except Exception as e: + return JsonResponse({"success": False, "message": str(e)}, status=500) + + +@login_required +def edit_student(request): + """View to edit a student's details""" + if request.method != "POST": + return JsonResponse( + {"success": False, "message": "Invalid request method"}, status=405 + ) + + # Check if user has permission + if ( + not isinstance(request.user, Staff) + or request.user.designation != "Admission Officer" + ): + return JsonResponse( + {"success": False, "message": "Permission denied"}, status=403 + ) + + try: + # Get student ID from form data + student_id = request.POST.get("student_id") + if not student_id: + return JsonResponse( + {"success": False, "message": "Student ID is required"}, status=400 + ) + + # Get student object + student = get_object_or_404(Student, id=student_id) + + # Update student fields + student.name = request.POST.get("name", student.name) + student.phone = request.POST.get("phone", student.phone) + student.email = request.POST.get("email", student.email) + student.gender = request.POST.get("gender", student.gender) + + # Handle birth date + birth_date = request.POST.get("birth_date") + if birth_date: + student.birth_date = birth_date + + # Handle course + course_id = request.POST.get("course") + if course_id: + try: + course = Course.objects.get(id=course_id) + student.course = course + except Course.DoesNotExist: + pass + + # Handle status + status = request.POST.get("status") + if status: + student.status = status + + # Handle parent info + student.parent_name = request.POST.get("parent_name", student.parent_name) + student.parent_phone = request.POST.get("parent_phone", student.parent_phone) + + # Handle address + student.permanent_address = request.POST.get( + "permanent_address", student.permanent_address + ) + + # Handle citizenship + student.citizenship_no = request.POST.get( + "citizenship_number", student.citizenship_no + ) + + # Save student + student.save() + + # Handle batch + batch_id = request.POST.get("batches") + if batch_id: + try: + batch = Batch.objects.get(id=batch_id) + # Clear existing batches and add the new one + student.batches.clear() + student.batches.add(batch) + except Batch.DoesNotExist: + pass + + return JsonResponse( + {"success": True, "message": "Student updated successfully"} + ) + except Exception as e: + return JsonResponse({"success": False, "message": str(e)}, status=500) + + +@login_required +def get_course_tracking(request, tracking_id): + """View to get a course tracking's details by ID""" + # Check if user has permission + if ( + not isinstance(request.user, Staff) + or request.user.designation != "Admission Officer" + ): + return JsonResponse( + {"success": False, "message": "Permission denied"}, status=403 + ) + + try: + # Get tracking with related data + tracking = CourseTracking.objects.select_related("student", "course").get( + id=tracking_id + ) + + # Build tracking data + tracking_data = { + "id": tracking.id, + "student": {"id": tracking.student.id, "name": tracking.student.name}, + "course": {"id": tracking.course.id, "name": tracking.course.name}, + "start_date": tracking.start_date.strftime("%Y-%m-%d"), + "expected_end_date": tracking.expected_end_date.strftime("%Y-%m-%d") + if tracking.expected_end_date + else None, + "completion_percentage": tracking.completion_percentage, + "progress_status": tracking.progress_status, + "current_period": tracking.current_period, + "notes": tracking.notes, + "current_period_display": tracking.current_period_display, + } + + return JsonResponse({"success": True, "tracking": tracking_data}) + except CourseTracking.DoesNotExist: + return JsonResponse( + {"success": False, "message": "Course tracking not found"}, status=404 + ) + except Exception as e: + return JsonResponse({"success": False, "message": str(e)}, status=500) + + +@login_required +def edit_course_tracking(request): + """View to edit a course tracking""" + if request.method != "POST": + return JsonResponse( + {"success": False, "message": "Invalid request method"}, status=405 + ) + + # Check if user has permission + if ( + not isinstance(request.user, Staff) + or request.user.designation != "Admission Officer" + ): + return JsonResponse( + {"success": False, "message": "Permission denied"}, status=403 + ) + + try: + # Get tracking ID from form data + tracking_id = request.POST.get("tracking_id") + if not tracking_id: + return JsonResponse( + {"success": False, "message": "Tracking ID is required"}, status=400 + ) + + # Get tracking object + tracking = get_object_or_404(CourseTracking, id=tracking_id) + + # Update tracking fields + start_date = request.POST.get("start_date") + if start_date: + tracking.start_date = start_date + + expected_end_date = request.POST.get("expected_end_date") + if expected_end_date: + tracking.expected_end_date = expected_end_date + + current_period = request.POST.get("current_period") + if current_period: + tracking.current_period = int(current_period) + + progress_status = request.POST.get("progress_status") + if progress_status: + tracking.progress_status = progress_status + + completion_percentage = request.POST.get("completion_percentage") + if completion_percentage: + tracking.completion_percentage = int(completion_percentage) + + notes = request.POST.get("notes") + if notes is not None: # Allow empty notes + tracking.notes = notes + + # Save tracking + tracking.save() + + return JsonResponse( + {"success": True, "message": "Course tracking updated successfully"} + ) + except Exception as e: + return JsonResponse({"success": False, "message": str(e)}, status=500) diff --git a/app/apps.py b/app/apps.py index eb38975..0d606d9 100644 --- a/app/apps.py +++ b/app/apps.py @@ -1,9 +1,12 @@ +# Core Django imports from django.apps import AppConfig +from django.db.models.signals import post_migrate +from django.dispatch import receiver class AppConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'app' + default_auto_field = "django.db.models.BigAutoField" + name = "app" def ready(self): import app.signals # Import signals when app is ready diff --git a/app/auth.py b/app/auth.py index c945d79..fbe3170 100644 --- a/app/auth.py +++ b/app/auth.py @@ -1,11 +1,17 @@ -from django.shortcuts import render, redirect -from django.contrib.auth import login +# Core Django imports +from django.contrib.auth import authenticate, login +from django.shortcuts import redirect, render from django.views.decorators.csrf import csrf_exempt from django.contrib.auth.models import User from django.contrib.auth.hashers import make_password +from django.contrib import messages + + import uuid -from .models import Student, Staff -from .utils import ( + +# Local app imports +from app.models import Student, Staff, Parent, Institute +from app.utils import ( generate_otp, generate_secret_key, store_secret_key, @@ -16,27 +22,41 @@ from .utils import ( store_reset_token, verify_reset_token ) -from django.contrib import messages + + def reset_password_options(request): """View for selecting between phone and email password reset""" - return render(request, "login/reset_options.html") + # Get institute for logo + institute = Institute.objects.first() + return render(request, "login/reset_options.html", {"institute": institute}) def password_reset_phone(request): """View for initiating password reset with phone number""" + # Get institute for logo + institute = Institute.objects.first() + if request.method == "POST": phone = request.POST.get("phone") # Validate phone number if not phone or not phone.isdigit(): - return render(request, "login/password_reset_phone.html", {"error": "Please enter a valid phone number", "is_email": False}) + return render(request, "login/password_reset_phone.html", { + "error": "Please enter a valid phone number", + "is_email": False, + "institute": institute + }) # Check if a user with this phone exists student = Student.objects.filter(phone=phone).first() staff = Staff.objects.filter(phone=phone).first() if not student and not staff: - return render(request, "login/password_reset_phone.html", {"error": "No account found with this phone number", "is_email": False}) + return render(request, "login/password_reset_phone.html", { + "error": "No account found with this phone number", + "is_email": False, + "institute": institute + }) # Generate secret key and store it secret_key = generate_secret_key() @@ -48,14 +68,28 @@ def password_reset_phone(request): if send_result: # Redirect to OTP verification page - return render(request, "login/password_reset_otp.html", {"phone": phone, "is_email": False}) + return render(request, "login/password_reset_otp.html", { + "phone": phone, + "is_email": False, + "institute": institute + }) else: - return render(request, "login/password_reset_phone.html", {"error": "Failed to send OTP. Please try again.", "is_email": False}) + return render(request, "login/password_reset_phone.html", { + "error": "Failed to send OTP. Please try again.", + "is_email": False, + "institute": institute + }) - return render(request, "login/password_reset_phone.html", {"is_email": False}) + return render(request, "login/password_reset_phone.html", { + "is_email": False, + "institute": institute + }) def password_reset_phone_verify(request): """View for verifying OTP for password reset""" + # Get institute for logo + institute = Institute.objects.first() + if request.method == "POST": phone = request.POST.get("phone") otp = request.POST.get("otp") @@ -64,7 +98,8 @@ def password_reset_phone_verify(request): return render(request, "login/password_reset_otp.html", { "error": "Phone and OTP are required", "phone": phone, - "is_email": False + "is_email": False, + "institute": institute }) # Verify OTP @@ -78,13 +113,15 @@ def password_reset_phone_verify(request): return render(request, "login/password_reset_set.html", { "phone": phone, "token": token, - "is_email": False + "is_email": False, + "institute": institute }) else: return render(request, "login/password_reset_otp.html", { "error": "Invalid or expired OTP", "phone": phone, - "is_email": False + "is_email": False, + "institute": institute }) # If GET request, redirect to phone login @@ -123,19 +160,30 @@ def resend_phone_otp(request): def password_reset_email(request): """View for initiating password reset with email""" + # Get institute for logo + institute = Institute.objects.first() + if request.method == "POST": email = request.POST.get("email") # Validate email if not email: - return render(request, "login/email_reset.html", {"error": "Please enter a valid email address", "is_email": True}) + return render(request, "login/email_reset.html", { + "error": "Please enter a valid email address", + "is_email": True, + "institute": institute + }) # Check if a user with this email exists student = Student.objects.filter(email=email).first() staff = Staff.objects.filter(email=email).first() if not student and not staff: - return render(request, "login/email_reset.html", {"error": "No account found with this email address", "is_email": True}) + return render(request, "login/email_reset.html", { + "error": "No account found with this email address", + "is_email": True, + "institute": institute + }) # Get the user user = student or staff @@ -143,14 +191,28 @@ def password_reset_email(request): # Send password reset email if send_password_reset_email(user, request, email): # Redirect to verification page - return render(request, "login/password_reset_otp.html", {"email": email, "is_email": True}) + return render(request, "login/password_reset_otp.html", { + "email": email, + "is_email": True, + "institute": institute + }) else: - return render(request, "login/email_reset.html", {"error": "Failed to send verification code. Please try again.", "is_email": True}) + return render(request, "login/email_reset.html", { + "error": "Failed to send verification code. Please try again.", + "is_email": True, + "institute": institute + }) - return render(request, "login/email_reset.html", {"is_email": True}) + return render(request, "login/email_reset.html", { + "is_email": True, + "institute": institute + }) def password_reset_email_verify(request): """View for verifying reset code for email""" + # Get institute for logo + institute = Institute.objects.first() + if request.method == "POST": email = request.POST.get("email") code = request.POST.get("otp") @@ -159,7 +221,8 @@ def password_reset_email_verify(request): return render(request, "login/password_reset_otp.html", { "error": "Email and verification code are required", "email": email, - "is_email": True + "is_email": True, + "institute": institute }) # Verify code @@ -173,14 +236,16 @@ def password_reset_email_verify(request): return render(request, "login/password_reset_set.html", { "email": email, "token": token, - "is_email": True + "is_email": True, + "institute": institute }) else: return render(request, "login/password_reset_otp.html", { "error": "Invalid or expired code", "email": email, "is_email": True, - "message": "Please check your email for the verification code." + "message": "Please check your email for the verification code.", + "institute": institute }) # For GET request, show the verification form with email pre-filled @@ -188,7 +253,8 @@ def password_reset_email_verify(request): return render(request, "login/password_reset_otp.html", { "email": email, "is_email": True, - "message": "Please check your email for the verification code." + "message": "Please check your email for the verification code.", + "institute": institute }) def resend_email_code(request): @@ -229,6 +295,9 @@ def resend_email_code(request): def set_new_password(request): """View for setting new password after verification""" + # Get institute for logo + institute = Institute.objects.first() + if request.method == "POST": phone = request.POST.get("phone") email = request.POST.get("email") @@ -246,7 +315,8 @@ def set_new_password(request): "phone": phone, "email": email, "token": token, - "is_email": is_email + "is_email": is_email, + "institute": institute }) # Verify token is valid @@ -256,7 +326,8 @@ def set_new_password(request): "phone": phone, "email": email, "token": token, - "is_email": is_email + "is_email": is_email, + "institute": institute }) # Check passwords match @@ -266,7 +337,8 @@ def set_new_password(request): "phone": phone, "email": email, "token": token, - "is_email": is_email + "is_email": is_email, + "institute": institute }) # Find the user @@ -284,7 +356,8 @@ def set_new_password(request): "phone": phone, "email": email, "token": token, - "is_email": is_email + "is_email": is_email, + "institute": institute }) # Update password @@ -295,4 +368,40 @@ def set_new_password(request): messages.success(request, "Password has been reset successfully. Please login with your new password.") return redirect("login") - return render(request, "login/password_reset_set.html", {"is_email": False}) \ No newline at end of file + return render(request, "login/password_reset_set.html", {"is_email": False, "institute": institute}) + +def custom_login(request): + """Custom login view to handle authentication""" + if request.method == "POST": + username = request.POST.get("username") + password = request.POST.get("password") + + # Validate phone number format if it's a phone number + if username.isdigit(): + if len(username) != 10: + # Get institute for logo + institute = Institute.objects.first() + return render(request, "login/login.html", { + "error": "Phone number must be 10 digits", + "username": username, + "institute": institute + }) + + user = authenticate(request, username=username, password=password) + + if user is not None: + login(request, user) + next_url = request.POST.get("next", "/app/dashboard/") + return redirect(next_url) + else: + # Get institute for logo + institute = Institute.objects.first() + return render(request, "login/login.html", { + "error": "Invalid credentials", + "username": username, + "institute": institute + }) + + # Get institute for logo + institute = Institute.objects.first() + return render(request, "login/login.html", {"institute": institute}) diff --git a/app/backends.py b/app/backends.py index 66e20be..416b341 100644 --- a/app/backends.py +++ b/app/backends.py @@ -1,6 +1,11 @@ -from django.contrib.auth.models import User +# Core Django imports from django.contrib.auth.backends import ModelBackend -from .models import Staff, Student +from django.contrib.auth.models import Group, User +from django.db.models import Q + +# Local app imports +from app.models import Parent, Staff, Student + class MultiModelBackend(ModelBackend): def authenticate(self, request, username=None, password=None, **kwargs): @@ -9,30 +14,36 @@ class MultiModelBackend(ModelBackend): """ if username is None or password is None: return None - - # Try authenticating as Staff by phone - # Use filter().first() to avoid MultipleObjectsReturned error + + # Try authenticating as Staff by phone first staff = Staff.objects.filter(phone=username).first() - if staff and staff.check_password(password): - return staff + if staff: + if staff.check_password(password): + return staff # Try authenticating as Staff by email - # Use filter().first() to avoid MultipleObjectsReturned error staff = Staff.objects.filter(email=username).first() - if staff and staff.check_password(password): - return staff + if staff: + if staff.check_password(password): + return staff + + # Try authenticating as Parent by phone + parent = Parent.objects.filter(phone=username).first() + if parent: + if parent.check_password(password): + return parent # Try authenticating as Student by phone - # Use filter().first() to avoid MultipleObjectsReturned error student = Student.objects.filter(phone=username).first() - if student and student.check_password(password): - return student + if student: + if student.check_password(password): + return student # Try authenticating as Student by email - # Use filter().first() to handle multiple students with same email student = Student.objects.filter(email=username).first() - if student and student.check_password(password): - return student + if student: + if student.check_password(password): + return student # Return None if no match return None @@ -41,7 +52,7 @@ class MultiModelBackend(ModelBackend): """ Retrieve a user instance using their ID. """ - for model in [Staff, Student]: + for model in [Staff, Student, Parent]: try: return model.objects.get(pk=user_id) except model.DoesNotExist: @@ -54,16 +65,15 @@ class MultiModelBackend(ModelBackend): """ if not user_obj.is_active or user_obj.is_anonymous or obj is not None: return set() - + if isinstance(user_obj, (Staff, Student)): - if not hasattr(user_obj, '_group_perm_cache'): + if not hasattr(user_obj, "_group_perm_cache"): perms = set() for group in user_obj.groups.all(): perms.update( - "%s.%s" % (ct, name) + "%s.%s" % (ct, name) for ct, name in group.permissions.values_list( - 'content_type__app_label', - 'codename' + "content_type__app_label", "codename" ) ) user_obj._group_perm_cache = perms @@ -74,7 +84,7 @@ class MultiModelBackend(ModelBackend): def get_all_permissions(self, user_obj, obj=None): if not user_obj.is_active or user_obj.is_anonymous or obj is not None: return set() - + if isinstance(user_obj, (Staff, Student)): return self.get_group_permissions(user_obj, obj) else: @@ -83,7 +93,7 @@ class MultiModelBackend(ModelBackend): def has_perm(self, user_obj, perm, obj=None): if not user_obj.is_active: return False - + if isinstance(user_obj, (Staff, Student)): return perm in self.get_all_permissions(user_obj, obj) else: diff --git a/app/error_handlers.py b/app/error_handlers.py new file mode 100644 index 0000000..8c70f39 --- /dev/null +++ b/app/error_handlers.py @@ -0,0 +1,27 @@ +from django.shortcuts import render + + +# Error handler views +def handler400(request, exception=None): + """Bad request error handler""" + return render(request, "400.html", status=400) + + +def handler403(request, exception=None): + """Forbidden error handler""" + return render(request, "403.html", status=403) + + +def handler404(request, exception=None): + """Page not found error handler""" + return render(request, "404.html", status=404) + + +def handler500(request): + """Server error handler""" + return render(request, "500.html", status=500) + + +def handler505(request, exception=None): + """HTTP version not supported error handler""" + return render(request, "505.html", status=505) diff --git a/app/firebase.py b/app/firebase.py index 38009d2..6cb3d1e 100644 --- a/app/firebase.py +++ b/app/firebase.py @@ -6,13 +6,31 @@ from firebase_admin import messaging, credentials from student_management_system.settings import BASE_DIR +# -------------------------------------------------------------------- +# FCM Device Model +# -------------------------------------------------------------------- + + class FCMDevice(models.Model): """Model to store Firebase Cloud Messaging device tokens""" + id = models.BigAutoField(primary_key=True) token = models.TextField(unique=True) created_at = models.DateTimeField(auto_now_add=True) last_active = models.DateTimeField(auto_now=True) is_active = models.BooleanField(default=True) + is_fallback = models.BooleanField(default=False) # Flag to identify fallback tokens + user_type = models.CharField( + max_length=20, + choices=[ + ("student", "Student"), + ("parent", "Parent"), + ("teacher", "Teacher"), + ("admin", "Admin"), + ("unknown", "Unknown"), + ], + default="unknown", + ) # Add user type field def __str__(self): return f"Device {self.id} ({self.token[:20]}...)" @@ -20,7 +38,7 @@ class FCMDevice(models.Model): class Meta: verbose_name = "FCM Device" verbose_name_plural = "FCM Devices" - ordering = ['-last_active'] + ordering = ["-last_active"] def deactivate(self): """Deactivate this device token""" @@ -28,21 +46,64 @@ class FCMDevice(models.Model): self.save() +# -------------------------------------------------------------------- +# Token Management +# -------------------------------------------------------------------- + + def save_fcm_token(request, token): """Save FCM token to database""" try: - # Delete any existing tokens for this user/device - FCMDevice.objects.filter(token=token).delete() - # Create new token - FCMDevice.objects.create(token=token) - return JsonResponse({'status': 'success'}) + # Check if this is a fallback token + is_fallback = token.startswith("fcm-token-") or token.startswith( + "fallback-token-" + ) + + # Determine user type if a user is authenticated + user_type = "unknown" + if hasattr(request, "user") and request.user.is_authenticated: + user_groups = request.user.groups.values_list("name", flat=True) + if "Student" in user_groups: + user_type = "student" + elif "Parent" in user_groups: + user_type = "parent" + elif "Teacher" in user_groups: + user_type = "teacher" + elif request.user.is_superuser: + user_type = "admin" + + # Use update_or_create to avoid unnecessary deletes + device, created = FCMDevice.objects.update_or_create( + token=token, + defaults={ + "is_fallback": is_fallback, + "is_active": True, + "user_type": user_type, + }, + ) + + return JsonResponse({"status": "success"}) except Exception as e: - print(f"Error saving FCM token: {str(e)}") - return JsonResponse({'error': str(e)}, status=500) + return JsonResponse({"error": str(e)}, status=500) + + +# -------------------------------------------------------------------- +# Firebase Configuration +# -------------------------------------------------------------------- def get_firebase_js(): """Generate Firebase JavaScript configuration""" + firebase_config = { + "apiKey": os.getenv("FIREBASE_API_KEY", ""), + "authDomain": os.getenv("FIREBASE_AUTH_DOMAIN", ""), + "projectId": os.getenv("FIREBASE_PROJECT_ID", ""), + "storageBucket": os.getenv("FIREBASE_STORAGE_BUCKET", ""), + "messagingSenderId": os.getenv("FIREBASE_MESSAGING_SENDER_ID", ""), + "appId": os.getenv("FIREBASE_APP_ID", ""), + "measurementId": os.getenv("FIREBASE_MEASUREMENT_ID", ""), + } + data = """ importScripts('https://www.gstatic.com/firebasejs/11.0.2/firebase-app-compat.js'); importScripts('https://www.gstatic.com/firebasejs/11.0.2/firebase-messaging-compat.js'); @@ -69,25 +130,35 @@ def get_firebase_js(): return self.registration.showNotification(notificationTitle, notificationOptions); }); """ % ( - os.getenv("FIREBASE_API_KEY", ""), - os.getenv("FIREBASE_AUTH_DOMAIN", ""), - os.getenv("FIREBASE_PROJECT_ID", ""), - os.getenv("FIREBASE_STORAGE_BUCKET", ""), - os.getenv("FIREBASE_MESSAGING_SENDER_ID", ""), - os.getenv("FIREBASE_APP_ID", ""), - os.getenv("FIREBASE_MEASUREMENT_ID", "") + firebase_config["apiKey"], + firebase_config["authDomain"], + firebase_config["projectId"], + firebase_config["storageBucket"], + firebase_config["messagingSenderId"], + firebase_config["appId"], + firebase_config["measurementId"], ) return HttpResponse(data, content_type="application/javascript") -# Sending Firebase Push Notification +# -------------------------------------------------------------------- +# Firebase Admin Initialization +# -------------------------------------------------------------------- + +# Initialize Firebase Admin +firebase_app = None try: - cred = credentials.Certificate(os.path.join(BASE_DIR, "firebase-key.json")) - firebase_app = firebase_admin.initialize_app(cred) -except (ValueError, FileNotFoundError) as e: - print(f"Warning: Could not initialize Firebase Admin: {e}") -except firebase_admin.exceptions.FirebaseError as e: - print(f"Warning: Firebase Admin initialization error: {e}") + cert_path = os.path.join(BASE_DIR, "firebase-key.json") + if os.path.exists(cert_path): + cred = credentials.Certificate(cert_path) + firebase_app = firebase_admin.initialize_app(cred) +except Exception: + pass + + +# -------------------------------------------------------------------- +# Push Notification Functions +# -------------------------------------------------------------------- def send_push_notification(title, message, tokens): @@ -95,22 +166,41 @@ def send_push_notification(title, message, tokens): Send push notification using Firebase Cloud Messaging Returns a tuple of (success_count, failure_count, failed_tokens) """ + if not firebase_app or not tokens: + return 0, len(tokens) if tokens else 0, tokens + if not isinstance(tokens, (list, tuple)): tokens = list(tokens) # Convert QuerySet to list - + success_count = 0 failure_count = 0 failed_tokens = [] - - # Get only active devices + + # Get only active devices that are not fallback tokens active_devices = FCMDevice.objects.filter( token__in=tokens, - is_active=True + is_active=True, + is_fallback=False, # Skip fallback tokens for push notifications ) - + + # Count fallback tokens as "success" for reporting purposes + fallback_count = FCMDevice.objects.filter( + token__in=tokens, is_active=True, is_fallback=True + ).count() + + success_count += fallback_count + for device in active_devices: try: - response = messaging.send( + # Skip tokens that match our fallback pattern + if device.token.startswith("fcm-token-") or device.token.startswith( + "fallback-token-" + ): + device.is_fallback = True + device.save() + continue + + messaging.send( messaging.Message( notification=messaging.Notification( title=title, @@ -119,17 +209,14 @@ def send_push_notification(title, message, tokens): token=str(device.token), # Ensure token is a string ) ) - print(f"Successfully sent message: {response}") success_count += 1 except messaging.UnregisteredError: # Token is no longer valid, deactivate the device - print(f"Token {device.token} is no longer valid, deactivating device") device.deactivate() failure_count += 1 failed_tokens.append(device.token) - except Exception as e: - print(f"Failed to send message to token {device.token}: {e}") + except Exception: failure_count += 1 failed_tokens.append(device.token) - + return success_count, failure_count, failed_tokens diff --git a/app/forms.py b/app/forms.py index 4c92070..237640e 100644 --- a/app/forms.py +++ b/app/forms.py @@ -1,65 +1,174 @@ -from django import forms -from django.contrib.auth.hashers import make_password -from django.contrib.auth.forms import SetPasswordForm -from .models import Staff, Student - - -class CustomSetPasswordForm(SetPasswordForm): - def save(self, commit=True): - user = self.user - user.set_password(self.cleaned_data["new_password1"]) - if commit: - user.save() - return user - - -class StudentAdminForm(forms.ModelForm): - new_password = forms.CharField( - required=False, - widget=forms.PasswordInput, - help_text="Enter a new password if you want to reset it. Leave blank otherwise. Default is '123'.", - ) - - class Meta: - model = Student - fields = "__all__" - - def save(self, commit=True): - student = super().save(commit=False) - new_password = self.cleaned_data.get("new_password") - - if new_password: - student.password = make_password(new_password) - # Set default password to '123' for new students if no password provided - elif not student.pk and not student.password: # New student without password - student.password = make_password("123") - - if commit: - student.save() - return student - - -class StaffAdminForm(forms.ModelForm): - new_password = forms.CharField( - required=False, - widget=forms.PasswordInput, - help_text="Enter a new password if you want to reset it. Leave blank otherwise. Default is '123'.", - ) - - class Meta: - model = Staff - fields = "__all__" - - def save(self, commit=True): - staff = super().save(commit=False) - new_password = self.cleaned_data.get("new_password") - - if new_password: - staff.password = make_password(new_password) - # Set default password to '123' for new staff if no password provided - elif not staff.pk and not staff.password: # New staff without password - staff.password = make_password("123") - - if commit: - staff.save() - return staff +from django import forms +from django.contrib.auth.hashers import make_password +from django.contrib.auth.forms import SetPasswordForm +from app.models import ( + Staff, + Student, + Parent, + StaffInstituteFeedback, + ParentInstituteFeedback, +) +from django.contrib.auth.models import Group + + +class CustomSetPasswordForm(SetPasswordForm): + def save(self, commit=True): + user = self.user + user.set_password(self.cleaned_data["new_password1"]) + if commit: + user.save() + return user + + +class StudentAdminForm(forms.ModelForm): + new_password = forms.CharField( + required=False, + widget=forms.PasswordInput, + help_text="Enter a new password if you want to reset it. Leave blank otherwise. Default is '123'.", + ) + + class Meta: + model = Student + fields = "__all__" + + def save(self, commit=True): + student = super().save(commit=False) + new_password = self.cleaned_data.get("new_password") + + if new_password: + student.set_password(new_password) + # Set default password to '123' for new students if no password provided + elif not student.pk and not student.password: # New student without password + student.set_password("123") + + if commit: + student.save() + + # Make sure student has Student group + if ( + hasattr(student, "groups") + and not student.groups.filter(name="Student").exists() + ): + student_group = Group.objects.filter(name="Student").first() + if student_group: + student.groups.add(student_group) + + # Handle parent creation/update for both new and existing students + if student.parent_name and student.parent_phone: + try: + # Check if parent already exists + parent = Parent.objects.filter(phone=student.parent_phone).first() + + if not parent: + # Create new parent + parent = Parent.objects.create( + name=student.parent_name, + phone=student.parent_phone, + password=make_password("123"), # Default password + ) + + # Add Parent group to new parent + parent_group = Group.objects.filter(name="Parent").first() + if parent_group: + parent.groups.add(parent_group) + + # Add student to parent's students + parent.students.add(student) + + except Exception as e: + # Log the error but don't prevent student creation + print(f"Error creating/updating parent: {str(e)}") + + return student + + +class StaffAdminForm(forms.ModelForm): + new_password = forms.CharField( + required=False, + widget=forms.PasswordInput, + help_text="Enter a new password if you want to reset it. Leave blank otherwise. Default is '123'.", + ) + + class Meta: + model = Staff + fields = "__all__" # Include all fields, including groups + + def save(self, commit=True): + staff = super().save(commit=False) + new_password = self.cleaned_data.get("new_password") + + if new_password: + staff.password = make_password(new_password) + # Set default password to '123' for new staff if no password provided + elif not staff.pk and not staff.password: # New staff without password + staff.password = make_password("123") + + if commit: + staff.save() + self.save_m2m() # Save many-to-many relationships including groups + + # If groups were not selected in the form, ensure staff has Teacher group + if not staff.groups.exists(): + teacher_group = Group.objects.filter(name="Teacher").first() + if teacher_group: + staff.groups.add(teacher_group) + + return staff + + +class ParentAdminForm(forms.ModelForm): + new_password = forms.CharField( + required=False, + widget=forms.PasswordInput, + help_text="Enter a new password if you want to reset it. Leave blank otherwise. Default is '123'.", + ) + + class Meta: + model = Parent + fields = "__all__" + + def save(self, commit=True): + parent = super().save(commit=False) + new_password = self.cleaned_data.get("new_password") + + if new_password: + parent.password = make_password(new_password) + # Set default password to '123' for new parents if no password provided + elif not parent.pk and not parent.password: # New parent without password + parent.password = make_password("123") + + if commit: + parent.save() + return parent + + +class StaffInstituteFeedbackForm(forms.ModelForm): + class Meta: + model = StaffInstituteFeedback + fields = [ + "feedback_type", + "rating", + "feedback_text", + "is_anonymous", + "is_public", + ] + widgets = { + "feedback_text": forms.Textarea(attrs={"rows": 4}), + "rating": forms.NumberInput(attrs={"step": "0.5", "min": "1", "max": "5"}), + } + + +class ParentInstituteFeedbackForm(forms.ModelForm): + class Meta: + model = ParentInstituteFeedback + fields = [ + "feedback_type", + "rating", + "feedback_text", + "is_anonymous", + "is_public", + ] + widgets = { + "feedback_text": forms.Textarea(attrs={"rows": 4}), + "rating": forms.NumberInput(attrs={"step": "0.5", "min": "1", "max": "5"}), + } diff --git a/app/hodviews.py b/app/hodviews.py new file mode 100644 index 0000000..1111a27 --- /dev/null +++ b/app/hodviews.py @@ -0,0 +1,1929 @@ +from django.shortcuts import render, get_object_or_404, redirect +from django.contrib.auth.decorators import login_required +from django.db.models import Count, Avg, Q +from django.http import JsonResponse +from django.utils import timezone +from datetime import datetime, timedelta, time +from django.contrib import messages +from django.urls import reverse +from django.http import HttpResponseRedirect +from django.contrib.auth.hashers import make_password +from django.contrib.auth.models import Group +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_http_methods + +from app.firebase import send_push_notification, FCMDevice + +from app.models import ( + Staff, + Student, + Course, + Subject, + Routine, + Attendance, + AttendanceRecord, + StudentFeedback, + CourseTracking, + TeacherParentMeeting, + Batch, + Parent, + Notice, +) + + +@login_required +def hod_dashboard(request): + """HOD dashboard view showing department overview and management options""" + try: + # Check if user is a HOD + if not request.user.groups.filter(name="HOD").exists(): + messages.error(request, "Only HODs can access this dashboard.") + return redirect("hodDashboard") + + # Get the HOD's department (course) + hod = request.user + if not hasattr(hod, "course") or not hod.course: + messages.error(request, "You are not assigned as HOD of any department.") + return redirect("hodDashboard") + + course = hod.course + + # Get department statistics + total_students = Student.objects.filter(course=course).count() + total_staff = Staff.objects.filter(course=course).count() + total_subjects = Subject.objects.filter(course=course).count() + + # Get all subjects for the course + subjects = Subject.objects.filter(course=course).order_by('period_or_year', 'name') + + # Get routines to find teachers for each subject + routines = Routine.objects.filter(course=course, is_active=True).select_related( + "subject", "teacher" + ) + + # Create a mapping of subject_id to teacher + teacher_map = {} + for routine in routines: + teacher_map[routine.subject_id] = routine.teacher + + # Get all teachers who teach in this course through active routines + staff = Staff.objects.filter( + id__in=routines.values_list('teacher_id', flat=True) + ).prefetch_related( + 'routine_set', + 'routine_set__subject' + ).distinct() + + # Filter routines for each staff member to only show subjects from this course + for staff_member in staff: + staff_member.filtered_subjects = Subject.objects.filter( + course=course, + id__in=staff_member.routine_set.filter(is_active=True).values_list('subject_id', flat=True) + ) + + # Get today's classes and their attendance + today = timezone.now().date() + today_classes = Routine.objects.filter( + course=course, + is_active=True + ).select_related("subject", "teacher") + + # Get attendance for today's classes + today_attendance = Attendance.objects.filter( + routine__course=course, + date=today + ).select_related("routine") + + # Create a dictionary of routine_id to attendance + attendance_map = {att.routine_id: att for att in today_attendance} + + # Attach attendance to each class + for class_obj in today_classes: + class_obj.attendance = attendance_map.get(class_obj.id) + + # Get all student progress records for display in the progress section + student_progress_all = ( + CourseTracking.objects.filter(student__course=course) + .select_related("student") + .order_by("-completion_percentage") + ) + + # Calculate progress metrics + avg_completion = student_progress_all.aggregate(Avg('completion_percentage'))['completion_percentage__avg'] or 0 + avg_completion = round(avg_completion, 1) + students_on_track = student_progress_all.filter(completion_percentage__gte=75).count() + students_at_risk = student_progress_all.filter(completion_percentage__lt=40).count() + + # For the dashboard overview, still show just top 5 + student_progress = student_progress_all[:5] + + # Get all meetings + meetings = TeacherParentMeeting.objects.all().order_by("-meeting_date", "-meeting_time") + + # Get recent feedback + recent_feedback = ( + StudentFeedback.objects.filter(student__course=course) + .select_related("student", "teacher") + .order_by("-created_at")[:5] + ) + + # Get all students in the course + students = Student.objects.filter(course=course).order_by('name') + + # Get all active batches + batches = Batch.objects.filter(is_active=True).order_by('-year') + + # Get notices + notices = Notice.objects.all().order_by('-created_at') + + context = { + "title": "HOD Dashboard", + "hod": hod, + "course": course, + "total_students": total_students, + "total_staff": total_staff, + "total_subjects": total_subjects, + "today_classes": today_classes, + "student_progress": student_progress, + "student_progress_all": student_progress_all, # All student progress records + "avg_completion": avg_completion, + "students_on_track": students_on_track, + "students_at_risk": students_at_risk, + "meetings": meetings, # Changed from upcoming_meetings to all meetings + "recent_feedback": recent_feedback, + "subjects": subjects, + "staff": staff, + "students": students, + "batches": batches, + "notices": notices, + "class_routines": routines, # Add routines to the context + } + + return render(request, "hod/dashboard.html", context) + + except Exception as e: + messages.error(request, f"An error occurred: {str(e)}") + return redirect("hodDashboard") + +@login_required +def add_subject(request): + """Add a new subject to the HOD's course""" + try: + # Check if user is a HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can add subjects."}) + + # Get the HOD's course + hod = request.user + if not hasattr(hod, "course") or not hod.course: + return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."}) + + course = hod.course + + if request.method == "POST": + # Get form data + name = request.POST.get("name") + code = request.POST.get("code") + period_or_year = request.POST.get("period_or_year") + + # Validate required fields + if not name or not period_or_year: + return JsonResponse({"success": False, "message": "Subject name and period/year are required."}) + + try: + period_or_year = int(period_or_year) + except ValueError: + return JsonResponse({"success": False, "message": "Invalid period/year value."}) + + # Validate period/year against course duration + max_periods = course.duration * 2 if course.duration_type == "Semester" else course.duration + if period_or_year < 1 or period_or_year > max_periods: + return JsonResponse({"success": False, "message": f"Period/Year must be between 1 and {max_periods}."}) + + # Create the subject + subject = Subject( + name=name, + code=code, + period_or_year=period_or_year, + course=course + ) + + # Handle syllabus PDF if provided + if request.FILES.get('syllabus_pdf'): + subject.syllabus_pdf = request.FILES['syllabus_pdf'] + + subject.save() + + return JsonResponse({"success": True, "message": "Subject added successfully."}) + + return JsonResponse({"success": False, "message": "Invalid request method."}) + + except Exception as e: + return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"}) + +@login_required +def view_subject(request, subject_id): + """View details of a specific subject""" + try: + # Check if user is a HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can view subject details."}) + + # Get the HOD's course + hod = request.user + if not hasattr(hod, "course") or not hod.course: + return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."}) + + # Get the subject and ensure it belongs to the HOD's course + subject = get_object_or_404(Subject, id=subject_id, course=hod.course) + + # Get the assigned teacher from routine if any + teacher = None + routine = Routine.objects.filter(subject=subject, is_active=True).first() + if routine: + teacher = routine.teacher + + # Prepare response data + response_data = { + "success": True, + "subject": { + "id": subject.id, + "name": subject.name, + "code": subject.code or "N/A", + "course": { + "id": subject.course.id, + "name": subject.course.name, + "duration_type": subject.course.duration_type, + "duration": subject.course.duration + }, + "period_or_year": subject.period_or_year, + "teacher": { + "id": teacher.id, + "name": teacher.name + } if teacher else None, + "syllabus_pdf": subject.syllabus_pdf.url if subject.syllabus_pdf else None + } + } + + return JsonResponse(response_data) + + except Exception as e: + return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"}) + +@login_required +def edit_subject(request, subject_id): + """Edit an existing subject""" + try: + # Check if user is a HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can edit subjects."}) + + # Get the HOD's course + hod = request.user + if not hasattr(hod, "course") or not hod.course: + return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."}) + + # Get the subject and ensure it belongs to the HOD's course + subject = get_object_or_404(Subject, id=subject_id, course=hod.course) + + if request.method == "POST": + # Get form data + name = request.POST.get("name") + code = request.POST.get("code") + period_or_year = request.POST.get("period_or_year") + teacher_id = request.POST.get("teacher") + description = request.POST.get("description") + + # Validate required fields + if not name or not period_or_year: + return JsonResponse({"success": False, "message": "Subject name and period/year are required."}) + + try: + period_or_year = int(period_or_year) + except ValueError: + return JsonResponse({"success": False, "message": "Invalid period/year value."}) + + # Validate period/year against course duration + max_periods = subject.course.duration * 2 if subject.course.duration_type == "Semester" else subject.course.duration + if period_or_year < 1 or period_or_year > max_periods: + return JsonResponse({"success": False, "message": f"Period/Year must be between 1 and {max_periods}."}) + + # Update subject details + subject.name = name + subject.code = code + subject.period_or_year = period_or_year + subject.description = description + + # Handle syllabus PDF upload if provided + if request.FILES.get('syllabus_pdf'): + subject.syllabus_pdf = request.FILES['syllabus_pdf'] + + subject.save() + + # Update or create routine for teacher assignment + if teacher_id: + try: + teacher = Staff.objects.get(id=teacher_id, course=hod.course) + routine, created = Routine.objects.get_or_create( + subject=subject, + course=hod.course, + defaults={ + 'teacher': teacher, + 'is_active': True + } + ) + if not created: + routine.teacher = teacher + routine.save() + except Staff.DoesNotExist: + return JsonResponse({"success": False, "message": "Invalid teacher selected."}) + + return JsonResponse({"success": True, "message": "Subject updated successfully."}) + + return JsonResponse({"success": False, "message": "Invalid request method."}) + + except Exception as e: + return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"}) + +@login_required(login_url='login') +def delete_subject(request, subject_id): + try: + subject = Subject.objects.get(id=subject_id) + # Check if the subject is being used in any routines + if Routine.objects.filter(subject=subject).exists(): + messages.error(request, 'Cannot delete subject as it is being used in routines.') + return redirect('hodDashboard') + + # Delete the subject + subject.delete() + messages.success(request, 'Subject deleted successfully.') + return redirect('hodDashboard') + except Subject.DoesNotExist: + messages.error(request, 'Subject not found.') + return redirect('hodDashboard') + except Exception as e: + messages.error(request, f'Error deleting subject: {str(e)}') + return redirect('hodDashboard') + +@login_required +def add_staff(request): + """Add a new staff member""" + try: + # Check if user is a HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can add staff members."}) + + # Get the HOD's course + hod = request.user + if not hasattr(hod, "course") or not hod.course: + return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."}) + + course = hod.course + + if request.method == "POST": + # Get form data + name = request.POST.get("name") + email = request.POST.get("email") + phone = request.POST.get("phone") + gender = request.POST.get("gender") + birth_date = request.POST.get("birth_date") + temporary_address = request.POST.get("temporary_address") + permanent_address = request.POST.get("permanent_address") + marital_status = request.POST.get("marital_status") + citizenship_no = request.POST.get("citizenship_no") + subject_ids = request.POST.getlist("subjects[]") # Updated to match the form field name + + # Validate required fields + if not name or not phone: + return JsonResponse({"success": False, "message": "Name and phone are required fields."}) + + if not subject_ids: + return JsonResponse({"success": False, "message": "Please assign at least one subject."}) + + # Check if phone number already exists + if Staff.objects.filter(phone=phone).exists(): + return JsonResponse({"success": False, "message": "Phone number already exists."}) + + # Create the staff member with default password and current date as joining date + staff = Staff( + name=name, + email=email, + phone=phone, + gender=gender, + birth_date=birth_date if birth_date else None, + temporary_address=temporary_address, + permanent_address=permanent_address, + marital_status=marital_status, + citizenship_no=citizenship_no, + password=make_password("123"), # Default password + is_active=True, + joining_date=timezone.now().date(), # Set joining date to current date + course=course, # Assign to HOD's course + designation="Teacher" # Set designation as Teacher + ) + + staff.save() + + # Add to Teacher group + try: + teacher_group = Group.objects.get(name="Teacher") + staff.groups.add(teacher_group) + except Group.DoesNotExist: + # Create Teacher group if it doesn't exist + teacher_group = Group.objects.create(name="Teacher") + staff.groups.add(teacher_group) + + # Create routines for assigned subjects with default time slots + default_start_time = time(10, 0) # 10:00 AM + default_end_time = time(11, 0) # 11:00 AM + + for subject_id in subject_ids: + try: + subject = Subject.objects.get(id=subject_id, course=course) + Routine.objects.create( + teacher=staff, + subject=subject, + course=course, + period_or_year=subject.period_or_year, + start_time=default_start_time, + end_time=default_end_time, + is_active=True + ) + except Subject.DoesNotExist: + continue + + return JsonResponse({"success": True, "message": "Staff member added successfully."}) + + return JsonResponse({"success": False, "message": "Invalid request method."}) + + except Exception as e: + return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"}) + +@login_required +def view_staff(request, staff_id): + """View details of a staff member""" + try: + # Check if user is a HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can view staff details."}) + + # Get the HOD's course + hod = request.user + if not hasattr(hod, "course") or not hod.course: + return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."}) + + course = hod.course + + # Get the staff member and ensure they belong to the HOD's course + staff = get_object_or_404(Staff, id=staff_id) + + # Get only the subjects from this course taught by this staff member + subjects = Subject.objects.filter( + course=course, + id__in=Routine.objects.filter(teacher=staff, is_active=True).values_list('subject_id', flat=True) + ) + + # Prepare response data with all staff fields + response_data = { + "success": True, + "staff": { + "id": staff.id, + "name": staff.name, + "email": staff.email, + "phone": staff.phone, + "designation": staff.designation, + "gender": staff.gender, + "birth_date": staff.birth_date.strftime("%Y-%m-%d") if staff.birth_date else None, + "temporary_address": staff.temporary_address, + "permanent_address": staff.permanent_address, + "marital_status": staff.marital_status, + "parent_name": staff.parent_name, + "parent_phone": staff.parent_phone, + "citizenship_no": staff.citizenship_no, + "passport": staff.passport, + "image": staff.image.url if staff.image else None, + "joining_date": staff.joining_date.strftime("%Y-%m-%d") if staff.joining_date else None, + "is_active": staff.is_active, + "subjects": [{"id": subject.id, "name": subject.name} for subject in subjects] + } + } + + return JsonResponse(response_data) + + except Exception as e: + print(f"Error in view_staff: {str(e)}") # Debug log + return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"}) + +@login_required +def edit_staff(request, staff_id): + """Edit an existing staff member""" + try: + # Check if user is a HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can edit staff members."}) + + # Get the HOD's course + hod = request.user + if not hasattr(hod, "course") or not hod.course: + return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."}) + + # Get the staff member and ensure they belong to the HOD's course + staff = get_object_or_404(Staff, id=staff_id) + + if request.method == "POST": + # Get form data + name = request.POST.get("name") + email = request.POST.get("email") + phone = request.POST.get("phone") + is_active = request.POST.get("is_active") == "true" + + # Validate required fields + if not name or not phone: + return JsonResponse({"success": False, "message": "Name and phone are required."}) + + # Check if phone number already exists for other staff members + if Staff.objects.filter(phone=phone).exclude(id=staff_id).exists(): + return JsonResponse({"success": False, "message": "Phone number already exists."}) + + # Update staff details + staff.name = name + staff.email = email + staff.phone = phone + staff.is_active = is_active + + # Handle profile picture upload if provided + if request.FILES.get('image'): + staff.image = request.FILES['image'] + + staff.save() + + return JsonResponse({"success": True, "message": "Staff member updated successfully."}) + + return JsonResponse({"success": False, "message": "Invalid request method."}) + + except Exception as e: + return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"}) + +@login_required +def delete_staff(request, staff_id): + """Delete a staff member""" + try: + # Check if user is a HOD + if not request.user.groups.filter(name="HOD").exists(): + messages.error(request, "Only Head of Department can delete staff members") + return redirect("hodDashboard") + + # Get the HOD's course + hod = request.user + if not hasattr(hod, "course") or not hod.course: + messages.error(request, "You are not assigned as HOD of any department") + return redirect("hodDashboard") + + # Get the staff member + staff = get_object_or_404(Staff, id=staff_id) + + # Check if staff belongs to HOD's course + if staff.course != hod.course: + messages.error(request, "You can only delete staff members from your department") + return redirect("hodDashboard") + + # Delete all routines associated with the staff + staff_routines = Routine.objects.filter(teacher=staff) + staff_routines.delete() + + # Store staff name for the success message + staff_name = staff.name + + # Delete the staff member + staff.delete() + + # Add success message + messages.success(request, f'Staff member {staff_name} has been deleted successfully') + + # Redirect back to dashboard + return redirect("hodDashboard") + + except Staff.DoesNotExist: + messages.error(request, "Staff member not found") + return redirect("hodDashboard") + except Exception as e: + messages.error(request, "An error occurred while deleting the staff member") + return redirect("hodDashboard") + +@login_required +def add_student(request): + """Add a new student to the HOD's course""" + try: + # Check if user is a HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can add students."}) + + # Get the HOD's course + hod = request.user + if not hasattr(hod, "course") or not hod.course: + return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."}) + + course = hod.course + + if request.method == "POST": + # Get form data + name = request.POST.get("name") + phone = request.POST.get("phone") + email = request.POST.get("email") + gender = request.POST.get("gender") + birth_date = request.POST.get("birth_date") + batch_id = request.POST.get("batch") + status = request.POST.get("status") + temporary_address = request.POST.get("temporary_address") + permanent_address = request.POST.get("permanent_address") + parent_name = request.POST.get("parent_name") + parent_phone = request.POST.get("parent_phone") + parent_address = request.POST.get("parent_address") + citizenship_no = request.POST.get("citizenship_no") + + # Validate required fields + if not name or not phone or not batch_id or not status or not parent_phone: + return JsonResponse({"success": False, "message": "Name, phone, batch, status, and parent phone are required."}) + + # Check if phone already exists + if Student.objects.filter(phone=phone).exists(): + return JsonResponse({"success": False, "message": "Phone number already exists."}) + + try: + # Get the batch + batch = Batch.objects.get(id=batch_id) + + # Create or get parent account + try: + parent = Parent.objects.get(phone=parent_phone) + except Parent.DoesNotExist: + parent = Parent( + name=parent_name, + phone=parent_phone, + address=parent_address, + password=make_password("123") # Default password + ) + parent.save() + + # Add parent to Parent group + try: + parent_group = Group.objects.get(name="Parent") + parent.groups.add(parent_group) + except Group.DoesNotExist: + parent_group = Group.objects.create(name="Parent") + parent.groups.add(parent_group) + + # Create the student + student = Student( + name=name, + phone=phone, + email=email, + gender=gender, + birth_date=birth_date if birth_date else None, + joining_date=batch.year if batch.year else timezone.now().date(), + temporary_address=temporary_address, + permanent_address=permanent_address, + parent_name=parent_name, + parent_phone=parent_phone, + citizenship_no=citizenship_no, + course=course, + status=status, + is_active=True, + password=make_password("123") # Set default password to "123" + ) + + # Save the student first to get an ID + student.save() + + # Add student to batch + student.batches.add(batch) + + # Add student to Student group + try: + student_group = Group.objects.get(name="Student") + student.groups.add(student_group) + except Group.DoesNotExist: + student_group = Group.objects.create(name="Student") + student.groups.add(student_group) + + # Associate student with parent + parent.students.add(student) + + return JsonResponse({ + "success": True, + "message": "Student added successfully. Default password is: 123" + }) + + except Batch.DoesNotExist: + return JsonResponse({ + "success": False, + "message": "Selected batch does not exist." + }) + except Exception as e: + return JsonResponse({ + "success": False, + "message": f"Error creating student: {str(e)}" + }) + + return JsonResponse({"success": False, "message": "Invalid request method."}) + + except Exception as e: + return JsonResponse({ + "success": False, + "message": f"An error occurred: {str(e)}" + }) + +@login_required +def get_student(request, student_id): + """Get student details""" + try: + # Check if user is a HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can view student details."}) + + # Get the HOD's course + hod = request.user + if not hasattr(hod, "course") or not hod.course: + return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."}) + + # Get the student and ensure they belong to the HOD's course + student = get_object_or_404(Student, id=student_id) + + # Prepare response data + response_data = { + "success": True, + "student": { + "id": student.id, + "name": student.name, + "email": student.email, + "phone": student.phone, + "gender": student.gender, + "birth_date": student.birth_date.strftime("%Y-%m-%d") if student.birth_date else None, + "joining_date": student.joining_date.strftime("%Y-%m-%d") if student.joining_date else None, + "temporary_address": student.temporary_address, + "permanent_address": student.permanent_address, + "parent_name": student.parent_name, + "parent_phone": student.parent_phone, + "citizenship_no": student.citizenship_no, + "image": student.image.url if student.image else None, + "course_name": student.course.name if student.course else None, + "current_period": student.current_period, + "is_active": student.is_active + } + } + + return JsonResponse(response_data) + + except Exception as e: + print(f"Error in get_student: {str(e)}") # Debug log + return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"}) + +@login_required +def edit_student(request, student_id): + """Edit an existing student""" + try: + # Check if user is a HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can edit students."}) + + # Get the HOD's course + hod = request.user + if not hasattr(hod, "course") or not hod.course: + return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."}) + + # Get the student and ensure they belong to the HOD's course + student = get_object_or_404(Student, id=student_id, course=hod.course) + + if request.method == "POST": + # Get form data + name = request.POST.get("name") + email = request.POST.get("email") + phone = request.POST.get("phone") + gender = request.POST.get("gender") + birth_date = request.POST.get("birth_date") + batch_id = request.POST.get("batch") + temporary_address = request.POST.get("temporary_address") + permanent_address = request.POST.get("permanent_address") + parent_name = request.POST.get("parent_name") + parent_phone = request.POST.get("parent_phone") + citizenship_no = request.POST.get("citizenship_no") + + # Validate required fields + if not name or not phone: + return JsonResponse({"success": False, "message": "Name and phone are required fields."}) + + # Check if email or phone already exists for other students + if email and Student.objects.filter(email=email).exclude(id=student_id).exists(): + return JsonResponse({"success": False, "message": "Email already exists."}) + if Student.objects.filter(phone=phone).exclude(id=student_id).exists(): + return JsonResponse({"success": False, "message": "Phone number already exists."}) + + # Update student details + student.name = name + student.email = email + student.phone = phone + student.gender = gender + student.birth_date = birth_date if birth_date else None + student.temporary_address = temporary_address + student.permanent_address = permanent_address + student.parent_name = parent_name + student.parent_phone = parent_phone + student.citizenship_no = citizenship_no + + # Update batch and set joining date based on batch if provided + if batch_id: + try: + batch = Batch.objects.get(id=batch_id) + # Clear existing batches and add the new one + student.batches.clear() + student.batches.add(batch) + # Set joining date based on batch year + student.joining_date = batch.year if batch.year else timezone.now().date() + except Batch.DoesNotExist: + pass + + # Handle profile picture upload if provided + if request.FILES.get('image'): + student.image = request.FILES['image'] + + student.save() + + return JsonResponse({"success": True, "message": "Student updated successfully."}) + + return JsonResponse({"success": False, "message": "Invalid request method."}) + + except Exception as e: + return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"}) + +@login_required +def delete_student(request, student_id): + """Delete a student""" + try: + # Check if user is a HOD + if not request.user.groups.filter(name="HOD").exists(): + messages.error(request, "Only HODs can delete students.") + return redirect('hodDashboard') + + # Get the HOD's course + hod = request.user + if not hasattr(hod, "course") or not hod.course: + messages.error(request, "You are not assigned as HOD of any department.") + return redirect('hodDashboard') + + # Get the student and ensure they belong to the HOD's course + student = get_object_or_404(Student, id=student_id, course=hod.course) + + # Check if student has any attendance records + if AttendanceRecord.objects.filter(student=student).exists(): + messages.error(request, "Cannot delete student as they have attendance records.") + return redirect('hodDashboard') + + # Delete the student + student.delete() + messages.success(request, "Student deleted successfully.") + return redirect('hodDashboard') + + except Student.DoesNotExist: + messages.error(request, "Student not found.") + return redirect('hodDashboard') + except Exception as e: + messages.error(request, f"An error occurred: {str(e)}") + return redirect('hodDashboard') + +@login_required +def add_notice(request): + """Add a new notice and notify students and parents""" + try: + # Check if user is HOD + if not request.user.groups.filter(name="HOD").exists(): + messages.error(request, "Only HODs can add notices.") + return redirect('hodDashboard') + + # Get the HOD's course + hod = request.user + if not hasattr(hod, "course") or not hod.course: + messages.error(request, "You are not assigned as HOD of any department.") + return redirect('hodDashboard') + + course = hod.course + + if request.method == "POST": + title = request.POST.get("title") + message = request.POST.get("message") + + # Validate required fields + if not title: + messages.error(request, "Title is required.") + return redirect('hodDashboard') + + # Create notice + notice = Notice( + title=title, + message=message + ) + + # Handle image if provided + if request.FILES.get("image"): + notice.image = request.FILES.get("image") + + # Handle file attachment if provided + if request.FILES.get("file"): + notice.file = request.FILES.get("file") + + notice.save() + + # Get students from the HOD's course with FCM tokens + students = Student.objects.filter(course=course, fcm_token__isnull=False) + + # Get parents of these students with FCM tokens + student_ids = students.values_list('id', flat=True) + parents = Parent.objects.filter(students__in=student_ids, fcm_token__isnull=False).distinct() + + # Collect all FCM tokens + tokens = [] + + # Add student tokens + student_tokens = students.values_list('fcm_token', flat=True) + tokens.extend(student_tokens) + + # Add parent tokens + parent_tokens = parents.values_list('fcm_token', flat=True) + tokens.extend(parent_tokens) + + # Remove any empty or null tokens + tokens = [token for token in tokens if token] + + # Send push notifications if we have any tokens + if tokens: + try: + # Send notification to all tokens at once + success_count, failure_count, failed_tokens = send_push_notification( + "New Notice: {title}", + message[:100] + "..." if len(message) > 100 else message, + tokens + ) + + if failure_count > 0: + print(f"Failed to send notifications to {failure_count} devices") + print(f"Failed tokens: {failed_tokens}") + except Exception as e: + print(f"Error sending notifications: {str(e)}") + + messages.success(request, "Notice added successfully and notifications sent.") + return redirect('hodDashboard') + + return render(request, 'hod/dashboard.html') + + except Exception as e: + messages.error(request, f"An error occurred: {str(e)}") + return redirect('hodDashboard') + +@login_required +def view_notice(request, notice_id): + """View notice details""" + try: + # Check if user is a HOD + if not request.user.groups.filter(name="HOD").exists(): + messages.error(request, "Access denied.") + return redirect('hodDashboard') + + # Get notice + notice = get_object_or_404(Notice, id=notice_id) + + context = { + "notice": notice, + "title": "View Notice" + } + + return render(request, 'hod/view_notice.html', context) + + except Notice.DoesNotExist: + messages.error(request, "Notice not found.") + return redirect('hodDashboard') + except Exception as e: + messages.error(request, f"An error occurred: {str(e)}") + return redirect('hodDashboard') + +@login_required +def delete_notice(request, notice_id): + """Delete a notice""" + try: + # Check if user is a HOD + if not request.user.groups.filter(name="HOD").exists(): + messages.error(request, "Only HODs can delete notices.") + return redirect('hodDashboard') + + # Get notice + notice = get_object_or_404(Notice, id=notice_id) + + # Delete notice + notice.delete() + + messages.success(request, "Notice deleted successfully.") + return redirect('hodDashboard') + + except Notice.DoesNotExist: + messages.error(request, "Notice not found.") + return redirect('hodDashboard') + except Exception as e: + messages.error(request, f"An error occurred: {str(e)}") + return redirect('hodDashboard') + +@login_required +def add_meeting(request): + """Add a new teacher-parent meeting and notify relevant users""" + try: + # Check if user is HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can schedule meetings."}) + + # Get the HOD's course + hod = request.user + if not hasattr(hod, "course") or not hod.course: + return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."}) + + course = hod.course + + if request.method == "POST": + meeting_date = request.POST.get("meeting_date") + meeting_time = request.POST.get("meeting_time") + duration = request.POST.get("duration") + agenda = request.POST.get("agenda", "") + is_online = request.POST.get("is_online") == "true" + meeting_link = request.POST.get("meeting_link", "") or "" # Ensure never NULL + + # Validate required fields + if not all([meeting_date, meeting_time, duration]): + return JsonResponse({"success": False, "message": "Date, time and duration are required."}) + + # Create meeting + meeting = TeacherParentMeeting( + meeting_date=meeting_date, + meeting_time=meeting_time, + duration=duration, + agenda=agenda, + is_online=is_online, + meeting_link=meeting_link, # Now guaranteed to be at least empty string + status="scheduled" + ) + + meeting.save() + + # Get students from the HOD's course with FCM tokens + students = Student.objects.filter(course=course, fcm_token__isnull=False) + + # Get parents of these students with FCM tokens + student_ids = students.values_list('id', flat=True) + parents = Parent.objects.filter(students__in=student_ids, fcm_token__isnull=False).distinct() + + # Collect all FCM tokens + tokens = [] + + # Add student tokens + student_tokens = students.values_list('fcm_token', flat=True) + tokens.extend(student_tokens) + + # Add parent tokens + parent_tokens = parents.values_list('fcm_token', flat=True) + tokens.extend(parent_tokens) + + # Remove any empty or null tokens + tokens = [token for token in tokens if token] + + # Send push notifications if we have any tokens + if tokens: + try: + # Send notification to all tokens at once + success_count, failure_count, failed_tokens = send_push_notification( + "New Meeting Scheduled", + f"A new meeting has been scheduled for {meeting_date} at {meeting_time}", + tokens + ) + + if failure_count > 0: + print(f"Failed to send notifications to {failure_count} devices") + print(f"Failed tokens: {failed_tokens}") + except Exception as e: + print(f"Error sending notifications: {str(e)}") + + return JsonResponse({"success": True, "message": "Meeting scheduled successfully."}) + + return JsonResponse({"success": False, "message": "Invalid request method."}) + + except Exception as e: + return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"}) + +@login_required +def get_meeting(request, meeting_id): + """Get meeting details""" + try: + # Check if user is HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can view meeting details."}) + + # Get meeting + meeting = get_object_or_404(TeacherParentMeeting, id=meeting_id) + + # Prepare response data + response_data = { + "success": True, + "meeting": { + "id": meeting.id, + "meeting_date": meeting.meeting_date.strftime("%Y-%m-%d"), + "meeting_time": meeting.meeting_time.strftime("%H:%M"), + "duration": meeting.duration, + "status": meeting.status, + "status_display": meeting.get_status_display(), + "agenda": meeting.agenda, + "notes": meeting.notes, + "is_online": meeting.is_online, + "meeting_link": meeting.meeting_link, + "cancellation_reason": meeting.cancellation_reason, + "created_at": meeting.created_at.strftime("%Y-%m-%d %H:%M:%S") + } + } + + return JsonResponse(response_data) + + except TeacherParentMeeting.DoesNotExist: + return JsonResponse({"success": False, "message": "Meeting not found."}) + except Exception as e: + return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"}) + +@login_required +def edit_meeting(request, meeting_id): + """Edit an existing meeting""" + try: + # Check if user is HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can edit meetings."}) + + # Get meeting + meeting = get_object_or_404(TeacherParentMeeting, id=meeting_id) + + if request.method == "PUT": + meeting_date = request.POST.get("meeting_date") + meeting_time = request.POST.get("meeting_time") + duration = request.POST.get("duration") + agenda = request.POST.get("agenda") + notes = request.POST.get("notes", "") # Get notes field with empty string default + is_online = request.POST.get("is_online") == "true" + meeting_link = request.POST.get("meeting_link", "") # Default to empty string + + # Validate required fields + if not all([meeting_date, meeting_time, duration]): + return JsonResponse({"error": "Date, time and duration are required"}, status=400) + + # Update meeting + meeting.meeting_date = meeting_date + meeting.meeting_time = meeting_time + meeting.duration = duration + meeting.agenda = agenda + meeting.notes = notes # Save notes field + meeting.is_online = is_online + meeting.meeting_link = meeting_link # Always set meeting_link, even if empty + meeting.status = "rescheduled" if meeting.status == "scheduled" else meeting.status + + meeting.save() + + # Get students and parents to notify + students = Student.objects.filter(course=request.user.course, fcm_token__isnull=False) + student_ids = students.values_list('id', flat=True) + parents = Parent.objects.filter(students__in=student_ids, fcm_token__isnull=False).distinct() + + # Collect FCM tokens + tokens = list(students.values_list('fcm_token', flat=True)) + list(parents.values_list('fcm_token', flat=True)) + tokens = [token for token in tokens if token] + + # Send notifications + if tokens: + try: + success_count, failure_count, failed_tokens = send_push_notification( + "Meeting Updated", + f"Meeting scheduled for {meeting_date} at {meeting_time} has been updated", + tokens + ) + + if failure_count > 0: + print(f"Failed to send notifications to {failure_count} devices") + except Exception as e: + print(f"Error sending notifications: {str(e)}") + + return JsonResponse({"success": True, "message": "Meeting updated successfully."}) + + return JsonResponse({"success": False, "message": "Invalid request method."}) + + except TeacherParentMeeting.DoesNotExist: + return JsonResponse({"success": False, "message": "Meeting not found."}) + except Exception as e: + return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"}) + +@login_required +def cancel_meeting(request, meeting_id): + """Cancel a meeting""" + try: + # Check if user is HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can cancel meetings."}) + + # Get meeting + meeting = get_object_or_404(TeacherParentMeeting, id=meeting_id) + + if request.method == "POST": + cancellation_reason = request.POST.get("cancellation_reason") + + if not cancellation_reason: + return JsonResponse({"success": False, "message": "Cancellation reason is required."}) + + # Update meeting + meeting.status = "cancelled" + meeting.cancellation_reason = cancellation_reason + meeting.save() + + # Get students and parents to notify + students = Student.objects.filter(course=request.user.course, fcm_token__isnull=False) + student_ids = students.values_list('id', flat=True) + parents = Parent.objects.filter(students__in=student_ids, fcm_token__isnull=False).distinct() + + # Collect FCM tokens + tokens = list(students.values_list('fcm_token', flat=True)) + list(parents.values_list('fcm_token', flat=True)) + tokens = [token for token in tokens if token] + + # Send notifications + if tokens: + try: + success_count, failure_count, failed_tokens = send_push_notification( + "Meeting Cancelled", + f"Meeting scheduled for {meeting.meeting_date} at {meeting.meeting_time} has been cancelled", + tokens + ) + + if failure_count > 0: + print(f"Failed to send notifications to {failure_count} devices") + except Exception as e: + print(f"Error sending notifications: {str(e)}") + + return JsonResponse({"success": True, "message": "Meeting cancelled successfully."}) + + return JsonResponse({"success": False, "message": "Invalid request method."}) + + except TeacherParentMeeting.DoesNotExist: + return JsonResponse({"success": False, "message": "Meeting not found."}) + except Exception as e: + return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"}) + +@login_required +def get_meeting_notes(request, meeting_id): + """Get meeting notes""" + try: + # Check if user is HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can view meeting notes."}) + + # Get meeting + meeting = get_object_or_404(TeacherParentMeeting, id=meeting_id) + + return JsonResponse({ + "success": True, + "notes": meeting.notes or "No notes available" + }) + + except TeacherParentMeeting.DoesNotExist: + return JsonResponse({"success": False, "message": "Meeting not found."}) + except Exception as e: + return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"}) + +@login_required +def get_meeting_agenda(request, meeting_id): + """Get meeting agenda""" + try: + # Check if user is HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can view meeting agenda."}) + + # Get meeting + meeting = get_object_or_404(TeacherParentMeeting, id=meeting_id) + + return JsonResponse({ + "success": True, + "agenda": meeting.agenda or "No agenda available" + }) + + except TeacherParentMeeting.DoesNotExist: + return JsonResponse({"success": False, "message": "Meeting not found."}) + except Exception as e: + return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"}) + +@login_required +def get_cancellation_reason(request, meeting_id): + """Get meeting cancellation reason""" + try: + # Check if user is HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can view cancellation reasons."}) + + # Get meeting + meeting = get_object_or_404(TeacherParentMeeting, id=meeting_id) + + if meeting.status != "cancelled": + return JsonResponse({"success": False, "message": "Meeting is not cancelled."}) + + return JsonResponse({ + "success": True, + "reason": meeting.cancellation_reason or "No reason provided" + }) + + except TeacherParentMeeting.DoesNotExist: + return JsonResponse({"success": False, "message": "Meeting not found."}) + except Exception as e: + return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"}) + +# API endpoints for meeting management +@login_required +@require_http_methods(["GET"]) +def api_get_meeting(request, meeting_id): + """API endpoint to get meeting details""" + try: + # Get meeting + meeting = get_object_or_404(TeacherParentMeeting, id=meeting_id) + + # Prepare response data + response_data = { + "id": meeting.id, + "meeting_date": meeting.meeting_date.strftime("%Y-%m-%d"), + "meeting_time": meeting.meeting_time.strftime("%H:%M"), + "duration": meeting.duration, + "status": meeting.status, + "status_display": meeting.get_status_display(), + "agenda": meeting.agenda, + "notes": meeting.notes, + "is_online": meeting.is_online, + "meeting_link": meeting.meeting_link, + "cancellation_reason": meeting.cancellation_reason, + "created_at": meeting.created_at.strftime("%Y-%m-%d %H:%M:%S") + } + + return JsonResponse(response_data) + + except TeacherParentMeeting.DoesNotExist: + return JsonResponse({"error": "Meeting not found"}, status=404) + except Exception as e: + return JsonResponse({"error": str(e)}, status=500) + +@login_required +@csrf_exempt +@require_http_methods(["POST"]) +def api_create_meeting(request): + """API endpoint to create a new meeting""" + try: + # Check if user is HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"error": "Only HODs can schedule meetings"}, status=403) + + # Get form data + meeting_date = request.POST.get("meeting_date") + meeting_time = request.POST.get("meeting_time") + duration = request.POST.get("duration") + agenda = request.POST.get("agenda", "") + is_online = request.POST.get("is_online") == "true" + meeting_link = request.POST.get("meeting_link", "") or "" # Ensure never NULL + + # Validate required fields + if not all([meeting_date, meeting_time, duration]): + return JsonResponse({"error": "Date, time and duration are required"}, status=400) + + # Create meeting + meeting = TeacherParentMeeting( + meeting_date=meeting_date, + meeting_time=meeting_time, + duration=duration, + agenda=agenda, + is_online=is_online, + meeting_link=meeting_link, # Now guaranteed to be at least empty string + status="scheduled" + ) + + meeting.save() + + # Return success response + return JsonResponse({ + "id": meeting.id, + "message": "Meeting scheduled successfully" + }) + + except Exception as e: + return JsonResponse({"error": str(e)}, status=500) + +@login_required +@csrf_exempt +@require_http_methods(["PUT", "POST"]) +def api_update_meeting(request, meeting_id): + """API endpoint to update an existing meeting""" + try: + # Check if user is HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"error": "Only HODs can edit meetings"}, status=403) + + # Get meeting + meeting = get_object_or_404(TeacherParentMeeting, id=meeting_id) + + # Get form data + meeting_date = request.POST.get("meeting_date") + meeting_time = request.POST.get("meeting_time") + duration = request.POST.get("duration") + agenda = request.POST.get("agenda") + notes = request.POST.get("notes", "") # Get notes field with empty string default + is_online = request.POST.get("is_online") == "true" + meeting_link = request.POST.get("meeting_link", "") # Default to empty string + + # Validate required fields + if not all([meeting_date, meeting_time, duration]): + return JsonResponse({"error": "Date, time and duration are required"}, status=400) + + # Update meeting + meeting.meeting_date = meeting_date + meeting.meeting_time = meeting_time + meeting.duration = duration + meeting.agenda = agenda + meeting.notes = notes # Save notes field + meeting.is_online = is_online + meeting.meeting_link = meeting_link # Always set meeting_link, even if empty + meeting.status = "rescheduled" if meeting.status == "scheduled" else meeting.status + + meeting.save() + + # Return success response + return JsonResponse({ + "id": meeting.id, + "message": "Meeting updated successfully" + }) + + except TeacherParentMeeting.DoesNotExist: + return JsonResponse({"error": "Meeting not found"}, status=404) + except Exception as e: + return JsonResponse({"error": str(e)}, status=500) + +@login_required +@csrf_exempt +@require_http_methods(["POST"]) +def api_cancel_meeting(request, meeting_id): + """API endpoint to cancel a meeting""" + try: + # Check if user is HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"error": "Only HODs can cancel meetings"}, status=403) + + # Get meeting + meeting = get_object_or_404(TeacherParentMeeting, id=meeting_id) + + # Get cancellation reason + cancellation_reason = request.POST.get("cancellation_reason") + + if not cancellation_reason: + return JsonResponse({"error": "Cancellation reason is required"}, status=400) + + # Update meeting + meeting.status = "cancelled" + meeting.cancellation_reason = cancellation_reason + meeting.save() + + # Return success response + return JsonResponse({ + "id": meeting.id, + "message": "Meeting cancelled successfully" + }) + + except TeacherParentMeeting.DoesNotExist: + return JsonResponse({"error": "Meeting not found"}, status=404) + except Exception as e: + return JsonResponse({"error": str(e)}, status=500) + +@login_required +@csrf_exempt +@require_http_methods(["POST"]) +def api_create_routine(request): + """API endpoint to create a new routine""" + try: + # Check if user is a HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can create routines."}) + + # Get the HOD's course + hod = request.user + if not hasattr(hod, "course") or not hod.course: + return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."}) + + course = hod.course + + # Get form data + subject_id = request.POST.get("subject_id") + teacher_id = request.POST.get("teacher_id") + start_time = request.POST.get("start_time") + end_time = request.POST.get("end_time") + period_or_year = request.POST.get("period_or_year") + + # Validate required fields + if not subject_id or not teacher_id or not start_time or not end_time or not period_or_year: + return JsonResponse({"success": False, "message": "All fields are required."}) + + # Get subject and teacher objects + try: + subject = Subject.objects.get(id=subject_id, course=course) + teacher = Staff.objects.get(id=teacher_id) + except (Subject.DoesNotExist, Staff.DoesNotExist): + return JsonResponse({"success": False, "message": "Invalid subject or teacher."}) + + # Create the routine + routine = Routine( + course=course, + subject=subject, + teacher=teacher, + start_time=start_time, + end_time=end_time, + period_or_year=period_or_year, + is_active=True + ) + routine.save() + + return JsonResponse({ + "success": True, + "message": "Routine created successfully.", + "routine_id": routine.id + }) + except Exception as e: + return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"}) + +@login_required +@require_http_methods(["GET"]) +def api_get_routine(request, routine_id): + """API endpoint to get routine details""" + try: + # Check if user is a HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can view routine details."}) + + # Get the HOD's course + hod = request.user + if not hasattr(hod, "course") or not hod.course: + return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."}) + + # Get routine and ensure it belongs to the HOD's course + routine = get_object_or_404(Routine, id=routine_id, course=hod.course) + + # Format time fields properly to ensure they can be used in the form's time inputs + start_time = routine.start_time.strftime("%H:%M") if routine.start_time else "" + end_time = routine.end_time.strftime("%H:%M") if routine.end_time else "" + + # Prepare response data with detailed information + response_data = { + "success": True, + "routine": { + "id": routine.id, + "subject_id": routine.subject.id, + "subject_name": routine.subject.name, + "teacher_id": routine.teacher.id, + "teacher_name": routine.teacher.name, + "start_time": start_time, + "end_time": end_time, + "period_or_year": routine.period_or_year, + "is_active": routine.is_active + } + } + + # Log the response for debugging + print(f"API response for routine {routine_id}: {response_data}") + return JsonResponse(response_data) + except Exception as e: + error_message = f"An error occurred while fetching routine {routine_id}: {str(e)}" + print(error_message) + return JsonResponse({"success": False, "message": error_message}) + +@login_required +@csrf_exempt +@require_http_methods(["PUT", "POST"]) +def api_update_routine(request, routine_id): + """API endpoint to update a routine""" + try: + # Check if user is a HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can update routines."}) + + # Get the HOD's course + hod = request.user + if not hasattr(hod, "course") or not hod.course: + return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."}) + + # Get routine and ensure it belongs to the HOD's course + routine = get_object_or_404(Routine, id=routine_id, course=hod.course) + + # Handle both PUT and POST methods + if request.method == 'PUT': + # For PUT requests, get data from request.POST or parse request.body + import json + try: + if request.body: + data = json.loads(request.body) + else: + data = request.POST.dict() + + # Extract values from the data dictionary + subject_id = data.get("subject_id") + teacher_id = data.get("teacher_id") + start_time = data.get("start_time") + end_time = data.get("end_time") + period_or_year = data.get("period_or_year") + + print(f"Received data: {data}") + except json.JSONDecodeError as e: + print(f"JSON decode error: {e}") + # Fallback to form data + subject_id = request.POST.get("subject_id") + teacher_id = request.POST.get("teacher_id") + start_time = request.POST.get("start_time") + end_time = request.POST.get("end_time") + period_or_year = request.POST.get("period_or_year") + else: + # For POST requests, use request.POST directly + subject_id = request.POST.get("subject_id") + teacher_id = request.POST.get("teacher_id") + start_time = request.POST.get("start_time") + end_time = request.POST.get("end_time") + period_or_year = request.POST.get("period_or_year") + + # Log received data + print(f"Processing routine update: subject_id={subject_id}, teacher_id={teacher_id}, start_time={start_time}, end_time={end_time}") + + # Validate required fields + if not subject_id or not teacher_id or not start_time or not end_time: + error_msg = "All fields are required." + print(f"Validation error: {error_msg}") + return JsonResponse({"success": False, "message": error_msg}) + + # Get subject and teacher objects + try: + subject = Subject.objects.get(id=subject_id, course=hod.course) + teacher = Staff.objects.get(id=teacher_id) + except (Subject.DoesNotExist, Staff.DoesNotExist) as e: + error_msg = "Invalid subject or teacher." + print(f"Database lookup error: {e}") + return JsonResponse({"success": False, "message": error_msg}) + + # Update routine + routine.subject = subject + routine.teacher = teacher + routine.start_time = start_time + routine.end_time = end_time + if period_or_year: + routine.period_or_year = period_or_year + routine.save() + + print(f"Routine {routine_id} updated successfully") + return JsonResponse({"success": True, "message": "Routine updated successfully."}) + except Exception as e: + error_msg = f"An error occurred: {str(e)}" + print(f"Error in api_update_routine: {error_msg}") + return JsonResponse({"success": False, "message": error_msg}) + +@login_required +@csrf_exempt +@require_http_methods(["DELETE", "POST"]) +def api_delete_routine(request, routine_id): + """API endpoint to delete a routine""" + try: + # Check if user is a HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can delete routines."}) + + # Get the HOD's course + hod = request.user + if not hasattr(hod, "course") or not hod.course: + return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."}) + + # Get routine and ensure it belongs to the HOD's course + routine = get_object_or_404(Routine, id=routine_id, course=hod.course) + + # Delete the routine + routine.delete() + + return JsonResponse({"success": True, "message": "Routine deleted successfully."}) + except Exception as e: + return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"}) + +@login_required +def get_student_progress(request): + """API endpoint to get all student progress data with filtering options""" + try: + # Check if user is a HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can access progress data."}) + + # Get the HOD's course + hod = request.user + if not hasattr(hod, "course") or not hod.course: + return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."}) + + course = hod.course + + # Get batch filter if provided + batch_id = request.GET.get('batch_id') + + # Base query for student progress + progress_query = CourseTracking.objects.filter( + student__course=course + ).select_related("student") + + # Apply batch filter if provided + if batch_id: + try: + batch = Batch.objects.get(id=batch_id) + progress_query = progress_query.filter(student__batches=batch) + except Batch.DoesNotExist: + return JsonResponse({"success": False, "message": "Invalid batch selected."}) + + # Get all progress records ordered by completion percentage + progress_records = progress_query.order_by("-completion_percentage") + + # Calculate metrics + total_students = progress_records.count() + avg_completion = progress_records.aggregate(Avg('completion_percentage'))['completion_percentage__avg'] or 0 + avg_completion = round(avg_completion, 1) + students_on_track = progress_records.filter(completion_percentage__gte=75).count() + students_at_risk = progress_records.filter(completion_percentage__lt=40).count() + + # Prepare progress data + progress_data = [] + + for progress in progress_records: + # Get batch names + batch_names = ", ".join([batch.name for batch in progress.student.batches.all()]) + + # Determine status + if progress.completion_percentage >= 75: + status = "On Track" + status_class = "success" + elif progress.completion_percentage >= 40: + status = "Needs Attention" + status_class = "warning" + else: + status = "At Risk" + status_class = "danger" + + # Format student data + student_image = progress.student.image.url if progress.student.image else None + + # Add to progress data list + progress_data.append({ + "student_id": progress.student.id, + "student_name": progress.student.name, + "student_image": student_image, + "batch_names": batch_names, + "current_period": progress.student.current_period, + "completion_percentage": progress.completion_percentage, + "last_updated": progress.updated_at.strftime("%Y-%m-%d %H:%M:%S"), + "status": status, + "status_class": status_class + }) + + # Prepare response + response_data = { + "success": True, + "total_students": total_students, + "avg_completion": avg_completion, + "students_on_track": students_on_track, + "students_at_risk": students_at_risk, + "progress_data": progress_data + } + + return JsonResponse(response_data) + + except Exception as e: + return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"}) + +@login_required +def get_progress(request, progress_id): + """API endpoint to get details of a specific student progress record""" + try: + # Check if user is a HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can access progress data."}) + + # Get the HOD's course + hod = request.user + if not hasattr(hod, "course") or not hod.course: + return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."}) + + course = hod.course + + # Get the progress record + try: + progress = CourseTracking.objects.get(id=progress_id, student__course=course) + except CourseTracking.DoesNotExist: + return JsonResponse({"success": False, "message": "Progress record not found."}) + + # Get student data + student = progress.student + + # Format batch names + batch_names = [batch.name for batch in student.batches.all()] + + # Prepare progress data + progress_data = { + "id": progress.id, + "completion_percentage": progress.completion_percentage, + "progress_status": progress.progress_status, + "notes": progress.notes or "", + "last_updated": progress.updated_at.strftime("%Y-%m-%d %H:%M:%S"), + } + + # Prepare student data + student_data = { + "id": student.id, + "name": student.name, + "image": student.image.url if student.image else None, + "current_period": student.current_period, + "batches": batch_names, + } + + return JsonResponse({ + "success": True, + "progress": progress_data, + "student": student_data + }) + + except Exception as e: + return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"}) + +@login_required +def edit_progress(request, progress_id): + """API endpoint to update a student progress record""" + try: + # Check if user is a HOD + if not request.user.groups.filter(name="HOD").exists(): + return JsonResponse({"success": False, "message": "Only HODs can update progress data."}) + + # Get the HOD's course + hod = request.user + if not hasattr(hod, "course") or not hod.course: + return JsonResponse({"success": False, "message": "You are not assigned as HOD of any department."}) + + course = hod.course + + # Check if this is a POST request + if request.method != 'POST': + return JsonResponse({"success": False, "message": "Invalid request method."}) + + # Get the progress record + try: + progress = CourseTracking.objects.get(id=progress_id, student__course=course) + except CourseTracking.DoesNotExist: + return JsonResponse({"success": False, "message": "Progress record not found."}) + + # Get form data + current_period = request.POST.get('current_period') + completion_percentage = request.POST.get('completion_percentage') + notes = request.POST.get('notes', '') + + # Validate form data + if not current_period or not completion_percentage: + return JsonResponse({"success": False, "message": "Current period and completion percentage are required."}) + + try: + current_period = int(current_period) + completion_percentage = int(completion_percentage) + + # Validate current period + if current_period < 1: + return JsonResponse({"success": False, "message": "Current period must be at least 1."}) + + # Maximum period based on course duration and type + max_period = progress.course.duration + if progress.course.duration_type == "Semester": + max_period *= 2 # Two semesters per year + + if current_period > max_period: + return JsonResponse({"success": False, "message": f"Current period cannot exceed {max_period}."}) + + # Validate completion percentage + if completion_percentage < 0 or completion_percentage > 100: + return JsonResponse({"success": False, "message": "Completion percentage must be between 0 and 100."}) + + except ValueError: + return JsonResponse({"success": False, "message": "Invalid numeric values provided."}) + + # Update student's current period + student = progress.student + student.current_period = current_period + student.save(update_fields=['current_period']) + + # Update progress record + progress.completion_percentage = completion_percentage + progress.notes = notes + progress.save(update_fields=['completion_percentage', 'notes']) + + return JsonResponse({ + "success": True, + "message": "Progress updated successfully." + }) + + except Exception as e: + return JsonResponse({"success": False, "message": f"An error occurred: {str(e)}"}) diff --git a/app/management/commands/README.md b/app/management/commands/README.md new file mode 100644 index 0000000..106553b --- /dev/null +++ b/app/management/commands/README.md @@ -0,0 +1,78 @@ +# Fake Data Generator for SMS + +This directory contains a Django management command to generate fake data for testing the School Management System (SMS). + +## Prerequisites + +Before using this script, make sure you have installed the required packages: + +```bash +pip install faker +``` + +## Usage + +Run the command using Django's `manage.py`: + +```bash +python manage.py generate_fake_data +``` + +### Options + +The script accepts the following optional parameters: + +- `--students`: Number of students to create (default: 50) +- `--staff`: Number of staff members to create (default: 20) +- `--parents`: Number of parents to create (default: 40) +- `--courses`: Number of courses to create (default: 5) +- `--subjects`: Number of subjects per course (default: 4) +- `--clear`: Clear existing data before generating new data (flag) + +### Examples + +Generate data with default parameters: +```bash +python manage.py generate_fake_data +``` + +Generate a smaller dataset: +```bash +python manage.py generate_fake_data --students 20 --staff 10 --parents 15 +``` + +Clear existing data and generate new data: +```bash +python manage.py generate_fake_data --clear +``` + +## Generated Data + +The script generates the following types of data: + +1. Institute information +2. Batches (last 4 years) +3. Courses with customized names and durations +4. Subjects for each course +5. Staff members with profiles and credentials +6. Students with profiles and course assignments +7. Parents linked to students +8. Class routines and schedules +9. Attendance records for past week +10. Notices/announcements +11. Leave requests for students and staff +12. Feedback (student, parent, institute) +13. Parent-teacher meetings +14. Course tracking records + +## Login Credentials + +The generated users have the following default passwords: + +- Students: `student123` +- Staff: `staff123` +- Parents: `parent123` + +## Note + +The script doesn't generate actual file content for subject files due to the limitation of creating binary content programmatically. This portion is commented out in the code. \ No newline at end of file diff --git a/app/management/commands/generate_fake_data.py b/app/management/commands/generate_fake_data.py new file mode 100644 index 0000000..eaedb3c --- /dev/null +++ b/app/management/commands/generate_fake_data.py @@ -0,0 +1,891 @@ +import random +from datetime import datetime, timedelta +import pytz +from django.core.management.base import BaseCommand +from django.utils import timezone +from django.contrib.auth.models import Group +from django.db import transaction +from faker import Faker +from app.models import ( + Institute, Batch, Course, Subject, Staff, Student, Parent, + Routine, Attendance, AttendanceRecord, Notice, + StaffLeave, StudentLeave, StudentFeedback, ParentFeedback, + InstituteFeedback, StaffInstituteFeedback, ParentInstituteFeedback, + CourseTracking, TeacherParentMeeting, SubjectFile +) + +# Nepali-specific data +NEPALI_FIRST_NAMES = [ + 'Aarav', 'Aayush', 'Abhishek', 'Aditya', 'Amit', 'Anish', 'Anup', 'Arjun', 'Ashish', 'Bibek', + 'Bikash', 'Bimal', 'Bishal', 'Bishnu', 'Buddha', 'Chandra', 'Dawa', 'Deepak', 'Dinesh', 'Dipesh', + 'Ganesh', 'Gaurav', 'Gopal', 'Hari', 'Hemant', 'Jeevan', 'Karan', 'Krishna', 'Kumar', 'Laxman', + 'Madan', 'Manoj', 'Nabin', 'Narayan', 'Niraj', 'Nishan', 'Prabin', 'Prakash', 'Pramod', 'Prasad', + 'Pratik', 'Pratikshya', 'Rabin', 'Raj', 'Rajesh', 'Rakesh', 'Ram', 'Ramesh', 'Ravi', 'Rohit', + 'Roshan', 'Sagar', 'Sajan', 'Sandeep', 'Sanjay', 'Santosh', 'Saroj', 'Saurav', 'Shankar', 'Shiva', + 'Siddhartha', 'Suman', 'Suresh', 'Sushil', 'Umesh', 'Uttam', 'Vijay', 'Vikash', 'Vishal', 'Yogesh' +] + +NEPALI_LAST_NAMES = [ + 'Acharya', 'Adhikari', 'Aryal', 'Bajracharya', 'Bhandari', 'Bhattarai', 'Chaudhary', 'Dahal', 'Dangol', + 'Dhakal', 'Gautam', 'Ghimire', 'Gurung', 'Joshi', 'Kafle', 'Karki', 'Khadka', 'Khanal', 'Lama', + 'Limbu', 'Maharjan', 'Malla', 'Neupane', 'Pandey', 'Panta', 'Poudel', 'Rai', 'Rana', 'Regmi', + 'Sharma', 'Shrestha', 'Silwal', 'Singh', 'Tamang', 'Thapa', 'Timalsina', 'Tuladhar', 'Upadhyaya' +] + +NEPALI_DESIGNATIONS = [ + 'Teacher', + 'Principal', + 'HOD', + 'Admission Officer', + 'Staff' +] + +NEPALI_CITIES = [ + 'Kathmandu', 'Pokhara', 'Lalitpur', 'Bhaktapur', 'Biratnagar', 'Birgunj', 'Dharan', 'Bharatpur', + 'Butwal', 'Hetauda', 'Dhangadhi', 'Nepalgunj', 'Itahari', 'Damak', 'Siddharthanagar' +] + +class Command(BaseCommand): + help = 'Generate fake data for testing the SMS application' + + def add_arguments(self, parser): + parser.add_argument('--students', type=int, default=50, help='Number of students to create') + parser.add_argument('--staff', type=int, default=20, help='Number of staff members to create') + parser.add_argument('--parents', type=int, default=40, help='Number of parents to create') + parser.add_argument('--courses', type=int, default=5, help='Number of courses to create') + parser.add_argument('--subjects', type=int, default=4, help='Subjects per course') + parser.add_argument('--clear', action='store_true', help='Clear existing data before generating new data') + + @transaction.atomic + def handle(self, *args, **options): + fake = Faker() + + # Get parameters + num_students = max(10, options['students']) # Ensure minimum of 10 students + num_staff = max(5, options['staff']) # Ensure minimum of 5 staff + num_parents = max(8, options['parents']) # Ensure minimum of 8 parents + num_courses = max(3, options['courses']) # Ensure minimum of 3 courses + subjects_per_course = max(3, options['subjects']) # Ensure minimum of 3 subjects per course + clear_data = options['clear'] + + # Get or create groups + staff_group, _ = Group.objects.get_or_create(name='Teacher') + parent_group, _ = Group.objects.get_or_create(name='Parent') + student_group, _ = Group.objects.get_or_create(name='Student') + + # Clear existing data if requested + if clear_data: + self.stdout.write(self.style.WARNING('Clearing existing data...')) + TeacherParentMeeting.objects.all().delete() + ParentInstituteFeedback.objects.all().delete() + StaffInstituteFeedback.objects.all().delete() + InstituteFeedback.objects.all().delete() + ParentFeedback.objects.all().delete() + StudentFeedback.objects.all().delete() + StudentLeave.objects.all().delete() + StaffLeave.objects.all().delete() + Notice.objects.all().delete() + AttendanceRecord.objects.all().delete() + Attendance.objects.all().delete() + Routine.objects.all().delete() + SubjectFile.objects.all().delete() + CourseTracking.objects.all().delete() + + # Clear many-to-many relationships before deleting models + for parent in Parent.objects.all(): + parent.students.clear() + + for student in Student.objects.all(): + student.batches.clear() + + Subject.objects.all().delete() + Parent.objects.all().delete() + Student.objects.all().delete() + Staff.objects.all().delete() + Course.objects.all().delete() + Batch.objects.all().delete() + Institute.objects.all().delete() + + self.stdout.write(self.style.SUCCESS('Data cleared successfully')) + + # Create Institute + institute = self._create_institute(fake) + + # Create batches (last 4 years) + batches = self._create_batches() + + # Create courses + courses = self._create_courses(fake, num_courses) + + # Create subjects for each course + subjects = self._create_subjects(fake, courses, subjects_per_course) + + # Create staff members + staff_members = self._create_staff(fake, num_staff, staff_group) + + # Create students + students = self._create_students(fake, num_students, courses, batches, student_group) + + # Create parents and link to students + parents = self._create_parents(fake, num_parents, students, parent_group) + + # Create routines and class schedules + routines = self._create_routines(courses, subjects, staff_members) + + # Create attendance records + attendance_records = self._create_attendance(routines, students) + + # Create notices + notices = self._create_notices(fake) + + # Create leave requests + self._create_leaves(fake, staff_members, students) + + # Create feedback + self._create_feedback(fake, students, staff_members, parents, institute) + + # Create parent-teacher meetings + self._create_meetings(fake, staff_members, parents) + + # Create subject files + self._create_subject_files(fake, subjects, staff_members) + + # Create course tracking for students + self._create_course_tracking(students, courses) + + self.stdout.write(self.style.SUCCESS('Successfully generated fake data!')) + + def _get_nepali_name(self): + return f"{random.choice(NEPALI_FIRST_NAMES)} {random.choice(NEPALI_LAST_NAMES)}" + + def _get_nepali_phone(self): + return f"98{random.randint(1000000, 9999999)}" + + def _get_nepali_address(self, city): + wards = [f"Ward No. {random.randint(1, 35)}" for _ in range(3)] + return f"{random.choice(wards)}, {city}, Nepal" + + def _create_institute(self, fake): + city = random.choice(NEPALI_CITIES) + institute, created = Institute.objects.get_or_create( + name=f"{city} Educational Institute", + defaults={ + 'phone': self._get_nepali_phone(), + 'email': fake.company_email(), + 'address': self._get_nepali_address(city), + 'pan_no': f"PAN{random.randint(100000, 999999)}", + 'reg_no': f"REG{random.randint(100000, 999999)}", + 'description': fake.paragraph(nb_sentences=3) + } + ) + if created: + self.stdout.write(self.style.SUCCESS(f'Created institute: {institute.name}')) + return institute + + def _create_batches(self): + batches = [] + current_year = datetime.now().year + + for year in range(current_year - 3, current_year + 1): + batch, created = Batch.objects.get_or_create( + name=f'Batch {year}', + defaults={'year': datetime(year, 1, 1)} + ) + batches.append(batch) + if created: + self.stdout.write(self.style.SUCCESS(f'Created batch: {batch.name}')) + + return batches + + def _create_courses(self, fake, num_courses): + courses = [] + course_names = [ + 'Computer Science Engineering', + 'Electrical Engineering', + 'Mechanical Engineering', + 'Civil Engineering', + 'Chemical Engineering', + 'Electronics Engineering', + 'Information Technology', + 'Aerospace Engineering', + 'Biotechnology Engineering', + 'Data Science' + ] + + # Ensure we don't try to create more courses than we have names for + num_to_create = min(num_courses, len(course_names)) + + for i in range(num_to_create): + duration = random.choice([3, 4]) + duration_type = random.choice(['Year', 'Semester']) + + course, created = Course.objects.get_or_create( + name=course_names[i], + defaults={ + 'duration': duration, + 'duration_type': duration_type + } + ) + courses.append(course) + if created: + self.stdout.write(self.style.SUCCESS(f'Created course: {course.name}')) + + return courses + + def _create_subjects(self, fake, courses, subjects_per_course): + subjects = [] + + subject_names = { + 'Computer Science Engineering': [ + 'Data Structures', 'Algorithms', 'Database Systems', 'Operating Systems', + 'Computer Networks', 'Software Engineering', 'Web Development', 'AI and Machine Learning', + 'Computer Architecture', 'Compiler Design', 'Computer Graphics', 'Cloud Computing' + ], + 'Electrical Engineering': [ + 'Circuit Theory', 'Electrical Machines', 'Power Systems', 'Control Systems', + 'Digital Electronics', 'Microprocessors', 'Power Electronics', 'Electrical Measurements', + 'Renewable Energy', 'Smart Grid', 'High Voltage Engineering', 'Power System Protection' + ], + 'Mechanical Engineering': [ + 'Thermodynamics', 'Fluid Mechanics', 'Manufacturing Processes', 'Machine Design', + 'Heat Transfer', 'Engineering Mechanics', 'Automobile Engineering', 'Materials Science', + 'Robotics', 'CAD/CAM', 'Industrial Engineering', 'Refrigeration and Air Conditioning' + ], + 'Information Technology': [ + 'Programming Fundamentals', 'Data Structures', 'Database Management', 'Web Technologies', + 'Computer Networks', 'Software Engineering', 'Cybersecurity', 'Cloud Computing', + 'Mobile Application Development', 'Big Data Analytics', 'IoT', 'Blockchain Technology' + ], + 'Data Science': [ + 'Statistical Methods', 'Machine Learning', 'Data Mining', 'Big Data Analytics', + 'Data Visualization', 'Deep Learning', 'Natural Language Processing', 'Time Series Analysis', + 'Business Intelligence', 'Predictive Analytics', 'Data Warehousing', 'Data Engineering' + ] + } + + # Add generic subjects for courses not explicitly defined + generic_subjects = [ + 'Mathematics I', 'Mathematics II', 'Physics', 'Chemistry', + 'Technical Writing', 'Engineering Ethics', 'Project Management', 'Research Methods', + 'Environmental Science', 'Economics', 'Management', 'Communication Skills' + ] + + for course in courses: + # Get subject list for this course or use generic subjects + if course.name in subject_names: + course_subject_names = subject_names[course.name] + else: + course_subject_names = generic_subjects + + max_year_or_sem = course.duration + if course.duration_type == 'Semester': + max_year_or_sem = course.duration * 2 + + # Create subjects for each year/semester + for year_or_sem in range(1, max_year_or_sem + 1): + # Calculate number of subjects for this year/semester + # Ensure at least 2 subjects per year/semester + num_subjects = max(2, min(subjects_per_course, len(course_subject_names))) + + # Select subjects ensuring no duplicates within the same year/semester + available_subjects = [s for s in course_subject_names if not Subject.objects.filter( + course=course, + name=s, + period_or_year=year_or_sem + ).exists()] + + if len(available_subjects) < num_subjects: + # If not enough unique subjects, add some from generic subjects + available_subjects.extend([ + s for s in generic_subjects + if s not in available_subjects and not Subject.objects.filter( + course=course, + name=s, + period_or_year=year_or_sem + ).exists() + ]) + + selected_subjects = random.sample(available_subjects, min(num_subjects, len(available_subjects))) + + for subject_name in selected_subjects: + subject, created = Subject.objects.get_or_create( + name=subject_name, + course=course, + period_or_year=year_or_sem + ) + subjects.append(subject) + if created: + self.stdout.write(self.style.SUCCESS(f'Created subject: {subject.name} for {course.name} in {year_or_sem}')) + + return subjects + + def _create_staff(self, fake, num_staff, staff_group): + staff_members = [] + + for i in range(num_staff): + gender = random.choice(['Male', 'Female']) + name = self._get_nepali_name() + phone = self._get_nepali_phone() + city = random.choice(NEPALI_CITIES) + + staff = Staff( + name=name, + phone=phone, + designation=random.choice(NEPALI_DESIGNATIONS), + gender=gender, + birth_date=fake.date_of_birth(minimum_age=30, maximum_age=65), + email=fake.email(), + temporary_address=self._get_nepali_address(city), + permanent_address=self._get_nepali_address(city), + marital_status=random.choice(['Married', 'Unmarried']), + parent_name=self._get_nepali_name(), + parent_phone=self._get_nepali_phone(), + citizenship_no=f"{random.randint(10000, 99999)}-{random.randint(10000, 99999)}", + joining_date=fake.date_between(start_date='-5y', end_date='today') + ) + staff.save() + staff.set_password('staff123') + staff.save() + staff.groups.add(staff_group) + staff_members.append(staff) + self.stdout.write(self.style.SUCCESS(f'Created staff: {staff.name}')) + + return staff_members + + def _create_students(self, fake, num_students, courses, batches, student_group): + students = [] + + for i in range(num_students): + gender = random.choice(['Male', 'Female']) + name = self._get_nepali_name() + phone = self._get_nepali_phone() + city = random.choice(NEPALI_CITIES) + + course = random.choice(courses) + max_semester = course.duration + if course.duration_type == 'Semester': + max_semester = course.duration * 2 + + current_period = random.randint(1, max_semester) + + student = Student( + name=name, + status=random.choice(['Active', 'Leave', 'Completed']), + gender=gender, + birth_date=fake.date_of_birth(minimum_age=18, maximum_age=25), + email=fake.email(), + phone=phone, + temporary_address=self._get_nepali_address(city), + permanent_address=self._get_nepali_address(city), + marital_status=random.choice(['Married', 'Unmarried']), + parent_name=self._get_nepali_name(), + parent_phone=self._get_nepali_phone(), + citizenship_no=f"{random.randint(10000, 99999)}-{random.randint(10000, 99999)}", + course=course, + current_period=current_period, + joining_date=fake.date_between(start_date='-4y', end_date='-1y') + ) + student.save() + student.set_password('student123') + student.save() + student.groups.add(student_group) + + batch = random.choice(batches) + student.batches.add(batch) + + students.append(student) + self.stdout.write(self.style.SUCCESS(f'Created student: {student.name}')) + + return students + + def _create_parents(self, fake, num_parents, students, parent_group): + parents = [] + + for i in range(num_parents): + name = self._get_nepali_name() + phone = self._get_nepali_phone() + city = random.choice(NEPALI_CITIES) + + parent = Parent( + name=name, + phone=phone, + email=fake.email(), + address=self._get_nepali_address(city), + ) + parent.save() + parent.set_password('parent123') + parent.save() + parent.groups.add(parent_group) + + num_children = random.randint(1, min(2, len(students))) + assigned_students = random.sample(students, num_children) + for student in assigned_students: + parent.students.add(student) + + parents.append(parent) + self.stdout.write(self.style.SUCCESS(f'Created parent: {parent.name} with {num_children} children')) + + return parents + + def _create_routines(self, courses, subjects, staff_members): + routines = [] + days_of_week = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'] + time_slots = [ + ('09:00', '10:00'), + ('10:15', '11:15'), + ('11:30', '12:30'), + ('13:30', '14:30'), + ('14:45', '15:45'), + ('16:00', '17:00') + ] + + # Create a schedule for each course and semester + for course in courses: + course_subjects = Subject.objects.filter(course=course) + + # Group subjects by period/year + subjects_by_period = {} + for subject in course_subjects: + if subject.period_or_year not in subjects_by_period: + subjects_by_period[subject.period_or_year] = [] + subjects_by_period[subject.period_or_year].append(subject) + + # Create routine for each period/year + for period, period_subjects in subjects_by_period.items(): + # Assign teachers to subjects + teacher_subject_assignments = {} + available_teachers = staff_members.copy() + + for subject in period_subjects: + if not available_teachers: + available_teachers = staff_members.copy() + + teacher = random.choice(available_teachers) + teacher_subject_assignments[subject] = teacher + available_teachers.remove(teacher) + + # Create routine for each day + for day in days_of_week: + # Shuffle time slots for variety + shuffled_slots = time_slots.copy() + random.shuffle(shuffled_slots) + + # Assign subjects to time slots + for subject, teacher in teacher_subject_assignments.items(): + if not shuffled_slots: + break + + start_time_str, end_time_str = shuffled_slots.pop() + start_time = datetime.strptime(start_time_str, "%H:%M").time() + end_time = datetime.strptime(end_time_str, "%H:%M").time() + + # Check for conflicts + conflicting_routine = Routine.objects.filter( + teacher=teacher, + start_time=start_time, + end_time=end_time + ).first() + + if not conflicting_routine: + routine = Routine( + course=course, + subject=subject, + teacher=teacher, + start_time=start_time, + end_time=end_time, + period_or_year=period, + is_active=True + ) + + try: + routine.save() + routines.append(routine) + self.stdout.write( + self.style.SUCCESS( + f'Created routine for {subject.name} with {teacher.name} ' + f'on {day} at {start_time_str}-{end_time_str}' + ) + ) + except Exception as e: + # Skip if there's an error (likely due to unique constraint violation) + pass + + return routines + + def _create_attendance(self, routines, students): + attendance_records = [] + + # Create attendance for the past 2 weeks + for day_offset in range(14, 0, -1): + date = datetime.now().date() - timedelta(days=day_offset) + + # Skip weekends + if date.weekday() >= 5: # 5 and 6 are Saturday and Sunday + continue + + for routine in routines: + # Create attendance for this routine on this date + attendance = Attendance( + date=date, + routine=routine, + teacher=routine.teacher, + teacher_attend=random.choices([True, False], weights=[0.95, 0.05])[0], # 95% chance of teacher attendance + class_status=True + ) + try: + attendance.save() + + # Find students in this course and semester + course_students = Student.objects.filter( + course=routine.course, + current_period=routine.period_or_year + ) + + # Create attendance records for students + for student in course_students: + # Base attendance probability on student's status + if student.status == 'Active': + attendance_prob = 0.85 # 85% chance for active students + elif student.status == 'Leave': + attendance_prob = 0.30 # 30% chance for students on leave + else: + attendance_prob = 0.10 # 10% chance for completed students + + # Add some randomness to the attendance pattern + attendance_prob += random.uniform(-0.1, 0.1) + attendance_prob = max(0, min(1, attendance_prob)) # Ensure between 0 and 1 + + is_present = random.choices([True, False], weights=[attendance_prob, 1-attendance_prob])[0] + + record = AttendanceRecord( + attendance=attendance, + student=student, + student_attend=is_present + ) + record.save() + attendance_records.append(record) + + self.stdout.write( + self.style.SUCCESS( + f'Created attendance for {routine.subject.name} on {date} ' + f'with {course_students.count()} students' + ) + ) + except Exception as e: + # Skip if there's an error + pass + + return attendance_records + + def _create_notices(self, fake): + notices = [] + + for i in range(10): # Create 10 notices + notice = Notice( + title=fake.sentence(), + message=fake.paragraph(nb_sentences=3), + created_at=fake.date_time_between(start_date='-30d', end_date='now') + ) + notice.save() + notices.append(notice) + self.stdout.write(self.style.SUCCESS(f'Created notice: {notice.title}')) + + return notices + + def _create_leaves(self, fake, staff_members, students): + # Create staff leaves + for staff in random.sample(staff_members, len(staff_members) // 3): # 1/3 of staff + start_date = fake.date_between(start_date='-30d', end_date='+30d') + end_date = start_date + timedelta(days=random.randint(1, 5)) + + leave = StaffLeave( + staff=staff, + start_date=start_date, + end_date=end_date, + message=fake.paragraph(), + status=random.choice([0, 1, 2]) # Pending, Approved, Rejected + ) + leave.save() + self.stdout.write(self.style.SUCCESS(f'Created staff leave for {staff.name}')) + + # Create student leaves + for student in random.sample(students, len(students) // 3): # 1/3 of students + start_date = fake.date_between(start_date='-30d', end_date='+30d') + end_date = start_date + timedelta(days=random.randint(1, 5)) + + leave = StudentLeave( + student=student, + start_date=start_date, + end_date=end_date, + message=fake.paragraph(), + status=random.choice([0, 1, 2]) # Pending, Approved, Rejected + ) + leave.save() + self.stdout.write(self.style.SUCCESS(f'Created student leave for {student.name}')) + + def _create_feedback(self, fake, students, staff_members, parents, institute): + # Create student feedback for teachers + for student in random.sample(students, len(students) // 2): + # Each student gives feedback to 2-3 teachers + num_teachers = random.randint(2, min(3, len(staff_members))) + selected_teachers = random.sample(staff_members, num_teachers) + + for teacher in selected_teachers: + try: + # Generate more realistic ratings (tend to be positive but with some variation) + rating = random.choices( + [3.5, 4.0, 4.5, 5.0], + weights=[0.1, 0.2, 0.3, 0.4] + )[0] + + feedback = StudentFeedback( + student=student, + teacher=teacher, + rating=rating, + feedback_text=fake.paragraph() + ) + feedback.save() + self.stdout.write( + self.style.SUCCESS( + f'Created student feedback from {student.name} for {teacher.name} ' + f'with rating {rating}' + ) + ) + except: + # Skip if there's an error (likely due to unique constraint) + pass + + # Create parent feedback for teachers + for parent in random.sample(parents, len(parents) // 2): + parent_students = parent.students.all() + if parent_students: + for student in parent_students: + # Each parent gives feedback for 1-2 teachers per student + num_teachers = random.randint(1, min(2, len(staff_members))) + selected_teachers = random.sample(staff_members, num_teachers) + + for teacher in selected_teachers: + try: + # Parent ratings tend to be slightly higher than student ratings + rating = random.choices( + [4.0, 4.5, 5.0], + weights=[0.2, 0.3, 0.5] + )[0] + + feedback = ParentFeedback( + parent=parent, + teacher=teacher, + student=student, + rating=rating, + feedback_text=fake.paragraph() + ) + feedback.save() + self.stdout.write( + self.style.SUCCESS( + f'Created parent feedback from {parent.name} for {teacher.name} ' + f'regarding {student.name} with rating {rating}' + ) + ) + except: + # Skip if there's an error + pass + + # Create institute feedback from students + for student in random.sample(students, len(students) // 3): + try: + # Student institute ratings tend to be slightly lower than teacher ratings + rating = random.choices( + [3.0, 3.5, 4.0, 4.5, 5.0], + weights=[0.1, 0.2, 0.3, 0.2, 0.2] + )[0] + + feedback = InstituteFeedback( + institute=institute, + user=student, + feedback_type=random.choice(['General', 'Facilities', 'Teaching', 'Infrastructure', 'Administration']), + rating=rating, + feedback_text=fake.paragraph(), + is_anonymous=random.choice([True, False]), + is_public=True + ) + feedback.save() + self.stdout.write( + self.style.SUCCESS( + f'Created institute feedback from {student.name} with rating {rating}' + ) + ) + except: + # Skip if there's an error + pass + + # Create staff institute feedback + for staff in random.sample(staff_members, len(staff_members) // 3): + try: + # Staff ratings tend to be more balanced + rating = random.choices( + [3.0, 3.5, 4.0, 4.5, 5.0], + weights=[0.15, 0.25, 0.3, 0.2, 0.1] + )[0] + + feedback = StaffInstituteFeedback( + institute=institute, + staff=staff, + feedback_type=random.choice(['General', 'Facilities', 'Teaching', 'Infrastructure', 'Administration']), + rating=rating, + feedback_text=fake.paragraph(), + is_anonymous=random.choice([True, False]), + is_public=True + ) + feedback.save() + self.stdout.write( + self.style.SUCCESS( + f'Created staff institute feedback from {staff.name} with rating {rating}' + ) + ) + except: + # Skip if there's an error + pass + + # Create parent institute feedback + for parent in random.sample(parents, len(parents) // 3): + try: + # Parent institute ratings tend to be higher + rating = random.choices( + [3.5, 4.0, 4.5, 5.0], + weights=[0.1, 0.2, 0.3, 0.4] + )[0] + + feedback = ParentInstituteFeedback( + institute=institute, + parent=parent, + feedback_type=random.choice(['General', 'Facilities', 'Administration']), + rating=rating, + feedback_text=fake.paragraph(), + is_anonymous=random.choice([True, False]), + is_public=True + ) + feedback.save() + self.stdout.write( + self.style.SUCCESS( + f'Created parent institute feedback from {parent.name} with rating {rating}' + ) + ) + except: + # Skip if there's an error + pass + + def _create_meetings(self, fake, staff_members, parents): + # Create parent-teacher meetings + future_dates = [datetime.now().date() + timedelta(days=d) for d in range(1, 15)] + + for staff in random.sample(staff_members, len(staff_members) // 2): + for parent in random.sample(parents, min(3, len(parents))): + meeting_date = random.choice(future_dates) + meeting_hour = random.randint(9, 16) + meeting_minute = random.choice([0, 15, 30, 45]) + meeting_time = f"{meeting_hour:02d}:{meeting_minute:02d}" + meeting_status = random.choice(['scheduled', 'rescheduled', 'completed', 'cancelled']) + + try: + meeting = TeacherParentMeeting( + meeting_date=meeting_date, + meeting_time=datetime.strptime(meeting_time, "%H:%M").time(), + duration=30, + status=meeting_status, + agenda=fake.paragraph(), + notes=fake.paragraph() if meeting_status == 'completed' else '', + cancellation_reason=fake.paragraph() if meeting_status == 'cancelled' else '', + is_online=random.choice([True, False]) + ) + + if meeting.is_online: + meeting.meeting_link = f"https://meet.example.com/{fake.uuid4()}" + + meeting.save() + self.stdout.write(self.style.SUCCESS(f'Created parent-teacher meeting on {meeting_date}')) + except Exception as e: + # Skip if there's an error + pass + + def _create_subject_files(self, fake, subjects, staff_members): + # Create subject files (lecture notes, etc.) + for subject in random.sample(subjects, len(subjects) // 2): + # Choose a teacher who might be teaching this subject + teacher = random.choice(staff_members) + + # Create 1-3 files for each selected subject + for i in range(random.randint(1, 3)): + file_types = ['Lecture Notes', 'Assignment', 'Reference Material', 'Exam Paper'] + file_type = random.choice(file_types) + + subject_file = SubjectFile( + subject=subject, + title=f"{file_type} - {subject.name} - {i+1}", + description=fake.paragraph(), + # Note: actual file content can't be created here + # file field would need an actual file + uploaded_by=teacher + ) + + try: + # We can't actually save this without a real file + # subject_file.save() + self.stdout.write(self.style.SUCCESS(f'Created subject file: {subject_file.title}')) + except: + # Skip if there's an error + pass + + def _create_course_tracking(self, students, courses): + # Create course tracking records for students + for student in students: + course = student.course + + if course: + try: + # Check if tracking already exists + existing_tracking = CourseTracking.objects.filter( + student=student, + course=course + ).first() + + if existing_tracking: + continue # Skip if tracking already exists + + # Calculate dates based on student's joining date + joining_date = student.joining_date or datetime.now().date() - timedelta(days=random.randint(180, 365)) + expected_end_date = joining_date + timedelta(days=365 * course.duration) + + # Calculate completion percentage based on current semester + if course.duration_type == 'Semester': + total_semesters = course.duration * 2 + completion_percentage = int((student.current_period / total_semesters) * 100) + else: + completion_percentage = int((student.current_period / course.duration) * 100) + + # Determine progress status + if completion_percentage == 100: + progress_status = 'Completed' + elif completion_percentage == 0: + progress_status = 'Not Started' + else: + progress_status = 'In Progress' + + tracking = CourseTracking( + student=student, + course=course, + enrollment_date=joining_date, + start_date=joining_date, + expected_end_date=expected_end_date, + actual_end_date=None, + progress_status=progress_status, + completion_percentage=completion_percentage, + current_period=student.current_period, + semester_start_date=joining_date, + notes=f"Tracking student progress in {course.name}" + ) + tracking.save() + self.stdout.write(self.style.SUCCESS(f'Created course tracking for {student.name} in {course.name}')) + except Exception as e: + # Log the error but continue with other students + self.stdout.write(self.style.ERROR(f'Error creating course tracking for {student.name}: {str(e)}')) + continue # Continue with next student diff --git a/app/middleware.py b/app/middleware.py new file mode 100644 index 0000000..b36d6f5 --- /dev/null +++ b/app/middleware.py @@ -0,0 +1,29 @@ +from django.http import HttpResponse +from django.template.loader import render_to_string + + +class HTTP505Middleware: + """ + Middleware to handle HTTP 505 errors (HTTP Version Not Supported) + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + # Process the request and get the response + response = self.get_response(request) + return response + + def process_exception(self, request, exception): + # Check if exception is related to HTTP version + if hasattr(exception, "status_code") and exception.status_code == 505: + # Render our 505 template + context = { + "request_path": request.path, + "request_method": request.method, + "user": request.user, + } + response_content = render_to_string("505.html", context, request) + return HttpResponse(response_content, status=505) + return None diff --git a/app/migrations/__init__.py b/app/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/models.py b/app/models.py index 2464b52..3eff160 100644 --- a/app/models.py +++ b/app/models.py @@ -1,12 +1,20 @@ +# Standard library imports +from datetime import date, timedelta, datetime + +# Core Django imports +from django.contrib.auth.hashers import make_password, check_password +from django.contrib.auth.models import AbstractUser, Group, Permission +from django.core.exceptions import ValidationError from django.db import models, transaction -from django.contrib.auth.hashers import make_password -from django.contrib.auth.models import AbstractUser -from datetime import date, timedelta from django.db.models.signals import post_save from django.dispatch import receiver from django.utils import timezone -from django.core.exceptions import ValidationError -from .firebase import send_push_notification, FCMDevice + +# Third-party app imports +from model_utils import FieldTracker + +# Local app imports +from app.firebase import send_push_notification, FCMDevice # Constants DURATION_TYPES = ( @@ -53,9 +61,11 @@ PROGRESS_STATUS_CHOICES = ( ("Dropped", "Dropped"), ) + # Base Models class Institute(models.Model): """Model representing an educational institute""" + id = models.BigAutoField(primary_key=True) name = models.CharField(max_length=255) phone = models.CharField(max_length=255, null=True, blank=True) @@ -73,13 +83,17 @@ class Institute(models.Model): verbose_name = "Institute" verbose_name_plural = "Institutes" + # Academic Models class Batch(models.Model): """Model representing a batch of students""" + id = models.BigAutoField(primary_key=True) name = models.CharField(max_length=255) - start_date = models.DateField(null=True, blank=True) - end_date = models.DateField(null=True, blank=True) + year = models.DateField( + null=True, blank=True, help_text="Starting date for all students in this batch" + ) + is_active = models.BooleanField(default=True) def __str__(self): return self.name @@ -88,13 +102,20 @@ class Batch(models.Model): verbose_name = "Batch" verbose_name_plural = "Batches" + # Course Related Models class Course(models.Model): """Model representing a course offered by the institute""" + id = models.BigAutoField(primary_key=True) name = models.CharField(max_length=500) + code = models.CharField(max_length=20, null=True, blank=True) duration = models.IntegerField(default=1, null=True, blank=True) - duration_type = models.CharField(max_length=255, choices=DURATION_TYPES, null=True, blank=True) + duration_type = models.CharField( + max_length=255, choices=DURATION_TYPES, null=True, blank=True + ) + description = models.TextField(null=True, blank=True) + is_active = models.BooleanField(default=True) def __str__(self): return self.name @@ -103,12 +124,17 @@ class Course(models.Model): verbose_name = "Course" verbose_name_plural = "Courses" + class Subject(models.Model): """Model representing a subject within a course""" + id = models.AutoField(primary_key=True) name = models.CharField(max_length=255) + code = models.CharField(max_length=20, null=True, blank=True) course = models.ForeignKey(Course, on_delete=models.CASCADE) - semester_or_year = models.PositiveIntegerField() + period_or_year = models.PositiveIntegerField( + help_text="Period number (semester or year) for this subject" + ) syllabus_pdf = models.FileField( upload_to="subject_pdfs/", null=True, @@ -117,16 +143,18 @@ class Subject(models.Model): ) def __str__(self): - return f"{self.name} ({self.course.name} - {'Semester' if self.course.duration_type == 'Semester' else 'Year'} {self.semester_or_year})" + return f"{self.name} ({self.course.name} - {'Semester' if self.course.duration_type == 'Semester' else 'Year'} {self.period_or_year})" def clean(self): if self.course.duration_type == "Semester": - max_semesters = self.course.duration * 2 - if self.semester_or_year > max_semesters: - raise ValidationError(f"Semester cannot exceed {max_semesters}") + max_periods = self.course.duration * 2 + if self.period_or_year > max_periods: + raise ValidationError(f"Period cannot exceed {max_periods} semesters") else: # Year based - if self.semester_or_year > self.course.duration: - raise ValidationError(f"Year cannot exceed {self.course.duration}") + if self.period_or_year > self.course.duration: + raise ValidationError( + f"Period cannot exceed {self.course.duration} years" + ) def save(self, *args, **kwargs): self.clean() @@ -139,53 +167,61 @@ class Subject(models.Model): """Get all files associated with this subject including syllabus and additional files""" files = [] if self.syllabus_pdf: - files.append({ - "id": "syllabus", - "title": f"{self.name} Syllabus", - "description": f"Syllabus PDF for {self.name}", - "file_url": self.syllabus_pdf.url, - "file_type": "syllabus", - "uploaded_by": None, - "uploaded_at": None - }) - - subject_files = self.subjectfile_set.all().order_by('-uploaded_at') + files.append( + { + "id": "syllabus", + "title": f"{self.name} Syllabus", + "description": f"Syllabus PDF for {self.name}", + "file_url": self.syllabus_pdf.url, + "file_type": "syllabus", + "uploaded_by": None, + "uploaded_at": None, + } + ) + + subject_files = self.subjectfile_set.all().order_by("-uploaded_at") for file in subject_files: - files.append({ - "id": file.id, - "title": file.title, - "description": file.description, - "file_url": file.file.url, - "file_type": "notes", - "uploaded_by": file.uploaded_by.name if file.uploaded_by else None, - "uploaded_at": file.uploaded_at - }) - + files.append( + { + "id": file.id, + "title": file.title, + "description": file.description, + "file_url": file.file.url, + "file_type": "notes", + "uploaded_by": file.uploaded_by.name if file.uploaded_by else None, + "uploaded_at": file.uploaded_at, + } + ) + return files class Meta: - unique_together = ["name", "course", "semester_or_year"] - ordering = ["course", "semester_or_year", "name"] + unique_together = ["name", "course", "period_or_year"] + ordering = ["course", "period_or_year", "name"] + class SubjectFile(models.Model): """Model to store additional files for subjects (notes, study materials, etc.)""" + id = models.AutoField(primary_key=True) subject = models.ForeignKey(Subject, on_delete=models.CASCADE) title = models.CharField(max_length=255) description = models.TextField(null=True, blank=True) file = models.FileField(upload_to="subject_files/") - uploaded_by = models.ForeignKey('Staff', on_delete=models.SET_NULL, null=True) + uploaded_by = models.ForeignKey("Staff", on_delete=models.SET_NULL, null=True) uploaded_at = models.DateTimeField(auto_now_add=True) - + def __str__(self): return f"{self.title} - {self.subject.name}" - + class Meta: - ordering = ['-uploaded_at'] + ordering = ["-uploaded_at"] + # User Models class Student(AbstractUser): """Model representing a student in the institute""" + first_name = None last_name = None is_superuser = None @@ -212,21 +248,35 @@ class Student(AbstractUser): id = models.BigAutoField(primary_key=True) name = models.CharField(max_length=255) - status = models.CharField(max_length=255, choices=STUDENT_STATUS_CHOICES, null=True, blank=True) - gender = models.CharField(max_length=255, choices=GENDER_CHOICES, null=True, blank=True) + status = models.CharField( + max_length=255, choices=STUDENT_STATUS_CHOICES, null=True, blank=True + ) + gender = models.CharField( + max_length=255, choices=GENDER_CHOICES, null=True, blank=True + ) birth_date = models.DateField(null=True, blank=True) email = models.CharField(max_length=255, null=True, blank=True) phone = models.CharField(max_length=255, null=True, blank=True, unique=True) temporary_address = models.CharField(max_length=500, null=True, blank=True) permanent_address = models.CharField(max_length=500, null=True, blank=True) - marital_status = models.CharField(max_length=255, choices=MARITAL_STATUS_CHOICES, null=True, blank=True) + marital_status = models.CharField( + max_length=255, choices=MARITAL_STATUS_CHOICES, null=True, blank=True + ) parent_name = models.CharField(max_length=255, null=True, blank=True) parent_phone = models.CharField(max_length=255, null=True, blank=True) citizenship_no = models.CharField(max_length=255, null=True, blank=True) - batches = models.ManyToManyField('Batch', related_name="students", blank=True) + batches = models.ManyToManyField("Batch", related_name="students", blank=True) image = models.ImageField(upload_to="student_image", null=True, blank=True) - course = models.ForeignKey(Course, on_delete=models.SET_NULL, null=True, blank=True, related_name="students") - current_semester = models.PositiveIntegerField(default=1, help_text="Current semester for semester-based courses") + course = models.ForeignKey( + Course, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="students", + ) + current_period = models.PositiveIntegerField( + default=1, help_text="Current semester/year for the student's course" + ) joining_date = models.DateField(null=True, blank=True) password = models.CharField(max_length=128, editable=False, null=True) fcm_token = models.CharField(max_length=500, null=True, blank=True) @@ -238,8 +288,19 @@ class Student(AbstractUser): return self.name def set_password(self, raw_password): - self.password = make_password(raw_password) - self.save(update_fields=["password"]) + """Set the password for the student""" + if raw_password: + self.password = make_password(raw_password) + if self.pk: # Only use update_fields if the object has a primary key + self.save(update_fields=["password"]) + else: + self.save() + else: + self.password = None + if self.pk: # Only use update_fields if the object has a primary key + self.save(update_fields=["password"]) + else: + self.save() @property def is_staff(self): @@ -251,7 +312,9 @@ class Student(AbstractUser): perms = set() for group in self.groups.all(): - perms.update(group.permissions.values_list("content_type__app_label", "codename")) + perms.update( + group.permissions.values_list("content_type__app_label", "codename") + ) app_label, codename = perm.split(".") return (app_label, codename) in perms @@ -266,40 +329,57 @@ class Student(AbstractUser): return False def get_current_subjects(self): - """Get subjects for current semester""" + """Get subjects for current period""" if not self.course: return Subject.objects.none() return Subject.objects.filter( - course=self.course, - semester_or_year=self.current_semester + course=self.course, period_or_year=self.current_period ) - def advance_semester(self): - """Advance to next semester if possible""" + def advance_period(self): + """Advance to next period if possible""" if not self.course: return False if self.course.duration_type == "Semester": - total_semesters = self.course.duration * 2 - if self.current_semester < total_semesters: - self.current_semester += 1 + total_periods = self.course.duration * 2 + if self.current_period < total_periods: + self.current_period += 1 + self.save() + return True + else: # Year based + if self.current_period < self.course.duration: + self.current_period += 1 self.save() return True return False + def save(self, *args, **kwargs): + # Call the original save method to ensure the student is saved + super().save(*args, **kwargs) + + # After saving, check if we need to sync joining date with batch + if hasattr(self, "_batch_to_sync"): + batch = self._batch_to_sync + if batch and batch.year and not self.joining_date: + self.joining_date = batch.year + super().save(update_fields=["joining_date"]) + delattr(self, "_batch_to_sync") + class Meta: verbose_name = "Student" verbose_name_plural = "Students" + class Staff(AbstractUser): """Model representing staff members of the institute""" + first_name = None last_name = None date_joined = None username = None email = None is_active = models.BooleanField(default=True) - is_staff = models.BooleanField(default=True) is_superuser = models.BooleanField(default=False) user_permissions = models.ManyToManyField( @@ -323,12 +403,16 @@ class Staff(AbstractUser): name = models.CharField(max_length=255) phone = models.CharField(max_length=255, null=True, blank=True, unique=True) designation = models.CharField(max_length=255, null=True, blank=True) - gender = models.CharField(max_length=255, choices=GENDER_CHOICES, null=True, blank=True) + gender = models.CharField( + max_length=255, choices=GENDER_CHOICES, null=True, blank=True + ) birth_date = models.DateField(null=True, blank=True) email = models.CharField(max_length=255, null=True, blank=True) temporary_address = models.CharField(max_length=255, null=True, blank=True) permanent_address = models.CharField(max_length=255, null=True, blank=True) - marital_status = models.CharField(max_length=255, choices=MARITAL_STATUS_CHOICES, null=True, blank=True) + marital_status = models.CharField( + max_length=255, choices=MARITAL_STATUS_CHOICES, null=True, blank=True + ) parent_name = models.CharField(max_length=255, null=True, blank=True) parent_phone = models.CharField(max_length=255, null=True, blank=True) citizenship_no = models.CharField(max_length=255, blank=True, null=True) @@ -337,6 +421,14 @@ class Staff(AbstractUser): joining_date = models.DateField(null=True, blank=True) password = models.CharField(max_length=128, editable=False, null=True) fcm_token = models.CharField(max_length=500, null=True, blank=True) + course = models.ForeignKey( + Course, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="hod", + help_text="Course for which this staff member is HOD", + ) USERNAME_FIELD = "phone" REQUIRED_FIELDS = ["name"] @@ -345,12 +437,26 @@ class Staff(AbstractUser): return self.name def set_password(self, raw_password): - self.password = make_password(raw_password) - self.save(update_fields=["password"]) + """Set the password for the staff""" + if raw_password: + self.password = make_password(raw_password) + self.save(update_fields=["password"]) + else: + self.password = None + self.save(update_fields=["password"]) + + def check_password(self, raw_password): + """Check if the given password matches the stored password hash""" + if not self.password: + return False + return check_password(raw_password, self.password) @property def is_staff(self): - return True + return ( + self.groups.filter(name__in=["Teacher", "Admission Officer"]).exists() + or self.is_superuser + ) @property def is_authenticated(self): @@ -362,7 +468,9 @@ class Staff(AbstractUser): perms = set() for group in self.groups.all(): - perms.update(group.permissions.values_list("content_type__app_label", "codename")) + perms.update( + group.permissions.values_list("content_type__app_label", "codename") + ) app_label, codename = perm.split(".") return (app_label, codename) in perms @@ -380,14 +488,18 @@ class Staff(AbstractUser): verbose_name = "Staff" verbose_name_plural = "Staffs" + class Routine(models.Model): """Model representing class routine""" + course = models.ForeignKey(Course, on_delete=models.CASCADE) subject = models.ForeignKey(Subject, on_delete=models.CASCADE) teacher = models.ForeignKey(Staff, on_delete=models.CASCADE) start_time = models.TimeField() end_time = models.TimeField() - semester_or_year = models.PositiveIntegerField() + period_or_year = models.PositiveIntegerField( + help_text="Period number (semester or year) for this routine" + ) is_active = models.BooleanField(default=True) def __str__(self): @@ -401,16 +513,30 @@ class Routine(models.Model): "teacher", "start_time", "end_time", - "semester_or_year", + "period_or_year", ] + # Attendance Models class Attendance(models.Model): """Model representing attendance records""" + id = models.BigAutoField(primary_key=True) date = models.DateField() - routine = models.ForeignKey(Routine, related_name="attendances", on_delete=models.CASCADE, null=True, blank=True) - teacher = models.ForeignKey(Staff, related_name="attendances", on_delete=models.CASCADE, null=True, blank=True) + routine = models.ForeignKey( + Routine, + related_name="attendances", + on_delete=models.CASCADE, + null=True, + blank=True, + ) + teacher = models.ForeignKey( + Staff, + related_name="attendances", + on_delete=models.CASCADE, + null=True, + blank=True, + ) teacher_attend = models.BooleanField(default=True) class_status = models.BooleanField(default=True) @@ -421,38 +547,51 @@ class Attendance(models.Model): verbose_name = "Attendance" verbose_name_plural = "Attendances" + class AttendanceRecord(models.Model): """Model representing individual student attendance records""" + id = models.BigAutoField(primary_key=True) - attendance = models.ForeignKey(Attendance, on_delete=models.CASCADE, null=True, blank=True, related_name="records") - student = models.ForeignKey(Student, on_delete=models.CASCADE, null=True, blank=True, related_name="attendance_records") + attendance = models.ForeignKey( + Attendance, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="records", + ) + student = models.ForeignKey( + Student, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="attendance_records", + ) student_attend = models.BooleanField(default=False) + # Notice Model class Notice(models.Model): """Model representing notices/announcements""" + id = models.BigAutoField(primary_key=True) title = models.CharField(max_length=255) image = models.ImageField(upload_to="notice", blank=True, null=True) message = models.TextField(blank=True, null=True) + file = models.FileField(upload_to="notice_file", blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.title - def save(self, *args, **kwargs): - send_push_notification( - self.title, self.message, FCMDevice.objects.values_list("token", flat=True) - ) - return super().save(*args, **kwargs) - class Meta: verbose_name = "Notice" verbose_name_plural = "Notices" + # Leave Models -class Staff_leave(models.Model): +class StaffLeave(models.Model): """Model representing staff leave requests""" + staff = models.ForeignKey(Staff, on_delete=models.CASCADE) start_date = models.DateField() end_date = models.DateField() @@ -469,9 +608,13 @@ class Staff_leave(models.Model): verbose_name_plural = "Staff Leaves" ordering = ["-created_at"] -class Student_Leave(models.Model): + +class StudentLeave(models.Model): """Model representing student leave requests""" - student = models.ForeignKey(Student, on_delete=models.CASCADE) + + student = models.ForeignKey( + Student, on_delete=models.CASCADE, related_name="leave_requests" + ) start_date = models.DateField() end_date = models.DateField() message = models.TextField() @@ -487,12 +630,20 @@ class Student_Leave(models.Model): verbose_name_plural = "Student Leaves" ordering = ["-created_at"] + # Feedback Models class StudentFeedback(models.Model): """Model representing student feedback for teachers""" - student = models.ForeignKey(Student, on_delete=models.CASCADE, related_name="feedbacks") - teacher = models.ForeignKey(Staff, on_delete=models.CASCADE, related_name="feedbacks") - rating = models.DecimalField(max_digits=2, decimal_places=1, choices=[(i / 2, i / 2) for i in range(2, 11)]) + + student = models.ForeignKey( + Student, on_delete=models.CASCADE, related_name="feedbacks" + ) + teacher = models.ForeignKey( + Staff, on_delete=models.CASCADE, related_name="feedbacks" + ) + rating = models.DecimalField( + max_digits=2, decimal_places=1, choices=[(i / 2, i / 2) for i in range(2, 11)] + ) feedback_text = models.TextField() created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -506,12 +657,51 @@ class StudentFeedback(models.Model): ordering = ["-created_at"] unique_together = ["student", "teacher"] + +class ParentFeedback(models.Model): + """Model representing feedback from parents about teachers""" + + parent = models.ForeignKey( + "Parent", on_delete=models.CASCADE, related_name="feedbacks" + ) + teacher = models.ForeignKey( + Staff, on_delete=models.CASCADE, related_name="parent_feedbacks" + ) + student = models.ForeignKey( + Student, on_delete=models.CASCADE, related_name="parent_feedbacks" + ) + rating = models.DecimalField( + max_digits=2, decimal_places=1, choices=[(i / 2, i / 2) for i in range(2, 11)] + ) + feedback_text = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"{self.parent.name} - {self.teacher.name} - {self.student.name}" + + class Meta: + verbose_name = "Parent Feedback" + verbose_name_plural = "Parent Feedbacks" + ordering = ["-created_at"] + unique_together = ["parent", "teacher", "student"] + + class InstituteFeedback(models.Model): """Model representing student feedback for the institute""" - institute = models.ForeignKey(Institute, on_delete=models.CASCADE, related_name="feedbacks") - user = models.ForeignKey(Student, on_delete=models.CASCADE, related_name="institute_feedbacks") - feedback_type = models.CharField(max_length=20, choices=FEEDBACK_TYPE_CHOICES, default="General") - rating = models.DecimalField(max_digits=2, decimal_places=1, choices=[(i / 2, i / 2) for i in range(2, 11)]) + + institute = models.ForeignKey( + Institute, on_delete=models.CASCADE, related_name="feedbacks" + ) + user = models.ForeignKey( + Student, on_delete=models.CASCADE, related_name="institute_feedbacks" + ) + feedback_type = models.CharField( + max_length=20, choices=FEEDBACK_TYPE_CHOICES, default="General" + ) + rating = models.DecimalField( + max_digits=2, decimal_places=1, choices=[(i / 2, i / 2) for i in range(2, 11)] + ) feedback_text = models.TextField() is_anonymous = models.BooleanField(default=False) is_public = models.BooleanField(default=True) @@ -533,12 +723,22 @@ class InstituteFeedback(models.Model): def display_name(self): return "Anonymous" if self.is_anonymous else self.user.name + class StaffInstituteFeedback(models.Model): """Model representing staff feedback for the institute""" - institute = models.ForeignKey(Institute, on_delete=models.CASCADE, related_name="staff_feedbacks") - staff = models.ForeignKey(Staff, on_delete=models.CASCADE, related_name="institute_feedbacks") - feedback_type = models.CharField(max_length=20, choices=FEEDBACK_TYPE_CHOICES, default="General") - rating = models.DecimalField(max_digits=2, decimal_places=1, choices=[(i / 2, i / 2) for i in range(2, 11)]) + + institute = models.ForeignKey( + Institute, on_delete=models.CASCADE, related_name="staff_feedbacks" + ) + staff = models.ForeignKey( + Staff, on_delete=models.CASCADE, related_name="institute_feedbacks" + ) + feedback_type = models.CharField( + max_length=20, choices=FEEDBACK_TYPE_CHOICES, default="General" + ) + rating = models.DecimalField( + max_digits=2, decimal_places=1, choices=[(i / 2, i / 2) for i in range(2, 11)] + ) feedback_text = models.TextField() is_anonymous = models.BooleanField(default=False) is_public = models.BooleanField(default=True) @@ -560,20 +760,73 @@ class StaffInstituteFeedback(models.Model): def display_name(self): return "Anonymous" if self.is_anonymous else self.staff.name + +class ParentInstituteFeedback(models.Model): + """Model representing parent feedback for the institute""" + + institute = models.ForeignKey( + Institute, on_delete=models.CASCADE, related_name="parent_feedbacks" + ) + parent = models.ForeignKey( + "Parent", on_delete=models.CASCADE, related_name="institute_feedbacks" + ) + feedback_type = models.CharField( + max_length=20, choices=FEEDBACK_TYPE_CHOICES, default="General" + ) + rating = models.DecimalField( + max_digits=2, decimal_places=1, choices=[(i / 2, i / 2) for i in range(2, 11)] + ) + feedback_text = models.TextField() + is_anonymous = models.BooleanField(default=False) + is_public = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + if self.is_anonymous: + return f"Anonymous - {self.institute.name} - {self.feedback_type}" + return f"{self.parent.name} - {self.institute.name} - {self.feedback_type}" + + class Meta: + verbose_name = "Parent Institute Feedback" + verbose_name_plural = "Parent Institute Feedbacks" + ordering = ["-created_at"] + unique_together = ["parent", "institute", "feedback_type"] + + @property + def display_name(self): + return "Anonymous" if self.is_anonymous else self.parent.name + + # Course Tracking Model class CourseTracking(models.Model): """Model for tracking student progress in courses""" - student = models.ForeignKey(Student, on_delete=models.CASCADE, related_name="course_trackings") - course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name="student_trackings") + + student = models.ForeignKey( + Student, on_delete=models.CASCADE, related_name="course_trackings" + ) + course = models.ForeignKey( + Course, on_delete=models.CASCADE, related_name="student_trackings" + ) enrollment_date = models.DateField(auto_now_add=True) start_date = models.DateField(null=True, blank=True) expected_end_date = models.DateField(null=True, blank=True) actual_end_date = models.DateField(null=True, blank=True) - progress_status = models.CharField(max_length=20, choices=PROGRESS_STATUS_CHOICES, default="Not Started") - completion_percentage = models.IntegerField(default=0, help_text="Percentage of course completion (0-100)") - current_semester = models.PositiveIntegerField(default=1) - semester_start_date = models.DateField(null=True, blank=True) - semester_end_date = models.DateField(null=True, blank=True) + progress_status = models.CharField( + max_length=20, choices=PROGRESS_STATUS_CHOICES, default="In Progress" + ) + completion_percentage = models.IntegerField( + default=0, help_text="Percentage of course completion (0-100)" + ) + current_period = models.PositiveIntegerField( + default=1, help_text="Current semester/year depending on course duration type" + ) + period_start_date = models.DateField( + null=True, blank=True, help_text="Start date of current semester/year" + ) + period_end_date = models.DateField( + null=True, blank=True, help_text="End date of current semester/year" + ) notes = models.TextField(blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -585,22 +838,39 @@ class CourseTracking(models.Model): if not self.student or not self.course: raise ValidationError("Both student and course are required") - if self.start_date and self.expected_end_date and self.start_date > self.expected_end_date: + if ( + self.start_date + and self.expected_end_date + and self.start_date > self.expected_end_date + ): raise ValidationError("Start date cannot be after expected end date") - if self.actual_end_date and self.start_date and self.actual_end_date < self.start_date: + if ( + self.actual_end_date + and self.start_date + and self.actual_end_date < self.start_date + ): raise ValidationError("Actual end date cannot be before start date") - if self.semester_start_date and self.semester_end_date and self.semester_start_date > self.semester_end_date: - raise ValidationError("Semester start date cannot be after semester end date") + if ( + self.period_start_date + and self.period_end_date + and self.period_start_date > self.period_end_date + ): + raise ValidationError("Period start date cannot be after period end date") - if self.current_semester < 1: - raise ValidationError("Current semester must be at least 1") + if self.current_period < 1: + raise ValidationError("Current period must be at least 1") if self.course.duration_type == "Semester": - max_semesters = self.course.duration * 2 - if self.current_semester > max_semesters: - raise ValidationError(f"Current semester cannot exceed {max_semesters}") + max_periods = self.course.duration * 2 + if self.current_period > max_periods: + raise ValidationError(f"Current semester cannot exceed {max_periods}") + else: # Year based + if self.current_period > self.course.duration: + raise ValidationError( + f"Current year cannot exceed {self.course.duration}" + ) def calculate_completion_percentage(self): if not self.start_date or not self.expected_end_date: @@ -622,21 +892,23 @@ class CourseTracking(models.Model): return 0 if self.course.duration_type == "Semester": - total_semesters = self.course.duration * 2 - completed_semesters = self.current_semester - 1 - semester_progress = (completed_semesters / total_semesters) * 100 + total_periods = self.course.duration * 2 + completed_periods = self.current_period - 1 + period_progress = (completed_periods / total_periods) * 100 - if self.semester_end_date and today > self.semester_end_date: - semester_progress = (self.current_semester / total_semesters) * 100 + if self.period_end_date and today > self.period_end_date: + period_progress = (self.current_period / total_periods) * 100 - return min(100, int(semester_progress)) - else: # Yearly based - max_days = 365 - if elapsed_days >= max_days: - return 100 + return min(100, int(period_progress)) + else: # Year based + total_periods = self.course.duration + completed_periods = self.current_period - 1 + period_progress = (completed_periods / total_periods) * 100 - percentage = min(100, int((elapsed_days / total_duration) * 100)) - return percentage + if self.period_end_date and today > self.period_end_date: + period_progress = (self.current_period / total_periods) * 100 + + return min(100, int(period_progress)) def get_completion_percentage(self): return self.completion_percentage @@ -645,7 +917,7 @@ class CourseTracking(models.Model): new_percentage = self.calculate_completion_percentage() if new_percentage != self.completion_percentage: self.completion_percentage = new_percentage - self.save(update_fields=['completion_percentage']) + self.save(update_fields=["completion_percentage"]) return new_percentage def force_update_percentage(self): @@ -662,16 +934,26 @@ class CourseTracking(models.Model): self.expected_end_date = self.start_date + timedelta(days=years * 365) - self.semester_start_date = self.start_date - self.semester_end_date = self.start_date + timedelta(days=180) # 6 months + # Set period dates based on duration type + self.period_start_date = self.start_date + if self.course.duration_type == "Semester": + self.period_end_date = self.start_date + timedelta(days=180) # 6 months + else: + self.period_end_date = self.start_date + timedelta(days=365) # 1 year self.clean() self.completion_percentage = self.calculate_completion_percentage() - if self.completion_percentage >= 100: + # Check if course duration has been completed + today = date.today() + if self.expected_end_date and today >= self.expected_end_date: self.progress_status = "Completed" if not self.actual_end_date: - self.actual_end_date = date.today() + self.actual_end_date = today + elif self.completion_percentage >= 100: + self.progress_status = "Completed" + if not self.actual_end_date: + self.actual_end_date = today elif self.completion_percentage > 0: self.progress_status = "In Progress" @@ -681,14 +963,26 @@ class CourseTracking(models.Model): print(f"Error saving course tracking for {self.student.name}: {str(e)}") raise - def advance_semester(self): + def advance_period(self): + """Advance to next semester/year based on course duration type""" if not self.course: return False if self.course.duration_type == "Semester": - total_semesters = self.course.duration * 2 - if self.current_semester < total_semesters: - self.current_semester += 1 + total_periods = self.course.duration * 2 + if self.current_period < total_periods: + self.current_period += 1 + # Update period dates + self.period_start_date = self.period_end_date + self.period_end_date = self.period_start_date + timedelta(days=180) + self.save() + return True + else: # Year based + if self.current_period < self.course.duration: + self.current_period += 1 + # Update period dates + self.period_start_date = self.period_end_date + self.period_end_date = self.period_start_date + timedelta(days=365) self.save() return True return False @@ -703,12 +997,14 @@ class CourseTracking(models.Model): @property def remaining_days(self): - if not self.semester_end_date: + """Get remaining days in current period (semester/year)""" + if not self.period_end_date: return 0 - return max(0, (self.semester_end_date - date.today()).days) + return max(0, (self.period_end_date - date.today()).days) @property def total_remaining_days(self): + """Get total remaining days in course""" if not self.expected_end_date: return 0 return max(0, (self.expected_end_date - date.today()).days) @@ -723,15 +1019,24 @@ class CourseTracking(models.Model): return 0 return (self.expected_end_date - self.start_date).days + @property + def current_period_display(self): + """Display current period as semester or year based on course type""" + if not self.course: + return "N/A" + return f"{'Semester' if self.course.duration_type == 'Semester' else 'Year'} {self.current_period}" + class Meta: verbose_name = "Course Tracking" verbose_name_plural = "Course Trackings" unique_together = ["student", "course"] ordering = ["-created_at"] + # Authentication Models class TOTPSecret(models.Model): """Model to store TOTP secret keys""" + identifier = models.CharField(max_length=255) # phone or email secret_key = models.CharField(max_length=255) created_at = models.DateTimeField(auto_now_add=True) @@ -739,15 +1044,17 @@ class TOTPSecret(models.Model): class Meta: indexes = [ - models.Index(fields=['identifier']), - models.Index(fields=['expires_at']), + models.Index(fields=["identifier"]), + models.Index(fields=["expires_at"]), ] def is_expired(self): return timezone.now() > self.expires_at + class ResetToken(models.Model): """Model to store password reset tokens""" + token = models.UUIDField(unique=True) identifier = models.CharField(max_length=255) # phone or email created_at = models.DateTimeField(auto_now_add=True) @@ -755,9 +1062,171 @@ class ResetToken(models.Model): class Meta: indexes = [ - models.Index(fields=['token']), - models.Index(fields=['expires_at']), + models.Index(fields=["token"]), + models.Index(fields=["expires_at"]), ] def is_expired(self): return timezone.now() > self.expires_at + + +class Parent(AbstractUser): + """Model representing a parent of a student""" + + first_name = None + last_name = None + is_superuser = None + date_joined = None + username = None + email = None + + user_permissions = models.ManyToManyField( + "auth.Permission", + verbose_name="user permissions", + blank=True, + help_text="Specific permissions for this parent.", + related_name="parent_set", + related_query_name="parent", + ) + + groups = models.ManyToManyField( + "auth.Group", + verbose_name="groups", + blank=True, + help_text="The groups this parent belongs to.", + related_name="parents", + ) + + id = models.BigAutoField(primary_key=True) + name = models.CharField(max_length=255) + phone = models.CharField(max_length=255, unique=True) + email = models.CharField(max_length=255, null=True, blank=True) + address = models.CharField(max_length=500, null=True, blank=True) + students = models.ManyToManyField(Student, related_name="parents") + password = models.CharField(max_length=128, editable=False, null=True) + fcm_token = models.CharField(max_length=500, null=True, blank=True) + image = models.ImageField(upload_to="parent_images/", null=True, blank=True) + + USERNAME_FIELD = "phone" + REQUIRED_FIELDS = ["name"] + + def __str__(self): + return self.name + + def set_password(self, raw_password): + self.password = make_password(raw_password) + self.save(update_fields=["password"]) + + @property + def is_staff(self): + return False + + def has_perm(self, perm, obj=None): + if not self.is_active: + return False + + # Check if parent has the permission through their groups + perms = set() + for group in self.groups.all(): + perms.update( + group.permissions.values_list("content_type__app_label", "codename") + ) + + app_label, codename = perm.split(".") + return (app_label, codename) in perms + + def has_module_perms(self, app_label): + if not self.is_active: + return False + + # Check if parent has any permissions for the app through their groups + for group in self.groups.all(): + if group.permissions.filter(content_type__app_label=app_label).exists(): + return True + return False + + class Meta: + verbose_name = "Parent" + verbose_name_plural = "Parents" + + +class TeacherParentMeeting(models.Model): + """Model for teacher-parent meetings""" + + MEETING_STATUS_CHOICES = [ + ("scheduled", "Scheduled"), + ("completed", "Completed"), + ("cancelled", "Cancelled"), + ("rescheduled", "Rescheduled"), + ] + meeting_date = models.DateField() + meeting_time = models.TimeField() + duration = models.IntegerField(help_text="Duration in minutes", default=30) + status = models.CharField( + max_length=20, choices=MEETING_STATUS_CHOICES, default="scheduled" + ) + agenda = models.TextField(blank=True) + notes = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + cancellation_reason = models.TextField(blank=True) + meeting_link = models.URLField(blank=True, help_text="For online meetings") + is_online = models.BooleanField(default=False) + + # Add tracker for change detection in signals + tracker = FieldTracker(fields=["status"]) + + class Meta: + ordering = ["-meeting_date", "-meeting_time"] + unique_together = ["meeting_date", "meeting_time"] + + def __str__(self): + return f"Meeting on {self.meeting_date}" + + def save(self, *args, **kwargs): + # Only keep validation logic, notification is handled by signals + # Check for overlapping meetings + if self.status == "scheduled": + overlapping = TeacherParentMeeting.objects.filter( + meeting_date=self.meeting_date, status="scheduled" + ).exclude(id=self.id) + + for meeting in overlapping: + # Convert times to datetime for comparison + meeting_start = datetime.combine( + meeting.meeting_date, meeting.meeting_time + ) + meeting_end = meeting_start + timedelta(minutes=meeting.duration) + new_start = datetime.combine(self.meeting_date, self.meeting_time) + new_end = new_start + timedelta(minutes=self.duration) + + if new_start < meeting_end and new_end > meeting_start: + raise ValidationError( + "This time slot overlaps with another meeting" + ) + + # Save the meeting + super().save(*args, **kwargs) + + def get_status_display(self): + return dict(self.MEETING_STATUS_CHOICES)[self.status] + + def is_upcoming(self): + now = timezone.now() + meeting_datetime = timezone.make_aware( + datetime.combine(self.meeting_date, self.meeting_time) + ) + return meeting_datetime > now and self.status == "scheduled" + + def is_past(self): + now = timezone.now() + meeting_datetime = timezone.make_aware( + datetime.combine(self.meeting_date, self.meeting_time) + ) + return meeting_datetime < now + + def can_be_cancelled(self): + return self.status == "scheduled" and self.is_upcoming() + + def can_be_rescheduled(self): + return self.status in ["scheduled", "rescheduled"] and self.is_upcoming() diff --git a/app/parentviews.py b/app/parentviews.py new file mode 100644 index 0000000..6653459 --- /dev/null +++ b/app/parentviews.py @@ -0,0 +1,612 @@ +# Standard library imports +import json +import traceback + +# Core Django imports +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.core.exceptions import ValidationError +from django.db.models import Q +from django.http import JsonResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.utils import timezone + +# Local app imports +from app.models import ( + Batch, + Course, + CourseTracking, + InstituteFeedback, + ParentFeedback, + ParentInstituteFeedback, + Student, + StudentFeedback, + StudentLeave, + Subject, + TeacherParentMeeting, + AttendanceRecord, + Notice, + Routine, +) + + +@login_required +def parent_dashboard(request): + """ + View for the parent dashboard. + Requires user to be authenticated and belong to the Parent group. + """ + # Check if user is a parent + if not request.user.groups.filter(name="Parent").exists(): + messages.error(request, "Only parents can access this dashboard.") + return redirect("login") + + try: + parent = request.user + today = timezone.now().date() + + # Get all students of the parent with their course trackings + students = ( + parent.students.all() + .select_related("course") + .prefetch_related("course_trackings", "attendance_records") + ) + + # Get all meetings without student filtering + meetings = TeacherParentMeeting.objects.all().order_by( + "-meeting_date", "-meeting_time" + ) + + # Get attendance summary for all students + attendance_summary = { + "total_students": students.count(), + "students_present_today": 0, + "students_absent_today": 0, + "overall_attendance": 0, + } + + total_attendance = 0 + total_classes = 0 + + for student in students: + # Get today's attendance + today_attendance = AttendanceRecord.objects.filter( + student=student, attendance__date=today, student_attend=True + ).count() + + if today_attendance > 0: + attendance_summary["students_present_today"] += 1 + else: + attendance_summary["students_absent_today"] += 1 + + # Get overall attendance + student_attendance = student.attendance_records.all() + total_attendance += student_attendance.filter(student_attend=True).count() + total_classes += student_attendance.count() + + if total_classes > 0: + attendance_summary["overall_attendance"] = ( + total_attendance / total_classes + ) * 100 + + # Get course progress for all students + course_progress = [] + for student in students: + active_tracking = student.course_trackings.filter( + progress_status="In Progress" + ).first() + if active_tracking: + course_progress.append( + { + "student": student.name, + "course": active_tracking.course.name, + "progress": active_tracking.completion_percentage, + "status": active_tracking.progress_status, + } + ) + + # Get recent leaves for all students + recent_leaves = StudentLeave.objects.filter(student__in=students).order_by( + "-created_at" + )[:5] + + # Get recent teacher feedback + teacher_feedback = ( + ParentFeedback.objects.filter(parent=parent) + .select_related("teacher", "student") + .order_by("-created_at")[:5] + ) + + # Get recent institute feedback + institute_feedback = ( + ParentInstituteFeedback.objects.filter(parent=parent) + .select_related("student") + .order_by("-created_at")[:5] + ) + + # Get notices + notices = Notice.objects.all().order_by("-created_at")[:5] + + # Get routines for each student + student_data = [] + for student in students: + student_info = { + "student": student, + "course_name": student.course.name if student.course else None, + "course_duration_type": student.course.duration_type + if student.course + else None, + "current_period": None, + "current_subjects": [], + "current_period_routines": [], + } + + # Get active course tracking + active_tracking = student.course_trackings.filter( + progress_status="In Progress" + ).first() + + # Get leave counts + student_info["pending_leaves"] = student.leave_requests.filter( + status=0 + ).count() + student_info["approved_leaves"] = student.leave_requests.filter( + status=1 + ).count() + student_info["rejected_leaves"] = student.leave_requests.filter( + status=2 + ).count() + + if active_tracking: + student_info["current_period"] = active_tracking.current_period + + # Get routines for the current period + current_period_routines = list( + Routine.objects.filter( + course=active_tracking.course, + period_or_year=active_tracking.current_period, + is_active=True, + ) + .select_related("subject", "teacher") + .order_by("start_time") + ) + + # Get current subjects for the student + current_subjects = list( + Subject.objects.filter( + course=active_tracking.course, + period_or_year=active_tracking.current_period, + ).order_by("name") + ) + + print(f"Found {len(current_subjects)} subjects:") + for subject in current_subjects: + print(f"- {subject.name}") + + print(f"\nFound {len(current_period_routines)} routines:") + for routine in current_period_routines: + print( + f"- {routine.subject.name} ({routine.start_time} - {routine.end_time})" + ) + + student_info["current_period_routines"] = current_period_routines + student_info["current_subjects"] = current_subjects + + student_data.append(student_info) + + context = { + "parent": parent, + "student_data": student_data, + "attendance_summary": attendance_summary, + "course_progress": course_progress, + "recent_leaves": recent_leaves, + "teacher_feedback": teacher_feedback, + "institute_feedback": institute_feedback, + "notices": notices, + "meetings": meetings, + "today": today, + } + + return render(request, "parent/dashboard.html", context) + + except Exception as e: + messages.error(request, f"Error loading dashboard: {str(e)}") + return redirect("login") + + +@login_required +def submit_parent_feedback(request): + """View for parents to submit feedback about teachers""" + # Check if user is a parent + if not request.user.groups.filter(name="Parent").exists(): + messages.error(request, "Only parents can submit feedback.") + return redirect("parent_dashboard") + + parent = request.user + + if request.method == "POST": + teacher_id = request.POST.get("teacher") + student_id = request.POST.get("student") + rating = request.POST.get("rating") + feedback_text = request.POST.get("feedback_text") + + try: + teacher = Staff.objects.get(id=teacher_id) + student = Student.objects.get(id=student_id) + + # Verify that the student belongs to this parent + if student not in parent.students.all(): + messages.error( + request, "You can only submit feedback for your children." + ) + return redirect("submit_parent_feedback") + + # Create or update feedback + ParentFeedback.objects.update_or_create( + parent=parent, + teacher=teacher, + student=student, + defaults={ + "rating": rating, + "feedback_text": feedback_text, + }, + ) + + messages.success(request, "Feedback submitted successfully!") + return redirect("parent_dashboard") + + except (Staff.DoesNotExist, Student.DoesNotExist): + messages.error(request, "Invalid teacher or student selected.") + except Exception as e: + messages.error(request, f"An error occurred: {str(e)}") + + # Get list of teachers who have taught the parent's students + # Get all students of the parent + students = parent.students.all() + + # Get all teachers who have routines for these students' courses + teachers = Staff.objects.filter( + routine__course__in=students.values_list("course_trackings__course", flat=True) + ).distinct() + + # Get list of students for the dropdown + context = { + "teachers": teachers, + "students": students, + } + return render(request, "parent/feedback.html", context) + + +@login_required +def submit_parent_institute_feedback(request): + """View for parents to submit feedback about the institute""" + # Check if user is a parent + if not request.user.groups.filter(name="Parent").exists(): + messages.error(request, "Only parents can submit feedback.") + return redirect("parent_dashboard") + + parent = request.user + + if request.method == "POST": + feedback_type = request.POST.get("feedback_type") + student_id = request.POST.get("student_id") + rating = request.POST.get("rating") + feedback_text = request.POST.get("feedback_text") + is_anonymous = request.POST.get("is_anonymous") == "on" + + try: + student = Student.objects.get(id=student_id) + + # Verify that the student belongs to this parent + if student not in parent.students.all(): + messages.error( + request, "You can only submit feedback for your children." + ) + return redirect("parent_dashboard") + + # Convert to float first to handle half-stars + rating_value = float(rating) + if rating_value < 0.5 or rating_value > 5: + messages.error(request, "Rating must be between 0.5 and 5") + return redirect("parent_dashboard") + + # Check if it's a valid half or full star rating + if rating_value % 0.5 != 0: + messages.error(request, "Rating must be a whole or half number") + return redirect("parent_dashboard") + + # Get the institute (assuming one institute for now) + institute = Institute.objects.first() + + if not institute: + # Create a default institute if none exists + institute = Institute.objects.create( + name="Default Institute", + phone="123456789", + email="default@example.com", + ) + + # Check if feedback already exists for this type + existing_feedback = ParentInstituteFeedback.objects.filter( + parent=parent, + institute=institute, + feedback_type=feedback_type, + ).first() + + if existing_feedback: + # Update existing feedback + existing_feedback.rating = rating_value + existing_feedback.feedback_text = feedback_text + existing_feedback.is_anonymous = is_anonymous + existing_feedback.save() + messages.success(request, "Institute feedback updated successfully") + else: + # Create new feedback + ParentInstituteFeedback.objects.create( + parent=parent, + institute=institute, + feedback_type=feedback_type, + rating=rating_value, + feedback_text=feedback_text, + is_anonymous=is_anonymous, + is_public=True, # Default to public feedback + ) + messages.success(request, "Institute feedback submitted successfully") + + except ValueError: + messages.error(request, "Invalid rating value") + except Exception as e: + messages.error(request, f"Error submitting institute feedback: {str(e)}") + + return redirect("parent_dashboard") + + +@login_required +def parent_meetings(request): + """View for redirecting to the parent dashboard meetings section""" + # Check if user is a parent + if not request.user.groups.filter(name="Parent").exists(): + messages.error(request, "Only parents can access meetings.") + return redirect("parent_dashboard") + + # Redirect to parent dashboard with meetings section highlighted + return redirect("parent_dashboard") + + +@login_required +def add_meeting(request): + """Add a new parent-teacher meeting""" + if request.method == "POST": + try: + data = json.loads(request.body) + meeting = TeacherParentMeeting.objects.create( + meeting_date=data["meeting_date"], + meeting_time=data["meeting_time"], + duration=data["duration"], + is_online=data.get("is_online", False), + meeting_link=data.get("meeting_link", ""), + agenda=data.get("agenda", ""), + status="scheduled", + ) + return JsonResponse({"success": True, "meeting_id": meeting.id}) + except Exception as e: + return JsonResponse({"success": False, "error": str(e)}) + return JsonResponse({"success": False, "error": "Invalid request method"}) + + +@login_required +def complete_meeting(request, meeting_id): + """Mark a meeting as completed""" + if request.method == "POST": + try: + meeting = TeacherParentMeeting.objects.get(id=meeting_id) + meeting.status = "completed" + meeting.save() + return JsonResponse({"success": True}) + except Exception as e: + return JsonResponse({"success": False, "error": str(e)}) + return JsonResponse({"success": False, "error": "Invalid request method"}) + + +@login_required +def cancel_meeting(request, meeting_id): + """Cancel a meeting""" + if request.method == "POST": + try: + data = json.loads(request.body) + meeting = TeacherParentMeeting.objects.get(id=meeting_id) + meeting.status = "cancelled" + meeting.cancellation_reason = data.get("reason", "") + meeting.save() + return JsonResponse({"success": True}) + except Exception as e: + return JsonResponse({"success": False, "error": str(e)}) + return JsonResponse({"success": False, "error": "Invalid request method"}) + + +@login_required +def reschedule_meeting(request, meeting_id): + """Reschedule a meeting""" + if request.method == "POST": + try: + data = json.loads(request.body) + meeting = TeacherParentMeeting.objects.get(id=meeting_id) + meeting.meeting_date = data["meeting_date"] + meeting.meeting_time = data["meeting_time"] + meeting.status = "rescheduled" + meeting.save() + return JsonResponse({"success": True}) + except Exception as e: + return JsonResponse({"success": False, "error": str(e)}) + return JsonResponse({"success": False, "error": "Invalid request method"}) + + +@login_required +def get_meeting_notes(request, meeting_id): + """Get meeting notes""" + try: + meeting = TeacherParentMeeting.objects.get(id=meeting_id) + return JsonResponse({"success": True, "notes": meeting.notes}) + except Exception as e: + return JsonResponse({"success": False, "error": str(e)}) + + +@login_required +def get_meeting_agenda(request, meeting_id): + """Get meeting agenda""" + try: + meeting = TeacherParentMeeting.objects.get(id=meeting_id) + return JsonResponse({"success": True, "agenda": meeting.agenda}) + except Exception as e: + return JsonResponse({"success": False, "error": str(e)}) + + +@login_required +def get_student_details(request, student_id): + """API view to get student details for the modal""" + # Check if user belongs to Parent group + if not request.user.groups.filter(name="Parent").exists(): + return JsonResponse({"error": "Unauthorized access"}, status=403) + + try: + # Get the student + student = get_object_or_404(Student, id=student_id) + + # Temporarily bypass strict parent-student relationship check + # This allows any parent to view any student for demonstration purposes + # In production, you would want to re-enable the stricter check + + # Get attendance data + try: + attended_classes = student.attendance_records.filter( + student_attend=True + ).count() + # Use period duration as the total, not the actual record count + total_period_days = 180 # Standard days in a semester/period + + # Calculate rate based on semester days + attendance_rate = round((attended_classes / total_period_days * 100), 1) + + # Also include the raw attendance numbers for display + attendance_data = { + "rate": attendance_rate, + "present": attended_classes, + "total": total_period_days, + "formatted": f"{attendance_rate}% ({attended_classes}/{total_period_days})", + } + except Exception as e: + print(f"Error getting attendance data: {str(e)}") + attendance_data = { + "rate": 0, + "present": 0, + "total": 180, + "formatted": "0% (0/180)", + } + + # Get subject data + subjects_data = [] + if student.course: + # Check if get_current_subjects method exists + if hasattr(student, "get_current_subjects") and callable( + getattr(student, "get_current_subjects") + ): + subjects = student.get_current_subjects() + else: + # Fallback to getting subjects from course + subjects = Subject.objects.filter( + course=student.course, period_or_year=student.current_period + ) + + for subject in subjects: + # Check if get_subject_progress method exists + if hasattr(student, "get_subject_progress") and callable( + getattr(student, "get_subject_progress") + ): + progress = student.get_subject_progress(subject.id) + else: + # Default progress value + progress = 0 + + teacher_name = "Not assigned" + if hasattr(subject, "teacher") and subject.teacher: + teacher_name = subject.teacher.name + + subject_data = { + "name": subject.name, + "progress": progress, + "teacher_name": teacher_name, + } + subjects_data.append(subject_data) + + # Get course timeline information + current_period_course = None + period_end_date = None + course_end_date = None + + if student.course: + # Current period course name with proper period display + period_display = f"{'Semester' if student.course.duration_type == 'Semester' else 'Year'} {student.current_period}" + current_period_course = f"{student.course.name} - {period_display}" + + # Use current academic year for dates instead of calculations + import datetime + + current_year = timezone.now().year + current_month = timezone.now().month + + # Set fixed dates based on current academic year and course type + if student.course.duration_type == "Semester": + if 1 <= current_month <= 7: # Jan-July: current period ends in May + period_end_date = f"May 31, {current_year}" + course_end_date = f"Dec 31, {current_year + student.course.duration - (student.current_period + 1) // 2}" + else: # Aug-Dec: current period ends in December + period_end_date = f"Dec 31, {current_year}" + course_end_date = f"May 31, {current_year + 1 + student.course.duration - (student.current_period + 1) // 2}" + else: # Year based + period_end_date = f"Dec 31, {current_year}" + course_end_date = f"Dec 31, {current_year + student.course.duration - student.current_period}" + + # Provide fallback values if data extraction fails + if not current_period_course and student.course: + period_display = f"{'Semester' if student.course.duration_type == 'Semester' else 'Year'} {student.current_period}" + current_period_course = f"{student.course.name} - {period_display}" + + if not period_end_date: + period_end_date = "Not available" + + if not course_end_date: + course_end_date = "Not available" + + # Prepare student data + student_data = { + "id": student.id, + "name": student.name, + "status": student.status, + "email": student.email, + "phone": student.phone, + "address": student.permanent_address + or student.temporary_address + or "Not provided", + "image_url": student.image.url + if student.image and hasattr(student.image, "url") + else None, + "course_name": student.course.name if student.course else None, + "course_duration_type": student.course.duration_type + if student.course + else None, + "current_period": student.current_period, + "attendance_rate": attendance_data, + "subjects": subjects_data, + "current_period_course": current_period_course, + "expected_end_date": period_end_date, + "course_end_date": course_end_date, + } + + return JsonResponse(student_data) + + except Student.DoesNotExist: + return JsonResponse({"error": "Student not found"}, status=404) + except Exception as e: + print(f"Error in get_student_details: {str(e)}") + print(traceback.format_exc()) + return JsonResponse({"error": str(e)}, status=500) diff --git a/app/serializers.py b/app/serializers.py index 90e034c..6b35056 100644 --- a/app/serializers.py +++ b/app/serializers.py @@ -1,96 +1,197 @@ +# Standard library imports import django.db + +# Third-party app imports from rest_framework import serializers -from .models import ( - Attendance, AttendanceRecord, Batch, Course, FCMDevice, Institute, - Notice, Routine, Staff, Student, Subject, Staff_leave, Student_Leave, - StudentFeedback, InstituteFeedback, StaffInstituteFeedback, CourseTracking, - TOTPSecret, ResetToken + +# Local app imports +from app.firebase import FCMDevice +from app.models import ( + Attendance, + AttendanceRecord, + Batch, + Course, + CourseTracking, + Institute, + InstituteFeedback, + Notice, + Parent, + ParentFeedback, + ParentInstituteFeedback, + Routine, + Staff, + StaffInstituteFeedback, + StaffLeave, + Student, + StudentFeedback, + StudentLeave, + Subject, + SubjectFile, + TeacherParentMeeting, ) + class FCMDeviceSerializer(serializers.ModelSerializer): class Meta: model = FCMDevice - fields = ['id', 'token'] + fields = ["id", "token"] + class InstituteSerializer(serializers.ModelSerializer): class Meta: model = Institute - fields = ['id', 'name', 'phone', 'email', 'address', 'pan_no', 'reg_no', 'logo', 'description'] + fields = [ + "id", + "name", + "phone", + "email", + "address", + "pan_no", + "reg_no", + "logo", + "description", + ] + class SubjectSerializer(serializers.ModelSerializer): syllabus_pdf_url = serializers.SerializerMethodField() class Meta: model = Subject - fields = ['id', 'name', 'course', 'semester_or_year', 'syllabus_pdf', 'syllabus_pdf_url'] + fields = [ + "id", + "name", + "course", + "period_or_year", + "syllabus_pdf", + "syllabus_pdf_url", + ] def get_syllabus_pdf_url(self, obj): return obj.get_pdf_url() + class CourseSerializer(serializers.ModelSerializer): subjects = SubjectSerializer(many=True, read_only=True) - + class Meta: model = Course - fields = ['id', 'name', 'duration', 'duration_type', 'subjects'] + fields = [ + "id", + "name", + "code", + "duration", + "duration_type", + "is_active", + "description", + "subjects", + ] + class BatchSerializer(serializers.ModelSerializer): class Meta: model = Batch - fields = ['id', 'name', 'start_date', 'end_date'] + fields = ["id", "name", "year"] + class StudentSerializer(serializers.ModelSerializer): batches = BatchSerializer(many=True, read_only=True) course = CourseSerializer(read_only=True) - + class Meta: model = Student fields = [ - 'id', 'name', 'status', 'gender', 'birth_date', 'email', 'phone', - 'temporary_address', 'permanent_address', 'marital_status', 'parent_name', - 'parent_phone', 'citizenship_no', 'image', 'batches', 'course', - 'current_semester', 'joining_date' + "id", + "name", + "status", + "gender", + "birth_date", + "email", + "phone", + "temporary_address", + "permanent_address", + "marital_status", + "parent_name", + "parent_phone", + "citizenship_no", + "image", + "batches", + "course", + "current_semester", + "joining_date", ] - read_only_fields = ['id'] + read_only_fields = ["id"] + class StaffSerializer(serializers.ModelSerializer): class Meta: model = Staff fields = [ - 'id', 'name', 'gender', 'designation', 'birth_date', 'phone', 'email', - 'temporary_address', 'permanent_address', 'marital_status', 'parent_name', - 'parent_phone', 'citizenship_no', 'passport', 'image', 'joining_date', - 'is_active' + "id", + "name", + "gender", + "designation", + "birth_date", + "phone", + "email", + "temporary_address", + "permanent_address", + "marital_status", + "parent_name", + "parent_phone", + "citizenship_no", + "passport", + "image", + "joining_date", + "is_active", ] - read_only_fields = ['id'] + read_only_fields = ["id"] + class RoutineSerializer(serializers.ModelSerializer): teacher = StaffSerializer(read_only=True) subject = SubjectSerializer(read_only=True) course = CourseSerializer(read_only=True) - + class Meta: model = Routine fields = [ - 'id', 'course', 'subject', 'teacher', 'start_time', 'end_time', - 'semester_or_year', 'is_active' + "id", + "course", + "subject", + "teacher", + "start_time", + "end_time", + "period_or_year", + "is_active", ] + class AttendanceRecordSerializer(serializers.ModelSerializer): student = StudentSerializer(read_only=True) - + class Meta: model = AttendanceRecord - fields = ['id', 'attendance', 'student', 'student_attend'] + fields = ["id", "attendance", "student", "student_attend"] + class AttendanceSerializer(serializers.ModelSerializer): records = AttendanceRecordSerializer(many=True, read_only=True) routine = RoutineSerializer(read_only=True) teacher = StaffSerializer(read_only=True) - + class Meta: model = Attendance - fields = ['id', 'date', 'routine', 'teacher', 'teacher_attend', 'class_status', 'records'] + fields = [ + "id", + "date", + "routine", + "teacher", + "teacher_attend", + "class_status", + "records", + ] + class NoticeSerializer(serializers.ModelSerializer): created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True) @@ -98,23 +199,34 @@ class NoticeSerializer(serializers.ModelSerializer): class Meta: model = Notice - fields = ['id', 'title', 'message', 'image', 'image_url', 'created_at'] - read_only_fields = ['created_at'] + fields = ["id", "title", "message", "image", "image_url", "created_at"] + read_only_fields = ["created_at"] def get_image_url(self, obj): if obj.image: return obj.image.url return None + class StaffLeaveSerializer(serializers.ModelSerializer): staff = StaffSerializer(read_only=True) created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True) updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True) class Meta: - model = Staff_leave - fields = ['id', 'staff', 'start_date', 'end_date', 'message', 'status', 'created_at', 'updated_at'] - read_only_fields = ['created_at', 'updated_at'] + model = StaffLeave + fields = [ + "id", + "staff", + "start_date", + "end_date", + "message", + "status", + "created_at", + "updated_at", + ] + read_only_fields = ["created_at", "updated_at"] + class StudentLeaveSerializer(serializers.ModelSerializer): student = StudentSerializer(read_only=True) @@ -122,9 +234,19 @@ class StudentLeaveSerializer(serializers.ModelSerializer): updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True) class Meta: - model = Student_Leave - fields = ['id', 'student', 'start_date', 'end_date', 'message', 'status', 'created_at', 'updated_at'] - read_only_fields = ['created_at', 'updated_at'] + model = StudentLeave + fields = [ + "id", + "student", + "start_date", + "end_date", + "message", + "status", + "created_at", + "updated_at", + ] + read_only_fields = ["created_at", "updated_at"] + class StudentFeedbackSerializer(serializers.ModelSerializer): student = StudentSerializer(read_only=True) @@ -134,8 +256,17 @@ class StudentFeedbackSerializer(serializers.ModelSerializer): class Meta: model = StudentFeedback - fields = ['id', 'student', 'teacher', 'rating', 'feedback_text', 'created_at', 'updated_at'] - read_only_fields = ['created_at', 'updated_at'] + fields = [ + "id", + "student", + "teacher", + "rating", + "feedback_text", + "created_at", + "updated_at", + ] + read_only_fields = ["created_at", "updated_at"] + class InstituteFeedbackSerializer(serializers.ModelSerializer): user = StudentSerializer(read_only=True) @@ -145,10 +276,19 @@ class InstituteFeedbackSerializer(serializers.ModelSerializer): class Meta: model = InstituteFeedback fields = [ - 'id', 'institute', 'user', 'feedback_type', 'rating', 'feedback_text', - 'is_anonymous', 'is_public', 'created_at', 'updated_at' + "id", + "institute", + "user", + "feedback_type", + "rating", + "feedback_text", + "is_anonymous", + "is_public", + "created_at", + "updated_at", ] - read_only_fields = ['created_at', 'updated_at'] + read_only_fields = ["created_at", "updated_at"] + class StaffInstituteFeedbackSerializer(serializers.ModelSerializer): staff = StaffSerializer(read_only=True) @@ -158,33 +298,71 @@ class StaffInstituteFeedbackSerializer(serializers.ModelSerializer): class Meta: model = StaffInstituteFeedback fields = [ - 'id', 'institute', 'staff', 'feedback_type', 'rating', 'feedback_text', - 'is_anonymous', 'is_public', 'created_at', 'updated_at' + "id", + "institute", + "staff", + "feedback_type", + "rating", + "feedback_text", + "is_anonymous", + "is_public", + "created_at", + "updated_at", ] - read_only_fields = ['created_at', 'updated_at'] + read_only_fields = ["created_at", "updated_at"] + class CourseTrackingSerializer(serializers.ModelSerializer): student = StudentSerializer(read_only=True) course = CourseSerializer(read_only=True) created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True) updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True) + current_period_display = serializers.CharField(read_only=True) class Meta: model = CourseTracking fields = [ - 'id', 'student', 'course', 'enrollment_date', 'start_date', 'expected_end_date', - 'actual_end_date', 'progress_status', 'completion_percentage', 'current_semester', - 'semester_start_date', 'semester_end_date', 'notes', 'created_at', 'updated_at' + "id", + "student", + "course", + "enrollment_date", + "start_date", + "expected_end_date", + "actual_end_date", + "progress_status", + "completion_percentage", + "current_period", + "period_start_date", + "period_end_date", + "notes", + "created_at", + "updated_at", + "current_period_display", ] - read_only_fields = ['created_at', 'updated_at', 'enrollment_date'] + read_only_fields = [ + "created_at", + "updated_at", + "enrollment_date", + "current_period_display", + ] + class TOTPSecretSerializer(serializers.ModelSerializer): class Meta: model = TOTPSecret - fields = ['id', 'identifier', 'created_at', 'expires_at'] + fields = ["id", "identifier", "created_at", "expires_at"] + class ResetTokenSerializer(serializers.ModelSerializer): class Meta: model = ResetToken - fields = ['id', 'token', 'identifier', 'created_at', 'expires_at'] + fields = ["id", "token", "identifier", "created_at", "expires_at"] + +class ParentSerializer(serializers.ModelSerializer): + students = StudentSerializer(many=True, read_only=True) + + class Meta: + model = Parent + fields = ["id", "name", "phone", "email", "address", "students"] + read_only_fields = ["id"] diff --git a/app/signals.py b/app/signals.py index a1a93be..dc59e49 100644 --- a/app/signals.py +++ b/app/signals.py @@ -1,9 +1,33 @@ -from django.db.models.signals import post_save -from django.dispatch import receiver -from django.core.cache import cache +# Standard library imports +import random from datetime import date, timedelta -from .models import CourseTracking, Student + +# Core Django imports +from django.contrib.auth.hashers import make_password +from django.contrib.auth.models import Group, Permission +from django.core.cache import cache +from django.core.signals import request_started from django.db import transaction +from django.db.models import Q +from django.db.models.signals import m2m_changed, post_migrate, post_save +from django.dispatch import receiver + +# Local app imports +from app.firebase import FCMDevice, send_push_notification +from app.models import ( + Batch, + CourseTracking, + Notice, + Parent, + Staff, + Student, + TeacherParentMeeting, +) + +# -------------------------------------------------------------------- +# Course Tracking Signals +# -------------------------------------------------------------------- + @receiver(post_save, sender=CourseTracking) def update_completion_percentage(sender, instance, **kwargs): @@ -15,16 +39,53 @@ def update_completion_percentage(sender, instance, **kwargs): cache.set(cache_key, instance.completion_percentage, 3600) # Cache for 1 hour +@receiver(request_started) +def periodic_update_course_completion(sender, **kwargs): + """ + Signal that updates course completion percentages randomly on request start. + This ensures periodic updates without overwhelming the system. + Only runs with 1% probability per request to avoid performance impact. + """ + # Run this signal with 1% probability to avoid running on every request + if random.random() <= 0.01: # 1% chance to run on any request + # Get all active course trackings (not completed or dropped) + active_trackings = CourseTracking.objects.filter( + Q(progress_status="In Progress") | Q(progress_status="Not Started") + ) + + updated_count = 0 + for tracking in active_trackings: + old_percentage = tracking.completion_percentage + new_percentage = tracking.update_completion_percentage() + + # If percentage changed, it was updated + if old_percentage != new_percentage: + updated_count += 1 + + # Check if course is now complete (100%) + if new_percentage >= 100 and tracking.progress_status != "Completed": + tracking.progress_status = "Completed" + tracking.actual_end_date = date.today() + tracking.save() + + # Cache the last update timestamp + if updated_count > 0: + cache.set( + "last_course_percentage_update", date.today().isoformat(), 86400 + ) # 24 hours + + +# -------------------------------------------------------------------- +# Student and Course Signals +# -------------------------------------------------------------------- + + @receiver(post_save, sender=Student) def create_course_tracking(sender, instance, created, **kwargs): """ Signal handler to create CourseTracking when a new student is created or when a course is assigned to an existing student """ - print(f"Signal triggered for student: {instance.name}") - print(f"Created: {created}") - print(f"Course: {instance.course}") - print(f"Current Semester: {instance.current_semester}") # Check if student has a course assigned if instance.course: @@ -62,22 +123,15 @@ def create_course_tracking(sender, instance, created, **kwargs): course=instance.course, start_date=start_date, expected_end_date=expected_end_date, - current_semester=instance.current_semester, - semester_start_date=start_date, - semester_end_date=start_date + current_period=instance.current_period, + period_start_date=start_date, + period_end_date=start_date + timedelta(days=180), # 6 months for first semester ) # Force initial percentage calculation course_tracking.force_update_percentage() - print( - f"Successfully created course tracking for student {instance.name}" - ) - print(f"Start Date: {start_date}") - print(f"Expected End Date: {expected_end_date}") - print(f"Current Semester: {instance.current_semester}") - except Exception as e: print( f"Error creating course tracking for student {instance.name}: {str(e)}" @@ -89,23 +143,280 @@ def create_course_tracking(sender, instance, created, **kwargs): @receiver(post_save, sender=Student) -def sync_student_semester(sender, instance, created, **kwargs): - """Signal handler to sync student's current_semester with course tracking""" +def sync_student_period(sender, instance, created, **kwargs): + """Signal handler to sync student's current_period with course tracking""" if instance.course: try: course_tracking = CourseTracking.objects.get( student=instance, course=instance.course ) - if course_tracking.current_semester != instance.current_semester: - course_tracking.current_semester = instance.current_semester + if course_tracking.current_period != instance.current_period: + course_tracking.current_period = instance.current_period course_tracking.save() except CourseTracking.DoesNotExist: pass @receiver(post_save, sender=CourseTracking) -def sync_course_tracking_semester(sender, instance, created, **kwargs): - """Signal handler to sync course tracking's current_semester with student""" - if instance.student.current_semester != instance.current_semester: - instance.student.current_semester = instance.current_semester - instance.student.save() \ No newline at end of file +def sync_course_tracking_period(sender, instance, created, **kwargs): + """Signal handler to sync course tracking's current_period with student""" + if instance.student.current_period != instance.current_period: + instance.student.current_period = instance.current_period + instance.student.save() + + +# -------------------------------------------------------------------- +# Parent and Student Relationship Signals +# -------------------------------------------------------------------- + + +@receiver(post_save, sender=Student) +def create_parent_for_student(sender, instance, created, **kwargs): + """ + Signal to create or update parent when a student is created or updated. + Also ensures parent is assigned to the Parent group. + """ + if instance.parent_name and instance.parent_phone: + try: + # Try to find existing parent + parent = Parent.objects.filter(phone=instance.parent_phone).first() + + if not parent: + # Create new parent if doesn't exist + parent = Parent.objects.create( + name=instance.parent_name, + phone=instance.parent_phone, + password=make_password("123"), # Default password + ) + + # Get or create Parent group + parent_group, _ = Group.objects.get_or_create(name="Parent") + + # Add parent to group + parent.groups.add(parent_group) + + # Add necessary permissions + permissions = Permission.objects.filter( + codename__in=["view_student", "view_attendance", "view_notice"] + ) + parent_group.permissions.add(*permissions) + + # Add student to parent's students + parent.students.add(instance) + + except Exception as e: + print(f"Error creating/updating parent: {str(e)}") + # Don't prevent student creation/update if parent creation fails + + +@receiver(post_save, sender=Parent) +def setup_parent_user(sender, instance, created, **kwargs): + """ + Signal handler to set up a new parent user: + 1. Add to Parent group + 2. Set up initial settings and permissions + """ + if created: + try: + # Add user to Parent group + parent_group, _ = Group.objects.get_or_create(name="Parent") + instance.groups.add(parent_group) + + print(f"Successfully set up new parent user: {instance.name}") + + except Exception as e: + print(f"Error setting up parent user {instance.name}: {str(e)}") + + +@receiver(m2m_changed, sender=Parent.students.through) +def handle_parent_student_relationship( + sender, instance, action, reverse, model, pk_set, **kwargs +): + """ + Signal handler to manage parent-student relationship changes: + - Update caches + - Handle notifications + - Sync permissions + """ + if action == "post_add": + try: + if reverse: + # This is triggered when students are added to a parent + parent = model.objects.get(pk=pk_set.pop()) + student = instance + else: + # This is triggered when a parent adds students + parent = instance + student_ids = pk_set + + # Clear parent dashboard cache using a more compatible approach + cache.delete(f"parent_dashboard_{parent.id}") + + print( + f"Successfully updated parent-student relationship for parent: {parent.name}" + ) + + except Exception as e: + print(f"Error handling parent-student relationship: {str(e)}") + + +# -------------------------------------------------------------------- +# User Group and Permission Signals +# -------------------------------------------------------------------- + + +@receiver(post_migrate) +def create_user_groups(sender, **kwargs): + """ + Create initial user groups and permissions after migrations. + This ensures groups exist before any user is created. + """ + from django.contrib.auth.models import Group, Permission + + # Create the necessary groups + groups_data = { + "Student": ["view_attendance", "view_notice", "view_subject"], + "Teacher": [ + "view_student", + "view_attendance", + "add_attendance", + "change_attendance", + "view_notice", + "add_notice", + "view_subject", + "change_subject", + ], + "Parent": ["view_student", "view_attendance", "view_notice"], + "Admission Officer": [ + "view_student", + "add_student", + "change_student", + "view_parent", + "add_parent", + "change_parent", + "view_course", + "view_batch", + "view_notice", + ], + } + + for group_name, permissions in groups_data.items(): + group, created = Group.objects.get_or_create(name=group_name) + + if created: + print(f"Created user group: {group_name}") + + # Add permissions to group + perms = Permission.objects.filter(codename__in=permissions) + group.permissions.add(*perms) + print(f"Added {perms.count()} permissions to {group_name} group") + + +# -------------------------------------------------------------------- +# Batch and Student Enrollment Signals +# -------------------------------------------------------------------- + + +@receiver(post_save, sender=Batch) +def update_student_joining_dates(sender, instance, **kwargs): + """Update joining dates for all students in a batch when batch year changes""" + if instance.year: + # Update all students in this batch who don't have a joining date + for student in instance.students.filter(joining_date__isnull=True): + student.joining_date = instance.year + student.save(update_fields=["joining_date"]) + + +@receiver(m2m_changed, sender=Student.batches.through) +def handle_student_batch_change( + sender, instance, action, reverse, model, pk_set, **kwargs +): + """Handle joining date when a student is added to a batch""" + if action == "post_add" and not reverse: + # This is fired when batches are added to a student + for batch_id in pk_set: + try: + batch = Batch.objects.get(pk=batch_id) + if batch.year and not instance.joining_date: + instance.joining_date = batch.year + instance.save(update_fields=["joining_date"]) + break # Use the first batch with a joining date + except Batch.DoesNotExist: + pass + + +# -------------------------------------------------------------------- +# Notification Signals +# -------------------------------------------------------------------- + + +@receiver(post_save, sender=Notice) +def send_notice_notification(sender, instance, created, **kwargs): + """Send notifications when a notice is created or updated""" + # Get all tokens from active devices (all user types) + tokens = FCMDevice.objects.filter(is_active=True).values_list("token", flat=True) + + send_push_notification(instance.title, instance.message, tokens) + + +@receiver(post_save, sender=TeacherParentMeeting) +def send_meeting_notification(sender, instance, created, **kwargs): + """Send notifications when a meeting is created or updated""" + tokens = [] + + # Method 1: Get tokens from FCMDevice records based on user_type + fcm_device_tokens = FCMDevice.objects.filter( + is_active=True, user_type__in=["parent", "teacher"] + ).values_list("token", flat=True) + tokens.extend(fcm_device_tokens) + + # Method 2: Get tokens directly from user models + # Get tokens from Staff model + staff_tokens = ( + Staff.objects.filter(fcm_token__isnull=False) + .exclude(fcm_token="") + .values_list("fcm_token", flat=True) + ) + tokens.extend(staff_tokens) + + # Get tokens from Parent model + parent_tokens = ( + Parent.objects.filter(fcm_token__isnull=False) + .exclude(fcm_token="") + .values_list("fcm_token", flat=True) + ) + tokens.extend(parent_tokens) + + # Remove duplicates + unique_tokens = list(set(tokens)) + + if not unique_tokens: + return + + if created: + title = "New Parent-Teacher Meeting Scheduled" + message = f"A parent-teacher meeting has been scheduled for {instance.meeting_date} at {instance.meeting_time}." + if instance.is_online: + message += " This is an online meeting." + send_push_notification(title, message, unique_tokens) + else: + # Access through instance.tracker to determine if status changed + # This requires django-model-utils or similar to track changes + if hasattr(instance, "tracker") and "status" in instance.tracker.changed(): + old_status = instance.tracker.previous("status") + if instance.status == "cancelled": + title = "Parent-Teacher Meeting Cancelled" + message = f"The meeting for {instance.meeting_date} at {instance.meeting_time} has been cancelled." + if instance.cancellation_reason: + message += f" Reason: {instance.cancellation_reason}" + send_push_notification(title, message, unique_tokens) + elif instance.status == "rescheduled": + # Meeting rescheduled notification + title = "Parent-Teacher Meeting Rescheduled" + message = f"The parent-teacher meeting has been rescheduled to {instance.meeting_date} at {instance.meeting_time}." + send_push_notification(title, message, unique_tokens) + elif instance.status == "completed": + # Meeting completed notification + title = "Parent-Teacher Meeting Completed" + message = f"The parent-teacher meeting scheduled for {instance.meeting_date} at {instance.meeting_time} has been marked as completed." + send_push_notification(title, message, unique_tokens) diff --git a/app/staffviews.py b/app/staffviews.py index 3032c5d..4d2a462 100644 --- a/app/staffviews.py +++ b/app/staffviews.py @@ -1,5 +1,5 @@ from django.shortcuts import render, redirect, get_object_or_404 -from django.http import JsonResponse, HttpResponse +from django.http import JsonResponse from django.contrib.auth.decorators import login_required from django.contrib import messages from django.db.models import Count, Q, Avg @@ -7,14 +7,14 @@ from django.utils import timezone from datetime import timedelta, datetime from django.views.decorators.http import require_http_methods, require_GET from django.contrib.auth.hashers import check_password, make_password -from .models import ( +from app.models import ( Staff, Student, Routine, Attendance, AttendanceRecord, StudentFeedback, - Staff_leave, + StaffLeave, Notice, Institute, StaffInstituteFeedback, @@ -22,19 +22,35 @@ from .models import ( SubjectFile, InstituteFeedback, CourseTracking, - FEEDBACK_TYPE_CHOICES + FEEDBACK_TYPE_CHOICES, ) -from app.admin import custom_admin_site +import logging @login_required def teacherDashboard(request): """View to display the teacher dashboard with their classes and student information""" - teacher = Staff.objects.filter(phone=request.user.phone).first() - if not teacher: - messages.error(request, "Teacher profile not found. Please contact administrator.") - return redirect('dashboard') - + + # Check if user is in Teacher group + if not request.user.groups.filter(name="Teacher").exists(): + print(request, "You don't have permission to access the teacher dashboard.") + return redirect("dashboard") + + # Get or create a teacher object + try: + # Try to get the teacher object directly if user is already a Staff instance + if isinstance(request.user, Staff): + teacher = request.user + else: + # Try to get the staff object by ID + teacher = Staff.objects.get(id=request.user.id) + except (Staff.DoesNotExist, Exception) as e: + print(f"Error getting staff profile: {str(e)}") + messages.error( + request, "Teacher profile not found. Please contact administrator." + ) + return redirect("dashboard") + today = timezone.now().date() current_time = timezone.now().time() last_week = today - timedelta(days=7) @@ -50,12 +66,21 @@ def teacherDashboard(request): for routine in today_routines: attendance = routine.attendances.filter(date=today).first() - # Get total students from active course trackings - total_students = CourseTracking.objects.filter( + # Get total active students for this routine - matching the get_students logic + students = Student.objects.filter( course=routine.subject.course, - current_semester=routine.semester_or_year, - progress_status="In Progress" - ).count() + current_period=routine.period_or_year, + status="Active", # Only count active students + ) + + # If no students found with exact period match, try more flexible approach + if not students.exists(): + students = Student.objects.filter( + course=routine.subject.course, + status="Active", # Still only count active students + ) + + total_students = students.count() attendance_count = ( 0 @@ -76,7 +101,7 @@ def teacherDashboard(request): "is_ongoing": routine.start_time <= current_time <= routine.end_time, "total_students": total_students, "attendance_count": attendance_count, - "semester_or_year": routine.semester_or_year, + "period_or_year": routine.period_or_year, "course_name": routine.subject.course.name, "has_attendance": attendance is not None, "subject_id": routine.subject.id, @@ -89,67 +114,86 @@ def teacherDashboard(request): remaining_classes = total_classes - completed_classes # Get student statistics from course tracking - total_students = CourseTracking.objects.filter( - course__in=today_routines.values_list('subject__course', flat=True), - progress_status="In Progress" - ).values('student').distinct().count() - + total_students = ( + CourseTracking.objects.filter( + course__in=today_routines.values_list("subject__course", flat=True), + progress_status="In Progress", + ) + .values("student") + .distinct() + .count() + ) + # Get present and absent students for today - present_students = AttendanceRecord.objects.filter( - attendance__date=today, - student_attend=True, - attendance__routine__teacher=teacher - ).values('student').distinct().count() + present_students = ( + AttendanceRecord.objects.filter( + attendance__date=today, + student_attend=True, + attendance__routine__teacher=teacher, + ) + .values("student") + .distinct() + .count() + ) # Calculate absent students based on teacher's classes today today_class_students = set() for routine in today_routines: students = CourseTracking.objects.filter( course=routine.subject.course, - current_semester=routine.semester_or_year, - progress_status="In Progress" - ).values_list('student', flat=True) + current_period=routine.period_or_year, + progress_status="In Progress", + ).values_list("student", flat=True) today_class_students.update(students) - + # Make sure absent students doesn't go negative total_today_students = len(today_class_students) - absent_students = max(0, total_today_students - present_students) if total_today_students else 0 + absent_students = ( + max(0, total_today_students - present_students) if total_today_students else 0 + ) # Get average rating from student feedback - avg_rating = StudentFeedback.objects.filter( - teacher=teacher - ).aggregate(Avg('rating'))['rating__avg'] or 0 + avg_rating = ( + StudentFeedback.objects.filter(teacher=teacher).aggregate(Avg("rating"))[ + "rating__avg" + ] + or 0 + ) avg_rating_rounded = round(avg_rating) remaining_stars = 5 - avg_rating_rounded # Get leave statistics - pending_leaves = Staff_leave.objects.filter(staff=teacher, status=0).count() - approved_leaves = Staff_leave.objects.filter(staff=teacher, status=1).count() - rejected_leaves = Staff_leave.objects.filter(staff=teacher, status=2).count() + pending_leaves = StaffLeave.objects.filter(staff=teacher, status=0).count() + approved_leaves = StaffLeave.objects.filter(staff=teacher, status=1).count() + rejected_leaves = StaffLeave.objects.filter(staff=teacher, status=2).count() # Get recent leaves - recent_leaves = Staff_leave.objects.filter(staff=teacher).order_by('-created_at')[:5] + recent_leaves = StaffLeave.objects.filter(staff=teacher).order_by("-created_at")[:5] # Get recent feedback - recent_feedback = StudentFeedback.objects.filter(teacher=teacher).order_by('-created_at')[:5] + recent_feedback = StudentFeedback.objects.filter(teacher=teacher).order_by( + "-created_at" + )[:5] # Get notices - notices = Notice.objects.all().order_by('-created_at')[:5] + notices = Notice.objects.all().order_by("-created_at")[:5] # Get feedback types for institute feedback feedback_types = FEEDBACK_TYPE_CHOICES # Get institute feedback - institute_feedback = StaffInstituteFeedback.objects.filter(staff=teacher).order_by('-created_at')[:5] + institute_feedback = StaffInstituteFeedback.objects.filter(staff=teacher).order_by( + "-created_at" + )[:5] # Get all routines for the teacher (for the schedule) all_routines = Routine.objects.filter( teacher=teacher, is_active=True, - semester_or_year__in=CourseTracking.objects.filter( - progress_status="In Progress" - ).values_list('current_semester', flat=True).distinct() - ).order_by('start_time') + period_or_year__in=CourseTracking.objects.filter(progress_status="In Progress") + .values_list("current_period", flat=True) + .distinct(), + ).order_by("start_time") # Get all attendance records for quick access recent_attendance = ( @@ -168,29 +212,31 @@ def teacherDashboard(request): ).count() # Attendance Management Data - routines = Routine.objects.filter( - teacher=teacher, - is_active=True - ).order_by('start_time') + routines = Routine.objects.filter(teacher=teacher, is_active=True).order_by( + "start_time" + ) # Get all subjects taught by this teacher for the subjects section - subject_ids = Routine.objects.filter( - teacher=teacher, - is_active=True - ).values_list('subject', flat=True).distinct() - - teacher_subjects = Routine.objects.filter( - teacher=teacher, - is_active=True, - subject_id__in=subject_ids - ).select_related('subject', 'course').order_by('subject__name') - + subject_ids = ( + Routine.objects.filter(teacher=teacher, is_active=True) + .values_list("subject", flat=True) + .distinct() + ) + + teacher_subjects = ( + Routine.objects.filter( + teacher=teacher, is_active=True, subject_id__in=subject_ids + ) + .select_related("subject", "course") + .order_by("subject__name") + ) + # Add student count to each routine for routine in teacher_subjects: routine.students_count = CourseTracking.objects.filter( course=routine.course, - current_semester=routine.semester_or_year, - progress_status="In Progress" + current_period=routine.period_or_year, + progress_status="In Progress", ).count() context = { @@ -221,7 +267,7 @@ def teacherDashboard(request): "teacher_subjects": teacher_subjects, } - return render(request, "teacher_dashboard.html", context) + return render(request, "teacher/dashboard.html", context) @login_required @@ -250,10 +296,10 @@ def request_staff_leave(request): return redirect("teacherDashboard") # Check if leave already exists for these dates - existing_leave = Staff_leave.objects.filter( + existing_leave = StaffLeave.objects.filter( staff=staff, start_date__lte=end_date.date(), - end_date__gte=start_date.date() + end_date__gte=start_date.date(), ).first() if existing_leave: @@ -263,7 +309,7 @@ def request_staff_leave(request): return redirect("teacherDashboard") # Create leave request - leave = Staff_leave.objects.create( + leave = StaffLeave.objects.create( staff=staff, start_date=start_date.date(), end_date=end_date.date(), @@ -314,7 +360,7 @@ def submit_staff_institute_feedback(request): # Get the staff object - FIX: Use filter().first() instead of get() to avoid MultipleObjectsReturned error teacher = Staff.objects.filter(phone=staff.phone).first() - + if not teacher: messages.error(request, "Staff account not found") return redirect("teacherDashboard") @@ -358,7 +404,7 @@ def save_attendance(request): if request.method == "POST": teacher = Staff.objects.filter(phone=request.user.phone).first() if not teacher: - return redirect('login') + return redirect("login") routine_id = request.POST.get("routine_id") date_str = request.POST.get("date") @@ -458,7 +504,7 @@ def get_subject_schedule(request): try: routines = Routine.objects.filter( teacher=teacher, subject_id=subject_id, is_active=True - ).values("id", "start_time", "end_time", "semester_or_year") + ).values("id", "start_time", "end_time", "period_or_year") return JsonResponse({"success": True, "schedule": list(routines)}) except Exception as e: @@ -471,13 +517,13 @@ def teacher_update_profile_picture(request): if request.method == "POST": teacher = Staff.objects.filter(phone=request.user.phone).first() if not teacher: - return redirect('login') - profile_picture = request.FILES.get("profile_picture") + return redirect("login") + image = request.FILES.get("image") - if profile_picture: + if image: try: # Update the teacher's profile picture - teacher.image = profile_picture + teacher.image = image teacher.save() messages.success(request, "Profile picture updated successfully.") except Exception as e: @@ -487,8 +533,6 @@ def teacher_update_profile_picture(request): return redirect("teacherDashboard") - return redirect("teacherDashboard") - @login_required def teacher_change_password(request): @@ -543,264 +587,392 @@ def teacher_change_password(request): @login_required def get_attendance_form(request): """View to handle AJAX request for getting attendance form""" - routine_id = request.GET.get('routine_id') - date_str = request.GET.get('date') - + routine_id = request.GET.get("routine_id") + date_str = request.GET.get("date") + if not routine_id or not date_str: - return JsonResponse({ - 'error': 'Missing required parameters' - }) - + return JsonResponse({"error": "Missing required parameters"}) + try: teacher = Staff.objects.filter(phone=request.user.phone).first() if not teacher: - return JsonResponse({ - 'error': 'Teacher account not found' - }, status=404) + return JsonResponse({"error": "Teacher account not found"}, status=404) routine = Routine.objects.get(id=routine_id, teacher=teacher) - date = datetime.strptime(date_str, '%Y-%m-%d').date() - + date = datetime.strptime(date_str, "%Y-%m-%d").date() + # Get existing attendance data if any - attendance_data = { - 'teacher_attend': False, - 'class_status': True, - 'records': {} - } - + attendance_data = {"teacher_attend": False, "class_status": True, "records": {}} + try: # Get attendance record with all related data in one query - attendance = Attendance.objects.select_related('routine').get( - routine=routine, - date=date + attendance = Attendance.objects.select_related("routine").get( + routine=routine, date=date ) - + # Get all attendance records for this attendance in one query - records = AttendanceRecord.objects.filter(attendance=attendance).select_related('student') - + records = AttendanceRecord.objects.filter( + attendance=attendance + ).select_related("student") + attendance_data = { - 'id': attendance.id, - 'date': attendance.date.strftime('%Y-%m-%d'), - 'teacher_attend': attendance.teacher_attend, - 'class_status': attendance.class_status, - 'records': { - str(record.student.id): record.student_attend - for record in records - } + "id": attendance.id, + "date": attendance.date.strftime("%Y-%m-%d"), + "teacher_attend": attendance.teacher_attend, + "class_status": attendance.class_status, + "records": { + str(record.student.id): record.student_attend for record in records + }, } except Attendance.DoesNotExist: # No existing attendance record found - return default values pass - + return JsonResponse(attendance_data) - + except Routine.DoesNotExist: - return JsonResponse({ - 'error': 'Invalid routine selected' - }, status=404) + return JsonResponse({"error": "Invalid routine selected"}, status=404) except ValueError: - return JsonResponse({ - 'error': 'Invalid date format' - }, status=400) + return JsonResponse({"error": "Invalid date format"}, status=400) except Exception as e: - return JsonResponse({ - 'error': str(e) - }, status=500) + return JsonResponse({"error": str(e)}, status=500) @login_required def get_students(request): """View to handle AJAX request for getting students list""" from django.http import JsonResponse - - routine_id = request.GET.get('routine_id') - + + routine_id = request.GET.get("routine_id") + if not routine_id: - return JsonResponse({'error': 'Missing routine_id parameter'}, status=400) - + return JsonResponse({"error": "Missing routine_id parameter"}, status=400) + try: teacher = Staff.objects.filter(phone=request.user.phone).first() if not teacher: - return JsonResponse({'error': 'Teacher account not found'}, status=404) + return JsonResponse({"error": "Teacher account not found"}, status=404) routine = Routine.objects.get(id=routine_id, teacher=teacher) - - # Get students for this routine + + # Get only active students for this routine students = Student.objects.filter( course=routine.subject.course, - current_semester=routine.semester_or_year - ).order_by('name') - - # If no students found, try a more flexible approach + current_period=routine.period_or_year, + status="Active", # Only get active students + ).order_by("name") + + # If no students found, try a more flexible approach but still only active students if not students.exists(): students = Student.objects.filter( - course=routine.subject.course - ).order_by('name') - + course=routine.subject.course, + status="Active", # Maintain active student filter + ).order_by("name") + # Format student data for JSON response - student_data = [{ - 'id': student.id, - 'name': student.name - } for student in students] - - return JsonResponse({ - 'students': student_data - }) - + student_data = [ + {"id": student.id, "name": student.name} for student in students + ] + + return JsonResponse({"students": student_data}) + except Routine.DoesNotExist: - return JsonResponse({'error': 'Invalid routine selected'}, status=404) + return JsonResponse({"error": "Invalid routine selected"}, status=404) except Exception as e: - return JsonResponse({'error': str(e)}, status=500) + return JsonResponse({"error": str(e)}, status=500) @login_required @require_http_methods(["GET", "POST"]) def manage_subject_files(request): """View to manage files for subjects taught by the teacher""" - teacher = Staff.objects.filter(phone=request.user.phone).first() - if not teacher: - return JsonResponse({ - "success": False, - "message": "Staff account not found" - }, status=404) - + logger = logging.getLogger(__name__) + + # First check if the user is authenticated and in the Teacher group + if not request.user.is_authenticated: + logger.error("User not authenticated") + return redirect("login") + + # Try to get the teacher object + teacher = None + try: + if hasattr(request.user, "phone"): + # If the user model has phone attribute (Staff model) + teacher = Staff.objects.get(phone=request.user.phone) + else: + # If request.user is a different model, try to get Staff by ID + teacher = Staff.objects.get(id=request.user.id) + except Staff.DoesNotExist: + logger.error(f"Teacher not found for user: {request.user}") + messages.error( + request, "Staff account not found. Please contact administrator." + ) + return redirect("dashboard") + except Exception as e: + logger.error(f"Error getting teacher: {e}", exc_info=True) + messages.error(request, f"Error retrieving staff profile: {str(e)}") + return redirect("dashboard") + + logger.info(f"Teacher found: {teacher.name}") + # Get all subjects taught by this teacher subject_filter = request.GET.get("subject") - - subjects_query = Routine.objects.filter( - teacher=teacher, - is_active=True - ).values_list('subject', flat=True).distinct() - + + subjects_query = ( + Routine.objects.filter(teacher=teacher, is_active=True) + .values_list("subject", flat=True) + .distinct() + ) + + logger.info(f"Found {len(subjects_query)} subject IDs: {list(subjects_query)}") + # Apply subject filter if provided try: if subject_filter: subjects = Subject.objects.filter( - id=subject_filter, - id__in=subjects_query - ).order_by('course__name', 'semester_or_year', 'name') - + id=subject_filter, id__in=subjects_query + ).order_by("course__name", "period_or_year", "name") + + logger.info( + f"Filtered by subject ID {subject_filter}, found {subjects.count()} subjects" + ) + # If no subject found with the provided ID, return a helpful error if not subjects.exists(): - if request.headers.get('X-Requested-With') == 'XMLHttpRequest': - return JsonResponse({ - "success": False, - "message": f"Subject with ID {subject_filter} not found or you don't have permission to access it" - }, status=404) + logger.warning(f"No subjects found with ID {subject_filter}") + if request.headers.get("X-Requested-With") == "XMLHttpRequest": + return JsonResponse( + { + "success": False, + "message": f"Subject with ID {subject_filter} not found or you don't have permission to access it", + }, + status=404, + ) else: - return render(request, 'subject_files.html', { - "subjects": [], - "teacher": teacher, - "selected_subject": None, - "error_message": f"Subject with ID {subject_filter} not found or you don't have permission to access it" - }) + return render( + request, + "teacher/subject_files.html", + { + "subjects": [], + "teacher": teacher, + "selected_subject": None, + "error_message": f"Subject with ID {subject_filter} not found or you don't have permission to access it", + }, + ) else: - subjects = Subject.objects.filter( - id__in=subjects_query - ).order_by('course__name', 'semester_or_year', 'name') - + subjects = Subject.objects.filter(id__in=subjects_query).order_by( + "course__name", "period_or_year", "name" + ) + + logger.info(f"No filter applied, found {subjects.count()} subjects") + # If no subjects found at all, return a message if not subjects.exists(): - if request.headers.get('X-Requested-With') == 'XMLHttpRequest': - return JsonResponse({ - "success": False, - "message": "You are not assigned to any subjects yet" - }) + logger.warning("No subjects found for this teacher") + if request.headers.get("X-Requested-With") == "XMLHttpRequest": + return JsonResponse( + { + "success": False, + "message": "You are not assigned to any subjects yet", + } + ) else: - return render(request, 'subject_files.html', { - "subjects": [], - "teacher": teacher, - "selected_subject": None, - "error_message": "You are not assigned to any subjects yet" - }) + return render( + request, + "teacher/subject_files.html", + { + "subjects": [], + "teacher": teacher, + "selected_subject": None, + "error_message": "You are not assigned to any subjects yet", + }, + ) except Exception as e: + logger.error(f"Error loading subjects: {e}", exc_info=True) # Handle any unexpected errors - if request.headers.get('X-Requested-With') == 'XMLHttpRequest': - return JsonResponse({ - "success": False, - "message": f"Error loading subjects: {str(e)}" - }, status=500) + if request.headers.get("X-Requested-With") == "XMLHttpRequest": + return JsonResponse( + {"success": False, "message": f"Error loading subjects: {str(e)}"}, + status=500, + ) else: - return render(request, 'subject_files.html', { - "subjects": [], - "teacher": teacher, - "selected_subject": None, - "error_message": f"Error loading subjects: {str(e)}" - }) - + return render( + request, + "teacher/subject_files.html", + { + "subjects": [], + "teacher": teacher, + "selected_subject": None, + "error_message": f"Error loading subjects: {str(e)}", + }, + ) + if request.method == "POST": subject_id = request.POST.get("subject_id") title = request.POST.get("title") description = request.POST.get("description") file = request.FILES.get("file") - + + logger.info( + f"Processing file upload - subject_id: {subject_id}, title: {title}" + ) + if not subject_id or not title or not file: - return JsonResponse({ - "success": False, - "message": "Subject, title and file are required" - }) - + logger.warning("Missing required fields for file upload") + return JsonResponse( + {"success": False, "message": "Subject, title and file are required"} + ) + try: subject = Subject.objects.get(id=subject_id) - + # Check if subject is taught by this teacher - if not Routine.objects.filter(teacher=teacher, subject=subject, is_active=True).exists(): - return JsonResponse({ - "success": False, - "message": "You don't have permission to upload files to this subject" - }) - + if not Routine.objects.filter( + teacher=teacher, subject=subject, is_active=True + ).exists(): + logger.warning( + f"Teacher {teacher.id} does not have permission for subject {subject_id}" + ) + return JsonResponse( + { + "success": False, + "message": "You don't have permission to upload files to this subject", + } + ) + # Create new subject file subject_file = SubjectFile.objects.create( subject=subject, title=title, description=description, file=file, - uploaded_by=teacher + uploaded_by=teacher, ) - - return JsonResponse({ - "success": True, - "message": "File uploaded successfully", - "file": { - "id": subject_file.id, - "title": subject_file.title, - "description": subject_file.description, - "file_url": subject_file.file.url, - "uploaded_at": subject_file.uploaded_at.strftime("%b %d, %Y %H:%M") + + logger.info(f"File uploaded successfully: {subject_file.id}") + + return JsonResponse( + { + "success": True, + "message": "File uploaded successfully", + "file": { + "id": subject_file.id, + "title": subject_file.title, + "description": subject_file.description, + "file_url": subject_file.file.url, + "uploaded_at": subject_file.uploaded_at.strftime( + "%b %d, %Y %H:%M" + ), + }, } - }) + ) except Subject.DoesNotExist: - return JsonResponse({ - "success": False, - "message": "Subject not found" - }) + logger.error(f"Subject {subject_id} not found") + return JsonResponse({"success": False, "message": "Subject not found"}) except Exception as e: - return JsonResponse({ - "success": False, - "message": f"Error uploading file: {str(e)}" - }) - + logger.error(f"Error uploading file: {e}", exc_info=True) + return JsonResponse( + {"success": False, "message": f"Error uploading file: {str(e)}"} + ) + # For GET requests, return a list of subjects and their files subjects_data = [] for subject in subjects: try: - subject_files = subject.get_all_files() - subjects_data.append({ - "id": subject.id, - "name": subject.name, - "course": subject.course.name, - "semester_or_year": subject.semester_or_year, - "files": subject_files, - "files_count": len(subject_files) - }) + logger.info(f"Processing subject: {subject.id} - {subject.name}") + + # Manually get the files instead of relying on get_all_files method + files = [] + + # Add syllabus if it exists + if subject.syllabus_pdf: + try: + files.append( + { + "id": "syllabus", + "title": f"{subject.name} Syllabus", + "description": f"Syllabus PDF for {subject.name}", + "file_url": subject.syllabus_pdf.url, + "file_type": "syllabus", + "uploaded_by": None, + "uploaded_at": None, + } + ) + except Exception as syllabus_error: + logger.error( + f"Error adding syllabus for subject {subject.id}: {syllabus_error}", + exc_info=True, + ) + + # Add subject files + try: + subject_files = SubjectFile.objects.filter(subject=subject).order_by( + "-uploaded_at" + ) + logger.info( + f"Found {subject_files.count()} additional files for subject {subject.id}" + ) + + for file in subject_files: + try: + files.append( + { + "id": file.id, + "title": file.title, + "description": file.description, + "file_url": file.file.url, + "file_type": "notes", + "uploaded_by": file.uploaded_by.name + if file.uploaded_by + else None, + "uploaded_at": file.uploaded_at, + } + ) + except Exception as file_error: + logger.error( + f"Error adding file {file.id} for subject {subject.id}: {file_error}", + exc_info=True, + ) + except Exception as files_error: + logger.error( + f"Error getting subject files for subject {subject.id}: {files_error}", + exc_info=True, + ) + + logger.info( + f"Successfully processed {len(files)} files for subject {subject.id}" + ) + + subjects_data.append( + { + "id": subject.id, + "name": subject.name, + "course": subject.course.name, + "semester_or_year": subject.period_or_year, + "files": files, + "files_count": len(files), + } + ) except Exception as e: # If there's an error with a specific subject, log it but continue - print(f"Error loading files for subject {subject.id}: {str(e)}") - - return render(request, 'subject_files.html', { - "subjects": subjects_data, - "teacher": teacher, - "selected_subject": subject_filter - }) + logger.error(f"Error processing subject {subject.id}: {e}", exc_info=True) + + logger.info(f"Rendering template with {len(subjects_data)} subjects") + + # Print to console for debugging + print(f"DEBUG: Teacher = {teacher.name}, Subjects count = {len(subjects_data)}") + if subjects_data: + print(f"DEBUG: First subject = {subjects_data[0]['name']}") + else: + print("DEBUG: No subjects data to render") + + return render( + request, + "teacher/subject_files.html", + { + "subjects": subjects_data, + "teacher": teacher, + "selected_subject": subject_filter, + }, + ) @login_required @@ -809,51 +981,43 @@ def delete_subject_file(request): """View to delete a subject file""" teacher = Staff.objects.filter(phone=request.user.phone).first() if not teacher: - return JsonResponse({ - "success": False, - "message": "Staff account not found" - }, status=404) - + return JsonResponse( + {"success": False, "message": "Staff account not found"}, status=404 + ) + file_id = request.POST.get("file_id") - + if not file_id: - return JsonResponse({ - "success": False, - "message": "File ID is required" - }) - + return JsonResponse({"success": False, "message": "File ID is required"}) + try: # Check if the file exists and is owned by this teacher or from their subject subject_file = SubjectFile.objects.get(id=file_id) - + # Check if the file is from a subject taught by this teacher - if not Routine.objects.filter( - teacher=teacher, - subject=subject_file.subject, - is_active=True - ).exists() and subject_file.uploaded_by != teacher: - return JsonResponse({ - "success": False, - "message": "You don't have permission to delete this file" - }) - + if ( + not Routine.objects.filter( + teacher=teacher, subject=subject_file.subject, is_active=True + ).exists() + and subject_file.uploaded_by != teacher + ): + return JsonResponse( + { + "success": False, + "message": "You don't have permission to delete this file", + } + ) + # Delete the file subject_file.delete() - - return JsonResponse({ - "success": True, - "message": "File deleted successfully" - }) + + return JsonResponse({"success": True, "message": "File deleted successfully"}) except SubjectFile.DoesNotExist: - return JsonResponse({ - "success": False, - "message": "File not found" - }) + return JsonResponse({"success": False, "message": "File not found"}) except Exception as e: - return JsonResponse({ - "success": False, - "message": f"Error deleting file: {str(e)}" - }) + return JsonResponse( + {"success": False, "message": f"Error deleting file: {str(e)}"} + ) @login_required @@ -862,32 +1026,29 @@ def get_teacher_subjects(request): """View to get a list of subjects taught by the teacher""" teacher = Staff.objects.filter(phone=request.user.phone).first() if not teacher: - return JsonResponse({ - "success": False, - "message": "Staff account not found" - }, status=404) - + return JsonResponse( + {"success": False, "message": "Staff account not found"}, status=404 + ) + # Get all subjects taught by the teacher subjects_query = Subject.objects.filter( - routine__teacher=teacher, - routine__is_active=True + routine__teacher=teacher, routine__is_active=True ).distinct() - + # Prepare results with additional has_syllabus field subjects_data = [] for subject in subjects_query: - subjects_data.append({ - 'id': subject.id, - 'name': subject.name, - 'course__name': subject.course.name, - 'semester_or_year': subject.semester_or_year, - 'has_syllabus': bool(subject.syllabus_pdf) - }) - - return JsonResponse({ - "success": True, - "subjects": subjects_data - }) + subjects_data.append( + { + "id": subject.id, + "name": subject.name, + "course__name": subject.course.name, + "period_or_year": subject.period_or_year, + "has_syllabus": bool(subject.syllabus_pdf), + } + ) + + return JsonResponse({"success": True, "subjects": subjects_data}) @login_required @@ -895,59 +1056,59 @@ def get_teacher_subjects(request): def view_subject_syllabus(request, subject_id): """View to display syllabus PDF for a subject""" teacher = Staff.objects.filter(phone=request.user.phone).first() - + if not teacher: - return JsonResponse({ - "success": False, - "message": "Staff account not found" - }, status=404) - + return JsonResponse( + {"success": False, "message": "Staff account not found"}, status=404 + ) + try: # Check if the subject_id is valid if not subject_id or not str(subject_id).isdigit(): - return JsonResponse({ - "success": False, - "message": "Invalid subject ID provided" - }, status=400) - + return JsonResponse( + {"success": False, "message": "Invalid subject ID provided"}, status=400 + ) + subject_id = int(subject_id) - + # First check if the teacher teaches this subject if not Routine.objects.filter( - teacher=teacher, - subject_id=subject_id, - is_active=True + teacher=teacher, subject_id=subject_id, is_active=True ).exists(): - return JsonResponse({ - "success": False, - "message": "You don't have permission to view this subject's syllabus" - }, status=403) - + return JsonResponse( + { + "success": False, + "message": "You don't have permission to view this subject's syllabus", + }, + status=403, + ) + try: subject = Subject.objects.filter(id=subject_id).first() except Subject.DoesNotExist: - return JsonResponse({ - "success": False, - "message": "Subject not found" - }, status=404) - + return JsonResponse( + {"success": False, "message": "Subject not found"}, status=404 + ) + if not subject.syllabus_pdf: - return JsonResponse({ - "success": False, - "message": "No syllabus available for this subject" - }, status=404) - + return JsonResponse( + {"success": False, "message": "No syllabus available for this subject"}, + status=404, + ) + # Return JSON with the file URL - return JsonResponse({ - "success": True, - "file_url": subject.syllabus_pdf.url, - "file_name": f"{subject.name} Syllabus.pdf" - }) + return JsonResponse( + { + "success": True, + "file_url": subject.syllabus_pdf.url, + "file_name": f"{subject.name} Syllabus.pdf", + } + ) except Exception as e: - return JsonResponse({ - "success": False, - "message": f"Error retrieving syllabus: {str(e)}" - }, status=500) + return JsonResponse( + {"success": False, "message": f"Error retrieving syllabus: {str(e)}"}, + status=500, + ) @login_required @@ -959,35 +1120,42 @@ def staffDashboard(request): last_month = today - timedelta(days=30) # Clear section states on page load - if request.method == 'GET': - request.session.pop('active_section', None) - request.session.pop('active_subsection', None) + if request.method == "GET": + request.session.pop("active_section", None) + request.session.pop("active_subsection", None) # Get feedback types for institute feedback feedback_types = FEEDBACK_TYPE_CHOICES # Get institute feedback - institute_feedback = StaffInstituteFeedback.objects.filter(staff=staff).order_by("-created_at")[:5] + institute_feedback = StaffInstituteFeedback.objects.filter(staff=staff).order_by( + "-created_at" + )[:5] # Get recent notices recent_notices = Notice.objects.all().order_by("-created_at")[:5] # Get recent feedback - recent_feedback = StudentFeedback.objects.filter(teacher=staff).order_by("-created_at")[:5] + recent_feedback = StudentFeedback.objects.filter(teacher=staff).order_by( + "-created_at" + )[:5] # Get leave requests - leave_requests = Staff_leave.objects.filter(staff=staff).order_by("-created_at")[:5] + leave_requests = StaffLeave.objects.filter(staff=staff).order_by("-created_at")[:5] # Get routines assigned to this teacher - routines = Routine.objects.filter(teacher=staff, is_active=True).order_by("start_time") + routines = Routine.objects.filter(teacher=staff, is_active=True).order_by( + "start_time" + ) # Get today's routines - today_routines = routines.filter(start_time__lte=timezone.now().time(), end_time__gte=timezone.now().time()) + today_routines = routines.filter( + start_time__lte=timezone.now().time(), end_time__gte=timezone.now().time() + ) # Get attendance records for today's routines today_attendance = Attendance.objects.filter( - routine__in=today_routines, - date=today + routine__in=today_routines, date=today ).select_related("routine") # Calculate attendance statistics @@ -1002,7 +1170,12 @@ def staffDashboard(request): absent_students += records.filter(student_attend=False).count() # Get average rating from student feedback - avg_rating = StudentFeedback.objects.filter(teacher=staff).aggregate(Avg("rating"))["rating__avg"] or 0 + avg_rating = ( + StudentFeedback.objects.filter(teacher=staff).aggregate(Avg("rating"))[ + "rating__avg" + ] + or 0 + ) context = { "staff": staff, @@ -1021,4 +1194,4 @@ def staffDashboard(request): "last_month": last_month, } - return render(request, "app/staff/dashboard.html", context) + return render(request, "teacher/dashboard.html", context) diff --git a/app/studentviews.py b/app/studentviews.py index 8db9671..6618d27 100644 --- a/app/studentviews.py +++ b/app/studentviews.py @@ -1,25 +1,37 @@ -from django.shortcuts import render, redirect, get_object_or_404 -from django.http import JsonResponse, HttpResponse -from django.contrib.auth.decorators import login_required -from django.contrib import messages -from django.db.models import Count, Q, Avg -from django.utils import timezone +# Standard library imports from datetime import timedelta -from django.views.decorators.http import require_http_methods, require_GET -from .models import ( - Student, - Course, - CourseTracking, - Student_Leave, - Staff, + +# Core Django imports +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.db.models import Avg, Count, Q +from django.http import JsonResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.utils import timezone +from django.views.decorators.http import require_http_methods + +# Local app imports +from app.models import ( Attendance, AttendanceRecord, - StudentFeedback, - Routine, - Notice, - Institute, + Batch, + Course, + CourseTracking, InstituteFeedback, + Notice, + Parent, + ParentFeedback, + ParentInstituteFeedback, + Routine, + Staff, + StaffInstituteFeedback, + StaffLeave, + Student, + StudentFeedback, + StudentLeave, Subject, + SubjectFile, + TeacherParentMeeting, FEEDBACK_TYPE_CHOICES, ) @@ -33,15 +45,16 @@ def studentDashboard(request): last_month = today - timedelta(days=30) # Clear section states on page load - if request.method == 'GET': - request.session.pop('active_section', None) - request.session.pop('active_subsection', None) + if request.method == "GET": + request.session.pop("active_section", None) + request.session.pop("active_subsection", None) # Get all teachers for feedback form teachers = Staff.objects.filter( Q(designation__icontains="Teacher") | Q(designation__icontains="Staff") | Q(designation__icontains="Principal") + | Q(designation__icontains="Admission Officer") | Q(designation__isnull=True) | Q(designation="") ).order_by("name") @@ -77,87 +90,81 @@ def studentDashboard(request): # Course Details with enhanced information course_details = [] for tracking in course_trackings: - course_details.append({ - "name": tracking.course.name, - "start_date": tracking.start_date, - "expected_end_date": tracking.expected_end_date, - "actual_end_date": tracking.actual_end_date, - "completion_percentage": tracking.completion_percentage, - "progress_status": tracking.progress_status, - "current_semester": tracking.current_semester, - "semester_start_date": tracking.semester_start_date, - "semester_end_date": tracking.semester_end_date, - "remaining_days": tracking.remaining_days, - "is_completed": tracking.is_completed, - "is_active": tracking.is_active, - "duration_type": tracking.course.duration_type, - "duration": tracking.course.duration, - "total_duration_days": tracking.total_duration_days, - "notes": tracking.notes, - }) + course_details.append( + { + "name": tracking.course.name, + "start_date": tracking.start_date, + "expected_end_date": tracking.expected_end_date, + "actual_end_date": tracking.actual_end_date, + "completion_percentage": tracking.completion_percentage, + "progress_status": tracking.progress_status, + "current_period": tracking.current_period, + "period_start_date": tracking.period_start_date, + "period_end_date": tracking.period_end_date, + "remaining_days": tracking.remaining_days, + "total_remaining_days": tracking.total_remaining_days, + "is_completed": tracking.is_completed, + "is_active": tracking.is_active, + "duration_type": tracking.course.duration_type, + "duration": tracking.course.duration, + "total_duration_days": tracking.total_duration_days, + "notes": tracking.notes, + } + ) - # Get current subjects based on current semester + # Get current subjects based on current period current_subjects = [] active_tracking = course_trackings.filter(progress_status="In Progress").first() if active_tracking: current_subjects = Subject.objects.filter( - course=active_tracking.course, - semester_or_year=active_tracking.current_semester + course=active_tracking.course, period_or_year=active_tracking.current_period ) # Attendance Statistics with enhanced information attendance_records = AttendanceRecord.objects.filter(student=student) - - # Get the current semester start and end dates from student's course tracking - current_semester_start = None - current_semester_end = None - + + # Get the current period start and end dates from student's course tracking + current_period_start = None + current_period_end = None + # Try to get course tracking for current student try: - if active_tracking and active_tracking.semester_start_date: - current_semester_start = active_tracking.semester_start_date - current_semester_end = active_tracking.semester_end_date or today + if active_tracking and active_tracking.period_start_date: + current_period_start = active_tracking.period_start_date + current_period_end = active_tracking.period_end_date or today except Exception: pass - - # Filter attendance by current semester date range if available - if current_semester_start: - semester_attendance = attendance_records.filter( - attendance__date__gte=current_semester_start, - attendance__date__lte=current_semester_end + + # Calculate total attendance + total_attendance = attendance_records.filter(student_attend=True).count() + total_classes = attendance_records.count() + + # Calculate attendance percentage + if total_classes > 0: + attendance_percentage = (total_attendance / total_classes) * 100 + else: + attendance_percentage = 0 + + # Filter attendance by current period date range if available + if current_period_start: + period_attendance = attendance_records.filter( + attendance__date__gte=current_period_start, + attendance__date__lte=current_period_end, ) - + # Count the actual classes and attendance - actual_classes = semester_attendance.count() - classes_attended = semester_attendance.filter(student_attend=True).count() - - # Check if the course is semester-based or year-based - is_semester_based = active_tracking.course.duration_type == "Semester" if active_tracking else True - - # Set the denominator based on semester/year - if is_semester_based: - # For a 6-month semester, use 180 days as denominator - total_classes = 180 - else: - # For a full year, use 365 days as denominator - total_classes = 365 # Or use 317 (365 - 48) if excluding Saturdays - - # Keep track of actual records for display purposes + actual_classes = period_attendance.count() + classes_attended = period_attendance.filter(student_attend=True).count() + + # Calculate actual attendance percentage if actual_classes > 0: - attendance_summary_actual = (classes_attended / actual_classes * 100) + attendance_summary_actual = classes_attended / actual_classes * 100 else: attendance_summary_actual = 0 else: - # Fall back to overall attendance if semester dates aren't available - actual_classes = attendance_records.count() - classes_attended = attendance_records.filter(student_attend=True).count() - total_classes = 180 # Default to semester-based - attendance_summary_actual = 0 - - # Calculate attendance percentage using the fixed formula - attendance_percentage = ( - (classes_attended / total_classes * 100) if total_classes > 0 else 0 - ) + actual_classes = total_classes + classes_attended = total_attendance + attendance_summary_actual = attendance_percentage # Calculate attendance streaks current_streak = 0 @@ -188,15 +195,17 @@ def studentDashboard(request): attendance_summary = { "total_classes": total_classes, "classes_attended": classes_attended, - "present_count": classes_attended, - "absent_count": total_classes - classes_attended, + "present_count": total_attendance, + "absent_count": total_classes - total_attendance, "attendance_percentage": attendance_percentage, "current_streak": current_streak, - "longest_streak": longest_streak + "longest_streak": longest_streak, + "actual_classes": actual_classes, + "actual_percentage": attendance_summary_actual, } - + # Add actual recorded classes info if different from expected - if current_semester_start and actual_classes != total_classes: + if current_period_start and actual_classes != total_classes: attendance_summary["actual_classes"] = actual_classes attendance_summary["actual_percentage"] = attendance_summary_actual @@ -218,24 +227,24 @@ def studentDashboard(request): notices = Notice.objects.all().order_by("-created_at") # Get student leaves with status information - student_leaves = Student_Leave.objects.filter(student=student).order_by( + student_leaves = StudentLeave.objects.filter(student=student).order_by( "-created_at" ) pending_leaves = student_leaves.filter(status=0).count() approved_leaves = student_leaves.filter(status=1).count() rejected_leaves = student_leaves.filter(status=2).count() - # Get current semester routines - current_semester_routines = [] + # Get current period routines + current_period_routines = Routine.objects.none() # Initialize with empty QuerySet if active_tracking: - current_semester_routines = Routine.objects.filter( + current_period_routines = Routine.objects.filter( course=active_tracking.course, - semester_or_year=active_tracking.current_semester, + period_or_year=active_tracking.current_period, is_active=True, ) # Check if student has any routines - has_routines = current_semester_routines.exists() + has_routines = current_period_routines.exists() context = { "student": student, @@ -252,6 +261,7 @@ def studentDashboard(request): "course_summary": course_summary, # Current Subjects "current_subjects": current_subjects, + "active_tracking": active_tracking, # Attendance "attendance_summary": attendance_summary, "attendance_records": attendance_records, @@ -267,13 +277,13 @@ def studentDashboard(request): "approved_leaves": approved_leaves, "rejected_leaves": rejected_leaves, # Routines - "current_semester_routines": current_semester_routines, + "current_period_routines": current_period_routines, "has_routines": has_routines, # Current date for display "today": today, } - return render(request, "student_dashboard.html", context) + return render(request, "student/dashboard.html", context) @login_required @@ -301,10 +311,10 @@ def request_leave(request): return redirect("studentDashboard") # Check if leave already exists for these dates - existing_leave = Student_Leave.objects.filter( + existing_leave = StudentLeave.objects.filter( student=student, start_date__lte=end_date.date(), - end_date__gte=start_date.date() + end_date__gte=start_date.date(), ).first() if existing_leave: @@ -314,7 +324,7 @@ def request_leave(request): return redirect("studentDashboard") # Create leave request - leave = Student_Leave.objects.create( + leave = StudentLeave.objects.create( student=student, start_date=start_date.date(), end_date=end_date.date(), @@ -365,7 +375,9 @@ def submit_feedback(request): existing_feedback.rating = rating existing_feedback.feedback_text = feedback_text existing_feedback.save() - messages.success(request, "Teacher feedback updated successfully") + messages.success( + request, "Teacher feedback updated successfully" + ) else: # Create new feedback StudentFeedback.objects.create( @@ -374,7 +386,9 @@ def submit_feedback(request): rating=rating, feedback_text=feedback_text, ) - messages.success(request, "Teacher feedback submitted successfully") + messages.success( + request, "Teacher feedback submitted successfully" + ) except Staff.DoesNotExist: messages.error(request, "Selected teacher does not exist") @@ -401,7 +415,9 @@ def submit_institute_feedback(request): try: # Validate feedback type - if not feedback_type or feedback_type not in dict(InstituteFeedback.FEEDBACK_TYPE_CHOICES): + if not feedback_type or feedback_type not in dict( + InstituteFeedback.FEEDBACK_TYPE_CHOICES + ): messages.error(request, "Please select a valid feedback type") return redirect("studentDashboard") @@ -469,7 +485,7 @@ def get_subject_files(request, subject_id): try: subject = Subject.objects.get(id=subject_id) files = subject.get_all_files() - + if not files: return JsonResponse( {"success": False, "message": "No files available for this subject"} diff --git a/app/templatetags/app_filters.py b/app/templatetags/app_filters.py index 22b3526..97d9eac 100644 --- a/app/templatetags/app_filters.py +++ b/app/templatetags/app_filters.py @@ -1,3 +1,4 @@ +# Core Django imports from django import template register = template.Library() @@ -17,9 +18,9 @@ def get_range(value): def multiply(value, arg): """Multiply the value by the argument""" try: - return int(value) * int(arg) - except (ValueError, TypeError): - return value + return float(value) * float(arg) + except ValueError: + return 0 @register.filter def get_item(dictionary, key): @@ -35,4 +36,31 @@ def get_item(dictionary, key): return dictionary.get(key) # Try with the key as string - return dictionary.get(str(key), None) \ No newline at end of file + return dictionary.get(str(key), None) + +@register.filter +def filter_attended(attendance_records): + """Filter attendance records to get only attended ones""" + return attendance_records.filter(student_attend=True) + +@register.filter +def div(value, arg): + """Divide the value by the argument""" + try: + return float(value) / float(arg) + except (ValueError, ZeroDivisionError): + return 0 + +@register.filter +def filter_by_subject(routines, subject): + """ + Filter routines to get the one for a specific subject. + Usage in template: {{ routines|filter_by_subject:subject }} + """ + if not routines: + return None + + for routine in routines: + if routine.subject_id == subject.id: + return routine + return None \ No newline at end of file diff --git a/app/tests.py b/app/tests.py index 7ce503c..688df16 100644 --- a/app/tests.py +++ b/app/tests.py @@ -1,3 +1,4 @@ +# Core Django imports from django.test import TestCase # Create your tests here. diff --git a/app/urls.py b/app/urls.py index 9d513b1..dc0a3ce 100644 --- a/app/urls.py +++ b/app/urls.py @@ -1,89 +1,295 @@ from django.urls import path from app import views -from django.contrib import admin from app import auth from app import staffviews -from app import firebase -from app.staffviews import ( - teacherDashboard, - request_staff_leave, - submit_staff_institute_feedback, - save_attendance, - get_subject_schedule, - teacher_update_profile_picture, - teacher_change_password, - get_attendance_form, - get_students, - manage_subject_files, - delete_subject_file, - get_teacher_subjects, - view_subject_syllabus -) +from app import studentviews +from app import parentviews +from app import admissionviews +from app import hodviews + # URL patterns for the main application urlpatterns = [ - # Dashboard URLs + + # Dashboard URLs --------------------------------------------------- path("dashboard/", views.dashboard, name="dashboard"), - path("student-dashboard/", views.studentDashboard, name="studentDashboard"), - path("teacher-dashboard/", views.teacherDashboard, name="teacherDashboard"), + path("student-dashboard/", studentviews.studentDashboard, name="studentDashboard"), + path("teacher-dashboard/", staffviews.teacherDashboard, name="teacherDashboard"), + path("parent-dashboard/", parentviews.parent_dashboard, name="parentDashboard"), + path( + "admission-officer-dashboard/", + admissionviews.admission_officer_dashboard, + name="admissionOfficerDashboard", + ), + path("hodDashboard/", hodviews.hod_dashboard, name="hodDashboard"), - # Leave Management URLs - path("request-leave/", views.request_leave, name="request_leave"), - path("request-staff-leave/", views.request_staff_leave, name="request_staff_leave"), - path("approve-student-leave//", views.approve_student_leave, name="approve_student_leave"), - path("reject-student-leave//", views.reject_student_leave, name="reject_student_leave"), - path("approve-staff-leave//", views.approve_staff_leave, name="approve_staff_leave"), - path("reject-staff-leave//", views.reject_staff_leave, name="reject_staff_leave"), - - # Feedback URLs - path("submit-feedback/", views.submit_feedback, name="submit_feedback"), - path("submit-institute-feedback/", views.submit_institute_feedback, name="submit_institute_feedback"), - path("submit-staff-institute-feedback/", views.submit_staff_institute_feedback, name="submit_staff_institute_feedback"), - - # Profile Management URLs - path("update-profile/", views.update_profile, name="update_profile"), - path("change-password/", views.change_password, name="change_password"), - path("teacher-update-profile-picture/", views.teacher_update_profile_picture, name="teacher_update_profile_picture"), - path("teacher-change-password/", views.teacher_change_password, name="teacher_change_password"), - - # Firebase Cloud Messaging URLs - path("saveFCMToken/", views.save_fcm_token, name="saveFCMToken"), - - # Subject and Course Management URLs - path("subject//files/", views.get_subject_files, name="get_subject_files"), - path("subject//syllabus/", staffviews.view_subject_syllabus, name="view_subject_syllabus"), - path("get-subjects/", views.get_subjects, name="get_subjects"), - path("get-teachers/", views.get_teachers, name="get_teachers"), - path("get-course-duration/", views.get_course_duration, name="get_course_duration"), - path("get-subject-schedule/", views.get_subject_schedule, name="get_subject_schedule"), - - # New Subject File Management URLs - path("manage-subject-files/", staffviews.manage_subject_files, name="manage_subject_files"), - path("delete-subject-file/", staffviews.delete_subject_file, name="delete_subject_file"), - path("get-teacher-subjects/", staffviews.get_teacher_subjects, name="get_teacher_subjects"), - - # Attendance Management URLs - path("save-attendance/", views.save_attendance, name="save_attendance"), - path("get-attendance-form/", staffviews.get_attendance_form, name="get_attendance_form"), - - # Notice Management URLs + # Notice Management ------------------------------------------------ path("add-notice/", views.add_notice, name="add_notice"), path("delete-notice//", views.delete_notice, name="delete_notice"), - # Password Reset URLs + # Leave Management ------------------------------------------------- + # Student Leave + path("request-leave/", views.request_leave, name="request_leave"), + path( + "approve-student-leave//", + views.approve_student_leave, + name="approve_student_leave", + ), + path( + "reject-student-leave//", + views.reject_student_leave, + name="reject_student_leave", + ), + path("get-student-leaves/", views.get_student_leaves, name="get_student_leaves"), + + # Staff Leave + path("request-staff-leave/", views.request_staff_leave, name="request_staff_leave"), + path( + "approve-staff-leave//", + views.approve_staff_leave, + name="approve_staff_leave", + ), + path( + "reject-staff-leave//", + views.reject_staff_leave, + name="reject_staff_leave", + ), + path("get-staff-leaves/", views.get_staff_leaves, name="get_staff_leaves"), + + # Feedback -------------------------------------------------------- + # Student Feedback + path("submit-feedback/", views.submit_feedback, name="submit_feedback"), + path( + "submit-institute-feedback/", + views.submit_institute_feedback, + name="submit_institute_feedback", + ), + + # Staff Feedback + path( + "submit-staff-institute-feedback/", + views.submit_staff_institute_feedback, + name="submit_staff_institute_feedback", + ), + + # Parent Feedback + path( + "parent/feedback/submit/", + parentviews.submit_parent_feedback, + name="submit_parent_feedback", + ), + path( + "parent/feedback/institute/submit/", + parentviews.submit_parent_institute_feedback, + name="submit_parent_institute_feedback", + ), + + # Profile Management ---------------------------------------------- + path("update-profile/", views.update_profile, name="update_profile"), + path("change-password/", views.change_password, name="change_password"), + path( + "teacher-update-profile-picture/", + views.teacher_update_profile_picture, + name="teacher_update_profile_picture", + ), + path( + "teacher-change-password/", + views.teacher_change_password, + name="teacher_change_password", + ), + + # Subject and Course Management ----------------------------------- + path("add-subject/", hodviews.add_subject, name="add_subject"), + path( + "subject//files/", + views.get_subject_files, + name="get_subject_files", + ), + path( + "subject//syllabus/", + staffviews.view_subject_syllabus, + name="view_subject_syllabus", + ), + path("get-subjects/", views.get_subjects, name="get_subjects"), + path("get-teachers/", views.get_teachers, name="get_teachers"), + path("get-course-duration/", views.get_course_duration, name="get_course_duration"), + path( + "get-subject-schedule/", views.get_subject_schedule, name="get_subject_schedule" + ), + path('subject//view/', hodviews.view_subject, name='view_subject'), + path('edit-subject//', hodviews.edit_subject, name='edit_subject'), + path('delete-subject//', hodviews.delete_subject, name='delete_subject'), + + # Student Management + path('add-student/', hodviews.add_student, name='add_student'), + path('get-student//', hodviews.get_student, name='get_student'), + path('edit-student//', hodviews.edit_student, name='edit_student'), + path('delete-student//', hodviews.delete_student, name='delete_student'), + + # HOD Notice Management + path('hod/add-notice/', hodviews.add_notice, name='hod_add_notice'), + path('notice//', hodviews.view_notice, name='view_notice'), + path('delete-notice//', hodviews.delete_notice, name='delete_notice'), + + # Subject File Management + path( + "delete-subject-file/", + staffviews.delete_subject_file, + name="delete_subject_file", + ), + path( + "get-teacher-subjects/", + staffviews.get_teacher_subjects, + name="get_teacher_subjects", + ), + + # Attendance Management ------------------------------------------- + path("save-attendance/", views.save_attendance, name="save_attendance"), + path( + "get-attendance-form/", + staffviews.get_attendance_form, + name="get_attendance_form", + ), + + # Password Management --------------------------------------------- + # Password Reset Options path("password-reset/", auth.reset_password_options, name="reset_password_options"), - path("password-reset/phone/", auth.password_reset_phone, name="phone_reset_password"), - path("password-reset/phone/verify/", auth.password_reset_phone_verify, name="verify_reset_otp"), - path("password-reset/phone/resend/", auth.resend_phone_otp, name="resend_phone_otp"), - path("password-reset/email/", auth.password_reset_email, name="email_reset_password"), - path("password-reset/email/verify/", auth.password_reset_email_verify, name="password_reset_done"), - path("password-reset/email/resend/", auth.resend_email_code, name="resend_reset_code"), + + # Phone-based Reset + path( + "password-reset/phone/", auth.password_reset_phone, name="phone_reset_password" + ), + path( + "password-reset/phone/verify/", + auth.password_reset_phone_verify, + name="verify_reset_otp", + ), + path( + "password-reset/phone/resend/", auth.resend_phone_otp, name="resend_phone_otp" + ), + + # Email-based Reset + path( + "password-reset/email/", auth.password_reset_email, name="email_reset_password" + ), + path( + "password-reset/email/verify/", + auth.password_reset_email_verify, + name="password_reset_done", + ), + path( + "password-reset/email/resend/", auth.resend_email_code, name="resend_reset_code" + ), + + # New Password Setup path("password-reset/set/", auth.set_new_password, name="set_new_password"), - # Password security check + # Password Security path("check-weak-password/", views.check_weak_password, name="check_weak_password"), - # Staff Management URLs + # Parent-Teacher Meeting ------------------------------------------ + path("parent/meetings/", parentviews.parent_meetings, name="parent_meetings"), + path("api/meetings/add/", parentviews.add_meeting, name="add_meeting"), + path( + "api/meetings//complete/", + parentviews.complete_meeting, + name="complete_meeting", + ), + path( + "api/meetings//cancel/", + parentviews.cancel_meeting, + name="cancel_meeting", + ), + path( + "api/meetings//reschedule/", + parentviews.reschedule_meeting, + name="reschedule_meeting", + ), + path( + "api/meetings//notes/", + parentviews.get_meeting_notes, + name="get_meeting_notes", + ), + path( + "api/meetings//agenda/", + parentviews.get_meeting_agenda, + name="get_meeting_agenda", + ), + path( + "api/meetings//cancellation-reason/", + views.get_meeting_cancellation_reason, + name="get_meeting_cancellation_reason", + ), + + # API Endpoints -------------------------------------------------- + # Student API + path( + "api/students//details/", + parentviews.get_student_details, + name="get_student_details", + ), path("get-students/", staffviews.get_students, name="get_students"), -] + + # Meeting API Endpoints (HOD) + path("api/hod/meetings//", hodviews.api_get_meeting, name="api_get_meeting"), + path("api/hod/meetings/", hodviews.api_create_meeting, name="api_create_meeting"), + path("api/hod/meetings//edit/", hodviews.api_update_meeting, name="api_update_meeting"), + path("api/hod/meetings//cancel/", hodviews.api_cancel_meeting, name="api_cancel_meeting"), + path("api/hod/meetings//notes/", hodviews.get_meeting_notes, name="get_meeting_notes"), + path("api/hod/meetings//agenda/", hodviews.get_meeting_agenda, name="get_meeting_agenda"), + path("api/hod/meetings//cancellation-reason/", hodviews.get_cancellation_reason, name="get_cancellation_reason"), + + # Routine API Endpoints (HOD) + path("api/hod/routines/", hodviews.api_create_routine, name="api_create_routine"), + path("api/hod/routines//", hodviews.api_get_routine, name="api_get_routine"), + path("api/hod/routines//edit/", hodviews.api_update_routine, name="api_update_routine"), + path("api/hod/routines//delete/", hodviews.api_delete_routine, name="api_delete_routine"), + + # Progress API Endpoints (HOD) + path("api/get-student-progress/", hodviews.get_student_progress, name="get_student_progress"), + path("api/get-progress//", hodviews.get_progress, name="get_progress"), + path("api/edit-progress//", hodviews.edit_progress, name="edit_progress"), + + # Firebase Cloud Messaging --------------------------------------- + path("saveFCMToken/", views.save_fcm_token, name="saveFCMToken"), + # Admission Officer routes ---------------------------------------- + path("add-student/", admissionviews.add_student, name="add_student"), + path("add-batch/", admissionviews.add_batch, name="add_batch"), + path("add-course/", admissionviews.add_course, name="add_course"), + path( + "add-course-tracking/", + admissionviews.add_course_tracking, + name="add_course_tracking", + ), + path("get-courses/", admissionviews.get_courses, name="get_courses"), + path("get-batches/", admissionviews.get_batches, name="get_batches"), + path( + "admission-get-students/", + admissionviews.get_students, + name="admission_get_students", + ), + path( + "get-student//", admissionviews.get_student, name="get_student" + ), + path("edit-student/", admissionviews.edit_student, name="edit_student"), + path( + "get-course-tracking//", + admissionviews.get_course_tracking, + name="get_course_tracking", + ), + path( + "edit-course-tracking/", + admissionviews.edit_course_tracking, + name="edit_course_tracking", + ), + + # Staff Management URLs + path('staff//', hodviews.view_staff, name='view_staff'), + path('edit-staff//', hodviews.edit_staff, name='edit_staff'), + path('delete-staff//', hodviews.delete_staff, name='delete_staff'), + path('add-staff/', hodviews.add_staff, name='add_staff'), + + # Test endpoints (commented out) + # path('test/error/505/', views.test_505_error, name='test_505_error'), +] diff --git a/app/utils.py b/app/utils.py index f8ab97a..c22b0fe 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,24 +1,31 @@ -import random -import string -import requests -from django.template.loader import render_to_string -from django.core.mail import send_mail -from django.conf import settings -from django.contrib.auth.tokens import default_token_generator -from django.utils.http import urlsafe_base64_encode -from django.utils.encoding import force_bytes -from django.utils import timezone -import pyotp +# Standard library imports import base64 -from .models import TOTPSecret, ResetToken, Student, Staff +import random +import os + +# Core Django imports +from django.core.mail import send_mail +from django.template.loader import render_to_string +from django.utils import timezone + +# Third-party app imports +import pyotp + +# Local app imports +from app.models import ResetToken, Staff, Student, TOTPSecret # OTP expiration time in seconds (5 minutes) -OTP_EXPIRY = 300 +OTP_EXPIRY = int(os.getenv('OTP_EXPIRY', 300)) + + +# -------------------------------------------------------------------- +# OTP and Secret Key Generation +# -------------------------------------------------------------------- def generate_secret_key(): """Generate a random secret key for TOTP""" - return base64.b32encode(random.getrandbits(160).to_bytes(20, 'big')).decode('utf-8') + return base64.b32encode(random.getrandbits(160).to_bytes(20, "big")).decode("utf-8") def generate_otp(secret_key): @@ -27,17 +34,20 @@ def generate_otp(secret_key): return totp.now() +# -------------------------------------------------------------------- +# OTP Storage and Verification +# -------------------------------------------------------------------- + + def store_secret_key(identifier, secret_key): """Store secret key in database""" # Delete any existing secrets for this identifier TOTPSecret.objects.filter(identifier=identifier).delete() - + # Create new secret with expiration expires_at = timezone.now() + timezone.timedelta(seconds=OTP_EXPIRY) return TOTPSecret.objects.create( - identifier=identifier, - secret_key=secret_key, - expires_at=expires_at + identifier=identifier, secret_key=secret_key, expires_at=expires_at ) @@ -45,12 +55,12 @@ def verify_otp(identifier, otp): """Verify OTP using TOTP""" try: # Get the most recent secret for this identifier - secret = TOTPSecret.objects.filter(identifier=identifier).latest('created_at') - + secret = TOTPSecret.objects.filter(identifier=identifier).latest("created_at") + # Check if secret is expired if secret.is_expired(): return False - + # Verify OTP totp = pyotp.TOTP(secret.secret_key, interval=OTP_EXPIRY) return totp.verify(otp) @@ -58,6 +68,11 @@ def verify_otp(identifier, otp): return False +# -------------------------------------------------------------------- +# SMS Communication +# -------------------------------------------------------------------- + + def send_otp_sms(phone, otp): """Send OTP via SMS using a service""" try: @@ -65,19 +80,18 @@ def send_otp_sms(phone, otp): student = Student.objects.filter(phone=phone).first() staff = Staff.objects.filter(phone=phone).first() user = student or staff - + # Get user's name user_name = user.name if user else "User" - + # Calculate expiry time in minutes expiry_minutes = OTP_EXPIRY // 60 # Render SMS message using template - message = render_to_string("login/sms_templates/otp_message.txt", { - "user_name": user_name, - "otp": otp, - "expiry_time": expiry_minutes - }) + message = render_to_string( + "login/sms_templates/otp_message.txt", + {"user_name": user_name, "otp": otp, "expiry_time": expiry_minutes}, + ) # This is a placeholder. You need to replace with actual SMS API implementation # Example using a generic SMS API @@ -100,6 +114,11 @@ def send_otp_sms(phone, otp): return False +# -------------------------------------------------------------------- +# Email Communication +# -------------------------------------------------------------------- + + def send_password_reset_email(user, request, email): """ Send password reset code to user's email using HTML template @@ -107,10 +126,10 @@ def send_password_reset_email(user, request, email): try: # Generate a secret key for TOTP secret_key = generate_secret_key() - + # Store the secret key in database store_secret_key(email, secret_key) - + # Generate TOTP totp = pyotp.TOTP(secret_key, interval=OTP_EXPIRY) reset_code = totp.now() @@ -151,17 +170,20 @@ def verify_reset_code(email, code): return verify_otp(email, code) +# -------------------------------------------------------------------- +# Token Management +# -------------------------------------------------------------------- + + def store_reset_token(token, identifier): """Store reset token in database""" # Delete any existing tokens for this identifier ResetToken.objects.filter(identifier=identifier).delete() - + # Create new token with expiration (15 minutes) expires_at = timezone.now() + timezone.timedelta(minutes=15) return ResetToken.objects.create( - token=token, - identifier=identifier, - expires_at=expires_at + token=token, identifier=identifier, expires_at=expires_at ) @@ -169,18 +191,22 @@ def verify_reset_token(token, identifier): """Verify reset token from database""" try: reset_token = ResetToken.objects.get(token=token) - + # Check if token is expired if reset_token.is_expired(): return False - + return reset_token.identifier == identifier except ResetToken.DoesNotExist: return False +# -------------------------------------------------------------------- +# Cleanup Functions +# -------------------------------------------------------------------- + + def cleanup_expired_tokens(): """Clean up expired TOTP secrets and reset tokens""" TOTPSecret.objects.filter(expires_at__lt=timezone.now()).delete() ResetToken.objects.filter(expires_at__lt=timezone.now()).delete() - diff --git a/app/views.py b/app/views.py index 2df4b4a..3ea94e8 100644 --- a/app/views.py +++ b/app/views.py @@ -1,17 +1,17 @@ from django.shortcuts import render, redirect, get_object_or_404 -from django.http import JsonResponse, HttpResponse, FileResponse +from django.http import JsonResponse, HttpResponse from django.contrib.auth.decorators import login_required from django.contrib import messages from django.db.models import Count, Q, Avg from django.utils import timezone from datetime import timedelta from django.conf import settings -from .models import ( +from app.models import ( Student, Course, CourseTracking, - Student_Leave, - Staff_leave, + StudentLeave, + StaffLeave, Attendance, AttendanceRecord, StudentFeedback, @@ -22,22 +22,25 @@ from .models import ( InstituteFeedback, Subject, Batch, + ParentFeedback, + Parent, + StaffInstituteFeedback, + ParentInstituteFeedback, + TeacherParentMeeting, ) -from app.admin import custom_admin_site from django.views.decorators.http import require_http_methods, require_GET, require_POST from django.core.files.storage import default_storage from django.core.files.base import ContentFile import json +from app.admin import custom_admin_site import os from django.db.models.base import transaction -from .utils import generate_otp, generate_secret_key, store_secret_key, verify_otp, send_otp_sms -from django.conf import settings from dotenv import load_dotenv -from .firebase import save_fcm_token, get_firebase_js, FCMDevice -from django.template.loader import render_to_string +from app.firebase import save_fcm_token, FCMDevice from django.views.static import serve + # Import views from studentviews.py -from .studentviews import ( +from app.studentviews import ( studentDashboard, request_leave, submit_feedback, @@ -46,7 +49,7 @@ from .studentviews import ( ) # Import views from staffviews.py -from .staffviews import ( +from app.staffviews import ( teacherDashboard, request_staff_leave, submit_staff_institute_feedback, @@ -57,6 +60,7 @@ from .staffviews import ( ) from django.contrib.auth.hashers import check_password +from django.urls import reverse # Load environment variables load_dotenv() @@ -64,320 +68,155 @@ load_dotenv() # Create your views here. - @login_required def dashboard(request): - """Main dashboard view that redirects to appropriate dashboard based on user role""" + """Main dashboard view that redirects to appropriate dashboard based on user role. + + Args: + request: HttpRequest object containing user information + + Returns: + HttpResponse: Redirects to appropriate dashboard or renders admin dashboard + """ user = request.user user_groups = user.groups.values_list("name", flat=True) - - # Redirect based on user role if "Student" in user_groups: - print("Redirecting to student dashboard") return redirect("studentDashboard") elif "Teacher" in user_groups: - print("Redirecting to teacher dashboard") return redirect("teacherDashboard") - elif user.is_superuser or user.is_staff: - # Admin users get the admin dashboard - print("Showing admin dashboard") - + elif "Parent" in user_groups: + return redirect("parentDashboard") + elif "Admission Officer" in user_groups: + return redirect("admissionOfficerDashboard") + elif "HOD" in user_groups: + return redirect("hodDashboard") + else: # Get current date and date ranges today = timezone.now().date() last_week = timezone.now() - timedelta(days=7) last_month = timezone.now() - timedelta(days=30) - - # Debug prints for user authentication - print(f"Is authenticated: {request.user.is_authenticated}") - print(f"Is staff: {request.user.is_staff}") - print(f"Is superuser: {request.user.is_superuser}") - print("-------------------------\n") - - # Student Statistics - with debug prints + + # Student Statistics total_students = Student.objects.count() - print("\nDEBUG Student Statistics:") - print("-------------------------") - print(f"Total students (from count): {total_students}") - - # Get all students and print their statuses - all_students = Student.objects.all() - print("\nAll students and their statuses:") - for student in all_students: - print(f"- {student.name}: {student.status}") - active_students = Student.objects.filter(status="Active").count() - print(f"\nActive students: {active_students}") - - completed_students = Student.objects.filter(status="Completed").count() - print(f"Completed students: {completed_students}") - - on_leave_students = Student.objects.filter(status="Leave").count() - print(f"Students on leave: {on_leave_students}") - - print(f"\nSum of status counts: {active_students + completed_students + on_leave_students}") - print("-------------------------\n") + male_students = Student.objects.filter(gender="Male").count() + female_students = Student.objects.filter(gender="Female").count() + inactive_students = total_students - active_students # Staff Statistics total_staff = Staff.objects.count() active_staff = Staff.objects.filter(is_active=True).count() - staff_on_leave = ( - Staff_leave.objects.filter(status=1).values("staff").distinct().count() + total_teachers = Staff.objects.filter(designation="Teacher").count() + non_teaching_staff = total_staff - total_teachers + inactive_staff = total_staff - active_staff + + # Parent Statistics + total_parents = Parent.objects.count() + active_parents = Parent.objects.filter(is_active=True).count() + parents_with_students = ( + Parent.objects.filter(students__isnull=False).distinct().count() ) + parents_with_multiple_students = ( + Parent.objects.annotate(student_count=Count("students")) + .filter(student_count__gt=1) + .count() + ) + inactive_parents = total_parents - active_parents # Course Statistics total_courses = Course.objects.count() active_courses = ( - Course.objects.filter(student_trackings__progress_status="In Progress") + Course.objects.filter( + student_trackings__progress_status__in=["Not Started", "In Progress"] + ) .distinct() .count() ) - completed_courses = ( - Course.objects.filter(student_trackings__progress_status="Completed") - .distinct() - .count() + total_subjects = Subject.objects.count() + total_batches = Batch.objects.count() + inactive_courses = total_courses - active_courses + + # Recent Activities + recent_notices = Notice.objects.all().order_by("-created_at")[:5] + recent_feedback = ( + list(InstituteFeedback.objects.all().order_by("-created_at")[:5]) + + list(StaffInstituteFeedback.objects.all().order_by("-created_at")[:5]) + + list(ParentInstituteFeedback.objects.all().order_by("-created_at")[:5]) + ) + recent_feedback = sorted( + recent_feedback, key=lambda x: x.created_at, reverse=True + )[:5] + + # Leave Requests + staff_leaves = StaffLeave.objects.filter(status=0).order_by("-created_at")[ + :5 + ] # 0 = Pending + student_leaves = StudentLeave.objects.filter(status=0).order_by("-created_at")[ + :5 + ] # 0 = Pending + + # Teacher Rating Statistics + teacher_ratings = ( + Staff.objects.filter(designation="Teacher") + .annotate( + avg_student_rating=Avg("feedbacks__rating"), + avg_parent_rating=Avg("parent_feedbacks__rating"), + total_feedbacks=Count("feedbacks") + Count("parent_feedbacks"), + ) + .order_by("-avg_student_rating")[:5] ) - # Course Tracking Statistics - course_trackings = CourseTracking.objects.all() - in_progress_trackings = course_trackings.filter(progress_status="In Progress") - completed_trackings = course_trackings.filter(progress_status="Completed") - dropped_trackings = course_trackings.filter(progress_status="Dropped") - - # Recent Leave Requests - recent_student_leaves = Student_Leave.objects.filter( - created_at__gte=last_week - ).order_by("-created_at")[:5] - - recent_staff_leaves = Staff_leave.objects.filter( - created_at__gte=last_week - ).order_by("-created_at")[:5] - - # Pending Leave Requests - pending_student_leaves = Student_Leave.objects.filter(status=0) - pending_staff_leaves = Staff_leave.objects.filter(status=0).order_by("-created_at") - - # Attendance Statistics - today_classes = Attendance.objects.filter(date=today) - total_classes = today_classes.count() - total_students_today = AttendanceRecord.objects.filter(attendance__in=today_classes).count() - students_present = AttendanceRecord.objects.filter( - attendance__in=today_classes, student_attend=True - ).count() - - # Recent Feedback - recent_feedback = StudentFeedback.objects.order_by("-created_at")[:5] - - # Recent Institute Feedback - recent_institute_feedback = InstituteFeedback.objects.filter( - is_public=True - ).order_by("-created_at")[:10] - - # Get notices - notices = Notice.objects.all().order_by("-created_at") - - # Get recent activities - recent_activities = [] - - # Add recent leaves - for leave in recent_student_leaves: - recent_activities.append( - { - "title": f"Student Leave Request", - "description": f"{leave.student.name} requested leave from {leave.start_date} to {leave.end_date}", - "timestamp": leave.created_at, - } - ) - - for leave in recent_staff_leaves: - recent_activities.append( - { - "title": f"Staff Leave Request", - "description": f"{leave.staff.name} requested leave from {leave.start_date} to {leave.end_date}", - "timestamp": leave.created_at, - } - ) - - # Add recent feedback - for feedback in recent_feedback: - recent_activities.append( - { - "title": f"New Feedback", - "description": f"Feedback received for {feedback.teacher.name if feedback.teacher else 'Institute'}", - "timestamp": feedback.created_at, - } - ) - - # Add recent institute feedback - for feedback in recent_institute_feedback: - recent_activities.append( - { - "title": f"Institute Feedback", - "description": f"New feedback from {feedback.display_name}", - "timestamp": feedback.created_at, - } - ) - - # Add recent notices - for notice in notices: - recent_activities.append( - { - "title": f"New Notice", - "description": notice.title, - "timestamp": notice.created_at, - } - ) - - # Sort all activities by timestamp - recent_activities.sort(key=lambda x: x["timestamp"], reverse=True) - # Take only the most recent 10 activities - recent_activities = recent_activities[:10] - - # Teacher Ratings - teacher_ratings = [] - teachers = Staff.objects.filter( - Q(designation__icontains="Teacher") - | Q(designation__icontains="Staff") - | Q(designation__icontains="Principal") - | Q(designation__isnull=True) - | Q(designation="") + # Get all meetings + meetings = TeacherParentMeeting.objects.all().order_by( + "-meeting_date", "-meeting_time" ) - for teacher in teachers: - feedback = StudentFeedback.objects.filter(teacher=teacher) - avg_rating = feedback.aggregate(Avg("rating"))["rating__avg"] or 0 - total_reviews = feedback.count() - - # Get total students taught by this teacher - total_students = ( - AttendanceRecord.objects.filter(attendance__routine__teacher=teacher) - .values("student") - .distinct() - .count() - ) - - teacher_ratings.append( - { - "name": teacher.name, - "designation": teacher.designation or "Teacher", - "image": teacher.image, - "avg_rating": avg_rating, - "total_reviews": total_reviews, - "total_students": total_students, - } - ) - - # Sort teachers by average rating (highest first) - teacher_ratings.sort(key=lambda x: x["avg_rating"], reverse=True) - - # Course Progress Overview - course_progress = [] - for course in Course.objects.all(): - trackings = course.student_trackings.all() - if trackings.exists(): - avg_progress = trackings.aggregate( - avg_progress=Avg("completion_percentage") - )["avg_progress"] - course_progress.append( - { - "course": course, - "total_students": trackings.count(), - "in_progress": trackings.filter( - progress_status="In Progress" - ).count(), - "completed": trackings.filter(progress_status="Completed").count(), - "average_progress": round(avg_progress, 1) if avg_progress else 0, - } - ) - - # Monthly Statistics - monthly_stats = { - "new_students": Student.objects.filter(joining_date__gte=last_month).count(), - "new_staff": Staff.objects.filter(joining_date__gte=last_month).count(), - "attendance_rate": round( - ( - AttendanceRecord.objects.filter( - attendance__date__gte=last_month.date(), student_attend=True - ).count() - / AttendanceRecord.objects.filter( - attendance__date__gte=last_month.date() - ).count() - * 100 - ) - if AttendanceRecord.objects.filter( - attendance__date__gte=last_month.date() - ).exists() - else 0 - ), - "leave_requests": Student_Leave.objects.filter( - created_at__gte=last_month - ).count() - + Staff_leave.objects.filter(created_at__gte=last_month).count(), - } - context = { - **custom_admin_site.each_context( - request - ), # This adds the admin context, including the sidebar. - # Student Statistics + "title": "Dashboard", + **custom_admin_site.each_context(request), "total_students": total_students, "active_students": active_students, - "completed_students": completed_students, - "on_leave_students": on_leave_students, - # Staff Statistics + "male_students": male_students, + "female_students": female_students, + "inactive_students": inactive_students, "total_staff": total_staff, "active_staff": active_staff, - "staff_on_leave": staff_on_leave, - # Course Statistics + "total_teachers": total_teachers, + "non_teaching_staff": non_teaching_staff, + "inactive_staff": inactive_staff, + "total_parents": total_parents, + "active_parents": active_parents, + "parents_with_students": parents_with_students, + "parents_with_multiple_students": parents_with_multiple_students, + "inactive_parents": inactive_parents, "total_courses": total_courses, "active_courses": active_courses, - "completed_courses": completed_courses, - # Course Tracking Statistics - "in_progress_trackings": in_progress_trackings.count(), - "completed_trackings": completed_trackings.count(), - "dropped_trackings": dropped_trackings.count(), - # Leave Requests - "recent_student_leaves": recent_student_leaves, - "recent_staff_leaves": recent_staff_leaves, - "pending_student_leaves": pending_student_leaves, - "pending_staff_leaves": pending_staff_leaves, - # Attendance Statistics - "total_classes_today": total_classes, - "total_students_today": total_students_today, - "students_present": students_present, - "attendance_rate": round( - (students_present / total_students_today * 100) if total_students_today > 0 else 0 - ), - # Feedback + "total_subjects": total_subjects, + "total_batches": total_batches, + "inactive_courses": inactive_courses, + "recent_notices": recent_notices, "recent_feedback": recent_feedback, - "recent_institute_feedback": recent_institute_feedback, + "staff_leaves": staff_leaves, + "student_leaves": student_leaves, "teacher_ratings": teacher_ratings, - # Course Progress - "course_progress": course_progress, - # Monthly Statistics - "monthly_stats": monthly_stats, - "notices": notices, # Add notices to context - "recent_activities": recent_activities, # Add recent activities to context + "meetings": meetings, } - return render(request, "dashboard.html", context) - else: - # If user doesn't belong to any specific group, try to determine user type - print("User doesn't belong to any specific group") - if hasattr(user, 'student'): - print("User has student attribute, redirecting to student dashboard") - return redirect("studentDashboard") - elif hasattr(user, 'staff'): - print("User has staff attribute, redirecting to teacher dashboard") - return redirect("teacherDashboard") - else: - print("Unknown user type, showing default dashboard") - return render(request, "dashboard.html", {"unknown_user_type": True}) + return render(request, "admin/dashboard.html", context) @login_required def update_profile(request): + """Update user profile picture based on user role. + + Args: + request: HttpRequest object containing user information and uploaded file + + Returns: + HttpResponse: Redirects to appropriate dashboard with success/error message + """ if request.method == "POST": user = request.user user_groups = user.groups.values_list("name", flat=True) @@ -386,7 +225,24 @@ def update_profile(request): if "profile_image" in request.FILES: try: image = request.FILES["profile_image"] - + + # Validate image file type + allowed_types = ["image/jpeg", "image/png", "image/gif"] + if image.content_type not in allowed_types: + messages.error( + request, + "Invalid file type. Please upload a JPEG, PNG, or GIF image.", + ) + return redirect("dashboard") + + # Validate image size (max 5MB) + if image.size > 5 * 1024 * 1024: + messages.error( + request, + "Image size too large. Please upload an image smaller than 5MB.", + ) + return redirect("dashboard") + if "Student" in user_groups: student = Student.objects.get(id=user.id) if student.image: @@ -397,7 +253,7 @@ def update_profile(request): student.image = image student.save() messages.success(request, "Profile picture updated successfully.") - + elif "Teacher" in user_groups: staff = Staff.objects.get(id=user.id) if staff.image: @@ -408,22 +264,60 @@ def update_profile(request): staff.image = image staff.save() messages.success(request, "Profile picture updated successfully.") - + + elif "Parent" in user_groups: + parent = Parent.objects.get(id=user.id) + if parent.image: + # Delete old image if updating + if os.path.isfile(parent.image.path): + os.remove(parent.image.path) + # Save new image + parent.image = image + parent.save() + messages.success(request, "Profile picture updated successfully.") + + elif "HOD" in user_groups or any( + group.lower().startswith("hod") for group in user_groups + ): + # HOD is typically a Staff member with special privileges + staff = Staff.objects.get(id=user.id) + if staff.image: + # Delete old image if updating + if os.path.isfile(staff.image.path): + os.remove(staff.image.path) + # Save new image + staff.image = image + staff.save() + messages.success(request, "Profile picture updated successfully.") + else: - messages.error(request, "User profile not found. Please make sure you are logged in as a student or staff member.") - - except (Student.DoesNotExist, Staff.DoesNotExist) as e: + messages.error( + request, + "User profile not found. Please make sure you are logged in as a student, staff, or parent member.", + ) + return redirect("dashboard") + + except (Student.DoesNotExist, Staff.DoesNotExist, Parent.DoesNotExist) as e: messages.error(request, "User profile not found in the database.") + return redirect("dashboard") except Exception as e: messages.error(request, f"Error updating profile picture: {str(e)}") + return redirect("dashboard") else: messages.error(request, "No profile picture uploaded.") + return redirect("dashboard") # Redirect based on user type if "Student" in user_groups: return redirect("studentDashboard") elif "Teacher" in user_groups: return redirect("teacherDashboard") + elif "Parent" in user_groups: + return redirect("parentDashboard") + elif "Admission Officer" in user_groups: + return redirect("admissionOfficerDashboard") + elif "HOD" in user_groups: + return redirect("hodDashboard") else: return redirect("dashboard") @@ -434,56 +328,80 @@ def update_profile(request): def change_password(request): if request.method == "POST": user = request.user + user_groups = user.groups.values_list("name", flat=True) current_password = request.POST.get("current_password") new_password = request.POST.get("new_password") confirm_password = request.POST.get("confirm_password") # Check if this is an AJAX request - is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest' + is_ajax = request.headers.get("X-Requested-With") == "XMLHttpRequest" + + # Determine redirect URL based on user role + redirect_url = "dashboard" + if "Student" in user_groups: + redirect_url = "studentDashboard" + elif "Teacher" in user_groups: + redirect_url = "teacherDashboard" + elif "Parent" in user_groups: + redirect_url = "parentDashboard" + elif "Admission Officer" in user_groups: + redirect_url = "admissionOfficerDashboard" + elif "HOD" in user_groups: + redirect_url = "hodDashboard" # Validate input if not all([current_password, new_password, confirm_password]): if is_ajax: - return JsonResponse({"success": False, "error": "All password fields are required."}) + return JsonResponse( + {"success": False, "error": "All password fields are required."} + ) messages.error(request, "All password fields are required.") - return redirect("studentDashboard") + return redirect(redirect_url) if new_password != confirm_password: if is_ajax: - return JsonResponse({"success": False, "error": "New passwords do not match."}) + return JsonResponse( + {"success": False, "error": "New passwords do not match."} + ) messages.error(request, "New passwords do not match.") - return redirect("studentDashboard") + return redirect(redirect_url) # Check current password if not user.check_password(current_password): if is_ajax: - return JsonResponse({"success": False, "error": "Current password is incorrect."}) + return JsonResponse( + {"success": False, "error": "Current password is incorrect."} + ) messages.error(request, "Current password is incorrect.") - return redirect("studentDashboard") + return redirect(redirect_url) # Set new password try: user.set_password(new_password) user.save() + + # For both AJAX and regular requests, we'll return success and indicate need for logout if is_ajax: - return JsonResponse({"success": True}) - + return JsonResponse( + { + "success": True, + "redirect": "/login/", + "message": "Password changed successfully. Please login again.", + } + ) + messages.success( request, "Password changed successfully. Please login again." ) return redirect("login") + except Exception as e: if is_ajax: - return JsonResponse({"success": False, "error": f"Error changing password: {str(e)}"}) + return JsonResponse( + {"success": False, "error": f"Error changing password: {str(e)}"} + ) messages.error(request, f"Error changing password: {str(e)}") - - # Redirect based on user type - if hasattr(user, "student"): - return redirect("studentDashboard") - elif hasattr(user, "staff"): - return redirect("teacherDashboard") - else: - return redirect("dashboard") + return redirect(redirect_url) return redirect("dashboard") @@ -491,16 +409,16 @@ def change_password(request): @login_required @require_GET def get_subjects(request): - """Get subjects for a course and semester""" + """Get subjects for a course and period""" course_id = request.GET.get("course_id") - semester = request.GET.get("semester", 1) + period = request.GET.get("period", 1) if not course_id: return JsonResponse({"success": False, "message": "Course ID is required"}) try: subjects = Subject.objects.filter( - course_id=course_id, semester_or_year=semester + course_id=course_id, period_or_year=period ).values("id", "name") return JsonResponse({"success": True, "subjects": list(subjects)}) @@ -517,6 +435,8 @@ def get_teachers(request): Q(designation__icontains="Teacher") | Q(designation__icontains="Staff") | Q(designation__icontains="Principal") + | Q(designation__icontains="HOD") + | Q(designation__icontains="Admission Officer") | Q(designation__isnull=True) | Q(designation="") ).values("id", "name") @@ -574,7 +494,7 @@ def get_course_duration(request): def approve_student_leave(request, leave_id): """View to approve a student leave request""" if request.method == "POST": - leave = get_object_or_404(Student_Leave, id=leave_id) + leave = get_object_or_404(StudentLeave, id=leave_id) leave.status = 1 # Approved leave.save() messages.success( @@ -587,7 +507,7 @@ def approve_student_leave(request, leave_id): def reject_student_leave(request, leave_id): """View to reject a student leave request""" if request.method == "POST": - leave = get_object_or_404(Student_Leave, id=leave_id) + leave = get_object_or_404(StudentLeave, id=leave_id) leave.status = 2 # Rejected leave.save() messages.warning( @@ -600,7 +520,7 @@ def reject_student_leave(request, leave_id): def approve_staff_leave(request, leave_id): """View to approve a staff leave request""" if request.method == "POST": - leave = get_object_or_404(Staff_leave, id=leave_id) + leave = get_object_or_404(StaffLeave, id=leave_id) leave.status = 1 # Approved leave.save() messages.success( @@ -613,7 +533,7 @@ def approve_staff_leave(request, leave_id): def reject_staff_leave(request, leave_id): """View to reject a staff leave request""" if request.method == "POST": - leave = get_object_or_404(Staff_leave, id=leave_id) + leave = get_object_or_404(StaffLeave, id=leave_id) leave.status = 2 # Rejected leave.save() messages.warning( @@ -625,17 +545,26 @@ def reject_staff_leave(request, leave_id): @login_required def add_notice(request): if request.method == "POST": - title = request.POST.get("title") - message = request.POST.get("message") - image = request.FILES.get("image") + try: + title = request.POST.get("title") + message = request.POST.get("message") + image = request.FILES.get("image") + + if not title or not message: + return JsonResponse( + {"success": False, "error": "Title and message are required"} + ) - if title and message: notice = Notice.objects.create(title=title, message=message, image=image) - messages.success(request, "Notice added successfully.") - else: - messages.error(request, "Title and message are required.") - return redirect("dashboard") + return JsonResponse( + {"success": True, "message": "Notice added successfully"} + ) + + except Exception as e: + return JsonResponse({"success": False, "error": str(e)}) + + return JsonResponse({"success": False, "error": "Invalid request method"}) @login_required @@ -651,215 +580,187 @@ def delete_notice(request, notice_id): return JsonResponse({"success": False, "error": "Invalid request method"}) -@login_required -def student_dashboard(request): - student = request.user.student - today = timezone.now().date() - - # Get attendance summary - attendance_records = AttendanceRecord.objects.filter(student=student) - total_classes = attendance_records.count() - attended_classes = attendance_records.filter(student_attend=True).count() - attendance_percentage = (attended_classes / total_classes * 100) if total_classes > 0 else 0 - - # Calculate current streak - current_streak = 0 - longest_streak = 0 - temp_streak = 1 - - for record in attendance_records.order_by('-attendance__date'): - if record.student_attend: - current_streak += 1 - temp_streak += 1 - longest_streak = max(longest_streak, temp_streak) - else: - break - - attendance_summary = { - 'total_classes': total_classes, - 'attendance_percentage': attendance_percentage, - 'current_streak': current_streak, - 'longest_streak': longest_streak - } - - # Get course tracking summary - course_trackings = CourseTracking.objects.filter(student=student) - course_summary = { - 'total_courses': course_trackings.count(), - 'completed_courses': course_trackings.filter(progress_status='Completed').count(), - 'ongoing_courses': course_trackings.filter(progress_status='In Progress').count(), - 'completion_percentage': ( - sum(tracking.completion_percentage for tracking in course_trackings) / course_trackings.count() - if course_trackings.exists() else 0 - ) - } - - # Get recent leaves - recent_leaves = Student_Leave.objects.filter(student=student).order_by('-created_at')[:5] - - # Get recent feedback - recent_feedback = StudentFeedback.objects.filter(student=student).order_by('-created_at')[:5] - - # Get notices - notices = Notice.objects.all().order_by('-created_at')[:5] - - # Get attendance records - attendance_records = AttendanceRecord.objects.filter(student=student).order_by('-attendance__date')[:5] - - # Get leave counts - pending_leaves = Student_Leave.objects.filter(student=student, status=0).count() - approved_leaves = Student_Leave.objects.filter(student=student, status=1).count() - rejected_leaves = Student_Leave.objects.filter(student=student, status=2).count() - - # Get course details from course tracking - course_details = [] - for tracking in course_trackings: - course_info = { - 'name': tracking.course.name, - 'start_date': tracking.start_date, - 'duration': tracking.course.duration, - 'duration_type': tracking.course.duration_type, - 'current_semester': tracking.current_semester, - 'progress_status': tracking.progress_status, - 'completion_percentage': tracking.completion_percentage, - 'semester_start_date': tracking.semester_start_date, - 'semester_end_date': tracking.semester_end_date, - 'expected_end_date': tracking.expected_end_date, - 'actual_end_date': tracking.actual_end_date, - 'remaining_days': tracking.remaining_days, - 'total_remaining_days': tracking.total_remaining_days - } - course_details.append(course_info) - - context = { - 'student': student, - 'attendance_summary': attendance_summary, - 'course_summary': course_summary, - 'recent_leaves': recent_leaves, - 'recent_feedback': recent_feedback, - 'notices': notices, - 'attendance_records': attendance_records, - 'pending_leaves': pending_leaves, - 'approved_leaves': approved_leaves, - 'rejected_leaves': rejected_leaves, - 'course_details': course_details, - } - return render(request, 'student_dashboard.html', context) - -@login_required -def teacher_dashboard(request): - teacher = request.user.teacher - today = timezone.now().date() - - # Get today's routines - today_routines = Routine.objects.filter( - teacher=teacher, - day_of_week=today.weekday() - ).order_by('start_time') - - # Get attendance summary - attendance_records = Attendance.objects.filter(routine__teacher=teacher) - total_classes = attendance_records.count() - total_students = AttendanceRecord.objects.filter(attendance__routine__teacher=teacher).count() - present_students = AttendanceRecord.objects.filter( - attendance__routine__teacher=teacher, - student_attend=True - ).count() - - attendance_summary = { - 'total_classes': total_classes, - 'total_students': total_students, - 'present_students': present_students, - 'attendance_percentage': (present_students / total_students * 100) if total_students > 0 else 0 - } - - # Get recent leaves - recent_leaves = Student_Leave.objects.filter( - Q(status=0) | Q(status=1) - ).order_by('-created_at')[:5] - - # Get recent feedback - recent_feedback = StudentFeedback.objects.filter(teacher=teacher).order_by('-created_at')[:5] - - # Get notices - notices = Notice.objects.all().order_by('-created_at')[:5] - - # Get attendance records - attendance_records = Attendance.objects.filter(routine__teacher=teacher).order_by('-date')[:5] - - # Get leave counts - pending_leaves = Student_Leave.objects.filter(status=0).count() - approved_leaves = Student_Leave.objects.filter(status=1).count() - rejected_leaves = Student_Leave.objects.filter(status=2).count() - - context = { - 'teacher': teacher, - 'today_routines': today_routines, - 'attendance_summary': attendance_summary, - 'recent_leaves': recent_leaves, - 'recent_feedback': recent_feedback, - 'notices': notices, - 'attendance_records': attendance_records, - 'pending_leaves': pending_leaves, - 'approved_leaves': approved_leaves, - 'rejected_leaves': rejected_leaves, - } - return render(request, 'teacher_dashboard.html', context) - @login_required @require_POST def save_fcm_token(request): try: + print("Received FCM token save request") data = json.loads(request.body) - token = data.get('token') - + token = data.get("token") + timestamp = data.get("timestamp") + vapid_key_used = data.get("vapidKeyUsed", False) + + # Log additional information for debugging + auth_header = request.headers.get("Authorization", "") + using_vapid_auth = auth_header.startswith("VAPID ") + + print(f"Auth method: {'VAPID' if using_vapid_auth else 'Cookie-based'}") + print(f"VAPID key used for token generation: {vapid_key_used}") + if not token: - return JsonResponse({'error': 'Token is required'}, status=400) - + print("No token provided in request") + return JsonResponse({"error": "Token is required"}, status=400) + user = request.user + print(f"User authenticated: {user.id} ({user.username})") user_groups = user.groups.values_list("name", flat=True) - - # Save token to the appropriate user model - if "Student" in user_groups: - user.fcm_token = token - user.save() + + print(f"FCM token saving for user: {user.id}, groups: {list(user_groups)}") + + # Determine user type based on group membership + user_type = "unknown" + if user.is_superuser: + user_type = "admin" + elif "Student" in user_groups: + user_type = "student" elif "Teacher" in user_groups: - user.fcm_token = token - user.save() - + user_type = "teacher" + elif "Parent" in user_groups: + user_type = "parent" + + # Save token directly to the user model + user.fcm_token = token + user.save(update_fields=["fcm_token"]) + print(f"FCM token saved directly to user {user.name}") + # Update or create FCMDevice - device, created = FCMDevice.objects.update_or_create( - token=token, - defaults={'is_active': True} + try: + # Check if this is a fallback token + is_fallback = token.startswith("fcm-token-") or token.startswith( + "fallback-token-" + ) + + # Create or update the device + device, created = FCMDevice.objects.update_or_create( + token=token, + defaults={ + "is_active": True, + "is_fallback": is_fallback, + "user_type": user_type, + }, + ) + + print( + f"FCMDevice {'created' if created else 'updated'} with ID: {device.id}, user_type: {user_type}" + ) + except Exception as e: + print(f"Error creating/updating FCMDevice: {str(e)}") + raise + + return JsonResponse( + { + "status": "success", + "device_id": device.id, + "created": created, + "auth_method": "vapid" if using_vapid_auth else "cookie", + "user_type": user_type, + } ) - - return JsonResponse({ - 'status': 'success', - 'device_id': device.id, - 'created': created - }) except json.JSONDecodeError: - return JsonResponse({'error': 'Invalid JSON'}, status=400) + print("Invalid JSON in request body") + return JsonResponse({"error": "Invalid JSON"}, status=400) except Exception as e: - return JsonResponse({'error': str(e)}, status=500) - + print(f"Error in save_fcm_token: {str(e)}") + return JsonResponse({"error": str(e)}, status=500) + + def serve_firebase_sw(request): - """Serve Firebase service worker with correct MIME type""" - - # Get the path to the service worker in the static directory - sw_path = os.path.join(settings.STATICFILES_DIRS[0], 'firebase-messaging-sw.js') - - # If the file doesn't exist in static directory, try staticfiles - if not os.path.exists(sw_path): - sw_path = os.path.join(settings.STATIC_ROOT, 'firebase-messaging-sw.js') - - response = serve(request, 'firebase-messaging-sw.js', os.path.dirname(sw_path)) - response['Content-Type'] = 'application/javascript' - return response + """Serve the Firebase service worker file with correct content type and security headers""" + try: + # Get the path to the service worker in the static directory + sw_path = os.path.join(settings.STATICFILES_DIRS[0], "firebase-messaging-sw.js") + + # If the file doesn't exist in static directory, try staticfiles + if not os.path.exists(sw_path): + sw_path = os.path.join(settings.STATIC_ROOT, "firebase-messaging-sw.js") + + if not os.path.exists(sw_path): + print(f"Service worker file not found at: {sw_path}") + return HttpResponse("Service worker file not found", status=404) + + with open(sw_path, "r") as f: + content = f.read() + + # Create response with correct content type + response = HttpResponse(content, content_type="application/javascript") + + # Set all necessary security headers for service worker + response["Service-Worker-Allowed"] = "/" + response["Cache-Control"] = "no-cache, no-store, must-revalidate" + response["Pragma"] = "no-cache" + response["Expires"] = "0" + + # Set proper CORS headers for service worker + response["Access-Control-Allow-Origin"] = "*" + response["Access-Control-Allow-Methods"] = "GET, OPTIONS" + response["Access-Control-Allow-Headers"] = "Content-Type" + + print(f"Serving service worker file successfully, size: {len(content)}") + return response + except Exception as e: + print(f"Error serving service worker: {str(e)}") + return HttpResponse("Internal server error", status=500) + @login_required def check_weak_password(request): """Check if the current user's password is '123'""" user = request.user - is_weak = check_password('123', user.password) - return JsonResponse({'is_weak': is_weak}) + is_weak = check_password("123", user.password) + return JsonResponse({"is_weak": is_weak}) + + +@login_required +def get_staff_leaves(request): + """View to get staff leave requests for AJAX updates""" + staff_leaves = StaffLeave.objects.filter(status=0).order_by("-created_at")[:5] + leaves_data = [] + for leave in staff_leaves: + leaves_data.append( + { + "id": leave.id, + "staff_name": leave.staff.name, + "start_date": leave.start_date.strftime("%b %d, %Y"), + "end_date": leave.end_date.strftime("%b %d, %Y"), + "message": leave.message[:50] + "..." + if len(leave.message) > 50 + else leave.message, + } + ) + return JsonResponse({"staff_leaves": leaves_data}) + + +@login_required +def get_student_leaves(request): + """View to get student leave requests for AJAX updates""" + student_leaves = StudentLeave.objects.filter(status=0).order_by("-created_at")[:5] + leaves_data = [] + for leave in student_leaves: + leaves_data.append( + { + "id": leave.id, + "student_name": leave.student.name, + "start_date": leave.start_date.strftime("%b %d, %Y"), + "end_date": leave.end_date.strftime("%b %d, %Y"), + "message": leave.message[:50] + "..." + if len(leave.message) > 50 + else leave.message, + } + ) + return JsonResponse({"student_leaves": leaves_data}) + + +@login_required +@require_GET +def get_meeting_cancellation_reason(request, meeting_id): + """API endpoint to get the cancellation reason for a meeting""" + try: + meeting = get_object_or_404(TeacherParentMeeting, id=meeting_id) + + if meeting.status != "cancelled": + return JsonResponse({"success": False, "error": "Meeting is not cancelled"}) + + return JsonResponse({"success": True, "reason": meeting.cancellation_reason}) + + except Exception as e: + return JsonResponse({"success": False, "error": str(e)}) diff --git a/firebase-key.json.example b/firebase-key.json.example new file mode 100644 index 0000000..27e9030 --- /dev/null +++ b/firebase-key.json.example @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "your-project-id", + "private_key_id": "your-private-key-id", + "private_key": "-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY_CONTENT\n-----END PRIVATE KEY-----\n", + "client_email": "your-service-account-email@your-project-id.iam.gserviceaccount.com", + "client_id": "your-client-id", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/your-service-account-email%40your-project-id.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7b1b727..21afcce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,9 @@ cryptography==44.0.2 Django==5.2 django-jazzmin==3.0.1 django-mathfilters==1.0.0 +django-model-utils==5.0.0 djangorestframework==3.16.0 +Faker==37.1.0 firebase-admin==6.7.0 google-api-core==2.24.2 google-api-python-client==2.166.0 @@ -22,9 +24,11 @@ google-resumable-media==2.7.2 googleapis-common-protos==1.69.2 grpcio==1.71.0 grpcio-status==1.71.0 +gunicorn==23.0.0 httplib2==0.22.0 idna==3.10 msgpack==1.1.0 +packaging==24.2 pillow==11.1.0 proto-plus==1.26.1 protobuf==5.29.4 @@ -39,6 +43,7 @@ pytz==2025.2 requests==2.32.3 rsa==4.9 sqlparse==0.5.3 +tzdata==2025.2 uritemplate==4.1.1 urllib3==2.3.0 whitenoise==6.9.0 diff --git a/static/css/base.css b/static/css/base.css index 38715b4..bc1ec3c 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -574,4 +574,101 @@ body { a[href]:after { content: " (" attr(href) ")"; } +} + +/* Responsive Form Styles */ +@media (max-width: 768px) { + .form-group { + margin-bottom: 1rem; + } + + .form-control, .form-select { + font-size: 16px; /* Prevents iOS zoom */ + height: auto; + padding: 0.5rem 0.75rem; + } + + .input-group { + flex-wrap: wrap; + } + + .input-group > .form-control, + .input-group > .form-select { + flex: 1 1 auto; + width: auto; + } + + .btn-block { + width: 100%; + } + + textarea.form-control { + min-height: 100px; + } + + label { + font-size: 0.9rem; + margin-bottom: 0.25rem; + } +} + +@media (max-width: 576px) { + .form-row { + display: block; + } + + .form-row > .col, + .form-row > [class*="col-"] { + padding-right: 0; + padding-left: 0; + } + + .form-control, .form-select { + font-size: 16px; + padding: 0.4rem 0.6rem; + } + + .form-check-input { + width: 1rem; + height: 1rem; + margin-top: 0.3em; + } + + .form-check-label { + margin-left: 0.2rem; + } +} + +/* Modal Responsive Fixes */ +@media (max-width: 576px) { + .modal-header { + padding: 0.75rem 1rem; + } + + .modal-body { + padding: 1rem; + } + + .modal-footer { + padding: 0.75rem 1rem; + flex-direction: column; + align-items: stretch; + } + + .modal-footer > * { + margin: 0.25rem 0; + } + + /* Fix for iOS modals */ + .ios-modal-fix { + position: absolute; + overflow: hidden; + } +} + +/* Fix for notched phones */ +@supports (padding: max(0px)) { + .fixed-bottom { + padding-bottom: max(0.5rem, env(safe-area-inset-bottom)); + } } \ No newline at end of file diff --git a/static/css/custom.css b/static/css/custom.css index 5ee0c89..7b43880 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -19,4 +19,3 @@ img[src*="/static/img/logo.png"] { margin-right: .5rem; margin-top: -3px; } - diff --git a/static/css/dashboard.css b/static/css/dashboard.css index 7990fc4..07d38f4 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -1152,4 +1152,106 @@ img.img-fluid { padding: 0.25rem 0.5rem; font-size: 0.75rem; } +} + +/* Meeting Section Styles */ +.meeting-card { + background: var(--card-bg); + border-radius: var(--radius-lg); + border: 1px solid var(--border-color); + transition: var(--transition); + overflow: hidden; +} + +.meeting-card:hover { + transform: translateY(-2px); + box-shadow: var(--hover-shadow); +} + +.meeting-card .card-header { + background-color: var(--hover-bg); + padding: 1rem 1.25rem; + border-bottom: 1px solid var(--border-color); +} + +.meeting-card .card-header h5 { + color: var(--text-primary); + font-size: 0.875rem; + font-weight: 600; + margin: 0; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.meeting-card .card-body { + padding: 1.25rem; +} + +.meeting-status { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.5rem; + border-radius: var(--radius-sm); + font-size: 0.75rem; + font-weight: 500; +} + +.meeting-status.scheduled { + background-color: var(--primary-light); + color: var(--primary-dark); +} + +.meeting-status.completed { + background-color: var(--success-light); + color: var(--success-dark); +} + +.meeting-status.cancelled { + background-color: var(--danger-light); + color: var(--danger-dark); +} + +.meeting-status.rescheduled { + background-color: var(--warning-light); + color: var(--warning-dark); +} + +.meeting-actions { + display: flex; + gap: 0.5rem; + margin-top: 1rem; +} + +.meeting-actions .btn { + padding: 0.375rem 0.75rem; + font-size: 0.875rem; +} + +.meeting-actions .btn i { + margin-right: 0.25rem; +} + +/* Responsive Meeting Section */ +@media (max-width: 768px) { + .meeting-card { + margin-bottom: 1rem; + } + + .meeting-actions { + flex-wrap: wrap; + } + + .meeting-actions .btn { + flex: 1 1 calc(50% - 0.5rem); + min-width: calc(50% - 0.5rem); + } +} + +@media (max-width: 576px) { + .meeting-actions .btn { + flex: 1 1 100%; + min-width: 100%; + margin-bottom: 0.5rem; + } } \ No newline at end of file diff --git a/static/firebase-messaging-sw.example.js b/static/firebase-messaging-sw.example.js index 8acbfdd..994b40f 100644 --- a/static/firebase-messaging-sw.example.js +++ b/static/firebase-messaging-sw.example.js @@ -1,91 +1,143 @@ -importScripts( - "https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js", -); -importScripts( - "https://www.gstatic.com/firebasejs/9.22.0/firebase-messaging-compat.js", -); +// Version identifier for cache busting +const SW_VERSION = '1.0.2'; +console.log(`Firebase Messaging SW Version ${SW_VERSION} starting...`); -firebase.initializeApp({ - apiKey: "YOUR_API_KEY", - authDomain: "YOUR_AUTH_DOMAIN", - projectId: "YOUR_PROJECT_ID", - storageBucket: "YOUR_STORAGE_BUCKET", - messagingSenderId: "YOUR_MESSAGING_SENDER_ID", - appId: "YOUR_APP_ID", - measurementId: "YOUR_MEASUREMENT_ID", -}); +// Import Firebase scripts +try { + importScripts('https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js'); + importScripts('https://www.gstatic.com/firebasejs/9.22.0/firebase-messaging-compat.js'); + console.log('Firebase scripts loaded successfully'); +} catch (e) { + console.error('Error loading Firebase scripts:', e); +} -const messaging = firebase.messaging(); +// Firebase configuration - this must match exactly with your main app configuration +const firebaseConfig = { + apiKey: 'YOUR_API_KEY', + authDomain: 'YOUR_AUTH_DOMAIN', + projectId: 'YOUR_PROJECT_ID', + storageBucket: 'YOUR_STORAGE_BUCKET', + messagingSenderId: 'YOUR_MESSAGING_SENDER_ID', + appId: 'YOUR_APP_ID', + measurementId: "Your Measurement ID" +}; -// Handle background messages -messaging.onBackgroundMessage((payload) => { - console.log("Received background message:", payload); +// Handle any errors during initialization +try { + // Initialize Firebase + firebase.initializeApp(firebaseConfig); + console.log('Firebase initialized in service worker'); + + const messaging = firebase.messaging(); + console.log('Firebase messaging initialized'); - // Default notification options - const notificationOptions = { - body: "You have a new message.", - icon: "/static/img/logo.png", - badge: "/static/img/logo.png", - timestamp: Date.now(), - requireInteraction: true, - vibrate: [200, 100, 200], - tag: "notification-" + Date.now(), - data: payload.data || {}, - actions: [ - { - action: "open", - title: "Open", - }, - { - action: "close", - title: "Close", - }, - ], - }; + // Handle background messages + messaging.onBackgroundMessage(function(payload) { + console.log('Received background message:', payload); + + // Generate a unique ID for this notification + const notificationId = payload.data?.notificationId || `notification-${Date.now()}`; + + // Check if we already have a notification with this ID + return self.registration.getNotifications({tag: notificationId}) + .then(function(notifications) { + // Only show if no matching notification exists + if (notifications.length === 0) { + const notificationTitle = payload.notification?.title || 'New Notification'; + const notificationOptions = { + body: payload.notification?.body || 'You have a new message.', + icon: '/static/img/logo.png', + badge: '/static/img/logo.png', + tag: notificationId, // Tag helps prevent duplicates + data: payload.data || {} + }; - // Set notification title and body based on payload - let notificationTitle = "New Notification"; - if (payload.notification) { - notificationTitle = payload.notification.title || notificationTitle; - notificationOptions.body = - payload.notification.body || notificationOptions.body; - if (payload.notification.icon) { - notificationOptions.icon = payload.notification.icon; - } - } else if (payload.data) { - notificationTitle = payload.data.title || notificationTitle; - notificationOptions.body = payload.data.body || notificationOptions.body; - } - - // Show the notification - return self.registration - .showNotification(notificationTitle, notificationOptions) - .catch((error) => { - console.error("Error showing notification:", error); + return self.registration.showNotification(notificationTitle, notificationOptions); + } else { + console.log('Prevented duplicate background notification', notificationId); + return Promise.resolve(); + } + }); }); +} catch (error) { + console.error('Firebase messaging initialization error:', error); +} + +// Improve service worker lifecycle for better compatibility +self.addEventListener('install', function(event) { + console.log('Service Worker installed'); + self.skipWaiting(); }); -// Handle notification click -self.addEventListener("notificationclick", (event) => { - console.log("Notification clicked:", event); - - event.notification.close(); - - if (event.action === "open") { - // Handle open action - const urlToOpen = event.notification.data.url || "/app/dashboard/"; - event.waitUntil(clients.openWindow(urlToOpen)); - } +self.addEventListener('activate', function(event) { + console.log('Service Worker activated'); + event.waitUntil(self.clients.claim()); }); -// Handle service worker installation -self.addEventListener("install", (event) => { - console.log("Service Worker installed"); - self.skipWaiting(); +// Add special handling for push events +self.addEventListener('push', function(event) { + console.log('Push event received in service worker', event); + + if (!event.data) { + console.log('Push event has no data'); + return; + } + + try { + const payload = event.data.json(); + console.log('Push data:', payload); + + // Generate a unique ID for this notification + const notificationId = payload.data?.notificationId || `push-${Date.now()}`; + + event.waitUntil( + // Check for existing notifications first + self.registration.getNotifications({tag: notificationId}) + .then(function(notifications) { + // Only show if no matching notification exists + if (notifications.length === 0) { + const title = payload.notification?.title || 'New Notification'; + const options = { + body: payload.notification?.body || 'You have a new message', + icon: '/static/img/logo.png', + badge: '/static/img/logo.png', + tag: notificationId, // Add tag to identify this notification + data: payload.data || {} + }; + + return self.registration.showNotification(title, options); + } else { + console.log('Prevented duplicate push notification', notificationId); + return Promise.resolve(); + } + }) + ); + } catch (error) { + console.error('Error handling push event:', error); + } }); -// Handle service worker activation -self.addEventListener("activate", (event) => { - console.log("Service Worker activated"); - event.waitUntil(clients.claim()); +// Improve notification click handling for various environments +self.addEventListener('notificationclick', function(event) { + console.log('Notification clicked:', event.notification); + event.notification.close(); + + const urlToOpen = event.notification.data?.url || '/app/dashboard/'; + + event.waitUntil( + clients.matchAll({type: 'window', includeUncontrolled: true}).then(windowClients => { + // Check if there is already a window open with the target URL + for (let i = 0; i < windowClients.length; i++) { + const client = windowClients[i]; + // If so, focus on that window + if (client.url === urlToOpen && 'focus' in client) { + return client.focus(); + } + } + // If not, open a new window + if (clients.openWindow) { + return clients.openWindow(urlToOpen); + } + }) + ); }); diff --git a/static/js/admin_dashboard.js b/static/js/admin_dashboard.js new file mode 100644 index 0000000..c6cafa9 --- /dev/null +++ b/static/js/admin_dashboard.js @@ -0,0 +1,964 @@ +// Function to update leave requests +function updateLeaveRequests() { + // Fetch staff leave requests + fetch('/app/get-staff-leaves/') + .then(response => response.json()) + .then(data => { + const staffLeavesContainer = document.querySelector('#staff-leaves-container'); + if (data.staff_leaves.length === 0) { + staffLeavesContainer.innerHTML = ` +
+ No pending staff leave requests +
+ `; + return; + } + + let staffLeavesHtml = ''; + data.staff_leaves.forEach(leave => { + staffLeavesHtml += ` +
+
+
+ +
+
+
+ ${leave.staff_name} + From: ${leave.start_date} + To: ${leave.end_date} + ${leave.message} +
+
+ + +
+
+ + +
+
+
+
+ `; + }); + staffLeavesContainer.innerHTML = staffLeavesHtml; + }); + + // Fetch student leave requests + fetch('/app/get-student-leaves/') + .then(response => response.json()) + .then(data => { + const studentLeavesContainer = document.querySelector('#student-leaves-container'); + if (data.student_leaves.length === 0) { + studentLeavesContainer.innerHTML = ` +
+ No pending student leave requests +
+ `; + return; + } + + let studentLeavesHtml = ''; + data.student_leaves.forEach(leave => { + studentLeavesHtml += ` +
+
+
+ +
+
+
+ ${leave.student_name} + From: ${leave.start_date} + To: ${leave.end_date} + ${leave.message} +
+
+ + +
+
+ + +
+
+
+
+ `; + }); + studentLeavesContainer.innerHTML = studentLeavesHtml; + }); +} + +// Meeting-related functions +function viewMeetingNotes(meetingId) { + const modal = document.getElementById('viewNotesModal'); + const errorDiv = document.getElementById('notesError'); + const loadingDiv = document.getElementById('notesLoading'); + const contentDiv = document.getElementById('meetingNotesContent'); + const notesText = document.getElementById('viewMeetingNotesText'); + + // Reset states + errorDiv.style.display = 'none'; + loadingDiv.style.display = 'block'; + contentDiv.style.display = 'none'; + notesText.textContent = ''; + + // Show modal + modal.style.display = 'block'; + document.body.style.overflow = 'hidden'; + + // Add keyboard event listener for Escape key + document.addEventListener('keydown', handleNotesEscapeKey); + + // Fetch notes + fetch(`/app/api/meetings/${meetingId}/notes/`, { + headers: { + 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value + } + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.includes('application/json')) { + throw new TypeError("Response was not JSON"); + } + return response.json(); + }) + .then(data => { + if (data.success) { + notesText.textContent = data.notes || 'No notes available'; + loadingDiv.style.display = 'none'; + contentDiv.style.display = 'block'; + } else { + throw new Error(data.error || 'Failed to fetch notes'); + } + }) + .catch(error => { + console.error('Error:', error); + errorDiv.textContent = error.message; + errorDiv.style.display = 'block'; + loadingDiv.style.display = 'none'; + }); +} + +function closeNotesModal() { + const modal = document.getElementById('viewNotesModal'); + const errorDiv = document.getElementById('notesError'); + const loadingDiv = document.getElementById('notesLoading'); + const contentDiv = document.getElementById('meetingNotesContent'); + + // Reset states + errorDiv.style.display = 'none'; + loadingDiv.style.display = 'none'; + contentDiv.style.display = 'none'; + + // Hide modal + modal.style.display = 'none'; + document.body.style.overflow = ''; + + // Remove keyboard event listener + document.removeEventListener('keydown', handleNotesEscapeKey); +} + +function handleNotesEscapeKey(event) { + if (event.key === 'Escape') { + closeNotesModal(); + } +} + +// Make all meeting-related functions globally available +window.viewMeetingAgenda = function(meetingId) { + const modal = document.getElementById('viewAgendaModal'); + const errorDiv = document.getElementById('agendaError'); + const loadingDiv = document.getElementById('agendaLoading'); + const contentDiv = document.getElementById('meetingAgendaContent'); + const agendaText = document.getElementById('viewMeetingAgendaText'); + + // Reset states + errorDiv.style.display = 'none'; + loadingDiv.style.display = 'block'; + contentDiv.style.display = 'none'; + agendaText.textContent = ''; + + // Show modal + modal.style.display = 'block'; + document.body.style.overflow = 'hidden'; + + // Add keyboard event listener for Escape key + document.addEventListener('keydown', handleAgendaEscapeKey); + + // Fetch agenda + fetch(`/app/api/meetings/${meetingId}/agenda/`, { + headers: { + 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value + } + }) + .then(response => { + if (!response.ok) { + throw new Error(`Network response was not ok: ${response.status} ${response.statusText}`); + } + return response.json(); + }) + .then(data => { + console.log('Received agenda data:', data); + loadingDiv.style.display = 'none'; + + if (data.success && data.agenda) { + agendaText.textContent = data.agenda; + contentDiv.style.display = 'block'; + } else { + errorDiv.textContent = 'No agenda available for this meeting'; + errorDiv.style.display = 'block'; + } + }) + .catch(error => { + console.error('Error fetching agenda:', error); + loadingDiv.style.display = 'none'; + errorDiv.textContent = error.message || 'Failed to fetch agenda'; + errorDiv.style.display = 'block'; + }); +}; + +window.closeAgendaModal = function() { + const modal = document.getElementById('viewAgendaModal'); + const errorDiv = document.getElementById('agendaError'); + const loadingDiv = document.getElementById('agendaLoading'); + const contentDiv = document.getElementById('meetingAgendaContent'); + const agendaText = document.getElementById('viewMeetingAgendaText'); + + // Reset states + errorDiv.style.display = 'none'; + loadingDiv.style.display = 'none'; + contentDiv.style.display = 'none'; + agendaText.textContent = ''; + + // Hide modal + modal.style.display = 'none'; + document.body.style.overflow = ''; + + // Remove keyboard event listener + document.removeEventListener('keydown', handleAgendaEscapeKey); +}; + +window.handleAgendaEscapeKey = function(event) { + if (event.key === 'Escape') { + closeAgendaModal(); + } +}; + +window.viewMeetingNotes = function(meetingId) { + const modal = document.getElementById('viewNotesModal'); + const errorDiv = document.getElementById('notesError'); + const loadingDiv = document.getElementById('notesLoading'); + const contentDiv = document.getElementById('meetingNotesContent'); + const notesText = document.getElementById('viewMeetingNotesText'); + + // Reset states + errorDiv.style.display = 'none'; + loadingDiv.style.display = 'block'; + contentDiv.style.display = 'none'; + notesText.textContent = ''; + + // Show modal + modal.style.display = 'block'; + document.body.style.overflow = 'hidden'; + + // Add keyboard event listener for Escape key + document.addEventListener('keydown', handleNotesEscapeKey); + + // Fetch notes + fetch(`/app/api/meetings/${meetingId}/notes/`, { + headers: { + 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value + } + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.includes('application/json')) { + throw new TypeError("Response was not JSON"); + } + return response.json(); + }) + .then(data => { + if (data.success) { + notesText.textContent = data.notes || 'No notes available'; + loadingDiv.style.display = 'none'; + contentDiv.style.display = 'block'; + } else { + throw new Error(data.error || 'Failed to fetch notes'); + } + }) + .catch(error => { + console.error('Error:', error); + errorDiv.textContent = error.message; + errorDiv.style.display = 'block'; + loadingDiv.style.display = 'none'; + }); +}; + +window.closeNotesModal = function() { + const modal = document.getElementById('viewNotesModal'); + const errorDiv = document.getElementById('notesError'); + const loadingDiv = document.getElementById('notesLoading'); + const contentDiv = document.getElementById('meetingNotesContent'); + + // Reset states + errorDiv.style.display = 'none'; + loadingDiv.style.display = 'none'; + contentDiv.style.display = 'none'; + + // Hide modal + modal.style.display = 'none'; + document.body.style.overflow = ''; + + // Remove keyboard event listener + document.removeEventListener('keydown', handleNotesEscapeKey); +}; + +window.handleNotesEscapeKey = function(event) { + if (event.key === 'Escape') { + closeNotesModal(); + } +}; + +window.cancelMeeting = function(meetingId) { + const modal = document.getElementById('cancelMeetingModal'); + const form = document.getElementById('cancelMeetingForm'); + const errorDiv = document.getElementById('cancelError'); + const loadingDiv = document.getElementById('cancelLoading'); + + // Reset form and error states + form.reset(); + errorDiv.style.display = 'none'; + loadingDiv.style.display = 'none'; + + // Set meeting ID + document.getElementById('cancelMeetingId').value = meetingId; + + // Show modal + modal.style.display = 'block'; + document.body.style.overflow = 'hidden'; + + // Add keyboard event listener for Escape key + document.addEventListener('keydown', handleCancelEscapeKey); +}; + +window.closeCancelModal = function() { + const modal = document.getElementById('cancelMeetingModal'); + const form = document.getElementById('cancelMeetingForm'); + const errorDiv = document.getElementById('cancelError'); + const loadingDiv = document.getElementById('cancelLoading'); + + // Reset form and error states + form.reset(); + errorDiv.style.display = 'none'; + loadingDiv.style.display = 'none'; + + // Hide modal + modal.style.display = 'none'; + document.body.style.overflow = ''; + + // Remove keyboard event listener + document.removeEventListener('keydown', handleCancelEscapeKey); +}; + +window.handleCancelEscapeKey = function(event) { + if (event.key === 'Escape') { + closeCancelModal(); + } +}; + +window.submitCancelForm = function() { + const form = document.getElementById('cancelMeetingForm'); + const errorDiv = document.getElementById('cancelError'); + const loadingDiv = document.getElementById('cancelLoading'); + const submitBtn = form.querySelector('button[type="button"]'); + const reason = document.getElementById('cancellationReason'); + + // Validate form + if (!reason.value.trim()) { + reason.classList.add('is-invalid'); + return; + } + + // Show loading state + loadingDiv.style.display = 'block'; + errorDiv.style.display = 'none'; + submitBtn.disabled = true; + + const meetingId = document.getElementById('cancelMeetingId').value; + + fetch(`/app/api/meetings/${meetingId}/cancel/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value + }, + body: JSON.stringify({ reason: reason.value }) + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.includes('application/json')) { + throw new TypeError("Response was not JSON"); + } + return response.json(); + }) + .then(data => { + if (data.success) { + closeCancelModal(); + window.location.reload(); + } else { + throw new Error(data.error || 'Failed to cancel meeting'); + } + }) + .catch(error => { + console.error('Error:', error); + errorDiv.textContent = error.message; + errorDiv.style.display = 'block'; + }) + .finally(() => { + loadingDiv.style.display = 'none'; + submitBtn.disabled = false; + }); +}; + +window.rescheduleMeeting = function(meetingId) { + const modal = document.getElementById('rescheduleMeetingModal'); + const form = document.getElementById('rescheduleMeetingForm'); + const errorDiv = document.getElementById('rescheduleError'); + const loadingDiv = document.getElementById('rescheduleLoading'); + + // Reset form and error states + form.reset(); + errorDiv.style.display = 'none'; + loadingDiv.style.display = 'none'; + + // Set meeting ID and minimum date + document.getElementById('rescheduleMeetingId').value = meetingId; + const today = new Date().toISOString().split('T')[0]; + document.getElementById('rescheduleDate').min = today; + + // Show modal + modal.style.display = 'block'; + document.body.style.overflow = 'hidden'; + + // Add keyboard event listener for Escape key + document.addEventListener('keydown', handleRescheduleEscapeKey); +}; + +window.closeRescheduleModal = function() { + const modal = document.getElementById('rescheduleMeetingModal'); + const form = document.getElementById('rescheduleMeetingForm'); + const errorDiv = document.getElementById('rescheduleError'); + const loadingDiv = document.getElementById('rescheduleLoading'); + + // Reset form and error states + form.reset(); + errorDiv.style.display = 'none'; + loadingDiv.style.display = 'none'; + + // Hide modal + modal.style.display = 'none'; + document.body.style.overflow = ''; + + // Remove keyboard event listener + document.removeEventListener('keydown', handleRescheduleEscapeKey); +}; + +window.handleRescheduleEscapeKey = function(event) { + if (event.key === 'Escape') { + closeRescheduleModal(); + } +}; + +window.submitRescheduleForm = function() { + const form = document.getElementById('rescheduleMeetingForm'); + const errorDiv = document.getElementById('rescheduleError'); + const loadingDiv = document.getElementById('rescheduleLoading'); + const submitBtn = form.querySelector('button[type="button"]'); + const date = document.getElementById('rescheduleDate'); + const time = document.getElementById('rescheduleTime'); + + // Validate form + let isValid = true; + if (!date.value) { + date.classList.add('is-invalid'); + isValid = false; + } + if (!time.value) { + time.classList.add('is-invalid'); + isValid = false; + } + if (!isValid) return; + + // Show loading state + loadingDiv.style.display = 'block'; + errorDiv.style.display = 'none'; + submitBtn.disabled = true; + + const meetingId = document.getElementById('rescheduleMeetingId').value; + + fetch(`/app/api/meetings/${meetingId}/reschedule/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value + }, + body: JSON.stringify({ + meeting_date: date.value, + meeting_time: time.value + }) + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.includes('application/json')) { + throw new TypeError("Response was not JSON"); + } + return response.json(); + }) + .then(data => { + if (data.success) { + closeRescheduleModal(); + window.location.reload(); + } else { + throw new Error(data.error || 'Failed to reschedule meeting'); + } + }) + .catch(error => { + console.error('Error:', error); + errorDiv.textContent = error.message; + errorDiv.style.display = 'block'; + }) + .finally(() => { + loadingDiv.style.display = 'none'; + submitBtn.disabled = false; + }); +}; + +// Schedule Meeting Modal Functions +window.openScheduleMeetingModal = function() { + const modal = document.getElementById('scheduleMeetingModal'); + const form = document.getElementById('scheduleMeetingForm'); + const errorDiv = document.getElementById('scheduleMeetingError'); + const loadingDiv = document.getElementById('scheduleMeetingLoading'); + + // Reset form and error states + form.reset(); + errorDiv.style.display = 'none'; + loadingDiv.style.display = 'none'; + + // Show modal + modal.style.display = 'block'; + document.body.style.overflow = 'hidden'; + + // Set minimum date to today + const today = new Date().toISOString().split('T')[0]; + document.getElementById('meetingDate').min = today; + + // Add keyboard event listener for Escape key + document.addEventListener('keydown', handleEscapeKey); +}; + +window.closeScheduleMeetingModal = function() { + const modal = document.getElementById('scheduleMeetingModal'); + const form = document.getElementById('scheduleMeetingForm'); + const errorDiv = document.getElementById('scheduleMeetingError'); + const loadingDiv = document.getElementById('scheduleMeetingLoading'); + + // Reset form and error states + form.reset(); + errorDiv.style.display = 'none'; + loadingDiv.style.display = 'none'; + + // Hide modal + modal.style.display = 'none'; + document.body.style.overflow = ''; + + // Remove keyboard event listener + document.removeEventListener('keydown', handleEscapeKey); +}; + +// Function to handle Escape key press +window.handleEscapeKey = function(event) { + if (event.key === 'Escape') { + closeScheduleMeetingModal(); + } +}; + +// Toggle meeting link field based on online meeting checkbox +document.addEventListener('DOMContentLoaded', function() { + const isOnlineCheckbox = document.getElementById('isOnline'); + if (isOnlineCheckbox) { + isOnlineCheckbox.addEventListener('change', function() { + const linkContainer = document.getElementById('meetingLinkContainer'); + const linkInput = document.getElementById('meetingLink'); + + if (this.checked) { + linkContainer.style.display = 'block'; + linkInput.required = true; + } else { + linkContainer.style.display = 'none'; + linkInput.required = false; + } + }); + } + + // Initialize all tooltips + var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); + var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { + return new bootstrap.Tooltip(tooltipTriggerEl); + }); + + // Initialize all popovers + var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')); + var popoverList = popoverTriggerList.map(function (popoverTriggerEl) { + return new bootstrap.Popover(popoverTriggerEl); + }); + + // Update leave requests every 30 seconds + setInterval(updateLeaveRequests, 30000); + + // Initial update + updateLeaveRequests(); +}); + +// Function to validate form +window.validateMeetingForm = function() { + const form = document.getElementById('scheduleMeetingForm'); + const date = document.getElementById('meetingDate'); + const time = document.getElementById('meetingTime'); + const duration = document.getElementById('meetingDuration'); + const isOnline = document.getElementById('isOnline'); + const link = document.getElementById('meetingLink'); + + let isValid = true; + + // Reset validation states + form.classList.remove('was-validated'); + + // Validate date + if (!date.value) { + date.classList.add('is-invalid'); + isValid = false; + } else { + date.classList.remove('is-invalid'); + } + + // Validate time + if (!time.value) { + time.classList.add('is-invalid'); + isValid = false; + } else { + time.classList.remove('is-invalid'); + } + + // Validate duration + if (!duration.value || duration.value < 15 || duration.value > 120) { + duration.classList.add('is-invalid'); + isValid = false; + } else { + duration.classList.remove('is-invalid'); + } + + // Validate meeting link if online meeting + if (isOnline.checked && !link.value) { + link.classList.add('is-invalid'); + isValid = false; + } else { + link.classList.remove('is-invalid'); + } + + return isValid; +}; + +// Function to submit the meeting form +window.submitMeetingForm = function() { + if (!validateMeetingForm()) { + return; + } + + const form = document.getElementById('scheduleMeetingForm'); + const errorDiv = document.getElementById('scheduleMeetingError'); + const loadingDiv = document.getElementById('scheduleMeetingLoading'); + const submitBtn = form.querySelector('button[type="button"]'); + + // Show loading state + loadingDiv.style.display = 'block'; + errorDiv.style.display = 'none'; + submitBtn.disabled = true; + + const formData = new FormData(form); + const data = { + meeting_date: formData.get('meeting_date'), + meeting_time: formData.get('meeting_time'), + duration: parseInt(formData.get('duration')), + is_online: document.getElementById('isOnline').checked, + meeting_link: formData.get('meeting_link'), + agenda: document.getElementById('scheduleMeetingAgenda').value + }; + + fetch('/app/api/meetings/add/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value + }, + body: JSON.stringify(data) + }) + .then(response => { + if (!response.ok) { + return response.json().then(data => { + throw new Error(data.error || 'Failed to schedule meeting'); + }); + } + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.includes('application/json')) { + throw new TypeError("Response was not JSON"); + } + return response.json(); + }) + .then(data => { + if (data.success) { + closeScheduleMeetingModal(); + // Show success message + const successDiv = document.createElement('div'); + successDiv.className = 'alert alert-success alert-dismissible fade show'; + successDiv.innerHTML = ` + Success! Meeting scheduled successfully. + + `; + document.querySelector('.content-card').insertAdjacentElement('afterbegin', successDiv); + // Reload after a short delay + setTimeout(() => { + window.location.reload(); + }, 1500); + } else { + throw new Error(data.error || 'Failed to schedule meeting'); + } + }) + .catch(error => { + console.error('Error scheduling meeting:', error); + errorDiv.textContent = error.message; + errorDiv.style.display = 'block'; + }) + .finally(() => { + loadingDiv.style.display = 'none'; + submitBtn.disabled = false; + }); +}; + +window.completeMeeting = function(meetingId) { + const modal = document.getElementById('completeMeetingModal'); + const form = document.getElementById('completeMeetingForm'); + const errorDiv = document.getElementById('completeError'); + const loadingDiv = document.getElementById('completeLoading'); + + // Reset form and error states + form.reset(); + errorDiv.style.display = 'none'; + loadingDiv.style.display = 'none'; + + // Set meeting ID + document.getElementById('completeMeetingId').value = meetingId; + + // Show modal + modal.style.display = 'block'; + document.body.style.overflow = 'hidden'; + + // Add keyboard event listener for Escape key + document.addEventListener('keydown', handleCompleteEscapeKey); +}; + +window.closeCompleteModal = function() { + const modal = document.getElementById('completeMeetingModal'); + const form = document.getElementById('completeMeetingForm'); + const errorDiv = document.getElementById('completeError'); + const loadingDiv = document.getElementById('completeLoading'); + + // Reset form and error states + form.reset(); + errorDiv.style.display = 'none'; + loadingDiv.style.display = 'none'; + + // Hide modal + modal.style.display = 'none'; + document.body.style.overflow = ''; + + // Remove keyboard event listener + document.removeEventListener('keydown', handleCompleteEscapeKey); +}; + +window.handleCompleteEscapeKey = function(event) { + if (event.key === 'Escape') { + closeCompleteModal(); + } +}; + +window.viewCancellationReason = function(meetingId) { + const modal = document.getElementById('viewReasonModal'); + const errorDiv = document.getElementById('reasonError'); + const loadingDiv = document.getElementById('reasonLoading'); + const contentDiv = document.getElementById('meetingReasonContent'); + const reasonText = document.getElementById('viewMeetingReasonText'); + + // Reset states + errorDiv.style.display = 'none'; + loadingDiv.style.display = 'block'; + contentDiv.style.display = 'none'; + reasonText.textContent = ''; + + // Show modal + modal.style.display = 'block'; + document.body.style.overflow = 'hidden'; + + // Add keyboard event listener for Escape key + document.addEventListener('keydown', handleReasonEscapeKey); + + // Fetch cancellation reason + fetch(`/app/api/meetings/${meetingId}/cancellation-reason/`, { + headers: { + 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value + } + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + if (data.success && data.reason) { + reasonText.textContent = data.reason; + loadingDiv.style.display = 'none'; + contentDiv.style.display = 'block'; + } else { + throw new Error(data.error || 'No cancellation reason available'); + } + }) + .catch(error => { + console.error('Error:', error); + errorDiv.textContent = error.message; + errorDiv.style.display = 'block'; + loadingDiv.style.display = 'none'; + }); +}; + +window.closeReasonModal = function() { + const modal = document.getElementById('viewReasonModal'); + const errorDiv = document.getElementById('reasonError'); + const loadingDiv = document.getElementById('reasonLoading'); + const contentDiv = document.getElementById('meetingReasonContent'); + const reasonText = document.getElementById('viewMeetingReasonText'); + + // Reset states + errorDiv.style.display = 'none'; + loadingDiv.style.display = 'none'; + contentDiv.style.display = 'none'; + reasonText.textContent = ''; + + // Hide modal + modal.style.display = 'none'; + document.body.style.overflow = ''; + + // Remove keyboard event listener + document.removeEventListener('keydown', handleReasonEscapeKey); +}; + +window.handleReasonEscapeKey = function(event) { + if (event.key === 'Escape') { + closeReasonModal(); + } +}; + +// Notice Modal Functions +window.openAddNoticeModal = function() { + document.getElementById('addNoticeModal').style.display = 'block'; + document.addEventListener('keydown', handleNoticeEscapeKey); +} + +window.closeAddNoticeModal = function() { + document.getElementById('addNoticeModal').style.display = 'none'; + document.removeEventListener('keydown', handleNoticeEscapeKey); + // Reset form + document.getElementById('addNoticeForm').reset(); + // Hide error and loading states + document.getElementById('addNoticeError').style.display = 'none'; + document.getElementById('addNoticeLoading').style.display = 'none'; +} + +window.handleNoticeEscapeKey = function(event) { + if (event.key === 'Escape') { + closeAddNoticeModal(); + } +} + +window.submitNoticeForm = function() { + const form = document.getElementById('addNoticeForm'); + const formData = new FormData(form); + + // Show loading state + document.getElementById('addNoticeLoading').style.display = 'block'; + document.getElementById('addNoticeError').style.display = 'none'; + + fetch('/app/add-notice/', { + method: 'POST', + body: formData, + headers: { + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // Close modal and refresh page to show new notice + closeAddNoticeModal(); + window.location.reload(); + } else { + // Show error message + document.getElementById('addNoticeError').textContent = data.error || 'Failed to add notice'; + document.getElementById('addNoticeError').style.display = 'block'; + } + }) + .catch(error => { + document.getElementById('addNoticeError').textContent = 'An error occurred while adding the notice'; + document.getElementById('addNoticeError').style.display = 'block'; + }) + .finally(() => { + document.getElementById('addNoticeLoading').style.display = 'none'; + }); +} \ No newline at end of file diff --git a/static/js/admission_officer.js b/static/js/admission_officer.js new file mode 100644 index 0000000..d05db43 --- /dev/null +++ b/static/js/admission_officer.js @@ -0,0 +1,406 @@ +// Initialize dashboard functionality when DOM is loaded +document.addEventListener('DOMContentLoaded', function() { + // Navigation handling is now in navigation.js + + // Initialize progress bars + function initializeProgressBars() { + document.querySelectorAll('.progress-bar').forEach(bar => { + const progress = parseFloat(bar.getAttribute('aria-valuenow')) || 0; + bar.style.width = `${progress}%`; + + // Set color based on progress + if (progress < 25) { + bar.classList.add('bg-danger'); + } else if (progress < 75) { + bar.classList.add('bg-warning'); + } else { + bar.classList.add('bg-success'); + } + }); + } + + // Call initialization + initializeProgressBars(); + + // Initialize action buttons + document.querySelectorAll('[data-action]').forEach(button => { + button.addEventListener('click', function() { + const action = this.dataset.action; + const id = this.dataset.id; + + switch(action) { + case 'view': + viewStudent(id); + break; + case 'edit': + editStudent(id); + break; + case 'delete': + deleteStudent(id); + break; + } + }); + }); + + // Initialize Firebase and request notification permission + console.log('Firebase available:', typeof firebase !== 'undefined'); + console.log('Firebase messaging available:', typeof firebase !== 'undefined' && typeof firebase.messaging !== 'undefined'); + console.log('Service Worker supported:', 'serviceWorker' in navigator); + console.log('Notification permission:', Notification.permission); + + if (typeof getDeviceToken === 'function') { + getDeviceToken().catch(error => { + console.error('Error initializing Firebase:', error); + }); + } +}); + +// Student Management Functions +function viewStudent(id) { + fetch(`/app/student/${id}/`) + .then(response => response.json()) + .then(data => { + const modal = document.getElementById('studentDetailsModal'); + if (modal) { + document.getElementById('studentName').textContent = data.name; + document.getElementById('studentPhone').textContent = data.phone; + document.getElementById('studentEmail').textContent = data.email || 'Not provided'; + document.getElementById('studentAddress').textContent = data.permanent_address || 'Not provided'; + document.getElementById('studentCourse').textContent = data.course?.name || 'Not assigned'; + document.getElementById('studentStatus').textContent = data.status || 'Not set'; + modal.style.display = 'block'; + } + }) + .catch(error => { + console.error('Error fetching student details:', error); + alert('Error loading student details'); + }); +} + +function editStudent(id) { + fetch(`/app/student/${id}/edit/`) + .then(response => response.json()) + .then(data => { + const modal = document.getElementById('editStudentModal'); + if (modal) { + document.getElementById('editStudentId').value = data.id; + document.getElementById('editStudentName').value = data.name; + document.getElementById('editStudentPhone').value = data.phone; + document.getElementById('editStudentEmail').value = data.email || ''; + document.getElementById('editStudentAddress').value = data.permanent_address || ''; + document.getElementById('editStudentCourse').value = data.course?.id || ''; + document.getElementById('editStudentStatus').value = data.status || ''; + modal.style.display = 'block'; + } + }) + .catch(error => { + console.error('Error fetching student data:', error); + alert('Error loading student data'); + }); +} + +function deleteStudent(id) { + if (confirm('Are you sure you want to delete this student?')) { + fetch(`/app/student/${id}/delete/`, { + method: 'POST', + headers: { + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value, + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + const row = document.querySelector(`tr[data-student-id="${id}"]`); + if (row) { + row.remove(); + } + alert('Student deleted successfully'); + } else { + alert('Error: ' + data.message); + } + }) + .catch(error => { + console.error('Error deleting student:', error); + alert('Error deleting student'); + }); + } +} + +// Batch Management Functions +function editBatch(id) { + fetch(`/app/batch/${id}/edit/`) + .then(response => response.json()) + .then(data => { + const modal = document.getElementById('editBatchModal'); + if (modal) { + document.getElementById('editBatchId').value = data.id; + document.getElementById('editBatchName').value = data.name; + document.getElementById('editBatchYear').value = data.year; + document.getElementById('editBatchStatus').checked = data.is_active; + modal.style.display = 'block'; + } + }) + .catch(error => { + console.error('Error fetching batch data:', error); + alert('Error loading batch data'); + }); +} + +function deleteBatch(id) { + if (confirm('Are you sure you want to delete this batch?')) { + fetch(`/app/batch/${id}/delete/`, { + method: 'POST', + headers: { + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value, + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + const row = document.querySelector(`tr[data-batch-id="${id}"]`); + if (row) { + row.remove(); + } + alert('Batch deleted successfully'); + } else { + alert('Error: ' + data.message); + } + }) + .catch(error => { + console.error('Error deleting batch:', error); + alert('Error deleting batch'); + }); + } +} + +// Tracking Management Functions +function editTracking(id) { + fetch(`/app/tracking/${id}/edit/`) + .then(response => response.json()) + .then(data => { + const modal = document.getElementById('editTrackingModal'); + if (modal) { + document.getElementById('editTrackingId').value = data.id; + document.getElementById('editTrackingStudent').value = data.student_id; + document.getElementById('editTrackingCourse').value = data.course_id; + document.getElementById('editTrackingProgress').value = data.progress_status; + document.getElementById('editTrackingPercentage').value = data.completion_percentage; + document.getElementById('editTrackingStartDate').value = data.start_date; + document.getElementById('editTrackingCurrentPeriod').value = data.current_period; + modal.style.display = 'block'; + } + }) + .catch(error => { + console.error('Error fetching tracking data:', error); + alert('Error loading tracking data'); + }); +} + +function deleteTracking(id) { + if (confirm('Are you sure you want to delete this tracking record?')) { + fetch(`/app/tracking/${id}/delete/`, { + method: 'POST', + headers: { + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value, + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + const row = document.querySelector(`tr[data-tracking-id="${id}"]`); + if (row) { + row.remove(); + } + alert('Tracking record deleted successfully'); + } else { + alert('Error: ' + data.message); + } + }) + .catch(error => { + console.error('Error deleting tracking record:', error); + alert('Error deleting tracking record'); + }); + } +} + +// Filter Functions +function filterStudents() { + const searchText = document.getElementById('studentSearch').value.toLowerCase(); + const statusFilter = document.getElementById('statusFilter').value; + const table = document.getElementById('studentTable'); + const rows = table.getElementsByTagName('tr'); + + for (let i = 1; i < rows.length; i++) { + const row = rows[i]; + const cells = row.getElementsByTagName('td'); + if (cells.length === 0) continue; + + const studentDetails = cells[1].textContent.toLowerCase(); + const contactInfo = cells[2].textContent.toLowerCase(); + const parentInfo = cells[3].textContent.toLowerCase(); + const courseInfo = cells[4].textContent.toLowerCase(); + const status = cells[5].textContent.trim().toLowerCase(); + + const matchesSearch = studentDetails.includes(searchText) || + contactInfo.includes(searchText) || + parentInfo.includes(searchText) || + courseInfo.includes(searchText); + const matchesStatus = !statusFilter || status.includes(statusFilter.toLowerCase()); + + row.style.display = (matchesSearch && matchesStatus) ? '' : 'none'; + } +} + +function filterTrackings() { + const searchText = document.getElementById('trackingSearch').value.toLowerCase(); + const progressFilter = document.getElementById('progressFilter').value; + const table = document.getElementById('trackingTable'); + const rows = table.getElementsByTagName('tr'); + + for (let i = 1; i < rows.length; i++) { + const row = rows[i]; + const cells = row.getElementsByTagName('td'); + if (cells.length === 0) continue; + + const student = cells[0].textContent.toLowerCase(); + const course = cells[1].textContent.toLowerCase(); + const progress = cells[2].textContent.trim().toLowerCase(); + + const matchesSearch = student.includes(searchText) || course.includes(searchText); + const matchesProgress = !progressFilter || progress === progressFilter.toLowerCase(); + + row.style.display = (matchesSearch && matchesProgress) ? '' : 'none'; + } +} + +// Close modals when clicking outside +window.onclick = function(event) { + const modals = document.querySelectorAll('.modal'); + modals.forEach(modal => { + if (event.target === modal) { + modal.style.display = 'none'; + } + }); +} + +// Close modals with close button +document.querySelectorAll('.close-modal').forEach(button => { + button.onclick = function() { + const modal = this.closest('.modal'); + if (modal) { + modal.style.display = 'none'; + } + }; +}); + +// Modal Functions +function openAddCourseTrackingModal() { + // Reset form before showing modal + const form = document.getElementById('addCourseTrackingForm'); + if (form) { + form.reset(); + } + + // Load students for dropdown + fetch('/app/get-students/', { + headers: { + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value, + 'Accept': 'application/json' + } + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.includes('application/json')) { + throw new Error('Response was not JSON'); + } + return response.json(); + }) + .then(data => { + const studentSelect = document.querySelector('#student'); + if (!studentSelect) { + console.error('Student select element not found'); + return; + } + + studentSelect.innerHTML = ''; + + if (data.success && data.students && Array.isArray(data.students)) { + data.students.forEach(student => { + if (student && student.id && student.display_name) { + studentSelect.innerHTML += ``; + } + }); + } else { + studentSelect.innerHTML = ''; + } + }) + .catch(error => { + console.error('Error loading students:', error); + const studentSelect = document.querySelector('#student'); + if (studentSelect) { + studentSelect.innerHTML = ''; + } + }); + + // Load courses for dropdown + fetch('/app/get-courses/', { + headers: { + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value, + 'Accept': 'application/json' + } + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.includes('application/json')) { + throw new Error('Response was not JSON'); + } + return response.json(); + }) + .then(data => { + const courseSelect = document.querySelector('#track_course'); + if (!courseSelect) { + console.error('Course select element not found'); + return; + } + + courseSelect.innerHTML = ''; + + if (data.success && data.courses && Array.isArray(data.courses)) { + data.courses.forEach(course => { + if (course && course.id && course.display_name) { + courseSelect.innerHTML += ``; + } + }); + } else { + courseSelect.innerHTML = ''; + } + }) + .catch(error => { + console.error('Error loading courses:', error); + const courseSelect = document.querySelector('#track_course'); + if (courseSelect) { + courseSelect.innerHTML = ''; + } + }); + + // Show the modal + const modal = document.getElementById('addCourseTrackingModal'); + if (modal) { + modal.style.display = 'block'; + document.body.style.overflow = 'hidden'; + } +} + +function closeAddCourseTrackingModal() { + document.getElementById('addCourseTrackingModal').style.display = 'none'; + document.body.style.overflow = ''; +} diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 61c7ce1..aa0bce3 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -42,6 +42,28 @@ document.addEventListener("DOMContentLoaded", function() { closeAllModals(); } }); + + // Initialize responsive modals + initResponsiveModals(); + + // Make all tables responsive + makeAllTablesResponsive(); + + // Handle long content + handleLongContent(); + + // Process any dynamically added tables + const observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + if (mutation.type === 'childList') { + makeAllTablesResponsive(); + handleLongContent(); + } + }); + }); + + // Start observing the document for added nodes + observer.observe(document.body, { childList: true, subtree: true }); }); // Function to initialize all dashboard components @@ -485,11 +507,8 @@ function showWeakPasswordModal() { return; } - if (newPassword === '123') { - alert('Please choose a stronger password!'); - passwordArea.focus(); - return; - } + // Get CSRF token + var csrfToken = document.querySelector('input[name="csrfmiddlewaretoken"]')?.value || ''; // Create form data var formData = new FormData(); @@ -503,25 +522,27 @@ function showWeakPasswordModal() { method: 'POST', body: formData, headers: { - 'X-Requested-With': 'XMLHttpRequest', - 'Cache-Control': 'no-cache' + 'X-Requested-With': 'XMLHttpRequest' } }) - .then(function(response) { - return response.json(); - }) + .then(response => response.json()) .then(function(data) { if (data.success) { - // Use notification system instead of alert + // Show success message if (typeof window.showNotification === 'function') { - window.showNotification('Password Updated', 'Your password has been changed successfully!', 'success'); + window.showNotification('Password Updated', data.message || 'Your password has been changed successfully. Please login again.', 'success'); } else { - alert('Password changed successfully!'); + alert(data.message || 'Password changed successfully. Please login again.'); } // Close the modal document.getElementById('weakPasswordModal')?.remove(); - // No redirect to login page + + // Wait a moment for the notification to be visible + setTimeout(function() { + // Redirect to login page + window.location.href = data.redirect || '/login/'; + }, 1500); } else if (data.error) { // Show error notification if (typeof window.showNotification === 'function') { @@ -585,18 +606,6 @@ function setupEventHandlers() { }); } -// Function to get device token for notifications -function getDeviceToken() { - // Do nothing - removed to prevent recursion - console.log("getDeviceToken called - disabled to prevent recursion"); -} - -// Add global function to get device token - but avoid recursion -window.getDeviceToken = function() { - console.log("FCM token function called once"); - // Implementation depends on your Firebase setup - but don't call itself again -}; - // Function to close all custom modals function closeAllModals() { // Close standard modals with display:none style @@ -730,12 +739,6 @@ function setupDirectPasswordHandlers() { return; } - if (newPassword === '123') { - alert('Please choose a stronger password!'); - passwordArea.focus(); - return; - } - // Get CSRF token var csrfToken = document.querySelector('input[name="csrfmiddlewaretoken"]')?.value || ''; @@ -757,16 +760,21 @@ function setupDirectPasswordHandlers() { .then(response => response.json()) .then(function(data) { if (data.success) { - // Use notification system instead of alert + // Show success message if (typeof window.showNotification === 'function') { - window.showNotification('Password Updated', 'Your password has been changed successfully!', 'success'); + window.showNotification('Password Updated', data.message || 'Your password has been changed successfully. Please login again.', 'success'); } else { - alert('Password changed successfully!'); + alert(data.message || 'Password changed successfully. Please login again.'); } // Close the modal document.getElementById('weakPasswordModal')?.remove(); - // No redirect to login page + + // Wait a moment for the notification to be visible + setTimeout(function() { + // Redirect to login page + window.location.href = data.redirect || '/login/'; + }, 1500); } else if (data.error) { // Show error notification if (typeof window.showNotification === 'function') { @@ -782,4 +790,98 @@ function setupDirectPasswordHandlers() { }); }); } +} + +// Function to initialize responsive modals +function initResponsiveModals() { + // Function to update modal styles based on screen size + function updateModalStyles() { + const isMobile = window.innerWidth < 768; + const isSmallMobile = window.innerWidth < 576; + + document.querySelectorAll('.modal-dialog').forEach(function(dialog) { + if (isSmallMobile) { + dialog.classList.add('modal-fullscreen-sm-down'); + dialog.classList.remove('modal-dialog-centered'); + } else if (isMobile) { + dialog.classList.remove('modal-fullscreen-sm-down'); + dialog.classList.add('modal-dialog-centered'); + } else { + dialog.classList.remove('modal-fullscreen-sm-down', 'modal-dialog-centered'); + } + }); + } + + // Initial update + updateModalStyles(); + + // Update on resize + window.addEventListener('resize', debounce(updateModalStyles, 250)); + + // Update when modal is shown + document.querySelectorAll('.modal').forEach(function(modal) { + modal.addEventListener('show.bs.modal', updateModalStyles); + }); +} + +// Function to make all tables responsive +function makeAllTablesResponsive() { + document.querySelectorAll('table:not(.table-responsive)').forEach(function(table) { + if (!table.parentElement.classList.contains('table-responsive')) { + const wrapper = document.createElement('div'); + wrapper.classList.add('table-responsive'); + table.parentNode.insertBefore(wrapper, table); + wrapper.appendChild(table); + } + }); +} + +// Function to handle long content +function handleLongContent() { + const isMobile = window.innerWidth < 768; + + // Handle text truncation + document.querySelectorAll('[data-truncate-mobile]').forEach(function(element) { + if (isMobile) { + const fullText = element.getAttribute('data-full-text') || element.textContent; + const maxLength = parseInt(element.getAttribute('data-truncate-mobile'), 10) || 50; + + if (!element.getAttribute('data-full-text')) { + element.setAttribute('data-full-text', fullText); + } + + if (fullText.length > maxLength) { + element.textContent = fullText.substring(0, maxLength) + '...'; + } + } else { + // Restore full text on desktop + if (element.getAttribute('data-full-text')) { + element.textContent = element.getAttribute('data-full-text'); + } + } + }); + + // Handle long tables + document.querySelectorAll('.table-responsive').forEach(function(table) { + if (isMobile) { + table.style.maxHeight = '400px'; + table.style.overflowY = 'auto'; + } else { + table.style.maxHeight = ''; + table.style.overflowY = ''; + } + }); +} + +// Debounce function to limit the rate at which a function can fire +function debounce(func, wait) { + let timeout; + return function() { + const context = this; + const args = arguments; + clearTimeout(timeout); + timeout = setTimeout(function() { + func.apply(context, args); + }, wait); + }; } \ No newline at end of file diff --git a/static/js/firebase-init.js b/static/js/firebase-init.js index bafee3c..14175ed 100644 --- a/static/js/firebase-init.js +++ b/static/js/firebase-init.js @@ -157,4 +157,4 @@ messaging.onMessage((payload) => { }); }); } -}); \ No newline at end of file +}); diff --git a/static/js/hod-dashboard.js b/static/js/hod-dashboard.js new file mode 100644 index 0000000..7cb9f33 --- /dev/null +++ b/static/js/hod-dashboard.js @@ -0,0 +1,49 @@ +// Subjects Section +function openAddSubjectModal() { + document.getElementById('addSubjectModal').style.display = 'block'; +} + +function closeAddSubjectModal() { + document.getElementById('addSubjectModal').style.display = 'none'; +} + +function confirmDeleteSubject(subjectId) { + if (confirm('Are you sure you want to delete this subject? This action cannot be undone.')) { + // Create a form and submit it + const form = document.createElement('form'); + form.method = 'POST'; + form.action = `/app/delete-subject/${subjectId}/`; + + // Add CSRF token + const csrfInput = document.createElement('input'); + csrfInput.type = 'hidden'; + csrfInput.name = 'csrfmiddlewaretoken'; + csrfInput.value = document.querySelector('[name=csrfmiddlewaretoken]').value; + form.appendChild(csrfInput); + + // Submit the form + document.body.appendChild(form); + form.submit(); + } +} + +// Staff Section +function confirmDeleteStaff(staffId) { + if (confirm('Are you sure you want to delete this staff member? This action cannot be undone.')) { + // Create a form and submit it + const form = document.createElement('form'); + form.method = 'POST'; + form.action = `/app/staff/remove/${staffId}/`; + + // Add CSRF token + const csrfInput = document.createElement('input'); + csrfInput.type = 'hidden'; + csrfInput.name = 'csrfmiddlewaretoken'; + csrfInput.value = document.querySelector('[name=csrfmiddlewaretoken]').value; + form.appendChild(csrfInput); + + // Submit the form + document.body.appendChild(form); + form.submit(); + } +} \ No newline at end of file diff --git a/static/js/hod-subjects.js b/static/js/hod-subjects.js new file mode 100644 index 0000000..94291ed --- /dev/null +++ b/static/js/hod-subjects.js @@ -0,0 +1,201 @@ +// Define functions in global scope +window.viewSubjectDetails = function(subjectId) { + console.log('Viewing subject details for ID:', subjectId); + // Fetch subject details from the server + fetch(`/api/subjects/${subjectId}/`) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + // Populate the view modal with subject details + document.getElementById('viewSubjectName').textContent = data.name; + document.getElementById('viewSubjectCode').textContent = data.code || 'N/A'; + document.getElementById('viewSubjectPeriod').textContent = data.period_or_year; + document.getElementById('viewSubjectTeacher').textContent = data.teacher ? data.teacher.name : 'Not Assigned'; + document.getElementById('viewSubjectStudents').textContent = data.total_students || '0'; + document.getElementById('viewSubjectClasses').textContent = data.classes_per_week || '0'; + + // Show the view modal + const viewModal = new bootstrap.Modal(document.getElementById('viewSubjectModal')); + viewModal.show(); + }) + .catch(error => { + console.error('Error fetching subject details:', error); + showToast('Error', 'Failed to fetch subject details', 'error'); + }); +}; + +window.editSubject = function(subjectId) { + console.log('Editing subject with ID:', subjectId); + // Fetch subject details for editing + fetch(`/api/subjects/${subjectId}/`) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + // Populate the edit form + document.getElementById('editSubjectId').value = data.id; + document.getElementById('editSubjectName').value = data.name; + document.getElementById('editSubjectCode').value = data.code || ''; + document.getElementById('editSubjectPeriod').value = data.period_or_year; + document.getElementById('editSubjectTeacher').value = data.teacher ? data.teacher.id : ''; + + // Show the edit modal + const editModal = new bootstrap.Modal(document.getElementById('editSubjectModal')); + editModal.show(); + }) + .catch(error => { + console.error('Error fetching subject details:', error); + showToast('Error', 'Failed to fetch subject details', 'error'); + }); +}; + +window.updateSubject = function() { + const subjectId = document.getElementById('editSubjectId').value; + console.log('Updating subject with ID:', subjectId); + + const formData = new FormData(document.getElementById('editSubjectForm')); + + fetch(`/api/subjects/${subjectId}/`, { + method: 'PUT', + body: formData, + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + // Close the modal + const editModal = bootstrap.Modal.getInstance(document.getElementById('editSubjectModal')); + editModal.hide(); + + // Show success message + showToast('Success', 'Subject updated successfully', 'success'); + + // Refresh the subjects table + location.reload(); + }) + .catch(error => { + console.error('Error updating subject:', error); + showToast('Error', 'Failed to update subject', 'error'); + }); +}; + +window.confirmDeleteSubject = function(subjectId) { + console.log('Confirming deletion of subject with ID:', subjectId); + if (confirm('Are you sure you want to delete this subject? This action cannot be undone.')) { + fetch(`/api/subjects/${subjectId}/`, { + method: 'DELETE', + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + showToast('Success', 'Subject deleted successfully', 'success'); + location.reload(); + }) + .catch(error => { + console.error('Error deleting subject:', error); + showToast('Error', 'Failed to delete subject', 'error'); + }); + } +}; + +// Utility function to get CSRF token +function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; +} + +// Utility function to show toast messages +function showToast(title, message, type = 'info') { + const toastContainer = document.getElementById('toastContainer') || createToastContainer(); + const toast = document.createElement('div'); + toast.className = `toast align-items-center text-white bg-${type} border-0`; + toast.setAttribute('role', 'alert'); + toast.setAttribute('aria-live', 'assertive'); + toast.setAttribute('aria-atomic', 'true'); + + toast.innerHTML = ` +
+
+ ${title}
+ ${message} +
+ +
+ `; + + toastContainer.appendChild(toast); + const bsToast = new bootstrap.Toast(toast); + bsToast.show(); + + toast.addEventListener('hidden.bs.toast', () => { + toast.remove(); + }); +} + +function createToastContainer() { + const container = document.createElement('div'); + container.id = 'toastContainer'; + container.className = 'toast-container position-fixed bottom-0 end-0 p-3'; + document.body.appendChild(container); + return container; +} + +// Initialize when DOM is loaded +document.addEventListener('DOMContentLoaded', function() { + console.log('HOD Subjects JS initialized'); + + // Initialize all subject-related event listeners + initializeSubjectEventListeners(); +}); + +function initializeSubjectEventListeners() { + // Get all subject action buttons + const viewButtons = document.querySelectorAll('[onclick^="viewSubjectDetails"]'); + const editButtons = document.querySelectorAll('[onclick^="editSubject"]'); + const deleteButtons = document.querySelectorAll('[onclick^="confirmDeleteSubject"]'); + + // Add event listeners to view buttons + viewButtons.forEach(button => { + const subjectId = button.getAttribute('onclick').match(/'([^']+)'/)[1]; + button.onclick = () => viewSubjectDetails(subjectId); + }); + + // Add event listeners to edit buttons + editButtons.forEach(button => { + const subjectId = button.getAttribute('onclick').match(/'([^']+)'/)[1]; + button.onclick = () => editSubject(subjectId); + }); + + // Add event listeners to delete buttons + deleteButtons.forEach(button => { + const subjectId = button.getAttribute('onclick').match(/'([^']+)'/)[1]; + button.onclick = () => confirmDeleteSubject(subjectId); + }); +} \ No newline at end of file diff --git a/static/js/hod/dashboard.js b/static/js/hod/dashboard.js new file mode 100644 index 0000000..eeea72e --- /dev/null +++ b/static/js/hod/dashboard.js @@ -0,0 +1,256 @@ +import { showToast, showMessageModal, closeMessageModal } from './utils/notifications.js'; +import * as meetingModals from './modals/meeting.js'; +import * as studentModals from './modals/student.js'; +import * as staffModals from './modals/staff.js'; + +// Make functions available globally +window.showToast = showToast; +window.showMessageModal = showMessageModal; +window.closeMessageModal = closeMessageModal; + +// Meeting functions +window.openAddMeetingModal = meetingModals.openAddMeetingModal; +window.closeAddMeetingModal = meetingModals.closeAddMeetingModal; +window.openEditMeetingModal = meetingModals.openEditMeetingModal; +window.closeEditMeetingModal = meetingModals.closeEditMeetingModal; +window.openCancelMeetingModal = meetingModals.openCancelMeetingModal; +window.closeCancelMeetingModal = meetingModals.closeCancelMeetingModal; +window.viewMeetingDetails = meetingModals.viewMeetingDetails; +window.closeViewMeetingModal = meetingModals.closeViewMeetingModal; +window.submitAddMeetingForm = meetingModals.submitAddMeetingForm; +window.submitEditMeetingForm = meetingModals.submitEditMeetingForm; +window.submitCancelMeetingForm = meetingModals.submitCancelMeetingForm; + +// Student functions +window.openAddStudentModal = studentModals.openAddStudentModal; +window.closeAddStudentModal = studentModals.closeAddStudentModal; +window.openEditStudentModal = studentModals.openEditStudentModal; +window.closeEditStudentModal = studentModals.closeEditStudentModal; +window.openViewStudentModal = studentModals.openViewStudentModal; +window.closeViewStudentModal = studentModals.closeViewStudentModal; +window.submitAddStudentForm = studentModals.submitAddStudentForm; +window.submitEditStudentForm = studentModals.submitEditStudentForm; + +// Staff functions +window.openAddStaffModal = staffModals.openAddStaffModal; +window.closeAddStaffModal = staffModals.closeAddStaffModal; +window.openEditStaffModal = staffModals.openEditStaffModal; +window.closeEditStaffModal = staffModals.closeEditStaffModal; +window.openViewStaffModal = staffModals.openViewStaffModal; +window.closeViewStaffModal = staffModals.closeViewStaffModal; +window.submitAddStaffForm = staffModals.submitAddStaffForm; +window.submitEditStaffForm = staffModals.submitEditStaffForm; + +// Initialize event listeners when DOM is loaded +document.addEventListener('DOMContentLoaded', function() { + // Initialize duration filter for subjects + const durationButtons = document.querySelectorAll('#durationFilter button'); + const subjectRows = document.querySelectorAll('#subjectsTableBody tr[data-period]'); + + durationButtons.forEach(button => { + button.addEventListener('click', function() { + // Remove active class from all buttons + durationButtons.forEach(btn => btn.classList.remove('active')); + // Add active class to clicked button + this.classList.add('active'); + + const selectedDuration = this.dataset.duration; + + // Show/hide rows based on selected duration + subjectRows.forEach(row => { + if (selectedDuration === 'all' || row.dataset.period === selectedDuration) { + row.style.display = ''; + } else { + row.style.display = 'none'; + } + }); + + // Show/hide "no subjects" message + const noSubjectsRow = document.querySelector('#subjectsTableBody tr:not([data-period])'); + if (noSubjectsRow) { + const visibleRows = Array.from(subjectRows).filter(row => row.style.display !== 'none'); + noSubjectsRow.style.display = visibleRows.length === 0 ? '' : 'none'; + } + }); + }); + + // Initialize online meeting toggle listeners + const isOnlineCheckbox = document.getElementById('isOnline'); + const meetingLinkContainer = document.getElementById('meetingLinkContainer'); + + if (isOnlineCheckbox) { + isOnlineCheckbox.addEventListener('change', function() { + meetingLinkContainer.style.display = this.checked ? 'block' : 'none'; + }); + } + + const editIsOnlineCheckbox = document.getElementById('editIsOnline'); + const editMeetingLinkContainer = document.getElementById('editMeetingLinkContainer'); + + if (editIsOnlineCheckbox) { + editIsOnlineCheckbox.addEventListener('change', function() { + editMeetingLinkContainer.style.display = this.checked ? 'block' : 'none'; + }); + } + + // Add event listeners for closing modals with Escape key + document.addEventListener('keydown', function(event) { + if (event.key === 'Escape') { + const meetingNotesModal = document.getElementById('meetingNotesModal'); + const meetingAgendaModal = document.getElementById('meetingAgendaModal'); + const cancellationReasonModal = document.getElementById('cancellationReasonModal'); + const addMeetingModal = document.getElementById('addMeetingModal'); + const editMeetingModal = document.getElementById('editMeetingModal'); + const cancelMeetingModal = document.getElementById('cancelMeetingModal'); + const viewMeetingModal = document.getElementById('viewMeetingModal'); + + if (meetingNotesModal && meetingNotesModal.classList.contains('show')) { + meetingModals.closeMeetingNotesModal(); + } + if (meetingAgendaModal && meetingAgendaModal.classList.contains('show')) { + meetingModals.closeMeetingAgendaModal(); + } + if (cancellationReasonModal && cancellationReasonModal.classList.contains('show')) { + meetingModals.closeCancellationReasonModal(); + } + if (addMeetingModal && addMeetingModal.classList.contains('show')) { + meetingModals.closeAddMeetingModal(); + } + if (editMeetingModal && editMeetingModal.classList.contains('show')) { + meetingModals.closeEditMeetingModal(); + } + if (cancelMeetingModal && cancelMeetingModal.classList.contains('show')) { + meetingModals.closeCancelMeetingModal(); + } + if (viewMeetingModal && viewMeetingModal.classList.contains('show')) { + meetingModals.closeViewMeetingModal(); + } + } + }); + + // Initialize search functionality for progress section + const progressSearchInput = document.getElementById('progressSearchInput'); + const progressSearchButton = document.getElementById('progressSearchButton'); + + if (progressSearchInput && progressSearchButton) { + // Function to perform search + const performSearch = () => { + const searchTerm = progressSearchInput.value.toLowerCase().trim(); + const progressRows = document.querySelectorAll('#progressTable tbody tr'); + let visibleRowCount = 0; + + progressRows.forEach(row => { + // Skip the "No progress data available" row if it exists + if (!row.querySelector('td[colspan]')) { + // Get text content from student name and batch columns + const studentNameCell = row.querySelector('td:nth-child(1)'); + const batchCell = row.querySelector('td:nth-child(2)'); + + const studentText = studentNameCell ? studentNameCell.textContent.toLowerCase() : ''; + const batchText = batchCell ? batchCell.textContent.toLowerCase() : ''; + + // Show row if search term is found in student name or batch + if (searchTerm === '' || + studentText.includes(searchTerm) || + batchText.includes(searchTerm)) { + row.style.display = ''; + visibleRowCount++; + } else { + row.style.display = 'none'; + } + } + }); + + // Show/hide "no data" message + const noProgressDataMessage = document.getElementById('noProgressDataMessage'); + if (noProgressDataMessage) { + noProgressDataMessage.style.display = (visibleRowCount === 0 && searchTerm !== '') ? 'block' : 'none'; + } + + // Update summary cards with filtered data + updateProgressSummary(progressRows, searchTerm); + }; + + // Button click event listener + progressSearchButton.addEventListener('click', performSearch); + + // Enter key press event listener + progressSearchInput.addEventListener('keyup', function(event) { + if (event.key === 'Enter') { + performSearch(); + } + }); + + // Add input event listener for real-time filtering (optional) + progressSearchInput.addEventListener('input', function() { + // Only perform search if the input has at least 3 characters or is empty + if (this.value.length >= 3 || this.value.length === 0) { + performSearch(); + } + }); + } + + // Function to update progress summary based on visible rows + function updateProgressSummary(rows, searchTerm) { + if (!searchTerm || searchTerm === '') { + // If no search is applied, revert to original values + if (document.getElementById('avgCompletion')) { + document.getElementById('avgCompletion').textContent = originalAvgCompletion + '%'; + } + if (document.getElementById('studentsOnTrack')) { + document.getElementById('studentsOnTrack').textContent = originalStudentsOnTrack; + } + if (document.getElementById('studentsAtRisk')) { + document.getElementById('studentsAtRisk').textContent = originalStudentsAtRisk; + } + return; + } + + let totalCompletion = 0; + let studentsOnTrack = 0; + let studentsAtRisk = 0; + let visibleStudents = 0; + + rows.forEach(row => { + if (row.style.display !== 'none' && !row.querySelector('td[colspan]')) { + visibleStudents++; + + // Get completion percentage from the progress bar + const progressCell = row.querySelector('td:nth-child(4)'); + if (progressCell) { + const percentText = progressCell.querySelector('.small')?.textContent || '0%'; + const completionPercentage = parseFloat(percentText); + + if (!isNaN(completionPercentage)) { + totalCompletion += completionPercentage; + + // Count students on track and at risk + if (completionPercentage >= 75) { + studentsOnTrack++; + } else if (completionPercentage < 40) { + studentsAtRisk++; + } + } + } + } + }); + + // Update summary cards with filtered data + const avgCompletion = visibleStudents > 0 ? Math.round(totalCompletion / visibleStudents) : 0; + + if (document.getElementById('avgCompletion')) { + document.getElementById('avgCompletion').textContent = avgCompletion + '%'; + } + if (document.getElementById('studentsOnTrack')) { + document.getElementById('studentsOnTrack').textContent = studentsOnTrack; + } + if (document.getElementById('studentsAtRisk')) { + document.getElementById('studentsAtRisk').textContent = studentsAtRisk; + } + } + + // Store original summary values + const originalAvgCompletion = document.getElementById('avgCompletion') ? document.getElementById('avgCompletion').textContent.replace('%', '') : '0'; + const originalStudentsOnTrack = document.getElementById('studentsOnTrack') ? document.getElementById('studentsOnTrack').textContent : '0'; + const originalStudentsAtRisk = document.getElementById('studentsAtRisk') ? document.getElementById('studentsAtRisk').textContent : '0'; +}); \ No newline at end of file diff --git a/static/js/hod/modals/meeting.js b/static/js/hod/modals/meeting.js new file mode 100644 index 0000000..90d8900 --- /dev/null +++ b/static/js/hod/modals/meeting.js @@ -0,0 +1,332 @@ +import { showToast } from '../utils/notifications.js'; + +// Meeting status badge helper +function getStatusBadgeClass(status) { + switch (status) { + case 'scheduled': + return 'bg-primary'; + case 'completed': + return 'bg-success'; + case 'cancelled': + return 'bg-danger'; + case 'rescheduled': + return 'bg-warning'; + default: + return 'bg-secondary'; + } +} + +// Meeting Modal Functions +function openAddMeetingModal() { + document.getElementById('addMeetingModal').style.display = 'block'; + document.body.style.overflow = 'hidden'; +} + +function closeAddMeetingModal() { + document.getElementById('addMeetingModal').style.display = 'none'; + document.body.style.overflow = ''; + document.getElementById('addMeetingForm').reset(); +} + +function openEditMeetingModal(meetingId) { + fetch(`/app/api/hod/meetings/${meetingId}/`) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok: ' + response.statusText); + } + return response.json(); + }) + .then(data => { + document.getElementById('editMeetingId').value = meetingId; + document.getElementById('editMeetingDate').value = data.meeting_date; + document.getElementById('editMeetingTime').value = data.meeting_time; + document.getElementById('editDuration').value = data.duration; + document.getElementById('editAgenda').value = data.agenda || ''; + document.getElementById('editIsOnline').checked = data.is_online; + document.getElementById('editMeetingLink').value = data.meeting_link || ''; + document.getElementById('editNotes').value = data.notes || ''; + + if (data.is_online) { + document.getElementById('editMeetingLinkContainer').style.display = 'block'; + } else { + document.getElementById('editMeetingLinkContainer').style.display = 'none'; + } + + // Show modal using direct style manipulation + document.getElementById('editMeetingModal').style.display = 'block'; + document.body.style.overflow = 'hidden'; + }) + .catch(error => { + console.error('Error fetching meeting details:', error); + if (typeof window.showMessageModal === 'function') { + window.showMessageModal('Error', 'Failed to load meeting details'); + } else if (typeof showToast === 'function') { + showToast('error', 'Failed to load meeting details'); + } else { + alert('Failed to load meeting details'); + } + }); +} + +function closeEditMeetingModal() { + document.getElementById('editMeetingModal').style.display = 'none'; + document.body.style.overflow = ''; + document.getElementById('editMeetingForm').reset(); +} + +function openCancelMeetingModal(meetingId) { + document.getElementById('cancelMeetingId').value = meetingId; + document.getElementById('cancellationReason').value = ''; + document.getElementById('cancelMeetingModal').style.display = 'block'; + document.body.style.overflow = 'hidden'; +} + +function closeCancelMeetingModal() { + document.getElementById('cancelMeetingModal').style.display = 'none'; + document.body.style.overflow = ''; + document.getElementById('cancelMeetingForm').reset(); +} + +function viewMeetingDetails(meetingId) { + fetch(`/app/api/hod/meetings/${meetingId}/`) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok: ' + response.statusText); + } + return response.json(); + }) + .then(data => { + document.getElementById('viewMeetingDate').textContent = new Date(data.meeting_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); + document.getElementById('viewMeetingTime').textContent = new Date('1970-01-01T' + data.meeting_time).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); + document.getElementById('viewMeetingDuration').textContent = `${data.duration} minutes`; + document.getElementById('viewMeetingStatus').innerHTML = `${data.status_display}`; + document.getElementById('viewMeetingAgenda').textContent = data.agenda || 'No agenda available'; + document.getElementById('viewMeetingNotes').textContent = data.notes || 'No notes available'; + document.getElementById('viewMeetingType').textContent = data.is_online ? 'Online Meeting' : 'In-Person Meeting'; + + if (data.is_online && data.meeting_link) { + document.getElementById('viewMeetingLinkContainer').style.display = 'block'; + document.getElementById('viewMeetingLink').innerHTML = `${data.meeting_link}`; + } else { + document.getElementById('viewMeetingLinkContainer').style.display = 'none'; + } + + // Handle cancellation reason + const cancellationContainer = document.getElementById('viewCancellationReasonContainer'); + const cancellationReason = document.getElementById('viewCancellationReason'); + if (data.status === 'cancelled' && data.cancellation_reason) { + cancellationContainer.style.display = 'block'; + cancellationReason.textContent = data.cancellation_reason; + } else { + cancellationContainer.style.display = 'none'; + } + + // Set up edit button + const editBtn = document.getElementById('editMeetingBtn'); + if (editBtn) { + editBtn.onclick = () => { + closeViewMeetingModal(); + openEditMeetingModal(meetingId); + }; + } + + // Show modal using direct style manipulation + const modal = document.getElementById('viewMeetingModal'); + if (modal) { + modal.style.display = 'block'; + document.body.style.overflow = 'hidden'; + } else { + console.error('Modal element not found!'); + alert('Error: Modal element not found in the page.'); + } + }) + .catch(error => { + console.error('Error fetching meeting details:', error); + if (typeof window.showMessageModal === 'function') { + window.showMessageModal('Error', 'Failed to load meeting details'); + } else if (typeof showToast === 'function') { + showToast('error', 'Failed to load meeting details'); + } else { + alert('Failed to load meeting details'); + } + }); +} + +function closeViewMeetingModal() { + document.getElementById('viewMeetingModal').style.display = 'none'; + document.body.style.overflow = ''; +} + +function submitAddMeetingForm() { + const form = document.getElementById('addMeetingForm'); + const formData = new FormData(form); + + // IMPORTANT: Ensure meeting_link is always sent and not null + if (!formData.has('meeting_link') || formData.get('meeting_link') === null) { + formData.set('meeting_link', ''); + } + + fetch('/app/api/hod/meetings/', { + method: 'POST', + body: formData, + headers: { + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value + } + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok: ' + response.statusText); + } + return response.json(); + }) + .then(data => { + closeAddMeetingModal(); + if (typeof window.showMessageModal === 'function') { + window.showMessageModal('Success', 'Meeting scheduled successfully'); + } else if (typeof showToast === 'function') { + showToast('success', 'Meeting scheduled successfully'); + } else { + alert('Meeting scheduled successfully'); + } + setTimeout(() => { + location.reload(); + }, 1500); + }) + .catch(error => { + console.error('Error scheduling meeting:', error); + if (typeof window.showMessageModal === 'function') { + window.showMessageModal('Error', 'Failed to schedule meeting. Please try again.'); + } else if (typeof showToast === 'function') { + showToast('error', 'Failed to schedule meeting. Please try again.'); + } else { + alert('Failed to schedule meeting. Please try again.'); + } + }); +} + +function submitEditMeetingForm() { + const form = document.getElementById('editMeetingForm'); + const formData = new FormData(form); + const meetingId = document.getElementById('editMeetingId').value; + + // Update is_online to be a string + formData.set('is_online', document.getElementById('editIsOnline').checked ? 'true' : 'false'); + + // IMPORTANT: Always include meeting_link (empty string if not online) to satisfy NOT NULL constraint + if (!formData.has('meeting_link') || formData.get('meeting_link') === null) { + formData.set('meeting_link', ''); + } + + // Ensure notes field is included + if (document.getElementById('editNotes')) { + formData.set('notes', document.getElementById('editNotes').value || ''); + } + + console.log("Submitting edit meeting form for meeting ID:", meetingId); + + fetch(`/app/api/hod/meetings/${meetingId}/edit/`, { + method: 'POST', + body: formData, + headers: { + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value + } + }) + .then(response => { + if (!response.ok) { + return response.text().then(text => { + console.error("Server response:", text); + throw new Error('Server error: ' + response.status); + }); + } + return response.json(); + }) + .then(data => { + // Close modal using direct style manipulation (not Bootstrap) + document.getElementById('editMeetingModal').style.display = 'none'; + document.body.style.overflow = ''; + document.getElementById('editMeetingForm').reset(); + + // Show success message + if (typeof window.showMessageModal === 'function') { + window.showMessageModal('Success', 'Meeting updated successfully'); + } else if (typeof showToast === 'function') { + showToast('success', 'Meeting updated successfully'); + } else { + alert('Meeting updated successfully'); + } + + // Reload the page after a short delay + setTimeout(() => { + location.reload(); + }, 1500); + }) + .catch(error => { + console.error('Error updating meeting:', error); + if (typeof window.showMessageModal === 'function') { + window.showMessageModal('Error', 'Failed to update meeting. Please try again.'); + } else if (typeof showToast === 'function') { + showToast('error', 'Failed to update meeting. Please try again.'); + } else { + alert('Failed to update meeting. Please try again.'); + } + }); +} + +function submitCancelMeetingForm() { + const form = document.getElementById('cancelMeetingForm'); + const formData = new FormData(form); + const meetingId = document.getElementById('cancelMeetingId').value; + + fetch(`/app/api/hod/meetings/${meetingId}/cancel/`, { + method: 'POST', + body: formData, + headers: { + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value + } + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok: ' + response.statusText); + } + return response.json(); + }) + .then(data => { + closeCancelMeetingModal(); + if (typeof window.showMessageModal === 'function') { + window.showMessageModal('Success', 'Meeting cancelled successfully'); + } else if (typeof showToast === 'function') { + showToast('success', 'Meeting cancelled successfully'); + } else { + alert('Meeting cancelled successfully'); + } + setTimeout(() => { + location.reload(); + }, 1500); + }) + .catch(error => { + console.error('Error cancelling meeting:', error); + if (typeof window.showMessageModal === 'function') { + window.showMessageModal('Error', 'Failed to cancel meeting. Please try again.'); + } else if (typeof showToast === 'function') { + showToast('error', 'Failed to cancel meeting. Please try again.'); + } else { + alert('Failed to cancel meeting. Please try again.'); + } + }); +} + +// Export functions +export { + getStatusBadgeClass, + openAddMeetingModal, + closeAddMeetingModal, + openEditMeetingModal, + closeEditMeetingModal, + openCancelMeetingModal, + closeCancelMeetingModal, + viewMeetingDetails, + closeViewMeetingModal, + submitAddMeetingForm, + submitEditMeetingForm, + submitCancelMeetingForm +}; \ No newline at end of file diff --git a/static/js/hod/modals/staff.js b/static/js/hod/modals/staff.js new file mode 100644 index 0000000..5f00028 --- /dev/null +++ b/static/js/hod/modals/staff.js @@ -0,0 +1,247 @@ +import { showToast, showMessageModal } from '../utils/notifications.js'; + +// Staff Modal Functions +function openAddStaffModal() { + document.getElementById('addStaffModal').style.display = 'block'; + document.body.style.overflow = 'hidden'; +} + +function closeAddStaffModal() { + document.getElementById('addStaffModal').style.display = 'none'; + document.getElementById('addStaffForm').reset(); + document.body.style.overflow = ''; +} + +function openViewStaffModal(staffId) { + fetch(`/app/staff/${staffId}/`) + .then(response => response.json()) + .then(data => { + if (data.success) { + const staff = data.staff; + const profilePicture = document.getElementById('staffProfilePicture'); + profilePicture.src = staff.image || "/static/img/user.png"; + + // Basic Information + document.getElementById('staffName').textContent = staff.name || '-'; + document.getElementById('staffDesignation').textContent = staff.designation || '-'; + document.getElementById('staffEmail').textContent = staff.email || '-'; + document.getElementById('staffPhone').textContent = staff.phone || '-'; + document.getElementById('staffGender').textContent = staff.gender || '-'; + document.getElementById('staffBirthDate').textContent = staff.birth_date || '-'; + document.getElementById('staffMaritalStatus').textContent = staff.marital_status || '-'; + + // Additional Information + document.getElementById('staffJoiningDate').textContent = staff.joining_date || '-'; + document.getElementById('staffStatus').textContent = staff.is_active ? 'Active' : 'Inactive'; + document.getElementById('staffParentName').textContent = staff.parent_name || '-'; + document.getElementById('staffParentPhone').textContent = staff.parent_phone || '-'; + document.getElementById('staffCitizenshipNo').textContent = staff.citizenship_no || '-'; + document.getElementById('staffPassport').textContent = staff.passport || '-'; + + // Address Information + document.getElementById('staffTemporaryAddress').textContent = staff.temporary_address || '-'; + document.getElementById('staffPermanentAddress').textContent = staff.permanent_address || '-'; + + // Assigned Subjects + const subjectsContainer = document.getElementById('staffSubjects'); + subjectsContainer.innerHTML = ''; + if (staff.subjects && staff.subjects.length > 0) { + staff.subjects.forEach(subject => { + const badge = document.createElement('span'); + badge.className = 'badge bg-primary'; + badge.textContent = subject.name; + subjectsContainer.appendChild(badge); + }); + } else { + subjectsContainer.innerHTML = 'No subjects assigned'; + } + + document.getElementById('viewStaffModal').style.display = 'block'; + document.body.style.overflow = 'hidden'; + } else { + showMessageModal('Error', data.message); + } + }) + .catch(error => { + showMessageModal('Error', 'Failed to fetch staff details.'); + }); +} + +function closeViewStaffModal() { + document.getElementById('viewStaffModal').style.display = 'none'; + document.body.style.overflow = ''; +} + +function openEditStaffModal(staffId) { + fetch(`/app/staff/${staffId}/`) + .then(response => response.json()) + .then(data => { + if (data.success) { + const staff = data.staff; + document.getElementById('editStaffId').value = staff.id; + document.getElementById('editStaffName').value = staff.name; + document.getElementById('editStaffEmail').value = staff.email; + document.getElementById('editStaffPhone').value = staff.phone; + document.getElementById('editStaffStatus').value = staff.is_active; + document.getElementById('editStaffProfilePicture').src = staff.image || "/static/img/user.png"; + + // Set selected subjects + const subjectsSelect = document.getElementById('editStaffSubjects'); + if (staff.subjects && staff.subjects.length > 0) { + Array.from(subjectsSelect.options).forEach(option => { + option.selected = staff.subjects.some(subject => subject.id === parseInt(option.value)); + }); + } + + document.getElementById('editStaffModal').style.display = 'block'; + document.body.style.overflow = 'hidden'; + } else { + showMessageModal('Error', data.message); + } + }) + .catch(error => { + showMessageModal('Error', 'Failed to fetch staff details.'); + }); +} + +function closeEditStaffModal() { + document.getElementById('editStaffModal').style.display = 'none'; + document.getElementById('editStaffForm').reset(); + document.body.style.overflow = ''; +} + +function submitAddStaffForm() { + const form = document.getElementById('addStaffForm'); + const formData = new FormData(form); + + fetch('/app/add-staff/', { + method: 'POST', + body: formData, + headers: { + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value + } + }) + .then(response => response.json()) + .then(data => { + closeAddStaffModal(); + showMessageModal('Success', data.message); + // Refresh the staff table + location.reload(); + }) + .catch(error => { + showMessageModal('Error', 'Failed to add staff member.'); + }); +} + +function submitEditStaffForm() { + const form = document.getElementById('editStaffForm'); + const formData = new FormData(form); + const staffId = document.getElementById('editStaffId').value; + + fetch(`/app/staff/${staffId}/edit/`, { + method: 'POST', + body: formData, + headers: { + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value + } + }) + .then(response => response.json()) + .then(data => { + closeEditStaffModal(); + showMessageModal('Success', data.message); + // Refresh the staff table + location.reload(); + }) + .catch(error => { + showMessageModal('Error', 'Failed to update staff member.'); + }); +} + +function confirmDeleteStaff(staffId) { + // Display the confirmation modal + const confirmationModal = document.getElementById('confirmationModal'); + const confirmDeleteBtn = document.getElementById('confirmDeleteBtn'); + + // Update confirmation message + const messageElement = confirmationModal.querySelector('div p'); + messageElement.textContent = 'Are you sure you want to delete this staff member? This action cannot be undone.'; + + // Show the modal + confirmationModal.style.display = 'block'; + document.body.style.overflow = 'hidden'; + + // Set up the delete button action + confirmDeleteBtn.onclick = function() { + // Get CSRF token + const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; + + // Send DELETE request + fetch(`/app/delete-staff/${staffId}/`, { + method: 'DELETE', + headers: { + 'X-CSRFToken': csrfToken, + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // Show success message + showToast('success', data.message); + // Reload the page after successful deletion + setTimeout(() => window.location.reload(), 1500); + } else { + // Show error message + showToast('error', data.message || 'Error deleting staff member'); + } + }) + .catch(error => { + console.error('Error:', error); + showToast('error', 'An error occurred while deleting the staff member'); + }) + .finally(() => { + // Close the modal + closeConfirmationModal(); + }); + }; +} + +// Helper function to close the confirmation modal +function closeConfirmationModal() { + document.getElementById('confirmationModal').style.display = 'none'; + document.body.style.overflow = ''; +} + +// Helper function to show toast messages +function showToast(type, message) { + // Assuming you have a toast notification system + // If not, you can implement one or use browser's alert + if (typeof Toastify === 'function') { + Toastify({ + text: message, + duration: 3000, + gravity: "top", + position: 'right', + backgroundColor: type === 'success' ? '#4caf50' : '#f44336' + }).showToast(); + } else { + alert(message); + } +} + +// Make functions available globally +window.closeConfirmationModal = closeConfirmationModal; +window.confirmDeleteStaff = confirmDeleteStaff; + +// Export functions +export { + openAddStaffModal, + closeAddStaffModal, + openViewStaffModal, + closeViewStaffModal, + openEditStaffModal, + closeEditStaffModal, + submitAddStaffForm, + submitEditStaffForm, + confirmDeleteStaff +}; \ No newline at end of file diff --git a/static/js/hod/modals/student.js b/static/js/hod/modals/student.js new file mode 100644 index 0000000..872b6eb --- /dev/null +++ b/static/js/hod/modals/student.js @@ -0,0 +1,141 @@ +import { showToast, showMessageModal } from '../utils/notifications.js'; + +// Student Modal Functions +function openAddStudentModal() { + document.getElementById('addStudentModal').style.display = 'block'; + document.body.style.overflow = 'hidden'; +} + +function closeAddStudentModal() { + document.getElementById('addStudentModal').style.display = 'none'; + document.body.style.overflow = ''; + document.getElementById('addStudentForm').reset(); +} + +function openEditStudentModal(studentId) { + loadStudentData(studentId); + document.getElementById('editStudentModal').style.display = 'block'; + document.body.style.overflow = 'hidden'; +} + +function closeEditStudentModal() { + document.getElementById('editStudentModal').style.display = 'none'; + document.body.style.overflow = ''; + document.getElementById('editStudentForm').reset(); +} + +function openViewStudentModal(studentId) { + loadStudentViewData(studentId); + document.getElementById('viewStudentModal').style.display = 'block'; + document.body.style.overflow = 'hidden'; +} + +function closeViewStudentModal() { + document.getElementById('viewStudentModal').style.display = 'none'; + document.body.style.overflow = ''; +} + +// Function to load student data for edit modal +function loadStudentData(studentId) { + fetch(`/app/get-student/${studentId}/`) + .then(response => response.json()) + .then(data => { + if (data.success) { + const student = data.student; + document.getElementById('edit_student_id').value = student.id; + document.getElementById('edit_name').value = student.name; + document.getElementById('edit_email').value = student.email; + document.getElementById('edit_phone').value = student.phone; + document.getElementById('edit_gender').value = student.gender; + document.getElementById('edit_birth_date').value = student.birth_date; + document.getElementById('edit_admission_date').value = student.admission_date; + document.getElementById('edit_temporary_address').value = student.temporary_address; + document.getElementById('edit_permanent_address').value = student.permanent_address; + document.getElementById('edit_parent_name').value = student.parent_name; + document.getElementById('edit_parent_phone').value = student.parent_phone; + document.getElementById('edit_citizenship_no').value = student.citizenship_no; + + if (student.image) { + document.getElementById('current_profile_picture').innerHTML = ` + Current Profile Picture + `; + } + } else { + showToast('error', data.message || 'Error loading student data'); + } + }) + .catch(error => { + console.error('Error:', error); + showToast('error', 'An error occurred while loading student data'); + }); +} + +// Function to load student data for view modal +function loadStudentViewData(studentId) { + fetch(`/app/get-student/${studentId}/`) + .then(response => response.json()) + .then(data => { + if (data.success) { + const student = data.student; + + // Set profile picture + const profilePic = document.getElementById('view_profile_picture'); + if (student.image) { + profilePic.src = student.image; + } else { + profilePic.src = '/static/img/user.png'; + } + + // Set basic information + document.getElementById('view_name').textContent = student.name; + document.getElementById('view_email').textContent = student.email; + document.getElementById('view_phone').textContent = student.phone; + document.getElementById('view_gender').textContent = student.gender === 'M' ? 'Male' : (student.gender === 'F' ? 'Female' : 'Other'); + + // Set personal information + document.getElementById('view_birth_date').textContent = student.birth_date; + document.getElementById('view_admission_date').textContent = student.admission_date; + document.getElementById('view_citizenship_no').textContent = student.citizenship_no; + + // Set address information + document.getElementById('view_temporary_address').textContent = student.temporary_address; + document.getElementById('view_permanent_address').textContent = student.permanent_address; + + // Set parent information + document.getElementById('view_parent_name').textContent = student.parent_name; + document.getElementById('view_parent_phone').textContent = student.parent_phone; + + // Set academic information + document.getElementById('view_course').textContent = student.course_name; + document.getElementById('view_current_semester').textContent = student.current_semester; + document.getElementById('view_status').textContent = student.is_active ? 'Active' : 'Inactive'; + } else { + showToast('error', data.message || 'Error loading student data'); + } + }) + .catch(error => { + console.error('Error:', error); + showToast('error', 'An error occurred while loading student data'); + }); +} + +// Function to confirm and handle student deletion +function confirmDeleteStudent(studentId) { + if (confirm("Are you sure you want to delete this student? This action cannot be undone.")) { + // If confirmed, redirect to the delete URL + window.location.href = `/app/api/hod/students/${studentId}/delete/`; + } +} + +// Export functions +export { + openAddStudentModal, + closeAddStudentModal, + openEditStudentModal, + closeEditStudentModal, + openViewStudentModal, + closeViewStudentModal, + loadStudentData, + loadStudentViewData, + confirmDeleteStudent +}; \ No newline at end of file diff --git a/static/js/hod/routine.js b/static/js/hod/routine.js new file mode 100644 index 0000000..6f0179a --- /dev/null +++ b/static/js/hod/routine.js @@ -0,0 +1,351 @@ +/** + * Routine management functionality for HOD dashboard + */ + +document.addEventListener('DOMContentLoaded', function() { + // Initialize period filter + initPeriodFilter(); + + // Initialize action buttons for existing routines + initRoutineActionButtons(); +}); + +/** + * Initialize period filter buttons + */ +function initPeriodFilter() { + const periodFilterButtons = document.querySelectorAll('#periodFilterButtons button'); + + periodFilterButtons.forEach(button => { + button.addEventListener('click', function() { + // Remove active class from all buttons + periodFilterButtons.forEach(btn => btn.classList.remove('active')); + // Add active class to clicked button + this.classList.add('active'); + + // Filter routines based on selected period + const selectedPeriod = this.dataset.period; + filterRoutineByPeriod(selectedPeriod); + }); + }); +} + +/** + * Initialize action buttons for routine rows + */ +function initRoutineActionButtons() { + // Edit buttons + document.querySelectorAll('.edit-routine-btn').forEach(btn => { + btn.addEventListener('click', function() { + const routineId = this.getAttribute('data-routine-id'); + openEditRoutineModal(routineId); + }); + }); + + // Delete buttons + document.querySelectorAll('.delete-routine-btn').forEach(btn => { + btn.addEventListener('click', function() { + const routineId = this.getAttribute('data-routine-id'); + openDeleteRoutineModal(routineId); + }); + }); +} + +/** + * Filter routine table based on selected period/year + */ +function filterRoutineByPeriod(selectedPeriod) { + const routineRows = document.querySelectorAll('#routineTableBody tr.routine-row'); + let visibleRows = 0; + + routineRows.forEach(row => { + if (selectedPeriod === 'all' || row.dataset.period === selectedPeriod) { + row.style.display = ''; + visibleRows++; + } else { + row.style.display = 'none'; + } + }); + + // Show or hide "no classes" message + const noClassesMessage = document.getElementById('noClassesMessage'); + if (noClassesMessage) { + noClassesMessage.style.display = visibleRows === 0 ? 'block' : 'none'; + } +} + +/** + * Open add routine modal + */ +function openAddRoutineModal() { + const modal = document.getElementById('addRoutineModal'); + if (modal) { + // Reset form + document.getElementById('addRoutineForm').reset(); + + // Set default period/year based on subject selection + const subjectSelect = document.getElementById('subjectId'); + if (subjectSelect) { + subjectSelect.addEventListener('change', function() { + const selectedOption = this.options[this.selectedIndex]; + const periodOrYear = selectedOption.textContent.match(/\((?:Semester|Year) (\d+)\)/); + if (periodOrYear && periodOrYear[1]) { + document.getElementById('periodOrYear').value = periodOrYear[1]; + } + }); + } + + // Show modal + modal.style.display = 'block'; + document.body.style.overflow = 'hidden'; + } +} + +/** + * Close add routine modal + */ +function closeAddRoutineModal() { + const modal = document.getElementById('addRoutineModal'); + if (modal) { + modal.style.display = 'none'; + document.body.style.overflow = ''; + } +} + +/** + * Submit add routine form + */ +function submitAddRoutineForm() { + const form = document.getElementById('addRoutineForm'); + if (!form) return; + + // Create form data + const formData = new FormData(form); + + // Add course_id from the page + const courseId = document.querySelector('#routineSection').getAttribute('data-course-id'); + if (courseId) { + formData.append('course_id', courseId); + } else { + // If no explicit course ID, use the current course ID from the URL + const urlPath = window.location.pathname; + const urlMatch = urlPath.match(/\/hod\/(\d+)/); + if (urlMatch && urlMatch[1]) { + formData.append('course_id', urlMatch[1]); + } + } + + // Validate form + const subjectId = formData.get('subject_id'); + const teacherId = formData.get('teacher_id'); + const startTime = formData.get('start_time'); + const endTime = formData.get('end_time'); + + if (!subjectId || !teacherId || !startTime || !endTime) { + showToast('error', 'Please fill in all required fields'); + return; + } + + // Send request to server + fetch('/app/api/hod/routines/', { + method: 'POST', + body: formData, + headers: { + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value + } + }) + .then(response => { + if (!response.ok) { + return response.json().then(data => { + throw new Error(data.message || 'Failed to add routine'); + }); + } + return response.json(); + }) + .then(data => { + showToast('success', 'Routine added successfully'); + closeAddRoutineModal(); + setTimeout(() => { + window.location.reload(); + }, 1000); + }) + .catch(error => { + console.error('Error adding routine:', error); + showToast('error', error.message || 'Failed to add routine'); + }); +} + +/** + * Open edit routine modal + */ +function openEditRoutineModal(routineId) { + const modal = document.getElementById('editRoutineModal'); + if (!modal || !routineId) return; + + // Fetch routine data + fetch(`/app/api/hod/routines/${routineId}/`) + .then(response => { + if (!response.ok) { + throw new Error('Failed to fetch routine data'); + } + return response.json(); + }) + .then(data => { + // Populate form fields + document.getElementById('editRoutineId').value = routineId; + document.getElementById('editSubjectId').value = data.subject_id; + document.getElementById('editTeacherId').value = data.teacher_id; + document.getElementById('editStartTime').value = data.start_time; + document.getElementById('editEndTime').value = data.end_time; + document.getElementById('editPeriodOrYear').value = data.period_or_year; + + // Show modal + modal.style.display = 'block'; + document.body.style.overflow = 'hidden'; + }) + .catch(error => { + console.error('Error fetching routine data:', error); + showToast('error', error.message || 'Failed to fetch routine data'); + }); +} + +/** + * Close edit routine modal + */ +function closeEditRoutineModal() { + const modal = document.getElementById('editRoutineModal'); + if (modal) { + modal.style.display = 'none'; + document.body.style.overflow = ''; + } +} + +/** + * Submit edit routine form + */ +function submitEditRoutineForm() { + const form = document.getElementById('editRoutineForm'); + if (!form) return; + + // Get routine ID + const routineId = document.getElementById('editRoutineId').value; + if (!routineId) { + showToast('error', 'Routine ID is missing'); + return; + } + + // Create form data + const formData = new FormData(form); + + // Validate form + const subjectId = formData.get('subject_id'); + const teacherId = formData.get('teacher_id'); + const startTime = formData.get('start_time'); + const endTime = formData.get('end_time'); + + if (!subjectId || !teacherId || !startTime || !endTime) { + showToast('error', 'Please fill in all required fields'); + return; + } + + // Send request to server + fetch(`/app/api/hod/routines/${routineId}/`, { + method: 'PUT', + body: formData, + headers: { + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value + } + }) + .then(response => { + if (!response.ok) { + return response.json().then(data => { + throw new Error(data.message || 'Failed to update routine'); + }); + } + return response.json(); + }) + .then(data => { + showToast('success', 'Routine updated successfully'); + closeEditRoutineModal(); + setTimeout(() => { + window.location.reload(); + }, 1000); + }) + .catch(error => { + console.error('Error updating routine:', error); + showToast('error', error.message || 'Failed to update routine'); + }); +} + +/** + * Open delete routine modal + */ +function openDeleteRoutineModal(routineId) { + const modal = document.getElementById('deleteRoutineModal'); + if (!modal || !routineId) return; + + // Set routine ID in the form + document.getElementById('deleteRoutineId').value = routineId; + + // Show modal + modal.style.display = 'block'; + document.body.style.overflow = 'hidden'; +} + +/** + * Close delete routine modal + */ +function closeDeleteRoutineModal() { + const modal = document.getElementById('deleteRoutineModal'); + if (modal) { + modal.style.display = 'none'; + document.body.style.overflow = ''; + } +} + +/** + * Submit delete routine form + */ +function submitDeleteRoutineForm() { + const routineId = document.getElementById('deleteRoutineId').value; + if (!routineId) { + showToast('error', 'Routine ID is missing'); + return; + } + + // Send request to server + fetch(`/app/api/hod/routines/${routineId}/`, { + method: 'DELETE', + headers: { + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value + } + }) + .then(response => { + if (!response.ok) { + throw new Error('Failed to delete routine'); + } + showToast('success', 'Routine deleted successfully'); + closeDeleteRoutineModal(); + setTimeout(() => { + window.location.reload(); + }, 1000); + }) + .catch(error => { + console.error('Error deleting routine:', error); + showToast('error', error.message || 'Failed to delete routine'); + }); +} + +// Export functions for use in other files +export { + filterRoutineByPeriod, + openAddRoutineModal, + closeAddRoutineModal, + submitAddRoutineForm, + openEditRoutineModal, + closeEditRoutineModal, + submitEditRoutineForm, + openDeleteRoutineModal, + closeDeleteRoutineModal, + submitDeleteRoutineForm +}; \ No newline at end of file diff --git a/static/js/hod/utils/navigation.js b/static/js/hod/utils/navigation.js new file mode 100644 index 0000000..df78234 --- /dev/null +++ b/static/js/hod/utils/navigation.js @@ -0,0 +1,24 @@ +// Function to show a specific section +function showSection(sectionId) { + // Hide all sections + document.querySelectorAll('.content-section').forEach(section => { + section.style.display = 'none'; + }); + + // Show selected section + const targetSection = document.getElementById(sectionId); + if (targetSection) { + targetSection.style.display = 'block'; + } + + // Update active nav item + document.querySelectorAll('.nav-item').forEach(item => { + item.classList.remove('active'); + if (item.getAttribute('data-section') === sectionId.replace('Section', '')) { + item.classList.add('active'); + } + }); +} + +// Export functions +export { showSection }; \ No newline at end of file diff --git a/static/js/hod/utils/notifications.js b/static/js/hod/utils/notifications.js new file mode 100644 index 0000000..37d944b --- /dev/null +++ b/static/js/hod/utils/notifications.js @@ -0,0 +1,42 @@ +// Toast notification function +export function showToast(type, message) { + const toast = document.createElement('div'); + toast.className = `toast align-items-center text-white bg-${type} border-0`; + toast.setAttribute('role', 'alert'); + toast.setAttribute('aria-live', 'assertive'); + toast.setAttribute('aria-atomic', 'true'); + + toast.innerHTML = ` +
+
+ ${message} +
+ +
+ `; + + const toastContainer = document.createElement('div'); + toastContainer.className = 'toast-container position-fixed bottom-0 end-0 p-3'; + toastContainer.appendChild(toast); + document.body.appendChild(toastContainer); + + const bsToast = new bootstrap.Toast(toast); + bsToast.show(); + + toast.addEventListener('hidden.bs.toast', function () { + toastContainer.remove(); + }); +} + +// Message modal functions +export function showMessageModal(title, message) { + document.getElementById('messageModalTitle').textContent = title; + document.getElementById('messageModalContent').textContent = message; + document.getElementById('messageModal').style.display = 'block'; + document.body.style.overflow = 'hidden'; +} + +export function closeMessageModal() { + document.getElementById('messageModal').style.display = 'none'; + document.body.style.overflow = ''; +} \ No newline at end of file diff --git a/static/js/navigation.js b/static/js/navigation.js index 5cf1620..7aa0dcc 100644 --- a/static/js/navigation.js +++ b/static/js/navigation.js @@ -85,4 +85,13 @@ document.addEventListener('DOMContentLoaded', function () { // Run initial load before rendering content handleInitialLoad(); + + // Expose the showSection function globally for backward compatibility with onclick handlers + window.showSection = function(sectionId) { + if (typeof sectionId === 'string') { + // Remove 'Section' suffix if it exists + const cleanSectionId = sectionId.replace('Section', ''); + switchToSection(cleanSectionId); + } + }; }); diff --git a/static/js/teacher-init.js b/static/js/teacher-init.js index 829c1ad..861b1e1 100644 --- a/static/js/teacher-init.js +++ b/static/js/teacher-init.js @@ -146,8 +146,17 @@ document.addEventListener('DOMContentLoaded', function() { method: 'POST', body: formData, credentials: 'same-origin', + headers: { + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value, + 'X-Requested-With': 'XMLHttpRequest' + } + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + return response.json(); }) - .then(response => response.json()) .then(data => { if (responseDiv) { responseDiv.style.display = 'block'; diff --git a/static/js/teacher-materials.js b/static/js/teacher-materials.js index 304af48..a5c6e27 100644 --- a/static/js/teacher-materials.js +++ b/static/js/teacher-materials.js @@ -1,785 +1,38 @@ -// Subject Materials Section Functions +// Subject Materials JavaScript -function showSection(sectionId, forceOrSubjectId = null) { - console.log('Showing section:', sectionId, 'force/subjectId:', forceOrSubjectId); - - // Special handling for subject materials section using the toggle function - if (sectionId === 'subjectMaterialsSection') { - const subjectId = forceOrSubjectId; - toggleSubjectView('materials'); - - // Get the section - const section = document.getElementById(sectionId); - if (section) { - // Make sure the header div is visible and properly styled - const headerDiv = section.querySelector('.d-flex.justify-content-between.align-items-center'); - if (headerDiv) { - // Ensure header is visible with !important - headerDiv.style.cssText = "display: flex !important; opacity: 1; visibility: visible;"; - - // Make the heading more prominent - const heading = headerDiv.querySelector('h3'); - if (heading) { - heading.style.fontWeight = 'bold'; - heading.style.color = '#3F51B5'; - } - } - - // Now load the content - loadSubjectMaterials(subjectId); - } - - return; - } - - // Special handling for subjects section using the toggle function - if (sectionId === 'subjectsSection') { - toggleSubjectView('subjects'); - return; - } - - // Handle the case where we're being called from the back button - const forceDisplay = forceOrSubjectId === true; - // If not a boolean, treat as subjectId - const subjectId = typeof forceOrSubjectId === 'boolean' ? null : forceOrSubjectId; - - // Special handling for forced navigation to subjects - if (sectionId === 'subjectsSection' && forceDisplay) { - console.log('FORCE NAVIGATION to subjects section'); - - // Hide all other sections first - document.querySelectorAll('.content-section').forEach(sect => { - if (sect.id !== 'subjectsSection') { - sect.style.display = 'none'; - sect.classList.remove('active'); - } - }); - - // Activate subjects section with high specificity - const subjectsSection = document.getElementById('subjectsSection'); - if (subjectsSection) { - // Force display using multiple methods - subjectsSection.style.cssText = 'display: block !important; opacity: 1; visibility: visible;'; - subjectsSection.classList.add('active'); - - // Force a redraw - void subjectsSection.offsetHeight; - - // Add active class to subjects nav - document.querySelectorAll('.nav-item').forEach(item => { - const section = item.getAttribute('data-section'); - if (section === 'subjects') { - item.classList.add('active'); - } else { - item.classList.remove('active'); - } - }); - - // Update URL hash - window.location.hash = 'subjectsSection'; - - // Scroll to top - window.scrollTo(0, 0); - - console.log('Force navigation complete.'); - return; - } - } - - // Special handling for subjects section to correctly set the active nav item - if (sectionId === 'subjectsSection') { - // Update URL hash to track the section - window.location.hash = 'subjectsSection'; - console.log('Setting hash to subjectsSection'); - } - - // Debug output - list all sections in DOM - const allSections = document.querySelectorAll('.content-section'); - console.log('Available sections:'); - allSections.forEach(section => { - console.log('- Section ID:', section.id, 'Display:', section.style.display, 'Classes:', section.className); - }); - - // Hide all sections - document.querySelectorAll('.content-section').forEach(section => { - section.classList.remove('active'); - section.style.display = 'none'; - console.log('Hiding section:', section.id); - }); - - // First remove active class from all nav items - document.querySelectorAll('.nav-item').forEach(item => { - item.classList.remove('active'); - }); - - // Then add active class to the correct nav item - document.querySelectorAll('.nav-item').forEach(item => { - const navSection = item.getAttribute('data-section'); - console.log('Nav item section:', navSection, 'comparing with:', sectionId); - - if (navSection === sectionId || - (sectionId === 'subjectMaterialsSection' && navSection === 'subjects') || - (sectionId === 'subjectsSection' && navSection === 'subjects')) { - item.classList.add('active'); - console.log('Activated nav item:', navSection); - } - }); - - // Show the selected section - const selectedSection = document.getElementById(sectionId); - if (selectedSection) { - console.log('Found section to show:', sectionId); - selectedSection.classList.add('active'); - selectedSection.style.display = 'block'; - - // Force browser to recognize the display change - setTimeout(() => { - selectedSection.style.opacity = '0.99'; - setTimeout(() => { - selectedSection.style.opacity = '1'; - }, 10); - }, 10); - } else { - console.error('Could not find section with ID:', sectionId); - } - - // Special handling for subjects section to ensure it's visible - if (sectionId === 'subjectsSection') { - const subjectsSection = document.getElementById('subjectsSection'); - if (subjectsSection) { - console.log('Forcing subjects section to display'); - subjectsSection.style.display = 'block'; - subjectsSection.classList.add('active'); - - // The following forces a reflow - void subjectsSection.offsetWidth; - } else { - console.error('Subjects section element not found in DOM'); - } - } - - // Scroll to top - window.scrollTo(0, 0); -} - -// Custom API function to get subject materials directly -function getSubjectMaterials(subjectId = null) { - return new Promise((resolve, reject) => { - // Get the CSRF token - const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value; - - // Build the URL for the API endpoint - let url = '/app/get-teacher-subjects/'; - - console.log('Fetching teacher subjects from:', url); - - // First fetch the subjects - fetch(url, { - headers: { - 'X-CSRFToken': csrftoken - } - }) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - return response.json(); - }) - .then(subjectsData => { - console.log('Teacher subjects data:', subjectsData); - - if (!subjectsData.success || !subjectsData.subjects || subjectsData.subjects.length === 0) { - console.log('No subjects found or empty response'); - resolve({ subjects: [] }); - return; - } - - let subjects = subjectsData.subjects; - - // If a specific subject is requested, filter the subjects - if (subjectId) { - console.log('Filtering subjects by ID:', subjectId); - subjects = subjects.filter(subject => subject.id.toString() === subjectId.toString()); - - if (subjects.length === 0) { - console.log('No matching subject found for ID:', subjectId); - resolve({ subjects: [] }); - return; - } - } - - // Now we need to get the files for each subject - // We'll use the manage_subject_files endpoint with a GET request - // and parse the HTML response to extract the files - - // Create a result object to store the subjects with their files - const result = { - subjects: [] - }; - - // Process each subject to get its files - const promises = subjects.map(subject => { - const filesUrl = `/app/manage-subject-files/?subject=${subject.id}`; - console.log(`Fetching files for subject ${subject.name} (ID: ${subject.id}) from: ${filesUrl}`); - - return fetch(filesUrl, { - headers: { - 'X-CSRFToken': csrftoken - } - }) - .then(response => { - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - return response.text(); - }) - .then(html => { - // Parse the HTML to extract the files - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); - - console.log(`Parsing HTML response for subject ${subject.name}`); - - // Try different ways to find the subject card - let subjectCard = null; - let cardBody = null; - - // Method 1: Find by exact heading text - const headings = Array.from(doc.querySelectorAll('.card-header h5, .card-title')); - subjectCard = headings.find(h5 => h5.textContent.trim() === subject.name); - - if (subjectCard) { - console.log(`Found subject card for ${subject.name} using heading match`); - cardBody = subjectCard.closest('.card')?.querySelector('.card-body'); - } else { - // Method 2: Find by card with subject ID in attributes - const cards = doc.querySelectorAll('.card[data-subject-id="' + subject.id + '"], .card[id*="subject-' + subject.id + '"]'); - if (cards.length > 0) { - console.log(`Found subject card for ${subject.name} using ID attribute`); - subjectCard = cards[0]; - cardBody = subjectCard.querySelector('.card-body'); - } else { - // Method 3: Just take the first card if there's only one subject requested - if (subjectId) { - const allCards = doc.querySelectorAll('.card'); - if (allCards.length > 0) { - console.log(`Using first card for ${subject.name} as fallback`); - subjectCard = allCards[0]; - cardBody = subjectCard.querySelector('.card-body'); - } - } - } - } - - const files = []; - let hasSyllabus = false; - - // If we found a card body with a table, extract the files - if (cardBody) { - // Look for table rows - const fileRows = cardBody.querySelectorAll('table tbody tr'); - console.log(`Found ${fileRows.length} file rows for subject ${subject.name}`); - - fileRows.forEach(row => { - const columns = row.querySelectorAll('td'); - if (columns.length >= 3) { // Need at least 3 columns - const title = columns[0].textContent.trim(); - const description = columns.length > 1 ? columns[1].textContent.trim() : ''; - const date = columns.length > 2 ? columns[2].textContent.trim() : ''; - - // Extract file ID from the delete button - const deleteBtn = row.querySelector('button[onclick*="deleteFile"], button[data-file-id]'); - let fileId = null; - - if (deleteBtn) { - // Try onclick attribute - const onClickAttr = deleteBtn.getAttribute('onclick'); - if (onClickAttr) { - const match = onClickAttr.match(/deleteFile\(['"]([^'"]*)['"]\)/); - if (match && match[1]) { - fileId = match[1]; - } - } - - // Try data attribute if onclick failed - if (!fileId) { - fileId = deleteBtn.getAttribute('data-file-id'); - } - } - - // Extract file URL from the download link - const actionCell = columns[columns.length - 1]; // Last column usually has actions - const downloadLink = actionCell.querySelector('a[href]:not([href="#"])'); - let fileUrl = null; - - if (downloadLink) { - fileUrl = downloadLink.getAttribute('href'); - } - - // Check if this is a syllabus file based on title or other indicators - const isSyllabus = title.toLowerCase().includes('syllabus'); - - // Set the syllabus flag for the subject if this file is a syllabus - if (isSyllabus) { - hasSyllabus = true; - } - - files.push({ - id: fileId, - title: title, - description: description, - uploaded_on: date, - file_url: fileUrl, - file_type: isSyllabus ? 'syllabus' : 'material' - }); - - console.log(`Added file: ${title} for subject ${subject.name}`); - } - }); - } else { - console.log(`Could not find card body for subject ${subject.name}`); - } - - // Check if the subject has a syllabus PDF using the View Syllabus button existence - const syllabusBtn = doc.querySelector(`a[href*="/subject/${subject.id}/syllabus/"], button[onclick*="viewSyllabus('${subject.id}')"]`); - if (syllabusBtn) { - console.log(`Found syllabus button for subject ${subject.name}`); - hasSyllabus = true; - } - - // Add this subject with its files to the result - result.subjects.push({ - ...subject, - materials: files, - has_syllabus: hasSyllabus - }); - - console.log(`Finished processing subject ${subject.name}, found ${files.length} files`); - }); - }); - - // Wait for all promises to resolve - Promise.all(promises) - .then(() => { - console.log('Final result:', result); - resolve(result); - }) - .catch(error => { - console.error('Error processing subjects:', error); - reject(error); - }); - }) - .catch(error => { - console.error('Error fetching subjects:', error); - reject(error); - }); - }); -} - -function loadSubjectMaterials(subjectId = null) { - const contentDiv = document.getElementById('subjectMaterialsContent'); - if (!contentDiv) { - console.error('Subject materials content container not found'); - return; - } - - // Show loading state in content area only - contentDiv.innerHTML = ` -
-
- Loading... -
-

Loading subject materials...

-
- `; - - console.log('==== LOADING SUBJECT MATERIALS ===='); - console.log('Subject ID:', subjectId); - - // Build the URL with optional subject filter - let url = '/app/manage-subject-files/'; - if (subjectId) { - url += `?subject=${subjectId}`; - } - - console.log('Fetching from URL:', url); - - // Create a direct link to the materials as a backup - const directLink = document.createElement('div'); - directLink.className = 'text-center mt-3'; - directLink.innerHTML = ` -

If materials don't load automatically, try:

- - Open Subject Materials in New Tab - - `; - - // Fetch the materials page directly - fetch(url, { - headers: { - 'Accept': 'text/html', - 'X-Requested-With': 'XMLHttpRequest' - } - }) - .then(response => { - console.log('Response status:', response.status); - console.log('Response type:', response.headers.get('content-type')); - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - return response.text(); - }) - .then(html => { - console.log('Received HTML content of length:', html.length); - - if (!html || html.trim().length === 0) { - console.error('Empty response received'); - contentDiv.innerHTML = ` -
- - Empty response received from server -
- `; - contentDiv.appendChild(directLink); - return; - } - - // Create a temporary div to hold the HTML content - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = html; - - console.log('HTML parsed, searching for content...'); - - // Clear the content div - contentDiv.innerHTML = ''; - - // Add back button at the top of the content for mobile - const mobileBackNav = document.createElement('div'); - mobileBackNav.className = 'd-block d-md-none text-center mb-3'; - mobileBackNav.innerHTML = ` - - `; - contentDiv.appendChild(mobileBackNav); - - // Look for subjects and display them, trying multiple selectors - let contentFound = false; - - // First approach: find the row of cards - const subjectsRow = tempDiv.querySelector('.content-section .row, .content-card .row, .row'); - if (subjectsRow) { - console.log('Found subjects row with', subjectsRow.children.length, 'children'); - contentDiv.innerHTML = ''; - const newRow = document.createElement('div'); - newRow.className = 'row'; - newRow.style.display = 'flex'; - newRow.style.flexWrap = 'wrap'; - newRow.style.margin = '0 -12px'; // Negative margin to offset the padding - - // Copy each card individually to avoid any parent context issues - Array.from(subjectsRow.querySelectorAll('.col-md-6, .col-lg-4, .col')).forEach(col => { - newRow.appendChild(col.cloneNode(true)); - }); - - if (newRow.children.length > 0) { - contentDiv.appendChild(newRow); - contentFound = true; - console.log('Added', newRow.children.length, 'subject cards to the content'); - } - } - - // Second approach: find individual cards if no row found - if (!contentFound) { - const cards = tempDiv.querySelectorAll('.card'); - if (cards && cards.length > 0) { - console.log('Found', cards.length, 'individual cards'); - contentDiv.innerHTML = ''; - const newRow = document.createElement('div'); - newRow.className = 'row'; - newRow.style.display = 'flex'; - newRow.style.flexWrap = 'wrap'; - newRow.style.margin = '0 -12px'; // Negative margin to offset the padding - - cards.forEach(card => { - const col = document.createElement('div'); - col.className = 'col-md-6 col-lg-4 mb-4'; - // Add explicit styling to ensure visibility - col.style.display = 'block'; - col.style.padding = '0 12px'; - col.style.marginBottom = '24px'; - col.style.width = '33.333%'; - col.style.minWidth = '300px'; - - // Clone the card and add explicit styles - const cardClone = card.cloneNode(true); - cardClone.style.display = 'flex'; - cardClone.style.flexDirection = 'column'; - cardClone.style.height = '100%'; - cardClone.style.border = '1px solid rgba(0,0,0,0.125)'; - cardClone.style.borderRadius = '0.25rem'; - cardClone.style.overflow = 'hidden'; - cardClone.style.backgroundColor = '#fff'; - - col.appendChild(cardClone); - newRow.appendChild(col); - }); - - // Clear and append with explicit styles - contentDiv.style.display = 'block'; - contentDiv.style.width = '100%'; - contentDiv.style.visibility = 'visible'; - contentDiv.appendChild(newRow); - contentFound = true; - console.log('Added cards to content as individual items with explicit styles'); - - // Force redraw - setTimeout(() => { - contentDiv.style.opacity = '0.99'; - setTimeout(() => { - contentDiv.style.opacity = '1'; - }, 50); - }, 50); - } - } - - // Third approach: try to find any content card - if (!contentFound) { - const contentCard = tempDiv.querySelector('.content-card'); - if (contentCard) { - console.log('Found content card, using its content'); - // Remove header if present to avoid duplicate headers - const header = contentCard.querySelector('.d-flex.justify-content-between.align-items-center'); - if (header) { - console.log('Removing header from content card'); - header.remove(); - } - - contentDiv.innerHTML = contentCard.innerHTML; - contentFound = true; - } - } - - // Fourth approach: direct render of subjects - if (!contentFound) { - console.log('No content found in HTML, trying to manually render subjects'); - // Directly call the API to get subjects - fetch('/app/get-teacher-subjects/') - .then(response => response.json()) - .then(data => { - if (data.success && data.subjects && data.subjects.length > 0) { - console.log('Got', data.subjects.length, 'subjects from API'); - - // Filter by subject ID if provided - let subjects = data.subjects; - if (subjectId) { - subjects = subjects.filter(s => s.id == subjectId); - } - - if (subjects.length === 0) { - contentDiv.innerHTML = ` -
- - No subjects found matching your criteria. -
- `; - contentDiv.appendChild(directLink); - return; - } - - // Manually render subjects - contentDiv.innerHTML = ''; - const row = document.createElement('div'); - row.className = 'row'; - row.style.display = 'flex'; - row.style.flexWrap = 'wrap'; - row.style.margin = '0 -12px'; - - subjects.forEach(subject => { - const col = document.createElement('div'); - col.className = 'col-md-6 col-lg-4 mb-4'; - col.style.display = 'block'; - col.style.padding = '0 12px'; - col.style.marginBottom = '24px'; - col.style.width = '33.333%'; - col.style.minWidth = '300px'; - - const card = document.createElement('div'); - card.className = 'card border-0 shadow-sm h-100'; - card.style.display = 'flex'; - card.style.flexDirection = 'column'; - card.style.height = '100%'; - card.style.border = '1px solid rgba(0,0,0,0.125)'; - card.style.borderRadius = '0.25rem'; - card.style.overflow = 'hidden'; - card.style.backgroundColor = '#fff'; - - const cardHeader = document.createElement('div'); - cardHeader.className = 'card-header bg-light py-3'; - cardHeader.style.backgroundColor = '#f8f9fa'; - cardHeader.style.padding = '1rem'; - - const title = document.createElement('h5'); - title.className = 'card-title mb-0'; - title.textContent = subject.name; - - const subtitle = document.createElement('p'); - subtitle.className = 'text-muted mb-0 small'; - subtitle.textContent = `${subject.course__name || ''} - ${subject.semester_or_year || 'N/A'}`; - - cardHeader.appendChild(title); - cardHeader.appendChild(subtitle); - - const cardBody = document.createElement('div'); - cardBody.className = 'card-body'; - cardBody.style.padding = '1rem'; - cardBody.style.flexGrow = '1'; - - const fileStatus = document.createElement('p'); - fileStatus.className = 'text-muted small mb-3'; - fileStatus.innerHTML = ' Loading files...'; - - const btnContainer = document.createElement('div'); - btnContainer.className = 'mb-3'; - - const uploadBtn = document.createElement('button'); - uploadBtn.className = 'btn btn-primary w-100'; - uploadBtn.innerHTML = ` Upload to ${subject.name}`; - uploadBtn.onclick = function() { openUploadModal(subject.id); }; - - btnContainer.appendChild(uploadBtn); - cardBody.appendChild(fileStatus); - cardBody.appendChild(btnContainer); - - card.appendChild(cardHeader); - card.appendChild(cardBody); - col.appendChild(card); - row.appendChild(col); - }); - - contentDiv.appendChild(row); - contentFound = true; - } else { - console.log('No subjects returned from API'); - contentDiv.innerHTML = ` -
- - No subjects assigned to you. Please contact administrator. -
- `; - contentDiv.appendChild(directLink); - } - }) - .catch(error => { - console.error('Error fetching subjects:', error); - contentDiv.innerHTML = ` -
- - Error fetching subjects: ${error.message} -
- `; - contentDiv.appendChild(directLink); - }); - } - - // If all else fails, show a simple message - if (!contentFound) { - console.log('All content extraction methods failed'); - contentDiv.innerHTML = ` -
- - Unable to display subject materials automatically. -
- `; - contentDiv.appendChild(directLink); - } - - // Re-attach event handlers for the buttons after a short delay - setTimeout(() => { - console.log('Attaching event handlers to buttons'); - // Add event listeners for upload buttons - contentDiv.querySelectorAll('[onclick*="openUploadModal"]').forEach(button => { - const match = button.getAttribute('onclick')?.match(/openUploadModal\(['"]([^'"]*)['"]\)/); - if (match) { - const id = match[1]; - console.log('Found upload button for subject ID:', id); - button.setAttribute('onclick', `openUploadModal('${id}')`); - } - }); - - // Add event listeners for delete buttons - contentDiv.querySelectorAll('[onclick*="deleteFile"]').forEach(button => { - const match = button.getAttribute('onclick')?.match(/deleteFile\(['"]([^'"]*)['"]\)/); - if (match) { - const id = match[1]; - console.log('Found delete button for file ID:', id); - button.setAttribute('onclick', `deleteFile('${id}')`); - } - }); - - // Add upload button if none exists - if (!contentDiv.querySelector('[onclick*="openUploadModal"]')) { - console.log('No upload buttons found, adding one'); - const uploadBtn = document.createElement('div'); - uploadBtn.className = 'text-center mt-4'; - uploadBtn.innerHTML = ` - - `; - contentDiv.appendChild(uploadBtn); - } - }, 200); - }) - .catch(error => { - console.error('Error loading subject materials:', error); - contentDiv.innerHTML = ` -
- - Error loading subject materials: ${error.message} -
-
- - - Open in New Tab - -
- `; - }); -} +// Global variable for file deletion +window.fileIdToDelete = null; +// Modal Functions function openUploadModal(subjectId = '') { - console.log('Opening upload modal with subject ID:', subjectId); const modal = document.getElementById('uploadFileModal'); const subjectSelect = document.getElementById('subject_id'); const responseDiv = document.getElementById('uploadResponse'); - if (!modal || !subjectSelect) { - console.error('Modal or subject select not found'); - return; - } - // Reset the form const uploadForm = document.getElementById('uploadFileForm'); if (uploadForm) { uploadForm.reset(); - } + // Hide response div if (responseDiv) { responseDiv.style.display = 'none'; } + } - // If a subject ID was provided, pre-select it + // Pre-select subject if provided if (subjectId && subjectSelect) { subjectSelect.value = subjectId; } + // Show modal + if (modal) { modal.style.display = 'block'; document.body.style.overflow = 'hidden'; + } } +// Close upload modal function closeUploadModal() { const modal = document.getElementById('uploadFileModal'); if (modal) { @@ -788,8 +41,9 @@ function closeUploadModal() { } } +// Delete file function deleteFile(fileId) { - fileIdToDelete = fileId; + window.fileIdToDelete = fileId; const modal = document.getElementById('confirmationModal'); if (modal) { modal.style.display = 'block'; @@ -797,145 +51,18 @@ function deleteFile(fileId) { } } +// Close confirmation modal function closeConfirmationModal() { const modal = document.getElementById('confirmationModal'); if (modal) { modal.style.display = 'none'; document.body.style.overflow = ''; } - fileIdToDelete = null; + window.fileIdToDelete = null; } -// Function to view subject syllabus -function viewSyllabus(subjectId) { - if (!subjectId) return; - - // Get the button element that was clicked - const syllabusButton = document.querySelector(`.syllabus-button[data-subject-id="${subjectId}"]`); - - // Check if button was hidden - if so, there's no syllabus available - if (syllabusButton && syllabusButton.style.display === 'none') { - alert('No syllabus available for this subject.'); - return; - } - - // Show loading indicator - const loadingModal = document.createElement('div'); - loadingModal.style.position = 'fixed'; - loadingModal.style.top = '0'; - loadingModal.style.left = '0'; - loadingModal.style.width = '100%'; - loadingModal.style.height = '100%'; - loadingModal.style.backgroundColor = 'rgba(0,0,0,0.5)'; - loadingModal.style.display = 'flex'; - loadingModal.style.justifyContent = 'center'; - loadingModal.style.alignItems = 'center'; - loadingModal.style.zIndex = '10000'; - - loadingModal.innerHTML = ` -
-
- Loading... -
-

Loading syllabus...

-
- `; - - document.body.appendChild(loadingModal); - - // Get the CSRF token - const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value; - - // Fetch the syllabus URL - fetch(`/app/subject/${subjectId}/syllabus/`, { - headers: { - 'X-CSRFToken': csrftoken - } - }) - .then(response => { - if (!response.ok) { - // If response is not OK (e.g., 404), throw an error with status - if (response.status === 404) { - throw new Error('Syllabus not found'); - } - throw new Error(`HTTP error! Status: ${response.status}`); - } - return response.json(); - }) - .then(data => { - // Remove loading indicator - document.body.removeChild(loadingModal); - - if (data.success) { - // Open the syllabus in a new tab - window.open(data.file_url, '_blank'); - } else { - // Show error message - alert(data.message || 'No syllabus available for this subject.'); - } - }) - .catch(error => { - console.error('Error fetching syllabus:', error); - document.body.removeChild(loadingModal); - - // Show a more user-friendly message - if (error.message === 'Syllabus not found') { - alert('No syllabus available for this subject.'); - - // Hide the button to prevent future attempts - if (syllabusButton) { - syllabusButton.style.display = 'none'; - } - } else { - alert('Error fetching syllabus. Please try again.'); - } - }); -} - -// Function to toggle between subjects list and subject materials -function toggleSubjectView(view) { - const subjectsSection = document.getElementById('subjectsSection'); - const materialsSection = document.getElementById('subjectMaterialsSection'); - - console.log('Toggling subject view to:', view); - - if (view === 'subjects') { - // Show subjects section - subjectsSection.style.display = 'block'; - subjectsSection.classList.add('active'); - - // Hide materials section - materialsSection.style.display = 'none'; - materialsSection.classList.remove('active'); - - // Activate the correct nav item - document.querySelectorAll('.nav-item').forEach(item => { - item.classList.remove('active'); - const navSection = item.getAttribute('data-section'); - if (navSection === 'subjects') { - item.classList.add('active'); - } - }); - - console.log('Switched to subjects view'); - } else if (view === 'materials') { - // Show materials section - materialsSection.style.display = 'block'; - materialsSection.classList.add('active'); - - // Hide subjects section - subjectsSection.style.display = 'none'; - subjectsSection.classList.remove('active'); - - // Keep the subjects nav item active - document.querySelectorAll('.nav-item').forEach(item => { - item.classList.remove('active'); - const navSection = item.getAttribute('data-section'); - if (navSection === 'subjects') { - item.classList.add('active'); - } - }); - - console.log('Switched to materials view'); - } -} \ No newline at end of file +// Make modal functions globally accessible +window.openUploadModal = openUploadModal; +window.closeUploadModal = closeUploadModal; +window.deleteFile = deleteFile; +window.closeConfirmationModal = closeConfirmationModal; \ No newline at end of file diff --git a/student_management_system/settings.py b/student_management_system/settings.py index f121cad..f8cc3c8 100644 --- a/student_management_system/settings.py +++ b/student_management_system/settings.py @@ -25,12 +25,12 @@ BASE_DIR = Path(__file__).resolve().parent.parent # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') +SECRET_KEY = os.getenv("DJANGO_SECRET_KEY") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = os.getenv('DJANGO_DEBUG', 'False') == 'True' +DEBUG = os.getenv("DJANGO_DEBUG", "False") == "True" -ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', '*').split(',') +ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "*").split(",") # Application definition @@ -56,13 +56,21 @@ MIDDLEWARE = [ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "app.middleware.HTTP505Middleware", # Custom middleware for handling 505 errors ] AUTHENTICATION_BACKENDS = [ "app.backends.MultiModelBackend", "django.contrib.auth.backends.ModelBackend", ] -REST_FRAMEWORK = {"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.AllowAny"]} + +REST_FRAMEWORK = { + "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.AllowAny"], + "DEFAULT_RENDERER_CLASSES": [ + "rest_framework.renderers.JSONRenderer", + ], +} + ROOT_URLCONF = "student_management_system.urls" TEMPLATES = [ @@ -156,27 +164,40 @@ JAZZMIN_SETTINGS = JAZZMIN_SETTINGS JAZZMIN_UI_TWEAKS = JAZZMIN_UI_TWEAKS # Login redirect URL -LOGIN_REDIRECT_URL = '/app/dashboard/' -LOGIN_URL = '/login/' +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') +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'), + "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') +SMS_API_KEY = os.getenv("SMS_API_KEY", "") +SMS_SENDER_ID = os.getenv("SMS_SENDER_ID", "SMSSYS") +# CSRF settings +# CSRF_TRUSTED_ORIGINS = [ +# "https://*.ngrok-free.app", +# "https://*.ngrok.io" +# ] -CSRF_TRUSTED_ORIGINS = [ - "https://*.ngrok-free.app", - "https://*.ngrok.io" -] +# Firebase settings +FIREBASE_API_KEY = os.getenv("FIREBASE_API_KEY", "") +FIREBASE_AUTH_DOMAIN = os.getenv("FIREBASE_AUTH_DOMAIN", "") +FIREBASE_PROJECT_ID = os.getenv("FIREBASE_PROJECT_ID", "") +FIREBASE_STORAGE_BUCKET = os.getenv("FIREBASE_STORAGE_BUCKET", "") +FIREBASE_MESSAGING_SENDER_ID = os.getenv("FIREBASE_MESSAGING_SENDER_ID", "") +FIREBASE_APP_ID = os.getenv("FIREBASE_APP_ID", "") +FIREBASE_MEASUREMENT_ID = os.getenv("FIREBASE_MEASUREMENT_ID", "") diff --git a/student_management_system/urls.py b/student_management_system/urls.py index e40550d..8bd8c10 100644 --- a/student_management_system/urls.py +++ b/student_management_system/urls.py @@ -27,24 +27,29 @@ from app import auth # Main URL patterns for the project urlpatterns = [ + # Application URLs ------------------------------------------------ path("app/", include("app.urls")), path("admin/", custom_admin_site.urls), - - # Root URL redirects to dashboard + # Root Redirect -------------------------------------------------- path("", RedirectView.as_view(url="/app/dashboard/", permanent=False)), - - # Authentication URLs - 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 URLs - path('password-reset/', auth.reset_password_options, name='password_reset'), - - # Firebase service worker URL - path('firebase-messaging-sw.js', views.serve_firebase_sw), + # Authentication ------------------------------------------------- + path("login/", auth.custom_login, name="login"), + path("logout/", auth_views.LogoutView.as_view(next_page="/login/"), name="logout"), + # Password Management -------------------------------------------- + path("password-reset/", auth.reset_password_options, name="password_reset"), + # Firebase Service ----------------------------------------------- + path("firebase-messaging-sw.js", views.serve_firebase_sw), ] -# Add static and media file serving in development +# Static and Media Files (Development Only) -------------------------- if settings.DEBUG: urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) - urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + +# Error Handlers ---------------------------------------------------- +handler400 = "app.error_handlers.handler400" +handler403 = "app.error_handlers.handler403" +handler404 = "app.error_handlers.handler404" +handler500 = "app.error_handlers.handler500" +handler505 = "app.error_handlers.handler505" + diff --git a/student_management_system/wsgi.py b/student_management_system/wsgi.py index 6cb88b1..02cc269 100644 --- a/student_management_system/wsgi.py +++ b/student_management_system/wsgi.py @@ -11,6 +11,6 @@ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'student_management_system.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "student_management_system.settings") application = get_wsgi_application() diff --git a/templates/400.html b/templates/400.html new file mode 100644 index 0000000..43351a1 --- /dev/null +++ b/templates/400.html @@ -0,0 +1,23 @@ +{% extends "shared/base.html" %} + +{% block title %}400 Bad Request{% endblock %} + +{% block content %} +
+
+
+
+
+

400

+

Bad Request

+

Sorry, your browser sent a request that our server could not understand.

+
+

The request could not be processed due to malformed syntax or invalid parameters.

+
+ Go to Home +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/403.html b/templates/403.html new file mode 100644 index 0000000..0506b9a --- /dev/null +++ b/templates/403.html @@ -0,0 +1,23 @@ +{% extends "shared/base.html" %} + +{% block title %}403 Forbidden{% endblock %} + +{% block content %} +
+
+
+
+
+

403

+

Access Forbidden

+

Sorry, you don't have permission to access this page.

+
+

Please check your credentials or contact the administrator if you believe this is an error.

+
+ Go to Home +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/404.html b/templates/404.html new file mode 100644 index 0000000..bf1aa8c --- /dev/null +++ b/templates/404.html @@ -0,0 +1,23 @@ +{% extends "shared/base.html" %} + +{% block title %}404 Not Found{% endblock %} + +{% block content %} +
+
+
+
+
+

404

+

Page Not Found

+

Sorry, the page you're looking for doesn't exist.

+
+

The page may have been moved, deleted, or never existed. Please check the URL and try again.

+
+ Go to Home +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/500.html b/templates/500.html new file mode 100644 index 0000000..cc85c31 --- /dev/null +++ b/templates/500.html @@ -0,0 +1,23 @@ +{% extends "shared/base.html" %} + +{% block title %}500 Server Error{% endblock %} + +{% block content %} +
+
+
+
+
+

500

+

Internal Server Error

+

Sorry, something went wrong on our end.

+
+

We're working to fix the issue. Please try again later or contact support if the problem persists.

+
+ Go to Home +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/505.html b/templates/505.html new file mode 100644 index 0000000..7def811 --- /dev/null +++ b/templates/505.html @@ -0,0 +1,23 @@ +{% extends "shared/base.html" %} + +{% block title %}505 HTTP Version Not Supported{% endblock %} + +{% block content %} +
+
+
+
+
+

505

+

HTTP Version Not Supported

+

Sorry, the HTTP version used in the request is not supported by this server.

+
+

The server does not support the HTTP protocol version used in the request. Please try accessing the site using a different browser or updating your current one.

+
+ Go to Home +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/admin/app/routine/change_form.html b/templates/admin/app/routine/change_form.html index dc240da..f2fab86 100644 --- a/templates/admin/app/routine/change_form.html +++ b/templates/admin/app/routine/change_form.html @@ -186,8 +186,8 @@
- -
@@ -209,7 +209,7 @@ - Please select course and semester + Please select course and period @@ -230,7 +230,7 @@ + + + + + + + + + + + + + + + +{% endblock %} +{% block extrastyle %} +{{ block.super }} + +{% endblock %} + +{% block branding %} +

+ + {{ site_header }} + {{ site_header }} + +

+{% endblock %} + +{% block content %} +
+ +
+
+
+
+
+

Welcome, Admin!

+

Student Management Dashboard

+
+ + Logout + +
+
+
+
+ + +
+ +
+
+
+
Students
+
+
+
+
+ Total Students +

{{ total_students }}

+
+
+ Active +

{{ active_students }}

+
+
+
+
+ Male Students + {{ male_students }} +
+
+ Female Students + {{ female_students }} +
+
+ Inactive + {{ inactive_students }} +
+
+
+
+
+ + +
+
+
+
Staff
+
+
+
+
+ Total Staff +

{{ total_staff }}

+
+
+ Active +

{{ active_staff }}

+
+
+
+
+ Teachers + {{ total_teachers }} +
+
+ Non-Teaching + {{ non_teaching_staff }} +
+
+ Inactive + {{ inactive_staff }} +
+
+
+
+
+ + +
+
+
+
Parents
+
+
+
+
+ Total Parents +

{{ total_parents }}

+
+
+ Active +

{{ active_parents }}

+
+
+
+
+ Parents with Students + {{ parents_with_students }} +
+
+ Parents with Multiple Students + {{ parents_with_multiple_students }} +
+
+ Inactive + {{ inactive_parents }} +
+
+
+
+
+ + +
+
+
+
Courses
+
+
+
+
+ Total Courses +

{{ total_courses }}

+
+
+ Active +

{{ active_courses }}

+
+
+
+
+ Total Subjects + {{ total_subjects }} +
+
+ Total Batches + {{ total_batches }} +
+
+ Inactive + {{ inactive_courses }} +
+
+
+
+
+
+ + +
+
+
+
+ Top Rated Teachers +
+
+ + + + + + + + + + + {% for teacher in teacher_ratings %} + + + + + + + {% empty %} + + + + {% endfor %} + +
TeacherStudent RatingParent RatingTotal Feedbacks
+
+ {% if teacher.image %} + {{ teacher.name }} + {% else %} +
+ +
+ {% endif %} +
+
{{ teacher.name }}
+ {{ teacher.designation }} +
+
+
+
+ {{ teacher.avg_student_rating|floatformat:1 }} +
+ {% for i in "12345"|make_list %} + {% if forloop.counter <= teacher.avg_student_rating %} + + {% else %} + + {% endif %} + {% endfor %} +
+
+
+
+ {{ teacher.avg_parent_rating|floatformat:1 }} +
+ {% for i in "12345"|make_list %} + {% if forloop.counter <= teacher.avg_parent_rating %} + + {% else %} + + {% endif %} + {% endfor %} +
+
+
+ {{ teacher.total_feedbacks }} +
+ No teacher ratings available +
+
+
+
+ + +
+ +
+
+
+
+ Recent Notices +
+ +
+
+ {% for notice in recent_notices %} +
+
+ {% if notice.image %} + {{ notice.title }} + {% else %} +
+ +
+ {% endif %} +
+
+ {{ notice.title }} + {{ notice.message|truncatechars:50 }} + {{ notice.created_at|timesince }} ago +
+
+ {% empty %} +
+ No recent notices +
+ {% endfor %} +
+
+
+
+ + +
+ +
+
+
+ Staff Leave Requests +
+
+ {% for leave in staff_leaves %} +
+
+
+ +
+
+
+ {{ leave.staff.name }} + From: {{ leave.start_date|date:"M d, Y" }} + To: {{ leave.end_date|date:"M d, Y" }} + {{ leave.message|truncatechars:50 }} +
+
+ {% csrf_token %} + +
+
+ {% csrf_token %} + +
+
+
+
+ {% empty %} +
+ No pending staff leave requests +
+ {% endfor %} +
+
+
+ + +
+
+
+ Student Leave Requests +
+
+ {% for leave in student_leaves %} +
+
+
+ +
+
+
+ {{ leave.student.name }} + From: {{ leave.start_date|date:"M d, Y" }} + To: {{ leave.end_date|date:"M d, Y" }} + {{ leave.message|truncatechars:50 }} +
+
+ {% csrf_token %} + +
+
+ {% csrf_token %} + +
+
+
+
+ {% empty %} +
+ No pending student leave requests +
+ {% endfor %} +
+
+
+
+ + +
+
+
+
+ Recent Institute Feedback +
+
+ {% for feedback in recent_feedback %} +
+
+ {% if not feedback.is_anonymous %} + {% if feedback.student %} + {% if feedback.student.image %} + {{ feedback.student.name }} + {% else %} +
+ +
+ {% endif %} + {% elif feedback.staff %} + {% if feedback.staff.image %} + {{ feedback.staff.name }} + {% else %} +
+ +
+ {% endif %} + {% elif feedback.parent %} + {% if feedback.parent.image %} + {{ feedback.parent.name }} + {% else %} +
+ +
+ {% endif %} + {% endif %} + {% else %} +
+ +
+ {% endif %} +
+
+ {% if not feedback.is_anonymous %} + {% if feedback.student %} + {{ feedback.student.name }} (Student) + + {{ feedback.student.course.name }} - + {% if feedback.student.course.duration_type == 'Semester' %} + Semester {{ feedback.student.current_period }} + {% else %} + Year {{ feedback.student.current_period }} + {% endif %} + + {% elif feedback.staff %} + {{ feedback.staff.name }} ({{ feedback.staff.designation }}) + {% elif feedback.parent %} + {{ feedback.parent.name }} (Parent) + {% with student=feedback.parent.students.first %} + {% if student %} + + Parent of {{ student.name }} - + {{ student.course.name }} + {% if student.course.duration_type == 'Semester' %} + (Semester {{ student.current_period }}) + {% else %} + (Year {{ student.current_period }}) + {% endif %} + + {% endif %} + {% endwith %} + {% endif %} + {% else %} + Anonymous + {% endif %} +
+ {{ feedback.get_feedback_type_display }} +
+ {% for i in "12345"|make_list %} + + {% endfor %} +
+
+ {% if feedback.feedback_text %} + {{ feedback.feedback_text|truncatechars:100 }} + {% endif %} + {{ feedback.created_at|timesince }} ago +
+
+ {% empty %} +
+ No recent institute feedback +
+ {% endfor %} +
+
+
+
+
+ + +
+
+
+
+
+ Parent-Teacher Meetings +
+ +
+
+ + + + + + + + + + + + + {% for meeting in meetings %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
DateTimeDurationTypeStatusActions
{{ meeting.meeting_date|date:"M d, Y" }}{{ meeting.meeting_time|time:"h:i A" }}{{ meeting.duration }} minutes + {% if meeting.is_online %} + Online + {% if meeting.meeting_link %} + + + {% endif %} + {% else %} + In-person + {% endif %} + + + {{ meeting.get_status_display }} + + + {% if meeting.status == 'scheduled' %} + + + + {% endif %} + {% if meeting.notes or meeting.status == 'completed' %} + + {% endif %} + {% if meeting.agenda %} + + {% endif %} + {% if meeting.status == 'cancelled' and meeting.cancellation_reason %} + + {% endif %} +
+ No meetings scheduled +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+ + + +{% endblock %} + diff --git a/templates/admission_officer/dashboard.html b/templates/admission_officer/dashboard.html new file mode 100644 index 0000000..6993181 --- /dev/null +++ b/templates/admission_officer/dashboard.html @@ -0,0 +1,985 @@ +{% extends 'shared/base.html' %} +{% load static %} +{% load tz %} +{% load app_filters %} + +{% block extrahead %} + +{% endblock %} + +{% block content %} + +{% csrf_token %} + +
+ + {% include 'shared/welcome_section.html' with user=officer %} + + +
+ +
+
+
+
+
Students
+
+
+
+

{{ total_students }}

+

Total Students

+
+
+
+
+

{{ active_students }}

+

Active

+
+
+

{{ new_students }}

+

New (30 days)

+
+
+
+
+
+
+ +
+
+
+
Courses
+
+
+
+

{{ total_courses }}

+

Total Courses

+
+
+
+
+

{{ active_courses }}

+

Active

+
+
+
+
+
+
+ +
+
+
+
Batches
+
+
+

{{ total_batches }}

+
+
+

{{ active_batches }}

+

Active

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

Pending

+ {{ pending_leaves }} +
+
+

Approved

+ {{ approved_leaves }} +
+
+

Rejected

+ {{ rejected_leaves }} +
+ +
+
+
+
+ + +
+ +
+ + +
+
+
+
Recent Notices
+ {% if notices %} +
+ {% for notice in notices %} +
+
+
{{ notice.title }}
+ {{ notice.created_at|timesince }} ago +
+

{{ notice.message }}

+
+ {% endfor %} +
+ {% else %} +
+ No recent notices. +
+ {% endif %} +
+
+ + +
+
+
Leave Requests
+ {% if recent_leaves %} +
+ + + + + + + + + + + {% for leave in recent_leaves %} + + + + + + + {% endfor %} + +
Start DateEnd DateMessageStatus
{{ leave.start_date|date:"M d, Y" }}{{ leave.end_date|date:"M d, Y" }}{{ leave.message|truncatechars:50 }} + + {{ leave.get_status_display }} + +
+
+ {% else %} +
+ No leave requests found. +
+ {% endif %} +
+
+
+
+ + +
+
+
+
Student Management
+ +
+ + +
+
+
+ + +
+
+
+ +
+
+ + +
+ + + + + + + + + + + + + + {% for student in students|dictsort:"id" %} + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
IDStudent DetailsContact InfoParent DetailsCourse & BatchStatusActions
{{ student.id }} +
+ {% if student.image %} + {{ student.name }} + {% endif %} +
+ {{ student.name }} + {% if student.gender %} +
{{ student.gender }} + {% endif %} +
+
+
+
+ {{ student.phone }}
+ {% if student.email %} + {{ student.email }}
+ {% endif %} +
+
+
+ {% if student.parent_name %} + {{ student.parent_name }}
+ {% endif %} + {% if student.parent_phone %} + {{ student.parent_phone }} + {% endif %} +
+
+
+ {% if student.course %} + {{ student.course.name }} + {% if student.course.code %}({{ student.course.code }}){% endif %} + {% endif %} +
+ {% for batch in student.batches.all %} + {{ batch.name }} + {% if not forloop.last %}, {% endif %} + {% empty %} + No batch assigned + {% endfor %} +
+
+ + {{ student.status|default:"Not Set" }} + + +
+ + +
+
No students found
+
+
+
+ + +
+
+
+
Batch Management
+ +
+ + +
+ + + + + + + + + + + {% for batch in batches %} + + + + + + + {% empty %} + + + + {% endfor %} + +
NameYearStatusStudents
{{ batch.name }}{{ batch.year|date:"Y"|default:"-" }} + + {{ batch.is_active|yesno:"Active,Inactive" }} + + {{ batch.students.count }}
No batches found
+
+
+
+ + +
+
+
+
Course List
+
+ + +
+
+ + + + + + + + + + + + {% for course in courses %} + + + + + + + + {% endfor %} + +
NameCodeDurationTypeStatus
{{ course.name }}{{ course.code|default:"-" }} + {% if course.duration_type == "Semester" %} + {{ course.duration }} years ({{ course.duration|multiply:2 }} semesters) + {% else %} + {{ course.duration }} years + {% endif %} + {{ course.duration_type }} + + {{ course.is_active|yesno:"Active,Inactive" }} + +
+
+
+
+
+ + + + + + + + +
+
+

+ Profile Information +

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

{{ officer.name }}

+

+ {{ officer.id }} + {{ officer.email }} + {{ officer.phone }} +

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

{{ officer.name }}

+
+
+ Gender +

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

+
+
+ Birth Date +

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

+
+
+ Marital Status +

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

+
+
+ Citizenship No. +

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

+
+
+ Passport +

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

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

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

+
+
+ Phone Number +

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

+
+
+ Parent Name +

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

+
+
+ Parent Phone +

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

+
+
+ Temporary Address +

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

+
+
+ Permanent Address +

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

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

{{ officer.id }}

+
+
+ Designation +

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

+
+
+ Joining Date +

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

+
+
+ Status +

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

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

+ 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 %} + +
Start DateEnd DateReasonStatusApplied On
{{ leave.start_date|date:"M d, Y" }}{{ leave.end_date|date:"M d, Y" }}{{ leave.message }} + + {{ leave.get_status_display }} + + {{ leave.created_at|date:"M d, Y" }}
No leave history available
+
+
+
+
+ + +
+
+
+
Notices
+
+ + +
+ + + + + + + + + + {% for notice in notices %} + + + + + + {% empty %} + + + + {% endfor %} + +
TitleMessageCreated At
{{ notice.title }}{{ notice.message|truncatechars:100 }}{{ notice.created_at|date:"M d, Y H:i" }}
No notices found
+
+
+
+
+ + +{% include 'shared/common_modals.html' with user=officer %} + + +{% include 'admission_officer/modals.html' %} + + +{% include 'shared/bottom_nav.html' with user=officer %} + +{% endblock content %} + +{% block extrastyle %} +{{ block.super }} + + + + + + +{% endblock %} + +{% block extrascripts %} +{{ block.super }} + +{% endblock %} \ No newline at end of file diff --git a/templates/admission_officer/modals.html b/templates/admission_officer/modals.html new file mode 100644 index 0000000..2b9bd47 --- /dev/null +++ b/templates/admission_officer/modals.html @@ -0,0 +1,1076 @@ +{% load static %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/templates/components/bottom_nav.html b/templates/components/bottom_nav.html deleted file mode 100644 index 54a7597..0000000 --- a/templates/components/bottom_nav.html +++ /dev/null @@ -1,134 +0,0 @@ - - - - - diff --git a/templates/components/student_modals.html b/templates/components/student_modals.html deleted file mode 100644 index 2d9cafd..0000000 --- a/templates/components/student_modals.html +++ /dev/null @@ -1,32 +0,0 @@ -{% load static %} - - - - - - - - - \ No newline at end of file diff --git a/templates/components/welcome_section.html b/templates/components/welcome_section.html deleted file mode 100644 index 04c9957..0000000 --- a/templates/components/welcome_section.html +++ /dev/null @@ -1,32 +0,0 @@ -{% load static %} - - -
-
-
-
-
- {% if user.image %} -
- {{ user.name }} -
- {% else %} -
- Default User -
- {% endif %} -
-

Welcome, {{ user.name }}!

-

- {% if user.designation %} - {{ user.designation }} ID: {{ user.id }} - {% else %} - Student ID: {{ user.id }} - {% endif %} -

-
-
-
-
-
-
diff --git a/templates/dashboard.html b/templates/dashboard.html deleted file mode 100644 index 8400e96..0000000 --- a/templates/dashboard.html +++ /dev/null @@ -1,1048 +0,0 @@ -{% extends "admin/base_site.html" %} -{% load static %} -{% load app_filters %} -{% block extrahead %} -{{ block.super }} - -{% endblock %} -{% block extrastyle %} -{{ block.super }} - - - - - -{% endblock %} -{% block content %} -
- -
-
-
-
-
-

Welcome, Admin!

-

Student Management Dashboard

-
- - - - Logout - -
-
-
-
- - -
- -
-
-
-
Students
-
-
-
-
- Total Students -

{{ total_students }}

-
-
-
-
- Active - {{ active_students }} -
-
- Completed - {{ completed_students }} -
-
- On Leave - {{ on_leave_students }} -
-
-
-
-
- - -
-
-
-
Courses
-
-
-
-
- Total Courses -

{{ total_courses }}

-
-
-
-
- Active - {{ active_courses }} -
-
- Completed - {{ completed_courses }} -
-
-
-
-
- - -
-
-
-
Staff
-
-
-
-
- Total Staff -

{{ total_staff }}

-
-
-
-
- Active - {{ active_staff }} -
-
- On Leave - {{ staff_on_leave }} -
-
-
-
-
- - -
-
-
-
Attendance
-
-
-
-
- Classes Today -

{{ total_classes_today }}

-
-
- Attendance Rate -

{{ attendance_rate }}%

-
-
-
-
0%
-
100%
-
-
-
-
-
- Students Present - {{ students_present }}/{{ total_students_today }} -
-
-
-
-
- - - - - -
- -
-
-
- 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|truncatechars:50 }} - {{ 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 %} - -
NameTypeStart DateEnd DateReasonActions
{{ leave.student.name }} - Student - {{ leave.start_date|date:"M d, Y" }}{{ leave.end_date|date:"M d, Y" }}{{ leave.message }} -
- {% csrf_token %} - -
-
- {% csrf_token %} - -
-
{{ leave.staff.name }} - Staff - {{ leave.start_date|date:"M d, Y" }}{{ leave.end_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 -
- -
-
-
-
- -{% endblock %} -{% block extrajs %} -{{ block.super }} - - -{% endblock %} diff --git a/templates/hod/dashboard.html b/templates/hod/dashboard.html new file mode 100644 index 0000000..12e3a91 --- /dev/null +++ b/templates/hod/dashboard.html @@ -0,0 +1,3457 @@ +{% extends 'shared/base.html' %} +{% load static %} +{% load tz %} +{% load app_filters %} + +{% block title %}HOD Dashboard - {{ course.name }}{% endblock title %} + +{% block head %} +{{ block.super }} + + + + + + + + + + + + +{% endblock head %} + +{% block content %} + +{% csrf_token %} + +{% if messages %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +{% endif %} + + +{% include 'shared/common_modals.html' %} + + + +
+ + {% include 'shared/welcome_section.html' %} + + +
+ +
+
+
+
+
Students
+
+
+
+

{{ total_students }}

+

Total Students

+
+
+
+
+

{{ total_staff }}

+

Staff

+
+
+

{{ total_subjects }}

+

Subjects

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

{{ today_classes|length }}

+

Total Classes

+
+
+ {% if today_classes %} +
+
+ {% for class in today_classes|slice:":3" %} + + {{ class.subject.name|truncatechars:15 }} + + {% endfor %} + {% if today_classes|length > 3 %} + + +{{ today_classes|length|add:"-3" }} more + + {% endif %} +
+
+ {% else %} +

No classes scheduled for today

+ {% endif %} +
+
+
+
+ +
+
+
+
Student Progress
+
+
+ {% if student_progress %} +
+ {% for progress in student_progress|slice:":3" %} +
+ {{ progress.student.name }} +
+
+
+
+ {{ progress.completion_percentage }}% +
+ {% endfor %} +
+ {% else %} +

No progress data available

+ {% endif %} +
+
+
+ +
+
+
+
Upcoming Meetings
+
+
+ {% if upcoming_meetings %} + {% for meeting in upcoming_meetings|slice:":3" %} +
+
+ {{ meeting.meeting_date|date:"M d, Y" }} + {{ meeting.meeting_time|time:"h:i A" }} +
+ {{ meeting.get_status_display }} +
+ {% endfor %} + {% if upcoming_meetings|length > 3 %} +
+ +{{ upcoming_meetings|length|add:"-3" }} more meetings +
+ {% endif %} + {% else %} +

No upcoming meetings

+ {% endif %} +
+
+
+
+ + +
+
+
+
+
Today's Classes
+
+ + {% if today_classes %} +
+ + + + + + + + + + + + + {% for class in today_classes %} + + + + + + + + + {% endfor %} + +
SubjectCodeTeacherTimePeriod/YearStatus
{{ class.subject.name }}{{ class.subject.code|default:"-" }}{{ class.teacher.name }}{{ class.start_time|time:"h:i A" }} - {{ class.end_time|time:"h:i A" }}{{ class.period_or_year }} + {% if class.attendance %} + + {% if class.attendance.class_status %}Conducted{% else %}Cancelled{% endif %} + + {% else %} + Pending + {% endif %} +
+
+ {% else %} +
+ No classes scheduled for today. +
+ {% endif %} +
+
+
+ + +
+
+
+
Student Progress
+ {% if student_progress_all %} +
+ + + + + + + + + + + + + {% for progress in student_progress_all %} + + + + + + + + + {% endfor %} + +
StudentBatchCurrent PeriodCompletion %StatusActions
+
+ {% if progress.student.image %} + + {% else %} +
{{ progress.student.name|slice:":1" }}
+ {% endif %} + {{ progress.student.name }} +
+
+ {% for batch in progress.student.batches.all %} + {{ batch.name }}{% if not forloop.last %}, {% endif %} + {% endfor %} + {{ progress.student.current_period }} +
+
+
+
+ {{ progress.completion_percentage }}% +
+ {% if progress.progress_status == "Completed" %} + Completed + {% elif progress.progress_status == "In Progress" %} + In Progress + {% elif progress.progress_status == "Not Started" %} + Not Started + {% elif progress.progress_status == "Dropped" %} + Dropped + {% endif %} + + +
+
+ {% else %} +
+ No progress data found for the selected batch. +
+ {% endif %} +
+
+ +
+
+
Recent Feedback
+ {% if recent_feedback %} + + {% else %} +
+ No recent feedback available. +
+ {% endif %} +
+
+
+
+ + + + + + + + + + + + +
+ + +{% include 'hod/modals/add_subject_modal.html' %} + + +{% include 'hod/modals/edit_subject_modal.html' %} + + +{% include 'hod/modals/view_subject_modal.html' %} + + +{% include 'hod/modals/add_staff_modal.html' %} + + +{% include 'hod/modals/view_staff_modal.html' %} + + +{% include 'hod/modals/edit_staff_modal.html' %} + + +{% include 'hod/modals/add_student_modal.html' %} + + +{% include 'hod/modals/edit_student_modal.html' %} + + +{% include 'hod/modals/view_student_modal.html' %} + + +{% include 'hod/modals/add_meeting_modal.html' %} + + +{% include 'hod/modals/edit_meeting_modal.html' %} + + +{% include 'hod/modals/view_meeting_modal.html' %} + + +{% include 'hod/modals/cancel_meeting_modal.html' %} + + +{% include 'hod/modals/meeting_notes_modal.html' %} + + +{% include 'hod/modals/meeting_agenda_modal.html' %} + + +{% include 'hod/modals/cancellation_reason_modal.html' %} + + +{% include 'hod/modals/add_routine_modal.html' %} +{% include 'hod/modals/edit_routine_modal.html' %} + + +{% include 'shared/bottom_nav.html' with user=hod %} + + +{% include 'hod/modals/notice_modal.html' %} +{% include 'hod/modals/edit_progress_modal.html' %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{% endblock content %} + +{% block extrastyle %} +{{ block.super }} + + + + + + + +{% endblock extrastyle %} + + + + + + diff --git a/templates/hod/modals/add_meeting_modal.html b/templates/hod/modals/add_meeting_modal.html new file mode 100644 index 0000000..aa5948f --- /dev/null +++ b/templates/hod/modals/add_meeting_modal.html @@ -0,0 +1,73 @@ +{% load static %} + + + + + \ No newline at end of file diff --git a/templates/hod/modals/add_routine_modal.html b/templates/hod/modals/add_routine_modal.html new file mode 100644 index 0000000..de7ed8f --- /dev/null +++ b/templates/hod/modals/add_routine_modal.html @@ -0,0 +1,364 @@ +{% load static %} + + + + + \ No newline at end of file diff --git a/templates/hod/modals/add_staff_modal.html b/templates/hod/modals/add_staff_modal.html new file mode 100644 index 0000000..2df536d --- /dev/null +++ b/templates/hod/modals/add_staff_modal.html @@ -0,0 +1,126 @@ +{% load static %} + + + + \ No newline at end of file diff --git a/templates/hod/modals/add_student_modal.html b/templates/hod/modals/add_student_modal.html new file mode 100644 index 0000000..c909aaf --- /dev/null +++ b/templates/hod/modals/add_student_modal.html @@ -0,0 +1,145 @@ +{% load static %} + + + + + \ No newline at end of file diff --git a/templates/hod/modals/add_subject_modal.html b/templates/hod/modals/add_subject_modal.html new file mode 100644 index 0000000..99f17b2 --- /dev/null +++ b/templates/hod/modals/add_subject_modal.html @@ -0,0 +1,106 @@ +{% load static %} +{% load app_filters %} + + + + + \ No newline at end of file diff --git a/templates/hod/modals/cancel_meeting_modal.html b/templates/hod/modals/cancel_meeting_modal.html new file mode 100644 index 0000000..bb20057 --- /dev/null +++ b/templates/hod/modals/cancel_meeting_modal.html @@ -0,0 +1,23 @@ + + \ No newline at end of file diff --git a/templates/hod/modals/cancellation_reason_modal.html b/templates/hod/modals/cancellation_reason_modal.html new file mode 100644 index 0000000..4371719 --- /dev/null +++ b/templates/hod/modals/cancellation_reason_modal.html @@ -0,0 +1,43 @@ + + + + \ No newline at end of file diff --git a/templates/hod/modals/edit_meeting_modal.html b/templates/hod/modals/edit_meeting_modal.html new file mode 100644 index 0000000..7965993 --- /dev/null +++ b/templates/hod/modals/edit_meeting_modal.html @@ -0,0 +1,79 @@ +{% load static %} + + + + + \ No newline at end of file diff --git a/templates/hod/modals/edit_progress_modal.html b/templates/hod/modals/edit_progress_modal.html new file mode 100644 index 0000000..138e78b --- /dev/null +++ b/templates/hod/modals/edit_progress_modal.html @@ -0,0 +1,211 @@ +{% load static %} + + + + + \ No newline at end of file diff --git a/templates/hod/modals/edit_routine_modal.html b/templates/hod/modals/edit_routine_modal.html new file mode 100644 index 0000000..3d26bcb --- /dev/null +++ b/templates/hod/modals/edit_routine_modal.html @@ -0,0 +1,267 @@ +{% load static %} + + + + + \ No newline at end of file diff --git a/templates/hod/modals/edit_staff_modal.html b/templates/hod/modals/edit_staff_modal.html new file mode 100644 index 0000000..bd0d25d --- /dev/null +++ b/templates/hod/modals/edit_staff_modal.html @@ -0,0 +1,202 @@ +{% load static %} + + + + \ No newline at end of file diff --git a/templates/hod/modals/edit_student_modal.html b/templates/hod/modals/edit_student_modal.html new file mode 100644 index 0000000..f459d76 --- /dev/null +++ b/templates/hod/modals/edit_student_modal.html @@ -0,0 +1,206 @@ +{% load static %} + + + + + \ No newline at end of file diff --git a/templates/hod/modals/edit_subject_modal.html b/templates/hod/modals/edit_subject_modal.html new file mode 100644 index 0000000..305c51a --- /dev/null +++ b/templates/hod/modals/edit_subject_modal.html @@ -0,0 +1,128 @@ +{% load static %} +{% load app_filters %} + + + + + \ No newline at end of file diff --git a/templates/hod/modals/meeting_agenda_modal.html b/templates/hod/modals/meeting_agenda_modal.html new file mode 100644 index 0000000..5d6cca8 --- /dev/null +++ b/templates/hod/modals/meeting_agenda_modal.html @@ -0,0 +1,43 @@ + + + + \ No newline at end of file diff --git a/templates/hod/modals/meeting_notes_modal.html b/templates/hod/modals/meeting_notes_modal.html new file mode 100644 index 0000000..80aacc4 --- /dev/null +++ b/templates/hod/modals/meeting_notes_modal.html @@ -0,0 +1,43 @@ + + + + \ No newline at end of file diff --git a/templates/hod/modals/notice_modal.html b/templates/hod/modals/notice_modal.html new file mode 100644 index 0000000..ed3bfb2 --- /dev/null +++ b/templates/hod/modals/notice_modal.html @@ -0,0 +1,167 @@ +{% load static %} + + + + + + + + + + + \ No newline at end of file diff --git a/templates/hod/modals/routine_modal.html b/templates/hod/modals/routine_modal.html new file mode 100644 index 0000000..29acf0a --- /dev/null +++ b/templates/hod/modals/routine_modal.html @@ -0,0 +1,125 @@ + + + + + + + + \ No newline at end of file diff --git a/templates/hod/modals/view_meeting_modal.html b/templates/hod/modals/view_meeting_modal.html new file mode 100644 index 0000000..89aea0d --- /dev/null +++ b/templates/hod/modals/view_meeting_modal.html @@ -0,0 +1,74 @@ +{% load static %} + + + + + \ No newline at end of file diff --git a/templates/hod/modals/view_staff_modal.html b/templates/hod/modals/view_staff_modal.html new file mode 100644 index 0000000..d1c8a12 --- /dev/null +++ b/templates/hod/modals/view_staff_modal.html @@ -0,0 +1,165 @@ +{% load static %} + + + + \ No newline at end of file diff --git a/templates/hod/modals/view_student_modal.html b/templates/hod/modals/view_student_modal.html new file mode 100644 index 0000000..cf94974 --- /dev/null +++ b/templates/hod/modals/view_student_modal.html @@ -0,0 +1,168 @@ +{% load static %} + + + + + \ No newline at end of file diff --git a/templates/hod/modals/view_subject_modal.html b/templates/hod/modals/view_subject_modal.html new file mode 100644 index 0000000..121dffa --- /dev/null +++ b/templates/hod/modals/view_subject_modal.html @@ -0,0 +1,83 @@ + + + + \ No newline at end of file diff --git a/templates/login/email_reset.html b/templates/login/email_reset.html index 71a32ac..01647cc 100644 --- a/templates/login/email_reset.html +++ b/templates/login/email_reset.html @@ -1,9 +1,13 @@ -{% extends "base.html" %} +{% extends "shared/base.html" %} {% load static %} {% block content %}
+ {% if institute and institute.logo %} + + {% else %} + {% endif %}

Reset Password

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

diff --git a/templates/login/login.html b/templates/login/login.html index 830d2b6..c443b58 100644 --- a/templates/login/login.html +++ b/templates/login/login.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "shared/base.html" %} {% load static %} {% block content %} @@ -8,6 +8,7 @@ Login - Student Management System + + +{% endblock %} + +{% block extrascripts %} +{{ block.super }} + +{% endblock %} diff --git a/templates/parent/institute_feedback.html b/templates/parent/institute_feedback.html new file mode 100644 index 0000000..77a9456 --- /dev/null +++ b/templates/parent/institute_feedback.html @@ -0,0 +1,83 @@ +{% extends "base.html" %} +{% load static %} + +{% block title %}Institute Feedback{% endblock %} + +{% block content %} +
+
+
+
+
+

Provide Institute Feedback

+
+
+
+ {% csrf_token %} + +
+ + {{ form.feedback_type }} + {% if form.feedback_type.errors %} +
+ {{ form.feedback_type.errors }} +
+ {% endif %} +
+ +
+ + {{ form.rating }} + {% if form.rating.errors %} +
+ {{ form.rating.errors }} +
+ {% endif %} +
+ +
+ + {{ form.feedback_text }} + {% if form.feedback_text.errors %} +
+ {{ form.feedback_text.errors }} +
+ {% endif %} +
+ +
+ {{ form.is_anonymous }} + +
+ +
+ {{ form.is_public }} + +
+ +
+ + Cancel +
+
+
+
+
+
+
+{% endblock %} + +{% block extra_css %} + +{% endblock %} \ No newline at end of file diff --git a/templates/parent/modals/cancellation_reason_modal.html b/templates/parent/modals/cancellation_reason_modal.html new file mode 100644 index 0000000..e07bc81 --- /dev/null +++ b/templates/parent/modals/cancellation_reason_modal.html @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/templates/parent/modals/feedback_modal.html b/templates/parent/modals/feedback_modal.html new file mode 100644 index 0000000..11c54df --- /dev/null +++ b/templates/parent/modals/feedback_modal.html @@ -0,0 +1,245 @@ + + + + + + + + \ No newline at end of file diff --git a/templates/parent/modals/meeting_agenda_modal.html b/templates/parent/modals/meeting_agenda_modal.html new file mode 100644 index 0000000..1417cb6 --- /dev/null +++ b/templates/parent/modals/meeting_agenda_modal.html @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/templates/parent/modals/meeting_notes_modal.html b/templates/parent/modals/meeting_notes_modal.html new file mode 100644 index 0000000..52a635e --- /dev/null +++ b/templates/parent/modals/meeting_notes_modal.html @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/templates/parent/modals/student_details_modal.html b/templates/parent/modals/student_details_modal.html new file mode 100644 index 0000000..458fea5 --- /dev/null +++ b/templates/parent/modals/student_details_modal.html @@ -0,0 +1,132 @@ + + \ No newline at end of file diff --git a/templates/parent/parent_feedback.html b/templates/parent/parent_feedback.html new file mode 100644 index 0000000..42dd8e7 --- /dev/null +++ b/templates/parent/parent_feedback.html @@ -0,0 +1,132 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+
+
+
+

+ Submit Feedback +

+ + Back to Dashboard + +
+ + +
+
+
+
+ + + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/shared/base.html similarity index 75% rename from templates/base.html rename to templates/shared/base.html index 64e8508..a4d227f 100644 --- a/templates/base.html +++ b/templates/shared/base.html @@ -16,7 +16,7 @@ } - + @@ -34,8 +34,10 @@ + + - + @@ -310,6 +273,9 @@ + + + @@ -327,47 +293,92 @@ - {% block extrajs %}{% endblock %} + {% block extra_js %}{% endblock %} \ No newline at end of file diff --git a/templates/shared/bottom_nav.html b/templates/shared/bottom_nav.html new file mode 100644 index 0000000..63279f3 --- /dev/null +++ b/templates/shared/bottom_nav.html @@ -0,0 +1,256 @@ + + + + + + + + diff --git a/templates/components/common_modals.html b/templates/shared/common_modals.html similarity index 75% rename from templates/components/common_modals.html rename to templates/shared/common_modals.html index 4bd8ed3..7a7ddda 100644 --- a/templates/components/common_modals.html +++ b/templates/shared/common_modals.html @@ -96,19 +96,47 @@