diff --git a/.gitignore b/.gitignore
index be973b7..0883934 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,3 @@
-
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@@ -62,6 +61,7 @@ local_settings.py
db.sqlite3
db.sqlite3-journal
*/migrations/*.py
+*/migrations/*
!*/migrations/__init__.py
# Flask stuff:
@@ -157,3 +157,4 @@ dump.rdb
# Firebase configuration
firebase-config.json
firebase.json
+static/js/firebase-config.js
diff --git a/README.md b/README.md
index 0739372..f4c230f 100644
--- a/README.md
+++ b/README.md
@@ -2,131 +2,263 @@
-[](https://www.python.org/)
+[](https://www.python.org/)
[](https://www.djangoproject.com/)
[](LICENSE)
[](https://github.com/S4NKALP/Student-Management-System-In-Django/stargazers)
-[](https://archlinux.org/)
[]()
+[]()
+[]()
+[]()
-A modern and comprehensive Student Management System built with Django.
+A comprehensive Student Management System built with Django, featuring multi-role access, real-time notifications, and secure authentication.
+## ๐ Table of Contents
+
+- [๐ป 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)
+
## ๐ป Tech Stack
-
+
+โก Frontend Technologies
-## ๐ฅ๏ธ Development Environment
+| 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) |
-- **Processor**: Intel Core i5-7300U
-- **Operating System**: Arch Linux
-- **Window Manager**: [Hyprland](https://github.com/S4NKALP/hyprland)
-- **Status Bar**: [Modus](https://github.com/S4NKALP/Modus)
-- **Editor**: [Neovim](https://github.com/S4NKALP/nvim)
+
+
+
+๐ ๏ธ Development Tools
+
+| Tool | 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
-| Feature | Description |
-|---------|-------------|
-| ๐ฅ Multi-User Roles | Admin, Staff, and Student interfaces |
-| ๐ Course Management | Comprehensive course and subject tracking |
-| ๐ Attendance System | Real-time attendance monitoring |
-| ๐ Leave Management | Streamlined leave application process |
-| ๐ข Notice Board | Instant announcements and updates |
-| ๐ฌ Feedback System | Student and staff feedback collection |
-| ๐ OTP Verification | Secure authentication system |
-| โ๏ธ Cloud Storage | Google Cloud integration |
-| ๐ฅ Firebase | Real-time notifications and auth |
+### ๐ Academic Management
+- **Course Management**: Create and manage courses 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
-## ๐ Quick Start
+### ๐ 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
-### Linux Users
+### ๐ 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 & Setup
+# Clone the repository
git clone https://github.com/S4NKALP/Student-Management-System-In-Django.git
+
+# Navigate to the project directory
cd Student-Management-System-In-Django
-python -m venv venv && source venv/bin/activate
-pip install -r requirements.txt
-
-# Configure
-cp .env.example .env # Edit with your settings
-
-# Run
-python manage.py migrate
-python manage.py createsuperuser
-python manage.py runserver
```
-### Windows Users
-```powershell
-# Prerequisites
-# Install Python from https://www.python.org/downloads/
-# Install Git from https://git-scm.com/download/win
-# Install Visual C++ Build Tools: https://visualstudio.microsoft.com/visual-cpp-build-tools/
-
-# Clone & Setup
-git clone https://github.com/S4NKALP/Student-Management-System-In-Django.git
-cd Student-Management-System-In-Django
+#### 2๏ธโฃ Set Up Virtual Environment
+```bash
+# Create a virtual environment
python -m venv venv
+
+# Activate the virtual environment
+# For Linux/Mac:
+source venv/bin/activate
+
+# For Windows:
.\venv\Scripts\activate
-pip install -r requirements.txt
-
-# Configure
-copy .env.example .env
-# Edit .env file with your settings
-
-# Run
-python manage.py migrate
-python manage.py createsuperuser
-python manage.py runserver
```
-### โ ๏ธ Common Issues (Windows)
-- If `pip install` fails, make sure you have Visual C++ Build Tools installed
-- If `python` command not found, add Python to your PATH environment variable
-- If permission errors occur, run Command Prompt as Administrator
-- If virtualenv fails, try: `python -m pip install --upgrade virtualenv`
+#### 3๏ธโฃ Install Dependencies
+```bash
+# Install Python dependencies
+pip install -r requirements.txt
+```
-### ๐ Environment Variables (.env)
+#### 4๏ธโฃ Configure Environment Variables
+```bash
+# Copy the example environment file
+cp .env.example .env
+
+# Edit the .env file with your settings
+# Required settings:
+# - SECRET_KEY
+# - DEBUG
+# - ALLOWED_HOSTS
+# - Firebase credentials
+# - Google Cloud credentials
+```
+
+#### 5๏ธโฃ Set Up Firebase
+1. 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-here
+SECRET_KEY=your-secret-key
ALLOWED_HOSTS=localhost,127.0.0.1
-# Database Configuration
+# Database
DB_NAME=sms_db
DB_USER=your_db_user
DB_PASSWORD=your_db_password
DB_HOST=localhost
DB_PORT=3306
-# Email Configuration (for OTP)
+# Email (for OTP)
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_HOST_USER=your-email@gmail.com
-EMAIL_HOST_PASSWORD=your-app-specific-password
+EMAIL_HOST_PASSWORD=your-app-password
-# Firebase Configuration
+# Firebase
FIREBASE_DATABASE_URL=your-firebase-url
FIREBASE_TYPE=service_account
FIREBASE_PROJECT_ID=your-project-id
@@ -134,88 +266,241 @@ FIREBASE_PRIVATE_KEY_ID=your-private-key-id
FIREBASE_PRIVATE_KEY=your-private-key
FIREBASE_CLIENT_EMAIL=your-client-email
FIREBASE_CLIENT_ID=your-client-id
-FIREBASE_AUTH_URI=https://accounts.google.com/o/oauth2/auth
-FIREBASE_TOKEN_URI=https://oauth2.googleapis.com/token
-FIREBASE_AUTH_PROVIDER_X509_CERT_URL=https://www.googleapis.com/oauth2/v1/certs
-FIREBASE_CLIENT_X509_CERT_URL=your-cert-url
-# Google Cloud Storage
-GS_BUCKET_NAME=your-bucket-name
-GS_CREDENTIALS=path/to/credentials.json
+# Google Cloud
+GOOGLE_CLOUD_PROJECT=your-google-cloud-project
+GOOGLE_CLOUD_KEY_FILE=path/to/your/google-cloud-key.json
```
-> **Note**: Never commit your `.env` file to version control. Keep your credentials secure!
+## ๐ฆ Dependencies
-## ๐ฏ Project Roadmap
+### Backend
+- Django 5.1.7
+- Django REST Framework
+- Firebase Admin
+- Google Cloud Storage
+- PyOTP
+- WhiteNoise
-
+### Frontend
+- Bootstrap
+- jQuery
+- Firebase Web SDK
-### โ ๏ธ Important Notice
-```
-This is a Minor Project created for educational purposes.
-Features listed in the roadmap may or may not be implemented.
+## ๐ Security Features
+
+### Authentication
+- OTP-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
-### ๐ฏ Current (v1.0)
-- โ
Core SMS functionality
-- โ
User management
-- โ
Course tracking
-- โ
Basic features
+2. Data Management
+ - CRUD operations
+ - Data validation
+ - File handling
-### ๐ Upcoming (v1.1+)
-- ๐จโ๐ฉโ๐งโ๐ฆ Parents dashboard
-- ๐ Teacher study materials
-- ๐ Library management
-- ๐ Enhanced analytics
-- ๐ค Parent-teacher meetings
+3. Business Logic
+ - Attendance calculation
+ - Leave approval workflow
+ - Course progress tracking
-## ๐ฎ Support & Links
+4. Integration
+ - Firebase messaging
+ - File storage
+ - Email/SMS notifications
-
+## ๐ Performance
-[](https://github.com/S4NKALP/Student-Management-System-In-Django/issues)
-[](https://github.com/S4NKALP/Student-Management-System-In-Django/pulls)
-[](https://github.com/S4NKALP/Student-Management-System-In-Django/wiki)
+### Optimization Techniques
+- Database indexing
+- Query optimization
+- Caching
+- Lazy loading
+- Code minification
-
+### Monitoring
+- Performance metrics
+- Error tracking
+- User analytics
+- Resource usage
-## ๐ Credits
+## ๐ค Contributing
-If you use this project in your work, please give credit where credit is due:
+1. Fork the repository
+2. Create your feature branch
+3. Commit your changes
+4. Push to the branch
+5. Create a Pull Request
-```markdown
-Student Management System
-Created by Sankalp
-GitHub: https://github.com/S4NKALP
-```
+### Code Style
+- Follow PEP 8
+- Use type hints
+- Write docstrings
+- Add comments
-### ๐ Citation
-If you're using this project in an academic context, you can cite it as:
-
-```bibtex
-@software{StudentManagementSystem2025,
- author = {Sankalp},
- title = {Student Management System},
- year = {2025},
- publisher = {GitHub},
- url = {https://github.com/S4NKALP/Student-Management-System-In-Django}
-}
-```
-
-### ๐ค Acknowledgments
-- Thanks to all contributors who have helped improve this project
-- Special thanks to the open-source community for their valuable resources
-- Appreciation to all users who have provided feedback and suggestions
+### Commit Messages
+- Use present tense
+- Be descriptive
+- Reference issues
## ๐ License
-
-
-[](LICENSE)
-
-
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
---
@@ -224,7 +509,7 @@ If you're using this project in an academic context, you can cite it as:
### Made with โค๏ธ by [Sankalp](https://github.com/S4NKALP)
[](https://github.com/S4NKALP)
-
+[](https://linkedin.com/in/sankalp)
+[](https://twitter.com/sankalp)
-
+
\ No newline at end of file
diff --git a/app/admin.py b/app/admin.py
index 9de4070..1bb20cc 100644
--- a/app/admin.py
+++ b/app/admin.py
@@ -1,8 +1,9 @@
from django.contrib import admin
-from app.models import (
+from .models import (
Institute,
Course,
Subject,
+ SubjectFile,
Student,
Staff,
Batch,
@@ -16,8 +17,10 @@ from app.models import (
CourseTracking,
InstituteFeedback,
StaffInstituteFeedback,
- FCMDevice,
+ 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
@@ -47,6 +50,14 @@ class CustomAdminSite(admin.AdminSite):
"group": "Authentication",
"group_label": "authentication",
},
+ "TOTPSecret": {
+ "group": "Authentication",
+ "group_label": "authentication",
+ },
+ "ResetToken": {
+ "group": "Authentication",
+ "group_label": "authentication",
+ },
# Organization Models
"Institute": {
"group": "Organization",
@@ -60,13 +71,22 @@ class CustomAdminSite(admin.AdminSite):
"group": "Organization",
"group_label": "organization",
},
+ # Course Management Models
"Course": {
- "group": "Organization",
- "group_label": "organization",
+ "group": "Course Management",
+ "group_label": "course_management",
},
"Subject": {
- "group": "Organization",
- "group_label": "organization",
+ "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": {
@@ -89,25 +109,32 @@ class CustomAdminSite(admin.AdminSite):
"group": "Human Management",
"group_label": "human_management",
},
- "StudentFeedback": {
- "group": "Human Management",
- "group_label": "human_management",
- },
- "CourseTracking": {
- "group": "Human Management",
- "group_label": "human_management",
- },
+ # Attendance Models
"Attendance": {
- "group": "Human Management",
- "group_label": "human_management",
+ "group": "Attendance",
+ "group_label": "attendance",
+ },
+ "AttendanceRecord": {
+ "group": "Attendance",
+ "group_label": "attendance",
+ },
+ # Feedback Models
+ "StudentFeedback": {
+ "group": "Feedback",
+ "group_label": "feedback",
},
"InstituteFeedback": {
- "group": "Human Management",
- "group_label": "human_management",
+ "group": "Feedback",
+ "group_label": "feedback",
},
"StaffInstituteFeedback": {
- "group": "Human Management",
- "group_label": "human_management",
+ "group": "Feedback",
+ "group_label": "feedback",
+ },
+ # Device Management
+ "FCMDevice": {
+ "group": "Device Management",
+ "group_label": "device_management",
},
}
@@ -194,7 +221,7 @@ class BatchAdmin(admin.ModelAdmin):
@admin.register(Subject, site=custom_admin_site)
class SubjectModel(admin.ModelAdmin):
- list_display = ("id", "name", "course", "get_period_display", "has_syllabus")
+ list_display = ("id", "name", "course", "get_period_display", "has_syllabus", "file_count")
search_fields = ("name", "course__name")
list_filter = ("course", "semester_or_year")
advanced_filter_fields = ("name", "course", "semester_or_year")
@@ -204,6 +231,11 @@ class SubjectModel(admin.ModelAdmin):
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":
@@ -588,8 +620,8 @@ class RoutineAdmin(admin.ModelAdmin):
@admin.register(Staff_leave, site=custom_admin_site)
class StaffLeaveAdmin(admin.ModelAdmin):
- list_display = ("staff", "date", "get_status_display", "created_at")
- list_filter = ("status", "date", "created_at")
+ 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",)
@@ -598,8 +630,8 @@ class StaffLeaveAdmin(admin.ModelAdmin):
@admin.register(Student_Leave, site=custom_admin_site)
class StudentLeaveAdmin(admin.ModelAdmin):
- list_display = ("student", "date", "get_status_display", "created_at")
- list_filter = ("status", "date", "created_at")
+ 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",)
@@ -756,12 +788,26 @@ class StaffInstituteFeedbackAdmin(admin.ModelAdmin):
return super().get_queryset(request).select_related("staff", "institute")
-@admin.register(FCMDevice)
+@admin.register(FCMDevice, site=custom_admin_site)
class FCMDeviceAdmin(admin.ModelAdmin):
- list_display = ("token",)
+ list_display = ("id", "token", "created_at", "last_active", "is_active")
search_fields = ("token",)
- list_filter = ("token",)
- advanced_filter_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)
diff --git a/app/api_urls.py b/app/api_urls.py
deleted file mode 100644
index 0519ecb..0000000
--- a/app/api_urls.py
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/app/api_views.py b/app/api_views.py
deleted file mode 100644
index 0519ecb..0000000
--- a/app/api_views.py
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/app/apps.py b/app/apps.py
index ed327d2..eb38975 100644
--- a/app/apps.py
+++ b/app/apps.py
@@ -4,3 +4,6 @@ from django.apps import AppConfig
class AppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'app'
+
+ def ready(self):
+ import app.signals # Import signals when app is ready
diff --git a/app/auth.py b/app/auth.py
index 0c0bc34..c945d79 100644
--- a/app/auth.py
+++ b/app/auth.py
@@ -3,17 +3,20 @@ from django.contrib.auth import login
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.models import User
from django.contrib.auth.hashers import make_password
-from django.core.cache import cache
import uuid
from .models import Student, Staff
from .utils import (
generate_otp,
- store_otp,
+ generate_secret_key,
+ store_secret_key,
verify_otp,
send_otp_sms,
send_password_reset_email,
- verify_reset_code
+ 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"""
@@ -35,11 +38,12 @@ def password_reset_phone(request):
if not student and not staff:
return render(request, "login/password_reset_phone.html", {"error": "No account found with this phone number", "is_email": False})
- # Generate and store OTP
- otp = generate_otp()
- store_otp(phone, otp)
+ # Generate secret key and store it
+ secret_key = generate_secret_key()
+ store_secret_key(phone, secret_key)
- # Send OTP via SMS
+ # Generate and send OTP
+ otp = generate_otp(secret_key)
send_result = send_otp_sms(phone, otp)
if send_result:
@@ -67,8 +71,8 @@ def password_reset_phone_verify(request):
if verify_otp(phone, otp):
# Generate a secure token for this reset
token = str(uuid.uuid4())
- # Store token with phone in cache for 15 minutes
- cache.set(f"reset_token_{token}", phone, 60 * 15)
+ # Store token in database
+ store_reset_token(token, phone)
# Redirect to set new password
return render(request, "login/password_reset_set.html", {
@@ -94,11 +98,12 @@ def resend_phone_otp(request):
if not phone:
return redirect("phone_reset_password")
- # Generate and store new OTP
- otp = generate_otp()
- store_otp(phone, otp)
+ # Generate new secret key and store it
+ secret_key = generate_secret_key()
+ store_secret_key(phone, secret_key)
- # Send OTP via SMS
+ # Generate and send new OTP
+ otp = generate_otp(secret_key)
send_result = send_otp_sms(phone, otp)
if send_result:
@@ -161,8 +166,8 @@ def password_reset_email_verify(request):
if verify_reset_code(email, code):
# Generate a secure token for this reset
token = str(uuid.uuid4())
- # Store token with email in cache for 15 minutes
- cache.set(f"reset_token_{token}", email, 60 * 15)
+ # Store token in database
+ store_reset_token(token, email)
# Redirect to set new password
return render(request, "login/password_reset_set.html", {
@@ -245,8 +250,7 @@ def set_new_password(request):
})
# Verify token is valid
- stored_identifier = cache.get(f"reset_token_{token}")
- if not stored_identifier or stored_identifier != identifier:
+ if not verify_reset_token(token, identifier):
return render(request, "login/password_reset_set.html", {
"error": "Invalid or expired reset session",
"phone": phone,
@@ -274,18 +278,7 @@ def set_new_password(request):
staff = Staff.objects.filter(phone=phone).first()
user = student or staff
-
- if user:
- # Set new password
- user.password = make_password(new_password1)
- user.save()
-
- # Invalidate reset token
- cache.delete(f"reset_token_{token}")
-
- # Show success message
- return render(request, "login/password_reset_complete.html")
- else:
+ if not user:
return render(request, "login/password_reset_set.html", {
"error": "User not found",
"phone": phone,
@@ -293,6 +286,13 @@ def set_new_password(request):
"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")
- # If GET request, redirect to options page
- return redirect("reset_password_options")
\ No newline at end of file
+ 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
index 1aa92f5..66e20be 100644
--- a/app/backends.py
+++ b/app/backends.py
@@ -11,28 +11,22 @@ class MultiModelBackend(ModelBackend):
return None
# Try authenticating as Staff by phone
- try:
- staff = Staff.objects.get(phone=username)
- if staff.check_password(password):
- return staff
- except Staff.DoesNotExist:
- pass
+ # 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
- try:
- staff = Staff.objects.get(email=username)
- if staff.check_password(password):
- return staff
- except Staff.DoesNotExist:
- pass
+ # 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
- try:
- student = Student.objects.get(phone=username)
- if student.check_password(password):
- return student
- except Student.DoesNotExist:
- pass
+ # 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
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 deba05d..4c92070 100644
--- a/app/forms.py
+++ b/app/forms.py
@@ -17,7 +17,7 @@ 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.",
+ help_text="Enter a new password if you want to reset it. Leave blank otherwise. Default is '123'.",
)
class Meta:
@@ -27,8 +27,13 @@ class StudentAdminForm(forms.ModelForm):
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
@@ -38,7 +43,7 @@ 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.",
+ help_text="Enter a new password if you want to reset it. Leave blank otherwise. Default is '123'.",
)
class Meta:
@@ -48,8 +53,13 @@ class StaffAdminForm(forms.ModelForm):
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/__init__.py b/app/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/models.py b/app/models.py
index d1ddf96..2464b52 100644
--- a/app/models.py
+++ b/app/models.py
@@ -4,25 +4,58 @@ 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.core.cache import cache
-from .utils import send_push_notification
+from django.utils import timezone
+from django.core.exceptions import ValidationError
+from .firebase import send_push_notification, FCMDevice
-# Create your models here.
-duration_types_choices = (("Year", "Yearl Based"), ("Semester", "Semester Based"))
-student_status_choices = (
+# Constants
+DURATION_TYPES = (
+ ("Year", "Year Based"),
+ ("Semester", "Semester Based"),
+)
+
+STUDENT_STATUS_CHOICES = (
("Active", "Active"),
("Leave", "Leave"),
("Completed", "Completed"),
)
-gender_choices = (("Male", "Male"), ("Female", "Female"), ("Other", "Other"))
-marital_status_choices = (
+
+GENDER_CHOICES = (
+ ("Male", "Male"),
+ ("Female", "Female"),
+ ("Other", "Other"),
+)
+
+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)
@@ -40,14 +73,28 @@ class Institute(models.Model):
verbose_name = "Institute"
verbose_name_plural = "Institutes"
+# Academic Models
+class Batch(models.Model):
+ """Model representing a batch of students"""
+ id = models.BigAutoField(primary_key=True)
+ name = models.CharField(max_length=255)
+ start_date = models.DateField(null=True, blank=True)
+ end_date = models.DateField(null=True, blank=True)
+ def __str__(self):
+ return self.name
+
+ class Meta:
+ verbose_name = "Batch"
+ verbose_name_plural = "Batches"
+
+# Course Related Models
class Course(models.Model):
+ """Model representing a course offered by the institute"""
id = models.BigAutoField(primary_key=True)
name = models.CharField(max_length=500)
duration = models.IntegerField(default=1, null=True, blank=True)
- duration_type = models.CharField(
- max_length=255, choices=duration_types_choices, null=True, blank=True
- )
+ duration_type = models.CharField(max_length=255, choices=DURATION_TYPES, null=True, blank=True)
def __str__(self):
return self.name
@@ -56,21 +103,23 @@ class Course(models.Model):
verbose_name = "Course"
verbose_name_plural = "Courses"
-
class Subject(models.Model):
+ """Model representing a subject within a course"""
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255)
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")
+ syllabus_pdf = models.FileField(
+ upload_to="subject_pdfs/",
+ null=True,
+ blank=True,
+ help_text="Upload syllabus or study material PDF",
+ )
def __str__(self):
return f"{self.name} ({self.course.name} - {'Semester' if self.course.duration_type == 'Semester' else 'Year'} {self.semester_or_year})"
def clean(self):
- from django.core.exceptions import ValidationError
-
- # Validate semester/year based on course duration type
if self.course.duration_type == "Semester":
max_semesters = self.course.duration * 2
if self.semester_or_year > max_semesters:
@@ -84,30 +133,59 @@ class Subject(models.Model):
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:
- return self.syllabus_pdf.url
- return None
+ 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 Batch(models.Model):
- id = models.BigAutoField(primary_key=True)
- name = models.CharField(max_length=255)
- start_date = models.DateField(null=True, blank=True)
- end_date = models.DateField(null=True, blank=True)
+ 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 self.name
-
+ return f"{self.title} - {self.subject.name}"
+
class Meta:
- verbose_name = "Batch"
- verbose_name_plural = "Batches"
-
+ ordering = ['-uploaded_at']
+# User Models
class Student(AbstractUser):
+ """Model representing a student in the institute"""
first_name = None
last_name = None
is_superuser = None
@@ -134,37 +212,24 @@ class Student(AbstractUser):
id = models.BigAutoField(primary_key=True)
name = models.CharField(max_length=255)
- status = models.CharField(
- max_length=255, choices=student_status_choices, null=True, blank=True
- )
- gender = models.CharField(
- max_length=255, choices=gender_choices, null=True, blank=True
- )
+ status = models.CharField(max_length=255, choices=STUDENT_STATUS_CHOICES, null=True, blank=True)
+ gender = models.CharField(max_length=255, choices=GENDER_CHOICES, null=True, blank=True)
birth_date = models.DateField(null=True, blank=True)
email = models.CharField(max_length=255, null=True, blank=True)
phone = models.CharField(max_length=255, null=True, blank=True, unique=True)
temporary_address = models.CharField(max_length=500, null=True, blank=True)
permanent_address = models.CharField(max_length=500, null=True, blank=True)
- marital_status = models.CharField(
- max_length=255, choices=marital_status_choices, null=True, blank=True
- )
+ marital_status = models.CharField(max_length=255, choices=MARITAL_STATUS_CHOICES, null=True, blank=True)
parent_name = models.CharField(max_length=255, null=True, blank=True)
parent_phone = models.CharField(max_length=255, null=True, blank=True)
citizenship_no = models.CharField(max_length=255, null=True, blank=True)
- batches = models.ManyToManyField(Batch, related_name="students", blank=True)
+ batches = models.ManyToManyField('Batch', related_name="students", blank=True)
image = models.ImageField(upload_to="student_image", null=True, blank=True)
- course = models.ForeignKey(
- Course,
- on_delete=models.SET_NULL,
- null=True,
- blank=True,
- related_name="students",
- )
- current_semester = models.PositiveIntegerField(
- default=1, help_text="Current semester for semester-based courses"
- )
+ course = models.ForeignKey(Course, on_delete=models.SET_NULL, null=True, blank=True, related_name="students")
+ current_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"]
@@ -186,9 +251,7 @@ class Student(AbstractUser):
perms = set()
for group in self.groups.all():
- perms.update(
- group.permissions.values_list("content_type__app_label", "codename")
- )
+ perms.update(group.permissions.values_list("content_type__app_label", "codename"))
app_label, codename = perm.split(".")
return (app_label, codename) in perms
@@ -207,7 +270,8 @@ class Student(AbstractUser):
if not self.course:
return Subject.objects.none()
return Subject.objects.filter(
- course=self.course, semester_or_year=self.current_semester
+ course=self.course,
+ semester_or_year=self.current_semester
)
def advance_semester(self):
@@ -216,23 +280,19 @@ class Student(AbstractUser):
return False
if self.course.duration_type == "Semester":
- total_semesters = self.course.duration * 2 # 2 semesters per year
+ total_semesters = self.course.duration * 2
if self.current_semester < total_semesters:
self.current_semester += 1
self.save()
return True
return False
- def save(self, *args, **kwargs):
- # Remove the course tracking creation from here
- super().save(*args, **kwargs)
-
class Meta:
verbose_name = "Student"
verbose_name_plural = "Students"
-
class Staff(AbstractUser):
+ """Model representing staff members of the institute"""
first_name = None
last_name = None
date_joined = None
@@ -247,44 +307,39 @@ class Staff(AbstractUser):
verbose_name="user permissions",
blank=True,
help_text="Specific permissions for this staff.",
- related_name="staff_set", # This fixes the clash
+ related_name="staff_set",
related_query_name="staff",
)
- # Similarly, let's also update groups to be explicit
groups = models.ManyToManyField(
"auth.Group",
verbose_name="groups",
blank=True,
help_text="The groups this staff belongs to.",
- related_name="staffs", # This matches your existing related_name
+ 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
- )
+ gender = models.CharField(max_length=255, choices=GENDER_CHOICES, null=True, blank=True)
birth_date = models.DateField(null=True, blank=True)
email = models.CharField(max_length=255, null=True, blank=True)
temporary_address = models.CharField(max_length=255, null=True, blank=True)
permanent_address = models.CharField(max_length=255, null=True, blank=True)
- marital_status = models.CharField(
- max_length=255, choices=marital_status_choices, null=True, blank=True
- )
+ marital_status = models.CharField(max_length=255, choices=MARITAL_STATUS_CHOICES, null=True, blank=True)
parent_name = models.CharField(max_length=255, null=True, blank=True)
parent_phone = models.CharField(max_length=255, null=True, blank=True)
citizenship_no = models.CharField(max_length=255, blank=True, null=True)
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
- ) # Editable is set to False to prevent direct editing
+ 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"] # Add required fields
+ REQUIRED_FIELDS = ["name"]
def __str__(self):
return self.name
@@ -295,7 +350,6 @@ class Staff(AbstractUser):
@property
def is_staff(self):
- """Check if student has any groups (permissions)"""
return True
@property
@@ -303,27 +357,20 @@ class Staff(AbstractUser):
return True
def has_perm(self, perm, obj=None):
- """Check if student has a specific permission"""
if not self.is_active:
return False
- # Get permissions from groups
perms = set()
for group in self.groups.all():
- perms.update(
- group.permissions.values_list("content_type__app_label", "codename")
- )
+ perms.update(group.permissions.values_list("content_type__app_label", "codename"))
- # Convert permission string to app_label and codename
app_label, codename = perm.split(".")
return (app_label, codename) in perms
def has_module_perms(self, app_label):
- """Check if student has any permissions in the given app"""
if not self.is_active:
return False
- # Check if any group permissions match the app_label
for group in self.groups.all():
if group.permissions.filter(content_type__app_label=app_label).exists():
return True
@@ -333,8 +380,8 @@ class Staff(AbstractUser):
verbose_name = "Staff"
verbose_name_plural = "Staffs"
-
class Routine(models.Model):
+ """Model representing class routine"""
course = models.ForeignKey(Course, on_delete=models.CASCADE)
subject = models.ForeignKey(Subject, on_delete=models.CASCADE)
teacher = models.ForeignKey(Staff, on_delete=models.CASCADE)
@@ -347,27 +394,23 @@ class Routine(models.Model):
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']
-
+ 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,
- )
+ 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)
@@ -378,38 +421,16 @@ class Attendance(models.Model):
verbose_name = "Attendance"
verbose_name_plural = "Attendances"
-
class AttendanceRecord(models.Model):
+ """Model representing individual student attendance records"""
id = models.BigAutoField(primary_key=True)
- attendance = models.ForeignKey(
- Attendance,
- on_delete=models.CASCADE,
- null=True,
- blank=True,
- related_name="records",
- )
- student = models.ForeignKey(
- Student,
- on_delete=models.CASCADE,
- null=True,
- blank=True,
- related_name="attendance_records",
- )
+ attendance = models.ForeignKey(Attendance, on_delete=models.CASCADE, null=True, blank=True, related_name="records")
+ student = models.ForeignKey(Student, on_delete=models.CASCADE, null=True, blank=True, related_name="attendance_records")
student_attend = models.BooleanField(default=False)
- # def __str__(self):
- # return self.student.name
-
-
-class FCMDevice(models.Model):
- id = models.BigAutoField(primary_key=True)
- token = models.TextField(unique=True)
-
- def __str__(self):
- return self.token
-
-
+# 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)
@@ -429,63 +450,49 @@ class Notice(models.Model):
verbose_name = "Notice"
verbose_name_plural = "Notices"
-
+# Leave Models
class Staff_leave(models.Model):
- LEAVE_STATUS_CHOICES = (
- (0, "Pending"),
- (1, "Approved"),
- (2, "Rejected"),
- )
-
+ """Model representing staff leave requests"""
staff = models.ForeignKey(Staff, on_delete=models.CASCADE)
- date = models.DateField()
+ 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.staff.name} - {self.date}"
+ 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):
- LEAVE_STATUS_CHOICES = (
- (0, "Pending"),
- (1, "Approved"),
- (2, "Rejected"),
- )
-
+ """Model representing student leave requests"""
student = models.ForeignKey(Student, on_delete=models.CASCADE)
- date = models.DateField()
+ 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.date}"
+ 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):
- student = models.ForeignKey(
- Student, on_delete=models.CASCADE, related_name="feedbacks"
- )
- teacher = models.ForeignKey(
- Staff, on_delete=models.CASCADE, related_name="feedbacks"
- )
- rating = models.DecimalField(
- max_digits=2, decimal_places=1, choices=[(i / 2, i / 2) for i in range(2, 11)]
- ) # 1 to 5 rating
+ """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)
@@ -499,28 +506,12 @@ class StudentFeedback(models.Model):
ordering = ["-created_at"]
unique_together = ["student", "teacher"]
-
class InstituteFeedback(models.Model):
- FEEDBACK_TYPE_CHOICES = (
- ("General", "General"),
- ("Facilities", "Facilities"),
- ("Teaching", "Teaching"),
- ("Infrastructure", "Infrastructure"),
- ("Administration", "Administration"),
- )
-
- institute = models.ForeignKey(
- Institute, on_delete=models.CASCADE, related_name="feedbacks"
- )
- user = models.ForeignKey(
- Student, on_delete=models.CASCADE, related_name="institute_feedbacks"
- )
- feedback_type = models.CharField(
- max_length=20, choices=FEEDBACK_TYPE_CHOICES, default="General"
- )
- rating = models.DecimalField(
- max_digits=2, decimal_places=1, choices=[(i / 2, i / 2) for i in range(2, 11)]
- ) # 1 to 5 rating
+ """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)
@@ -536,40 +527,18 @@ class InstituteFeedback(models.Model):
verbose_name = "Student Institute Feedback"
verbose_name_plural = "Student Institute Feedbacks"
ordering = ["-created_at"]
- unique_together = [
- "user",
- "institute",
- "feedback_type",
- ] # One feedback per type per user
+ unique_together = ["user", "institute", "feedback_type"]
@property
def display_name(self):
- """Return anonymous if feedback is anonymous, otherwise return user's name"""
return "Anonymous" if self.is_anonymous else self.user.name
-
class StaffInstituteFeedback(models.Model):
- """Model for tracking feedback from staff members to the institute"""
- FEEDBACK_TYPE_CHOICES = (
- ("General", "General"),
- ("Facilities", "Facilities"),
- ("Teaching", "Teaching"),
- ("Infrastructure", "Infrastructure"),
- ("Administration", "Administration"),
- )
-
- institute = models.ForeignKey(
- Institute, on_delete=models.CASCADE, related_name="staff_feedbacks"
- )
- staff = models.ForeignKey(
- Staff, on_delete=models.CASCADE, related_name="institute_feedbacks"
- )
- feedback_type = models.CharField(
- max_length=20, choices=FEEDBACK_TYPE_CHOICES, default="General"
- )
- rating = models.DecimalField(
- max_digits=2, decimal_places=1, choices=[(i / 2, i / 2) for i in range(2, 11)]
- ) # 1 to 5 rating
+ """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)
@@ -585,42 +554,23 @@ class StaffInstituteFeedback(models.Model):
verbose_name = "Staff Institute Feedback"
verbose_name_plural = "Staff Institute Feedbacks"
ordering = ["-created_at"]
- unique_together = [
- "staff",
- "institute",
- "feedback_type",
- ] # One feedback per type per staff
+ unique_together = ["staff", "institute", "feedback_type"]
@property
def display_name(self):
- """Return anonymous if feedback is anonymous, otherwise return staff's name"""
return "Anonymous" if self.is_anonymous else self.staff.name
-
+# Course Tracking Model
class CourseTracking(models.Model):
- PROGRESS_STATUS_CHOICES = (
- ("Not Started", "Not Started"),
- ("In Progress", "In Progress"),
- ("Completed", "Completed"),
- ("Dropped", "Dropped"),
- )
-
- student = models.ForeignKey(
- Student, on_delete=models.CASCADE, related_name="course_trackings"
- )
- course = models.ForeignKey(
- Course, on_delete=models.CASCADE, related_name="student_trackings"
- )
+ """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)"
- )
+ 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)
@@ -628,33 +578,25 @@ class CourseTracking(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
- class Meta:
- verbose_name = "Course Tracking"
- verbose_name_plural = "Course Trackings"
- unique_together = ["student", "course"]
- ordering = ["-created_at"]
-
def __str__(self):
return f"{self.student.name} - {self.course.name}"
def clean(self):
- from django.core.exceptions import ValidationError
-
if not self.student or not self.course:
raise ValidationError("Both student and course are required")
if self.start_date and self.expected_end_date and self.start_date > self.expected_end_date:
raise ValidationError("Start date cannot be after expected end date")
-
+
if self.actual_end_date and self.start_date and self.actual_end_date < self.start_date:
raise ValidationError("Actual end date cannot be before start date")
-
+
if self.semester_start_date and self.semester_end_date and self.semester_start_date > self.semester_end_date:
raise ValidationError("Semester start date cannot be after semester end date")
-
+
if self.current_semester < 1:
raise ValidationError("Current semester must be at least 1")
-
+
if self.course.duration_type == "Semester":
max_semesters = self.course.duration * 2
if self.current_semester > max_semesters:
@@ -666,68 +608,66 @@ class CourseTracking(models.Model):
today = date.today()
- # If course is completed, return 100%
if self.progress_status == "Completed":
return 100
elif self.progress_status == "Dropped":
return 0
- # Calculate total duration in days
total_duration = (self.expected_end_date - self.start_date).days
if total_duration <= 0:
return 0
- # Calculate elapsed time in days
elapsed_days = (today - self.start_date).days
if elapsed_days <= 0:
return 0
- # Calculate percentage based on course duration type
if self.course.duration_type == "Semester":
- # For semester-based courses
- semester_duration = 180 # 6 months = 180 days
- total_semesters = self.course.duration * 2 # 2 semesters per year
+ total_semesters = self.course.duration * 2
completed_semesters = self.current_semester - 1
semester_progress = (completed_semesters / total_semesters) * 100
- # If current semester is completed
if self.semester_end_date and today > self.semester_end_date:
semester_progress = (self.current_semester / total_semesters) * 100
return min(100, int(semester_progress))
else: # Yearly based
- max_days = 365 # 1 year = 365 days
+ max_days = 365
if elapsed_days >= max_days:
return 100
- # Calculate percentage based on elapsed time vs total duration
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):
- # Calculate dates if this is a new instance
if not self.pk:
self.start_date = self.start_date or date.today()
-
- # Calculate expected end date based on course duration
+
if self.course.duration_type == "Year":
years = self.course.duration
else: # Semester based
- years = self.course.duration # Duration is already in years
-
+ years = self.course.duration
+
self.expected_end_date = self.start_date + timedelta(days=years * 365)
-
- # Set initial semester dates
+
self.semester_start_date = self.start_date
self.semester_end_date = self.start_date + timedelta(days=180) # 6 months
- # Validate dates and other fields
self.clean()
-
- # Calculate completion percentage
self.completion_percentage = self.calculate_completion_percentage()
- # Update progress status based on completion percentage
if self.completion_percentage >= 100:
self.progress_status = "Completed"
if not self.actual_end_date:
@@ -742,22 +682,15 @@ class CourseTracking(models.Model):
raise
def advance_semester(self):
- """Advance to next semester if possible"""
- if not self.course or self.course.duration_type != "Semester":
+ if not self.course:
return False
- total_semesters = self.course.duration * 2
- if self.current_semester < total_semesters:
- self.current_semester += 1
- self.semester_start_date = date.today()
- self.semester_end_date = self.semester_start_date + timedelta(
- days=180
- ) # 6 months
- # Update student's current semester as well
- self.student.current_semester = self.current_semester
- self.student.save()
- self.save()
- return True
+ 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
@@ -766,156 +699,65 @@ class CourseTracking(models.Model):
@property
def is_active(self):
- return self.progress_status == "In Progress"
+ return self.progress_status in ["Not Started", "In Progress"]
@property
def remaining_days(self):
- """Returns remaining days in current semester"""
if not self.semester_end_date:
- return None
- today = date.today()
- remaining = (self.semester_end_date - today).days
- return max(0, remaining)
+ return 0
+ return max(0, (self.semester_end_date - date.today()).days)
@property
def total_remaining_days(self):
- """Returns remaining days for entire course"""
if not self.expected_end_date:
- return None
- today = date.today()
- remaining = (self.expected_end_date - today).days
- return max(0, remaining)
+ return 0
+ return max(0, (self.expected_end_date - date.today()).days)
@property
def duration_type(self):
- return self.course.duration_type
+ 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 None
+ return 0
return (self.expected_end_date - self.start_date).days
- def get_completion_percentage(self):
- return get_cached_percentage(self.id)
+ class Meta:
+ verbose_name = "Course Tracking"
+ verbose_name_plural = "Course Trackings"
+ unique_together = ["student", "course"]
+ ordering = ["-created_at"]
- def force_update_percentage(self):
- cache_key = f"course_tracking_percentage_{self.id}"
- cache.delete(cache_key)
- self.save()
+# 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)
+ expires_at = models.DateTimeField()
+ class Meta:
+ indexes = [
+ models.Index(fields=['identifier']),
+ models.Index(fields=['expires_at']),
+ ]
-def get_cached_percentage(tracking_id):
- """
- Get cached completion percentage for a course tracking
- """
- cache_key = f"course_tracking_percentage_{tracking_id}"
- percentage = cache.get(cache_key)
- if percentage is None:
- try:
- tracking = CourseTracking.objects.get(id=tracking_id)
- percentage = tracking.completion_percentage
- cache.set(cache_key, percentage, 3600)
- except CourseTracking.DoesNotExist:
- return 0
- return percentage
+ 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)
+ expires_at = models.DateTimeField()
-@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
+ class Meta:
+ indexes = [
+ models.Index(fields=['token']),
+ models.Index(fields=['expires_at']),
+ ]
-
-@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()
+ def is_expired(self):
+ return timezone.now() > self.expires_at
diff --git a/app/serializers.py b/app/serializers.py
index 8dfd092..90e034c 100644
--- a/app/serializers.py
+++ b/app/serializers.py
@@ -3,7 +3,8 @@ 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
+ StudentFeedback, InstituteFeedback, StaffInstituteFeedback, CourseTracking,
+ TOTPSecret, ResetToken
)
class FCMDeviceSerializer(serializers.ModelSerializer):
@@ -112,7 +113,7 @@ class StaffLeaveSerializer(serializers.ModelSerializer):
class Meta:
model = Staff_leave
- fields = ['id', 'staff', 'date', 'message', 'status', 'created_at', 'updated_at']
+ fields = ['id', 'staff', 'start_date', 'end_date', 'message', 'status', 'created_at', 'updated_at']
read_only_fields = ['created_at', 'updated_at']
class StudentLeaveSerializer(serializers.ModelSerializer):
@@ -122,7 +123,7 @@ class StudentLeaveSerializer(serializers.ModelSerializer):
class Meta:
model = Student_Leave
- fields = ['id', 'student', 'date', 'message', 'status', 'created_at', 'updated_at']
+ fields = ['id', 'student', 'start_date', 'end_date', 'message', 'status', 'created_at', 'updated_at']
read_only_fields = ['created_at', 'updated_at']
class StudentFeedbackSerializer(serializers.ModelSerializer):
@@ -177,3 +178,13 @@ class CourseTrackingSerializer(serializers.ModelSerializer):
]
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
index 5096594..3032c5d 100644
--- a/app/staffviews.py
+++ b/app/staffviews.py
@@ -1,11 +1,11 @@
-from django.shortcuts import render, redirect
-from django.http import JsonResponse
+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.db.models import Avg, Q
+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 import messages
from django.contrib.auth.hashers import check_password, make_password
from .models import (
Staff,
@@ -19,7 +19,10 @@ from .models import (
Institute,
StaffInstituteFeedback,
Subject,
- InstituteFeedback
+ SubjectFile,
+ InstituteFeedback,
+ CourseTracking,
+ FEEDBACK_TYPE_CHOICES
)
from app.admin import custom_admin_site
@@ -27,7 +30,11 @@ from app.admin import custom_admin_site
@login_required
def teacherDashboard(request):
"""View to display the teacher dashboard with their classes and student information"""
- teacher = Staff.objects.get(phone=request.user.phone) # Get the Staff object
+ 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)
@@ -43,8 +50,13 @@ def teacherDashboard(request):
for routine in today_routines:
attendance = routine.attendances.filter(date=today).first()
- # Get total students from the course associated with the subject
- total_students = Student.objects.filter(course=routine.subject.course).count()
+ # 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
@@ -56,17 +68,18 @@ def teacherDashboard(request):
# Add routine to processed_routines
processed_routines.append(
{
- "id": routine.id, # Add the routine ID for use in the template
+ "id": routine.id,
"name": routine.subject.name,
"start_time": routine.start_time,
"end_time": routine.end_time,
- "is_completed": attendance is not None, # Only mark as completed if attendance exists
+ "is_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, # Flag to check if attendance has been taken
+ "has_attendance": attendance is not None,
+ "subject_id": routine.subject.id,
}
)
@@ -75,8 +88,11 @@ def teacherDashboard(request):
completed_classes = len([r for r in processed_routines if r["is_completed"]])
remaining_classes = total_classes - completed_classes
- # Get student statistics - count unique students from teacher's assigned subjects and semesters
- total_students = Student.objects.filter(status="Active").count()
+ # Get 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(
@@ -88,14 +104,16 @@ def teacherDashboard(request):
# Calculate absent students based on teacher's classes today
today_class_students = set()
for routine in today_routines:
- students = Student.objects.filter(
+ students = CourseTracking.objects.filter(
course=routine.subject.course,
current_semester=routine.semester_or_year,
- status="Active"
- )
- today_class_students.update(students.values_list('id', flat=True))
+ progress_status="In Progress"
+ ).values_list('student', flat=True)
+ today_class_students.update(students)
- absent_students = len(today_class_students) - present_students if today_class_students else 0
+ # 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(
@@ -119,7 +137,7 @@ def teacherDashboard(request):
notices = Notice.objects.all().order_by('-created_at')[:5]
# Get feedback types for institute feedback
- feedback_types = InstituteFeedback.FEEDBACK_TYPE_CHOICES
+ feedback_types = FEEDBACK_TYPE_CHOICES
# Get institute feedback
institute_feedback = StaffInstituteFeedback.objects.filter(staff=teacher).order_by('-created_at')[:5]
@@ -128,9 +146,9 @@ def teacherDashboard(request):
all_routines = Routine.objects.filter(
teacher=teacher,
is_active=True,
- semester_or_year__in=Student.objects.filter(status="Active")
- .values_list('current_semester', flat=True)
- .distinct()
+ 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
@@ -155,6 +173,26 @@ def teacherDashboard(request):
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",
@@ -178,9 +216,9 @@ def teacherDashboard(request):
"institute_feedback": institute_feedback,
"all_routines": all_routines,
"recent_attendance": recent_attendance,
- # Attendance Management Context
"routines": routines,
"today": today,
+ "teacher_subjects": teacher_subjects,
}
return render(request, "teacher_dashboard.html", context)
@@ -191,34 +229,44 @@ def request_staff_leave(request):
"""View to handle leave requests from staff"""
if request.method == "POST":
staff = request.user
- date = request.POST.get("date")
+ start_date = request.POST.get("start_date")
+ end_date = request.POST.get("end_date")
message = request.POST.get("message")
try:
- # Convert date string to timezone-aware datetime
- leave_date = timezone.datetime.strptime(date, "%Y-%m-%d")
- leave_date = timezone.make_aware(leave_date)
+ # 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 leave_date.date() < today.date():
- messages.error(request, "Leave date cannot be in the past.")
+ if start_date.date() < today.date():
+ messages.error(request, "Start date cannot be in the past.")
return redirect("teacherDashboard")
- # Check if leave already exists for this date
+ 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, date=leave_date.date()
+ 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 this date."
+ request, "You have already requested leave for these dates."
)
return redirect("teacherDashboard")
# Create leave request
leave = Staff_leave.objects.create(
staff=staff,
- date=leave_date.date(),
+ start_date=start_date.date(),
+ end_date=end_date.date(),
message=message,
status=0, # Pending status
)
@@ -264,8 +312,12 @@ def submit_staff_institute_feedback(request):
email="default@example.com",
)
- # Get the staff object
- teacher = Staff.objects.get(phone=staff.phone)
+ # 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(
@@ -304,7 +356,9 @@ def submit_staff_institute_feedback(request):
def save_attendance(request):
"""View to save attendance data"""
if request.method == "POST":
- teacher = Staff.objects.get(phone=request.user.phone)
+ 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")
@@ -393,7 +447,9 @@ def save_attendance(request):
@require_GET
def get_subject_schedule(request):
"""View to get schedule for a subject"""
- teacher = Staff.objects.get(phone=request.user.phone)
+ 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:
@@ -413,7 +469,9 @@ def get_subject_schedule(request):
def teacher_update_profile_picture(request):
"""View to handle profile picture update for teachers"""
if request.method == "POST":
- teacher = Staff.objects.get(phone=request.user.phone)
+ 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:
@@ -494,7 +552,12 @@ def get_attendance_form(request):
})
try:
- routine = Routine.objects.get(id=routine_id, teacher=request.user)
+ 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
@@ -555,7 +618,10 @@ def get_students(request):
return JsonResponse({'error': 'Missing routine_id parameter'}, status=400)
try:
- routine = Routine.objects.get(id=routine_id, teacher=request.user)
+ 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(
@@ -583,3 +649,376 @@ def get_students(request):
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
index 33e2500..8db9671 100644
--- a/app/studentviews.py
+++ b/app/studentviews.py
@@ -20,6 +20,7 @@ from .models import (
Institute,
InstituteFeedback,
Subject,
+ FEEDBACK_TYPE_CHOICES,
)
@@ -31,6 +32,11 @@ def studentDashboard(request):
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")
@@ -41,7 +47,7 @@ def studentDashboard(request):
).order_by("name")
# Get feedback types for institute feedback
- feedback_types = InstituteFeedback.FEEDBACK_TYPE_CHOICES
+ feedback_types = FEEDBACK_TYPE_CHOICES
# Course Progress and Tracking
course_trackings = CourseTracking.objects.filter(student=student).select_related(
@@ -71,51 +77,84 @@ def studentDashboard(request):
# Course Details with enhanced information
course_details = []
for tracking in course_trackings:
- # Calculate expected end date based on course duration
- if tracking.start_date:
- if tracking.course.duration_type == "Year":
- duration_days = tracking.course.duration * 365 # Years to days
- else: # Semester based
- duration_days = (
- tracking.course.duration * 365
- ) # Use full years for total duration
-
- expected_end_date = tracking.start_date + timedelta(days=duration_days)
- else:
- expected_end_date = None
-
- course_details.append(
- {
- "name": tracking.course.name,
- "start_date": tracking.start_date,
- "expected_end_date": expected_end_date,
- "actual_end_date": tracking.actual_end_date,
- "completion_percentage": tracking.completion_percentage,
- "progress_status": tracking.progress_status,
- "current_semester": tracking.current_semester,
- "semester_start_date": tracking.semester_start_date,
- "semester_end_date": tracking.semester_end_date,
- "remaining_days": tracking.remaining_days,
- "is_completed": tracking.is_completed,
- "is_active": tracking.is_active,
- "duration_type": tracking.course.duration_type,
- "duration": tracking.course.duration,
- "total_duration_days": tracking.total_duration_days,
- "notes": tracking.notes,
- "book": tracking.course.book
- if hasattr(tracking.course, "book")
- else None,
- "topics": [], # This will be populated if you have topic tracking
- }
- )
+ 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 = student.get_current_subjects()
+ 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)
- total_classes = attendance_records.count()
- classes_attended = attendance_records.filter(student_attend=True).count()
+
+ # 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
)
@@ -149,10 +188,17 @@ def studentDashboard(request):
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,
+ "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(
@@ -180,23 +226,16 @@ def studentDashboard(request):
rejected_leaves = student_leaves.filter(status=2).count()
# Get current semester routines
- try:
- # Get routines that match the student's course and current semester directly
+ current_semester_routines = []
+ if active_tracking:
current_semester_routines = Routine.objects.filter(
- course=student.course,
- semester_or_year=student.current_semester,
+ course=active_tracking.course,
+ semester_or_year=active_tracking.current_semester,
is_active=True,
)
- except Exception as e:
- current_semester_routines = []
- # Check if student has any routines for any semester
- has_routines = False
- try:
- if student.course:
- has_routines = Routine.objects.filter(course=student.course).exists()
- except Exception as e:
- has_routines = False
+ # Check if student has any routines
+ has_routines = current_semester_routines.exists()
context = {
"student": student,
@@ -241,34 +280,44 @@ def studentDashboard(request):
def request_leave(request):
if request.method == "POST":
student = request.user
- date = request.POST.get("date")
+ start_date = request.POST.get("start_date")
+ end_date = request.POST.get("end_date")
message = request.POST.get("message")
try:
- # Convert date string to timezone-aware datetime
- leave_date = timezone.datetime.strptime(date, "%Y-%m-%d")
- leave_date = timezone.make_aware(leave_date)
+ # 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 leave_date.date() < today.date():
- messages.error(request, "Leave date cannot be in the past.")
+ if start_date.date() < today.date():
+ messages.error(request, "Start date cannot be in the past.")
return redirect("studentDashboard")
- # Check if leave already exists for this date
+ 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, date=leave_date.date()
+ 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 this date."
+ request, "You have already requested leave for these dates."
)
return redirect("studentDashboard")
# Create leave request
leave = Student_Leave.objects.create(
student=student,
- date=leave_date.date(),
+ start_date=start_date.date(),
+ end_date=end_date.date(),
message=message,
status=0, # Pending status
)
@@ -291,21 +340,18 @@ def submit_feedback(request):
# Validate rating range (1 to 5)
if not (1 <= rating <= 5):
- return JsonResponse(
- {"success": False, "message": "Rating must be between 1 and 5"}
- )
+ messages.error(request, "Rating must be between 1 and 5")
+ return redirect("studentDashboard")
if not feedback_text:
- return JsonResponse(
- {"success": False, "message": "Please provide 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:
- return JsonResponse(
- {"success": False, "message": "Please select a teacher"}
- )
+ messages.error(request, "Please select a teacher")
+ return redirect("studentDashboard")
try:
teacher = Staff.objects.get(id=teacher_id)
@@ -319,6 +365,7 @@ def submit_feedback(request):
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(
@@ -327,85 +374,20 @@ def submit_feedback(request):
rating=rating,
feedback_text=feedback_text,
)
-
- return JsonResponse(
- {
- "success": True,
- "message": "Teacher feedback submitted successfully",
- }
- )
+ messages.success(request, "Teacher feedback submitted successfully")
except Staff.DoesNotExist:
- return JsonResponse(
- {"success": False, "message": "Selected teacher does not exist"}
- )
-
- elif feedback_type == "institute":
- category = request.POST.get("category")
- is_anonymous = request.POST.get("is_anonymous") == "on"
-
- if not category:
- return JsonResponse(
- {
- "success": False,
- "message": "Please select a feedback category",
- }
- )
-
- try:
- institute = Institute.objects.first() # Assuming single institute
- # Check if feedback already exists for this category - use new structure
- existing_feedback = InstituteFeedback.objects.filter(
- user_type="student",
- content_object_id=request.user.id,
- institute=institute,
- feedback_type=category,
- ).first()
-
- if existing_feedback:
- # Update existing feedback
- existing_feedback.rating = rating
- existing_feedback.feedback_text = feedback_text
- existing_feedback.is_anonymous = is_anonymous
- existing_feedback.save()
- else:
- # Create new feedback - use new structure
- InstituteFeedback.objects.create(
- user_type="student",
- content_object_id=request.user.id,
- user=request.user,
- institute=institute,
- feedback_type=category,
- rating=rating,
- feedback_text=feedback_text,
- is_anonymous=is_anonymous,
- )
-
- return JsonResponse(
- {
- "success": True,
- "message": "Institute feedback submitted successfully",
- }
- )
-
- except Institute.DoesNotExist:
- return JsonResponse(
- {"success": False, "message": "Institute not found"}
- )
+ messages.error(request, "Selected teacher does not exist")
else:
- return JsonResponse(
- {"success": False, "message": "Invalid feedback type"}
- )
+ messages.error(request, "Invalid feedback type")
except ValueError:
- return JsonResponse({"success": False, "message": "Invalid rating value"})
+ messages.error(request, "Invalid rating value")
except Exception as e:
- return JsonResponse(
- {"success": False, "message": f"An error occurred: {str(e)}"}
- )
+ messages.error(request, f"An error occurred: {str(e)}")
- return JsonResponse({"success": False, "message": "Invalid request method"})
+ return redirect("studentDashboard")
@login_required
@@ -418,6 +400,11 @@ def submit_institute_feedback(request):
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:
@@ -440,10 +427,9 @@ def submit_institute_feedback(request):
email="default@example.com",
)
- # Check if feedback already exists for this type - using new structure
+ # Check if feedback already exists for this type
existing_feedback = InstituteFeedback.objects.filter(
- user_type="student",
- content_object_id=student.id,
+ user=student,
institute=institute,
feedback_type=feedback_type,
).first()
@@ -454,58 +440,50 @@ def submit_institute_feedback(request):
existing_feedback.feedback_text = feedback_text
existing_feedback.is_anonymous = is_anonymous
existing_feedback.save()
- messages.success(request, "Institute feedback updated successfully.")
+ messages.success(request, "Institute feedback updated successfully")
else:
- # Create new feedback with new structure
- feedback = InstituteFeedback.objects.create(
- user_type="student",
- content_object_id=student.id,
+ # 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.")
+ messages.success(request, "Institute feedback submitted successfully")
+
except ValueError:
messages.error(request, "Invalid rating value")
except Exception as e:
messages.error(request, f"Error submitting institute feedback: {str(e)}")
- return redirect("studentDashboard")
-
return redirect("studentDashboard")
@login_required
@require_http_methods(["GET"])
def get_subject_files(request, subject_id):
- """View to get syllabus PDF for a subject"""
+ """View to get all files for a subject"""
try:
subject = Subject.objects.get(id=subject_id)
- pdf_url = subject.get_pdf_url()
-
- if not pdf_url:
+ files = subject.get_all_files()
+
+ if not files:
return JsonResponse(
- {"success": False, "message": "No syllabus available for this subject"}
+ {"success": False, "message": "No files available for this subject"}
)
return JsonResponse(
{
"success": True,
- "files": [
- {
- "title": f"{subject.name} Syllabus",
- "description": f"Syllabus PDF for {subject.name}",
- "file_url": pdf_url,
- }
- ],
+ "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 syllabus: {str(e)}"}
+ {"success": False, "message": f"Error retrieving files: {str(e)}"}
)
diff --git a/app/urls.py b/app/urls.py
index 840a3b0..9d513b1 100644
--- a/app/urls.py
+++ b/app/urls.py
@@ -1,59 +1,89 @@
from django.urls import path
-from . import views
+from app import views
from django.contrib import admin
-from . import auth
-from . import staffviews
+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 = [
+ # 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",
- ),
- path("save-fcm-token/", views.saveFCMToken, name="saveFCMToken"),
- path("firebase-messaging-sw.js", views.showFirebaseJS, name="showFirebaseJS"),
+ 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(
- "subject//files/",
- views.get_subject_files,
- name="get_subject_files",
- ),
- path("get-subjects/", views.get_subjects, name="get_subjects"),
- path('get-teachers/', views.get_teachers, name='get_teachers'),
- path('get-course-duration/', views.get_course_duration, name='get_course_duration'),
- path('save-attendance/', views.save_attendance, name='save_attendance'),
- path('get-subject-schedule/', views.get_subject_schedule, name='get_subject_schedule'),
- path('get-attendance-form/', staffviews.get_attendance_form, name='get_attendance_form'),
- path('teacher-update-profile-picture/', views.teacher_update_profile_picture, name='teacher_update_profile_picture'),
- path('teacher-change-password/', views.teacher_change_password, name='teacher_change_password'),
- path('approve-student-leave//', views.approve_student_leave, name='approve_student_leave'),
- path('reject-student-leave//', views.reject_student_leave, name='reject_student_leave'),
- path('approve-staff-leave//', views.approve_staff_leave, name='approve_staff_leave'),
- path('reject-staff-leave//', views.reject_staff_leave, name='reject_staff_leave'),
- path('add-notice/', views.add_notice, name='add_notice'),
- path('delete-notice//', views.delete_notice, name='delete_notice'),
+ 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"),
- # Password reset paths
- path('password-reset/', auth.reset_password_options, name='reset_password_options'),
- path('password-reset/phone/', auth.password_reset_phone, name='phone_reset_password'),
- path('password-reset/phone/verify/', auth.password_reset_phone_verify, name='verify_reset_otp'),
- path('password-reset/phone/resend/', auth.resend_phone_otp, name='resend_phone_otp'),
- path('password-reset/email/', auth.password_reset_email, name='email_reset_password'),
- path('password-reset/email/verify/', auth.password_reset_email_verify, name='password_reset_done'),
- path('password-reset/email/resend/', auth.resend_email_code, name='resend_reset_code'),
- path('password-reset/set/', auth.set_new_password, name='set_new_password'),
- path('get-students/', staffviews.get_students, name='get_students'),
+ # 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
index 34d8b68..f8ab97a 100644
--- a/app/utils.py
+++ b/app/utils.py
@@ -1,128 +1,119 @@
import random
import string
-from django.core.cache import cache
import requests
-from firebase_admin import messaging, initialize_app, get_app, credentials
-import os
from django.template.loader import render_to_string
from django.core.mail import send_mail
from django.conf import settings
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import urlsafe_base64_encode
from django.utils.encoding import force_bytes
+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_otp(length=6):
- """Generate a numeric OTP of specified length"""
- digits = string.digits
- return "".join(random.choice(digits) for _ in range(length))
+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 store_otp(phone, otp):
- """Store OTP in cache with phone as key"""
- cache_key = f"otp_{phone}"
- cache.set(cache_key, otp, OTP_EXPIRY)
- return True
+def generate_otp(secret_key):
+ """Generate a TOTP using the secret key"""
+ totp = pyotp.TOTP(secret_key, interval=OTP_EXPIRY)
+ return totp.now()
-def verify_otp(phone, otp):
- """Verify if the provided OTP matches the stored OTP for the phone"""
- cache_key = f"otp_{phone}"
- stored_otp = cache.get(cache_key)
+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
+ )
- if stored_otp and stored_otp == otp:
- # Delete the OTP after successful verification
- cache.delete(cache_key)
- return True
- return False
+
+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 like Twilio, MSG91, etc."""
+ """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": f"Your OTP for SMS login is: {otp}. Valid for 5 minutes."
+ # "message": message
# }
# response = requests.post(api_url, json=payload)
# return response.status_code == 200
- print(f"OTP for {phone}: {otp}")
+ 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_push_notification(title, message, tokens):
- """
- Send push notification using Firebase Cloud Messaging
- """
- try:
- try:
- app = get_app()
- except ValueError:
- # Initialize Firebase Admin SDK if not already initialized
- cred_path = os.path.join(
- os.path.dirname(os.path.dirname(__file__)),
- "firebase-key.json",
- )
- if not os.path.exists(cred_path):
- print(f"Firebase credentials file not found at {cred_path}")
- return
-
- cred = credentials.Certificate(cred_path)
- app = initialize_app(cred)
-
- if not tokens:
- print("No FCM tokens available to send notifications")
- return
-
- # Create message
- message = messaging.MulticastMessage(
- notification=messaging.Notification(
- title=title,
- body=message,
- ),
- tokens=list(tokens),
- )
-
- # Send message
- response = messaging.send_multicast(message)
-
- if response.failure_count > 0:
- failures = []
- for idx, resp in enumerate(response.responses):
- if not resp.success:
- failures.append(
- f"Failed to send message to token {tokens[idx]}: {resp.exception}"
- )
- print("\n".join(failures))
-
- except Exception as e:
- print(f"Error sending notification: {e}")
- # Don't raise the exception to prevent save from failing
- pass
-
-
def send_password_reset_email(user, request, email):
"""
Send password reset code to user's email using HTML template
"""
try:
- # Generate a reset code (OTP)
- reset_code = generate_otp(length=6)
-
- # Store the reset code in cache with user's email as key
- cache_key = f"reset_code_{email}"
- cache.set(cache_key, reset_code, OTP_EXPIRY) # 5 minutes expiry
+ # 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 = {
@@ -156,15 +147,40 @@ def send_password_reset_email(user, request, email):
def verify_reset_code(email, code):
- """
- Verify if the provided reset code matches the stored code for the email
- """
- cache_key = f"reset_code_{email}"
- stored_code = cache.get(cache_key)
+ """Verify password reset code using TOTP"""
+ return verify_otp(email, code)
- if stored_code and stored_code == code:
- # Delete the code after successful verification
- cache.delete(cache_key)
- return True
- return False
+
+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 6e537d0..2df4b4a 100644
--- a/app/views.py
+++ b/app/views.py
@@ -1,11 +1,11 @@
-from app.models import FCMDevice
from django.shortcuts import render, redirect, get_object_or_404
-from django.http import JsonResponse, HttpResponse
+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,
@@ -24,16 +24,18 @@ from .models import (
Batch,
)
from app.admin import custom_admin_site
-from django.views.decorators.http import require_http_methods, require_GET
+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, store_otp, verify_otp, send_otp_sms
+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,
@@ -54,395 +56,373 @@ from .staffviews import (
teacher_change_password,
)
+from django.contrib.auth.hashers import check_password
+
# Load environment variables
load_dotenv()
# Create your views here.
-# FireBase
-def saveFCMToken(request, token):
- try:
- FCMDevice(token=token).save()
- except:
- pass
- return JsonResponse({})
-
-
-def showFirebaseJS(request):
- data = f"""
- importScripts('https://www.gstatic.com/firebasejs/11.0.2/firebase-app-compat.js');
- importScripts('https://www.gstatic.com/firebasejs/11.0.2/firebase-messaging-compat.js');
-
- firebase.initializeApp({{
- apiKey: "{os.getenv('FIREBASE_API_KEY')}",
- authDomain: "{os.getenv('FIREBASE_AUTH_DOMAIN')}",
- projectId: "{os.getenv('FIREBASE_PROJECT_ID')}",
- storageBucket: "{os.getenv('FIREBASE_STORAGE_BUCKET')}",
- messagingSenderId: "{os.getenv('FIREBASE_MESSAGING_SENDER_ID')}",
- appId: "{os.getenv('FIREBASE_APP_ID')}",
- measurementId: "{os.getenv('FIREBASE_MEASUREMENT_ID')}"
- }});
-
- const messaging = firebase.messaging();
-
- messaging.onBackgroundMessage((payload) => {{
- const notificationTitle = payload.notification.title;
- const notificationOptions = {{
- body: payload.notification.body,
- icon: payload.notification.icon
- }};
-
- return self.registration.showNotification(notificationTitle, notificationOptions);
- }});
- """
- return HttpResponse(data, content_type="text/javascript")
-
@login_required
def dashboard(request):
"""Main dashboard view that redirects to appropriate dashboard based on user role"""
user = request.user
user_groups = user.groups.values_list("name", flat=True)
+
+
# Redirect based on user role
if "Student" in user_groups:
+ 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")
- # Get current date and date ranges
- today = timezone.now().date()
- last_week = timezone.now() - timedelta(days=7)
- last_month = timezone.now() - timedelta(days=30)
-
- # Student Statistics
- total_students = Student.objects.count()
- active_students = Student.objects.filter(status="Active").count()
- completed_students = Student.objects.filter(status="Completed").count()
- on_leave_students = Student.objects.filter(status="Leave").count()
-
- # Staff Statistics
- total_staff = Staff.objects.count()
- active_staff = Staff.objects.filter(is_active=True).count()
- staff_on_leave = (
- Staff_leave.objects.filter(status=1).values("staff").distinct().count()
- )
-
- # Course Statistics
- total_courses = Course.objects.count()
- active_courses = (
- 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 for {leave.date}",
- "timestamp": leave.created_at,
- }
+ # 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()
)
- for leave in recent_staff_leaves:
- recent_activities.append(
- {
- "title": f"Staff Leave Request",
- "description": f"{leave.staff.name} requested leave for {leave.date}",
- "timestamp": leave.created_at,
- }
+ # Course Statistics
+ total_courses = Course.objects.count()
+ active_courses = (
+ Course.objects.filter(student_trackings__progress_status="In Progress")
+ .distinct()
+ .count()
)
-
- # 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")
+ completed_courses = (
+ Course.objects.filter(student_trackings__progress_status="Completed")
.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,
- }
- )
+ # 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")
- # Sort teachers by average rating (highest first)
- teacher_ratings.sort(key=lambda x: x["avg_rating"], reverse=True)
+ # Recent Leave Requests
+ recent_student_leaves = Student_Leave.objects.filter(
+ created_at__gte=last_week
+ ).order_by("-created_at")[:5]
- # 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(
+ 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(
{
- "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,
+ "title": f"Student Leave Request",
+ "description": f"{leave.student.name} requested leave from {leave.start_date} to {leave.end_date}",
+ "timestamp": leave.created_at,
}
)
- # 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
+ 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,
+ }
)
- 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,
+ # 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": monthly_stats,
- "notices": notices, # Add notices to context
- "recent_activities": recent_activities, # Add recent activities to context
- }
+ 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(),
+ }
- return render(request, "dashboard.html", context)
+ 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
- name = request.POST.get("name")
- email = request.POST.get("email")
- phone = request.POST.get("phone")
- address = request.POST.get("address")
- gender = request.POST.get("gender")
- dob = request.POST.get("dob")
+ user_groups = user.groups.values_list("name", flat=True)
- # Validate data
- if not all([name, email, phone]):
- messages.error(request, "Name, email and phone are required fields.")
- return redirect("studentDashboard")
-
- # Update profile based on user type
- try:
- if hasattr(user, "student"):
- student = user
- student.name = name
- student.email = email
- student.phone = phone
- student.address = address
- student.gender = gender
- if dob:
- student.dob = dob
-
- # Handle profile image
- if "profile_image" in request.FILES:
- image = request.FILES["profile_image"]
+ # 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 updated successfully.")
-
- elif hasattr(user, "staff"):
- staff = user
- staff.name = name
- staff.email = email
- staff.phone = phone
- staff.address = address
- staff.gender = gender
- if dob:
- staff.dob = dob
-
- # Handle profile image
- if "profile_image" in request.FILES:
- image = request.FILES["profile_image"]
+ 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 updated successfully.")
-
- else:
- messages.error(request, "User profile not found.")
-
- except Exception as e:
- messages.error(request, f"Error updating profile: {str(e)}")
+ 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 hasattr(user, "student"):
+ if "Student" in user_groups:
return redirect("studentDashboard")
- elif hasattr(user, "staff"):
+ elif "Teacher" in user_groups:
return redirect("teacherDashboard")
else:
return redirect("dashboard")
@@ -458,17 +438,26 @@ def change_password(request):
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")
@@ -476,11 +465,16 @@ def change_password(request):
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
@@ -657,4 +651,215 @@ def delete_notice(request, notice_id):
return JsonResponse({"success": False, "error": "Invalid request method"})
+@login_required
+def student_dashboard(request):
+ student = request.user.student
+ today = timezone.now().date()
+
+ # Get attendance summary
+ attendance_records = AttendanceRecord.objects.filter(student=student)
+ total_classes = attendance_records.count()
+ attended_classes = attendance_records.filter(student_attend=True).count()
+ attendance_percentage = (attended_classes / total_classes * 100) if total_classes > 0 else 0
+
+ # Calculate current streak
+ current_streak = 0
+ longest_streak = 0
+ temp_streak = 1
+
+ for record in attendance_records.order_by('-attendance__date'):
+ if record.student_attend:
+ current_streak += 1
+ temp_streak += 1
+ longest_streak = max(longest_streak, temp_streak)
+ else:
+ break
+
+ attendance_summary = {
+ 'total_classes': total_classes,
+ 'attendance_percentage': attendance_percentage,
+ 'current_streak': current_streak,
+ 'longest_streak': longest_streak
+ }
+
+ # Get course tracking summary
+ course_trackings = CourseTracking.objects.filter(student=student)
+ course_summary = {
+ 'total_courses': course_trackings.count(),
+ 'completed_courses': course_trackings.filter(progress_status='Completed').count(),
+ 'ongoing_courses': course_trackings.filter(progress_status='In Progress').count(),
+ 'completion_percentage': (
+ sum(tracking.completion_percentage for tracking in course_trackings) / course_trackings.count()
+ if course_trackings.exists() else 0
+ )
+ }
+
+ # Get recent leaves
+ recent_leaves = Student_Leave.objects.filter(student=student).order_by('-created_at')[:5]
+
+ # Get recent feedback
+ recent_feedback = StudentFeedback.objects.filter(student=student).order_by('-created_at')[:5]
+
+ # Get notices
+ notices = Notice.objects.all().order_by('-created_at')[:5]
+
+ # Get attendance records
+ attendance_records = AttendanceRecord.objects.filter(student=student).order_by('-attendance__date')[:5]
+
+ # Get leave counts
+ pending_leaves = Student_Leave.objects.filter(student=student, status=0).count()
+ approved_leaves = Student_Leave.objects.filter(student=student, status=1).count()
+ rejected_leaves = Student_Leave.objects.filter(student=student, status=2).count()
+
+ # Get course details from course tracking
+ course_details = []
+ for tracking in course_trackings:
+ course_info = {
+ 'name': tracking.course.name,
+ 'start_date': tracking.start_date,
+ 'duration': tracking.course.duration,
+ 'duration_type': tracking.course.duration_type,
+ 'current_semester': tracking.current_semester,
+ 'progress_status': tracking.progress_status,
+ 'completion_percentage': tracking.completion_percentage,
+ 'semester_start_date': tracking.semester_start_date,
+ 'semester_end_date': tracking.semester_end_date,
+ 'expected_end_date': tracking.expected_end_date,
+ 'actual_end_date': tracking.actual_end_date,
+ 'remaining_days': tracking.remaining_days,
+ 'total_remaining_days': tracking.total_remaining_days
+ }
+ course_details.append(course_info)
+
+ context = {
+ 'student': student,
+ 'attendance_summary': attendance_summary,
+ 'course_summary': course_summary,
+ 'recent_leaves': recent_leaves,
+ 'recent_feedback': recent_feedback,
+ 'notices': notices,
+ 'attendance_records': attendance_records,
+ 'pending_leaves': pending_leaves,
+ 'approved_leaves': approved_leaves,
+ 'rejected_leaves': rejected_leaves,
+ 'course_details': course_details,
+ }
+ return render(request, 'student_dashboard.html', context)
+@login_required
+def teacher_dashboard(request):
+ teacher = request.user.teacher
+ today = timezone.now().date()
+
+ # Get today's routines
+ today_routines = Routine.objects.filter(
+ teacher=teacher,
+ day_of_week=today.weekday()
+ ).order_by('start_time')
+
+ # Get attendance summary
+ attendance_records = Attendance.objects.filter(routine__teacher=teacher)
+ total_classes = attendance_records.count()
+ total_students = AttendanceRecord.objects.filter(attendance__routine__teacher=teacher).count()
+ present_students = AttendanceRecord.objects.filter(
+ attendance__routine__teacher=teacher,
+ student_attend=True
+ ).count()
+
+ attendance_summary = {
+ 'total_classes': total_classes,
+ 'total_students': total_students,
+ 'present_students': present_students,
+ 'attendance_percentage': (present_students / total_students * 100) if total_students > 0 else 0
+ }
+
+ # Get recent leaves
+ recent_leaves = Student_Leave.objects.filter(
+ Q(status=0) | Q(status=1)
+ ).order_by('-created_at')[:5]
+
+ # Get recent feedback
+ recent_feedback = StudentFeedback.objects.filter(teacher=teacher).order_by('-created_at')[:5]
+
+ # Get notices
+ notices = Notice.objects.all().order_by('-created_at')[:5]
+
+ # Get attendance records
+ attendance_records = Attendance.objects.filter(routine__teacher=teacher).order_by('-date')[:5]
+
+ # Get leave counts
+ pending_leaves = Student_Leave.objects.filter(status=0).count()
+ approved_leaves = Student_Leave.objects.filter(status=1).count()
+ rejected_leaves = Student_Leave.objects.filter(status=2).count()
+
+ context = {
+ 'teacher': teacher,
+ 'today_routines': today_routines,
+ 'attendance_summary': attendance_summary,
+ 'recent_leaves': recent_leaves,
+ 'recent_feedback': recent_feedback,
+ 'notices': notices,
+ 'attendance_records': attendance_records,
+ 'pending_leaves': pending_leaves,
+ 'approved_leaves': approved_leaves,
+ 'rejected_leaves': rejected_leaves,
+ }
+ return render(request, 'teacher_dashboard.html', context)
+
+@login_required
+@require_POST
+def save_fcm_token(request):
+ try:
+ 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/firebase-key.json b/firebase-key.json
new file mode 100644
index 0000000..4b53a5a
--- /dev/null
+++ b/firebase-key.json
@@ -0,0 +1,13 @@
+{
+ "type": "service_account",
+ "project_id": "student-management-syste-2e040",
+ "private_key_id": "7f69c11c5fc3912654719387119959fa65ae1905",
+ "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDIDfN2l9xkXdVw\nv2kVlN5TSkWgYdWSBBqT6ji+CSWmasNfzyez4cr/yEriFBTXVD+tlxUHRGQTwUf7\nAR5i5j1fyr7/RCybp0x3go7ZQb0zI4vOXXTkGOEsZNItJG8Ca+mW5KW/PRSGbHll\nzbcLmmq7eH7Q5UfQvIBD8BgvdZFeqrsLNLjKQ9pi3z+jzZAFzYSRRT8sQeojqRnm\n4huaylg9u27sUroJ30n2Y3Q7SGFQKeu+DTVTdzr8+vcsp9wuASOHJ6lUhkmCApPu\n0iWH0fGPa11gyscCEMngwXqdAw7wC5BfMs5KtMwfI+zClschAga9EqXiFswcehaY\nTZtkRgovAgMBAAECggEAHbvabcv7GSfL1irSPD+YyRmxk47hprGEHxahlw0rqA/C\nOMvvWLsR6EZ6wB4VTZPbYGT43nr5wj3orD6UUkVqOyyairzf7G/x25QaFeHBdIev\nUIgaHDTecty2qGL75N4+emDtiaj9vfpYE3DUk0hA3jBLQ6fFrsP1mc4Q/4IijcaL\nKQn+NRUpbGrvQ95WupyLPEBlNSG1RhWHR1uFAfShZpjsMWtm9c7jdAmc356Xuvaj\n5FlRAqlTaHqDNnPwyRXfgDzegodgOaPKKjdXZi26/OQ+L80rSggippAjs5xvuugw\n+hfPVJwwdefCP1Ew/n8v6pPEH+Gx3qamhQ2gLIA0cQKBgQDk3ukGzFoG+V/gdqXO\nXzBZrNe72qXgMlzQU3nnMsZS73/3IwOUebSCEOTsxVdkM83mVg6BsQ+eVBsiyzWj\npt0fDr7sTsDHfJoX58nRoiGt9HRglq/zO8vobMAXgJaX3qhcYt6O9jpwcByNnZ/g\nSE6Y7i1CyUE/xmgeCV6VVrY4rQKBgQDfxJxhyYrCxzVNF1EkmizzA6j1ZKp3LJpp\nuJ5IV27LGnvxLkIwTomJW2aAWY3EP8b7baPWPTHAHCE+aQCiZevG6n8X1Rnek1z+\n7VMZooSomQpI6tDlEjzofur0CL5yJOzwZkaUHu3CNuGXnxnUnYKRxH80FGxyKw9U\nW9+gHLmdywKBgGDWFACFXB4RS0i8ysC7s8htgkYAaCipBm46s5xn9lPG8/zL+srK\nh950aXfos2Y5KQgpJzAmi3YxHd4l3sckZfCrG4r+c0C0brponxKsp9VjVg48E6w1\ng7wOr2QCSzVsxw1347TXZ7iFJv+gTrHlxiOcutfEMXBbcVgsHLHRI35tAoGAKeh2\nqVHHrScF8oQ1bDd7x5+d2yeZTWyLFtW+vymw1FUHdWoGd2meFJ2LmuoqxOB08c3c\n61jWonYiO8AKoF+aQszUNLBygd7zgcMM1zY6xiCzbmib6ur5gUmT01R35oTH41Yk\nTcVlHgSNR4pCmjFNrOMyN5zDmrY5zsDOJvx6PvUCgYB5iD0STmUSFDwOLSRD28LD\nW8STN3gvzcF9HCK2bzfkbr0PMA2QLn6VNueoe2mXKs3aJcWOmBoB0lMEqY3GKEW6\nA8MKEhT2ZFPAHYg3sm2e8cPAfPTei9PfB0yvdbKdMWwVCg9Jh8xSinWKMGvOpTTQ\nnYZC/nI+43nC3H8nKJzAqg==\n-----END PRIVATE KEY-----\n",
+ "client_email": "firebase-adminsdk-fbsvc@student-management-syste-2e040.iam.gserviceaccount.com",
+ "client_id": "110491093306637942148",
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+ "token_uri": "https://oauth2.googleapis.com/token",
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+ "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40student-management-syste-2e040.iam.gserviceaccount.com",
+ "universe_domain": "googleapis.com"
+}
diff --git a/requirements.txt b/requirements.txt
index 03275c1..109db62 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -34,10 +34,12 @@ pyasn1_modules==0.4.1
pycparser==2.22
PyJWT==2.10.1
pyparsing==3.2.3
+python-dotenv==1.0.1
pytz==2025.2
requests==2.32.3
rsa==4.9
sqlparse==0.5.3
uritemplate==4.1.1
urllib3==2.3.0
-python-dotenv==1.0.1
+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/dashboard.css b/static/css/dashboard.css
index 9dc9685..7990fc4 100644
--- a/static/css/dashboard.css
+++ b/static/css/dashboard.css
@@ -1,51 +1,47 @@
+/* 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;
}
-/* Material Design Color System */
-:root {
- --primary: #3F51B5; /* Indigo */
- --primary-light: #7986CB;
- --secondary: #9E9E9E; /* Grey */
- --success: #4CAF50; /* Green */
- --warning: #FFC107; /* Amber */
- --danger: #F44336; /* Red */
- --info: #2196F3; /* Blue */
- --background: #F5F5F5; /* Grey 100 */
- --card-bg: #FFFFFF;
- --text-primary: #212121; /* Grey 900 */
- --text-secondary: #757575; /* Grey 600 */
- --border-color: #BDBDBD; /* Grey 400 */
- --hover-bg: #E8EAF6; /* Indigo 50 */
- --card-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
- --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
-}
-
/* Base Layout Styles */
body {
background-color: var(--background);
+ opacity: 0;
+ transition: opacity 0.3s ease;
min-height: 100vh;
padding-bottom: 80px;
position: relative;
color: var(--text-primary);
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
-}
-
-/* Modern Card Styles */
-.content-card {
- background-color: var(--card-bg);
- border-radius: 8px;
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
- padding: 20px;
- margin-bottom: 20px;
- transition: var(--transition);
-}
-
-.content-card:hover {
- transform: translateY(-2px);
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
/* Bottom Navigation */
@@ -54,76 +50,537 @@ body {
bottom: 0;
left: 0;
right: 0;
- background-color: white;
- box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
+ 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: 10px;
+ 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;
- color: var(--secondary);
+ justify-content: center;
+ color: var(--text-secondary);
text-decoration: none;
- padding: 8px;
- font-size: 0.8rem;
- transition: all 0.2s;
- border-radius: 5px;
- min-width: 60px;
+ 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.2rem;
- margin-bottom: 4px;
+ 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);
- transform: scale(1.1);
+}
+
+.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) {
- background-color: rgba(0, 0, 0, 0.04);
+ color: var(--primary);
+ background-color: var(--hover-bg);
}
-/* Content Section Styles */
-.content-section {
- display: none;
- opacity: 0;
- transform: translateY(20px);
- transition: opacity 0.3s ease, transform 0.3s ease;
+/* 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;
+ }
}
-.content-section.active {
- display: block;
- opacity: 1;
- transform: translateY(0);
+/* 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-color: var(--card-bg);
- border-radius: 8px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ 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: 12px 16px;
+ padding: 1rem 1.25rem;
border-bottom: 1px solid var(--border-color);
}
-.card-body {
+.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 */
@@ -172,483 +629,7 @@ body {
background-color: var(--hover-bg);
}
-/* Status Badges */
-.status-badge {
- padding: 4px 8px;
- border-radius: 4px;
- font-size: 0.75rem;
- font-weight: 500;
- display: inline-block;
-}
-
-.bg-success, .completed, .leave-approved {
- background-color: var(--success) !important;
- color: white;
-}
-
-.bg-warning, .ongoing, .leave-pending {
- background-color: var(--warning) !important;
- color: #212121; /* Material design suggests dark text on light colors like Amber */
-}
-
-.bg-danger, .leave-rejected {
- background-color: var(--danger) !important;
- color: white;
-}
-
-.bg-info, .upcoming {
- background-color: var(--info) !important;
- color: white;
-}
-
-/* Animations */
-@keyframes fadeIn {
- from { opacity: 0; transform: translateY(20px); }
- to { opacity: 1; transform: translateY(0); }
-}
-
-/* Responsive Design */
-@media (max-width: 768px) {
- .container-fluid {
- padding: 0.5rem;
- }
-
- .content-card {
- padding: 16px;
- }
-
- .summary-card {
- margin-bottom: 1rem;
- }
-
- .card-header {
- padding: 1rem;
- }
-
- .card-body {
- padding: 1rem;
- }
-
- .table {
- font-size: 0.875rem;
- }
-
- .table th,
- .table td {
- padding: 0.75rem;
- white-space: nowrap;
- }
-
- .table-responsive {
- margin: 0 -0.5rem;
- }
-
- .row {
- margin-left: -0.5rem;
- margin-right: -0.5rem;
- }
-
- .col-12, .col-md-6 {
- padding-left: 0.5rem;
- padding-right: 0.5rem;
- }
-
- .bottom-nav {
- padding: 0.5rem;
- }
-
- .nav-item {
- padding: 0.375rem;
- }
-
- .nav-item i {
- font-size: 1.25rem;
- }
-
- .nav-item span {
- display: none;
- }
-
- .nav-item.active::after {
- bottom: -0.5rem;
- }
-
- /* Welcome Section */
- .welcome-section img {
- width: 60px !important;
- height: 60px !important;
- }
-
- .welcome-section h2 {
- font-size: 1.5rem;
- }
-
- /* Course Cards */
- .course-card {
- margin-bottom: 1rem;
- }
-
- .course-card .card-title {
- font-size: 1.1rem;
- }
-
- /* Subject Cards */
- .subject-card {
- margin-bottom: 1rem;
- }
-
- /* Feedback Cards */
- .feedback-card {
- margin-bottom: 1rem;
- }
-
- /* Notice Cards */
- .notice-card {
- margin-bottom: 1rem;
- }
-
- /* Profile Section */
- .profile-image {
- width: 120px !important;
- height: 120px !important;
- }
-
- /* Modal Adjustments */
- .modal-dialog {
- margin: 0.5rem;
- }
-
- .modal-body {
- padding: 1rem;
- }
-
- /* Form Elements */
- .form-control,
- .form-select {
- font-size: 0.875rem;
- }
-
- .btn {
- padding: 0.375rem 0.75rem;
- font-size: 0.875rem;
- }
-
- /* Rating Stars */
- .rating-container .fa-star {
- font-size: 1.25rem;
- }
-
- .nav-item {
- min-width: auto;
- }
-}
-
-/* Small Mobile Devices */
-@media (max-width: 480px) {
- .nav-container {
- padding: 0 0.5rem;
- }
-
- .nav-item {
- padding: 0.25rem;
- }
-
- .nav-item i {
- font-size: 1.1rem;
- }
-
- .nav-item span {
- font-size: 0.65rem;
- }
-
- .content-card {
- padding: 0.75rem;
- }
-
- .card-header {
- padding: 0.75rem;
- }
-
- .card-body {
- padding: 0.75rem;
- }
-
- .table th,
- .table td {
- padding: 0.5rem;
- }
-
- .status-badge {
- padding: 0.2rem 0.5rem;
- font-size: 0.75rem;
- }
-
- .btn {
- padding: 0.25rem 0.5rem;
- font-size: 0.75rem;
- }
-}
-
-/* Landscape Mode */
-@media (max-height: 500px) and (orientation: landscape) {
- .bottom-nav {
- position: static;
- margin-top: 1rem;
- }
-
- body {
- padding-bottom: 0;
- }
-
- .content-card {
- margin-bottom: 1rem;
- }
-}
-
-/* High-DPI Screens */
-@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
- .content-card {
- box-shadow: 0 5px 10px -3px rgba(0, 0, 0, 0.1);
- }
-}
-
-/* Print Styles */
-@media print {
- .bottom-nav,
- .btn-primary,
- .modal,
- .modal-backdrop {
- display: none !important;
- }
-
- .content-card {
- box-shadow: none;
- border: 1px solid #ddd;
- }
-
- .table {
- box-shadow: none;
- }
-}
-
-/* Modal styles */
-.modal {
- z-index: 1050 !important;
-}
-
-.modal-backdrop {
- z-index: 1040 !important;
-}
-
-.modal-content {
- border-radius: 8px !important;
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1) !important;
-}
-
-.modal-header {
- border-bottom: 1px solid #e5e7eb;
- padding: 1rem 1.5rem;
-}
-
-.modal-body {
- padding: 1.5rem;
-}
-
-.modal-footer {
- border-top: 1px solid #e5e7eb;
- padding: 1rem 1.5rem;
-}
-
-/* Navigation enhanced styles */
-.nav-item {
- transition: transform 0.2s ease-in-out;
-}
-
-.content-section {
- transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out;
- opacity: 0;
- transform: translateY(20px);
-}
-
-.content-section.active {
- opacity: 1;
- transform: translateY(0);
-}
-
-/* Improve active state visibility */
-.nav-item.active {
- color: var(--primary);
- font-weight: 600;
-}
-
-.nav-item.active i {
- transform: scale(1.1);
-}
-
-/* Add ripple effect for touch devices */
-.nav-item {
- position: relative;
- overflow: hidden;
-}
-
-.nav-item::after {
- content: '';
- position: absolute;
- top: 50%;
- left: 50%;
- width: 5px;
- height: 5px;
- background: rgba(79, 70, 229, 0.3);
- opacity: 0;
- border-radius: 100%;
- transform: scale(1, 1) translate(-50%);
- transform-origin: 50% 50%;
-}
-
-.nav-item:active::after {
- animation: ripple 0.6s ease-out;
-}
-
-@keyframes ripple {
- 0% {
- transform: scale(0, 0);
- opacity: 0.5;
- }
- 100% {
- transform: scale(100, 100);
- opacity: 0;
- }
-}
-
-/* Star Rating Styles */
-.rating-container .form-check-label {
- cursor: pointer;
- padding: 0.25rem;
-}
-
-.rating-container .fa-star,
-.rating-container .fa-star-half-alt {
- color: #f8d74c;
-}
-
-/* Profile Images */
-.profile-image-small {
- width: 80px;
- height: 80px;
- object-fit: cover;
-}
-
-.profile-image-large {
- width: 200px;
- height: 200px;
- object-fit: cover;
-}
-
-/* Add notification toast styles if they don't already exist */
-.notification-toast {
- position: fixed;
- top: 20px;
- right: 20px;
- z-index: 9999;
- min-width: 250px;
- box-shadow: 0 4px 8px rgba(0,0,0,0.1);
- border-radius: 4px;
-}
-
-/* Welcome Section */
-.welcome-section {
- background: linear-gradient(to right, #4338ca, #6366f1);
- color: white;
- padding: 20px;
-}
-
-/* Schedule styling */
-.schedule-item {
- padding: 12px;
- border-radius: 6px;
- margin-bottom: 10px;
- background-color: #f8f9fa;
- border-left: 4px solid var(--primary);
-}
-
-/* Rating stars styling */
-.rating-stars {
- color: #FFD700;
-}
-
/* Custom tabs styling */
-.feedback-tabs {
- border-bottom: 2px solid #e9ecef;
- margin-bottom: 1.5rem;
-}
-
-.feedback-tab-btn {
- padding: 10px 20px;
- cursor: pointer;
- border-radius: 4px;
- transition: var(--transition);
- border: 1px solid var(--border-color);
- background-color: white;
- color: var(--text-primary);
- text-align: center;
- flex: 1;
-}
-
-.feedback-tab-btn.active {
- background-color: var(--primary);
- color: white;
- border-color: var(--primary);
-}
-
-.feedback-tab-btn:hover:not(.active) {
- background-color: var(--hover-bg);
-}
-
-.tab-pane {
- display: none;
-}
-
-.tab-pane.active {
- display: block;
-}
-
-/* Star rating styling */
-.rating-container .form-check-label i.fas {
- color: #FFD700;
-}
-
-/* Custom smaller scrollbar styling */
-.activity-scroll::-webkit-scrollbar {
- width: 4px;
-}
-
-.activity-scroll::-webkit-scrollbar-track {
- background: #f1f1f1;
- border-radius: 10px;
-}
-
-.activity-scroll::-webkit-scrollbar-thumb {
- background: #ccc;
- border-radius: 10px;
-}
-
-.activity-scroll::-webkit-scrollbar-thumb:hover {
- background: #999;
-}
-
-/* Firefox scrollbar */
-.activity-scroll {
- scrollbar-width: thin;
- scrollbar-color: #ccc #f1f1f1;
-}
-
-@media (min-width: 992px) {
- .container-fluid {
- padding-bottom: 60px;
- }
-}
-
-/* Custom Tab System */
.custom-tabs {
display: flex;
border-bottom: 1px solid #dee2e6;
@@ -709,4 +690,466 @@ body {
.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/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
index c724eb3..61c7ce1 100644
--- a/static/js/dashboard.js
+++ b/static/js/dashboard.js
@@ -1,563 +1,785 @@
-document.addEventListener('DOMContentLoaded', function() {
- console.log("Dashboard script loaded");
-
- // Set default date to today for leave requests
- const dateInput = document.getElementById('customLeaveDate');
- if (dateInput) {
- const today = new Date();
- const year = today.getFullYear();
- let month = today.getMonth() + 1;
- let day = today.getDate();
+// 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
+ );
- month = month < 10 ? '0' + month : month;
- day = day < 10 ? '0' + day : day;
+ if (isLoggedIn) {
+ // Call with a slight delay to ensure page is fully loaded
+ setTimeout(function() {
+ checkWeakPassword();
+ }, 100);
+ }
- dateInput.min = `${year}-${month}-${day}`;
- dateInput.value = `${year}-${month}-${day}`;
- }
-
- // Close modals with Escape key
- document.addEventListener('keydown', function(event) {
- if (event.key === 'Escape') {
- // Check each modal and close if visible
- const modalIds = [
- { id: 'customLeaveModal', closeFn: closeLeaveModal },
- { id: 'profileModal', closeFn: closeProfileModal },
- { id: 'passwordModal', closeFn: closePasswordModal },
- { id: 'feedbackModal', closeFn: closeFeedbackModal },
- { id: 'pdfViewerModal', closeFn: closePdfViewerModal },
- { id: 'subjectFilesModal', closeFn: closeSubjectFilesModal }
- ];
-
- modalIds.forEach(modal => {
- const modalElement = document.getElementById(modal.id);
- if (modalElement && modalElement.style.display === 'block') {
- modal.closeFn();
+ // 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();
}
- });
- }
- });
-
- // Navigation
- const navItems = document.querySelectorAll('.nav-item');
- const contentSections = document.querySelectorAll('.content-section');
-
- // Function to scroll to section with smooth animation
- function scrollToSection(sectionId) {
- const section = document.getElementById(sectionId);
- if (section) {
- const offset = 20; // Offset from top
- const sectionPosition = section.getBoundingClientRect().top + window.pageYOffset - offset;
-
- window.scrollTo({
- top: sectionPosition,
- behavior: 'smooth'
- });
- }
- }
-
- // Function to update active state
- function updateActiveState(clickedItem) {
- // Remove active class from all nav items
- navItems.forEach(nav => {
- nav.classList.remove('active');
- nav.style.transform = 'scale(1)';
});
-
- // Add active class to clicked item
- clickedItem.classList.add('active');
- clickedItem.style.transform = 'scale(1.1)';
-
- // Remove active class from all sections
- contentSections.forEach(section => {
- section.classList.remove('active');
- section.style.opacity = '0';
- section.style.transform = 'translateY(20px)';
- });
-
- // Add active class to target section
- const sectionId = clickedItem.dataset.section + 'Section';
- const targetSection = document.getElementById(sectionId);
- if (targetSection) {
- targetSection.classList.add('active');
- targetSection.style.opacity = '1';
- targetSection.style.transform = 'translateY(0)';
- }
- }
-
- // Add click event listeners to nav items
- navItems.forEach(item => {
- item.addEventListener('click', function(e) {
- e.preventDefault();
- const sectionId = this.dataset.section + 'Section';
-
- // Update active state
- updateActiveState(this);
-
- // Scroll to section
- scrollToSection(sectionId);
- });
- });
-
- // Add scroll event listener to update active state based on scroll position
- let scrollTimeout;
- window.addEventListener('scroll', function() {
- clearTimeout(scrollTimeout);
- scrollTimeout = setTimeout(function() {
- const scrollPosition = window.scrollY + 100; // Offset for better detection
-
- contentSections.forEach(section => {
- const sectionTop = section.offsetTop - 100;
- const sectionBottom = sectionTop + section.offsetHeight;
-
- if (scrollPosition >= sectionTop && scrollPosition < sectionBottom) {
- const sectionId = section.id;
- const navItem = document.querySelector(`.nav-item[data-section="${sectionId.replace('Section', '')}"]`);
- if (navItem) {
- updateActiveState(navItem);
- }
- }
- });
- }, 100);
- });
-
- // Add touch feedback
- navItems.forEach(item => {
- item.addEventListener('touchstart', function() {
- this.style.transform = 'scale(0.95)';
- });
-
- item.addEventListener('touchend', function() {
- this.style.transform = this.classList.contains('active') ? 'scale(1.1)' : 'scale(1)';
- });
- });
-
- // Initialize first section as active
- const firstNavItem = navItems[0];
- if (firstNavItem) {
- updateActiveState(firstNavItem);
- }
-
- // Initialize rating functionality
- setupStarRating();
-
- // Set up subject files buttons
- document.querySelectorAll('.view-subject-files').forEach(button => {
- button.addEventListener('click', function() {
- const subjectId = this.getAttribute('data-subject-id');
- viewSubjectFiles(subjectId);
- });
- });
-
- // Handle image loading errors
- document.querySelectorAll('img[src*="student.image.url"]').forEach(img => {
- img.addEventListener('error', function() {
- this.onerror = null;
- this.src = '/static/img/user.png';
- });
- });
-
- // Handle feedback form tabs
- setupFeedbackTabs();
-
- // Initialize Bootstrap tooltip and popover components
- initializeBootstrapComponents();
-
- // Setup custom tabs
- setupCustomTabs();
-
- // Add form validation for rating form
- setupRatingFormValidation();
});
-// Modal Functions
-function openLeaveModal() {
- document.getElementById('customLeaveModal').style.display = 'block';
- document.body.style.overflow = 'hidden';
-}
-
-function closeLeaveModal() {
- document.getElementById('customLeaveModal').style.display = 'none';
- document.body.style.overflow = '';
-}
-
-function openProfileModal() {
- document.getElementById('profileModal').style.display = 'block';
- document.body.style.overflow = 'hidden';
-}
-
-function closeProfileModal() {
- document.getElementById('profileModal').style.display = 'none';
- document.body.style.overflow = '';
-}
-
-function openPasswordModal() {
- document.getElementById('passwordModal').style.display = 'block';
- document.body.style.overflow = 'hidden';
-}
-
-function closePasswordModal() {
- document.getElementById('passwordModal').style.display = 'none';
- document.body.style.overflow = '';
-}
-
-function openFeedbackModal() {
- document.getElementById('feedbackModal').style.display = 'block';
- document.body.style.overflow = 'hidden';
-}
-
-function closeFeedbackModal() {
- document.getElementById('feedbackModal').style.display = 'none';
- document.body.style.overflow = '';
-}
-
-function openPdfViewerModal(url) {
- document.getElementById('pdfViewer').src = url;
- document.getElementById('pdfViewerModal').style.display = 'block';
- document.body.style.overflow = 'hidden';
-}
-
-function closePdfViewerModal() {
- document.getElementById('pdfViewerModal').style.display = 'none';
- document.body.style.overflow = '';
-}
-
-function openSubjectFilesModal(subjectId) {
- // Use the viewSubjectFiles function which fetches the files from the server
- viewSubjectFiles(subjectId);
-}
-
-function closeSubjectFilesModal() {
- document.getElementById('subjectFilesModal').style.display = 'none';
- document.body.style.overflow = '';
-}
-
-// Setup custom tabs
-function setupCustomTabs() {
- const tabButtons = document.querySelectorAll('.custom-tab-btn');
- if (tabButtons.length > 0) {
- tabButtons.forEach(button => {
- button.addEventListener('click', function() {
- // Remove active class from all buttons
- tabButtons.forEach(btn => btn.classList.remove('active'));
-
- // Add active class to clicked button
- this.classList.add('active');
-
- // Hide all content
- document.querySelectorAll('.custom-tab-content').forEach(content => {
- content.classList.remove('active');
+// 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);
});
- // Show target content
- const target = this.getAttribute('data-target');
- document.getElementById(target).classList.add('active');
- });
- });
- }
-}
-
-// Setup feedback form validation
-function setupRatingFormValidation() {
- const ratingForm = document.querySelector('.feedback-form');
- const ratingInput = document.getElementById('selected_rating');
- const ratingError = document.getElementById('rating-error');
-
- if (ratingForm && ratingInput && ratingError) {
- // Add form validation before submission
- ratingForm.addEventListener('submit', function(e) {
- // Check if rating is selected
- if (!ratingInput.value) {
- e.preventDefault(); // Stop form submission
- if (ratingError) {
- ratingError.style.display = 'block';
- ratingError.scrollIntoView({ behavior: 'smooth' });
- }
- return false;
- }
- return true;
- });
- }
-}
-
-// Implementation for subject files
-function viewSubjectFiles(subjectId) {
- // Show loading state
- const filesList = document.getElementById('subjectFilesList');
- if (!filesList) return;
-
- filesList.innerHTML = `
-
-
- Loading...
-
-
Loading syllabus...
-
- `;
-
- // Show modal
- document.getElementById('subjectFilesModal').style.display = 'block';
- document.body.style.overflow = 'hidden';
-
- // Fetch syllabus
- fetch(`/app/subject/${subjectId}/files/`)
- .then(response => {
- if (!response.ok) {
- throw new Error('Network response was not ok');
- }
- return response.json();
- })
- .then(data => {
- if (data.success) {
- if (!data.files || data.files.length === 0) {
- filesList.innerHTML = `
-
-
-
No syllabus available for this subject
-
- `;
- } else {
- let html = '';
- data.files.forEach(file => {
- html += `
-
-
-
-
-
- ${file.title}
-
-
${file.description || 'No description'}
-
-
- ${file.file_url ? `
-
- Download
-
- ` : `
-
- File not available
-
- `}
-
-
-
- `;
- });
- filesList.innerHTML = html;
- }
- } else {
- throw new Error(data.message || 'Failed to load syllabus');
- }
- })
- .catch(error => {
- console.error('Error:', error);
- filesList.innerHTML = `
-
-
-
Unable to load syllabus. Please try again later.
-
${error.message}
-
- `;
- });
-}
-
-// Setup feedback tabs
-function setupFeedbackTabs() {
- const teacherTabBtn = document.getElementById('teacher-tab-btn');
- const instituteTabBtn = document.getElementById('institute-tab-btn');
-
- if (teacherTabBtn && instituteTabBtn) {
- teacherTabBtn.addEventListener('click', function() {
- switchFeedbackTab('teacher');
+ // 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";
+ });
});
- instituteTabBtn.addEventListener('click', function() {
- switchFeedbackTab('institute');
- });
- }
+ // 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 = `
+
+
+
+
+ Message goes here
+
+
+
+ `;
+ document.body.appendChild(toastContainer.firstElementChild);
+ }
}
-// Tab switching function
-function switchFeedbackTab(tabId) {
- // Hide all tab contents
- document.querySelectorAll('.tab-pane').forEach(tab => {
- tab.classList.remove('active');
- });
-
- // Deactivate all tab buttons
- document.querySelectorAll('.feedback-tab-btn').forEach(btn => {
- btn.classList.remove('active');
- });
-
- // Show selected tab content and activate button
- document.getElementById(`${tabId}-tab-content`).classList.add('active');
- document.getElementById(`${tabId}-tab-btn`).classList.add('active');
-}
-
-// Initialize Bootstrap components
-function initializeBootstrapComponents() {
- // Initialize tooltips if available
- if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
- const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
- tooltipTriggerList.map(function (tooltipTriggerEl) {
- return new bootstrap.Tooltip(tooltipTriggerEl);
- });
- }
-
- // Initialize popovers if available
- if (typeof bootstrap !== 'undefined' && bootstrap.Popover) {
- const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
- popoverTriggerList.map(function (popoverTriggerEl) {
- return new bootstrap.Popover(popoverTriggerEl);
- });
- }
-}
-
-// Function to show notification toast
-function showNotification(title, message, type = 'success') {
- const toast = document.getElementById('notificationToast');
- if (!toast) return;
-
- const toastTitle = document.getElementById('toastTitle');
- const toastMessage = document.getElementById('toastMessage');
-
- toastTitle.innerText = title;
- toastMessage.innerText = message;
-
- // Set toast color based on type
- toast.className = 'toast';
- if (type === 'success') {
- toast.classList.add('bg-success', 'text-white');
- } else if (type === 'error') {
- toast.classList.add('bg-danger', 'text-white');
- } else if (type === 'warning') {
- toast.classList.add('bg-warning');
- } else if (type === 'info') {
- toast.classList.add('bg-info', 'text-white');
- }
-
- const bsToast = new bootstrap.Toast(toast);
- bsToast.show();
-}
-
-// Function to set up star rating
+// Star rating functionality
function setupStarRating() {
- // Teacher feedback
- setupStarRatingComponent('teacher-rating', 'teacherStar');
- // Institute feedback
- setupStarRatingComponent('institute-rating', 'instituteStar');
- // Generic rating stars
- setupGenericRatingStars();
+ 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 setupStarRatingComponent(containerId, inputPrefix) {
- const container = document.getElementById(containerId);
- if (!container) return;
-
- const stars = container.querySelectorAll('.form-check-label');
- const inputs = Array.from(container.querySelectorAll('input[type="radio"]'));
-
- stars.forEach((star, index) => {
- // Click event
- star.addEventListener('click', function() {
- // Update visual state
- updateStars(stars, index);
-
- // Update form input
- inputs[index].checked = true;
+// 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');
+ });
});
- // Hover effects
- star.addEventListener('mouseenter', function() {
- highlightStars(stars, index);
+ // 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 = `
+
+
+
+
+
+
+ Your current password is weak and poses a security risk.
+
+
Please change your password to a more secure one to continue using the system.
+
+
+
+
+
`;
+
+ // 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');
+ }
+ });
});
- container.addEventListener('mouseleave', function() {
- resetStars(stars, inputs);
+ // 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 setupGenericRatingStars() {
- const stars = document.querySelectorAll('.rating-star');
- const ratingInput = document.getElementById('selected_rating');
- const ratingError = document.getElementById('rating-error');
-
- if (stars.length > 0 && ratingInput) {
- stars.forEach(star => {
- // Click event
- star.addEventListener('click', function() {
- const value = parseInt(this.getAttribute('data-value'));
- ratingInput.value = value;
-
- // Update the stars
- stars.forEach((s, index) => {
- if (index < value) {
- s.className = 'fas fa-star fs-4 text-warning rating-star';
- } else {
- s.className = 'far fa-star fs-4 text-warning rating-star';
- }
- });
-
- // Hide error message if displayed
- if (ratingError) ratingError.style.display = 'none';
- });
-
- // Hover events
- star.addEventListener('mouseenter', function() {
- const value = parseInt(this.getAttribute('data-value'));
-
- stars.forEach((s, index) => {
- if (index < value) {
- s.className = 'fas fa-star fs-4 text-warning rating-star';
- }
- });
- });
-
- star.addEventListener('mouseleave', function() {
- const selectedValue = parseInt(ratingInput.value) || 0;
-
- stars.forEach((s, index) => {
- if (index < selectedValue) {
- s.className = 'fas fa-star fs-4 text-warning rating-star';
- } else {
- s.className = 'far fa-star fs-4 text-warning rating-star';
- }
- });
- });
+// Function 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();
+ }
+ }
});
- }
}
-function updateStars(stars, selectedIndex) {
- stars.forEach((star, i) => {
- const starIcon = star.querySelector('i');
- if (i <= selectedIndex) {
- starIcon.className = 'fas fa-star fs-4';
- } else {
- starIcon.className = 'far fa-star fs-4';
+// 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();
}
- });
-}
-
-function highlightStars(stars, hoverIndex) {
- stars.forEach((star, i) => {
- const starIcon = star.querySelector('i');
- if (i <= hoverIndex) {
- starIcon.className = 'fas fa-star fs-4';
+
+ // 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 resetStars(stars, inputs) {
- const selectedIndex = inputs.findIndex(input => input.checked);
- stars.forEach((star, i) => {
- const starIcon = star.querySelector('i');
- if (selectedIndex >= 0 && i <= selectedIndex) {
- starIcon.className = 'fas fa-star fs-4';
- } else {
- starIcon.className = 'far fa-star fs-4';
+
+ 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
+}
\ No newline at end of file
diff --git a/static/js/feedback_rating.js b/static/js/feedback_rating.js
deleted file mode 100644
index 7e7b505..0000000
--- a/static/js/feedback_rating.js
+++ /dev/null
@@ -1,113 +0,0 @@
-document.addEventListener('DOMContentLoaded', function() {
- // Handle star rating selection for teacher feedback
- const teacherStars = document.querySelectorAll('[id^="teacherStar"]');
- const teacherLabels = document.querySelectorAll('label[for^="teacherStar"]');
-
- teacherLabels.forEach(label => {
- label.addEventListener('click', function() {
- const starId = this.getAttribute('for');
- const starNumber = parseInt(starId.replace('teacherStar', ''));
-
- // Update visual appearance
- teacherLabels.forEach((lbl, index) => {
- const star = lbl.querySelector('i');
- if (index < starNumber) {
- star.className = 'fas fa-star fs-4 text-warning';
- } else {
- star.className = 'far fa-star fs-4 text-warning';
- }
- });
-
- // Set the correct radio button
- document.getElementById(starId).checked = true;
- });
-
- // Add hover effects
- label.addEventListener('mouseenter', function() {
- const starId = this.getAttribute('for');
- const starNumber = parseInt(starId.replace('teacherStar', ''));
-
- teacherLabels.forEach((lbl, index) => {
- const star = lbl.querySelector('i');
- if (index < starNumber) {
- star.className = 'fas fa-star fs-4 text-warning';
- }
- });
- });
-
- label.addEventListener('mouseleave', function() {
- // Reset to selected state
- let selectedValue = 0;
- teacherStars.forEach((radio, index) => {
- if (radio.checked) {
- selectedValue = index + 1;
- }
- });
-
- teacherLabels.forEach((lbl, index) => {
- const star = lbl.querySelector('i');
- if (index < selectedValue) {
- star.className = 'fas fa-star fs-4 text-warning';
- } else {
- star.className = 'far fa-star fs-4 text-warning';
- }
- });
- });
- });
-
- // Handle star rating selection for institute feedback
- const instituteStars = document.querySelectorAll('[id^="instituteStar"]');
- const instituteLabels = document.querySelectorAll('label[for^="instituteStar"]');
-
- instituteLabels.forEach(label => {
- label.addEventListener('click', function() {
- const starId = this.getAttribute('for');
- const starNumber = parseInt(starId.replace('instituteStar', ''));
-
- // Update visual appearance
- instituteLabels.forEach((lbl, index) => {
- const star = lbl.querySelector('i');
- if (index < starNumber) {
- star.className = 'fas fa-star fs-4 text-warning';
- } else {
- star.className = 'far fa-star fs-4 text-warning';
- }
- });
-
- // Set the correct radio button
- document.getElementById(starId).checked = true;
- });
-
- // Add hover effects
- label.addEventListener('mouseenter', function() {
- const starId = this.getAttribute('for');
- const starNumber = parseInt(starId.replace('instituteStar', ''));
-
- instituteLabels.forEach((lbl, index) => {
- const star = lbl.querySelector('i');
- if (index < starNumber) {
- star.className = 'fas fa-star fs-4 text-warning';
- }
- });
- });
-
- label.addEventListener('mouseleave', function() {
- // Reset to selected state
- let selectedValue = 0;
- instituteStars.forEach((radio, index) => {
- if (radio.checked) {
- selectedValue = index + 1;
- }
- });
-
- instituteLabels.forEach((lbl, index) => {
- const star = lbl.querySelector('i');
- if (index < selectedValue) {
- star.className = 'fas fa-star fs-4 text-warning';
- } else {
- star.className = 'far fa-star fs-4 text-warning';
- }
- });
- });
- });
-});
\ No newline at end of file
diff --git a/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 = `
+
+
+ `;
+}
+
+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 = `
+
+ `;
+}
+
+// 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 = `
+
+ Back to Subjects
+
+ `;
+ 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 = `
+
+ Upload New Material
+
+ `;
+ contentDiv.appendChild(uploadBtn);
+ }
+ }, 200);
+ })
+ .catch(error => {
+ console.error('Error loading subject materials:', error);
+ contentDiv.innerHTML = `
+
+
+ Error loading subject materials: ${error.message}
+
+
+
+ Upload New Material
+
+
+ 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 = `
+
+
+ Loading...
+
+
${message}
+
+ `;
+}
+
+function showError(container, error, withRetryButton = false) {
+ if (!container) return;
+
+ const errorMessage = error?.message || DEFAULT_ERROR_MESSAGE;
+ container.innerHTML = `
+
+
+ ${errorMessage}
+
+
+ ${withRetryButton ? `
+
+
+ Retry
+
+
+ ` : ''}
+ `;
+}
+
+function showSuccess(container, message, autoHide = true) {
+ if (!container) return;
+
+ container.innerHTML = `
+
+
+ ${message}
+
+
+ `;
+
+ 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/settings.py b/student_management_system/settings.py
index b764a53..f121cad 100644
--- a/student_management_system/settings.py
+++ b/student_management_system/settings.py
@@ -25,12 +25,12 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = os.getenv("DJANGO_SECRET_KEY")
+SECRET_KEY = os.getenv('DJANGO_SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = os.getenv("DJANGO_DEBUG", "False") == "True"
+DEBUG = os.getenv('DJANGO_DEBUG', 'False') == 'True'
-ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "*").split(",")
+ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', '*').split(',')
# Application definition
@@ -49,6 +49,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
+ "whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
@@ -128,7 +129,7 @@ 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"),
]
@@ -137,6 +138,10 @@ STATICFILES_FINDERS = [
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
]
+
+# Configure whitenoise
+STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
+
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
@@ -151,35 +156,27 @@ JAZZMIN_SETTINGS = JAZZMIN_SETTINGS
JAZZMIN_UI_TWEAKS = JAZZMIN_UI_TWEAKS
# Login redirect URL
-LOGIN_REDIRECT_URL = "/app/dashboard/"
-LOGIN_URL = "/login/"
+LOGIN_REDIRECT_URL = '/app/dashboard/'
+LOGIN_URL = '/login/'
# Email Configuration
-EMAIL_BACKEND = os.getenv(
- "EMAIL_BACKEND", "django.core.mail.backends.console.EmailBackend"
-)
-DEFAULT_FROM_EMAIL = os.getenv(
- "DEFAULT_FROM_EMAIL", "noreply@studentmanagementsystem.com"
-)
+EMAIL_BACKEND = os.getenv('EMAIL_BACKEND', 'django.core.mail.backends.console.EmailBackend')
+DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'noreply@studentmanagementsystem.com')
# Cache configuration for OTP storage
CACHES = {
- "default": {
- "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
- "LOCATION": os.getenv("CACHE_LOCATION", "unique-snowflake"),
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
+ 'LOCATION': os.getenv('CACHE_LOCATION', 'unique-snowflake'),
}
}
# SMS API configuration
-SMS_API_KEY = os.getenv("SMS_API_KEY", "")
-SMS_SENDER_ID = os.getenv("SMS_SENDER_ID", "SMSSYS")
+SMS_API_KEY = os.getenv('SMS_API_KEY', '')
+SMS_SENDER_ID = os.getenv('SMS_SENDER_ID', 'SMSSYS')
-# CSRF_TRUSTED_ORIGINS = [
-# "https://ab4c-2405-acc0-1504-65ce-00-2.ngrok-free.app"
-# ]
-#
-# CSRF_TRUSTED_ORIGINS = [
-# "https://*.ngrok-free.app",
-# "https://*.ngrok.io"
-# ]
+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 260b3ac..e40550d 100644
--- a/student_management_system/urls.py
+++ b/student_management_system/urls.py
@@ -21,26 +21,30 @@ from django.conf import settings
from django.conf.urls.static import static
from django.contrib.auth import views as auth_views
from app.admin import custom_admin_site
-from django.views.generic import RedirectView
+from django.views.generic import RedirectView, TemplateView
from app import views
from app import auth
-# Serve media files without requiring authentication
-urlpatterns = static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
-
-# Add other URL patterns
-urlpatterns += [
+# Main URL patterns for the project
+urlpatterns = [
path("app/", include("app.urls")),
- path("admin/", custom_admin_site.urls), # Admin site at /admin/
- path("", RedirectView.as_view(url="/app/dashboard/", permanent=False)), # Redirect root to dashboard
+ path("admin/", custom_admin_site.urls),
- # Custom login and logout views
+ # 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 views - main entry point
+ # 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/base.html b/templates/base.html
index 6acb351..64e8508 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -2,21 +2,372 @@
+
-
+
+
+
+
{% block title %}Student Management System{% endblock %}
-
+
+
+
+
+
+
-
- {% block extrastyle %}{% endblock %}
+
+
+
+
+
+ {% block extrahead %}{% endblock %}
+ {% if messages %}
+
+ {% for message in messages %}
+
+ {% endfor %}
+
+ {% endif %}
+
{% block content %}{% endblock %}
- {% block extrahead %}{% 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 %}
+
+
+
+
+
+
Update Profile Picture
+ ×
+
+
+
+
+
+
+
+
+
+
Change Password
+ ×
+
+
+
+
+
+
+
+
+
+
Request Leave
+ ×
+
+
+
+
+
+
+
+
+
+
Confirm Deletion
+ ×
+
+
+
Are you sure you want to delete this file? This action cannot be undone.
+
+
+ Cancel
+
+ Delete
+
+
+
+
+
\ 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 @@
+
+
+
+
+
Give Feedback
+ ×
+
+
+
+
+
+ Teacher Feedback
+
+
+ Institute Feedback
+
+
+
+
+
+
+
+
+ {% csrf_token %}
+
+
+ Select Teacher
+
+ Select Teacher
+ {% for teacher in teachers %}
+ {{ teacher.name }} - {{ teacher.designation }}
+ {% endfor %}
+
+
+
+
Rating
+
+
+
+
+
+
+
+
Please select a rating
+
+
+
+ Your Feedback
+
+
+
+ Cancel
+ Submit Feedback
+
+
+
+
+
+
+
+ {% csrf_token %}
+
+
+ Feedback Category
+
+ Select Category
+ General
+ Facilities
+ Teaching Quality
+ Infrastructure
+ Administration
+
+
+
+
Rating
+
+
+
+
+
+
+
+
Please select a rating
+
+
+
+ Your Feedback
+
+
+
+
+
+
+ Submit anonymously
+
+
+
+
+ Cancel
+ Submit Feedback
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 %}
+
+
+
+
+
+
Subject Syllabus
+ ×
+
+
+
+
+
+
+
+
+
+
+
+
Subject Materials
+ ×
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
Give Institute Feedback
+ ×
+
+
+
+ {% csrf_token %}
+
+ Feedback Type
+
+ Select Type
+ {% for value, name in feedback_types %}
+ {{ name }}
+ {% endfor %}
+
+
+
+
+
Rating
+
+
+
+
+
+
+
+
Please select a rating
+
+
+
+
+ Feedback
+
+
+
+
+
+ Submit anonymously
+
+
+
+ Cancel
+ Submit Feedback
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
Take Attendance
+ ×
+
+
+
+ {% csrf_token %}
+
+
+
+
+
+
+
+ Teacher Present
+
+
+
+
+ Conducted
+ Not Conducted
+
+
+
+
+
+
+
+
+ #
+ Name
+ Student ID
+ Present
+
+
+
+
+
+
+
+
+
+
+
+ Mark All Present
+
+
+ Mark All Absent
+
+
+
+ Save Attendance
+
+
+
+
+
+
+
+
\ 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 %}
+
+
+
+ {% else %}
+
+
+
+ {% 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
index 29deed2..8400e96 100644
--- a/templates/dashboard.html
+++ b/templates/dashboard.html
@@ -2,19 +2,16 @@
{% load static %}
{% load app_filters %}
{% block extrahead %}
- {{ block.super }}
-
+{{ block.super }}
+
{% endblock %}
{% block extrastyle %}
- {{ block.super }}
-
-
-
-
-
{% endblock %}
{% block content %}
-
-
-
-
-
-
-
Welcome, Admin!
-
Student Management Dashboard
-
-
-
-
-
-
-