diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b52d6c4 --- /dev/null +++ b/.env.example @@ -0,0 +1,24 @@ +# Django Settings +DJANGO_SECRET_KEY=your-secret-key-here +DJANGO_DEBUG=True +DJANGO_ALLOWED_HOSTS=* + +# Firebase Configuration +FIREBASE_API_KEY=your_firebase_api_key_here +FIREBASE_AUTH_DOMAIN=your_project_id.firebaseapp.com +FIREBASE_PROJECT_ID=your_project_id +FIREBASE_STORAGE_BUCKET=your_project_id.firebasestorage.app +FIREBASE_MESSAGING_SENDER_ID=your_sender_id +FIREBASE_APP_ID=your_app_id +FIREBASE_MEASUREMENT_ID=your_measurement_id + +# Email Configuration +EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend +DEFAULT_FROM_EMAIL=noreply@studentmanagementsystem.com + +# SMS Configuration +SMS_API_KEY=your_sms_api_key_here +SMS_SENDER_ID=SMSSYS + +# Cache Configuration +CACHE_LOCATION=unique-snowflake \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7ec4427..b819e7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -60,6 +61,9 @@ coverage.xml local_settings.py db.sqlite3 db.sqlite3-journal +*/migrations/*.py +*/migrations/* +!*/migrations/__init__.py # Flask stuff: instance/ @@ -137,16 +141,12 @@ 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 *.DS_Store + +# Firebase configuration +firebase-config.json +firebase.json +static/js/firebase-config.js diff --git a/README.md b/README.md index 5c8d57f..f4c230f 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,515 @@ -

- Logo -

+# 📚 Student Management System (SMS) -

Student Management System In Django

+
-

+[![Python](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/) +[![Django](https://img.shields.io/badge/django-5.1.7-green.svg)](https://www.djangoproject.com/) +[![License](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE) +[![Stars](https://img.shields.io/github/stars/S4NKALP/Student-Management-System-In-Django)](https://github.com/S4NKALP/Student-Management-System-In-Django/stargazers) +[![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)]() - -People +A comprehensive Student Management System built with Django, featuring multi-role access, real-time notifications, and secure authentication. - -Stars +

- -Forks +## 📑 Table of Contents - -Watches +- [💻 Tech Stack](#-tech-stack) +- [✨ Key Features](#-key-features) +- [🎯 Project Architecture](#-project-architecture) +- [🚀 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) - -Last Updated -

+## 💻 Tech Stack -
+
+🔥 Core Technologies -# TODO +| Technology | Version | Purpose | Documentation | +|------------|---------|---------|---------------| +| Python | 3.8+ | Backend Development | [Python Docs](https://docs.python.org/) | +| Django | 5.1.7 | Web Framework | [Django Docs](https://docs.djangoproject.com/) | +| Django REST Framework | 3.15.2 | 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) | -- [ ] Better UI -- [ ] Add more features -- [ ] Add more models that are given below +
-### models +
+⚡ Frontend Technologies -- [ ] Library Model -- [ ] Notification Model -- [ ] Calendar -- [ ] Parent Model -- [ ] Study Material -- [ ] Routine -- [ ] Complaint -- [ ] Feedback -- [ ] Student Performance Report -- [ ] Syllabus -- [ ] Online Exam +| Technology | Purpose | Documentation | +|------------|---------|---------------| +| Bootstrap | UI Framework | [Bootstrap Docs](https://getbootstrap.com/docs/) | +| jQuery | DOM Manipulation | [jQuery Docs](https://api.jquery.com/) | +| Firebase Web SDK | Real-time Updates | [Firebase Web Docs](https://firebase.google.com/docs/web) | + +
+ +
+🛠️ Development Tools + +| Tool | Purpose | Documentation | +|------|---------|---------------| +| Git | Version Control | [Git Docs](https://git-scm.com/doc) | +| WhiteNoise | Static File Serving | [WhiteNoise Docs](https://whitenoise.readthedocs.io/) | +| PyOTP | OTP Generation | [PyOTP Docs](https://github.com/pyauth/pyotp) | + +
+ +### 🖥️ 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/) | + +## ✨ Key Features + +### 👥 User Management +- **Multi-role System**: Admin, Staff, and Student interfaces +- **Secure Authentication**: OTP-based verification system +- **Role-based Access Control**: Granular permissions for each role +- **User Profiles**: Comprehensive user information management + +### 📚 Academic Management +- **Course Management**: Create and manage courses and subjects +- **Class Scheduling**: Timetable generation and management +- **Grade Management**: Track and calculate student grades +- **Exam Management**: Schedule and conduct examinations + +### 📊 Attendance System +- **Real-time Tracking**: Live attendance monitoring +- **Automated Reports**: Generate attendance reports +- **Notification System**: Alert for low attendance +- **Bulk Operations**: Manage multiple students at once + +### 📝 Leave Management +- **Online Applications**: Digital leave request system +- **Approval Workflow**: Multi-level approval process +- **Leave History**: Track all leave records +- **Automated Notifications**: Status updates via email/SMS + +### 🔐 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**: Works on all devices +- **Push Notifications**: Real-time updates via Firebase +- **Offline Support**: Basic functionality without internet +- **Mobile-first Approach**: Optimized for mobile devices + +## 🎯 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 +``` + +## 🚀 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 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 +``` + +#### 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 +``` + +#### 3️⃣ Install Dependencies +```bash +# Install Python dependencies +pip install -r requirements.txt +``` + +#### 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. Create a Firebase project +2. Download the service account key +3. Place it as `firebase-key.json` in the project root +4. Configure Firebase Web SDK in `static/js/firebase-config.js` + +#### 6️⃣ Run Migrations +```bash +# Apply database migrations +python manage.py migrate +``` + +#### 7️⃣ Create Superuser +```bash +# Create a superuser account +python manage.py createsuperuser +``` + +#### 8️⃣ Run the Development Server +```bash +# Start the development server +python manage.py runserver + +# The application will be available at: +# http://localhost:8000 +``` + +### 📱 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 +ALLOWED_HOSTS=localhost,127.0.0.1 + +# Database +DB_NAME=sms_db +DB_USER=your_db_user +DB_PASSWORD=your_db_password +DB_HOST=localhost +DB_PORT=3306 + +# Email (for OTP) +EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend +EMAIL_HOST=smtp.gmail.com +EMAIL_PORT=587 +EMAIL_USE_TLS=True +EMAIL_HOST_USER=your-email@gmail.com +EMAIL_HOST_PASSWORD=your-app-password + +# Firebase +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 + +# Google Cloud +GOOGLE_CLOUD_PROJECT=your-google-cloud-project +GOOGLE_CLOUD_KEY_FILE=path/to/your/google-cloud-key.json +``` + +## 📦 Dependencies + +### Backend +- Django 5.1.7 +- Django REST Framework +- Firebase Admin +- Google Cloud Storage +- PyOTP +- WhiteNoise + +### Frontend +- Bootstrap +- jQuery +- Firebase Web SDK + +## 🔐 Security Features + +### Authentication +- OTP-based authentication +- Session management +- Password hashing + +### 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`, `duration`, `duration_type` (Year/Semester) + +#### Subject +- `name`, `course` (FK), `semester_or_year`, `syllabus_pdf`, `files` + +#### Student +- `name`, `status`, `gender`, `birth_date`, `email`, `phone`, `addresses`, `marital_status` +- `parent_name`, `parent_phone`, `citizenship_no`, `batches` (M2M), `image` +- `course` (FK), `current_semester`, `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` + +#### Routine +- `course` (FK), `subject` (FK), `teacher` (FK), `start_time`, `end_time` +- `semester_or_year`, `is_active` + +#### Attendance +- `date`, `routine` (FK), `teacher` (FK), `teacher_attend`, `class_status` + +#### Leave Management +- `Staff_leave`: `staff` (FK), `start_date`, `end_date`, `message`, `status` +- `Student_Leave`: `student` (FK), `start_date`, `end_date`, `message`, `status` + +#### Feedback System +- `StudentFeedback`: `student` (FK), `teacher` (FK), `rating`, `feedback_text` +- `InstituteFeedback`: `institute` (FK), `user` (FK), `feedback_type`, `rating`, `feedback_text` +- `StaffInstituteFeedback`: `institute` (FK), `staff` (FK), `feedback_type`, `rating`, `feedback_text` + +#### Course Tracking +- `student` (FK), `course` (FK), `enrollment_date`, `start_date`, `expected_end_date` +- `actual_end_date`, `progress_status`, `completion_percentage`, `current_semester` +- `semester_start_date`, `semester_end_date`, `notes` + +### 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 +- `POST /update-profile/` - Update user profile +- `POST /change-password/` - Change password +- `POST /teacher-update-profile-picture/` - Update teacher profile picture +- `POST /teacher-change-password/` - Change teacher 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 + +### 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 + +### Firebase Integration +- `POST /saveFCMToken/` - Save Firebase Cloud Messaging token + +## 📱 Frontend Features + +### User Interface +- Responsive design +- Dark/Light mode +- Custom themes +- Accessibility support + +### Interactive Features +- Real-time updates +- File uploads +- Data visualization + +### Mobile Optimization +- Touch-friendly interface +- Offline support +- Push notifications + +## 🔍 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 + +2. Data Management + - CRUD operations + - Data validation + - File handling + +3. Business Logic + - Attendance calculation + - Leave approval workflow + - Course progress tracking + +4. Integration + - Firebase messaging + - File storage + - Email/SMS notifications + +## 📈 Performance + +### Optimization Techniques +- Database indexing +- Query optimization +- Caching +- Lazy loading +- Code minification + +### Monitoring +- Performance metrics +- Error tracking +- User analytics +- Resource usage + +## 🤝 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 + +### Commit Messages +- Use present tense +- Be descriptive +- Reference issues + +## 📄 License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +--- + +
+ +### 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) + +
\ No newline at end of file diff --git a/app/admin.py b/app/admin.py index edc6abc..1bb20cc 100644 --- a/app/admin.py +++ b/app/admin.py @@ -1,92 +1,814 @@ from django.contrib import admin -from django.shortcuts import redirect -from django.urls import reverse -from app.models import ( - Student, - Teacher, - AcademicYear, +from .models import ( + Institute, Course, Subject, + SubjectFile, + Student, + Staff, + Batch, Attendance, - AttendanceReport, - LeaveReportStudent, - LeaveReportTeacher, - FeedbackStudent, - FeedbackTeacher, + 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 +import json +from django.db import transaction +from django.db.models import Q +from datetime import datetime + # Register your models here. -@admin.register(LeaveReportStudent) -class LeaveReportStudentAdmin(admin.ModelAdmin): - def changelist_view(self, request, extra_context=None): - return redirect(reverse("student_leave_view")) +class CustomAdminSite(admin.AdminSite): + def get_app_list(self, request): + # Original app list + app_list = super().get_app_list(request) + + model_map = { + # Authentication Models + "User": { + "group": "Authentication", + "group_label": "authentication", + }, + "Group": { + "group": "Authentication", + "group_label": "authentication", + }, + "TOTPSecret": { + "group": "Authentication", + "group_label": "authentication", + }, + "ResetToken": { + "group": "Authentication", + "group_label": "authentication", + }, + # Organization Models + "Institute": { + "group": "Organization", + "group_label": "organization", + }, + "Batch": { + "group": "Organization", + "group_label": "organization", + }, + "Notice": { + "group": "Organization", + "group_label": "organization", + }, + # Course Management Models + "Course": { + "group": "Course Management", + "group_label": "course_management", + }, + "Subject": { + "group": "Course Management", + "group_label": "course_management", + }, + "SubjectFile": { + "group": "Course Management", + "group_label": "course_management", + }, + "CourseTracking": { + "group": "Course Management", + "group_label": "course_management", + }, + # Human Management Models + "Student": { + "group": "Human Management", + "group_label": "human_management", + }, + "Staff": { + "group": "Human Management", + "group_label": "human_management", + }, + "Routine": { + "group": "Human Management", + "group_label": "human_management", + }, + "Staff_leave": { + "group": "Human Management", + "group_label": "human_management", + }, + "Student_Leave": { + "group": "Human Management", + "group_label": "human_management", + }, + # Attendance Models + "Attendance": { + "group": "Attendance", + "group_label": "attendance", + }, + "AttendanceRecord": { + "group": "Attendance", + "group_label": "attendance", + }, + # Feedback Models + "StudentFeedback": { + "group": "Feedback", + "group_label": "feedback", + }, + "InstituteFeedback": { + "group": "Feedback", + "group_label": "feedback", + }, + "StaffInstituteFeedback": { + "group": "Feedback", + "group_label": "feedback", + }, + # Device Management + "FCMDevice": { + "group": "Device Management", + "group_label": "device_management", + }, + } + + grouped_app_list = [{"name": "Main", "app_label": "dashboard", "models": []}] + grouped_models = {} + + for app in app_list: + for model in app["models"]: + model_name = model["object_name"] + if model_name in model_map: + group = model_map[model_name]["group"] + if group not in grouped_models: + grouped_models[group] = { + "name": group, + "app_label": model_map[model_name]["group_label"], + "models": [], + } + grouped_models[group]["models"].append(model) + + dashboard_item = { + "name": "Dashboard", + "app_label": "dashboard", + "models": [ + { + "name": "Dashboard", + "object_name": "Dashboard", + "perms": {"has_module_perms": True}, + "admin_url": "/app/dashboard", + "view_only": True, + } + ], + } + + grouped_app_list = [dashboard_item] + list(grouped_models.values()) + + return grouped_app_list -@admin.register(LeaveReportTeacher) -class LeaveReportTeacherAdmin(admin.ModelAdmin): - def changelist_view(self, request, extra_context=None): - return redirect(reverse("teacher_leave_view")) +# Create a custom admin site instance +custom_admin_site = CustomAdminSite(name="custom_admin") -@admin.register(FeedbackTeacher) -class FeedbackTeacherAdmin(admin.ModelAdmin): - def changelist_view(self, request, extra_context=None): - return redirect(reverse("teacher_feedback")) +@admin.register(Institute, site=custom_admin_site) +class InstituteModel(admin.ModelAdmin): + list_display = ("id", "name", "address", "phone") + search_fields = ("name", "phone") + list_filter = ("name", "phone") + advanced_filter_fields = ("name", "phone") -@admin.register(FeedbackStudent) -class FeedbackStudentAdmin(admin.ModelAdmin): - def changelist_view(self, request, extra_context=None): - return redirect(reverse("student_feedback_message")) +@admin.register(Course, site=custom_admin_site) +class CourseModel(admin.ModelAdmin): + list_display = ("id", "name", "duration", "duration_type", "get_total_duration") + search_fields = ("name", "duration", "duration_type") + list_filter = ("duration_type", "duration") + advanced_filter_fields = ("name", "duration", "duration_type") + + def get_total_duration(self, obj): + if obj.duration_type == "Semester": + return f"{obj.duration} years ({obj.duration * 2} semesters)" + return f"{obj.duration} year{'s' if obj.duration > 1 else ''}" + + get_total_duration.short_description = "Total Duration" + + fieldsets = ( + (None, {"fields": ("name",)}), + ( + "Duration Settings", + { + "fields": ("duration", "duration_type"), + "description": "For yearly courses, duration is in years. For semester-based courses, duration is in years (2 semesters per year).", + }, + ), + ) -@admin.register(AcademicYear) -class AcademicYearAdmin(admin.ModelAdmin): - def changelist_view(self, request, extra_context=None): - return redirect(reverse("manage_academicyear")) +@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") -@admin.register(Attendance) -class AttendanceAdmin(admin.ModelAdmin): - def changelist_view(self, request, extra_context=None): - return redirect(reverse("admin_view_attendance")) - - -@admin.register(AttendanceReport) -class AttendanceReportAdmin(admin.ModelAdmin): - pass - - -@admin.register(Student) -class StudentAdmin(admin.ModelAdmin): - def changelist_view(self, request, extra_context=None): - return redirect(reverse("manage_student")) - - -@admin.register(Teacher) -class TeacherAdmin(admin.ModelAdmin): - def changelist_view(self, request, extra_context=None): - return redirect(reverse("manage_teacher")) - - -@admin.register(Course) -class CourseAdmin(admin.ModelAdmin): - list_display = ("id", "name", "course_type", "duration", "created_at", "updated_at") - search_fields = ("name",) - list_filter = ("course_type",) - readonly_fields = ("created_at", "updated_at") - - def changelist_view(self, request, extra_context=None): - return redirect(reverse("manage_course")) - - -@admin.register(Subject) -class SubjectAdmin(admin.ModelAdmin): - list_display = ("id", "name", "course", "duration", "created_at", "updated_at") +@admin.register(Subject, site=custom_admin_site) +class SubjectModel(admin.ModelAdmin): + list_display = ("id", "name", "course", "get_period_display", "has_syllabus", "file_count") search_fields = ("name", "course__name") - list_filter = ("course__course_type", "course__duration") - readonly_fields = ("created_at", "updated_at") + list_filter = ("course", "semester_or_year") + advanced_filter_fields = ("name", "course", "semester_or_year") - def changelist_view(self, request, extra_context=None): - return redirect(reverse("manage_subject")) + 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}" + + get_period_display.short_description = "Period" + + def add_view(self, request, form_url="", extra_context=None): + extra_context = extra_context or {} + extra_context["show_save_and_add_another"] = True + extra_context["show_save"] = False + return super().add_view(request, form_url, extra_context) + + def response_add(self, request, obj, post_url_continue=None): + if "_addanother" in request.POST: + # Pre-fill the course and semester fields for the next entry + return self.response_post_save_add(request, obj) + return super().response_add(request, obj, post_url_continue) + + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + if obj and obj.course: + if obj.course.duration_type == "Semester": + form.base_fields["semester_or_year"].label = "Semester" + form.base_fields[ + "semester_or_year" + ].help_text = f"Enter semester number (1-{obj.course.duration * 2})" + else: + form.base_fields["semester_or_year"].label = "Year" + form.base_fields[ + "semester_or_year" + ].help_text = f"Enter year number (1-{obj.course.duration})" + + # Keep the last used course selected for next entry + if not obj and request.method == "GET": + if "course" in request.GET: + form.base_fields["course"].initial = request.GET["course"] + if "semester_or_year" in request.GET: + form.base_fields["semester_or_year"].initial = request.GET[ + "semester_or_year" + ] + return form + + def response_post_save_add(self, request, obj): + from django.http import HttpResponseRedirect + from django.urls import reverse + + opts = self.model._meta + url = reverse(f"admin:{opts.app_label}_{opts.model_name}_add") + return HttpResponseRedirect( + f"{url}?course={obj.course.id}&semester_or_year={obj.semester_or_year}" + ) + + +@admin.register(Student, site=custom_admin_site) +class StudentAdmin(admin.ModelAdmin): + form = StudentAdminForm + list_display = ("id", "name", "phone", "course", "status") + search_fields = ("name", "phone", "course__name") + list_filter = ("course", "status", "gender", "joining_date") + advanced_filter_fields = ("name", "phone", "course", "batch", "status") + + def has_view_permission(self, request, obj=None): + """Check if user has view permission""" + return request.user.has_perm("app.view_student") + + def has_add_permission(self, request): + """Check if user has add permission""" + return request.user.has_perm("app.add_student") + + def has_change_permission(self, request, obj=None): + """Check if user has change permission""" + return request.user.has_perm("app.change_student") + + def has_delete_permission(self, request, obj=None): + """Check if user has delete permission""" + return request.user.has_perm("app.delete_student") + + 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(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_filter = ("designation", "joining_date", "gender") + advanced_filter_fields = ("name", "phone", "designation") + readonly_fields = ("password",) + ordering = ("name",) + filter_horizontal = ("groups",) + + def has_view_permission(self, request, obj=None): + """Check if user has view permission""" + return request.user.has_perm("app.view_staff") + + def has_add_permission(self, request): + """Check if user has add permission""" + return request.user.has_perm("app.add_staff") + + def has_change_permission(self, request, obj=None): + """Check if user has change permission""" + return request.user.has_perm("app.change_staff") + + def has_delete_permission(self, request, obj=None): + """Check if user has delete permission""" + return request.user.has_perm("app.delete_staff") + + 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 + + +class AttendanceRecordForm(ModelForm): + def clean(self): + cleaned_data = super().clean() + if not cleaned_data.get("student"): + if self.instance and self.instance.pk: + cleaned_data["student"] = self.instance.student + return cleaned_data + + class Meta: + model = AttendanceRecord + fields = "__all__" + + +class AttendanceRecordInline(admin.TabularInline): + model = AttendanceRecord + form = AttendanceRecordForm + extra = 0 + can_delete = False # Prevent deletion + + def get_formset(self, request, obj=None, **kwargs): + formset = super().get_formset(request, obj, **kwargs) + return formset + + +@admin.register(Attendance, site=custom_admin_site) +class AttendanceAdmin(admin.ModelAdmin): + list_display = ["date", "routine", "class_status", "teacher", "teacher_attend"] + inlines = [AttendanceRecordInline] + search_fields = ("date", "routine__name", "teacher__name") + list_filter = ("date", "routine", "teacher") + advanced_filter_fields = ("date", "routine", "teacher") + + def save_model(self, request, obj, form, change): + is_new = not obj.pk + super().save_model(request, obj, form, change) + + if is_new and obj.routine: + students = Student.objects.filter(routines__routine=obj.routine).distinct() + + for student in students: + AttendanceRecord.objects.create( + attendance=obj, student=student, student_attend=False + ) + + def save_formset(self, request, form, formset, change): + instances = formset.save(commit=False) + + # Preserve existing records + for instance in instances: + if instance.pk and not instance.student_id: + original = AttendanceRecord.objects.get(pk=instance.pk) + instance.student = original.student + instance.save() + + formset.save_m2m() + + +@admin.register(Notice, site=custom_admin_site) +class NoticeAdmin(admin.ModelAdmin): + list_display = ("title", "message", "created_at") + search_fields = ("title", "message") + list_filter = ("title", "message") + advanced_filter_fields = ("title", "message") + + +@admin.register(Routine, site=custom_admin_site) +class RoutineAdmin(admin.ModelAdmin): + list_display = [ + "course", + "subject", + "teacher", + "start_time", + "end_time", + "semester_or_year", + "is_active", + ] + list_filter = ["is_active", "course", "subject", "teacher", "semester_or_year"] + search_fields = ["course__name", "subject__name", "teacher__name"] + ordering = ["start_time"] + change_form_template = "admin/app/routine/change_form.html" + + def get_urls(self): + urls = super().get_urls() + custom_urls = [ + path( + "add-multiple/", + self.admin_site.admin_view(self.save_routines), + name="routine-add-multiple", + ), + ] + return custom_urls + urls + + def save_routines(self, request): + if request.method == "POST": + try: + data = json.loads(request.body) + routines = data.get("routines", []) + + if not routines: + return JsonResponse( + {"success": False, "message": "No routines data provided"} + ) + + with ( + transaction.atomic() + ): # Use transaction to ensure all routines are created or none + created_routines = [] + for index, routine_data in enumerate(routines): + try: + # Validate required fields + required_fields = [ + "course", + "subject", + "teacher", + "start_time", + "end_time", + "semester_or_year", + ] + missing_fields = [ + field + for field in required_fields + if field not in routine_data + or not routine_data.get(field) + ] + + if missing_fields: + error_msg = f"Missing required fields: {', '.join(missing_fields)}" + raise ValueError(error_msg) + + # Convert IDs to integers + try: + course_id = int(routine_data["course"]) + subject_id = int(routine_data["subject"]) + teacher_id = int(routine_data["teacher"]) + semester = int(routine_data["semester_or_year"]) + + except (ValueError, TypeError) as e: + raise ValueError(f"Error parsing IDs: {str(e)}") + + # Validate time format + try: + start_time = datetime.strptime( + routine_data["start_time"], "%H:%M" + ).time() + end_time = datetime.strptime( + routine_data["end_time"], "%H:%M" + ).time() + except ValueError as e: + raise ValueError( + "Invalid time format. Please use HH:MM format." + ) + + # Check for existing routine + existing_routine = Routine.objects.filter( + course_id=course_id, + subject_id=subject_id, + semester_or_year=semester, + start_time=start_time, + end_time=end_time, + ).first() + + if existing_routine: + # Update existing routine + existing_routine.teacher_id = teacher_id + existing_routine.save() + created_routines.append(existing_routine) + else: + # Create new routine + routine = Routine.objects.create( + course_id=course_id, + subject_id=subject_id, + teacher_id=teacher_id, + start_time=start_time, + end_time=end_time, + semester_or_year=semester, + is_active=True, + ) + created_routines.append(routine) + + except ValueError as e: + raise # Re-raise to trigger rollback + except Exception as e: + raise # Re-raise to trigger rollback + + return JsonResponse( + { + "success": True, + "message": f"Successfully saved {len(created_routines)} routines", + "routines": [ + { + "id": r.id, + "course": r.course.name, + "subject": r.subject.name, + "teacher": r.teacher.name, + "start_time": r.start_time.strftime("%H:%M"), + "end_time": r.end_time.strftime("%H:%M"), + "semester_or_year": r.semester_or_year, + } + for r in created_routines + ], + } + ) + + except ValueError as e: + return JsonResponse({"success": False, "message": str(e)}) + except json.JSONDecodeError as e: + return JsonResponse( + {"success": False, "message": f"Invalid JSON data: {str(e)}"} + ) + except Exception as e: + return JsonResponse({"success": False, "message": str(e)}) + + return JsonResponse({"success": False, "message": "Invalid request method"}) + + def formfield_for_foreignkey(self, db_field, request, **kwargs): + if db_field.name == "subject": + if request.GET.get("course"): + kwargs["queryset"] = Subject.objects.filter( + course_id=request.GET.get("course"), + semester_or_year=request.GET.get("semester_or_year", 1), + ) + else: + kwargs["queryset"] = Subject.objects.none() + elif db_field.name == "teacher": + kwargs["queryset"] = Staff.objects.filter( + Q(designation__icontains="Teacher") + | Q(designation__icontains="Staff") + | Q(designation__icontains="Principal") + | Q(designation__isnull=True) + | Q(designation="") + ).order_by("name") + return super().formfield_for_foreignkey(db_field, request, **kwargs) + + def get_form(self, request, obj=None, **kwargs): + form = super().get_form(request, obj, **kwargs) + if not obj: # Only for adding new routines + form.base_fields["course"].widget.attrs["class"] = "select2" + form.base_fields["subject"].widget.attrs["class"] = "select2" + form.base_fields["teacher"].widget.attrs["class"] = "select2" + form.base_fields["semester_or_year"].initial = 1 + return form + + def add_view(self, request, form_url="", extra_context=None): + extra_context = extra_context or {} + extra_context["show_save_and_add_another"] = True + return super().add_view(request, form_url, extra_context) + + class Media: + css = { + "all": ( + "https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css", + ) + } + js = ("https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js",) + + +@admin.register(Staff_leave, site=custom_admin_site) +class StaffLeaveAdmin(admin.ModelAdmin): + list_display = ("staff", "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") + ordering = ("-created_at",) + raw_id_fields = ("staff",) + + +@admin.register(Student_Leave, site=custom_admin_site) +class StudentLeaveAdmin(admin.ModelAdmin): + 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") + ordering = ("-created_at",) + raw_id_fields = ("student",) + + +@admin.register(StudentFeedback, site=custom_admin_site) +class StudentFeedbackAdmin(admin.ModelAdmin): + list_display = ("student", "rating", "created_at") + list_filter = ("rating", "created_at") + search_fields = ("student__name", "feedback_text") + readonly_fields = ("created_at", "updated_at") + ordering = ("-created_at",) + + +@admin.register(CourseTracking, site=custom_admin_site) +class CourseTrackingAdmin(admin.ModelAdmin): + list_display = ( + "student", + "course", + "progress_status", + "completion_percentage", + "start_date", + "expected_end_date", + ) + list_filter = ("progress_status", "course", "start_date", "expected_end_date") + search_fields = ("student__name", "course__name", "notes") + readonly_fields = ("enrollment_date", "created_at", "updated_at") + ordering = ("-created_at",) + raw_id_fields = ("student", "course") + list_per_page = 25 + + def has_view_permission(self, request, obj=None): + return request.user.has_perm("app.view_coursetracking") + + def has_add_permission(self, request): + return request.user.has_perm("app.add_coursetracking") + + def has_change_permission(self, request, obj=None): + return request.user.has_perm("app.change_coursetracking") + + def has_delete_permission(self, request, obj=None): + return request.user.has_perm("app.delete_coursetracking") + + def has_module_permission(self, request): + return request.user.has_module_perms("app") + + +@admin.register(InstituteFeedback, site=custom_admin_site) +class InstituteFeedbackAdmin(admin.ModelAdmin): + list_display = ( + "display_name", + "institute", + "feedback_type", + "rating", + "is_anonymous", + "is_public", + "created_at", + ) + list_filter = ( + "feedback_type", + "rating", + "is_anonymous", + "is_public", + "created_at", + "institute", + ) + search_fields = ("user__name", "institute__name", "feedback_text") + readonly_fields = ("created_at", "updated_at") + date_hierarchy = "created_at" + + def display_name(self, obj): + return obj.display_name + + display_name.short_description = "Student Name" + + fieldsets = ( + (None, {"fields": ("institute", "user")}), + ( + "Feedback Details", + { + "fields": ( + "feedback_type", + "rating", + "feedback_text", + "is_anonymous", + "is_public", + ) + }, + ), + ( + "Metadata", + {"fields": ("created_at", "updated_at"), "classes": ("collapse",)}, + ), + ) + + def get_queryset(self, request): + return super().get_queryset(request).select_related("user", "institute") + + class Media: + css = {"all": ("admin/css/forms.css",)} + js = ("admin/js/collapse.js",) + + +@admin.register(StaffInstituteFeedback, site=custom_admin_site) +class StaffInstituteFeedbackAdmin(admin.ModelAdmin): + list_display = ( + "display_name", + "institute", + "feedback_type", + "rating", + "is_anonymous", + "is_public", + "created_at", + ) + list_filter = ( + "feedback_type", + "rating", + "is_anonymous", + "is_public", + "created_at", + "institute", + ) + search_fields = ("staff__name", "institute__name", "feedback_text") + readonly_fields = ("created_at", "updated_at") + date_hierarchy = "created_at" + + def display_name(self, obj): + return obj.display_name + + display_name.short_description = "Staff Name" + + fieldsets = ( + (None, {"fields": ("institute", "staff")}), + ( + "Feedback Details", + { + "fields": ( + "feedback_type", + "rating", + "feedback_text", + "is_anonymous", + "is_public", + ) + }, + ), + ( + "Metadata", + {"fields": ("created_at", "updated_at"), "classes": ("collapse",)}, + ), + ) + + def get_queryset(self, request): + return super().get_queryset(request).select_related("staff", "institute") + + +@admin.register(FCMDevice, site=custom_admin_site) +class FCMDeviceAdmin(admin.ModelAdmin): + list_display = ("id", "token", "created_at", "last_active", "is_active") + search_fields = ("token",) + list_filter = ("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",)}), + ) + + +@admin.register(SubjectFile, site=custom_admin_site) +class SubjectFileAdmin(admin.ModelAdmin): + list_display = ("id", "title", "subject", "uploaded_by", "uploaded_at") + search_fields = ("title", "subject__name", "uploaded_by__name") + list_filter = ("subject__course", "uploaded_at") + autocomplete_fields = ["subject", "uploaded_by"] + + +custom_admin_site.register(User, UserAdmin) +custom_admin_site.register(Group, GroupAdmin) diff --git a/app/adminViews.py b/app/adminViews.py deleted file mode 100644 index 72d9491..0000000 --- a/app/adminViews.py +++ /dev/null @@ -1,724 +0,0 @@ -import json -import requests -from django.contrib.admin import site -from django.contrib import messages -from django.core.files.storage import FileSystemStorage -from django.http import HttpResponse, HttpResponseRedirect, JsonResponse -from django.shortcuts import render, redirect, get_object_or_404 -from django.urls import reverse -from django.views.decorators.csrf import csrf_exempt -from app.models import ( - Student, - Teacher, - Subject, - Course, - LeaveReportStudent, - LeaveReportTeacher, - Attendance, - AttendanceReport, - AcademicYear, - FeedbackStudent, - FeedbackTeacher, -) -from django.contrib.auth.decorators import login_required, user_passes_test -from app.forms import SubjectForm - -admin_site = site - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def admin_home(request): - student_count1 = Student.objects.all().count() - teacher_count = Teacher.objects.all().count() - subject_count = Subject.objects.all().count() - course_count = Course.objects.all().count() - course_all = Course.objects.all() - course_name_list = [] - subject_count_list = [] - student_count_list_in_course = [] - for course in course_all: - subjects = Subject.objects.filter(course_id=course.id).count() - students = Student.objects.filter(course_id=course.id).count() - course_name_list.append(course.name) - subject_count_list.append(subjects) - student_count_list_in_course.append(students) - - # subjects_all = Subject.objects.all() - # subject_list = [] - # student_count_list_in_subject = [] - # for subject in subjects_all: - # course = subject.course - # student_count = Student.objects.filter(course_id=course.id).count() - # subject_list.append(subject.name) - # student_count_list_in_subject.append(student_count) - - # teacher = Teacher.objects.all() - # attendance_present_list_teacher = [] - # attendance_absent_list_teacher = [] - # teacher_name_list = [] - # for teacher in teacher: - # subject_ids = Subject.objects.filter(teacher=teacher).values_list( - # "id", flat=True - # ) - # attendance = Attendance.objects.filter( - # subject_id__in=subject_ids, - # ).count() - # leaves = LeaveReportTeacher.objects.filter( - # teacher_id=teacher.id, leave_status=1 - # ).count() - # attendance_present_list_teacher.append(attendance) - # attendance_absent_list_teacher.append(leaves) - # teacher_name_list.append(teacher.first_name) - - # students_all = Student.objects.all() - # attendance_present_list_student = [] - # attendance_absent_list_student = [] - # student_name_list = [] - # for student in students_all: - # attendance = AttendanceReport.objects.filter( - # student_id=student.id, status=True - # ).count() - # absent = AttendanceReport.objects.filter( - # student_id=student.id, status=False - # ).count() - # leaves = LeaveReportStudent.objects.filter( - # student_id=student.id, leave_status=1 - # ).count() - # attendance_present_list_student.append(attendance) - # attendance_absent_list_student.append(leaves + absent) - # student_name_list.append(student.username) - - context = { - **admin_site.each_context(request), - "title": "Admin Dashboard", - "student_count": student_count1, - "teacher_count": teacher_count, - "subject_count": subject_count, - "course_count": course_count, - # "course_name_list": course_name_list, - "subject_count_list": subject_count_list, - # "student_count_list_in_course": student_count_list_in_course, - # "student_count_list_in_subject": student_count_list_in_subject, - # "subject_list": subject_list, - # "teacher_name_list": teacher_name_list, - # "attendance_present_list_teacher": attendance_present_list_teacher, - # "attendance_absent_list_teacher": attendance_absent_list_teacher, - # "student_name_list": student_name_list, - # "attendance_present_list_student": attendance_present_list_student, - # "attendance_absent_list_student": attendance_absent_list_student, - "opts": Student._meta, - } - return render(request, "hod/dashboard.html", context) - - -def add_course(request): - admin_site = site - if request.method == "POST": - name = request.POST.get("name") - course_type = request.POST.get("course_type") - duration = request.POST.get("duration") - - if not name or not course_type or not duration: - messages.error(request, "All fields are required!") - return redirect("add_course") - - try: - duration = int(duration) - if duration <= 0: - raise ValueError - except ValueError: - messages.error(request, "Duration must be a positive number!") - return redirect("add_course") - - if Course.objects.filter(name=name).exists(): - messages.error(request, "A course with this name already exists.") - return redirect("add_course") - - Course.objects.create(name=name, course_type=course_type, duration=duration) - messages.success(request, "Course added successfully!") - return redirect("add_course") - - context = { - **admin_site.each_context(request), - "title": "Add Course", - "COURSE_TYPE_CHOICES": Course.COURSE_TYPE_CHOICES, - "opts": Course._meta, - } - return render(request, "hod/add_course.html", context) - - -def manage_course(request): - courses = Course.objects.all() - context = { - **admin_site.each_context(request), - "courses": courses, - "title": "Manage Courses", - "opts": Course._meta, - } - return render(request, "hod/manage_course.html", context) - - -def edit_course(request, course_id): - course = get_object_or_404(Course, id=course_id) - - if request.method == "POST": - name = request.POST.get("name") - course_type = request.POST.get("course_type") - duration = request.POST.get("duration") - - # Validate unique name if it changed - if name != course.name and Course.objects.filter(name=name).exists(): - messages.error(request, "A course with this name already exists.") - return render( - request, "edit_course.html", {"course": course, "title": "Edit Course"} - ) - - try: - course.name = name - course.course_type = course_type - course.duration = duration - course.save() - - messages.success(request, "Course updated successfully.") - return redirect("manage_course") - - except Exception as e: - messages.error(request, f"Error updating course: {str(e)}") - context = { - **admin_site.each_context(request), - "title": "Edit Course", - "course": course, - } - return render(request, "hod/edit_course.html", context) - - -def delete_course(request, course_id): - course = Course.objects.get(id=course_id) - try: - course.delete() - messages.success(request, "Course Deleted Successfully.") - return redirect("manage_course") - except: - messages.error(request, "Failed to Delete Course.") - return redirect("manage_course") - - -def add_academicyear(request): - if request.method == "POST": - start_date = request.POST.get("start_date") - academic_year = AcademicYear.objects.create(start_date=start_date) - messages.success(request, "Academic Year added successfully.") - return redirect("manage_academicyear") - context = { - **admin_site.each_context(request), - "title": "Add Academic Year", - # "academic_year": academic_year, - "opts": AcademicYear._meta, - } - return render(request, "hod/add_academicyear.html", context) - - -def edit_academicyear(request, pk): - academic_year = get_object_or_404(AcademicYear, pk=pk) - if request.method == "POST": - start_date = request.POST.get("start_date") - - if start_date: - academic_year.start_date = start_date - academic_year.save() - messages.success(request, "Academic Year updated successfully.") - return redirect("manage_academicyear") - else: - messages.error(request, "Please provide a valid start date.") - context = { - **admin_site.each_context(request), - "title": "Edit Academic Year", - "opts": AcademicYear._meta, - } - return render(request, "hod/edit_academicyear.html", context) - - -def manage_academicyear(request): - context = { - **admin_site.each_context(request), - "academic_years": AcademicYear.objects.all(), - "title": "Manage Academic Year", - "opts": AcademicYear._meta, - } - return render(request, "hod/manage_academicyear.html", context) - - -def delete_academicyear(request, academic_year_id): - academic_year = AcademicYear.objects.get(id=academic_year_id) - try: - academic_year.delete() - messages.success(request, "AcademicYear Deleted Successfully.") - return redirect("manage_academicyear") - except: - messages.error(request, "Failed to Delete AcademicYear.") - return redirect("manage_academicyear") - - -def manage_subjects(request): - subjects = Subject.objects.all() - courses = Course.objects.all() - selected_course_id = request.GET.get("course") - selected_duration = request.GET.get("duration", "") - - if selected_course_id and selected_duration: - subjects = Subject.objects.filter( - course_id=selected_course_id, duration=selected_duration - ) - elif selected_course_id: - subjects = Subject.objects.filter(course_id=selected_course_id) - elif selected_duration: - subjects = Subject.objects.filter(duration=selected_duration) - else: - subjects = Subject.objects.all() - context = { - **admin_site.each_context(request), - "subjects": subjects, - "selected_course_id": selected_course_id, - "selected_duration": selected_duration, - "title": "Manage Subjects", - "courses": courses, - "opts": Subject._meta, - } - return render(request, "hod/manage_subject.html", context) - - -def add_subject(request): - courses = Course.objects.all() - if request.method == "POST": - form = SubjectForm(request.POST) - if form.is_valid(): - form.save() - return redirect("manage_subjects") - else: - form = SubjectForm() - - context = { - **admin_site.each_context(request), - "title": "Add Subject", - "courses": courses, - "form": form, - "opts": Subject._meta, - } - return render(request, "hod/add_subject.html", context) - - -def edit_subject(request, pk): - subject = get_object_or_404(Subject, pk=pk) - courses = Course.objects.all() - if request.method == "POST": - form = SubjectForm(request.POST, instance=subject) - if form.is_valid(): - form.save() - return redirect("manage_subject") - else: - form = SubjectForm(instance=subject) - context = { - **admin_site.each_context(request), - "title": "Add Subject", - "courses": courses, - "form": form, - "opts": Subject._meta, - } - - return render(request, "hod/edit_subject.html", context) - - -def delete_subject(request, subject_id): - subject = Subject.objects.get(id=subject_id) - try: - subject.delete() - messages.success(request, "Subject Deleted Successfully.") - return redirect("manage_subject") - except: - messages.error(request, "Failed to Delete Subject.") - return redirect("manage_subject") - - -def manage_student(request): - search_query = request.GET.get("search", "") - students = Student.objects.all() - - if search_query: - students = ( - students.filter(first_name__icontains=search_query) - | students.filter(last_name__icontains=search_query) - | students.filter(email__icontains=search_query) - ) - - context = { - **admin_site.each_context(request), - "title": "Manage Students", - "students": students, - "search_query": search_query, - } - return render(request, "hod/manage_student.html", context) - - -def add_student(request): - courses = Course.objects.all() - academic_years = AcademicYear.objects.all() - occupation_choices = dict(Student.occupation_choice) - relationship_choices = dict(Student.relationship_choice) - - if request.method == "POST": - first_name = request.POST.get("first_name") - middle_name = request.POST.get("middle_name") - last_name = request.POST.get("last_name") - email = request.POST.get("email") - phone = request.POST.get("phone") - dob = request.POST.get("dob") - gender = request.POST.get("gender") - course_id = request.POST.get("course") - academic_year_id = request.POST.get("academic_year") - address = request.POST.get("address") - father_name = request.POST.get("father_name") - father_occupation = request.POST.get("father_occupation") - mother_name = request.POST.get("mother_name") - mother_occupation = request.POST.get("mother_occupation") - guardian_name = request.POST.get("guardian_name") - guardian_number = request.POST.get("guardian_number") - parents_number = request.POST.get("parents_number") - relationship_with_student = request.POST.get("relationship_with_student") - - try: - course = get_object_or_404(Course, id=course_id) - academic_year = get_object_or_404(AcademicYear, id=academic_year_id) - - Student.objects.create( - first_name=first_name, - middle_name=middle_name, - last_name=last_name, - email=email, - phone=phone, - dob=dob, - gender=gender, - course=course, - academic_year=academic_year, - address=address, - father_name=father_name, - father_occupation=father_occupation, - mother_name=mother_name, - mother_occupation=mother_occupation, - guardian_name=guardian_name, - guardian_number=guardian_number, - parents_number=parents_number, - relationship_with_student=relationship_with_student, - ) - messages.success(request, "Student added successfully!") - return redirect("manage_student") - except Exception as e: - messages.error(request, f"Error adding student: {e}") - - context = { - **admin_site.each_context(request), - "title": "Add Student", - "courses": courses, - "academic_years": academic_years, - "occupation_choices": occupation_choices.items(), - "relationship_choices": relationship_choices.items(), - } - return render(request, "hod/add_student.html", context) - - -def edit_student(request, student_id): - student = get_object_or_404(Student, id=student_id) - courses = Course.objects.all() - academic_years = AcademicYear.objects.all() - occupation_choices = dict(Student.occupation_choice) - relationship_choices = dict(Student.relationship_choice) - - if request.method == "POST": - student.first_name = request.POST.get("first_name") - student.middle_name = request.POST.get("middle_name") - student.last_name = request.POST.get("last_name") - student.email = request.POST.get("email") - student.phone = request.POST.get("phone") - student.dob = request.POST.get("dob") - student.gender = request.POST.get("gender") - student.course = get_object_or_404(Course, id=request.POST.get("course")) - student.academic_year = get_object_or_404( - AcademicYear, id=request.POST.get("academic_year") - ) - student.father_occupation = request.POST.get("father_occupation") - student.relationship_with_student = request.POST.get( - "relationship_with_student" - ) - - try: - student.save() - messages.success(request, "Student updated successfully!") - return redirect("manage_student") - except Exception as e: - messages.error(request, f"Error updating student: {e}") - - context = { - **admin_site.each_context(request), - "title": "Edit Student", - "student": student, - "courses": courses, - "academic_years": academic_years, - "occupation_choices": occupation_choices.items(), - "relationship_choices": relationship_choices.items(), - } - return render(request, "hod/edit_student.html", context) - - -def delete_student(request, student_id): - student = get_object_or_404(Student, id=student_id) - student.delete() - messages.success(request, "Student deleted successfully!") - return redirect("manage_student") - - -def manage_teacher(request): - teachers = Teacher.objects.all() - context = { - **admin_site.each_context(request), - "title": "Manage Teachers", - "teachers": teachers, - } - return render(request, "hod/manage_teacher.html", context) - - -def add_teacher(request): - if request.method == "POST": - try: - Teacher.objects.create( - first_name=request.POST.get("first_name"), - middle_name=request.POST.get("middle_name"), - last_name=request.POST.get("last_name"), - email=request.POST.get("email"), - phone=request.POST.get("phone"), - dob=request.POST.get("dob"), - gender=request.POST.get("gender"), - address=request.POST.get("address"), - # username=request.POST.get("username"), - # password=request.POST.get("password"), - qualification=request.POST.get("qualification"), - specialization_on=request.POST.get("specialization_on"), - marital_status=request.POST.get("marital_status"), - date_of_joining=request.POST.get("date_of_joining"), - ) - messages.success(request, "Teacher added successfully!") - return redirect("manage_teacher") - except Exception as e: - messages.error(request, f"Error adding teacher: {e}") - - context = { - **admin_site.each_context(request), - "title": "Add Teacher", - "qualification_choices": dict(Teacher.qualification_choice).items(), - "marital_status_choices": dict(Teacher.marital_status_choice).items(), - } - return render(request, "hod/add_teacher.html", context) - - -def edit_teacher(request, teacher_id): - teacher = get_object_or_404(Teacher, id=teacher_id) - - if request.method == "POST": - try: - teacher.first_name = request.POST.get("first_name") - teacher.middle_name = request.POST.get("middle_name") - teacher.last_name = request.POST.get("last_name") - teacher.email = request.POST.get("email") - teacher.phone = request.POST.get("phone") - teacher.dob = request.POST.get("dob") - teacher.gender = request.POST.get("gender") - teacher.address = request.POST.get("address") - teacher.qualification = request.POST.get("qualification") - teacher.specialization_on = request.POST.get("specialization_on") - teacher.marital_status = request.POST.get("marital_status") - teacher.date_of_joining = request.POST.get("date_of_joining") - teacher.save() - - messages.success(request, "Teacher updated successfully!") - return redirect("manage_teacher") - except Exception as e: - messages.error(request, f"Error updating teacher: {e}") - - context = { - **admin_site.each_context(request), - "title": "Edit Teacher", - "teacher": teacher, - "qualification_choices": dict(Teacher.qualification_choice).items(), - "marital_status_choices": dict(Teacher.marital_status_choice).items(), - } - return render(request, "hod/edit_teacher.html", context) - - -def delete_teacher(request, teacher_id): - teacher = get_object_or_404(Teacher, id=teacher_id) - teacher.delete() - messages.success(request, "Teacher deleted successfully!") - return redirect("manage_teacher") - - -def student_feedback_message(request): - feedbacks = FeedbackStudent.objects.all() - context = { - **admin_site.each_context(request), - "title": "Student's Feedback", - "feedbacks": feedbacks, - } - return render(request, "hod/student_feedback.html", context) - - -@csrf_exempt -def student_feedback_message_reply(request): - feedback_id = request.POST.get("id") - feedback_reply = request.POST.get("reply") - - try: - feedback = FeedbackStudent.objects.get(id=feedback_id) - feedback.feedback_reply = feedback_reply - feedback.save() - return HttpResponse("True") - - except: - return HttpResponse("False") - - -def teacher_feedback(request): - feedbacks = FeedbackTeacher.objects.all() - context = { - **admin_site.each_context(request), - "title": "Staff's Feedback", - "feedbacks": feedbacks, - } - return render(request, "hod/teacher_feedback.html", context) - - -@csrf_exempt -def teacher_feedback_replied(request): - feedback_id = request.POST.get("id") - feedback_message = request.POST.get("message") - - try: - feedback = FeedbackTeacher.objects.get(id=feedback_id) - feedback.feedback_reply = feedback_message - feedback.save() - return HttpResponse("True") - except Exception: - return HttpResponse("False") - - -def student_leave_view(request): - leaves = LeaveReportStudent.objects.all() - context = { - **admin_site.each_context(request), - "title": "Student Application", - "leaves": leaves, - } - return render(request, "hod/student_leave_view.html", context) - - -def student_leave_approve(request, leave_id): - leave = LeaveReportStudent.objects.get(id=leave_id) - leave.leave_status = 1 - leave.save() - return HttpResponseRedirect(reverse("student_leave_view")) - - -def student_leave_reject(request, leave_id): - leave = LeaveReportStudent.objects.get(id=leave_id) - leave.leave_status = 2 - leave.save() - return HttpResponseRedirect(reverse("student_leave_view")) - - -def teacher_leave_view(request): - leaves = LeaveReportTeacher.objects.all() - context = { - **admin_site.each_context(request), - "title": "Teacher Application", - "leaves": leaves, - } - return render(request, "hod/teacher_leave_view.html", context) - - -def teacher_leave_approve(request, leave_id): - leave = LeaveReportTeacher.objects.get(id=leave_id) - leave.leave_status = 1 - leave.save() - return HttpResponseRedirect(reverse("teacher_leave_view")) - - -def teacher_leave_reject(request, leave_id): - leave = LeaveReportTeacher.objects.get(id=leave_id) - leave.leave_status = 2 - leave.save() - return HttpResponseRedirect(reverse("teacher_leave_view")) - - -def admin_view_attendance(request): - subjects = Subject.objects.all() - academic_years = AcademicYear.objects.all() - context = { - **admin_site.each_context(request), - "subjects": subjects, - "academic_years": academic_years, - } - return render(request, "hod/view_attendance.html", context) - - -@csrf_exempt -def admin_get_attendance_dates(request): - # Getting Values from Ajax POST 'Fetch Student' - subject_id = request.POST.get("subject") - academic_year = request.POST.get("academic_year_id") - - # Students enroll to Course, Course has Subjects - # Getting all data from subject model based on subject_id - subject_model = Subject.objects.get(id=subject_id) - - academic_model = AcademicYear.objects.get(id=academic_year) - - # students = Students.objects.filter(course_id=subject_model.course_id, session_year_id=session_model) - attendance = Attendance.objects.filter( - subject_id=subject_model, academic_year_id=academic_model - ) - - # Only Passing Student Id and Student Name Only - list_data = [] - - for attendance_single in attendance: - data_small = { - "id": attendance_single.id, - "attendance_date": str(attendance_single.attendance_date), - "academic_year_id": attendance_single.academic_year_id.id, - } - list_data.append(data_small) - - return JsonResponse( - json.dumps(list_data), content_type="application/json", safe=False - ) - - -@csrf_exempt -def admin_get_attendance_student(request): - # Getting Values from Ajax POST 'Fetch Student' - attendance_date = request.POST.get("attendance_date") - attendance = Attendance.objects.get(id=attendance_date) - - attendance_data = AttendanceReport.objects.filter(attendance_id=attendance) - # Only Passing Student Id and Student Name Only - list_data = [] - - for student in attendance_data: - data_small = { - "id": student.student_id.id, - "name": student.student_id.first_name + " " + student.student_id.last_name, - "status": student.status, - } - list_data.append(data_small) - - return JsonResponse( - json.dumps(list_data), content_type="application/json", safe=False - ) diff --git a/app/apps.py b/app/apps.py index bcfe39b..eb38975 100644 --- a/app/apps.py +++ b/app/apps.py @@ -2,5 +2,8 @@ from django.apps import AppConfig 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 new file mode 100644 index 0000000..c945d79 --- /dev/null +++ b/app/auth.py @@ -0,0 +1,298 @@ +from django.shortcuts import render, redirect +from django.contrib.auth import login +from django.views.decorators.csrf import csrf_exempt +from django.contrib.auth.models import User +from django.contrib.auth.hashers import make_password +import uuid +from .models import Student, Staff +from .utils import ( + generate_otp, + generate_secret_key, + store_secret_key, + verify_otp, + send_otp_sms, + send_password_reset_email, + verify_reset_code, + 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") + +def password_reset_phone(request): + """View for initiating password reset with phone number""" + if request.method == "POST": + phone = request.POST.get("phone") + + # Validate phone number + if not phone or not phone.isdigit(): + return render(request, "login/password_reset_phone.html", {"error": "Please enter a valid phone number", "is_email": False}) + + # Check if a user with this phone exists + student = Student.objects.filter(phone=phone).first() + staff = Staff.objects.filter(phone=phone).first() + + if not student and not staff: + return render(request, "login/password_reset_phone.html", {"error": "No account found with this phone number", "is_email": False}) + + # Generate secret key and store it + secret_key = generate_secret_key() + store_secret_key(phone, secret_key) + + # Generate and send OTP + otp = generate_otp(secret_key) + send_result = send_otp_sms(phone, otp) + + if send_result: + # Redirect to OTP verification page + return render(request, "login/password_reset_otp.html", {"phone": phone, "is_email": False}) + else: + return render(request, "login/password_reset_phone.html", {"error": "Failed to send OTP. Please try again.", "is_email": False}) + + return render(request, "login/password_reset_phone.html", {"is_email": False}) + +def password_reset_phone_verify(request): + """View for verifying OTP for password reset""" + if request.method == "POST": + phone = request.POST.get("phone") + otp = request.POST.get("otp") + + if not phone or not otp: + return render(request, "login/password_reset_otp.html", { + "error": "Phone and OTP are required", + "phone": phone, + "is_email": False + }) + + # Verify OTP + if verify_otp(phone, otp): + # Generate a secure token for this reset + token = str(uuid.uuid4()) + # Store token in database + store_reset_token(token, phone) + + # Redirect to set new password + return render(request, "login/password_reset_set.html", { + "phone": phone, + "token": token, + "is_email": False + }) + else: + return render(request, "login/password_reset_otp.html", { + "error": "Invalid or expired OTP", + "phone": phone, + "is_email": False + }) + + # If GET request, redirect to phone login + return redirect("phone_reset_password") + +def resend_phone_otp(request): + """View for resending OTP for password reset""" + if request.method == "POST": + phone = request.POST.get("phone") + + if not phone: + return redirect("phone_reset_password") + + # Generate new secret key and store it + secret_key = generate_secret_key() + store_secret_key(phone, secret_key) + + # Generate and send new OTP + otp = generate_otp(secret_key) + send_result = send_otp_sms(phone, otp) + + if send_result: + return render(request, "login/password_reset_otp.html", { + "phone": phone, + "message": "OTP resent successfully", + "is_email": False + }) + else: + return render(request, "login/password_reset_otp.html", { + "phone": phone, + "error": "Failed to resend OTP", + "is_email": False + }) + + return redirect("phone_reset_password") + +def password_reset_email(request): + """View for initiating password reset with email""" + if request.method == "POST": + email = request.POST.get("email") + + # Validate email + if not email: + return render(request, "login/email_reset.html", {"error": "Please enter a valid email address", "is_email": True}) + + # Check if a user with this email exists + student = Student.objects.filter(email=email).first() + staff = Staff.objects.filter(email=email).first() + + if not student and not staff: + return render(request, "login/email_reset.html", {"error": "No account found with this email address", "is_email": True}) + + # Get the user + user = student or staff + + # Send password reset email + if send_password_reset_email(user, request, email): + # Redirect to verification page + return render(request, "login/password_reset_otp.html", {"email": email, "is_email": True}) + else: + return render(request, "login/email_reset.html", {"error": "Failed to send verification code. Please try again.", "is_email": True}) + + return render(request, "login/email_reset.html", {"is_email": True}) + +def password_reset_email_verify(request): + """View for verifying reset code for email""" + if request.method == "POST": + email = request.POST.get("email") + code = request.POST.get("otp") + + if not email or not code: + return render(request, "login/password_reset_otp.html", { + "error": "Email and verification code are required", + "email": email, + "is_email": True + }) + + # Verify code + if verify_reset_code(email, code): + # Generate a secure token for this reset + token = str(uuid.uuid4()) + # Store token in database + store_reset_token(token, email) + + # Redirect to set new password + return render(request, "login/password_reset_set.html", { + "email": email, + "token": token, + "is_email": True + }) + else: + return render(request, "login/password_reset_otp.html", { + "error": "Invalid or expired code", + "email": email, + "is_email": True, + "message": "Please check your email for the verification code." + }) + + # For GET request, show the verification form with email pre-filled + email = request.GET.get("email") + return render(request, "login/password_reset_otp.html", { + "email": email, + "is_email": True, + "message": "Please check your email for the verification code." + }) + +def resend_email_code(request): + """View for resending verification code for password reset""" + if request.method == "POST": + email = request.POST.get("email") + + if not email: + return redirect("email_reset_password") + + # Find the user + student = Student.objects.filter(email=email).first() + staff = Staff.objects.filter(email=email).first() + user = student or staff + + if not user: + return render(request, "login/password_reset_otp.html", { + "email": email, + "is_email": True, + "error": "User not found" + }) + + # Send new verification code + if send_password_reset_email(user, request, email): + return render(request, "login/password_reset_otp.html", { + "email": email, + "is_email": True, + "message": "Verification code resent successfully" + }) + else: + return render(request, "login/password_reset_otp.html", { + "email": email, + "is_email": True, + "error": "Failed to resend verification code" + }) + + return redirect("email_reset_password") + +def set_new_password(request): + """View for setting new password after verification""" + if request.method == "POST": + phone = request.POST.get("phone") + email = request.POST.get("email") + token = request.POST.get("token") + new_password1 = request.POST.get("new_password1") + new_password2 = request.POST.get("new_password2") + + identifier = phone or email + is_email = bool(email) + + # Validate inputs + if not all([identifier, token, new_password1, new_password2]): + return render(request, "login/password_reset_set.html", { + "error": "All fields are required", + "phone": phone, + "email": email, + "token": token, + "is_email": is_email + }) + + # Verify token is valid + if not verify_reset_token(token, identifier): + return render(request, "login/password_reset_set.html", { + "error": "Invalid or expired reset session", + "phone": phone, + "email": email, + "token": token, + "is_email": is_email + }) + + # Check passwords match + if new_password1 != new_password2: + return render(request, "login/password_reset_set.html", { + "error": "Passwords do not match", + "phone": phone, + "email": email, + "token": token, + "is_email": is_email + }) + + # Find the user + if is_email: + student = Student.objects.filter(email=email).first() + staff = Staff.objects.filter(email=email).first() + else: + student = Student.objects.filter(phone=phone).first() + staff = Staff.objects.filter(phone=phone).first() + + user = student or staff + if not user: + return render(request, "login/password_reset_set.html", { + "error": "User not found", + "phone": phone, + "email": email, + "token": token, + "is_email": is_email + }) + + # Update password + user.password = make_password(new_password1) + user.save() + + # Redirect to login with success message + 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 diff --git a/app/backends.py b/app/backends.py new file mode 100644 index 0000000..66e20be --- /dev/null +++ b/app/backends.py @@ -0,0 +1,90 @@ +from django.contrib.auth.models import User +from django.contrib.auth.backends import ModelBackend +from .models import Staff, Student + +class MultiModelBackend(ModelBackend): + def authenticate(self, request, username=None, password=None, **kwargs): + """ + Authenticate the user based on username (email or phone) and password. + """ + if username is None or password is None: + return None + + # Try authenticating as Staff by phone + # Use filter().first() to avoid MultipleObjectsReturned error + staff = Staff.objects.filter(phone=username).first() + if staff and 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 + + # 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 + + # Try authenticating as Student by email + # Use filter().first() to handle multiple students with same email + student = Student.objects.filter(email=username).first() + if student and student.check_password(password): + return student + + # Return None if no match + return None + + def get_user(self, user_id): + """ + Retrieve a user instance using their ID. + """ + for model in [Staff, Student]: + try: + return model.objects.get(pk=user_id) + except model.DoesNotExist: + continue + return None + + def get_group_permissions(self, user_obj, obj=None): + """ + Return a set of permission strings that the user has through their groups. + """ + 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'): + perms = set() + for group in user_obj.groups.all(): + perms.update( + "%s.%s" % (ct, name) + for ct, name in group.permissions.values_list( + 'content_type__app_label', + 'codename' + ) + ) + user_obj._group_perm_cache = perms + return user_obj._group_perm_cache + else: + return super().get_group_permissions(user_obj, obj) + + 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: + return super().get_all_permissions(user_obj, obj) + + 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: + return super().has_perm(user_obj, perm, obj) diff --git a/app/firebase.py b/app/firebase.py new file mode 100644 index 0000000..38009d2 --- /dev/null +++ b/app/firebase.py @@ -0,0 +1,135 @@ +import os +from django.http import HttpResponse, JsonResponse +from django.db import models +import firebase_admin +from firebase_admin import messaging, credentials +from student_management_system.settings import BASE_DIR + + +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) + + def __str__(self): + return f"Device {self.id} ({self.token[:20]}...)" + + class Meta: + verbose_name = "FCM Device" + verbose_name_plural = "FCM Devices" + ordering = ['-last_active'] + + def deactivate(self): + """Deactivate this device token""" + self.is_active = False + self.save() + + +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'}) + except Exception as e: + print(f"Error saving FCM token: {str(e)}") + return JsonResponse({'error': str(e)}, status=500) + + +def get_firebase_js(): + """Generate Firebase JavaScript configuration""" + 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'); + + firebase.initializeApp({ + apiKey: "%s", + authDomain: "%s", + projectId: "%s", + storageBucket: "%s", + messagingSenderId: "%s", + appId: "%s", + measurementId: "%s" + }); + + const messaging = firebase.messaging(); + + messaging.onBackgroundMessage(function(payload) { + const notificationTitle = payload.notification.title; + const notificationOptions = { + body: payload.notification.body, + icon: payload.notification.icon || '/static/img/logo.png' + }; + + 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", "") + ) + return HttpResponse(data, content_type="application/javascript") + + +# Sending Firebase Push Notification +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}") + + +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 isinstance(tokens, (list, tuple)): + tokens = list(tokens) # Convert QuerySet to list + + success_count = 0 + failure_count = 0 + failed_tokens = [] + + # Get only active devices + active_devices = FCMDevice.objects.filter( + token__in=tokens, + is_active=True + ) + + for device in active_devices: + try: + response = messaging.send( + messaging.Message( + notification=messaging.Notification( + title=title, + body=message, + ), + 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}") + 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 20a99e4..4c92070 100644 --- a/app/forms.py +++ b/app/forms.py @@ -1,17 +1,65 @@ -from django import forms -from .models import Subject, Course - - -class SubjectForm(forms.ModelForm): - class Meta: - model = Subject - fields = ["name", "course", "duration"] # Include 'name' field here - - # Optionally, you can add custom validation if needed. - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields[ - "course" - ].queryset = ( - Course.objects.all() - ) # Dynamically set the course queryset if needed +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 diff --git a/app/migrations/0001_initial.py b/app/migrations/0001_initial.py deleted file mode 100644 index 2d6690f..0000000 --- a/app/migrations/0001_initial.py +++ /dev/null @@ -1,192 +0,0 @@ -# Generated by Django 5.1.5 on 2025-01-16 16:52 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [] - - operations = [ - migrations.CreateModel( - name="Student", - fields=[ - ("id", models.AutoField(primary_key=True, serialize=False)), - ("first_name", models.CharField(max_length=255)), - ( - "middle_name", - models.CharField(blank=True, max_length=255, null=True), - ), - ("last_name", models.CharField(max_length=255)), - ("username", models.CharField(max_length=255)), - ("password", models.CharField(max_length=255)), - ( - "phone", - models.CharField( - max_length=10, - validators=[ - django.core.validators.RegexValidator("^\\+?1?\\d{9,15}$") - ], - ), - ), - ("email", models.EmailField(max_length=255, unique=True)), - ("dob", models.DateField()), - ( - "gender", - models.CharField( - choices=[("M", "Male"), ("F", "Female")], max_length=1 - ), - ), - ("address", models.CharField(max_length=255)), - ("father_name", models.CharField(max_length=255)), - ("mother_name", models.CharField(max_length=255)), - ( - "parents_number", - models.CharField( - max_length=10, - validators=[ - django.core.validators.RegexValidator("^\\+?1?\\d{9,15}$") - ], - ), - ), - ( - "father_occupation", - models.CharField( - choices=[ - ("business", "Business"), - ("doctor", "Doctor"), - ("farmer", "Farmer"), - ("teacher", "teacher"), - ("public service", "Public Service"), - ("private service", "Private Service"), - ("shopkeeper", "Shopkeeper"), - ("driver", "Driver"), - ("housewife", "HouseWife"), - ("worker", "Worker"), - ("N/A", "N/A"), - ], - default="N/A", - max_length=45, - ), - ), - ( - "mother_occupation", - models.CharField( - choices=[ - ("business", "Business"), - ("doctor", "Doctor"), - ("farmer", "Farmer"), - ("teacher", "teacher"), - ("public service", "Public Service"), - ("private service", "Private Service"), - ("shopkeeper", "Shopkeeper"), - ("driver", "Driver"), - ("housewife", "HouseWife"), - ("worker", "Worker"), - ("N/A", "N/A"), - ], - default="N/A", - max_length=45, - ), - ), - ("guardian_name", models.CharField(max_length=255)), - ( - "guardian_number", - models.CharField( - max_length=10, - validators=[ - django.core.validators.RegexValidator("^\\+?1?\\d{9,15}$") - ], - ), - ), - ( - "relationship_with_student", - models.CharField( - choices=[ - ("father", "Father"), - ("mother", "Mother"), - ("brother", "Brother"), - ("sister", "Sister"), - ("uncle", "Uncle"), - ("aunt", "Aunt"), - ("grandfather", "Grandfather"), - ], - max_length=45, - ), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now_add=True)), - ], - ), - migrations.CreateModel( - name="Teacher", - fields=[ - ("id", models.AutoField(primary_key=True, serialize=False)), - ("first_name", models.CharField(max_length=255)), - ( - "middle_name", - models.CharField(blank=True, max_length=255, null=True), - ), - ("last_name", models.CharField(max_length=255)), - ( - "phone", - models.CharField( - max_length=10, - validators=[ - django.core.validators.RegexValidator("^\\+?1?\\d{9,15}$") - ], - ), - ), - ("email", models.EmailField(max_length=255, unique=True)), - ("dob", models.DateField()), - ( - "gender", - models.CharField( - choices=[("M", "Male"), ("F", "Female")], max_length=1 - ), - ), - ("address", models.CharField(max_length=255)), - ("username", models.CharField(max_length=255)), - ("password", models.CharField(max_length=255)), - ( - "qualification", - models.CharField( - choices=[ - ("BA", "BA"), - ("BBS", "BBS"), - ("BE", "BE"), - ("BEd", "BEd"), - ("BSc", "BSc"), - ("MA", "MA"), - ("MSc", "MSc"), - ("MBS", "MBS"), - ("ME", "ME"), - ("PhD", "PhD"), - ("Others", "Others"), - ], - max_length=45, - ), - ), - ("specialization", models.CharField(max_length=255)), - ( - "marital_status", - models.CharField( - choices=[ - ("married", "Married"), - ("widowed", "Widowed"), - ("separated", "Separated"), - ("divorced", "Divorced"), - ("single", "Single"), - ], - max_length=10, - ), - ), - ("date_of_joining", models.DateField()), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now_add=True)), - ], - ), - ] diff --git a/app/migrations/0002_rename_specialization_teacher_specialization_on_and_more.py b/app/migrations/0002_rename_specialization_teacher_specialization_on_and_more.py deleted file mode 100644 index 8555cb1..0000000 --- a/app/migrations/0002_rename_specialization_teacher_specialization_on_and_more.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 5.1.5 on 2025-01-16 16:55 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("app", "0001_initial"), - ] - - operations = [ - migrations.RenameField( - model_name="teacher", - old_name="specialization", - new_name="specialization_on", - ), - migrations.AlterField( - model_name="teacher", - name="qualification", - field=models.CharField( - choices=[ - ("SLC", "SLC"), - ("Intermediate", "Intermediate"), - ("BA", "BA"), - ("BBS", "BBS"), - ("BE", "BE"), - ("BEd", "BEd"), - ("BSc", "BSc"), - ("MA", "MA"), - ("MSc", "MSc"), - ("MBS", "MBS"), - ("ME", "ME"), - ("PhD", "PhD"), - ("Others", "Others"), - ], - max_length=45, - ), - ), - ] diff --git a/app/migrations/0003_academicyear_course_subject.py b/app/migrations/0003_academicyear_course_subject.py deleted file mode 100644 index 0fe5aa9..0000000 --- a/app/migrations/0003_academicyear_course_subject.py +++ /dev/null @@ -1,64 +0,0 @@ -# Generated by Django 5.1.5 on 2025-01-16 17:12 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("app", "0002_rename_specialization_teacher_specialization_on_and_more"), - ] - - operations = [ - migrations.CreateModel( - name="AcademicYear", - fields=[ - ("id", models.AutoField(primary_key=True, serialize=False)), - ("start_date", models.DateField()), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now_add=True)), - ], - ), - migrations.CreateModel( - name="Course", - fields=[ - ("id", models.AutoField(primary_key=True, serialize=False)), - ("name", models.CharField(max_length=255, unique=True)), - ( - "course_type", - models.CharField( - choices=[ - ("semester", "Semester-based"), - ("year", "Year-based"), - ], - max_length=10, - ), - ), - ( - "duration", - models.PositiveIntegerField(help_text="Duration in months"), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ], - ), - migrations.CreateModel( - name="Subject", - fields=[ - ("id", models.AutoField(primary_key=True, serialize=False)), - ("name", models.CharField(max_length=255, unique=True)), - ("description", models.TextField(blank=True, null=True)), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "course", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="subjects", - to="app.course", - ), - ), - ], - ), - ] diff --git a/app/migrations/0004_remove_subject_description_subject_duration.py b/app/migrations/0004_remove_subject_description_subject_duration.py deleted file mode 100644 index 98e7897..0000000 --- a/app/migrations/0004_remove_subject_description_subject_duration.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 5.1.5 on 2025-01-16 17:15 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("app", "0003_academicyear_course_subject"), - ] - - operations = [ - migrations.RemoveField( - model_name="subject", - name="description", - ), - migrations.AddField( - model_name="subject", - name="duration", - field=models.PositiveIntegerField( - default="1", - help_text="For semester-based courses, specify the semester number. For year-based courses, specify the year.", - ), - preserve_default=False, - ), - ] diff --git a/app/migrations/0005_student_academic_year_student_course.py b/app/migrations/0005_student_academic_year_student_course.py deleted file mode 100644 index 3af3ac7..0000000 --- a/app/migrations/0005_student_academic_year_student_course.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 5.1.5 on 2025-01-16 17:35 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("app", "0004_remove_subject_description_subject_duration"), - ] - - operations = [ - migrations.AddField( - model_name="student", - name="academic_year", - field=models.ForeignKey( - default="1", - on_delete=django.db.models.deletion.CASCADE, - related_name="students", - to="app.academicyear", - ), - preserve_default=False, - ), - migrations.AddField( - model_name="student", - name="course", - field=models.ForeignKey( - default="1", - on_delete=django.db.models.deletion.CASCADE, - related_name="students", - to="app.course", - ), - preserve_default=False, - ), - ] diff --git a/app/migrations/0006_attendance_attendancereport.py b/app/migrations/0006_attendance_attendancereport.py deleted file mode 100644 index c07a907..0000000 --- a/app/migrations/0006_attendance_attendancereport.py +++ /dev/null @@ -1,57 +0,0 @@ -# Generated by Django 5.1.5 on 2025-01-16 17:37 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("app", "0005_student_academic_year_student_course"), - ] - - operations = [ - migrations.CreateModel( - name="Attendance", - fields=[ - ("id", models.AutoField(primary_key=True, serialize=False)), - ("attendance_date", models.DateField()), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "academic_year", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="app.academicyear", - ), - ), - ( - "subject_id", - models.ForeignKey( - on_delete=django.db.models.deletion.DO_NOTHING, to="app.subject" - ), - ), - ], - ), - migrations.CreateModel( - name="AttendanceReport", - fields=[ - ("id", models.AutoField(primary_key=True, serialize=False)), - ("status", models.BooleanField(default=False)), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now_add=True)), - ( - "attendance_id", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="app.attendance" - ), - ), - ( - "student_id", - models.ForeignKey( - on_delete=django.db.models.deletion.DO_NOTHING, to="app.student" - ), - ), - ], - ), - ] diff --git a/app/migrations/0007_leavereportstudent_leavereportteacher.py b/app/migrations/0007_leavereportstudent_leavereportteacher.py deleted file mode 100644 index 0010b39..0000000 --- a/app/migrations/0007_leavereportstudent_leavereportteacher.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 5.1.5 on 2025-01-16 17:44 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("app", "0006_attendance_attendancereport"), - ] - - operations = [ - migrations.CreateModel( - name="LeaveReportStudent", - fields=[ - ("id", models.AutoField(primary_key=True, serialize=False)), - ("leave_date", models.CharField(max_length=255)), - ("leave_message", models.TextField()), - ("leave_status", models.IntegerField(default=0)), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "student_id", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="app.student" - ), - ), - ], - ), - migrations.CreateModel( - name="LeaveReportTeacher", - fields=[ - ("id", models.AutoField(primary_key=True, serialize=False)), - ("leave_date", models.CharField(max_length=255)), - ("leave_message", models.TextField()), - ("leave_status", models.IntegerField(default=0)), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "teacher_id", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="app.teacher" - ), - ), - ], - ), - ] diff --git a/app/migrations/0008_alter_leavereportstudent_leave_date_and_more.py b/app/migrations/0008_alter_leavereportstudent_leave_date_and_more.py deleted file mode 100644 index a749a6e..0000000 --- a/app/migrations/0008_alter_leavereportstudent_leave_date_and_more.py +++ /dev/null @@ -1,56 +0,0 @@ -# Generated by Django 5.1.5 on 2025-01-16 17:46 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("app", "0007_leavereportstudent_leavereportteacher"), - ] - - operations = [ - migrations.AlterField( - model_name="leavereportstudent", - name="leave_date", - field=models.DateField(), - ), - migrations.AlterField( - model_name="leavereportteacher", - name="leave_date", - field=models.DateField(), - ), - migrations.CreateModel( - name="FeedBackStudent", - fields=[ - ("id", models.AutoField(primary_key=True, serialize=False)), - ("feedback", models.TextField()), - ("feedback_reply", models.TextField()), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "student_id", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="app.student" - ), - ), - ], - ), - migrations.CreateModel( - name="FeedBackTeacher", - fields=[ - ("id", models.AutoField(primary_key=True, serialize=False)), - ("feedback", models.TextField()), - ("feedback_reply", models.TextField()), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "staff_id", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="app.teacher" - ), - ), - ], - ), - ] diff --git a/app/migrations/0009_remove_student_password_remove_student_username_and_more.py b/app/migrations/0009_remove_student_password_remove_student_username_and_more.py deleted file mode 100644 index bab96fc..0000000 --- a/app/migrations/0009_remove_student_password_remove_student_username_and_more.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 5.1.5 on 2025-01-18 06:51 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("app", "0008_alter_leavereportstudent_leave_date_and_more"), - ] - - operations = [ - migrations.RemoveField( - model_name="student", - name="password", - ), - migrations.RemoveField( - model_name="student", - name="username", - ), - migrations.RemoveField( - model_name="teacher", - name="password", - ), - migrations.RemoveField( - model_name="teacher", - name="username", - ), - ] diff --git a/app/migrations/0010_alter_feedbackstudent_feedback_reply.py b/app/migrations/0010_alter_feedbackstudent_feedback_reply.py deleted file mode 100644 index 4421ca8..0000000 --- a/app/migrations/0010_alter_feedbackstudent_feedback_reply.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.1.5 on 2025-01-18 07:59 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("app", "0009_remove_student_password_remove_student_username_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="feedbackstudent", - name="feedback_reply", - field=models.TextField(blank=True, null=True), - ), - ] diff --git a/app/migrations/0011_rename_staff_id_feedbackteacher_teacher_id_and_more.py b/app/migrations/0011_rename_staff_id_feedbackteacher_teacher_id_and_more.py deleted file mode 100644 index 667d5b0..0000000 --- a/app/migrations/0011_rename_staff_id_feedbackteacher_teacher_id_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.1.5 on 2025-01-18 09:04 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("app", "0010_alter_feedbackstudent_feedback_reply"), - ] - - operations = [ - migrations.RenameField( - model_name="feedbackteacher", - old_name="staff_id", - new_name="teacher_id", - ), - migrations.AlterField( - model_name="feedbackteacher", - name="feedback_reply", - field=models.TextField(blank=True, null=True), - ), - ] diff --git a/app/migrations/0012_rename_academic_year_attendance_academic_year_id.py b/app/migrations/0012_rename_academic_year_attendance_academic_year_id.py deleted file mode 100644 index e322820..0000000 --- a/app/migrations/0012_rename_academic_year_attendance_academic_year_id.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.1.5 on 2025-01-18 09:48 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("app", "0011_rename_staff_id_feedbackteacher_teacher_id_and_more"), - ] - - operations = [ - migrations.RenameField( - model_name="attendance", - old_name="academic_year", - new_name="academic_year_id", - ), - ] diff --git a/app/migrations/0013_student_fcm_token_student_password_student_user_and_more.py b/app/migrations/0013_student_fcm_token_student_password_student_user_and_more.py deleted file mode 100644 index d44d748..0000000 --- a/app/migrations/0013_student_fcm_token_student_password_student_user_and_more.py +++ /dev/null @@ -1,72 +0,0 @@ -# Generated by Django 5.1.5 on 2025-01-18 10:24 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("app", "0012_rename_academic_year_attendance_academic_year_id"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name="student", - name="fcm_token", - field=models.TextField(default=""), - ), - migrations.AddField( - model_name="student", - name="password", - field=models.CharField(default="1", max_length=255), - preserve_default=False, - ), - migrations.AddField( - model_name="student", - name="user", - field=models.OneToOneField( - default="1", - on_delete=django.db.models.deletion.CASCADE, - related_name="student", - to=settings.AUTH_USER_MODEL, - ), - preserve_default=False, - ), - migrations.AddField( - model_name="student", - name="username", - field=models.CharField(default="1", max_length=255), - preserve_default=False, - ), - migrations.AddField( - model_name="teacher", - name="fcm_token", - field=models.TextField(default=""), - ), - migrations.AddField( - model_name="teacher", - name="password", - field=models.CharField(default="1", max_length=255), - preserve_default=False, - ), - migrations.AddField( - model_name="teacher", - name="user", - field=models.OneToOneField( - default="1", - on_delete=django.db.models.deletion.CASCADE, - related_name="teacher", - to=settings.AUTH_USER_MODEL, - ), - preserve_default=False, - ), - migrations.AddField( - model_name="teacher", - name="username", - field=models.CharField(default="1", max_length=255), - preserve_default=False, - ), - ] diff --git a/app/migrations/0014_remove_student_user_remove_student_username_and_more.py b/app/migrations/0014_remove_student_user_remove_student_username_and_more.py deleted file mode 100644 index b357d8a..0000000 --- a/app/migrations/0014_remove_student_user_remove_student_username_and_more.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 5.1.5 on 2025-01-18 10:39 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("app", "0013_student_fcm_token_student_password_student_user_and_more"), - ] - - operations = [ - migrations.RemoveField( - model_name="student", - name="user", - ), - migrations.RemoveField( - model_name="student", - name="username", - ), - migrations.RemoveField( - model_name="teacher", - name="user", - ), - migrations.RemoveField( - model_name="teacher", - name="username", - ), - ] diff --git a/app/models.py b/app/models.py index 75fc506..2464b52 100644 --- a/app/models.py +++ b/app/models.py @@ -1,233 +1,763 @@ -from django.db import models -from django.core.validators import RegexValidator, ValidationError -from django.contrib.auth.models import User -# Create your models here. +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 +# Constants +DURATION_TYPES = ( + ("Year", "Year Based"), + ("Semester", "Semester Based"), +) -class Student(models.Model): - id = models.AutoField(primary_key=True) - first_name = models.CharField(max_length=255) - middle_name = models.CharField(max_length=255, blank=True, null=True) - last_name = models.CharField(max_length=255) - course = models.ForeignKey( - "Course", on_delete=models.CASCADE, related_name="students" - ) - academic_year = models.ForeignKey( - "AcademicYear", on_delete=models.CASCADE, related_name="students" - ) - password = models.CharField(max_length=255) - fcm_token = models.TextField(default="") - phone = models.CharField( - max_length=10, validators=[RegexValidator(r"^\+?1?\d{9,15}$")] - ) - email = models.EmailField(max_length=255, unique=True) - dob = models.DateField() - gender = models.CharField(max_length=1, choices=[("M", "Male"), ("F", "Female")]) - address = models.CharField(max_length=255) - father_name = models.CharField(max_length=255) - mother_name = models.CharField(max_length=255) - parents_number = models.CharField( - max_length=10, validators=[RegexValidator(r"^\+?1?\d{9,15}$")] - ) - occupation_choice = ( - ("business", "Business"), - ("doctor", "Doctor"), - ("farmer", "Farmer"), - ("teacher", "teacher"), - ("public service", "Public Service"), - ("private service", "Private Service"), - ("shopkeeper", "Shopkeeper"), - ("driver", "Driver"), - ("housewife", "HouseWife"), - ("worker", "Worker"), - ("N/A", "N/A"), - ) - father_occupation = models.CharField( - choices=occupation_choice, max_length=45, default="N/A" - ) - mother_occupation = models.CharField( - choices=occupation_choice, max_length=45, default="N/A" - ) - guardian_name = models.CharField(max_length=255) - guardian_number = models.CharField( - max_length=10, validators=[RegexValidator(r"^\+?1?\d{9,15}$")] - ) - relationship_choice = ( - ("father", "Father"), - ("mother", "Mother"), - ("brother", "Brother"), - ("sister", "Sister"), - ("uncle", "Uncle"), - ("aunt", "Aunt"), - ("grandfather", "Grandfather"), - ) - relationship_with_student = models.CharField( - choices=relationship_choice, max_length=45 - ) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now_add=True) +STUDENT_STATUS_CHOICES = ( + ("Active", "Active"), + ("Leave", "Leave"), + ("Completed", "Completed"), +) +GENDER_CHOICES = ( + ("Male", "Male"), + ("Female", "Female"), + ("Other", "Other"), +) -class Teacher(models.Model): - id = models.AutoField(primary_key=True) - first_name = models.CharField(max_length=255) - middle_name = models.CharField(max_length=255, blank=True, null=True) - last_name = models.CharField(max_length=255) - phone = models.CharField( - max_length=10, validators=[RegexValidator(r"^\+?1?\d{9,15}$")] - ) - email = models.EmailField(max_length=255, unique=True) - dob = models.DateField() - gender = models.CharField(max_length=1, choices=[("M", "Male"), ("F", "Female")]) - address = models.CharField(max_length=255) - fcm_token = models.TextField(default="") - password = models.CharField(max_length=255) - qualification_choice = ( - ("SLC", "SLC"), - ("Intermediate", "Intermediate"), - ("BA", "BA"), - ("BBS", "BBS"), - ("BE", "BE"), - ("BEd", "BEd"), - ("BSc", "BSc"), - ("MA", "MA"), - ("MSc", "MSc"), - ("MBS", "MBS"), - ("ME", "ME"), - ("PhD", "PhD"), - ("Others", "Others"), - ) - qualification = models.CharField(choices=qualification_choice, max_length=45) - specialization_on = models.CharField(max_length=255) - marital_status_choice = ( - ("married", "Married"), - ("widowed", "Widowed"), - ("separated", "Separated"), - ("divorced", "Divorced"), - ("single", "Single"), - ) - marital_status = models.CharField(choices=marital_status_choice, max_length=10) - date_of_joining = models.DateField() - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now_add=True) +MARITAL_STATUS_CHOICES = ( + ("Married", "Married"), + ("Unmarried", "Unmarried"), + ("Divorced", "Divorced"), +) + +LEAVE_STATUS_CHOICES = ( + (0, "Pending"), + (1, "Approved"), + (2, "Rejected"), +) + +FEEDBACK_TYPE_CHOICES = ( + ("General", "General"), + ("Facilities", "Facilities"), + ("Teaching", "Teaching"), + ("Infrastructure", "Infrastructure"), + ("Administration", "Administration"), +) + +PROGRESS_STATUS_CHOICES = ( + ("Not Started", "Not Started"), + ("In Progress", "In Progress"), + ("Completed", "Completed"), + ("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) + email = models.CharField(max_length=255, null=True, blank=True) + address = models.CharField(max_length=255, null=True, blank=True) + pan_no = models.CharField(max_length=255, null=True, blank=True) + reg_no = models.CharField(max_length=255, null=True, blank=True) + logo = models.ImageField(upload_to="image", null=True, blank=True) + description = models.TextField(null=True, blank=True) def __str__(self): - return self.first_name + "" + self.middle_name + "" + self.last_name + return self.name + class Meta: + verbose_name = "Institute" + verbose_name_plural = "Institutes" -class AcademicYear(models.Model): - id = models.AutoField(primary_key=True) - start_date = models.DateField() - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now_add=True) +# 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) def __str__(self): - return str(self.start_date) + return self.name + class Meta: + verbose_name = "Batch" + verbose_name_plural = "Batches" +# Course Related Models class Course(models.Model): - SEMESTER = "semester" - YEAR = "year" - - COURSE_TYPE_CHOICES = [ - (SEMESTER, "Semester-based"), - (YEAR, "Year-based"), - ] - - id = models.AutoField(primary_key=True) - name = models.CharField(max_length=255, unique=True) - course_type = models.CharField(max_length=10, choices=COURSE_TYPE_CHOICES) - duration = models.PositiveIntegerField(help_text="Duration in months") - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) + """Model representing a course offered by the institute""" + id = models.BigAutoField(primary_key=True) + name = models.CharField(max_length=500) + duration = models.IntegerField(default=1, null=True, blank=True) + duration_type = models.CharField(max_length=255, choices=DURATION_TYPES, null=True, blank=True) def __str__(self): - return f"{self.name} ({self.get_course_type_display()})" + return self.name + class Meta: + 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, unique=True) - course = models.ForeignKey( - Course, on_delete=models.CASCADE, related_name="subjects" + name = models.CharField(max_length=255) + course = models.ForeignKey(Course, on_delete=models.CASCADE) + semester_or_year = models.PositiveIntegerField() + syllabus_pdf = models.FileField( + upload_to="subject_pdfs/", + null=True, + blank=True, + help_text="Upload syllabus or study material PDF", ) - duration = models.PositiveIntegerField( - help_text="For semester-based courses, specify the semester number. For year-based courses, specify the year." + + def __str__(self): + return f"{self.name} ({self.course.name} - {'Semester' if self.course.duration_type == 'Semester' else 'Year'} {self.semester_or_year})" + + def clean(self): + if self.course.duration_type == "Semester": + max_semesters = self.course.duration * 2 + if self.semester_or_year > max_semesters: + raise ValidationError(f"Semester cannot exceed {max_semesters}") + else: # Year based + if self.semester_or_year > self.course.duration: + raise ValidationError(f"Year cannot exceed {self.course.duration}") + + def save(self, *args, **kwargs): + self.clean() + super().save(*args, **kwargs) + + def get_pdf_url(self): + return self.syllabus_pdf.url if self.syllabus_pdf else None + + def get_all_files(self): + """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') + 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 + }) + + return files + + class Meta: + unique_together = ["name", "course", "semester_or_year"] + ordering = ["course", "semester_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_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.title} - {self.subject.name}" + + class Meta: + ordering = ['-uploaded_at'] + +# User Models +class Student(AbstractUser): + """Model representing a student in the institute""" + 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 student.", + related_name="student_set", + related_query_name="student", ) + + groups = models.ManyToManyField( + "auth.Group", + verbose_name="groups", + blank=True, + help_text="The groups this student belongs to.", + related_name="students", + ) + + 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) + 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) + 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) + image = models.ImageField(upload_to="student_image", null=True, blank=True) + course = models.ForeignKey(Course, on_delete=models.SET_NULL, null=True, blank=True, related_name="students") + current_semester = models.PositiveIntegerField(default=1, help_text="Current semester for semester-based courses") + joining_date = models.DateField(null=True, blank=True) + password = models.CharField(max_length=128, editable=False, null=True) + fcm_token = models.CharField(max_length=500, 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 self.groups.exists() + + def has_perm(self, perm, obj=None): + if not self.is_active: + return False + + 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 + + for group in self.groups.all(): + if group.permissions.filter(content_type__app_label=app_label).exists(): + return True + return False + + def get_current_subjects(self): + """Get subjects for current semester""" + if not self.course: + return Subject.objects.none() + return Subject.objects.filter( + course=self.course, + semester_or_year=self.current_semester + ) + + def advance_semester(self): + """Advance to next semester if possible""" + if not self.course: + return False + + if self.course.duration_type == "Semester": + total_semesters = self.course.duration * 2 + if self.current_semester < total_semesters: + self.current_semester += 1 + self.save() + return True + return False + + 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( + "auth.Permission", + verbose_name="user permissions", + blank=True, + help_text="Specific permissions for this staff.", + related_name="staff_set", + related_query_name="staff", + ) + + groups = models.ManyToManyField( + "auth.Group", + verbose_name="groups", + blank=True, + help_text="The groups this staff belongs to.", + related_name="staffs", + ) + + id = models.BigAutoField(primary_key=True) + 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) + 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) + parent_name = models.CharField(max_length=255, null=True, blank=True) + parent_phone = models.CharField(max_length=255, null=True, blank=True) + citizenship_no = models.CharField(max_length=255, blank=True, null=True) + passport = models.CharField(max_length=255, blank=True, null=True) + image = models.ImageField(upload_to="staff_image", null=True, blank=True) + joining_date = models.DateField(null=True, blank=True) + password = models.CharField(max_length=128, editable=False, null=True) + fcm_token = models.CharField(max_length=500, 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 True + + @property + def is_authenticated(self): + return True + + def has_perm(self, perm, obj=None): + if not self.is_active: + return False + + 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 + + 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 = "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() + is_active = models.BooleanField(default=True) + + def __str__(self): + return f"{self.subject.name} - {self.teacher.name} ({self.start_time} - {self.end_time})" + + class Meta: + ordering = ["start_time"] + unique_together = [ + "course", + "subject", + "teacher", + "start_time", + "end_time", + "semester_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) + teacher_attend = models.BooleanField(default=True) + class_status = models.BooleanField(default=True) + + def __str__(self): + return str(self.date) + + class Meta: + 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") + 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) + 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): + """Model representing staff leave requests""" + staff = models.ForeignKey(Staff, on_delete=models.CASCADE) + start_date = models.DateField() + end_date = models.DateField() + message = models.TextField() + status = models.IntegerField(choices=LEAVE_STATUS_CHOICES, default=0) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): - return f"{self.name} ({self.course.name}, Duration: {self.duration})" + return f"{self.staff.name} - {self.start_date} to {self.end_date}" + + class Meta: + verbose_name = "Staff Leave" + verbose_name_plural = "Staff Leaves" + ordering = ["-created_at"] + +class Student_Leave(models.Model): + """Model representing student leave requests""" + student = models.ForeignKey(Student, on_delete=models.CASCADE) + start_date = models.DateField() + end_date = models.DateField() + message = models.TextField() + status = models.IntegerField(choices=LEAVE_STATUS_CHOICES, default=0) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"{self.student.name} - {self.start_date} to {self.end_date}" + + class Meta: + verbose_name = "Student Leave" + 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)]) + 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.student.name} - {self.teacher.name}" + + class Meta: + verbose_name = "Student Feedback" + verbose_name_plural = "Student Feedbacks" + ordering = ["-created_at"] + unique_together = ["student", "teacher"] + +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)]) + 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.user.name} - {self.institute.name} - {self.feedback_type}" + + class Meta: + verbose_name = "Student Institute Feedback" + verbose_name_plural = "Student Institute Feedbacks" + ordering = ["-created_at"] + unique_together = ["user", "institute", "feedback_type"] + + @property + 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)]) + feedback_text = models.TextField() + is_anonymous = models.BooleanField(default=False) + is_public = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + if self.is_anonymous: + return f"Anonymous - {self.institute.name} - {self.feedback_type}" + return f"{self.staff.name} - {self.institute.name} - {self.feedback_type}" + + class Meta: + verbose_name = "Staff Institute Feedback" + verbose_name_plural = "Staff Institute Feedbacks" + ordering = ["-created_at"] + unique_together = ["staff", "institute", "feedback_type"] + + @property + def display_name(self): + return "Anonymous" if self.is_anonymous else self.staff.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") + 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) + notes = models.TextField(blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"{self.student.name} - {self.course.name}" def clean(self): - if ( - self.course.course_type == Course.SEMESTER - and self.duration > self.course.duration - ): - raise ValidationError( - f"Duration (semester) must not exceed the course's total semesters ({self.course.duration})." - ) - if ( - self.course.course_type == Course.YEAR - and self.duration > self.course.duration - ): - raise ValidationError( - f"Duration (year) must not exceed the course's total years ({self.course.duration})." - ) + if not self.student or not self.course: + raise ValidationError("Both student and course are required") + if self.start_date and self.expected_end_date and self.start_date > self.expected_end_date: + raise ValidationError("Start date cannot be after expected end date") -class Attendance(models.Model): # Subject Attendance - id = models.AutoField(primary_key=True) - subject_id = models.ForeignKey(Subject, on_delete=models.DO_NOTHING) - attendance_date = models.DateField() - academic_year_id = models.ForeignKey(AcademicYear, on_delete=models.CASCADE) + if self.actual_end_date and self.start_date and self.actual_end_date < self.start_date: + raise ValidationError("Actual end date cannot be before start date") + + if self.semester_start_date and self.semester_end_date and self.semester_start_date > self.semester_end_date: + raise ValidationError("Semester start date cannot be after semester end date") + + if self.current_semester < 1: + raise ValidationError("Current semester must be at least 1") + + if self.course.duration_type == "Semester": + max_semesters = self.course.duration * 2 + if self.current_semester > max_semesters: + raise ValidationError(f"Current semester cannot exceed {max_semesters}") + + def calculate_completion_percentage(self): + if not self.start_date or not self.expected_end_date: + return 0 + + today = date.today() + + if self.progress_status == "Completed": + return 100 + elif self.progress_status == "Dropped": + return 0 + + total_duration = (self.expected_end_date - self.start_date).days + if total_duration <= 0: + return 0 + + elapsed_days = (today - self.start_date).days + if elapsed_days <= 0: + 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 + + if self.semester_end_date and today > self.semester_end_date: + semester_progress = (self.current_semester / total_semesters) * 100 + + return min(100, int(semester_progress)) + else: # Yearly based + max_days = 365 + if elapsed_days >= max_days: + return 100 + + percentage = min(100, int((elapsed_days / total_duration) * 100)) + return percentage + + def get_completion_percentage(self): + return self.completion_percentage + + def update_completion_percentage(self): + new_percentage = self.calculate_completion_percentage() + if new_percentage != self.completion_percentage: + self.completion_percentage = new_percentage + self.save(update_fields=['completion_percentage']) + return new_percentage + + def force_update_percentage(self): + return self.update_completion_percentage() + + def save(self, *args, **kwargs): + if not self.pk: + self.start_date = self.start_date or date.today() + + if self.course.duration_type == "Year": + years = self.course.duration + else: # Semester based + years = self.course.duration + + 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 + + self.clean() + self.completion_percentage = self.calculate_completion_percentage() + + if self.completion_percentage >= 100: + self.progress_status = "Completed" + if not self.actual_end_date: + self.actual_end_date = date.today() + elif self.completion_percentage > 0: + self.progress_status = "In Progress" + + try: + super().save(*args, **kwargs) + except Exception as e: + print(f"Error saving course tracking for {self.student.name}: {str(e)}") + raise + + def advance_semester(self): + 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 + self.save() + return True + return False + + @property + def is_completed(self): + return self.progress_status == "Completed" + + @property + def is_active(self): + return self.progress_status in ["Not Started", "In Progress"] + + @property + def remaining_days(self): + if not self.semester_end_date: + return 0 + return max(0, (self.semester_end_date - date.today()).days) + + @property + def total_remaining_days(self): + if not self.expected_end_date: + return 0 + return max(0, (self.expected_end_date - date.today()).days) + + @property + def duration_type(self): + return self.course.duration_type if self.course else None + + @property + def total_duration_days(self): + if not self.start_date or not self.expected_end_date: + return 0 + return (self.expected_end_date - self.start_date).days + + 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) - updated_at = models.DateTimeField(auto_now=True) + expires_at = models.DateTimeField() + class Meta: + indexes = [ + models.Index(fields=['identifier']), + models.Index(fields=['expires_at']), + ] -class AttendanceReport(models.Model): # Individual Student Attendance - id = models.AutoField(primary_key=True) - student_id = models.ForeignKey(Student, on_delete=models.DO_NOTHING) - attendance_id = models.ForeignKey(Attendance, on_delete=models.CASCADE) - status = models.BooleanField(default=False) + 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) - updated_at = models.DateTimeField(auto_now_add=True) + expires_at = models.DateTimeField() + class Meta: + indexes = [ + models.Index(fields=['token']), + models.Index(fields=['expires_at']), + ] -class LeaveReportStudent(models.Model): - id = models.AutoField(primary_key=True) - student_id = models.ForeignKey(Student, on_delete=models.CASCADE) - leave_date = models.DateField() - leave_message = models.TextField() - leave_status = models.IntegerField(default=0) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - -class LeaveReportTeacher(models.Model): - id = models.AutoField(primary_key=True) - teacher_id = models.ForeignKey(Teacher, on_delete=models.CASCADE) - leave_date = models.DateField() - leave_message = models.TextField() - leave_status = models.IntegerField(default=0) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - -class FeedbackStudent(models.Model): - id = models.AutoField(primary_key=True) - student_id = models.ForeignKey(Student, on_delete=models.CASCADE) - feedback = models.TextField() - feedback_reply = models.TextField(blank=True, null=True) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - -class FeedbackTeacher(models.Model): - id = models.AutoField(primary_key=True) - teacher_id = models.ForeignKey(Teacher, on_delete=models.CASCADE) - feedback = models.TextField() - feedback_reply = models.TextField(blank=True, null=True) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) + def is_expired(self): + return timezone.now() > self.expires_at diff --git a/app/serializers.py b/app/serializers.py new file mode 100644 index 0000000..90e034c --- /dev/null +++ b/app/serializers.py @@ -0,0 +1,190 @@ +import django.db +from rest_framework import serializers +from .models import ( + Attendance, AttendanceRecord, Batch, Course, FCMDevice, Institute, + Notice, Routine, Staff, Student, Subject, Staff_leave, Student_Leave, + StudentFeedback, InstituteFeedback, StaffInstituteFeedback, CourseTracking, + TOTPSecret, ResetToken +) + +class FCMDeviceSerializer(serializers.ModelSerializer): + class Meta: + model = FCMDevice + fields = ['id', 'token'] + +class InstituteSerializer(serializers.ModelSerializer): + class Meta: + model = Institute + fields = ['id', 'name', 'phone', 'email', 'address', 'pan_no', 'reg_no', 'logo', 'description'] + +class SubjectSerializer(serializers.ModelSerializer): + syllabus_pdf_url = serializers.SerializerMethodField() + + class Meta: + model = Subject + fields = ['id', 'name', 'course', 'semester_or_year', 'syllabus_pdf', 'syllabus_pdf_url'] + + def get_syllabus_pdf_url(self, obj): + return obj.get_pdf_url() + +class CourseSerializer(serializers.ModelSerializer): + subjects = SubjectSerializer(many=True, read_only=True) + + class Meta: + model = Course + fields = ['id', 'name', 'duration', 'duration_type', 'subjects'] + +class BatchSerializer(serializers.ModelSerializer): + class Meta: + model = Batch + fields = ['id', 'name', 'start_date', 'end_date'] + +class StudentSerializer(serializers.ModelSerializer): + batches = BatchSerializer(many=True, read_only=True) + course = CourseSerializer(read_only=True) + + class Meta: + model = Student + fields = [ + 'id', 'name', 'status', 'gender', 'birth_date', 'email', 'phone', + 'temporary_address', 'permanent_address', 'marital_status', 'parent_name', + 'parent_phone', 'citizenship_no', 'image', 'batches', 'course', + 'current_semester', 'joining_date' + ] + read_only_fields = ['id'] + +class StaffSerializer(serializers.ModelSerializer): + class Meta: + model = Staff + fields = [ + 'id', 'name', 'gender', 'designation', 'birth_date', 'phone', 'email', + 'temporary_address', 'permanent_address', 'marital_status', 'parent_name', + 'parent_phone', 'citizenship_no', 'passport', 'image', 'joining_date', + 'is_active' + ] + read_only_fields = ['id'] + +class RoutineSerializer(serializers.ModelSerializer): + teacher = StaffSerializer(read_only=True) + subject = SubjectSerializer(read_only=True) + course = CourseSerializer(read_only=True) + + class Meta: + model = Routine + fields = [ + 'id', 'course', 'subject', 'teacher', 'start_time', 'end_time', + 'semester_or_year', 'is_active' + ] + +class AttendanceRecordSerializer(serializers.ModelSerializer): + student = StudentSerializer(read_only=True) + + class Meta: + model = AttendanceRecord + fields = ['id', 'attendance', 'student', 'student_attend'] + +class AttendanceSerializer(serializers.ModelSerializer): + records = AttendanceRecordSerializer(many=True, read_only=True) + routine = RoutineSerializer(read_only=True) + teacher = StaffSerializer(read_only=True) + + class Meta: + model = Attendance + fields = ['id', 'date', 'routine', 'teacher', 'teacher_attend', 'class_status', 'records'] + +class NoticeSerializer(serializers.ModelSerializer): + created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True) + image_url = serializers.SerializerMethodField() + + class Meta: + model = Notice + fields = ['id', 'title', 'message', 'image', 'image_url', 'created_at'] + read_only_fields = ['created_at'] + + def get_image_url(self, obj): + if obj.image: + return obj.image.url + return None + +class StaffLeaveSerializer(serializers.ModelSerializer): + staff = StaffSerializer(read_only=True) + created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True) + updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True) + + class Meta: + model = Staff_leave + fields = ['id', 'staff', '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) + created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True) + updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True) + + class Meta: + model = Student_Leave + fields = ['id', 'student', '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) + teacher = StaffSerializer(read_only=True) + created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True) + updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True) + + class Meta: + model = StudentFeedback + fields = ['id', 'student', 'teacher', 'rating', 'feedback_text', 'created_at', 'updated_at'] + read_only_fields = ['created_at', 'updated_at'] + +class InstituteFeedbackSerializer(serializers.ModelSerializer): + user = StudentSerializer(read_only=True) + created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True) + updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True) + + class Meta: + model = InstituteFeedback + fields = [ + 'id', 'institute', 'user', 'feedback_type', 'rating', 'feedback_text', + 'is_anonymous', 'is_public', 'created_at', 'updated_at' + ] + read_only_fields = ['created_at', 'updated_at'] + +class StaffInstituteFeedbackSerializer(serializers.ModelSerializer): + staff = StaffSerializer(read_only=True) + created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True) + updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True) + + class Meta: + model = StaffInstituteFeedback + fields = [ + 'id', 'institute', 'staff', 'feedback_type', 'rating', 'feedback_text', + 'is_anonymous', 'is_public', 'created_at', 'updated_at' + ] + read_only_fields = ['created_at', 'updated_at'] + +class CourseTrackingSerializer(serializers.ModelSerializer): + student = StudentSerializer(read_only=True) + course = CourseSerializer(read_only=True) + created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True) + updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True) + + class Meta: + model = CourseTracking + fields = [ + 'id', 'student', 'course', 'enrollment_date', 'start_date', 'expected_end_date', + 'actual_end_date', 'progress_status', 'completion_percentage', 'current_semester', + 'semester_start_date', 'semester_end_date', 'notes', 'created_at', 'updated_at' + ] + read_only_fields = ['created_at', 'updated_at', 'enrollment_date'] + +class TOTPSecretSerializer(serializers.ModelSerializer): + class Meta: + model = TOTPSecret + fields = ['id', 'identifier', 'created_at', 'expires_at'] + +class ResetTokenSerializer(serializers.ModelSerializer): + class Meta: + model = ResetToken + fields = ['id', 'token', 'identifier', 'created_at', 'expires_at'] + diff --git a/app/signals.py b/app/signals.py new file mode 100644 index 0000000..a1a93be --- /dev/null +++ b/app/signals.py @@ -0,0 +1,111 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.core.cache import cache +from datetime import date, timedelta +from .models import CourseTracking, Student +from django.db import transaction + +@receiver(post_save, sender=CourseTracking) +def update_completion_percentage(sender, instance, **kwargs): + """ + Signal to update completion percentage after save + Also caches the percentage for 1 hour to avoid frequent recalculations + """ + cache_key = f"course_tracking_percentage_{instance.id}" + cache.set(cache_key, instance.completion_percentage, 3600) # Cache for 1 hour + + +@receiver(post_save, sender=Student) +def create_course_tracking(sender, instance, created, **kwargs): + """ + Signal handler to create CourseTracking when a new student is created + or when a course is assigned to an existing student + """ + print(f"Signal triggered for student: {instance.name}") + print(f"Created: {created}") + print(f"Course: {instance.course}") + print(f"Current Semester: {instance.current_semester}") + + # Check if student has a course assigned + if instance.course: + try: + # Check if course tracking already exists + existing_tracking = CourseTracking.objects.filter( + student=instance, course=instance.course + ).first() + + if existing_tracking: + print(f"Course tracking already exists for {instance.name}") + return + + # Calculate dates based on course duration + start_date = instance.joining_date or date.today() + + # Calculate expected end date based on course duration type + if instance.course.duration_type == "Year": + # For year-based courses, each year is 365 days + years = instance.course.duration + duration_days = years * 365 + else: # Semester based + # For semester-based courses: + # - Each year has 2 semesters + # - Total years is the same as course duration + years = instance.course.duration + duration_days = years * 365 # Use full years for total duration + + expected_end_date = start_date + timedelta(days=duration_days) + + # Create course tracking with transaction + with transaction.atomic(): + course_tracking = CourseTracking.objects.create( + student=instance, + course=instance.course, + start_date=start_date, + expected_end_date=expected_end_date, + current_semester=instance.current_semester, + semester_start_date=start_date, + semester_end_date=start_date + + timedelta(days=180), # 6 months for first semester + ) + + # Force initial percentage calculation + course_tracking.force_update_percentage() + + print( + f"Successfully created course tracking for student {instance.name}" + ) + print(f"Start Date: {start_date}") + print(f"Expected End Date: {expected_end_date}") + print(f"Current Semester: {instance.current_semester}") + + except Exception as e: + print( + f"Error creating course tracking for student {instance.name}: {str(e)}" + ) + # Log the error but don't prevent student creation + # You might want to add proper logging here instead of print statements + else: + print(f"No course assigned to student {instance.name}") + + +@receiver(post_save, sender=Student) +def sync_student_semester(sender, instance, created, **kwargs): + """Signal handler to sync student's current_semester with course tracking""" + if instance.course: + try: + course_tracking = CourseTracking.objects.get( + student=instance, course=instance.course + ) + if course_tracking.current_semester != instance.current_semester: + course_tracking.current_semester = instance.current_semester + course_tracking.save() + except CourseTracking.DoesNotExist: + pass + + +@receiver(post_save, sender=CourseTracking) +def sync_course_tracking_semester(sender, instance, created, **kwargs): + """Signal handler to sync course tracking's current_semester with student""" + if instance.student.current_semester != instance.current_semester: + instance.student.current_semester = instance.current_semester + instance.student.save() \ No newline at end of file diff --git a/app/staffviews.py b/app/staffviews.py new file mode 100644 index 0000000..3032c5d --- /dev/null +++ b/app/staffviews.py @@ -0,0 +1,1024 @@ +from django.shortcuts import render, redirect, get_object_or_404 +from django.http import JsonResponse, HttpResponse +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.db.models import Count, Q, Avg +from django.utils import timezone +from datetime import timedelta, 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 ( + Staff, + Student, + Routine, + Attendance, + AttendanceRecord, + StudentFeedback, + Staff_leave, + Notice, + Institute, + StaffInstituteFeedback, + Subject, + SubjectFile, + InstituteFeedback, + CourseTracking, + FEEDBACK_TYPE_CHOICES +) +from app.admin import custom_admin_site + + +@login_required +def teacherDashboard(request): + """View to display the teacher dashboard with their classes and student information""" + teacher = Staff.objects.filter(phone=request.user.phone).first() + if not teacher: + 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) + + # Get only the active routines assigned to this teacher + today_routines = Routine.objects.filter( + teacher=teacher, # Filter by the current teacher + is_active=True, # Only include active routines + ).order_by("start_time") + + # Process routines to include status and attendance + processed_routines = [] + for routine in today_routines: + attendance = routine.attendances.filter(date=today).first() + + # Get total students from active course trackings + total_students = CourseTracking.objects.filter( + course=routine.subject.course, + current_semester=routine.semester_or_year, + progress_status="In Progress" + ).count() + + attendance_count = ( + 0 + if not attendance + else AttendanceRecord.objects.filter( + attendance=attendance, student_attend=True + ).count() + ) + + # Add routine to processed_routines + processed_routines.append( + { + "id": routine.id, + "name": routine.subject.name, + "start_time": routine.start_time, + "end_time": routine.end_time, + "is_completed": attendance is not None, + "is_ongoing": routine.start_time <= current_time <= routine.end_time, + "total_students": total_students, + "attendance_count": attendance_count, + "semester_or_year": routine.semester_or_year, + "course_name": routine.subject.course.name, + "has_attendance": attendance is not None, + "subject_id": routine.subject.id, + } + ) + + # Calculate class statistics + total_classes = today_routines.count() + completed_classes = len([r for r in processed_routines if r["is_completed"]]) + remaining_classes = total_classes - completed_classes + + # Get student statistics 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() + + # Get present and absent students for today + present_students = AttendanceRecord.objects.filter( + attendance__date=today, + student_attend=True, + attendance__routine__teacher=teacher + ).values('student').distinct().count() + + # Calculate absent students based on teacher's classes today + today_class_students = set() + for routine in today_routines: + students = CourseTracking.objects.filter( + course=routine.subject.course, + current_semester=routine.semester_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 + + # Get average rating from student feedback + avg_rating = StudentFeedback.objects.filter( + teacher=teacher + ).aggregate(Avg('rating'))['rating__avg'] or 0 + avg_rating_rounded = round(avg_rating) + remaining_stars = 5 - avg_rating_rounded + + # Get leave statistics + pending_leaves = Staff_leave.objects.filter(staff=teacher, status=0).count() + approved_leaves = Staff_leave.objects.filter(staff=teacher, status=1).count() + rejected_leaves = Staff_leave.objects.filter(staff=teacher, status=2).count() + + # Get recent leaves + recent_leaves = Staff_leave.objects.filter(staff=teacher).order_by('-created_at')[:5] + + # Get recent feedback + recent_feedback = StudentFeedback.objects.filter(teacher=teacher).order_by('-created_at')[:5] + + # Get notices + notices = Notice.objects.all().order_by('-created_at')[:5] + + # Get feedback types for institute feedback + feedback_types = FEEDBACK_TYPE_CHOICES + + # Get institute feedback + institute_feedback = StaffInstituteFeedback.objects.filter(staff=teacher).order_by('-created_at')[:5] + + # Get all routines for the teacher (for the schedule) + all_routines = Routine.objects.filter( + teacher=teacher, + is_active=True, + semester_or_year__in=CourseTracking.objects.filter( + progress_status="In Progress" + ).values_list('current_semester', flat=True).distinct() + ).order_by('start_time') + + # Get all attendance records for quick access + recent_attendance = ( + Attendance.objects.filter(teacher=teacher, date__gte=last_week) + .select_related("routine") + .order_by("-date") + ) + + # Add present/total counts to each attendance record + for attendance in recent_attendance: + attendance.present_count = AttendanceRecord.objects.filter( + attendance=attendance, student_attend=True + ).count() + attendance.total_count = AttendanceRecord.objects.filter( + attendance=attendance + ).count() + + # Attendance Management Data + routines = Routine.objects.filter( + teacher=teacher, + is_active=True + ).order_by('start_time') + + # 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') + + # 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" + ).count() + + context = { + "teacher": teacher, + "title": "Teacher Dashboard", + "today_routines": processed_routines, + "total_classes": total_classes, + "completed_classes": completed_classes, + "remaining_classes": remaining_classes, + "total_students": total_students, + "present_students": present_students, + "absent_students": absent_students, + "avg_rating": avg_rating, + "avg_rating_rounded": avg_rating_rounded, + "remaining_stars": remaining_stars, + "pending_leaves": pending_leaves, + "approved_leaves": approved_leaves, + "rejected_leaves": rejected_leaves, + "recent_leaves": recent_leaves, + "recent_feedback": recent_feedback, + "notices": notices, + "feedback_types": feedback_types, + "institute_feedback": institute_feedback, + "all_routines": all_routines, + "recent_attendance": recent_attendance, + "routines": routines, + "today": today, + "teacher_subjects": teacher_subjects, + } + + return render(request, "teacher_dashboard.html", context) + + +@login_required +def request_staff_leave(request): + """View to handle leave requests from staff""" + if request.method == "POST": + staff = request.user + start_date = request.POST.get("start_date") + end_date = request.POST.get("end_date") + message = request.POST.get("message") + + try: + # Convert date strings to timezone-aware datetime + start_date = timezone.datetime.strptime(start_date, "%Y-%m-%d") + end_date = timezone.datetime.strptime(end_date, "%Y-%m-%d") + start_date = timezone.make_aware(start_date) + end_date = timezone.make_aware(end_date) + today = timezone.now() + + if start_date.date() < today.date(): + messages.error(request, "Start date cannot be in the past.") + return redirect("teacherDashboard") + + if end_date.date() < start_date.date(): + messages.error(request, "End date cannot be before start date.") + return redirect("teacherDashboard") + + # Check if leave already exists for these dates + existing_leave = Staff_leave.objects.filter( + staff=staff, + start_date__lte=end_date.date(), + end_date__gte=start_date.date() + ).first() + + if existing_leave: + messages.error( + request, "You have already requested leave for these dates." + ) + return redirect("teacherDashboard") + + # Create leave request + leave = Staff_leave.objects.create( + staff=staff, + start_date=start_date.date(), + end_date=end_date.date(), + message=message, + status=0, # Pending status + ) + messages.success(request, "Leave request submitted successfully.") + except Exception as e: + messages.error(request, f"Error submitting leave request: {str(e)}") + + return redirect("teacherDashboard") + + return redirect("teacherDashboard") + + +@login_required +def submit_staff_institute_feedback(request): + """View to handle feedback submission from staff to institute""" + if request.method == "POST": + staff = request.user + feedback_type = request.POST.get("feedback_type") + rating = request.POST.get("rating") + feedback_text = request.POST.get("feedback_text") + is_anonymous = request.POST.get("is_anonymous") == "on" + + try: + # Convert to float first to handle half-stars + rating_value = float(rating) + if rating_value < 0.5 or rating_value > 5: + messages.error(request, "Rating must be between 0.5 and 5") + return redirect("teacherDashboard") + + # Check if it's a valid half or full star rating + if rating_value % 0.5 != 0: + messages.error(request, "Rating must be a whole or half number") + return redirect("teacherDashboard") + + # Get the institute (assuming one institute for now) + institute = Institute.objects.first() + + if not institute: + # Create a default institute if none exists + institute = Institute.objects.create( + name="Default Institute", + phone="123456789", + email="default@example.com", + ) + + # Get the staff object - 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") + + # Check if feedback already exists for this type + existing_feedback = StaffInstituteFeedback.objects.filter( + staff=teacher, institute=institute, feedback_type=feedback_type + ).first() + + if existing_feedback: + # Update existing feedback + existing_feedback.rating = rating_value + existing_feedback.feedback_text = feedback_text + existing_feedback.is_anonymous = is_anonymous + existing_feedback.save() + messages.success(request, "Institute feedback updated successfully.") + else: + # Create new feedback + feedback = StaffInstituteFeedback.objects.create( + staff=teacher, + institute=institute, + feedback_type=feedback_type, + rating=rating_value, + feedback_text=feedback_text, + is_anonymous=is_anonymous, + ) + messages.success(request, "Institute feedback submitted successfully.") + except ValueError: + messages.error(request, "Invalid rating value") + except Exception as e: + messages.error(request, f"Error submitting institute feedback: {str(e)}") + + return redirect("teacherDashboard") + + return redirect("teacherDashboard") + + +@login_required +def save_attendance(request): + """View to save attendance data""" + if request.method == "POST": + teacher = Staff.objects.filter(phone=request.user.phone).first() + if not teacher: + return redirect('login') + routine_id = request.POST.get("routine_id") + date_str = request.POST.get("date") + + # Convert class_status to boolean - "Conducted" is True, others are False + class_status_str = request.POST.get("class_status", "True") + class_status = class_status_str == "True" + + teacher_attend = request.POST.get("teacher_attend") == "on" + + try: + # Validate routine - strict check to ensure teacher can only access their assigned routines + routine = Routine.objects.get( + id=routine_id, + teacher=teacher, # Ensure this routine is assigned to this teacher + is_active=True, # Only allow active routines + ) + + # Validate date + attendance_date = timezone.datetime.strptime(date_str, "%Y-%m-%d").date() + + # Get or create attendance record + attendance, created = Attendance.objects.get_or_create( + routine=routine, + date=attendance_date, + defaults={ + "teacher": teacher, + "class_status": class_status, + "teacher_attend": teacher_attend, + }, + ) + + if not created: + # Update existing attendance + attendance.class_status = class_status + attendance.teacher_attend = teacher_attend + attendance.save() + + # Process student attendance + for key, value in request.POST.items(): + if key.startswith("student_"): + student_id = key.replace("student_", "") + student_attend = value == "on" + + try: + # Convert student_id to integer to ensure proper lookup + student_id_int = int(student_id) + student = Student.objects.get(id=student_id_int) + + # Get or create attendance record for this student + record, _ = AttendanceRecord.objects.get_or_create( + attendance=attendance, + student=student, + defaults={"student_attend": student_attend}, + ) + + # Update if record already exists + if record.student_attend != student_attend: + record.student_attend = student_attend + record.save() + + except Student.DoesNotExist: + continue + + messages.success( + request, + f"Attendance for {attendance_date.strftime('%d %b, %Y')} saved successfully.", + ) + + except Routine.DoesNotExist: + messages.error( + request, + "You do not have permission to take attendance for this class. Only teachers assigned to this class can manage its attendance.", + ) + except ValueError: + messages.error(request, "Invalid date format.") + except Exception as e: + messages.error(request, f"Error saving attendance: {str(e)}") + + # Redirect to teacher dashboard after saving attendance + return redirect("teacherDashboard") + + return redirect("teacherDashboard") + + +@login_required +@require_GET +def get_subject_schedule(request): + """View to get schedule for a subject""" + teacher = Staff.objects.filter(phone=request.user.phone).first() + if not teacher: + return JsonResponse({"success": False, "message": "Teacher account not found"}) + subject_id = request.GET.get("subject_id") + + if not subject_id: + return JsonResponse({"success": False, "message": "Subject ID is required"}) + + try: + routines = Routine.objects.filter( + teacher=teacher, subject_id=subject_id, is_active=True + ).values("id", "start_time", "end_time", "semester_or_year") + + return JsonResponse({"success": True, "schedule": list(routines)}) + except Exception as e: + return JsonResponse({"success": False, "message": str(e)}) + + +@login_required +def teacher_update_profile_picture(request): + """View to handle profile picture update for teachers""" + if request.method == "POST": + teacher = Staff.objects.filter(phone=request.user.phone).first() + if not teacher: + return redirect('login') + profile_picture = request.FILES.get("profile_picture") + + if profile_picture: + try: + # Update the teacher's profile picture + teacher.image = profile_picture + teacher.save() + messages.success(request, "Profile picture updated successfully.") + except Exception as e: + messages.error(request, f"Error updating profile picture: {str(e)}") + else: + messages.error(request, "No profile picture uploaded.") + + return redirect("teacherDashboard") + + return redirect("teacherDashboard") + + +@login_required +def teacher_change_password(request): + """View to handle password change for teachers""" + if request.method == "POST": + current_password = request.POST.get("current_password") + new_password = request.POST.get("new_password") + confirm_password = request.POST.get("confirm_password") + + # Get the current user + user = request.user + + # Validate inputs + if not current_password or not new_password or not confirm_password: + messages.error(request, "All password fields are required.") + return redirect("teacherDashboard") + + # Check if current password is correct + if not check_password(current_password, user.password): + messages.error(request, "Current password is incorrect.") + return redirect("teacherDashboard") + + # Check if new passwords match + if new_password != confirm_password: + messages.error(request, "New passwords do not match.") + return redirect("teacherDashboard") + + # Check if new password is strong enough (add more criteria as needed) + if len(new_password) < 8: + messages.error(request, "New password must be at least 8 characters long.") + return redirect("teacherDashboard") + + try: + # Update the password + user.password = make_password(new_password) + user.save() + messages.success( + request, + "Password changed successfully. Please log in again with your new password.", + ) + return redirect( + "login" + ) # Redirect to login page to log in with new password + except Exception as e: + messages.error(request, f"Error changing password: {str(e)}") + + return redirect("teacherDashboard") + + return redirect("teacherDashboard") + + +@login_required +def get_attendance_form(request): + """View to handle AJAX request for getting attendance form""" + routine_id = request.GET.get('routine_id') + date_str = request.GET.get('date') + + if not routine_id or not date_str: + return JsonResponse({ + 'error': 'Missing required parameters' + }) + + try: + teacher = Staff.objects.filter(phone=request.user.phone).first() + if not teacher: + 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() + + # Get existing attendance data if any + attendance_data = { + 'teacher_attend': False, + 'class_status': True, + 'records': {} + } + + try: + # Get attendance record with all related data in one query + attendance = Attendance.objects.select_related('routine').get( + routine=routine, + date=date + ) + + # Get all attendance records for this attendance in one query + records = AttendanceRecord.objects.filter(attendance=attendance).select_related('student') + + attendance_data = { + 'id': attendance.id, + 'date': attendance.date.strftime('%Y-%m-%d'), + 'teacher_attend': attendance.teacher_attend, + 'class_status': attendance.class_status, + 'records': { + str(record.student.id): record.student_attend + for record in records + } + } + except Attendance.DoesNotExist: + # No existing attendance record found - return default values + pass + + return JsonResponse(attendance_data) + + except Routine.DoesNotExist: + return JsonResponse({ + 'error': 'Invalid routine selected' + }, status=404) + except ValueError: + return JsonResponse({ + 'error': 'Invalid date format' + }, status=400) + except Exception as e: + return JsonResponse({ + 'error': str(e) + }, status=500) + + +@login_required +def get_students(request): + """View to handle AJAX request for getting students list""" + from django.http import JsonResponse + + routine_id = request.GET.get('routine_id') + + if not routine_id: + return JsonResponse({'error': 'Missing routine_id parameter'}, status=400) + + try: + teacher = Staff.objects.filter(phone=request.user.phone).first() + if not teacher: + return JsonResponse({'error': 'Teacher account not found'}, status=404) + routine = Routine.objects.get(id=routine_id, teacher=teacher) + + # Get students for this routine + students = Student.objects.filter( + course=routine.subject.course, + current_semester=routine.semester_or_year + ).order_by('name') + + # If no students found, try a more flexible approach + if not students.exists(): + students = Student.objects.filter( + course=routine.subject.course + ).order_by('name') + + # Format student data for JSON response + student_data = [{ + 'id': student.id, + 'name': student.name + } for student in students] + + return JsonResponse({ + 'students': student_data + }) + + except Routine.DoesNotExist: + return JsonResponse({'error': 'Invalid routine selected'}, status=404) + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) + + +@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) + + # 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() + + # 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') + + # 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) + 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" + }) + else: + subjects = Subject.objects.filter( + id__in=subjects_query + ).order_by('course__name', 'semester_or_year', 'name') + + # 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" + }) + else: + return render(request, 'subject_files.html', { + "subjects": [], + "teacher": teacher, + "selected_subject": None, + "error_message": "You are not assigned to any subjects yet" + }) + except Exception as e: + # 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) + else: + return render(request, '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") + + if not subject_id or not title or not file: + 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" + }) + + # Create new subject file + subject_file = SubjectFile.objects.create( + subject=subject, + title=title, + description=description, + file=file, + 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") + } + }) + except Subject.DoesNotExist: + return JsonResponse({ + "success": False, + "message": "Subject not found" + }) + except Exception as e: + 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) + }) + 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 + }) + + +@login_required +@require_http_methods(["POST"]) +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) + + file_id = request.POST.get("file_id") + + if not file_id: + 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" + }) + + # Delete the file + subject_file.delete() + + return JsonResponse({ + "success": True, + "message": "File deleted successfully" + }) + except SubjectFile.DoesNotExist: + return JsonResponse({ + "success": False, + "message": "File not found" + }) + except Exception as e: + return JsonResponse({ + "success": False, + "message": f"Error deleting file: {str(e)}" + }) + + +@login_required +@require_GET +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) + + # Get all subjects taught by the teacher + subjects_query = Subject.objects.filter( + 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 + }) + + +@login_required +@require_GET +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) + + 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) + + 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 + ).exists(): + 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) + + if not subject.syllabus_pdf: + 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" + }) + except Exception as e: + return JsonResponse({ + "success": False, + "message": f"Error retrieving syllabus: {str(e)}" + }, status=500) + + +@login_required +def staffDashboard(request): + """View to display the staff dashboard with their personal information and progress""" + staff = Staff.objects.get(phone=request.user.phone) # Get the Staff object + today = timezone.now().date() + last_week = today - timedelta(days=7) + 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) + + # 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] + + # 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] + + # Get leave requests + leave_requests = Staff_leave.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") + + # Get today's routines + 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 + ).select_related("routine") + + # Calculate attendance statistics + total_students = 0 + present_students = 0 + absent_students = 0 + + for attendance in today_attendance: + records = attendance.records.all() + total_students += records.count() + present_students += records.filter(student_attend=True).count() + 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 + + context = { + "staff": staff, + "recent_notices": recent_notices, + "recent_feedback": recent_feedback, + "institute_feedback": institute_feedback, + "leave_requests": leave_requests, + "today_routines": today_routines, + "today_attendance": today_attendance, + "total_students": total_students, + "present_students": present_students, + "absent_students": absent_students, + "avg_rating": avg_rating, + "feedback_types": feedback_types, + "last_week": last_week, + "last_month": last_month, + } + + return render(request, "app/staff/dashboard.html", context) diff --git a/app/studentViews.py b/app/studentViews.py deleted file mode 100644 index a397429..0000000 --- a/app/studentViews.py +++ /dev/null @@ -1,163 +0,0 @@ -from django.contrib.auth.decorators import login_required - -from django.shortcuts import render - - -@login_required -def student_dashboard(request): - return render(request, "student/dashboard.html") # Student-specific page - - -# @login_required -# @student_required -# def student_view_attendance(request): -# student = Student.objects.get(admin=request.user.id) -# course = student.course_id -# subjects = Subject.objects.filter(course_id=course) -# student = Student.objects.get(admin=user) -# student_notifcation = Student.objects.get(admin=request.user.id) -# notifications = NotificationStudent.objects.filter( -# student_id=student_notifcation.id -# ) -# return render( -# request, -# "student_template/student_view_attendance.html", -# {"subjects": subjects, "student": student, "notifications": notifications}, -# ) -# -# -# @login_required -# @student_required -# def student_view_attendance_post(request): -# subject_id = request.POST.get("subject") -# start_date = request.POST.get("start_date") -# end_date = request.POST.get("end_date") -# -# start_data_parse = datetime.datetime.strptime(start_date, "%Y-%m-%d").date() -# end_data_parse = datetime.datetime.strptime(end_date, "%Y-%m-%d").date() -# subject_obj = Subjects.objects.get(id=subject_id) -# user_object = CustomUser.objects.get(id=request.user.id) -# stud_obj = Students.objects.get(admin=user_object) -# user = CustomUser.objects.get(id=request.user.id) -# student = Students.objects.get(admin=user) -# student_notifcation = Students.objects.get(admin=request.user.id) -# notifications = NotificationStudent.objects.filter( -# student_id=student_notifcation.id -# ) -# -# attendance = Attendance.objects.filter( -# attendance_date__range=(start_data_parse, end_data_parse), -# subject_id=subject_obj, -# ) -# attendance_reports = AttendanceReport.objects.filter( -# attendance_id__in=attendance, student_id=stud_obj -# ) -# return render( -# request, -# "student_template/student_attendance_data.html", -# { -# "attendance_reports": attendance_reports, -# "student": student, -# "notifications": notifications, -# }, -# ) -# -# -# @login_required -# @student_required -# def student_apply_leave(request): -# student_obj = Student.objects.get(admin=request.user.id) -# leave_data = LeaveReportStudent.objects.filter(student_id=student_obj) -# user = CustomUser.objects.get(id=request.user.id) -# student = Student.objects.get(admin=user) -# student_notifcation = Student.objects.get(admin=request.user.id) -# notifications = NotificationStudent.objects.filter( -# student_id=student_notifcation.id -# ) -# return render( -# request, -# "student/apply_leave.html", -# {"leave_data": leave_data, "student": student, "notifications": notifications}, -# ) -# -# -# @login_required -# @student_required -# def student_apply_leave_save(request): -# if request.method != "POST": -# return HttpResponseRedirect(reverse("student_apply_leave")) -# else: -# leave_start_date = request.POST.get("leave_start_date") -# leave_end_date = request.POST.get("leave_end_date") -# leave_date = request.POST.get("leave_date") -# leave_msg = request.POST.get("leave_msg") -# -# student_obj = Student.objects.get(admin=request.user.id) -# try: -# leave_report = LeaveReportStudent( -# student_id=student_obj, -# leave_start_date=leave_start_date, -# leave_end_date=leave_end_date, -# leave_message=leave_msg, -# leave_status=0, -# ) -# leave_report.save() -# messages.success(request, "Successfully Applied for Leave") -# return HttpResponseRedirect(reverse("student_apply_leave")) -# except: -# messages.error(request, "Failed To Apply for Leave") -# return HttpResponseRedirect(reverse("student_apply_leave")) -# -# -# @login_required -# @student_required -# def student_feedback(request): -# staff_id = Student.objects.get(admin=request.user.id) -# feedback_data = FeedBackStudent.objects.filter(student_id=staff_id) -# student = Student.objects.get(admin=user) -# student_notifcation = Student.objects.get(admin=request.user.id) -# notifications = NotificationStudent.objects.filter( -# student_id=student_notifcation.id -# ) -# return render( -# request, -# "student_template/student_feedback.html", -# { -# "feedback_data": feedback_data, -# "student": student, -# "notifications": notifications, -# }, -# ) -# -# -# @login_required -# @student_required -# def student_feedback_save(request): -# if request.method != "POST": -# return HttpResponseRedirect(reverse("student_feedback")) -# else: -# feedback_msg = request.POST.get("feedback_msg") -# -# student_obj = Student.objects.get(admin=request.user.id) -# try: -# feedback = FeedBackStudent( -# student_id=student_obj, feedback=feedback_msg, feedback_reply="" -# ) -# feedback.save() -# messages.success(request, "Feedback submitted for review") -# return HttpResponseRedirect(reverse("student_feedback")) -# except: -# messages.error(request, "Could not Submit") -# return HttpResponseRedirect(reverse("student_feedback")) -# -# -# @csrf_exempt -# def student_fcmtoken_save(request): -# token = request.POST.get("token") -# try: -# student = Student.objects.get(admin=request.user.id) -# student.fcm_token = token -# student.save() -# return HttpResponse("True") -# except: -# return HttpResponse("False") diff --git a/app/studentviews.py b/app/studentviews.py new file mode 100644 index 0000000..8db9671 --- /dev/null +++ b/app/studentviews.py @@ -0,0 +1,489 @@ +from django.shortcuts import render, redirect, get_object_or_404 +from django.http import JsonResponse, HttpResponse +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.db.models import Count, Q, Avg +from django.utils import timezone +from datetime import timedelta +from django.views.decorators.http import require_http_methods, require_GET +from .models import ( + Student, + Course, + CourseTracking, + Student_Leave, + Staff, + Attendance, + AttendanceRecord, + StudentFeedback, + Routine, + Notice, + Institute, + InstituteFeedback, + Subject, + FEEDBACK_TYPE_CHOICES, +) + + +@login_required +def studentDashboard(request): + """View to display the student dashboard with their personal information and progress""" + student = Student.objects.get(phone=request.user.phone) # Get the Student object + today = timezone.now().date() + last_week = today - timedelta(days=7) + last_month = today - timedelta(days=30) + + # Clear section states on page load + 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__isnull=True) + | Q(designation="") + ).order_by("name") + + # Get feedback types for institute feedback + feedback_types = FEEDBACK_TYPE_CHOICES + + # Course Progress and Tracking + course_trackings = CourseTracking.objects.filter(student=student).select_related( + "course" + ) + total_courses = course_trackings.count() + completed_courses = course_trackings.filter(progress_status="Completed").count() + in_progress_courses = course_trackings.filter(progress_status="In Progress").count() + dropped_courses = course_trackings.filter(progress_status="Dropped").count() + + # Calculate overall completion percentage + overall_completion = 0 + if total_courses > 0: + total_percentage = sum( + tracking.completion_percentage for tracking in course_trackings + ) + overall_completion = total_percentage / total_courses + + # Course Summary + course_summary = { + "total_courses": total_courses, + "completed_courses": completed_courses, + "ongoing_courses": in_progress_courses, + "completion_percentage": overall_completion, + } + + # Course Details with enhanced information + course_details = [] + for tracking in course_trackings: + 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, + }) + + # Get current subjects based on current semester + 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 + ) + + # 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 + + # 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 + 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 + ) + + # 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 + if actual_classes > 0: + 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 + ) + + # Calculate attendance streaks + current_streak = 0 + longest_streak = 0 + temp_streak = 0 + last_attendance_date = None + + for record in attendance_records.order_by("-attendance__date"): + if record.student_attend: + if last_attendance_date is None: + last_attendance_date = record.attendance.date + current_streak = 1 + temp_streak = 1 + elif (last_attendance_date - record.attendance.date).days == 1: + temp_streak += 1 + if temp_streak > longest_streak: + longest_streak = temp_streak + if record.attendance.date == today - timedelta(days=current_streak): + current_streak += 1 + else: + temp_streak = 1 + else: + temp_streak = 0 + if record.attendance.date == today: + current_streak = 0 + + # Attendance Summary + attendance_summary = { + "total_classes": total_classes, + "classes_attended": classes_attended, + "present_count": classes_attended, + "absent_count": total_classes - classes_attended, + "attendance_percentage": attendance_percentage, + "current_streak": current_streak, + "longest_streak": longest_streak + } + + # Add actual recorded classes info if different from expected + if current_semester_start and actual_classes != total_classes: + attendance_summary["actual_classes"] = actual_classes + attendance_summary["actual_percentage"] = attendance_summary_actual + + # Get attendance records with related information + attendance_records = attendance_records.select_related( + "attendance", "attendance__routine", "attendance__teacher" + ).order_by("-attendance__date") + + # Get recent attendance (last 7 days) + recent_attendance = attendance_records.filter( + attendance__date__gte=last_week + ).order_by("-attendance__date") + + # Get feedback + recent_feedback = student.feedbacks.all().order_by("-created_at")[:5] + institute_feedback = student.institute_feedbacks.all().order_by("-created_at")[:5] + + # Get notices + notices = Notice.objects.all().order_by("-created_at") + + # Get student leaves with status information + student_leaves = Student_Leave.objects.filter(student=student).order_by( + "-created_at" + ) + pending_leaves = student_leaves.filter(status=0).count() + approved_leaves = student_leaves.filter(status=1).count() + rejected_leaves = student_leaves.filter(status=2).count() + + # Get current semester routines + current_semester_routines = [] + if active_tracking: + current_semester_routines = Routine.objects.filter( + course=active_tracking.course, + semester_or_year=active_tracking.current_semester, + is_active=True, + ) + + # Check if student has any routines + has_routines = current_semester_routines.exists() + + context = { + "student": student, + "title": "Student Dashboard", + "teachers": teachers, + "feedback_types": feedback_types, + # Course Progress + "total_courses": total_courses, + "completed_courses": completed_courses, + "in_progress_courses": in_progress_courses, + "dropped_courses": dropped_courses, + "overall_completion": round(overall_completion, 1), + "course_details": course_details, + "course_summary": course_summary, + # Current Subjects + "current_subjects": current_subjects, + # Attendance + "attendance_summary": attendance_summary, + "attendance_records": attendance_records, + "recent_attendance": recent_attendance, + # Feedback + "recent_feedback": recent_feedback, + "institute_feedback": institute_feedback, + # Notices + "notices": notices, + # Leaves + "student_leaves": student_leaves, + "pending_leaves": pending_leaves, + "approved_leaves": approved_leaves, + "rejected_leaves": rejected_leaves, + # Routines + "current_semester_routines": current_semester_routines, + "has_routines": has_routines, + # Current date for display + "today": today, + } + + return render(request, "student_dashboard.html", context) + + +@login_required +def request_leave(request): + if request.method == "POST": + student = request.user + start_date = request.POST.get("start_date") + end_date = request.POST.get("end_date") + message = request.POST.get("message") + + try: + # Convert date strings to timezone-aware datetime + start_date = timezone.datetime.strptime(start_date, "%Y-%m-%d") + end_date = timezone.datetime.strptime(end_date, "%Y-%m-%d") + start_date = timezone.make_aware(start_date) + end_date = timezone.make_aware(end_date) + today = timezone.now() + + if start_date.date() < today.date(): + messages.error(request, "Start date cannot be in the past.") + return redirect("studentDashboard") + + if end_date.date() < start_date.date(): + messages.error(request, "End date cannot be before start date.") + return redirect("studentDashboard") + + # Check if leave already exists for these dates + existing_leave = Student_Leave.objects.filter( + student=student, + start_date__lte=end_date.date(), + end_date__gte=start_date.date() + ).first() + + if existing_leave: + messages.error( + request, "You have already requested leave for these dates." + ) + return redirect("studentDashboard") + + # Create leave request + leave = Student_Leave.objects.create( + student=student, + start_date=start_date.date(), + end_date=end_date.date(), + message=message, + status=0, # Pending status + ) + messages.success(request, "Leave request submitted successfully.") + except Exception as e: + messages.error(request, f"Error submitting leave request: {str(e)}") + + return redirect("studentDashboard") + + return redirect("studentDashboard") + + +@login_required +def submit_feedback(request): + if request.method == "POST": + try: + feedback_type = request.POST.get("feedback_type") + rating = float(request.POST.get("rating", 0)) + feedback_text = request.POST.get("feedback_text", "").strip() + + # Validate rating range (1 to 5) + if not (1 <= rating <= 5): + messages.error(request, "Rating must be between 1 and 5") + return redirect("studentDashboard") + + if not feedback_text: + messages.error(request, "Please provide feedback text") + return redirect("studentDashboard") + + if feedback_type == "teacher": + teacher_id = request.POST.get("teacher_id") + if not teacher_id: + messages.error(request, "Please select a teacher") + return redirect("studentDashboard") + + try: + teacher = Staff.objects.get(id=teacher_id) + # Check if feedback already exists + existing_feedback = StudentFeedback.objects.filter( + student=request.user, teacher=teacher + ).first() + + if existing_feedback: + # Update existing feedback + existing_feedback.rating = rating + existing_feedback.feedback_text = feedback_text + existing_feedback.save() + messages.success(request, "Teacher feedback updated successfully") + else: + # Create new feedback + StudentFeedback.objects.create( + student=request.user, + teacher=teacher, + rating=rating, + feedback_text=feedback_text, + ) + messages.success(request, "Teacher feedback submitted successfully") + + except Staff.DoesNotExist: + messages.error(request, "Selected teacher does not exist") + + else: + messages.error(request, "Invalid feedback type") + + except ValueError: + messages.error(request, "Invalid rating value") + except Exception as e: + messages.error(request, f"An error occurred: {str(e)}") + + return redirect("studentDashboard") + + +@login_required +def submit_institute_feedback(request): + if request.method == "POST": + student = request.user + feedback_type = request.POST.get("feedback_type") + rating = request.POST.get("rating") + feedback_text = request.POST.get("feedback_text") + is_anonymous = request.POST.get("is_anonymous") == "on" + + try: + # Validate feedback type + 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") + + # Convert to float first to handle half-stars + rating_value = float(rating) + if rating_value < 0.5 or rating_value > 5: + messages.error(request, "Rating must be between 0.5 and 5") + return redirect("studentDashboard") + + # Check if it's a valid half or full star rating + if rating_value % 0.5 != 0: + messages.error(request, "Rating must be a whole or half number") + return redirect("studentDashboard") + + # Get the institute (assuming one institute for now) + institute = Institute.objects.first() + + if not institute: + # Create a default institute if none exists + institute = Institute.objects.create( + name="Default Institute", + phone="123456789", + email="default@example.com", + ) + + # Check if feedback already exists for this type + existing_feedback = InstituteFeedback.objects.filter( + user=student, + 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 + InstituteFeedback.objects.create( + user=student, + 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("studentDashboard") + + +@login_required +@require_http_methods(["GET"]) +def get_subject_files(request, subject_id): + """View to get all files for a subject""" + 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"} + ) + + return JsonResponse( + { + "success": True, + "files": files, + } + ) + except Subject.DoesNotExist: + return JsonResponse({"success": False, "message": "Subject not found"}) + except Exception as e: + return JsonResponse( + {"success": False, "message": f"Error retrieving files: {str(e)}"} + ) diff --git a/app/teacherViews.py b/app/teacherViews.py deleted file mode 100644 index a0a88b9..0000000 --- a/app/teacherViews.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.shortcuts import render -from django.contrib.auth.decorators import login_required - - -@login_required -def teacher_dashboard(request): - return render(request, "teacher/dashboard.html") diff --git a/app/templatetags/__init__.py b/app/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/templatetags/app_filters.py b/app/templatetags/app_filters.py new file mode 100644 index 0000000..22b3526 --- /dev/null +++ b/app/templatetags/app_filters.py @@ -0,0 +1,38 @@ +from django import template + +register = template.Library() + +@register.filter +def get_range(value): + """ + Returns a range of numbers from 0 to value-1 + Used for creating star ratings + """ + try: + return range(int(value)) + except (ValueError, TypeError): + return range(0) + +@register.filter +def multiply(value, arg): + """Multiply the value by the argument""" + try: + return int(value) * int(arg) + except (ValueError, TypeError): + return value + +@register.filter +def get_item(dictionary, key): + """ + Gets a value from a dictionary by key. + Usage in template: {{ my_dict|get_item:key_variable }} + """ + if dictionary is None: + return None + + # Try with the key as is (likely integer ID) + if key in dictionary: + return dictionary.get(key) + + # Try with the key as string + return dictionary.get(str(key), None) \ No newline at end of file diff --git a/app/urls.py b/app/urls.py index d457173..9d513b1 100644 --- a/app/urls.py +++ b/app/urls.py @@ -1,121 +1,89 @@ from django.urls import path -from app import adminViews, studentViews, teacherViews +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 +) +# URL patterns for the main application urlpatterns = [ - path("dashboard/", adminViews.admin_home, name="dashboard"), - path("add_course/", adminViews.add_course, name="add_course"), - path("manage_course", adminViews.manage_course, name="manage_course"), - path("delete_course//", adminViews.delete_course, name="delete_course"), - path("edit_course//", adminViews.edit_course, name="edit_course"), - path("add_academicyear/", adminViews.add_academicyear, name="add_academicyear"), - path( - "manage_academicyear/", - adminViews.manage_academicyear, - name="manage_academicyear", - ), - path( - "delete_academicyear//", - adminViews.delete_academicyear, - name="delete_academicyear", - ), - path( - "edit_academicyears//", - adminViews.edit_academicyear, - name="edit_academicyear", - ), - path("manage_subject/", adminViews.manage_subjects, name="manage_subject"), - path("add_subject/", adminViews.add_subject, name="add_subject"), - path("edit_subject//", adminViews.edit_subject, name="edit_subject"), - path( - "delete_subject//", adminViews.delete_subject, name="delete_subject" - ), - path("manage_student/", adminViews.manage_student, name="manage_student"), - path("add_student/", adminViews.add_student, name="add_student"), - path( - "edit_student//", adminViews.edit_student, name="edit_student" - ), - path( - "delete_student//", - adminViews.delete_student, - name="delete_student", - ), - path("manage_teacher/", adminViews.manage_teacher, name="manage_teacher"), - path("add_teacher/", adminViews.add_teacher, name="add_teacher"), - path( - "edit_teacher//", adminViews.edit_teacher, name="edit_teacher" - ), - path( - "delete_teacher//", - adminViews.delete_teacher, - name="delete_teacher", - ), - path( - "student_feedback_message/", - adminViews.student_feedback_message, - name="student_feedback_message", - ), - path( - "student_feedback_message_reply/", - adminViews.student_feedback_message_reply, - name="student_feedback_message_reply", - ), - path( - "teacher_feedback/", - adminViews.teacher_feedback, - name="teacher_feedback", - ), - path( - "teacher_feedback_replied/", - adminViews.teacher_feedback_replied, - name="teacher_feedback_replied", - ), - path( - "student_leave_view/", adminViews.student_leave_view, name="student_leave_view" - ), - path( - "student_leave_approve//", - adminViews.student_leave_approve, - name="student_leave_approve", - ), - path( - "student_leave_reject//", - adminViews.student_leave_reject, - name="student_leave_reject", - ), - path( - "teacher_leave_view/", adminViews.teacher_leave_view, name="teacher_leave_view" - ), - path( - "teacher_leave_approve//", - adminViews.teacher_leave_approve, - name="teacher_leave_approve", - ), - path( - "teacher_leave_reject//", - adminViews.teacher_leave_reject, - name="teacher_leave_reject", - ), - path( - "admin_view_attendance/", - adminViews.admin_view_attendance, - name="admin_view_attendance", - ), - path( - "admin_get_attendance_dates/", - adminViews.admin_get_attendance_dates, - name="admin_get_attendance_dates", - ), - path( - "admin_get_attendance_student/", - adminViews.admin_get_attendance_student, - name="admin_get_attendance_student", - ), - # ── Student ─────────────────────────────────────────────────────────── - path( - "student_dashboard/", studentViews.student_dashboard, name="student_dashboard" - ), - # ── teacher ─────────────────────────────────────────────────────────── - path( - "teacher_dashboard/", teacherViews.teacher_dashboard, name="teacher_dashboard" - ), + # Dashboard URLs + path("dashboard/", views.dashboard, name="dashboard"), + path("student-dashboard/", views.studentDashboard, name="studentDashboard"), + path("teacher-dashboard/", views.teacherDashboard, name="teacherDashboard"), + + # 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 + path("add-notice/", views.add_notice, name="add_notice"), + path("delete-notice//", views.delete_notice, name="delete_notice"), + + # Password Reset URLs + path("password-reset/", auth.reset_password_options, name="reset_password_options"), + path("password-reset/phone/", auth.password_reset_phone, name="phone_reset_password"), + path("password-reset/phone/verify/", auth.password_reset_phone_verify, name="verify_reset_otp"), + path("password-reset/phone/resend/", auth.resend_phone_otp, name="resend_phone_otp"), + path("password-reset/email/", auth.password_reset_email, name="email_reset_password"), + path("password-reset/email/verify/", auth.password_reset_email_verify, name="password_reset_done"), + path("password-reset/email/resend/", auth.resend_email_code, name="resend_reset_code"), + path("password-reset/set/", auth.set_new_password, name="set_new_password"), + + # Password security check + path("check-weak-password/", views.check_weak_password, name="check_weak_password"), + + # Staff Management URLs + path("get-students/", staffviews.get_students, name="get_students"), ] + diff --git a/app/utils.py b/app/utils.py new file mode 100644 index 0000000..f8ab97a --- /dev/null +++ b/app/utils.py @@ -0,0 +1,186 @@ +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 +import base64 +from .models import TOTPSecret, ResetToken, Student, Staff + +# OTP expiration time in seconds (5 minutes) +OTP_EXPIRY = 300 + + +def generate_secret_key(): + """Generate a random secret key for TOTP""" + return base64.b32encode(random.getrandbits(160).to_bytes(20, 'big')).decode('utf-8') + + +def generate_otp(secret_key): + """Generate a TOTP using the secret key""" + totp = pyotp.TOTP(secret_key, interval=OTP_EXPIRY) + return totp.now() + + +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 + ) + + +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') + + # 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) + except TOTPSecret.DoesNotExist: + return False + + +def send_otp_sms(phone, otp): + """Send OTP via SMS using a service""" + try: + # Get user details from phone number + 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 + }) + + # This is a placeholder. You need to replace with actual SMS API implementation + # Example using a generic SMS API + # api_url = "https://api.example.com/send" + # payload = { + # "apikey": settings.SMS_API_KEY, + # "to": phone, + # "message": message + # } + # response = requests.post(api_url, json=payload) + # return response.status_code == 200 + + print("\nSMS Message Preview:") + print("-" * 50) + print(message) + print("-" * 50) + return True + except Exception as e: + print(f"Error sending OTP: {str(e)}") + return False + + +def send_password_reset_email(user, request, email): + """ + Send password reset code to user's email using HTML template + """ + 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() + + # Build email context + context = { + "user": user, + "domain": request.META.get("HTTP_HOST", "localhost:8000"), + "protocol": "https" if request.is_secure() else "http", + "reset_code": reset_code, + } + + # Render email content using template + subject = render_to_string("login/email_templates/password_reset_subject.txt") + message = render_to_string( + "login/email_templates/password_reset_email.html", context + ) + + # Send email using console backend + send_mail( + subject=subject, + message=message, + from_email="noreply@studentmanagement.com", + recipient_list=[email], + html_message=message, + fail_silently=False, + ) + print(f"\nPassword reset code sent to {email}") + print(f"Reset Code: {reset_code}\n") + return True + except Exception as e: + print(f"Failed to send email: {str(e)}") + return False + + +def verify_reset_code(email, code): + """Verify password reset code using TOTP""" + return verify_otp(email, code) + + +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 + ) + + +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 + + +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 9724de7..2df4b4a 100644 --- a/app/views.py +++ b/app/views.py @@ -1,6 +1,865 @@ -from django.contrib.auth import authenticate, login, logout -from django.http import HttpResponseRedirect, HttpResponse -from django.shortcuts import render, redirect -from django.contrib import messages +from django.shortcuts import render, redirect, get_object_or_404 +from django.http import JsonResponse, HttpResponse, FileResponse 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 ( + Student, + Course, + CourseTracking, + Student_Leave, + Staff_leave, + Attendance, + AttendanceRecord, + StudentFeedback, + Routine, + Staff, + Notice, + Institute, + InstituteFeedback, + Subject, + Batch, +) +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 +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 django.views.static import serve +# Import views from studentviews.py +from .studentviews import ( + studentDashboard, + request_leave, + submit_feedback, + submit_institute_feedback, + get_subject_files, +) + +# Import views from staffviews.py +from .staffviews import ( + teacherDashboard, + request_staff_leave, + submit_staff_institute_feedback, + save_attendance, + get_subject_schedule, + teacher_update_profile_picture, + teacher_change_password, +) + +from django.contrib.auth.hashers import check_password + +# Load environment variables +load_dotenv() + # Create your views here. + + + +@login_required +def dashboard(request): + """Main dashboard view that redirects to appropriate dashboard based on user role""" + user = request.user + user_groups = user.groups.values_list("name", flat=True) + + + + # Redirect based on user role + if "Student" in user_groups: + 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") + + # 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 + 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") + + # Staff Statistics + total_staff = Staff.objects.count() + active_staff = Staff.objects.filter(is_active=True).count() + staff_on_leave = ( + Staff_leave.objects.filter(status=1).values("staff").distinct().count() + ) + + # Course Statistics + total_courses = Course.objects.count() + active_courses = ( + Course.objects.filter(student_trackings__progress_status="In Progress") + .distinct() + .count() + ) + completed_courses = ( + Course.objects.filter(student_trackings__progress_status="Completed") + .distinct() + .count() + ) + + # 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="") + ) + + 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 + "total_students": total_students, + "active_students": active_students, + "completed_students": completed_students, + "on_leave_students": on_leave_students, + # Staff Statistics + "total_staff": total_staff, + "active_staff": active_staff, + "staff_on_leave": staff_on_leave, + # Course Statistics + "total_courses": total_courses, + "active_courses": active_courses, + "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 + "recent_feedback": recent_feedback, + "recent_institute_feedback": recent_institute_feedback, + "teacher_ratings": teacher_ratings, + # Course Progress + "course_progress": course_progress, + # Monthly Statistics + "monthly_stats": monthly_stats, + "notices": notices, # Add notices to context + "recent_activities": recent_activities, # Add recent activities to context + } + + return render(request, "dashboard.html", context) + 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}) + + +@login_required +def update_profile(request): + if request.method == "POST": + user = request.user + user_groups = user.groups.values_list("name", flat=True) + + # Handle profile image update + if "profile_image" in request.FILES: + try: + image = request.FILES["profile_image"] + + if "Student" in user_groups: + student = Student.objects.get(id=user.id) + if student.image: + # Delete old image if updating + if os.path.isfile(student.image.path): + os.remove(student.image.path) + # Save new image + 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: + # 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 in the database.") + except Exception as e: + messages.error(request, f"Error updating profile picture: {str(e)}") + else: + messages.error(request, "No profile picture uploaded.") + + # Redirect based on user type + if "Student" in user_groups: + return redirect("studentDashboard") + elif "Teacher" in user_groups: + return redirect("teacherDashboard") + else: + return redirect("dashboard") + + return redirect("dashboard") + + +@login_required +def change_password(request): + if request.method == "POST": + user = request.user + current_password = request.POST.get("current_password") + new_password = request.POST.get("new_password") + confirm_password = request.POST.get("confirm_password") + + # Check if this is an AJAX request + is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest' + + # Validate input + if not all([current_password, new_password, confirm_password]): + if is_ajax: + return JsonResponse({"success": False, "error": "All password fields are required."}) + messages.error(request, "All password fields are required.") + return redirect("studentDashboard") + + if new_password != confirm_password: + if is_ajax: + return JsonResponse({"success": False, "error": "New passwords do not match."}) + messages.error(request, "New passwords do not match.") + return redirect("studentDashboard") + + # Check current password + if not user.check_password(current_password): + if is_ajax: + return JsonResponse({"success": False, "error": "Current password is incorrect."}) + messages.error(request, "Current password is incorrect.") + return redirect("studentDashboard") + + # Set new password + try: + user.set_password(new_password) + user.save() + if is_ajax: + return JsonResponse({"success": True}) + + 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)}"}) + messages.error(request, f"Error changing password: {str(e)}") + + # Redirect based on user type + if hasattr(user, "student"): + return redirect("studentDashboard") + elif hasattr(user, "staff"): + return redirect("teacherDashboard") + else: + return redirect("dashboard") + + return redirect("dashboard") + + +@login_required +@require_GET +def get_subjects(request): + """Get subjects for a course and semester""" + course_id = request.GET.get("course_id") + semester = request.GET.get("semester", 1) + + if not course_id: + return JsonResponse({"success": False, "message": "Course ID is required"}) + + try: + subjects = Subject.objects.filter( + course_id=course_id, semester_or_year=semester + ).values("id", "name") + + return JsonResponse({"success": True, "subjects": list(subjects)}) + except Exception as e: + return JsonResponse({"success": False, "message": str(e)}) + + +@login_required +@require_GET +def get_teachers(request): + """Get teachers for the institute""" + try: + teachers = Staff.objects.filter( + Q(designation__icontains="Teacher") + | Q(designation__icontains="Staff") + | Q(designation__icontains="Principal") + | Q(designation__isnull=True) + | Q(designation="") + ).values("id", "name") + + return JsonResponse({"success": True, "teachers": list(teachers)}) + except Exception as e: + return JsonResponse({"success": False, "message": str(e)}) + + +@login_required +@require_GET +def get_course_duration(request): + """Get course duration details""" + course_id = request.GET.get("course_id") + + if not course_id: + return JsonResponse({"success": False, "message": "Course ID is required"}) + + try: + course = Course.objects.get(id=course_id) + + # Calculate number of semesters or years + total_periods = course.duration + if course.duration_type == "Semester": + total_periods = course.duration * 2 # 2 semesters per year + + # Create a list of periods + periods = [] + for i in range(1, total_periods + 1): + if course.duration_type == "Semester": + periods.append({"value": i, "label": f"Semester {i}"}) + else: + periods.append({"value": i, "label": f"Year {i}"}) + + return JsonResponse( + { + "success": True, + "course": { + "id": course.id, + "name": course.name, + "duration": course.duration, + "duration_type": course.duration_type, + "total_periods": total_periods, + "periods": periods, + }, + } + ) + except Course.DoesNotExist: + return JsonResponse({"success": False, "message": "Course not found"}) + except Exception as e: + return JsonResponse({"success": False, "message": str(e)}) + + +@login_required +def approve_student_leave(request, leave_id): + """View to approve a student leave request""" + if request.method == "POST": + leave = get_object_or_404(Student_Leave, id=leave_id) + leave.status = 1 # Approved + leave.save() + messages.success( + request, f"Leave request for {leave.student.name} has been approved." + ) + return redirect("dashboard") + + +@login_required +def reject_student_leave(request, leave_id): + """View to reject a student leave request""" + if request.method == "POST": + leave = get_object_or_404(Student_Leave, id=leave_id) + leave.status = 2 # Rejected + leave.save() + messages.warning( + request, f"Leave request for {leave.student.name} has been rejected." + ) + return redirect("dashboard") + + +@login_required +def approve_staff_leave(request, leave_id): + """View to approve a staff leave request""" + if request.method == "POST": + leave = get_object_or_404(Staff_leave, id=leave_id) + leave.status = 1 # Approved + leave.save() + messages.success( + request, f"Leave request for {leave.staff.name} has been approved." + ) + return redirect("dashboard") + + +@login_required +def reject_staff_leave(request, leave_id): + """View to reject a staff leave request""" + if request.method == "POST": + leave = get_object_or_404(Staff_leave, id=leave_id) + leave.status = 2 # Rejected + leave.save() + messages.warning( + request, f"Leave request for {leave.staff.name} has been rejected." + ) + return redirect("dashboard") + + +@login_required +def add_notice(request): + if request.method == "POST": + title = request.POST.get("title") + message = request.POST.get("message") + image = request.FILES.get("image") + + if title and message: + notice = Notice.objects.create(title=title, message=message, image=image) + messages.success(request, "Notice added successfully.") + else: + messages.error(request, "Title and message are required.") + + return redirect("dashboard") + + +@login_required +def delete_notice(request, notice_id): + if request.method == "POST": + try: + notice = Notice.objects.get(id=notice_id) + notice.delete() + return JsonResponse({"success": True}) + except Notice.DoesNotExist: + return JsonResponse({"success": False, "error": "Notice not found"}) + + return JsonResponse({"success": False, "error": "Invalid request method"}) + + +@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: + data = json.loads(request.body) + token = data.get('token') + + if not token: + return JsonResponse({'error': 'Token is required'}, status=400) + + user = request.user + 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() + elif "Teacher" in user_groups: + user.fcm_token = token + user.save() + + # Update or create FCMDevice + device, created = FCMDevice.objects.update_or_create( + token=token, + defaults={'is_active': True} + ) + + return JsonResponse({ + 'status': 'success', + 'device_id': device.id, + 'created': created + }) + except json.JSONDecodeError: + return JsonResponse({'error': 'Invalid JSON'}, status=400) + except Exception as 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 + +@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}) diff --git a/assets/logo.png b/assets/logo.png deleted file mode 100644 index 05e9f69..0000000 Binary files a/assets/logo.png and /dev/null differ diff --git a/requirements.txt b/requirements.txt index 4fa5351..4908d29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,45 @@ asgiref==3.8.1 +CacheControl==0.14.2 +cachetools==5.5.2 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +cryptography==44.0.2 Django==5.1.8 django-jazzmin==3.0.1 +django-mathfilters==1.0.0 +django-rest-framework==0.1.0 +djangorestframework==3.15.2 +firebase-admin==6.7.0 +google-api-core==2.24.2 +google-api-python-client==2.165.0 +google-auth==2.38.0 +google-auth-httplib2==0.2.0 +google-cloud-core==2.4.3 +google-cloud-firestore==2.20.1 +google-cloud-storage==3.1.0 +google-crc32c==1.7.1 +google-resumable-media==2.7.2 +googleapis-common-protos==1.69.2 +grpcio==1.71.0 +grpcio-status==1.71.0 +httplib2==0.22.0 +idna==3.10 +msgpack==1.1.0 +pillow==11.1.0 +proto-plus==1.26.1 +protobuf==5.29.4 +pyasn1==0.6.1 +pyasn1_modules==0.4.1 +pycparser==2.22 +PyJWT==2.10.1 +pyparsing==3.2.3 +python-dotenv==1.0.1 pytz==2025.2 -setuptools==78.1.0 -sqlparse==0.5.3 -typing_extensions==4.12.2 requests==2.32.3 +rsa==4.9 +sqlparse==0.5.3 +uritemplate==4.1.1 +urllib3==2.3.0 +whitenoise==6.9.0 +pyotp==2.9.0 diff --git a/static/css/base.css b/static/css/base.css new file mode 100644 index 0000000..38715b4 --- /dev/null +++ b/static/css/base.css @@ -0,0 +1,577 @@ +/* Base styles */ +:root { + /* Modern Color Palette */ + --primary: #4F46E5; /* Indigo 600 */ + --primary-light: #818CF8; + --primary-dark: #4338CA; + --secondary: #64748B; /* Slate 500 */ + --success: #10B981; /* Emerald 500 */ + --warning: #F59E0B; /* Amber 500 */ + --danger: #EF4444; /* Red 500 */ + --info: #3B82F6; /* Blue 500 */ + + /* Neutral Colors */ + --background: #F8FAFC; /* Slate 50 */ + --card-bg: #FFFFFF; + --text-primary: #1E293B; /* Slate 800 */ + --text-secondary: #64748B; /* Slate 500 */ + --border-color: #E2E8F0; /* Slate 200 */ + --hover-bg: #F1F5F9; /* Slate 100 */ + + /* Shadows */ + --card-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --hover-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + + /* Transitions */ + --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + + /* Border Radius */ + --radius-sm: 0.375rem; + --radius-md: 0.5rem; + --radius-lg: 0.75rem; + --radius-xl: 1rem; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + line-height: 1.6; + color: var(--text-primary); + background-color: var(--background); + margin: 0; + padding: 0; +} + +.container { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container { + max-width: 1140px; + } +} + +/* Common utility classes */ +.text-center { + text-align: center; +} + +.mt-3 { + margin-top: 1rem; +} + +.mb-3 { + margin-bottom: 1rem; +} + +.py-3 { + padding-top: 1rem; + padding-bottom: 1rem; +} + +/* Animations */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.fade-in { + animation: fadeIn 0.3s ease-out; +} + +/* Status Badges */ +.status-badge { + padding: 0.25rem 0.75rem; + border-radius: var(--radius-md); + font-size: 0.75rem; + font-weight: 500; + display: inline-flex; + align-items: center; + gap: 0.25rem; +} + +.bg-success, .completed, .leave-approved { + background-color: var(--success); + color: white; +} + +.bg-warning, .ongoing, .leave-pending { + background-color: var(--warning); + color: white; +} + +.bg-danger, .leave-rejected { + background-color: var(--danger); + color: white; +} + +.bg-info, .upcoming { + background-color: var(--info); + color: white; +} + +/* Form styles */ +.form-group { + margin-bottom: 1rem; +} + +.form-control { + padding: 0.625rem 0.875rem; + border-radius: var(--radius-md); + border: 1px solid var(--border-color); + transition: var(--transition); + font-size: 0.875rem; +} + +.form-control:focus { + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1); +} + +/* Button styles */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 500; + padding: 0.5rem 1rem; + border-radius: var(--radius-md); + transition: var(--transition); + gap: 0.5rem; + font-size: 0.875rem; + line-height: 1.25rem; +} + +.btn-primary { + background-color: var(--primary); + color: white; + border: none; +} + +.btn-primary:hover { + background-color: var(--primary-dark); + transform: translateY(-1px); +} + +/* Modern Card Styles */ +.content-card { + background-color: var(--card-bg); + border-radius: var(--radius-lg); + box-shadow: var(--card-shadow); + padding: 1.5rem; + margin-bottom: 1.5rem; + transition: var(--transition); + border: 1px solid var(--border-color); +} + +.content-card:hover { + transform: translateY(-2px); + box-shadow: var(--hover-shadow); +} + +/* Print Styles */ +@media print { + .btn-primary, + .modal, + .modal-backdrop { + display: none !important; + } + + .content-card { + box-shadow: none; + border: 1px solid #ddd; + } +} + +/* Tables */ +.table { + width: 100%; + border-collapse: separate; + border-spacing: 0; +} + +.table th { + background-color: var(--hover-bg); + padding: 1rem; + font-weight: 600; + text-align: left; + color: var(--text-primary); + border-bottom: 2px solid var(--border-color); +} + +.table td { + padding: 1rem; + border-bottom: 1px solid var(--border-color); + color: var(--text-secondary); +} + +.table tr:hover td { + background-color: var(--hover-bg); +} + +/* Responsive Design */ +@media (max-width: 1200px) { + .container { + padding-right: 12px; + padding-left: 12px; + } + + .content-card { + padding: 1.25rem; + } +} + +@media (max-width: 992px) { + .container { + padding-right: 10px; + padding-left: 10px; + } + + .content-card { + padding: 1rem; + } + + .btn { + padding: 0.4rem 0.8rem; + font-size: 0.8rem; + } +} + +@media (max-width: 768px) { + .container { + padding-right: 8px; + padding-left: 8px; + } + + .content-card { + padding: 0.875rem; + margin-bottom: 1rem; + } + + .form-control { + padding: 0.5rem 0.75rem; + font-size: 0.8rem; + } + + .table th, + .table td { + padding: 0.75rem; + font-size: 0.875rem; + } + + .status-badge { + padding: 0.2rem 0.5rem; + font-size: 0.7rem; + } +} + +@media (max-width: 576px) { + .container { + padding-right: 6px; + padding-left: 6px; + } + + .content-card { + padding: 0.75rem; + } + + .btn { + padding: 0.35rem 0.7rem; + font-size: 0.75rem; + } + + .form-control { + padding: 0.4rem 0.6rem; + font-size: 0.75rem; + } + + .table { + display: block; + overflow-x: auto; + white-space: nowrap; + } + + .table th, + .table td { + padding: 0.5rem; + font-size: 0.8rem; + } +} + +@media (max-width: 375px) { + .container { + padding-right: 4px; + padding-left: 4px; + } + + .content-card { + padding: 0.625rem; + } + + .btn { + padding: 0.3rem 0.6rem; + font-size: 0.7rem; + } + + .form-control { + padding: 0.35rem 0.5rem; + font-size: 0.7rem; + } + + .table th, + .table td { + padding: 0.4rem; + font-size: 0.75rem; + } + + .status-badge { + padding: 0.15rem 0.4rem; + font-size: 0.65rem; + } +} + +@media (max-width: 320px) { + .container { + padding-right: 3px; + padding-left: 3px; + } + + .content-card { + padding: 0.5rem; + } + + .btn { + padding: 0.25rem 0.5rem; + font-size: 0.65rem; + } + + .form-control { + padding: 0.3rem 0.4rem; + font-size: 0.65rem; + } + + .table th, + .table td { + padding: 0.3rem; + font-size: 0.7rem; + } + + .status-badge { + padding: 0.1rem 0.3rem; + font-size: 0.6rem; + } +} + +/* Enhanced Responsive Styles */ +/* Mobile-first approach for better performance */ + +/* Responsive layout containers */ +.container { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 576px) { + .container { + max-width: 540px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 720px; + } +} + +@media (min-width: 992px) { + .container { + max-width: 960px; + } +} + +@media (min-width: 1200px) { + .container { + max-width: 1140px; + } +} + +/* Responsive tables */ +.table-responsive-sm { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +/* Table adjustments for small screens */ +@media (max-width: 576px) { + .table th, + .table td { + padding: 0.5rem 0.25rem; + font-size: 0.75rem; + } + + .table-responsive-sm { + border: 0; + } +} + +/* Responsive form controls */ +.form-control { + width: 100%; +} + +/* Responsive card adjustments */ +@media (max-width: 576px) { + .card { + margin-bottom: 0.75rem; + } + + .card-body { + padding: 0.75rem; + } + + .card-title { + font-size: 1.1rem; + margin-bottom: 0.5rem; + } +} + +/* Responsive spacing utilities */ +@media (max-width: 576px) { + .px-sm-1 { padding-left: 0.25rem !important; padding-right: 0.25rem !important; } + .py-sm-1 { padding-top: 0.25rem !important; padding-bottom: 0.25rem !important; } + .p-sm-1 { padding: 0.25rem !important; } + .mx-sm-1 { margin-left: 0.25rem !important; margin-right: 0.25rem !important; } + .my-sm-1 { margin-top: 0.25rem !important; margin-bottom: 0.25rem !important; } + .m-sm-1 { margin: 0.25rem !important; } +} + +/* Adjustments for modals on small screens */ +@media (max-width: 576px) { + .modal-dialog { + margin: 0.5rem; + max-width: calc(100% - 1rem); + } + + .modal-body { + padding: 0.75rem; + } + + .modal-footer { + padding: 0.5rem; + } + + .modal-header { + padding: 0.75rem; + } +} + +/* Responsive image sizing */ +.img-fluid { + max-width: 100%; + height: auto; +} + +/* Responsive flex utilities */ +.d-sm-flex { + display: flex !important; +} + +@media (max-width: 576px) { + .flex-sm-column { + flex-direction: column !important; + } + + .justify-content-sm-center { + justify-content: center !important; + } + + .align-items-sm-center { + align-items: center !important; + } +} + +/* Enhanced responsive grid system */ +.row { + display: flex; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; +} + +@media (max-width: 576px) { + .row { + margin-right: -10px; + margin-left: -10px; + } + + [class*="col-"] { + padding-right: 10px; + padding-left: 10px; + } +} + +/* Responsive utilities for text */ +@media (max-width: 576px) { + .text-sm-center { + text-align: center !important; + } + + .text-sm-left { + text-align: left !important; + } + + .text-sm-right { + text-align: right !important; + } +} + +/* Print styles */ +@media print { + .no-print { + display: none !important; + } + + .container { + width: 100% !important; + max-width: none !important; + } + + body { + font-size: 12pt; + } + + a[href]:after { + content: " (" attr(href) ")"; + } +} \ No newline at end of file diff --git a/static/css/custom.css b/static/css/custom.css new file mode 100644 index 0000000..5ee0c89 --- /dev/null +++ b/static/css/custom.css @@ -0,0 +1,22 @@ +/* Target the specific logo image */ +img[src*="/static/img/logo.png"] { + max-height: 100px !important; + width: auto !important; +} + +/* Target login page logo */ +.jazzmin-login-page .login-logo img { + max-height: 100px !important; + width: auto !important; +} + +/* Target admin interface logo */ +.brand-link img.img-circle { + max-height: 33px !important; + width: auto !important; + opacity: .8; + margin-left: .8rem; + margin-right: .5rem; + margin-top: -3px; +} + diff --git a/static/css/dashboard.css b/static/css/dashboard.css new file mode 100644 index 0000000..7990fc4 --- /dev/null +++ b/static/css/dashboard.css @@ -0,0 +1,1155 @@ +/* Modern Dashboard Styles */ +:root { + /* Color Palette */ + --primary: #4F46E5; + --primary-dark: #4338CA; + --secondary: #6B7280; + --success: #10B981; + --warning: #F59E0B; + --danger: #EF4444; + --info: #3B82F6; + + /* Neutral Colors */ + --background: #F9FAFB; + --card-bg: #FFFFFF; + --text-primary: #111827; + --text-secondary: #6B7280; + --border-color: #E5E7EB; + --hover-bg: #F3F4F6; + + /* Effects */ + --card-shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px -1px rgba(0,0,0,0.1); + --hover-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1); + --transition: all 0.2s ease; + --radius-sm: 0.375rem; + --radius-md: 0.5rem; + --radius-lg: 0.75rem; +} + +/* Reset body padding since we removed the sidebar */ +body { + padding-left: 0 !important; + padding-top: 0 !important; +} + +/* Base Layout Styles */ +body { + background-color: var(--background); + opacity: 0; + transition: opacity 0.3s ease; + min-height: 100vh; + padding-bottom: 80px; + position: relative; + color: var(--text-primary); + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; +} + +/* Bottom Navigation */ +.bottom-nav { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background-color: var(--card-bg); + box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.1); + z-index: 1000; + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border-top: 1px solid var(--border-color); + padding: 0.5rem 0; +} + +.nav-container { + display: flex; + overflow-x: auto; + padding: 0.5rem 1rem; + justify-content: space-around; + -webkit-overflow-scrolling: touch; + gap: 1rem; + max-width: 1200px; + margin: 0 auto; +} + +.nav-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: var(--text-secondary); + text-decoration: none; + padding: 0.75rem 1rem; + transition: var(--transition); + border-radius: var(--radius-md); + min-width: 100px; + position: relative; + text-align: center; + flex: 1; +} + +.nav-item i { + font-size: 1.5rem; + margin-bottom: 0.5rem; + transition: var(--transition); +} + +.nav-item span { + white-space: nowrap; + font-size: 0.85rem; + line-height: 1.2; + font-weight: 500; + display: block; + margin-top: 0.25rem; +} + +.nav-item.active { + color: var(--primary); + background-color: var(--hover-bg); +} + +.nav-item.active::after { + content: ''; + position: absolute; + bottom: -0.25rem; + left: 50%; + transform: translateX(-50%); + width: 4px; + height: 4px; + background-color: var(--primary); + border-radius: 50%; +} + +.nav-item:hover:not(.active) { + color: var(--primary); + background-color: var(--hover-bg); +} + +/* Large Desktops */ +@media (min-width: 1200px) { + .nav-item { + min-width: 120px; + padding: 0.85rem 1.25rem; + } + + .nav-item i { + font-size: 1.75rem; + } + + .nav-item span { + font-size: 0.95rem; + } +} + +/* Desktop */ +@media (max-width: 1199px) { + .nav-item { + min-width: 110px; + padding: 0.75rem 1rem; + } + + .nav-item i { + font-size: 1.5rem; + } + + .nav-item span { + font-size: 0.85rem; + } +} + +/* Tablets */ +@media (max-width: 768px) { + .nav-container { + padding: 0.5rem; + gap: 0.5rem; + } + + .nav-item { + min-width: 90px; + padding: 0.6rem 0.75rem; + } + + .nav-item i { + font-size: 1.25rem; + margin-bottom: 0.35rem; + } + + .nav-item span { + font-size: 0.75rem; + } +} + +/* Mobile Landscape */ +@media (max-width: 576px) { + .nav-container { + padding: 0.4rem; + gap: 0.35rem; + } + + .nav-item { + min-width: 75px; + padding: 0.5rem 0.5rem; + } + + .nav-item i { + font-size: 1.1rem; + margin-bottom: 0.25rem; + } + + .nav-item span { + font-size: 0.7rem; + } +} + +/* Mobile Portrait */ +@media (max-width: 375px) { + .nav-container { + padding: 0.3rem; + gap: 0.25rem; + } + + .nav-item { + min-width: 65px; + padding: 0.4rem; + } + + .nav-item i { + font-size: 1rem; + margin-bottom: 0.2rem; + } + + .nav-item span { + font-size: 0.65rem; + font-weight: 400; + } +} + +/* Summary Cards */ +.summary-card { + background: var(--card-bg); + border-radius: var(--radius-lg); + border: 1px solid var(--border-color); + transition: var(--transition); + overflow: hidden; +} + +.summary-card:hover { + transform: translateY(-2px); + box-shadow: var(--hover-shadow); +} + +.summary-card .card-header { + background-color: var(--hover-bg); + padding: 1rem 1.25rem; + border-bottom: 1px solid var(--border-color); +} + +.summary-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; +} + +.summary-card .card-body { + padding: 1.25rem; +} + +/* Progress Bars */ +.progress { + height: 0.75rem; + background-color: var(--hover-bg); + border-radius: var(--radius-md); + overflow: hidden; +} + +.progress-bar { + border-radius: var(--radius-md); + transition: var(--transition); +} + +/* Welcome Section */ +.welcome-section { + background: linear-gradient(135deg, #4F46E5, #3B82F6) !important; + color: white !important; + padding: 2rem; + border-radius: var(--radius-lg); + margin-bottom: 2rem; + position: relative; + overflow: hidden; + box-shadow: 0 4px 6px -1px rgba(79, 70, 229, 0.1), 0 2px 4px -1px rgba(79, 70, 229, 0.06); +} + +.welcome-section::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(45deg, rgba(255,255,255,0.1) 25%, transparent 25%), + linear-gradient(-45deg, rgba(255,255,255,0.1) 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, rgba(255,255,255,0.1) 75%), + linear-gradient(-45deg, transparent 75%, rgba(255,255,255,0.1) 75%); + background-size: 20px 20px; + opacity: 0.1; +} + +.welcome-section .profile-picture-container.profile-sm { + width: 80px; + height: 80px; + margin-right: 1.5rem; + border-radius: 50%; + overflow: hidden; + border: 3px solid rgba(255, 255, 255, 0.2); +} + +.welcome-section .profile-picture { + width: 100%; + height: 100%; + object-fit: cover; +} + +.welcome-section h2 { + font-size: 1.75rem; + margin-bottom: 0.5rem; + color: white; +} + +.welcome-section .text-muted { + color: rgba(255, 255, 255, 0.9) !important; +} + +.welcome-section .badge { + font-size: 0.8rem; + padding: 0.35rem 0.65rem; +} + +.welcome-section .logout-btn { + background-color: rgba(255, 255, 255, 0.1); + color: white; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: var(--radius-md); + padding: 0.5rem 1rem; + font-size: 0.875rem; + transition: var(--transition); + display: flex; + align-items: center; + gap: 0.5rem; + text-decoration: none; +} + +.welcome-section .logout-btn:hover { + background-color: rgba(255, 255, 255, 0.2); + transform: translateY(-1px); +} + +/* Activity Scroll */ +.activity-scroll { + max-height: 300px; + overflow-y: auto; + padding-right: 0.5rem; + scrollbar-width: thin; + scrollbar-color: var(--text-secondary) var(--hover-bg); +} + +.activity-scroll::-webkit-scrollbar { + width: 4px; +} + +.activity-scroll::-webkit-scrollbar-track { + background: var(--hover-bg); + border-radius: var(--radius-md); +} + +.activity-scroll::-webkit-scrollbar-thumb { + background: var(--text-secondary); + border-radius: var(--radius-md); +} + +.activity-scroll::-webkit-scrollbar-thumb:hover { + background: var(--text-primary); +} + +/* Feedback Cards */ +.feedback-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.feedback-card { + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 1rem; + border: 1px solid #e0e0e0; + margin-bottom: 1rem; +} + +.feedback-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; +} + +.user-info { + display: flex; + gap: 1rem; + flex: 1; +} + +.student-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + background: var(--hover-bg); + flex-shrink: 0; +} + +.student-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.student-avatar i { + font-size: 1.5rem; + color: var(--text-secondary); +} + +.feedback-content { + flex: 1; + min-width: 0; /* Prevents text overflow */ +} + +.rating-stars { + color: var(--warning); + font-size: 1rem; + flex-shrink: 0; + padding-top: 0.25rem; +} + +.rating-stars i { + margin-right: 2px; +} + +.feedback-message { + color: #333; + font-size: 0.9rem; + line-height: 1.5; + margin-top: 0.5rem; + background: var(--hover-bg); + padding: 0.5rem; + border-radius: 6px; +} + +.feedback-message p { + margin: 0; + word-wrap: break-word; +} + +.empty-state { + text-align: center; + padding: 2rem; + color: var(--text-secondary); + background: var(--hover-bg); + border-radius: 8px; + margin: 1rem 0; +} + +.empty-state i { + font-size: 2rem; + margin-bottom: 1rem; + display: block; + color: var(--text-secondary); +} + +.empty-state p { + margin: 0; + font-size: 0.9rem; +} + +/* Rating Summary Styles */ +.rating-summary { + text-align: right; +} + +.rating-stats { + display: inline-flex; + flex-direction: column; + align-items: center; +} + +.rating-number { + font-size: 2rem; + font-weight: bold; + color: var(--primary); +} + +.rating-count { + font-size: 0.8rem; + color: var(--text-secondary); +} + +/* Status Badge Styles */ +.status-badge { + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.8rem; + font-weight: 500; +} + +.status-badge.completed { + background-color: var(--success); + color: white; +} + +.status-badge.ongoing { + background-color: var(--warning); + color: #000; +} + +.status-badge.upcoming { + background-color: var(--info); + color: white; +} + +/* Leave Badge Styles */ +.leave-pending { + background-color: var(--warning); + color: #000; +} + +.leave-approved { + background-color: var(--success); + color: white; +} + +.leave-rejected { + background-color: var(--danger); + color: white; +} + +/* Content Card Styles */ +.content-card { + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + padding: 1.5rem; + margin-bottom: 1.5rem; +} + +/* Special style for notice card to use full height */ +.notice-section .content-card { + height: 100%; /* Take full height of parent */ + display: flex; + flex-direction: column; +} + +.notice-section .content-card h5 { + margin-bottom: 1rem; + flex-shrink: 0; +} + +.notice-section .list-group { + flex-grow: 1; /* Take remaining space */ + height: auto; /* Let flex handle the height */ + min-height: 400px; /* Minimum height */ + margin-bottom: 0; +} + +/* Summary Card Styles */ +.summary-card { + background: #fff; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +.summary-card .card-header { + background: var(--hover-bg); + border-bottom: 1px solid #e0e0e0; + padding: 1rem; +} + +.summary-card .card-body { + padding: 1rem; +} + +/* Progress Bars */ +.progress { + height: 12px; + background-color: #E8EAF6; + border-radius: 24px; + overflow: hidden; +} + +.progress-bar { + border-radius: 24px; + transition: var(--transition); +} + +/* Add this to replace inline style attributes */ +.progress-bar[style] { + width: var(--progress-width, 0%); +} + +/* Tables */ +.table { + background: #FFFFFF; + border-radius: 16px; + overflow: hidden; + box-shadow: var(--card-shadow); + border-collapse: separate; + border-spacing: 0; +} + +.table thead th { + background: var(--hover-bg); + border: none; + padding: 1rem 1.5rem; + font-weight: 600; + color: var(--primary); +} + +.table tbody td { + padding: 1rem 1.5rem; + border-top: 1px solid var(--border-color); + transition: var(--transition); +} + +.table tbody tr:hover td { + background-color: var(--hover-bg); +} + +/* Custom tabs styling */ +.custom-tabs { + display: flex; + border-bottom: 1px solid #dee2e6; + margin-bottom: 1rem; +} + +.custom-tab-btn { + padding: 0.5rem 1rem; + margin-bottom: -1px; + background: none; + border: 1px solid transparent; + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; + cursor: pointer; + color: var(--secondary); +} + +.custom-tab-btn.active { + color: var(--primary); + background-color: #fff; + border-color: #dee2e6 #dee2e6 #fff; +} + +.custom-tab-content { + display: none; +} + +.custom-tab-content.active { + display: block; +} + +/* Fix for profile tab visibility */ +#profileSection.active .tab-pane.active { + display: block !important; +} + +#profileSection.active .nav-tabs .nav-link.active { + color: var(--primary); + border-color: var(--primary); + background-color: #fff; +} + +/* Action buttons enhanced */ +.action-button { + border-radius: 50px; + padding: 10px 20px; + font-weight: 500; + transition: var(--transition); + display: inline-flex; + align-items: center; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15); +} + +.action-button:hover { + transform: translateY(-3px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.action-button i { + margin-right: 8px; +} + +/* Feedback Section Styles */ +.feedback-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.student-avatar { + width: 40px; + height: 40px; + background: var(--primary-light); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 1.5rem; +} + +.rating { + display: flex; + gap: 0.25rem; +} + +.rating i { + font-size: 1rem; + transition: transform 0.2s ease; +} + +.rating i:hover { + transform: scale(1.2); +} + +.reply-btn { + padding: 0.5rem 1rem; + font-size: 0.875rem; + border-radius: 20px; + transition: all 0.2s ease; +} + +.reply-btn:hover { + background-color: var(--primary); + color: white; + transform: translateY(-1px); +} + +/* Rating Summary Styles */ +.rating-stats { + text-align: center; + padding: 1rem; + background: linear-gradient(145deg, #ffffff, #f5f5f5); + border-radius: 12px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + min-width: 200px; +} + +.rating-number { + line-height: 1; + margin-bottom: 0.5rem; +} + +.rating-number .display-4 { + font-size: 2.5rem; + font-weight: 700; + color: var(--primary); + margin-bottom: 0; +} + +.rating-stars { + display: flex; + justify-content: center; + gap: 0.25rem; + margin-bottom: 0.5rem; +} + +.rating-stars i { + font-size: 1.1rem; + transition: transform 0.2s ease; +} + +.rating-stars i:hover { + transform: scale(1.2); +} + +.rating-count { + font-size: 0.875rem; + color: var(--text-secondary); +} + +/* Responsive Welcome Section */ +@media (max-width: 992px) { + .welcome-section { + padding: 1.5rem; + } + + .welcome-section .profile-picture-container.profile-sm { + width: 70px; + height: 70px; + margin-right: 1rem; + } + + .welcome-section h2 { + font-size: 1.5rem; + } +} + +@media (max-width: 768px) { + .welcome-section { + padding: 1.25rem; + } + + .welcome-section .profile-picture-container.profile-sm { + width: 60px; + height: 60px; + } + + .welcome-section h2 { + font-size: 1.25rem; + } + + .welcome-section .text-muted { + font-size: 0.875rem; + } +} + +@media (max-width: 576px) { + .welcome-section { + padding: 1rem; + } + + .welcome-section .profile-picture-container.profile-sm { + width: 50px; + height: 50px; + margin-right: 0.75rem; + } + + .welcome-section h2 { + font-size: 1.1rem; + } + + .welcome-section .d-flex.justify-content-between { + gap: 1rem; + } + + .welcome-section .badge { + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + } + + .welcome-section .logout-btn { + padding: 0.35rem 0.75rem; + font-size: 0.8rem; + } +} + +@media (max-width: 375px) { + .welcome-section { + padding: 0.875rem; + } + + .welcome-section .profile-picture-container.profile-sm { + width: 45px; + height: 45px; + } + + .welcome-section h2 { + font-size: 1rem; + } + + .welcome-section .logout-text { + display: none; + } + + .welcome-section .logout-btn { + padding: 0.25rem 0.5rem; + } + + .welcome-section .badge { + font-size: 0.7rem; + padding: 0.2rem 0.4rem; + } +} + +/* Scrollable Containers */ +.feedback-list { + height: 300px; + overflow-y: auto; + padding-right: 10px; + scrollbar-width: thin; + scrollbar-color: var(--primary) var(--light); +} + +.feedback-list::-webkit-scrollbar { + width: 6px; +} + +.feedback-list::-webkit-scrollbar-track { + background: var(--light); + border-radius: 3px; +} + +.feedback-list::-webkit-scrollbar-thumb { + background: var(--primary); + border-radius: 3px; +} + +.feedback-list::-webkit-scrollbar-thumb:hover { + background: var(--primary-dark); +} + +/* Make notice list scrollable */ +.list-group { + height: 400px; + overflow-y: auto; + padding-right: 10px; + scrollbar-width: thin; + scrollbar-color: var(--primary) var(--light); +} + +.list-group::-webkit-scrollbar { + width: 6px; +} + +.list-group::-webkit-scrollbar-track { + background: var(--light); + border-radius: 3px; +} + +.list-group::-webkit-scrollbar-thumb { + background: var(--primary); + border-radius: 3px; +} + +.list-group::-webkit-scrollbar-thumb:hover { + background: var(--primary-dark); +} + +/* Responsive adjustments */ +@media (max-width: 576px) { + .list-group { + height: 300px; + } +} + +/* Responsive Navigation */ +@media (max-width: 768px) { + .nav-container { + padding: 0.5rem; + gap: 0.25rem; + } + + .nav-item { + min-width: 70px; + padding: 0.4rem; + } + + .nav-item i { + font-size: 1.1rem; + margin-bottom: 0.2rem; + } + + .nav-item span { + font-size: 0.65rem; + } +} + +@media (max-width: 576px) { + .nav-container { + padding: 0.4rem; + gap: 0.2rem; + } + + .nav-item { + min-width: 60px; + padding: 0.35rem; + } + + .nav-item i { + font-size: 1rem; + margin-bottom: 0.15rem; + } + + .nav-item span { + font-size: 0.6rem; + } +} + +@media (max-width: 375px) { + .nav-container { + padding: 0.3rem; + gap: 0.15rem; + } + + .nav-item { + min-width: 50px; + padding: 0.3rem; + } + + .nav-item i { + font-size: 0.9rem; + margin-bottom: 0.1rem; + } + + .nav-item span { + font-size: 0.55rem; + } +} + +/* Enhanced Responsive Styles for Dashboard */ + +/* Tablet and below adjustments for the welcome section */ +@media (max-width: 768px) { + /* Dashboard cards adjustments */ + .content-card { + padding: 16px; + margin-bottom: 16px; + } + + /* Better spacing for the summary cards */ + .row > [class*="col-"] { + margin-bottom: 16px; + } + + /* Tighter spacing for statistics cards */ + .summary-card .stat-number { + font-size: 2rem; + } +} + +/* Small devices (landscape phones) */ +@media (max-width: 576px) { + /* Dashboard cards adjustments */ + .content-card { + padding: 12px; + margin-bottom: 12px; + border-radius: 8px; + } + + /* Stacked layout for activity items */ + .activity-item { + flex-direction: column; + } + + .activity-item .activity-info { + margin-top: 8px; + width: 100%; + } + + /* Summary cards for mobile */ + .summary-card .stat-number { + font-size: 1.75rem; + } + + .summary-card .card-body { + padding: 16px; + } + + /* Simplified progress bars */ + .progress { + height: 6px; + } + + /* Mobile-optimized lists */ + .list-group-item { + padding: 0.75rem; + } + + /* Table adjustments */ + .table th, + .table td { + padding: 0.5rem 0.25rem; + font-size: 0.75rem; + } + + /* Modified tabs for mobile */ + .custom-tab-btn { + padding: 0.5rem 0.75rem; + font-size: 0.85rem; + } +} + +/* Extra small devices (portrait phones) */ +@media (max-width: 375px) { + /* Tighter spacing */ + .content-card { + padding: 10px; + margin-bottom: 10px; + } + + /* Further reduced stat sizes */ + .summary-card .stat-number { + font-size: 1.5rem; + } + + /* Stacked buttons on very small screens */ + .btn-sm-stack { + display: block; + width: 100%; + margin-bottom: 0.5rem; + } + + /* Even smaller tabs */ + .custom-tab-btn { + padding: 0.4rem 0.6rem; + font-size: 0.8rem; + } + + /* Simplified profile sections */ + .profile-info-row { + flex-direction: column; + } + + .profile-info-label, + .profile-info-value { + width: 100%; + text-align: left; + } +} + +/* Improvements for larger screens */ +@media (min-width: 992px) { + /* More breathing room */ + .content-card { + padding: 24px; + margin-bottom: 24px; + } + + /* Larger stats for desktop */ + .summary-card .stat-number { + font-size: 2.75rem; + } +} + +/* Extra styles for proper responsiveness of common elements */ +img.img-fluid { + max-width: 100%; + height: auto; +} + +.table-responsive { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +/* Ensure buttons stay properly sized on mobile */ +@media (max-width: 576px) { + .btn { + padding: 0.375rem 0.75rem; + font-size: 0.875rem; + } + + .btn-lg { + padding: 0.5rem 1rem; + font-size: 1rem; + } + + .btn-sm { + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + } +} \ No newline at end of file diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..1f51c1a Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/firebase-messaging-sw.js b/static/firebase-messaging-sw.js new file mode 100644 index 0000000..7de7799 --- /dev/null +++ b/static/firebase-messaging-sw.js @@ -0,0 +1,87 @@ +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'); + +firebase.initializeApp({ + apiKey: "AIzaSyAYgYXsf4-iwSDEEBcNAJ3YJxz76C3STsU", + authDomain: "student-management-syste-2e040.firebaseapp.com", + projectId: "student-management-syste-2e040", + storageBucket: "student-management-syste-2e040.appspot.com", + messagingSenderId: "800148418884", + appId: "1:800148418884:web:144b9c85bfeeec12252711", + measurementId: "G-BNRM6QEJQV" +}); + +const messaging = firebase.messaging(); + +// Handle background messages +messaging.onBackgroundMessage((payload) => { + console.log('Received background message:', payload); + + // 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' + } + ] + }; + + // 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); + }); +}); + +// 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) + ); + } +}); + +// Handle service worker installation +self.addEventListener('install', (event) => { + console.log('Service Worker installed'); + self.skipWaiting(); +}); + +// Handle service worker activation +self.addEventListener('activate', (event) => { + console.log('Service Worker activated'); + event.waitUntil(clients.claim()); +}); diff --git a/static/img/favicon.ico b/static/img/favicon.ico new file mode 100644 index 0000000..1f51c1a Binary files /dev/null and b/static/img/favicon.ico differ diff --git a/static/img/logo.png b/static/img/logo.png new file mode 100644 index 0000000..1f51c1a Binary files /dev/null and b/static/img/logo.png differ diff --git a/static/img/user.png b/static/img/user.png new file mode 100644 index 0000000..7127aa8 Binary files /dev/null and b/static/img/user.png differ diff --git a/static/js/bootstrap-init.js b/static/js/bootstrap-init.js new file mode 100644 index 0000000..7f40c70 --- /dev/null +++ b/static/js/bootstrap-init.js @@ -0,0 +1,25 @@ +// Initialize Bootstrap components +function initializeBootstrapComponents() { + // Initialize tooltips if available + if (typeof bootstrap !== "undefined" && bootstrap.Tooltip) { + const tooltipTriggerList = [].slice.call( + document.querySelectorAll('[data-bs-toggle="tooltip"]'), + ); + tooltipTriggerList.map(function(tooltipTriggerEl) { + return new bootstrap.Tooltip(tooltipTriggerEl); + }); + } + + // Initialize popovers if available + if (typeof bootstrap !== "undefined" && bootstrap.Popover) { + const popoverTriggerList = [].slice.call( + document.querySelectorAll('[data-bs-toggle="popover"]'), + ); + popoverTriggerList.map(function(popoverTriggerEl) { + return new bootstrap.Popover(popoverTriggerEl); + }); + } +} + +// Make function globally accessible +window.initializeBootstrapComponents = initializeBootstrapComponents; \ No newline at end of file diff --git a/static/js/dashboard.js b/static/js/dashboard.js new file mode 100644 index 0000000..61c7ce1 --- /dev/null +++ b/static/js/dashboard.js @@ -0,0 +1,785 @@ +// Dashboard functionality + +// Wait for the DOM to be fully loaded before running any code +document.addEventListener("DOMContentLoaded", function() { + // More reliable login detection - check for dashboard-specific elements + const isLoggedIn = ( + // Check for common dashboard elements + !!document.querySelector('.dashboard-content') || + !!document.querySelector('.side-navbar') || + !!document.querySelector('#user-dropdown') || + !!document.querySelector('.user-menu') || + !!document.querySelector('.nav-link[href*="logout"]') || + // Check URL patterns that would only be accessible when logged in + window.location.pathname.includes('/dashboard') || + window.location.pathname.includes('/profile') || + window.location.pathname.includes('/app/') || + // Explicit authentication flag + window.IS_AUTHENTICATED === true + ); + + if (isLoggedIn) { + // Call with a slight delay to ensure page is fully loaded + setTimeout(function() { + checkWeakPassword(); + }, 100); + } + + // Initialize all components + initializeComponents(); + + // Set up event handlers + setupEventHandlers(); + + // Get device token for notifications + if (typeof window.getDeviceToken === 'function' && isLoggedIn) { + window.getDeviceToken(); + } + + // Add global keyboard event handler for Escape key + document.addEventListener('keydown', function(event) { + if (event.key === 'Escape' || event.keyCode === 27) { + closeAllModals(); + } + }); +}); + +// Function to initialize all dashboard components +function initializeComponents() { + // Initialize any Bootstrap components + if (typeof bootstrap !== 'undefined') { + // Initialize tooltips + var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); + tooltipTriggerList.map(function(tooltipTriggerEl) { + return new bootstrap.Tooltip(tooltipTriggerEl); + }); + + // Initialize popovers + var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')); + popoverTriggerList.map(function(popoverTriggerEl) { + return new bootstrap.Popover(popoverTriggerEl); + }); + } + + // Handle image loading errors + document.querySelectorAll('img').forEach(function(img) { + img.addEventListener("error", function() { + this.onerror = null; + this.src = "/static/img/user.png"; + }); + }); + + // Setup star rating if elements exist + if (typeof window.setupStarRating === 'function') { + window.setupStarRating(); + } + + // Setup tabs if elements exist + if (typeof window.setupCustomTabs === 'function') { + window.setupCustomTabs(); + } + + // Setup feedback tabs if elements exist + if (typeof window.setupFeedbackTabs === 'function') { + window.setupFeedbackTabs(); + } + + // Initialize Bootstrap components + if (typeof window.initializeBootstrapComponents === 'function') { + window.initializeBootstrapComponents(); + } + + // Set up form validation + if (typeof window.setupRatingFormValidation === 'function') { + window.setupRatingFormValidation(); + } + + // Ensure notifications.js is loaded + if (typeof window.showNotification !== 'function') { + // Dynamically load the notifications script if not already available + const script = document.createElement('script'); + script.src = '/static/js/notifications.js'; + script.async = true; + document.head.appendChild(script); + } + + // Ensure notification toast container exists + if (!document.getElementById('notificationToast')) { + const toastContainer = document.createElement('div'); + toastContainer.innerHTML = ` +
+ +
+ `; + document.body.appendChild(toastContainer.firstElementChild); + } +} + +// Star rating functionality +function setupStarRating() { + document.querySelectorAll('.rating').forEach(function(container) { + var stars = container.querySelectorAll('.star'); + var ratingInput = container.querySelector('input[type="hidden"]'); + + stars.forEach(function(star, index) { + // Set initial state + if (ratingInput && parseInt(ratingInput.value) > index) { + star.classList.add('active'); + } + + // Add click handlers + star.addEventListener('click', function() { + // Update hidden input + if (ratingInput) { + ratingInput.value = index + 1; + } + + // Update star display + stars.forEach(function(s, i) { + if (i <= index) { + s.classList.add('active'); + } else { + s.classList.remove('active'); + } + }); + }); + + // Add hover effects + star.addEventListener('mouseenter', function() { + stars.forEach(function(s, i) { + if (i <= index) { + s.classList.add('hover'); + } else { + s.classList.remove('hover'); + } + }); + }); + + container.addEventListener('mouseleave', function() { + stars.forEach(function(s) { + s.classList.remove('hover'); + }); + }); + }); + }); +} + +// Function to set up custom tabs +function setupCustomTabs() { + var tabLinks = document.querySelectorAll('.custom-tabs .tab-link'); + + tabLinks.forEach(function(link) { + link.addEventListener('click', function(e) { + e.preventDefault(); + var target = this.getAttribute('data-target'); + + // Hide all tab content + document.querySelectorAll('.custom-tabs .tab-content').forEach(function(tab) { + tab.classList.remove('active'); + }); + + // Deactivate all tab links + tabLinks.forEach(function(tabLink) { + tabLink.classList.remove('active'); + }); + + // Activate clicked tab and content + this.classList.add('active'); + document.getElementById(target).classList.add('active'); + }); + }); + + // Activate first tab if none active + if (document.querySelector('.custom-tabs .tab-link.active') === null) { + var firstTab = document.querySelector('.custom-tabs .tab-link'); + if (firstTab) { + firstTab.click(); + } + } +} + +// Function to set up feedback tabs +function setupFeedbackTabs() { + var tabLinks = document.querySelectorAll('.feedback-tabs .tab-link'); + + tabLinks.forEach(function(link) { + link.addEventListener('click', function(e) { + e.preventDefault(); + var target = this.getAttribute('data-target'); + + // Hide all tab content + document.querySelectorAll('.feedback-tabs .tab-content').forEach(function(tab) { + tab.classList.remove('active'); + }); + + // Deactivate all tab links + tabLinks.forEach(function(tabLink) { + tabLink.classList.remove('active'); + }); + + // Activate clicked tab and content + this.classList.add('active'); + document.getElementById(target).classList.add('active'); + }); + }); +} + +// Function to check if the current user's password is weak and show a popup if needed +function checkWeakPassword() { + // Only skip on login/register pages + if (window.location.pathname.indexOf('/login') !== -1 || + window.location.pathname.indexOf('/register') !== -1 || + window.location.pathname === '/') { + return; + } + + // On dashboard page, we should definitely be logged in + const onDashboard = window.location.pathname.includes('/dashboard') || + window.location.pathname.includes('/app/'); + + try { + // Basic fetch with no extra options to keep it simple + fetch('/app/check-weak-password/') + .then(function(response) { + // Handle unauthorized responses (not logged in) + if (response.status === 401 || response.status === 403) { + return null; + } + + // Check if response is valid JSON + const contentType = response.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + return response.json().catch(error => { + return response.text().then(text => { + // Only show if there's an indication of a weak password + return text.includes('weak') ? { is_weak: true } : { is_weak: false }; + }); + }); + } else { + // Not JSON, try to get text instead + return response.text().then(text => { + // Only show if there's an indication of a weak password + return text.includes('weak') ? { is_weak: true } : { is_weak: false }; + }); + } + }) + .then(function(data) { + // Skip if null (user not authenticated) + if (data === null) return; + + if (data && data.is_weak === true) { + // Show modal immediately + showWeakPasswordModal(); + } + }) + .catch(function(error) { + console.error('Error checking password strength:', error); + }); + } catch (e) { + console.error('Error in checkWeakPassword:', e); + } +} + +// Function to create and show the weak password modal +function showWeakPasswordModal() { + // Check if modal already exists (prevent duplicates) + if (document.getElementById('weakPasswordModal')) { + document.getElementById('weakPasswordModal').style.display = 'block'; + return; + } + + // Get CSRF token + var csrfToken = ''; + var csrfInput = document.querySelector('input[name="csrfmiddlewaretoken"]'); + + if (csrfInput) { + csrfToken = csrfInput.value; + } else { + // Try to get from cookie if not found in form + var cookies = document.cookie.split(';'); + for (var i = 0; i < cookies.length; i++) { + var cookie = cookies[i].trim(); + if (cookie.startsWith('csrftoken=')) { + csrfToken = cookie.substring('csrftoken='.length, cookie.length); + break; + } + } + } + + // Create modal HTML with password inputs + var modalHtml = ` + `; + + // Append modal to body + document.body.insertAdjacentHTML('beforeend', modalHtml); + + // Add media query styles for better responsiveness + const styleEl = document.createElement('style'); + styleEl.textContent = ` + @media (max-width: 576px) { + #weakPasswordModal .modal-content { + width: 95% !important; + padding: 15px !important; + } + + #weakPasswordModal .modal-title { + font-size: 1.1rem !important; + } + + #weakPasswordModal .form-label { + font-size: 0.9rem !important; + } + } + + @media (max-height: 640px) { + #weakPasswordModal { + align-items: flex-start !important; + padding-top: 20px !important; + } + } + `; + document.head.appendChild(styleEl); + + // Set up password handling directly + var passwordArea = document.getElementById('new_password_area'); + var confirmArea = document.getElementById('confirm_password_area'); + var lengthDisplay = document.getElementById('password-length-display'); + var changeBtn = document.getElementById('password-change-btn'); + var showPasswordCheckbox = document.getElementById('show_password'); + var passwordForm = document.getElementById('password-form-div'); + + if (passwordArea && lengthDisplay) { + // Focus on password field + setTimeout(function() { + passwordArea.focus(); + }, 100); + + // Update function that directly reads the password field + function updateLengthDisplay() { + var password = passwordArea.value || ''; + // Update display with current length + lengthDisplay.textContent = 'Current length: ' + password.length + ' characters'; + } + + // Add multiple event listeners to catch all input cases + ['input', 'keyup', 'keydown', 'change', 'paste'].forEach(function(eventType) { + passwordArea.addEventListener(eventType, updateLengthDisplay); + }); + + // Add keydown event for Enter key + passwordArea.addEventListener('keydown', function(e) { + if (e.key === 'Enter') { + e.preventDefault(); + // Focus on confirm field if Enter is pressed in password field + confirmArea.focus(); + } + }); + + // Add keydown event for Enter key on confirm field + confirmArea.addEventListener('keydown', function(e) { + if (e.key === 'Enter') { + e.preventDefault(); + // Trigger submit button click if Enter is pressed in confirm field + if (changeBtn) { + changeBtn.click(); + } + } + }); + + // Initial update + updateLengthDisplay(); + } + + // Toggle password visibility + if (showPasswordCheckbox) { + showPasswordCheckbox.addEventListener('change', function() { + var type = this.checked ? 'text' : 'password'; + passwordArea.type = type; + confirmArea.type = type; + }); + } + + if (changeBtn) { + changeBtn.addEventListener('click', function() { + // Get the current password from the hidden field (more reliable) + var currentPassword = document.getElementById('current_password_hidden').value || '123'; + var newPassword = passwordArea.value || ''; + var confirmPassword = confirmArea.value || ''; + + // Check for empty fields first + if (!newPassword) { + alert('New password is required.'); + passwordArea.focus(); + return; + } + + if (!confirmPassword) { + alert('Please confirm your password.'); + confirmArea.focus(); + return; + } + + // Validate passwords + if (newPassword.length < 8) { + alert('Password must be at least 8 characters long! Your password is ' + newPassword.length + ' characters.'); + passwordArea.focus(); + return; + } + + if (newPassword !== confirmPassword) { + alert('Passwords do not match!'); + confirmArea.focus(); + return; + } + + if (newPassword === '123') { + alert('Please choose a stronger password!'); + passwordArea.focus(); + return; + } + + // Create form data + var formData = new FormData(); + formData.append('csrfmiddlewaretoken', csrfToken); + formData.append('current_password', currentPassword); + formData.append('new_password', newPassword); + formData.append('confirm_password', confirmPassword); + + // Submit form + fetch('/app/change-password/', { + method: 'POST', + body: formData, + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Cache-Control': 'no-cache' + } + }) + .then(function(response) { + return response.json(); + }) + .then(function(data) { + if (data.success) { + // Use notification system instead of alert + if (typeof window.showNotification === 'function') { + window.showNotification('Password Updated', 'Your password has been changed successfully!', 'success'); + } else { + alert('Password changed successfully!'); + } + + // Close the modal + document.getElementById('weakPasswordModal')?.remove(); + // No redirect to login page + } else if (data.error) { + // Show error notification + if (typeof window.showNotification === 'function') { + window.showNotification('Error', data.error, 'error'); + } else { + alert(data.error); + } + } + }) + .catch(error => { + console.error('Error:', error); + window.location.reload(); + }); + }); + } + + // Handle form submission (including Enter key) + if (passwordForm) { + passwordForm.addEventListener('submit', function(e) { + e.preventDefault(); + // Trigger the password change button click + if (changeBtn) { + changeBtn.click(); + } + }); + } +} + +// Function to set up event handlers +function setupEventHandlers() { + // Set up any tab switching behavior + var tabLinks = document.querySelectorAll('[data-bs-toggle="tab"]'); + tabLinks.forEach(function(tabLink) { + tabLink.addEventListener('click', function(event) { + event.preventDefault(); + var targetTab = document.querySelector(this.getAttribute('href')); + if (targetTab) { + // Hide all tabs + document.querySelectorAll('.tab-pane').forEach(function(tab) { + tab.classList.remove('active', 'show'); + }); + + // Show target tab + targetTab.classList.add('active', 'show'); + + // Update active state on tab links + tabLinks.forEach(function(link) { + link.classList.remove('active'); + }); + this.classList.add('active'); + } + }); + }); + + // Close any alerts after a delay + var alerts = document.querySelectorAll('.alert:not(.alert-warning)'); + alerts.forEach(function(alert) { + setTimeout(function() { + alert.style.display = 'none'; + }, 5000); + }); +} + +// 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 + document.querySelectorAll('[id$="Modal"]').forEach(modal => { + if (modal.style && modal.style.display === 'block') { + modal.style.display = 'none'; + + // If there's a specific close function, try to call it + const modalId = modal.id; + const closeFunction = window['close' + modalId.charAt(0).toUpperCase() + modalId.slice(1)]; + if (typeof closeFunction === 'function') { + closeFunction(); + } else { + // Reset body overflow as a fallback + document.body.style.overflow = ''; + } + } + }); + + // Explicitly handle the weak password modal - but only allow force close + const weakPasswordModal = document.getElementById('weakPasswordModal'); + if (weakPasswordModal && weakPasswordModal.style.display === 'block') { + // Do not auto-close the weak password modal + // This will be handled by the specific close function + } + + // Close Bootstrap modals + const bootstrapModals = document.querySelectorAll('.modal.show'); + bootstrapModals.forEach(modal => { + if (window.bootstrap && window.bootstrap.Modal) { + const bsModal = bootstrap.Modal.getInstance(modal); + if (bsModal) { + bsModal.hide(); + } + } + }); +} + +// Set up direct event handlers without jQuery +function setupDirectPasswordHandlers() { + // Get references to the new password elements + var passwordArea = document.getElementById('new_password_area'); + var confirmArea = document.getElementById('confirm_password_area'); + var lengthDisplay = document.getElementById('password-length-display'); + var changeBtn = document.getElementById('password-change-btn'); + var passwordForm = document.getElementById('password-form-div'); + + if (passwordArea && lengthDisplay) { + // Focus on password field + setTimeout(function() { + passwordArea.focus(); + }, 100); + + // Update function that directly reads the password field + function updateLengthDisplay() { + var password = passwordArea.value || ''; + // Update display with current length + lengthDisplay.textContent = 'Current length: ' + password.length + ' characters'; + } + + // Add multiple event listeners to catch all input cases + ['input', 'keyup', 'keydown', 'change', 'paste'].forEach(function(eventType) { + passwordArea.addEventListener(eventType, updateLengthDisplay); + }); + + // Add keydown event for Enter key + passwordArea.addEventListener('keydown', function(e) { + if (e.key === 'Enter') { + e.preventDefault(); + // Focus on confirm field if Enter is pressed in password field + confirmArea.focus(); + } + }); + + // Add keydown event for Enter key on confirm field + confirmArea.addEventListener('keydown', function(e) { + if (e.key === 'Enter') { + e.preventDefault(); + // Trigger submit button click if Enter is pressed in confirm field + if (changeBtn) { + changeBtn.click(); + } + } + }); + + // Initial update + updateLengthDisplay(); + } + + // Handle form submission (including Enter key) + if (passwordForm) { + passwordForm.addEventListener('submit', function(e) { + e.preventDefault(); + // Trigger the password change button click + if (changeBtn) { + changeBtn.click(); + } + }); + } + + if (changeBtn) { + changeBtn.addEventListener('click', function() { + // Get the current password from the hidden field (more reliable) + var currentPassword = document.getElementById('current_password_hidden').value || '123'; + var newPassword = passwordArea.value || ''; + var confirmPassword = confirmArea.value || ''; + + // Check for empty fields first + if (!newPassword) { + alert('New password is required.'); + passwordArea.focus(); + return; + } + + if (!confirmPassword) { + alert('Please confirm your password.'); + confirmArea.focus(); + return; + } + + // Validate passwords + if (newPassword.length < 8) { + alert('Password must be at least 8 characters long! Your password is ' + newPassword.length + ' characters.'); + passwordArea.focus(); + return; + } + + if (newPassword !== confirmPassword) { + alert('Passwords do not match!'); + confirmArea.focus(); + 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(); + formData.append('csrfmiddlewaretoken', csrfToken); + formData.append('current_password', currentPassword); + formData.append('new_password', newPassword); + formData.append('confirm_password', confirmPassword); + + // Submit form + fetch('/app/change-password/', { + method: 'POST', + body: formData, + headers: { + 'X-Requested-With': 'XMLHttpRequest' + } + }) + .then(response => response.json()) + .then(function(data) { + if (data.success) { + // Use notification system instead of alert + if (typeof window.showNotification === 'function') { + window.showNotification('Password Updated', 'Your password has been changed successfully!', 'success'); + } else { + alert('Password changed successfully!'); + } + + // Close the modal + document.getElementById('weakPasswordModal')?.remove(); + // No redirect to login page + } else if (data.error) { + // Show error notification + if (typeof window.showNotification === 'function') { + window.showNotification('Error', data.error, 'error'); + } else { + alert(data.error); + } + } + }) + .catch(error => { + console.error('Error:', error); + window.location.reload(); + }); + }); + } +} \ No newline at end of file diff --git a/static/js/firebase-config.example.js b/static/js/firebase-config.example.js new file mode 100644 index 0000000..dbac75a --- /dev/null +++ b/static/js/firebase-config.example.js @@ -0,0 +1,13 @@ +// Firebase 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" +}; + +// VAPID key for push notifications +const vapidKey = "YOUR_VAPID_KEY"; \ No newline at end of file diff --git a/static/js/firebase-init.js b/static/js/firebase-init.js new file mode 100644 index 0000000..bafee3c --- /dev/null +++ b/static/js/firebase-init.js @@ -0,0 +1,160 @@ +// Get CSRF token from cookie +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; +} + +// Initialize Firebase +firebase.initializeApp(firebaseConfig); +const messaging = firebase.messaging(); + +async function registerServiceWorker() { + try { + if (!('serviceWorker' in navigator)) { + throw new Error('Service Worker is not supported in this browser'); + } + + // Unregister any existing service workers + const existingRegistrations = await navigator.serviceWorker.getRegistrations(); + for (let registration of existingRegistrations) { + await registration.unregister(); + } + + // Register the new service worker + const registration = await navigator.serviceWorker.register('/firebase-messaging-sw.js', { + scope: '/', + updateViaCache: 'none' + }); + + console.log('Service Worker registered with scope:', registration.scope); + await navigator.serviceWorker.ready; + return registration; + } catch (error) { + console.error('Service Worker registration failed:', error); + throw error; + } +} + +async function getDeviceToken() { + try { + const registration = await registerServiceWorker(); + + // Request notification permission if not already granted + let permission = Notification.permission; + if (permission === 'default') { + permission = await Notification.requestPermission(); + } + + if (permission !== 'granted') { + throw new Error('Notification permission not granted'); + } + + // Get FCM token + const token = await messaging.getToken({ + vapidKey: vapidKey, + serviceWorkerRegistration: registration + }); + + if (!token) { + throw new Error('Failed to get FCM token'); + } + + console.log('Got FCM token:', token); + + // Get CSRF token + const csrftoken = getCookie('csrftoken'); + if (!csrftoken) { + throw new Error('CSRF token not found'); + } + + // Send the token to your backend for storage + const response = await fetch('/app/saveFCMToken/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrftoken + }, + body: JSON.stringify({ token: token }) + }); + + if (!response.ok) { + throw new Error('Failed to save token to server'); + } + + console.log('FCM Token saved successfully'); + return token; + } catch (error) { + console.error('Error in getDeviceToken:', error); + try { + // Clean up and retry on failure + await messaging.deleteToken().catch(console.error); + const registrations = await navigator.serviceWorker.getRegistrations(); + for (let registration of registrations) { + await registration.unregister(); + } + await new Promise(resolve => setTimeout(resolve, 1000)); + return getDeviceToken(); + } catch (retryError) { + console.error('Error during cleanup and retry:', retryError); + throw error; + } + } +} + +// Make getDeviceToken available globally +window.getDeviceToken = getDeviceToken; + +// Handle token refresh +messaging.onTokenRefresh = async () => { + try { + await getDeviceToken(); + } catch (error) { + console.error('Error refreshing token:', error); + } +}; + +// Handle foreground messages +messaging.onMessage((payload) => { + console.log('Foreground message received:', payload); + + if (Notification.permission === 'granted') { + const notificationTitle = payload.notification.title; + const notificationOptions = { + body: payload.notification.body, + icon: payload.notification.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' + } + ] + }; + + navigator.serviceWorker.ready.then(registration => { + registration.showNotification(notificationTitle, notificationOptions) + .catch(error => { + console.error('Error showing notification:', error); + }); + }); + } +}); \ No newline at end of file diff --git a/static/js/form-validation.js b/static/js/form-validation.js new file mode 100644 index 0000000..eb694bc --- /dev/null +++ b/static/js/form-validation.js @@ -0,0 +1,25 @@ +// Setup feedback form validation +function setupRatingFormValidation() { + const ratingForm = document.querySelector(".feedback-form"); + const ratingInput = document.getElementById("selected_rating"); + const ratingError = document.getElementById("rating-error"); + + if (ratingForm && ratingInput && ratingError) { + // Add form validation before submission + ratingForm.addEventListener("submit", function(e) { + // Check if rating is selected + if (!ratingInput.value) { + e.preventDefault(); // Stop form submission + if (ratingError) { + ratingError.style.display = "block"; + ratingError.scrollIntoView({ behavior: "smooth" }); + } + return false; + } + return true; + }); + } +} + +// Make function globally accessible +window.setupRatingFormValidation = setupRatingFormValidation; \ No newline at end of file diff --git a/static/js/navigation.js b/static/js/navigation.js new file mode 100644 index 0000000..5cf1620 --- /dev/null +++ b/static/js/navigation.js @@ -0,0 +1,88 @@ +document.addEventListener('DOMContentLoaded', function () { + + // Get all navigation items + const navItems = document.querySelectorAll('.nav-item[data-section]'); + + // Clear active state from all navigation items immediately + navItems.forEach(nav => nav.classList.remove('active')); + + // Hide all sections initially to prevent flickering + document.querySelectorAll('.content-section').forEach(section => { + section.classList.remove('active'); + section.style.display = 'none'; + section.style.visibility = 'hidden'; + }); + + // Function to switch to a section + function switchToSection(targetSection, skipStorage = false) { + if (!targetSection) return; + + if (!skipStorage) { + sessionStorage.setItem('activeSection', targetSection); + } + + // Remove active class from all nav items + navItems.forEach(nav => nav.classList.remove('active')); + + // Add active class to corresponding nav item + const activeNav = document.querySelector(`.nav-item[data-section="${targetSection}"]`); + if (activeNav) { + activeNav.classList.add('active'); + } + + // Hide all sections first + document.querySelectorAll('.content-section').forEach(section => { + section.classList.remove('active'); + section.style.display = 'none'; + section.style.visibility = 'hidden'; + }); + + // Show the target section + const targetElement = document.getElementById(`${targetSection}Section`); + + if (targetElement) { + targetElement.classList.add('active'); + targetElement.style.display = 'block'; + targetElement.style.visibility = 'visible'; + + // Update URL hash without scrolling + history.replaceState(null, null, `#${targetSection}`); + } + + // Make the page visible now that everything is ready + document.body.style.opacity = "1"; + } + + // Handle initial page load before showing content + function handleInitialLoad() { + let targetSection = 'dashboard'; // Default section + + if (window.location.hash) { + targetSection = window.location.hash.substring(1); + } else if (sessionStorage.getItem('activeSection')) { + targetSection = sessionStorage.getItem('activeSection'); + } + + // Switch to the target section without saving to sessionStorage again + switchToSection(targetSection, true); + } + + // Add event listeners to navigation items + navItems.forEach(item => { + const section = item.getAttribute('data-section'); + + item.addEventListener('click', function (e) { + e.preventDefault(); + switchToSection(section); + }); + }); + + // Handle hash changes (browser back/forward) + window.addEventListener('hashchange', function () { + const targetSection = window.location.hash.substring(1) || 'dashboard'; + switchToSection(targetSection); + }); + + // Run initial load before rendering content + handleInitialLoad(); +}); diff --git a/static/js/notifications.js b/static/js/notifications.js new file mode 100644 index 0000000..c9250a0 --- /dev/null +++ b/static/js/notifications.js @@ -0,0 +1,29 @@ +// Function to show notification toast +function showNotification(title, message, type = "success") { + const toast = document.getElementById("notificationToast"); + if (!toast) return; + + const toastTitle = document.getElementById("toastTitle"); + const toastMessage = document.getElementById("toastMessage"); + + toastTitle.innerText = title; + toastMessage.innerText = message; + + // Set toast color based on type + toast.className = "toast"; + if (type === "success") { + toast.classList.add("bg-success", "text-white"); + } else if (type === "error") { + toast.classList.add("bg-danger", "text-white"); + } else if (type === "warning") { + toast.classList.add("bg-warning"); + } else if (type === "info") { + toast.classList.add("bg-info", "text-white"); + } + + const bsToast = new bootstrap.Toast(toast); + bsToast.show(); +} + +// Make function globally accessible +window.showNotification = showNotification; \ No newline at end of file diff --git a/static/js/rating.js b/static/js/rating.js new file mode 100644 index 0000000..a7b6536 --- /dev/null +++ b/static/js/rating.js @@ -0,0 +1,163 @@ +// Function to set up star rating +function setupStarRating() { + console.log("Setting up star rating..."); + + // Handle display of existing ratings (read-only stars) + setupExistingRatings(); +} + +// Function to set up existing ratings (read-only stars) +function setupExistingRatings() { + // Find all rating divs (not in forms) + document.querySelectorAll('.rating').forEach(container => { + if (!container) return; + + // Make sure the stars have the right color + const stars = container.querySelectorAll('i.fas, i.far'); + stars.forEach(star => { + if (star.classList.contains('fas')) { + star.style.color = '#FFC107'; + } + }); + }); +} + +// Add CSS styles for rating stars +(function() { + const style = document.createElement('style'); + style.textContent = ` + .rating-star { + cursor: pointer; + transition: all 0.2s ease; + } + .rating-star.hover, + .rating-star:hover, + .rating-star.active, + .rating-star.selected { + color: #FFD700 !important; + transform: scale(1.1); + } + `; + document.head.appendChild(style); +})(); + +// Global function to select rating stars +function selectRating(star, inputId) { + console.log('selectRating called with star value:', star.getAttribute('data-value'), 'for input:', inputId); + + // Get the value + const value = parseInt(star.getAttribute('data-value')); + + // Find and update the input + const input = document.getElementById(inputId); + if (input) { + input.value = value; + console.log('Updated input value to:', value); + } else { + console.error('Input element not found with ID:', inputId); + } + + // Update the stars + const container = star.closest('.rating-stars'); + if (container) { + const stars = container.querySelectorAll('.rating-star'); + stars.forEach(s => { + const starValue = parseInt(s.getAttribute('data-value')); + if (starValue <= value) { + s.classList.remove('far'); + s.classList.add('fas'); + s.classList.add('selected'); + } else { + s.classList.remove('fas'); + s.classList.add('far'); + s.classList.remove('selected'); + } + }); + } else { + console.error('Rating container not found'); + } + + // Hide error message if any + let errorId; + if (inputId === 'teacher_rating') { + errorId = 'teacher-rating-error'; + } else if (inputId === 'institute_rating') { + errorId = 'institute-rating-error'; + } else if (inputId === 'teacher_modal_rating') { + errorId = 'teacher-modal-rating-error'; + } + + if (errorId) { + const errorElement = document.getElementById(errorId); + if (errorElement) { + errorElement.style.display = 'none'; + } + } + + return false; +} + +// Initialize rating functionality when DOM is loaded +document.addEventListener('DOMContentLoaded', () => { + console.log("DOM fully loaded"); + setupStarRating(); + + // Add an additional check after a short delay to catch dynamically loaded content + setTimeout(setupStarRating, 500); +}); + +// Initialize on document load +document.addEventListener('DOMContentLoaded', function() { + console.log('Rating.js loaded'); + + // Apply rating stars to static ratings (read-only) + const ratingContainers = document.querySelectorAll('.rating'); + console.log('Found', ratingContainers.length, 'static rating containers'); + + ratingContainers.forEach(container => { + const stars = container.querySelectorAll('i'); + stars.forEach(star => { + if (star.classList.contains('fas')) { + star.style.color = '#FFC107'; + } + }); + }); + + // Apply hover effects to rating stars + const ratingStars = document.querySelectorAll('.rating-star'); + console.log('Found', ratingStars.length, 'interactive rating stars'); + + ratingStars.forEach(star => { + star.addEventListener('mouseover', function() { + // Find all stars in the same container + const container = this.closest('.rating-stars'); + if (container) { + const stars = container.querySelectorAll('.rating-star'); + const value = parseInt(this.getAttribute('data-value')); + + stars.forEach(s => { + const starValue = parseInt(s.getAttribute('data-value')); + if (starValue <= value) { + s.classList.add('hover'); + } else { + s.classList.remove('hover'); + } + }); + } + }); + + star.addEventListener('mouseout', function() { + // Find all stars in the same container + const container = this.closest('.rating-stars'); + if (container) { + const stars = container.querySelectorAll('.rating-star'); + stars.forEach(s => { + s.classList.remove('hover'); + }); + } + }); + }); +}); + +// Make function globally available +window.selectRating = selectRating; \ No newline at end of file diff --git a/static/js/student_modals.js b/static/js/student_modals.js new file mode 100644 index 0000000..be58d35 --- /dev/null +++ b/static/js/student_modals.js @@ -0,0 +1,169 @@ +// Functions for handling student modals + +// Close PDF viewer modal +function closePdfViewerModal() { + document.getElementById('pdfViewerModal').style.display = 'none'; + document.body.style.overflow = ''; +} + +// Close subject files modal +function closeSubjectFilesModal() { + document.getElementById('subjectFilesModal').style.display = 'none'; + document.body.style.overflow = ''; +} + +// Open subject files modal and load content +function openSubjectFilesModal(subjectId) { + const modal = document.getElementById('subjectFilesModal'); + const filesList = document.getElementById('subjectFilesList'); + + modal.style.display = 'block'; + document.body.style.overflow = 'hidden'; + + filesList.innerHTML = ` +
+
+ Loading... +
+

Loading subject files...

+
+ `; + + fetch(`/app/subject/${subjectId}/files/`) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + if (!data.success) { + filesList.innerHTML = ` +
+ ${data.message || 'No files available for this subject'} +
+ `; + return; + } + + if (!data.files || data.files.length === 0) { + filesList.innerHTML = ` +
+ No study materials available for this subject. +
+ `; + return; + } + + filesList.innerHTML = data.files.map(file => { + const fileExtension = file.file_url ? file.file_url.split('.').pop().toLowerCase() : ''; + const iconClass = getFileIcon(fileExtension); + const fileIcon = ``; + + return ` +
+
+
+
+ ${fileIcon} + ${file.title} +
+

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

+ + ${file.file_type === 'syllabus' + ? 'Syllabus' + : 'Study Material'} + ${file.uploaded_by ? `Uploaded by: ${file.uploaded_by}` : ''} + ${file.uploaded_at ? `• ${formatDate(file.uploaded_at)}` : ''} + +
+
+ ${file.file_url ? ` + + ` : ` + + File not available + + `} +
+
+
+ `; + }).join(''); + }) + .catch(error => { + console.error('Error fetching subject files:', error); + filesList.innerHTML = ` +
+ Failed to load files. Please try again. +
+ `; + }); +} + +// Get appropriate icon based on file type +function getFileIcon(fileType) { + const iconMap = { + 'pdf': 'fa-file-pdf', + 'doc': 'fa-file-word', + 'docx': 'fa-file-word', + 'xls': 'fa-file-excel', + 'xlsx': 'fa-file-excel', + 'ppt': 'fa-file-powerpoint', + 'pptx': 'fa-file-powerpoint', + 'txt': 'fa-file-alt', + 'zip': 'fa-file-archive', + 'rar': 'fa-file-archive', + 'image': 'fa-file-image', + 'video': 'fa-file-video', + 'audio': 'fa-file-audio' + }; + return iconMap[fileType] || 'fa-file'; +} + +// Format file size for display +function formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +// Format date for display +function formatDate(dateString) { + if (!dateString) return ''; + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }); +} + +// Initialize event listeners when the DOM is loaded +document.addEventListener('DOMContentLoaded', function() { + // Handle Escape key to close modals + document.addEventListener('keydown', function(event) { + if (event.key === 'Escape') { + // Close all open modals + const pdfModal = document.getElementById('pdfViewerModal'); + const filesModal = document.getElementById('subjectFilesModal'); + + if (pdfModal && pdfModal.style.display === 'block') { + closePdfViewerModal(); + } + + if (filesModal && filesModal.style.display === 'block') { + closeSubjectFilesModal(); + } + } + }); +}); \ No newline at end of file diff --git a/static/js/tabs.js b/static/js/tabs.js new file mode 100644 index 0000000..bc4e612 --- /dev/null +++ b/static/js/tabs.js @@ -0,0 +1,62 @@ +// Setup custom tabs +function setupCustomTabs() { + const tabButtons = document.querySelectorAll(".custom-tab-btn"); + if (tabButtons.length > 0) { + tabButtons.forEach((button) => { + button.addEventListener("click", function() { + // Remove active class from all buttons + tabButtons.forEach((btn) => btn.classList.remove("active")); + + // Add active class to clicked button + this.classList.add("active"); + + // Hide all content + document.querySelectorAll(".custom-tab-content").forEach((content) => { + content.classList.remove("active"); + }); + + // Show target content + const target = this.getAttribute("data-target"); + document.getElementById(target).classList.add("active"); + }); + }); + } +} + +// Setup feedback tabs +function setupFeedbackTabs() { + const teacherTabBtn = document.getElementById("teacher-tab-btn"); + const instituteTabBtn = document.getElementById("institute-tab-btn"); + + if (teacherTabBtn && instituteTabBtn) { + teacherTabBtn.addEventListener("click", function() { + switchFeedbackTab("teacher"); + }); + + instituteTabBtn.addEventListener("click", function() { + switchFeedbackTab("institute"); + }); + } +} + +// Tab switching function +function switchFeedbackTab(tabId) { + // Hide all tab contents + document.querySelectorAll(".tab-pane").forEach((tab) => { + tab.classList.remove("active"); + }); + + // Deactivate all tab buttons + document.querySelectorAll(".feedback-tab-btn").forEach((btn) => { + btn.classList.remove("active"); + }); + + // Show selected tab content and activate button + document.getElementById(`${tabId}-tab-content`).classList.add("active"); + document.getElementById(`${tabId}-tab-btn`).classList.add("active"); +} + +// Make functions globally accessible +window.setupCustomTabs = setupCustomTabs; +window.setupFeedbackTabs = setupFeedbackTabs; +window.switchFeedbackTab = switchFeedbackTab; \ No newline at end of file diff --git a/static/js/teacher-attendance.js b/static/js/teacher-attendance.js new file mode 100644 index 0000000..669ba9f --- /dev/null +++ b/static/js/teacher-attendance.js @@ -0,0 +1,379 @@ +// Attendance Functions for Teacher Dashboard + +// Constants +const ATTENDANCE_MODAL_ID = 'attendanceModal'; +const QUICK_ATTENDANCE_FORM_ID = 'quickAttendanceForm'; +const ATTENDANCE_FORM_ID = 'attendanceForm'; + +// Attendance Modal Functions +async function openAttendanceModal(routineId, date) { + try { + console.log('Opening attendance modal for routine:', routineId, 'date:', date); + + // Validate and clean inputs + routineId = routineId?.trim().replace(/['"]+/g, ''); + date = date || getCurrentDate(); + + if (!routineId) { + throw new Error('Invalid routine ID'); + } + + // Get required elements + const modal = document.getElementById(ATTENDANCE_MODAL_ID); + const modalRoutineId = document.getElementById('modalRoutineId'); + const modalDate = document.getElementById('modalDate'); + const modalStudentList = document.getElementById('modalStudentList'); + + if (!modal || !modalRoutineId || !modalDate || !modalStudentList) { + throw new Error('Required modal elements not found'); + } + + // Set form values + modalRoutineId.value = routineId; + modalDate.value = date; + + // Show loading state + showLoadingSpinner(modalStudentList, 'Loading students...'); + + // Show modal + showModal(ATTENDANCE_MODAL_ID); + + // Fetch students + const data = await apiRequest(`${API_BASE_URL}/get-students/?routine_id=${routineId}`); + + if (data.error) { + throw new Error(data.error); + } + + // Render student list with enhanced UI + renderStudentList(modalStudentList, data.students); + + } catch (error) { + console.error('Error in openAttendanceModal:', error); + showError(document.getElementById('modalStudentList'), error); + hideModal(ATTENDANCE_MODAL_ID); + } +} + +function renderStudentList(container, students) { + if (!container || !Array.isArray(students)) return; + + container.innerHTML = ` +
+ + + ${students.map((student, index) => ` + + + + + + + `).join('')} + +
${index + 1}${student.name}${student.id} +
+ +
+
+
+ + `; +} + +function closeAttendanceModal() { + hideModal(ATTENDANCE_MODAL_ID); + resetForm(QUICK_ATTENDANCE_FORM_ID); +} + +// Attendance View Functions +function toggleAttendanceView(view) { + const headerSection = document.getElementById('attendanceHeader'); + const manageView = document.getElementById('manageAttendanceView'); + const routineSelectionForm = document.getElementById('routineSelectionForm'); + const attendanceFormContainer = document.getElementById('attendanceFormContainer'); + + if (view === 'recent') { + // Show recent view + headerSection.style.display = 'block'; + manageView.style.display = 'none'; + + // Reset form and container + if (attendanceFormContainer) { + attendanceFormContainer.style.display = 'none'; + attendanceFormContainer.innerHTML = ''; + } + + // Safely reset routine selection form + if (routineSelectionForm) { + routineSelectionForm.style.display = 'block'; + // Reset form elements individually to avoid potential issues + const routineSelect = routineSelectionForm.querySelector('#routineSelect'); + const dateSelect = routineSelectionForm.querySelector('#dateSelect'); + + if (routineSelect) { + routineSelect.selectedIndex = 0; + } + + if (dateSelect) { + dateSelect.value = ''; + } + + // Clear any error messages + routineSelectionForm.querySelectorAll('.alert').forEach(alert => alert.remove()); + } + } else { + // Show manage view + headerSection.style.display = 'none'; + manageView.style.display = 'block'; + + // Load data if routine and date are selected + const routineSelect = document.getElementById('routineSelect'); + const dateSelect = document.getElementById('dateSelect'); + + if (routineSelect?.value && dateSelect?.value) { + loadAttendanceData(); + } else if (attendanceFormContainer) { + attendanceFormContainer.style.display = 'none'; + attendanceFormContainer.innerHTML = ''; + } + } +} + +// Attendance Data Functions +async function editAttendance(routineId, date) { + try { + console.log('Editing attendance for routine:', routineId, 'date:', date); + + // Switch to manage view + toggleAttendanceView('manage'); + + // Hide routine selection form + const routineSelectionForm = document.getElementById('routineSelectionForm'); + if (routineSelectionForm) { + routineSelectionForm.style.display = 'none'; + } + + // Update form inputs + const attendanceForm = document.getElementById(ATTENDANCE_FORM_ID); + if (attendanceForm) { + const routineInput = attendanceForm.querySelector('input[name="routine_id"]'); + const dateInput = attendanceForm.querySelector('input[name="date"]'); + if (routineInput) routineInput.value = routineId; + if (dateInput) dateInput.value = date; + } + + // Show loading state + const container = document.getElementById('attendanceFormContainer'); + if (!container) return; + + container.style.display = 'block'; + showLoadingSpinner(container, 'Loading attendance data...'); + + // Fetch attendance data + const [attendanceData, studentsData] = await Promise.all([ + apiRequest(`${API_BASE_URL}/get-attendance-form/?routine_id=${routineId}&date=${date}`), + apiRequest(`${API_BASE_URL}/get-students/?routine_id=${routineId}`) + ]); + + if (attendanceData.error || studentsData.error) { + throw new Error(attendanceData.error || studentsData.error); + } + + // Render attendance form + renderAttendanceForm(container, routineId, date, attendanceData, studentsData.students); + + } catch (error) { + console.error('Error editing attendance:', error); + if (container) { + showError(container, error); + } + } +} + +function renderAttendanceForm(container, routineId, date, attendanceData, students) { + if (!container || !Array.isArray(students)) return; + + container.innerHTML = ` +
+ + + + +
+
+
+ + +
+
+
+ +
+
+ +
+ + + + + + + + + + + ${students.map((student, index) => { + const isPresent = attendanceData.records && attendanceData.records[student.id] === true; + return ` + + + + + + + `; + }).join('')} + +
#NameStudent IDPresent
${index + 1}${student.name}${student.id} +
+ +
+
+
+ +
+
+ + +
+ +
+
+ `; +} + +// Attendance Marking Functions +function markAllPresent() { + const checkboxes = document.querySelectorAll(`#${ATTENDANCE_FORM_ID} input[type="checkbox"], #${QUICK_ATTENDANCE_FORM_ID} input[type="checkbox"]`); + checkboxes.forEach(checkbox => { + if (checkbox.id !== 'teacherAttend' && checkbox.id !== 'modalTeacherAttend') { + checkbox.checked = true; + } + }); +} + +function markAllAbsent() { + const checkboxes = document.querySelectorAll(`#${ATTENDANCE_FORM_ID} input[type="checkbox"], #${QUICK_ATTENDANCE_FORM_ID} input[type="checkbox"]`); + checkboxes.forEach(checkbox => { + if (checkbox.id !== 'teacherAttend' && checkbox.id !== 'modalTeacherAttend') { + checkbox.checked = false; + } + }); +} + +async function loadAttendanceData() { + try { + console.log('Loading attendance data...'); + const routineId = document.getElementById('routineSelect')?.value; + const date = document.getElementById('dateSelect')?.value; + const container = document.getElementById('attendanceFormContainer'); + + if (!container) return; + + if (!routineId || !date) { + container.style.display = 'none'; + container.innerHTML = ''; + return; + } + + container.style.display = 'block'; + showLoadingSpinner(container, 'Loading attendance data...'); + + const [attendanceData, studentsData] = await Promise.all([ + apiRequest(`${API_BASE_URL}/get-attendance-form/?routine_id=${routineId}&date=${date}`), + apiRequest(`${API_BASE_URL}/get-students/?routine_id=${routineId}`) + ]); + + if (attendanceData.error || studentsData.error) { + throw new Error(attendanceData.error || studentsData.error); + } + + renderAttendanceForm(container, routineId, date, attendanceData, studentsData.students); + + } catch (error) { + console.error('Error loading attendance data:', error); + if (container) { + showError(container, error); + } + } +} + +// Form Submission Handler +async function saveQuickAttendance() { + try { + const form = document.getElementById(QUICK_ATTENDANCE_FORM_ID); + if (!form) return; + + const formData = new FormData(form); + const data = Object.fromEntries(formData.entries()); + + // Validate required fields + if (!data.routine_id || !data.date) { + throw new Error('Missing required fields'); + } + + // Show loading state + const submitButton = form.querySelector('button[type="submit"]'); + if (submitButton) { + submitButton.disabled = true; + submitButton.innerHTML = ' Saving...'; + } + + // Submit attendance + const response = await apiRequest(`${API_BASE_URL}/save-attendance/`, { + method: 'POST', + body: JSON.stringify(data) + }); + + if (response.error) { + throw new Error(response.error); + } + + // Show success message + showSuccess(form, 'Attendance saved successfully'); + + // Close modal and reset form + setTimeout(() => { + closeAttendanceModal(); + }, 1500); + + } catch (error) { + console.error('Error saving attendance:', error); + showError(form, error); + + // Reset submit button + const submitButton = form.querySelector('button[type="submit"]'); + if (submitButton) { + submitButton.disabled = false; + submitButton.innerHTML = ' Save Attendance'; + } + } +} \ No newline at end of file diff --git a/static/js/teacher-dashboard.js b/static/js/teacher-dashboard.js new file mode 100644 index 0000000..bcbfae0 --- /dev/null +++ b/static/js/teacher-dashboard.js @@ -0,0 +1,9 @@ +// Main teacher dashboard entry point - imports all modules + +// Load utility functions first +document.write(''); + +// Load feature modules +document.write(''); +document.write(''); +document.write(''); \ No newline at end of file diff --git a/static/js/teacher-init.js b/static/js/teacher-init.js new file mode 100644 index 0000000..829c1ad --- /dev/null +++ b/static/js/teacher-init.js @@ -0,0 +1,243 @@ +// Initialization code for the teacher dashboard + +// Initialize when document is ready +document.addEventListener('DOMContentLoaded', function() { + try { + console.log('Initializing teacher dashboard and attendance functions...'); + + // Handle hash navigation with priority + const hash = window.location.hash; + if (hash) { + // Remove the # and get the section name + const sectionId = hash.substring(1); + console.log('Found hash navigation target:', sectionId); + + // Make all sections inactive and hidden first + document.querySelectorAll('.content-section').forEach(section => { + section.classList.remove('active'); + section.style.display = 'none'; + }); + + // Then activate just the target section + const targetSection = document.getElementById(sectionId); + if (targetSection) { + console.log('Activating section from hash:', sectionId); + targetSection.classList.add('active'); + targetSection.style.display = 'block'; + + // Also activate the correct nav item + document.querySelectorAll('.nav-item').forEach(item => { + item.classList.remove('active'); + const navSection = item.getAttribute('data-section'); + if (navSection === sectionId) { + item.classList.add('active'); + } + }); + + // If it's subjects, make sure the subjects nav is active + if (sectionId === 'subjectsSection') { + document.querySelectorAll('.nav-item').forEach(item => { + const navSection = item.getAttribute('data-section'); + if (navSection === 'subjects') { + item.classList.add('active'); + } + }); + } + } + } + + // Hide attendance form container by default + const attendanceFormContainer = document.getElementById('attendanceFormContainer'); + if (attendanceFormContainer) { + attendanceFormContainer.style.display = 'none'; + } + + // Set today's date as default for date inputs + const dateInputs = document.querySelectorAll('input[type="date"]'); + const today = new Date().toISOString().split('T')[0]; + + dateInputs.forEach(input => { + // Don't set a default value for the attendance date input + if (input.id !== 'dateSelect' && !input.value) { + input.value = today; + console.log('Set date for input:', input.id); + // Trigger any change event listeners + const event = new Event('change'); + input.dispatchEvent(event); + } + }); + + // Add event listeners for routine selection form + const routineSelect = document.getElementById('routineSelect'); + const dateSelect = document.getElementById('dateSelect'); + + if (routineSelect) { + // Remove auto-selection of first routine option + // We want nothing selected by default + + routineSelect.addEventListener('change', function() { + console.log('Routine changed:', this.value); + loadAttendanceData(); + }); + } + + if (dateSelect) { + dateSelect.addEventListener('change', function() { + console.log('Date changed:', this.value); + loadAttendanceData(); + }); + } + + // Add event listeners for edit buttons + document.querySelectorAll('.edit-attendance-btn').forEach(button => { + button.addEventListener('click', function() { + const routineId = this.getAttribute('data-routine-id'); + const date = this.getAttribute('data-date'); + console.log('Edit button clicked:', { routineId, date }); + editAttendance(routineId, date); + }); + }); + + // Add event listeners for today's classes attendance buttons + document.querySelectorAll('.take-attendance-btn, .attendance-btn, .today-attendance-btn').forEach(button => { + button.addEventListener('click', function() { + console.log('Today attendance button clicked'); + const routineId = this.getAttribute('data-routine-id'); + const date = this.getAttribute('data-date') || today; + + if (routineId) { + console.log('Taking attendance for routine:', routineId, 'date:', date); + + // If this is a quick attendance button, open the modal + if (this.classList.contains('quick-attendance')) { + openAttendanceModal(routineId, date); + } else { + // Otherwise go to manage attendance view + editAttendance(routineId, date); + } + } else { + console.error('Missing routine ID for attendance button'); + } + }); + }); + + // Load initial data if values are present + if (routineSelect && dateSelect && routineSelect.value && dateSelect.value) { + loadAttendanceData(); + } + + // Initialize file upload form submission + const uploadForm = document.getElementById('uploadFileForm'); + if (uploadForm) { + uploadForm.addEventListener('submit', function(e) { + e.preventDefault(); + + const formData = new FormData(uploadForm); + const submitBtn = uploadForm.querySelector('button[type="submit"]'); + const responseDiv = document.getElementById('uploadResponse'); + + // Disable the submit button and show loading state + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.innerHTML = ' Uploading...'; + } + + fetch('/app/manage-subject-files/', { + method: 'POST', + body: formData, + credentials: 'same-origin', + }) + .then(response => response.json()) + .then(data => { + if (responseDiv) { + responseDiv.style.display = 'block'; + responseDiv.className = data.success ? 'alert alert-success' : 'alert alert-danger'; + responseDiv.innerHTML = data.message; + } + + if (data.success) { + // Reload materials after successful upload + setTimeout(() => { + // Get the subject ID that was selected + const subjectId = document.getElementById('subject_id').value; + loadSubjectMaterials(subjectId); + closeUploadModal(); + }, 1500); + } + }) + .catch(error => { + console.error('Error:', error); + if (responseDiv) { + responseDiv.style.display = 'block'; + responseDiv.className = 'alert alert-danger'; + responseDiv.innerHTML = 'An error occurred while uploading the file. Please try again.'; + } + }) + .finally(() => { + if (submitBtn) { + submitBtn.disabled = false; + submitBtn.innerHTML = ' Upload'; + } + }); + }); + } + + // Initialize delete confirmation button + const confirmDeleteBtn = document.getElementById('confirmDeleteBtn'); + if (confirmDeleteBtn) { + confirmDeleteBtn.addEventListener('click', function() { + if (!fileIdToDelete) { + closeConfirmationModal(); + return; + } + + // Disable the button and show loading state + confirmDeleteBtn.disabled = true; + confirmDeleteBtn.innerHTML = ' Deleting...'; + + fetch('/app/delete-subject-file/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value + }, + body: JSON.stringify({ file_id: fileIdToDelete }) + }) + .then(response => response.json()) + .then(data => { + closeConfirmationModal(); + + if (data.success) { + // Store the current subject ID if displayed in the UI + const currentSubjectHeading = document.querySelector('#subjectMaterialsContent .card-header h5'); + let currentSubjectId = null; + + if (currentSubjectHeading) { + // Find the subject ID from the upload button + const uploadBtn = currentSubjectHeading.closest('.card-header').querySelector('[data-subject-id]'); + if (uploadBtn) { + currentSubjectId = uploadBtn.getAttribute('data-subject-id'); + } + } + + // Reload the materials section with the same subject if we had one + loadSubjectMaterials(currentSubjectId); + } else { + alert(data.message || 'Error deleting file.'); + } + }) + .catch(error => { + console.error('Error:', error); + closeConfirmationModal(); + alert('An error occurred while deleting the file. Please try again.'); + }) + .finally(() => { + confirmDeleteBtn.disabled = false; + confirmDeleteBtn.innerHTML = ' Delete'; + }); + }); + } + } catch (error) { + console.error('Error initializing teacher dashboard:', error); + } +}); \ No newline at end of file diff --git a/static/js/teacher-materials.js b/static/js/teacher-materials.js new file mode 100644 index 0000000..304af48 --- /dev/null +++ b/static/js/teacher-materials.js @@ -0,0 +1,941 @@ +// Subject Materials Section Functions + +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 + +
+ `; + }); +} + +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(); + } + + if (responseDiv) { + responseDiv.style.display = 'none'; + } + + // If a subject ID was provided, pre-select it + if (subjectId && subjectSelect) { + subjectSelect.value = subjectId; + } + + modal.style.display = 'block'; + document.body.style.overflow = 'hidden'; +} + +function closeUploadModal() { + const modal = document.getElementById('uploadFileModal'); + if (modal) { + modal.style.display = 'none'; + document.body.style.overflow = ''; + } +} + +function deleteFile(fileId) { + fileIdToDelete = fileId; + const modal = document.getElementById('confirmationModal'); + if (modal) { + modal.style.display = 'block'; + document.body.style.overflow = 'hidden'; + } +} + +function closeConfirmationModal() { + const modal = document.getElementById('confirmationModal'); + if (modal) { + modal.style.display = 'none'; + document.body.style.overflow = ''; + } + 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 diff --git a/static/js/teacher-utils.js b/static/js/teacher-utils.js new file mode 100644 index 0000000..c388481 --- /dev/null +++ b/static/js/teacher-utils.js @@ -0,0 +1,217 @@ +// Utility functions for the teacher dashboard + +// Global variables and constants +let fileIdToDelete = null; +const API_BASE_URL = '/app'; +const DEFAULT_ERROR_MESSAGE = 'An error occurred. Please try again.'; + +// Type checking utilities +const isString = (value) => typeof value === 'string'; +const isObject = (value) => value !== null && typeof value === 'object'; +const isEmpty = (value) => value === null || value === undefined || value === ''; + +// CSRF token management +function getCSRFToken() { + try { + const cookies = document.cookie.split(';'); + const csrfCookie = cookies.find(cookie => cookie.trim().startsWith('csrftoken=')); + return csrfCookie ? decodeURIComponent(csrfCookie.split('=')[1]) : null; + } catch (error) { + console.error('Error getting CSRF token:', error); + return null; + } +} + +// API request helper with better error handling and retries +async function apiRequest(url, options = {}, retries = 3) { + const defaultOptions = { + headers: { + 'X-CSRFToken': getCSRFToken(), + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + credentials: 'same-origin' + }; + + for (let attempt = 1; attempt <= retries; attempt++) { + try { + const response = await fetch(url, { ...defaultOptions, ...options }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error || `HTTP error! Status: ${response.status}`); + } + + return await response.json(); + } catch (error) { + if (attempt === retries) { + console.error(`API Request Error (attempt ${attempt}/${retries}):`, error); + throw error; + } + await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); // Exponential backoff + } + } +} + +// UI Helpers with enhanced styling and accessibility +function showLoadingSpinner(container, message = 'Loading...') { + if (!container) return; + + container.innerHTML = ` +
+ +

${message}

+
+ `; +} + +function showError(container, error, withRetryButton = false) { + if (!container) return; + + const errorMessage = error?.message || DEFAULT_ERROR_MESSAGE; + container.innerHTML = ` + + ${withRetryButton ? ` +
+ +
+ ` : ''} + `; +} + +function showSuccess(container, message, autoHide = true) { + if (!container) return; + + container.innerHTML = ` + + `; + + if (autoHide) { + setTimeout(() => { + const alert = container.querySelector('.alert'); + if (alert) { + alert.classList.remove('show'); + setTimeout(() => alert.remove(), 150); + } + }, 5000); + } +} + +// Modal Helpers with enhanced functionality +function showModal(modalId) { + const modal = document.getElementById(modalId); + if (!modal) return; + + modal.style.display = 'block'; + document.body.style.overflow = 'hidden'; + + // Focus trap for accessibility + const focusableElements = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); + if (focusableElements.length > 0) { + focusableElements[0].focus(); + } +} + +function hideModal(modalId) { + const modal = document.getElementById(modalId); + if (!modal) return; + + modal.style.display = 'none'; + document.body.style.overflow = ''; +} + +// Form Helpers with validation +function resetForm(formOrId) { + let form; + + // Handle both form element and form ID + if (typeof formOrId === 'string') { + form = document.getElementById(formOrId); + } else if (formOrId instanceof HTMLFormElement) { + form = formOrId; + } else { + console.warn('Invalid form reference:', formOrId); + return; + } + + if (!form) { + console.warn('Form not found:', formOrId); + return; + } + + // Reset form values + form.reset(); + + // Clear all alerts and validation states + form.querySelectorAll('.alert, .is-invalid, .is-valid').forEach(el => { + el.classList.remove('alert', 'is-invalid', 'is-valid'); + }); + + // Reset all checkboxes to unchecked state + form.querySelectorAll('input[type="checkbox"]').forEach(checkbox => { + checkbox.checked = false; + }); + + // Reset all select elements to first option + form.querySelectorAll('select').forEach(select => { + if (select.options.length > 0) { + select.selectedIndex = 0; + } + }); +} + +// Date Helpers with enhanced formatting options +function formatDate(date, format = 'YYYY-MM-DD') { + const d = new Date(date); + if (isNaN(d.getTime())) return ''; + + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + + return format + .replace('YYYY', year) + .replace('MM', month) + .replace('DD', day); +} + +function getCurrentDate() { + return formatDate(new Date()); +} + +// Validation Helpers +function validateEmail(email) { + const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return re.test(email); +} + +function validatePhone(phone) { + const re = /^\+?[\d\s-]{10,}$/; + return re.test(phone); +} + +// Export all functions +window.getCSRFToken = getCSRFToken; +window.apiRequest = apiRequest; +window.showLoadingSpinner = showLoadingSpinner; +window.showError = showError; +window.showSuccess = showSuccess; +window.showModal = showModal; +window.hideModal = hideModal; +window.resetForm = resetForm; +window.formatDate = formatDate; +window.getCurrentDate = getCurrentDate; +window.validateEmail = validateEmail; +window.validatePhone = validatePhone; \ No newline at end of file diff --git a/student_management_system/jazzmin.py b/student_management_system/jazzmin.py index 4cff028..8a3e150 100644 --- a/student_management_system/jazzmin.py +++ b/student_management_system/jazzmin.py @@ -1,56 +1,98 @@ JAZZMIN_SETTINGS = { - "site_title": "Student Management System Admin", - "site_header": "Student Management System", + "site_title": "Student Management System", + "site_header": "Student Management", "site_brand": "SMS", - "site_logo": "/dist/img/logo.png", + "site_logo": "img/logo.png", + "login_logo": "img/logo.png", + "login_logo_dark": None, "site_logo_classes": "img-circle", - "site_icon": "/dist/img/logo.png", - "welcome_sign": "Welcome to the Student Management System", - "copyright": "ADDRSSS", - ############ - # Top Menu # - ############ + "login_logo_classes": "", + "custom_css": "css/custom.css", + "welcome_sign": "Welcome to Student Management System", + "copyright": "Student Management System", + "search_model": ["app.Student", "app.Staff", "app.Course"], "topmenu_links": [ - {"name": "Home", "url": "admin:index", "permissions": ["auth.view_user"]}, - {"name": "Dashboard", "url": "dashboard", "permissions": ["auth.view_user"]}, + {"name": "Dashboard", "url": "admin:index", "permissions": ["auth.view_user"]}, + {"model": "auth.User"}, ], - ############# - # Side Menu # - ############# + "usermenu_links": [{"model": "auth.user"}], "show_sidebar": True, "navigation_expanded": True, - # List of apps (and/or models) to base side menu ordering off of (does not need to contain all apps/models) - "order_with_respect_to": ["auth", "dashboard", "app"], - "custom_links": { - "app": [ - { - "name": "Dashboard", - "url": "dashboard", - "icon": "fas fa-tachometer-alt", - }, - ], - }, - - + "hide_apps": [], + "hide_models": [], + "order_with_respect_to": [ + "dashboard", + "authentication", + "organization", + "human_managementorganization.institute", + "organization.batch", + "organization.course", + "human_management.student", + "human_management.staff", + "human_management.routine", + "human_management.staff_leave", + "human_management.student_leave", + "human_management.studentfeedback", + "human_management.coursetracking", + "human_management.attendance", + "human_management.institutefeedback", + ], "icons": { "auth": "fas fa-users-cog", "auth.user": "fas fa-user", - "auth.group": "fas fa-users", + "auth.Group": "fas fa-users", "app.student": "fas fa-user-graduate", - "app.teacher": "fas fa-chalkboard-teacher", + "app.staff": "fas fa-chalkboard-teacher", "app.course": "fas fa-book", "app.subject": "fas fa-book-open", - "app.academicyear": "fas fa-calendar", "app.attendance": "fas fa-calendar-check", - "app.attendancereport": "fas fa-clipboard-list", - "app.leavereportstudent": "fas fa-user-clock", - "app.leavereportteacher": "fas fa-clock", - "app.feedbackstudent": "fas fa-comment-alt", - "app.feedbackteacher": "fas fa-comments", + "app.studentfeedback": "fas fa-comment", + "app.institutefeedback": "fas fa-comments", + "app.student_leave": "fas fa-calendar-times", + "app.staff_leave": "fas fa-calendar-minus", + "app.notice": "fas fa-bullhorn", }, + "default_icon_parents": "fas fa-chevron-circle-right", + "default_icon_children": "fas fa-circle", + "related_modal_active": False, + "custom_js": None, + "show_ui_builder": False, + "changeform_format": "horizontal_tabs", + "changeform_format_overrides": { + "auth.user": "collapsible", + "auth.group": "vertical_tabs", + }, + "language_chooser": False, } JAZZMIN_UI_TWEAKS = { - "theme": "lumen", - # "dark_mode_theme": "cyborg", + "navbar_small_text": False, + "footer_small_text": False, + "body_small_text": False, + "brand_small_text": False, + "brand_colour": False, + "accent": "accent-primary", + "navbar": "navbar-dark", + "no_navbar_border": False, + "navbar_fixed": False, + "layout_boxed": False, + "footer_fixed": False, + "sidebar_fixed": False, + "sidebar": "sidebar-dark-primary", + "sidebar_nav_small_text": False, + "sidebar_disable_expand": False, + "sidebar_nav_child_indent": False, + "sidebar_nav_compact_style": False, + "sidebar_nav_legacy_style": False, + "sidebar_nav_flat_style": False, + "theme": "default", + "dark_mode_theme": None, + "button_classes": { + "primary": "btn-primary", + "secondary": "btn-secondary", + "info": "btn-info", + "warning": "btn-warning", + "danger": "btn-danger", + "success": "btn-success" + } } diff --git a/student_management_system/settings.py b/student_management_system/settings.py index 88327e7..f121cad 100644 --- a/student_management_system/settings.py +++ b/student_management_system/settings.py @@ -1,7 +1,7 @@ """ Django settings for student_management_system project. -Generated by 'django-admin startproject' using Django 5.1.4. +Generated by 'django-admin startproject' using Django 5.1.6. For more information on this file, see https://docs.djangoproject.com/en/5.1/topics/settings/ @@ -11,25 +11,26 @@ https://docs.djangoproject.com/en/5.1/ref/settings/ """ import os +from pathlib import Path +from dotenv import load_dotenv from student_management_system.jazzmin import JAZZMIN_SETTINGS, JAZZMIN_UI_TWEAKS -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +# Load environment variables +load_dotenv() +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "django-insecure-)bp&!(x=q)##0hl&7r&h2x$u_3bvd7td)yq4y&_k)$)ce%pj&m" +SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = os.getenv('DJANGO_DEBUG', 'False') == 'True' -ALLOWED_HOSTS = ["*"] - -JAZZMIN_SETTINGS = JAZZMIN_SETTINGS -JAZZMIN_UI_TWEAKS = JAZZMIN_UI_TWEAKS +ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', '*').split(',') # Application definition @@ -41,11 +42,14 @@ INSTALLED_APPS = [ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "rest_framework", + "mathfilters", "app", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", @@ -54,6 +58,11 @@ MIDDLEWARE = [ "django.middleware.clickjacking.XFrameOptionsMiddleware", ] +AUTHENTICATION_BACKENDS = [ + "app.backends.MultiModelBackend", + "django.contrib.auth.backends.ModelBackend", +] +REST_FRAMEWORK = {"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.AllowAny"]} ROOT_URLCONF = "student_management_system.urls" TEMPLATES = [ @@ -120,22 +129,54 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.1/howto/static-files/ -STATIC_URL = "static/" +STATIC_URL = "/static/" STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static"), ] STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") - STATICFILES_FINDERS = [ "django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders.AppDirectoriesFinder", ] -MEDIA_URL = "/media/" -MEDIA_ROOT = os.path.join(BASE_DIR, "media") - +# Configure whitenoise +STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" # Default primary key field type # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +MEDIA_URL = "/media/" +MEDIA_ROOT = os.path.join(BASE_DIR, "media") + + +# Jazmin Settings +JAZZMIN_SETTINGS = JAZZMIN_SETTINGS +JAZZMIN_UI_TWEAKS = JAZZMIN_UI_TWEAKS + +# Login redirect URL +LOGIN_REDIRECT_URL = '/app/dashboard/' +LOGIN_URL = '/login/' + +# Email Configuration +EMAIL_BACKEND = os.getenv('EMAIL_BACKEND', 'django.core.mail.backends.console.EmailBackend') +DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'noreply@studentmanagementsystem.com') + +# Cache configuration for OTP storage +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': os.getenv('CACHE_LOCATION', 'unique-snowflake'), + } +} + +# SMS API configuration +SMS_API_KEY = os.getenv('SMS_API_KEY', '') +SMS_SENDER_ID = os.getenv('SMS_SENDER_ID', 'SMSSYS') + + +CSRF_TRUSTED_ORIGINS = [ + "https://*.ngrok-free.app", + "https://*.ngrok.io" +] diff --git a/student_management_system/urls.py b/student_management_system/urls.py index 661260f..e40550d 100644 --- a/student_management_system/urls.py +++ b/student_management_system/urls.py @@ -19,12 +19,32 @@ from django.contrib import admin from django.urls import path, include from django.conf import settings from django.conf.urls.static import static +from django.contrib.auth import views as auth_views +from app.admin import custom_admin_site +from django.views.generic import RedirectView, TemplateView +from app import views +from app import auth -urlpatterns = ( - [ - path("admin/", admin.site.urls), - path("", include("app.urls")), - ] - + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) - + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) -) +# Main URL patterns for the project +urlpatterns = [ + path("app/", include("app.urls")), + path("admin/", custom_admin_site.urls), + + # Root URL redirects to dashboard + 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), +] + +# Add static and media file serving in development +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 diff --git a/templates/admin/app/routine/change_form.html b/templates/admin/app/routine/change_form.html new file mode 100644 index 0000000..dc240da --- /dev/null +++ b/templates/admin/app/routine/change_form.html @@ -0,0 +1,555 @@ +{% extends "admin/change_form.html" %} +{% load static %} +{% load mathfilters %} + +{% block extrahead %} +{{ block.super }} + + + +{% endblock %} + +{% block content %} +
+
+

Add Routines

+
+
+
+ {% csrf_token %} + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+ + + + + + + + + + + + + + + +
SubjectTeacherStart TimeEnd TimeAction
+ Please select course and semester +
+
+ +
+
+ +
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..64e8508 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,373 @@ +{% load static %} + + + + + + + + + + {% block title %}Student Management System{% endblock %} + + + + + + + + + + + + + + + + + {% block extrahead %}{% endblock %} + + + {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} + + {% block content %}{% endblock %} + + + + + + + + + + + + + + + + + + + + + + + + + + {% block extrajs %}{% endblock %} + + \ No newline at end of file diff --git a/templates/components/bottom_nav.html b/templates/components/bottom_nav.html new file mode 100644 index 0000000..54a7597 --- /dev/null +++ b/templates/components/bottom_nav.html @@ -0,0 +1,134 @@ + + + + + diff --git a/templates/components/common_modals.html b/templates/components/common_modals.html new file mode 100644 index 0000000..4bd8ed3 --- /dev/null +++ b/templates/components/common_modals.html @@ -0,0 +1,114 @@ +{% load static %} + + + + + + + + + + + + \ No newline at end of file diff --git a/templates/components/student_feedback_modal.html b/templates/components/student_feedback_modal.html new file mode 100644 index 0000000..de39b50 --- /dev/null +++ b/templates/components/student_feedback_modal.html @@ -0,0 +1,230 @@ + + + + + + + + \ No newline at end of file diff --git a/templates/components/student_modals.html b/templates/components/student_modals.html new file mode 100644 index 0000000..2d9cafd --- /dev/null +++ b/templates/components/student_modals.html @@ -0,0 +1,32 @@ +{% load static %} + + + + + + + + + \ No newline at end of file diff --git a/templates/components/teacher_feedback_modal.html b/templates/components/teacher_feedback_modal.html new file mode 100644 index 0000000..f539e98 --- /dev/null +++ b/templates/components/teacher_feedback_modal.html @@ -0,0 +1,109 @@ + + + + + + + + \ No newline at end of file diff --git a/templates/components/teacher_modals.html b/templates/components/teacher_modals.html new file mode 100644 index 0000000..30e9ff7 --- /dev/null +++ b/templates/components/teacher_modals.html @@ -0,0 +1,63 @@ + + + + \ No newline at end of file diff --git a/templates/components/welcome_section.html b/templates/components/welcome_section.html new file mode 100644 index 0000000..04c9957 --- /dev/null +++ b/templates/components/welcome_section.html @@ -0,0 +1,32 @@ +{% 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 new file mode 100644 index 0000000..8400e96 --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,1048 @@ +{% 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/add_academicyear.html b/templates/hod/add_academicyear.html deleted file mode 100644 index fc017b3..0000000 --- a/templates/hod/add_academicyear.html +++ /dev/null @@ -1,135 +0,0 @@ -{% extends "admin/base_site.html" %} -{% block page_title %}{{ title }}{% endblock %} -{% load static %} -{% block extrastyle %} - - - -{% endblock %} -{% block content %} - -
-
-
-
- {% csrf_token %} - -
- - -
Please provide a valid start date
-
- {% if messages %} -
- {% for message in messages %} -
- {{ message }} - -
- {% endfor %} -
- {% endif %} -
- - Cancel - - -
-
-
-
-
- {% block extrajs %} - - - {% endblock %} -{% endblock %} diff --git a/templates/hod/add_course.html b/templates/hod/add_course.html deleted file mode 100644 index 86b9ae1..0000000 --- a/templates/hod/add_course.html +++ /dev/null @@ -1,138 +0,0 @@ -{% extends "admin/base_site.html" %} -{% block page_title %}Add Course{% endblock %} -{% load static %} -{% block extrastyle %} - - - -{% endblock %} -{% block content %} - -
-
-
-

{{ title|default:"Add New Course" }}

-
-
-
- {% csrf_token %} - -
- - -
Course name must be unique
-
Please enter a course name
-
- -
- - -
Please select a course type
-
- -
- - -
Specify the duration
-
Please enter a valid duration (minimum 1 month)
-
- {% if messages %} -
- {% for message in messages %} -
- {{ message }} - -
- {% endfor %} -
- {% endif %} -
- - Cancel - - -
-
-
-
-
- {% block extrajs %} - - - {% endblock %} -{% endblock %} diff --git a/templates/hod/add_student.html b/templates/hod/add_student.html deleted file mode 100644 index 11567eb..0000000 --- a/templates/hod/add_student.html +++ /dev/null @@ -1,149 +0,0 @@ -{% extends "admin/base_site.html" %} -{% block page_title %}{{ title }}{% endblock %} -{% load static %} -{% block extrastyle %} - -{% endblock %} -{% block content %} -
-

Add Student

-
- {% csrf_token %} -
- -
- - -
-
- - -
-
- - -
- -
- - -
-
- - -
- -
- - -
-
- - -
-
- - -
-
- - -
- -
- - -
- -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
- - -
-
- - -
-
- - -
-
-
- - Cancel -
-
-
-{% endblock %} diff --git a/templates/hod/add_subject.html b/templates/hod/add_subject.html deleted file mode 100644 index dfb7e2c..0000000 --- a/templates/hod/add_subject.html +++ /dev/null @@ -1,54 +0,0 @@ -{% extends "admin/base_site.html" %} -{% block page_title %}Add Subject{% endblock %} -{% load static %} -{% block extrastyle %} - - - -{% endblock %} -{% block content %} - -
-
-
-
- {% csrf_token %} - {{ form.as_p }} - -
-
-
-
-{% endblock %} diff --git a/templates/hod/add_teacher.html b/templates/hod/add_teacher.html deleted file mode 100644 index 06dc64e..0000000 --- a/templates/hod/add_teacher.html +++ /dev/null @@ -1,100 +0,0 @@ -{% extends 'admin/base_site.html' %} -{% block content %} -
-

{{ title }}

-
- {% csrf_token %} -
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
- - Cancel -
-
-
-{% endblock %} diff --git a/templates/hod/dashboard.html b/templates/hod/dashboard.html deleted file mode 100644 index dc081d5..0000000 --- a/templates/hod/dashboard.html +++ /dev/null @@ -1,55 +0,0 @@ -{% extends 'admin/base_site.html' %} -{% block content %} -
-
-
-
-
- -
-
-
Total Students
-

{{ student_count }}

-
-
-
- -
-
-
- -
-
-
Total Teachers
-

{{ teacher_count }}

-
-
-
- -
-
-
- -
-
-
Total Courses
-

{{ course_count }}

-
-
-
- -
-
-
- -
-
-
Total Subjects
-

{{ subject_count }}

-
-
-
- -
-
-{% endblock %} diff --git a/templates/hod/edit_academicyear.html b/templates/hod/edit_academicyear.html deleted file mode 100644 index f65c802..0000000 --- a/templates/hod/edit_academicyear.html +++ /dev/null @@ -1,94 +0,0 @@ -{% extends "admin/base_site.html" %} -{% block page_title %}{{ title }}{% endblock %} -{% load static %} -{% block extrastyle %} - - - -{% endblock %} -{% block content %} - -
-
-
-
- {% csrf_token %} -
- - - -
-
- -
-
-
-
-
-{% endblock %} diff --git a/templates/hod/edit_course.html b/templates/hod/edit_course.html deleted file mode 100644 index 55b5502..0000000 --- a/templates/hod/edit_course.html +++ /dev/null @@ -1,152 +0,0 @@ -{% extends "admin/base_site.html" %} -{% block page_title %}{{ title }}{% endblock %} -{% load static %} -{% block extrastyle %} - - - -{% endblock %} -{% block content %} - -
-
-
-

Edit Course: {{ course.name }}

-
-
-
- {% csrf_token %} - -
- - -
Course name must be unique
-
Please enter a course name
-
- -
- - -
Please select a course type
-
- -
- - -
Specify the duration in months
-
Please enter a valid duration (minimum 1 month)
-
- {% if messages %} -
- {% for message in messages %} -
- {{ message }} - -
- {% endfor %} -
- {% endif %} -
- - Cancel - - -
-
-
-
-
- {% block extrajs %} - - - {% endblock %} -{% endblock %} diff --git a/templates/hod/edit_student.html b/templates/hod/edit_student.html deleted file mode 100644 index 00abaef..0000000 --- a/templates/hod/edit_student.html +++ /dev/null @@ -1,212 +0,0 @@ -{% extends "admin/base_site.html" %} -{% load static %} -{% block extrastyle %} - - - -{% endblock %} -{% block content %} -
- -
- {% csrf_token %} -
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
- - Cancel -
-
-
-{% endblock %} diff --git a/templates/hod/edit_subject.html b/templates/hod/edit_subject.html deleted file mode 100644 index c750ca8..0000000 --- a/templates/hod/edit_subject.html +++ /dev/null @@ -1,54 +0,0 @@ -{% extends "admin/base_site.html" %} -{% block page_title %}Edit Subject{% endblock %} -{% load static %} -{% block extrastyle %} - - - -{% endblock %} -{% block content %} - -
-
-
-
- {% csrf_token %} - {{ form.as_p }} - -
-
-
-
-{% endblock %} diff --git a/templates/hod/edit_teacher.html b/templates/hod/edit_teacher.html deleted file mode 100644 index 771fc5c..0000000 --- a/templates/hod/edit_teacher.html +++ /dev/null @@ -1,126 +0,0 @@ -{% extends 'admin/base_site.html' %} -{% block content %} -
-

Edit Teacher

-
- {% csrf_token %} -
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
-
- - Cancel -
-
-
-{% endblock %} diff --git a/templates/hod/manage_academicyear.html b/templates/hod/manage_academicyear.html deleted file mode 100644 index 520c9ba..0000000 --- a/templates/hod/manage_academicyear.html +++ /dev/null @@ -1,181 +0,0 @@ -{% extends "admin/base_site.html" %} -{% block page_title %}{{ title }}{% endblock %} -{% load static %} -{% block extrastyle %} - - - -{% endblock %} -{% block content %} - -
-
-
- -
-
- -
-
- {% if academic_years %} -
- - - - - - - - - - - {% for academic_year in academic_years %} - - - - - - - {% endfor %} - -
S.NStart DateCreated AtActions
{{ forloop.counter }}{{ academic_year.start_date }}{{ academic_year.created_at }} - - Edit - - - Delete - -
-
- {% else %} -
- -

No Academic Years Found

-

Start by adding your first academic year using the button above.

-
- {% endif %} -
-
-
- {% block extrajs %} - - - {% endblock %} -{% endblock %} diff --git a/templates/hod/manage_course.html b/templates/hod/manage_course.html deleted file mode 100644 index 7352948..0000000 --- a/templates/hod/manage_course.html +++ /dev/null @@ -1,194 +0,0 @@ -{% extends "admin/base_site.html" %} {% block page_title %}{{ title }} -{%endblock %} {% load static %} {% block extrastyle %} - - - -{% endblock %} {% block content %} - - -
-
-
- -
-
- -
-
- - {% if courses %} -
- - - - - - - - - - - - {% for course in courses %} - - - - - - - - {% endfor %} - -
S.NCourse NameCourse TypeDurationActions
{{ forloop.counter }}{{ course.name }}{{ course.get_course_type_display }} - {{ course.duration }} {% if course.course_type == 'semester' %} - Semesters {% else %} Years {% endif %} - - - Edit - - - Delete - -
-
- {% else %} -
- -

No Courses Found

-

- Start by adding your first course using the button above. -

-
- {% endif %} -
-
-
- -{% block extrajs %} - - -{% endblock %} {% endblock %} diff --git a/templates/hod/manage_student.html b/templates/hod/manage_student.html deleted file mode 100644 index 7dd536e..0000000 --- a/templates/hod/manage_student.html +++ /dev/null @@ -1,65 +0,0 @@ -{% extends "admin/base_site.html" %} -{% block page_title %}{{ title }}{% endblock %} -{% block content %} -
-
-
-

Manage Students

-
- -
-
-
-
- -
-
- -
-
-
-
- - - - - - - - - - - - - - {% for student in students %} - - - - - - - - - - {% empty %} - - - - {% endfor %} - -
S.NFull NameCourseAcademic YearPhoneEmailActions
{{ forloop.counter }}{{ student.first_name }}{{ student.course.name }}{{ student.academic_year.start_date|date:"Y" }}{{ student.phone }}{{ student.email }} - Edit - Delete -
No students found
-
-
-{% endblock %} diff --git a/templates/hod/manage_subject.html b/templates/hod/manage_subject.html deleted file mode 100644 index 78ba22a..0000000 --- a/templates/hod/manage_subject.html +++ /dev/null @@ -1,190 +0,0 @@ -{% extends "admin/base_site.html" %} -{% block page_title %}{{ title }}{% endblock %} -{% load static %} -{% block extrastyle %} - - - -{% endblock %} -{% block content %} - -
-
-
- -
-
- -
-
- {% if subjects %} -
- - - - - - - - - - - - {% for subject in subjects %} - - - - - - - - {% endfor %} - -
S.NSubject NameCourseDurationActions
{{ forloop.counter }}{{ subject.name }}{{ subject.course.name }} - {{ subject.duration }} - {% if subject.course.course_type == 'semester' %} - Semesters - {% else %} - Years - {% endif %} - - - Edit - - - Delete - -
-
- {% else %} -
- -

No Subjects Found

-

Start by adding your first subject using the button above.

-
- {% endif %} -
-
-
- {% block extrajs %} - - - {% endblock %} -{% endblock %} diff --git a/templates/hod/manage_teacher.html b/templates/hod/manage_teacher.html deleted file mode 100644 index cbd8cd2..0000000 --- a/templates/hod/manage_teacher.html +++ /dev/null @@ -1,43 +0,0 @@ -{% extends 'admin/base_site.html' %} -{% block content %} -
-

Manage Teachers

- Add Teacher - - - - - - - - - - - - - - - - {% for teacher in teachers %} - - - - - - - - - - - - {% endfor %} - -
S.NNameEmailPhoneGenderQualificationMarital StatusDate of JoiningActions
{{ forloop.counter }}{{ teacher.first_name }} {{ teacher.middle_name }} {{ teacher.last_name }}{{ teacher.email }}{{ teacher.phone }}{{ teacher.get_gender_display }}{{ teacher.get_qualification_display }}{{ teacher.get_marital_status_display }}{{ teacher.date_of_joining }} - Edit - Delete -
-
-{% endblock %} diff --git a/templates/hod/student_feedback.html b/templates/hod/student_feedback.html deleted file mode 100644 index d495376..0000000 --- a/templates/hod/student_feedback.html +++ /dev/null @@ -1,144 +0,0 @@ -{% extends 'admin/base_site.html' %} -{% block content %} - {% load static %} -
-
-
-
- -
-
-

Student Feedback

-
- -
- - - - - - - - - - - - {% for feedback in feedbacks %} - - - - - - - - - - - {% endfor %} -
S.NStudent NameStudent SessionMessageSended OnReply
{{ forloop.counter }}{{ feedback.student_id.first_name }}{{ feedback.student_id.academic_year.start_date }}{{ feedback.feedback }}{{ feedback.created_at }} - {% if feedback.feedback_reply == "" %} - - {% else %} - {{ feedback.feedback_reply }} - {% endif %} -
-
-
- -
-
-
- -
- {% comment %} Modal to Reply Feedback {% endcomment %} - - -{% endblock %} -{% comment %} Custom JS to Identify the Reply and Post Reply {% endcomment %} -{% block custom_js %} - -{% endblock custom_js %} diff --git a/templates/hod/student_leave_view.html b/templates/hod/student_leave_view.html deleted file mode 100644 index 79db973..0000000 --- a/templates/hod/student_leave_view.html +++ /dev/null @@ -1,188 +0,0 @@ -{% extends "admin/base_site.html" %} -{% block page_title %}{{ title }}{% endblock %} -{% load static %} -{% block extrastyle %} - - - -{% endblock %} -{% block content %} - -
-
-
- -
-
- -
-
- {% if leaves %} -
- - - - - - - - - - - - - {% for leave in leaves %} - - - - - - - - - {% endfor %} - -
S.NStudent NameLeave DateLeave MessageApplied OnActions
{{ forloop.counter }}{{ leave.student_id.first_name }}{{ leave.leave_date }}{{ leave.leave_message }}{{ leave.created_at }} - {% if leave.leave_status == 0 %} - - Approve - - - Reject - - {% elif leave.leave_status == 1 %} - - {% else %} - - {% endif %} -
-
- {% else %} -
- -

No Leave Applications Found

-

There are no leave applications to display at the moment.

-
- {% endif %} -
-
-
- {% block extrajs %} - - - {% endblock %} -{% endblock %} diff --git a/templates/hod/teacher_feedback.html b/templates/hod/teacher_feedback.html deleted file mode 100644 index dac239c..0000000 --- a/templates/hod/teacher_feedback.html +++ /dev/null @@ -1,115 +0,0 @@ -{% extends 'admin/base_site.html' %} -{% block content %} - -
-
-
-
- -
-
-

Staff Feedback

-
- - -
- - - - - - - - - {% for feedback in feedbacks %} - - - - - - - - {% endfor %} -
S.NStaff NameMessageSended OnReply
{{ forloop.counter }}{{ feedback.teacher_id.first_name }} {{ feedback.teacher_id.last_name }}{{ feedback.feedback }}{{ feedback.created_at }} - {% if feedback.feedback_reply == "" %} - - {% else %} - {{ feedback.feedback_reply }} - {% endif %} -
-
-
- -
-
-
-
- - - -{% endblock %} -{% block custom_js %} - -{% endblock custom_js %} diff --git a/templates/hod/teacher_leave_view.html b/templates/hod/teacher_leave_view.html deleted file mode 100644 index 6a4a946..0000000 --- a/templates/hod/teacher_leave_view.html +++ /dev/null @@ -1,186 +0,0 @@ -{% extends "admin/base_site.html" %} -{% block page_title %}{{ title }}{% endblock %} -{% load static %} -{% block extrastyle %} - - - -{% endblock %} -{% block content %} - -
-
-
- -
-
- -
-
- {% if leaves %} -
- - - - - - - - - - - - - {% for leave in leaves %} - - - - - - - - - {% endfor %} - -
S.NTeacher NameLeave DateLeave MessageApplied OnActions
{{ forloop.counter }}{{ leave.teacher_id.first_name }}{{ leave.teacher_id.middle_name }} {{ leave.teacher_id.last_name }}{{ leave.leave_date }}{{ leave.leave_message }}{{ leave.created_at }} - {% if leave.leave_status == 0 %} - - Approve - - - Reject - - {% elif leave.leave_status == 1 %} - - {% else %} - - {% endif %} -
-
- {% else %} -
- -

No Leave Applications Found

-

There are no leave applications to display at the moment.

-
- {% endif %} -
-
-
- {% block extrajs %} - - - {% endblock %} -{% endblock %} diff --git a/templates/hod/view_attendance.html b/templates/hod/view_attendance.html deleted file mode 100644 index 0184633..0000000 --- a/templates/hod/view_attendance.html +++ /dev/null @@ -1,208 +0,0 @@ -{% extends 'admin/base_site.html' %} -{% block content %} - {% load static %} -
-
-
-
- -
-
-

View Attendance

-
- - {% comment %} Display Messages {% endcomment %} - {% if messages %} -
-
- {% for message in messages %} - {% if message.tags == "error" %} - - {% elif message.tags == "success" %} - - {% endif %} - {% endfor %} -
-
- {% endif %} -
-
- - -
-
- - -
-
- - - -
- - -
- - - {% comment %} {% endcomment %} -
- {% comment %} Displaying Students Here {% endcomment %} - -
- -
-
- - -
-{% endblock %} -{% block custom_js %} - -{% endblock custom_js %} diff --git a/templates/login/email_reset.html b/templates/login/email_reset.html new file mode 100644 index 0000000..71a32ac --- /dev/null +++ b/templates/login/email_reset.html @@ -0,0 +1,170 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+ +

Reset Password

+ +

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

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

Password Reset

+
+ +
+

Hello,

+

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

+

To reset your password, use the following code:

+
{{ reset_code }}
+

This code will expire in 5 minutes.

+

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

+

Thank you,
Student Management System Team

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

Password Reset Complete

+ +
✓
+ +
+

Your password has been successfully reset.

+
+ +

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

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

Set New Password

+ + {% if validlink %} +

Please enter your new password below.

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

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

+
+

Please request a new password reset link.

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

Email Sent

+ +
✓
+ +
+

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

+
+ +

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

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

Verify OTP

+ + {% if is_email %} +

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

+ {% else %} +

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

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

Reset Password

+ + {% if is_email %} +

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

+ {% else %} +

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

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

Set New Password

+ +

Please enter your new password below.

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

Verify OTP

+

OTP sent to: {{ phone }}

+ + {% if error %} +
+ {{ error }} +
+ {% endif %} + + {% if messages %} +
+ {% for message in messages %} + {{ message }} + {% endfor %} +
+ {% endif %} + +
+ {% csrf_token %} +
+ + + +
+ + + +
+ Resend OTP in 60s + + {% csrf_token %} + + + +
+ + + +
+ + + + +{% endblock %} \ No newline at end of file diff --git a/templates/login/reset_options.html b/templates/login/reset_options.html new file mode 100644 index 0000000..f365771 --- /dev/null +++ b/templates/login/reset_options.html @@ -0,0 +1,156 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} + + + + + + Reset Password - Student Management System + + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/login/sms_templates/otp_message.txt b/templates/login/sms_templates/otp_message.txt new file mode 100644 index 0000000..8dca73a --- /dev/null +++ b/templates/login/sms_templates/otp_message.txt @@ -0,0 +1,9 @@ +Dear {{ user_name }}, + +Your OTP for secure access is: {{ otp }}. Valid for {{ expiry_time }} minutes. + +For security, do not share this OTP. If you didn't request this, please ignore. + +Best regards, +Student Management System +support@sms.com \ No newline at end of file diff --git a/templates/login/verify_reset_code.html b/templates/login/verify_reset_code.html new file mode 100644 index 0000000..a2fa42c --- /dev/null +++ b/templates/login/verify_reset_code.html @@ -0,0 +1,61 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+
+
+

Reset Password

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

Didn't receive the code? Request a new one

+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/student/dashboard.html b/templates/student/dashboard.html deleted file mode 100644 index 8252370..0000000 --- a/templates/student/dashboard.html +++ /dev/null @@ -1,83 +0,0 @@ -{% extends "admin/base_site.html" %} -{% block content %} -

Dashboard

-

Welcome

- -
-
-
-
Course
-

DCOM

-
-
-
-
-
Current Semester
-

5

-
-
-
-
-
Total Subject
-

12

-
-
-
- -
- -
- -
- -
- -{% endblock %} diff --git a/templates/student_dashboard.html b/templates/student_dashboard.html new file mode 100644 index 0000000..1b916a5 --- /dev/null +++ b/templates/student_dashboard.html @@ -0,0 +1,843 @@ +{% extends 'base.html' %} +{% load static %} +{% load tz %} +{% load app_filters %} + + +{% block content %} + +{% csrf_token %} + +
+ + {% include 'components/welcome_section.html' with user=student %} + + +
+ +
+ +
+
+
+
Attendance Summary
+
+
+
+
+ Days Attended +

{{ attendance_summary.classes_attended }}

+ {% if attendance_summary.actual_classes %} + Classes Recorded +
{{ attendance_summary.actual_classes }}
+ {% endif %} +
+
+ Attendance Rate +

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

+ {% if attendance_summary.actual_percentage %} + Rate (Recorded Only) +
{{ attendance_summary.actual_percentage|floatformat:1 }}%
+ {% endif %} +
+
+
+
0%
+
100%
+
+
+ +
+
+
+ +
+ Minimum required: 75% + {% if attendance_summary.attendance_percentage < 75 %} + Below minimum requirement + {% endif %} +
+ +
+
+
+ + +
+
+
+
Course Progress
+
+
+
+
+ Total Courses +

{{ course_summary.total_courses }}

+
+
+ Completion Rate +

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

+
+
+
+
+ Completed + {{ course_summary.completed_courses }} +
+
+ Ongoing + {{ course_summary.ongoing_courses }} +
+
+
+
+
+ + +
+
+
+
Recent Activity
+
+
+
+ {% for leave in recent_leaves %} +
+ +
+ Leave Request ({{ leave.date|date:"M d" }}) + {{ leave.get_status_display }} + {{ leave.created_at|timesince }} ago +
+
+ {% endfor %} + + {% for feedback in recent_feedback %} +
+ +
+ Gave feedback {% if feedback.teacher %}to {{ feedback.teacher.name }}{% endif %} +
+ {% for i in "12345"|make_list %} + {% if forloop.counter <= feedback.rating %} + + {% else %} + + {% endif %} + {% endfor %} + {{ feedback.created_at|timesince }} ago +
+
+
+ {% endfor %} + + {% for notice in notices %} +
+ +
+ {{ notice.title }} + {{ notice.created_at|timesince }} ago +
+
+ {% endfor %} + + {% for record in attendance_records %} +
+ +
+ {% if record.student_attend %}Attended{% else %}Missed{% endif %} {{ record.attendance.routine.subject.name|truncatechars:15 }} + {{ record.attendance.date|date:"M d" }} +
+
+ {% endfor %} + + {% if not recent_leaves and not recent_feedback and not notices and not attendance_records %} +
+ No recent activity +
+ {% endif %} +
+
+
+
+ + +
+
+
+
Leave Status
+
+
+
+ Pending + {{ pending_leaves }} +
+
+ Approved + {{ approved_leaves }} +
+
+ Rejected + {{ rejected_leaves }} +
+ +
+
+
+
+ + +
+
+
+

+ Course Details +

+ + {% if course_details %} + {% for course in course_details %} +
+
+
{{ course.name }}
+ + {{ course.progress_status }} + +
+ + +
+
+ {{ course.completion_percentage }}% +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + {% if course.actual_end_date %} + + + + + {% endif %} + +
Start Date{{ course.start_date|date:"M d, Y" }}
Duration Type{{ course.duration_type }}
Total Duration + {% if course.duration_type == "Semester" %} + {{ course.duration }} years ({{ course.duration|multiply:2 }} semesters) + {% else %} + {{ course.duration }} years + {% endif %} +
Expected End Date{{ course.expected_end_date|date:"M d, Y" }}
Actual End Date{{ course.actual_end_date|date:"M d, Y" }}
+
+
+ + + + + + + + + + + + + + + {% if course.remaining_days is not None %} + + + + + {% endif %} + {% if course.total_remaining_days is not None %} + + + + + {% endif %} + +
Current Semester{{ course.current_semester }}
Semester Start{{ course.semester_start_date|date:"M d, Y" }}
Semester End{{ course.semester_end_date|date:"M d, Y" }}
Semester Days Remaining{{ course.remaining_days }} days
Total Course Days Remaining{{ course.total_remaining_days }} days
+
+
+
+ {% endfor %} + {% else %} +
+ No courses assigned yet. +
+ {% endif %} +
+
+
+
+ + + + + + + + + + + + +
+ + +{% include 'components/common_modals.html' with user=student %} + + +{% include 'components/student_modals.html' %} +{% include 'components/student_feedback_modal.html' %} + + + + + + +{% include 'components/bottom_nav.html' with user=student %} + + + + +{% endblock %} + diff --git a/templates/subject_files.html b/templates/subject_files.html new file mode 100644 index 0000000..a3b89b1 --- /dev/null +++ b/templates/subject_files.html @@ -0,0 +1,327 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} + +{% csrf_token %} + +
+ + {% include 'components/welcome_section.html' with user=teacher %} + + +
+
+
+
+

+ Manage Subject Materials +

+ + Back to Subjects + +
+ +
+ + {% if subjects %} +
+ {% for subject in subjects %} +
+
+
+
{{ subject.name }}
+

{{ subject.course }} - {{ subject.semester_or_year }}

+
+
+

+ {{ subject.files_count }} file(s) available +

+ + {% if subject.files %} +
+ {% for file in subject.files %} +
+
+
+
+ {% if file.file_type == 'syllabus' %} + + {% else %} + + {% endif %} + {{ file.title }} +
+

+ {% if file.description %}{{ file.description }}{% endif %} +

+ + {% if file.file_type == 'syllabus' %} + Syllabus + {% else %} + Study Material + {% endif %} + {% if file.uploaded_at %} + Uploaded {{ file.uploaded_at|timesince }} ago + {% endif %} + {% if file.uploaded_by %} + by {{ file.uploaded_by }} + {% endif %} + +
+
+ + Download + + {% if file.file_type == 'syllabus' %} + + View Syllabus + + {% else %} + + {% endif %} +
+
+
+ {% endfor %} +
+ {% else %} +
+ No files available for this subject. +
+ {% endif %} + +
+ +
+
+
+
+ {% endfor %} +
+ {% else %} +
+ You don't have any subjects assigned. +
+ {% endif %} +
+
+
+ + + + + + + + +{% include 'components/bottom_nav.html' with user=teacher %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/teacher/dashboard.html b/templates/teacher/dashboard.html deleted file mode 100644 index 8252370..0000000 --- a/templates/teacher/dashboard.html +++ /dev/null @@ -1,83 +0,0 @@ -{% extends "admin/base_site.html" %} -{% block content %} -

Dashboard

-

Welcome

- -
-
-
-
Course
-

DCOM

-
-
-
-
-
Current Semester
-

5

-
-
-
-
-
Total Subject
-

12

-
-
-
- -
- -
- -
- -
- -{% endblock %} diff --git a/templates/teacher_attendance.html b/templates/teacher_attendance.html new file mode 100644 index 0000000..848b204 --- /dev/null +++ b/templates/teacher_attendance.html @@ -0,0 +1,254 @@ +{% extends 'base.html' %} +{% load static %} +{% load app_filters %} +{% block extrastyle %} + + + + + +{% endblock %} +{% block content %} +
+ +
+

Manage Attendance

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

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

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

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

+
+ {% endif %} +
+
+
+
+{% endblock %} +{% block extrahead %} + + +{% endblock %} diff --git a/templates/teacher_dashboard.html b/templates/teacher_dashboard.html new file mode 100644 index 0000000..a89b028 --- /dev/null +++ b/templates/teacher_dashboard.html @@ -0,0 +1,1108 @@ +{% extends 'base.html' %} +{% load static %} +{% load tz %} +{% load app_filters %} + +{% block content %} + +{% csrf_token %} + +
+ + {% include 'components/welcome_section.html' with user=teacher %} + + +
+ +
+
+
+
+
Students
+
+
+
+

{{ total_students }}

+

Total Students

+
+
+
+
+

{{ present_students }}

+

Present Today

+
+
+

{{ absent_students }}

+

Absent Today

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

{{ today_classes }}

+

Total Classes

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

No classes scheduled for today

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

{{ avg_rating|floatformat:1 }}/5.0

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

Based on {{ recent_feedback.count }} student feedbacks

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

Pending

+ {{ pending_leaves }} +
+
+

Approved

+ {{ approved_leaves }} +
+
+

Rejected

+ {{ rejected_leaves }} +
+ +
+
+
+
+ + +
+
+
+
+
Today's Classes
+
+ + {% if today_routines %} + +
+ + + + + + + + + + + + + + {% for routine in today_routines %} + + + + + + + + + + {% endfor %} + +
SubjectCourseSemester/YearTimeAttendanceStatusAction
{{ routine.name }}{{ routine.course_name }}{{ routine.semester_or_year }}{{ routine.start_time|time:"h:i A" }} - {{ routine.end_time|time:"h:i A" }}{{ routine.attendance_count }}/{{ routine.total_students }} + {% if routine.is_completed %} + Completed + {% elif routine.is_ongoing %} + In Progress + {% else %} + Upcoming + {% endif %} + + {% if routine.has_attendance %} + Recorded + {% else %} + + {% endif %} +
+
+ {% else %} +
+ No classes scheduled for today. +
+ {% endif %} +
+
+
+ + +
+ +
+
+
+
Recent Feedback
+
+
+
+ {{ avg_rating|floatformat:1 }}/5.0 +
+
+ {% for i in "12345"|make_list %} + {% if forloop.counter <= avg_rating_rounded %} + + {% else %} + + {% endif %} + {% endfor %} +
+
+ Based on {{ recent_feedback.count }} feedbacks +
+
+
+
+ + {% if recent_feedback %} + + {% else %} +
+ +

No recent feedback yet.

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

{{ notice.message }}

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

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

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

{{ notice.message }}

+ Posted {{ notice.created_at|timesince }} ago +
+ {% endfor %} +
+ {% else %} +
+ No notices available. +
+ {% endif %} +
+
+ + +
+
+ +
+
+
Give Feedback to Institute
+ +
+
+ + +
+
+
Your Recent Feedback
+ + {% if institute_feedback %} + + {% else %} +
+ +

You haven't submitted any feedback yet.

+
+ {% endif %} +
+
+
+
+ + +
+
+

+ Profile Information +

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

{{ teacher.name }}

+

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

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

{{ teacher.name }}

+
+
+ Gender +

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

+
+
+ Birth Date +

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

+
+
+ Marital Status +

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

+
+
+ Citizenship No. +

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

+
+
+ Passport +

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

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

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

+
+
+ Phone Number +

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

+
+
+ Parent Name +

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

+
+
+ Parent Phone +

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

+
+
+ Temporary Address +

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

+
+
+ Permanent Address +

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

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

{{ teacher.id }}

+
+
+ Designation +

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

+
+
+ Joining Date +

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

+
+
+ Status +

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

+
+
+ Permissions +

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

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

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

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

No educational qualifications recorded

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

{{ teacher.qualification }}

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

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

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

+ Leave Management +

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

{{ pending_leaves }}

+

Pending Requests

+
+
+
+
+
+
+ +

{{ approved_leaves }}

+

Approved Requests

+
+
+
+
+
+
+ +

{{ rejected_leaves }}

+

Rejected Requests

+
+
+
+
+ +
+
Leave History
+
+ + + + + + + + + + + + {% if recent_leaves %} + {% for leave in recent_leaves %} + + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
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
+
+
+
+
+
+ + +
+
+

+ Your Subjects & Materials +

+ +
+

Manage study materials, notes, and resources for students.

+ + Manage Subject Materials + +
+ +
+ {% for routine in teacher_subjects %} +
+
+
+
{{ routine.subject.name }}
+ {{ routine.course.name }} - {{ routine.subject.semester_or_year }} +
+
+
+
+

+ {{ routine.start_time|time:"h:i A" }} - {{ routine.end_time|time:"h:i A" }} +

+

+ {{ routine.students_count|default:"0" }} students +

+
+
+ +
+ + View Syllabus + + {% if routine.subject.get_all_files|length > 0 %} + + View Materials + + {% else %} + + Upload Materials + + {% endif %} +
+
+
+
+ {% empty %} +
+
+ You don't have any subjects assigned. +
+
+ {% endfor %} +
+
+
+ + + +
+
+
+
+

+ Manage Subject Materials +

+
+ +
+ + +
+
+
+ + + + + +{% include 'components/common_modals.html' with user=teacher %} + + +{% include 'components/teacher_modals.html' %} +{% include 'components/teacher_feedback_modal.html' %} + + +{% include 'components/bottom_nav.html' with user=teacher %} + + + + + + + + + + + +{% endblock %} + +{% block extrastyle %} +{{ block.super }} + + + + +{% endblock %} \ No newline at end of file