Files
Student-Management-System-I…/app/views.py
T

655 lines
23 KiB
Python

from django.shortcuts import render, redirect, get_object_or_404
from django.http import JsonResponse, HttpResponse
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.db.models import Count, Q, Avg
from django.utils import timezone
from datetime import timedelta
from django.conf import settings
from app.models import (
Student,
Course,
CourseTracking,
StudentLeave,
StaffLeave,
Attendance,
AttendanceRecord,
StudentFeedback,
Routine,
Staff,
Notice,
Institute,
InstituteFeedback,
Subject,
Batch,
ParentFeedback,
Parent,
StaffInstituteFeedback,
ParentInstituteFeedback,
TeacherParentMeeting,
)
from django.views.decorators.http import require_http_methods, require_GET, require_POST
from django.core.files.storage import default_storage
from django.core.files.base import ContentFile
import json
from app.admin import custom_admin_site
import os
from django.db.models.base import transaction
from dotenv import load_dotenv
from app.firebase import save_fcm_token, FCMDevice
from django.views.static import serve
from app.utils import (
handle_file_upload,
cleanup_failed_upload,
FileUploadError,
ALLOWED_IMAGE_TYPES,
MAX_IMAGE_SIZE,
ALLOWED_DOCUMENT_TYPES,
MAX_DOCUMENT_SIZE
)
from django.views.decorators.csrf import csrf_exempt
# Import views from studentviews.py
from app.studentviews import (
studentDashboard,
request_leave,
submit_feedback,
submit_institute_feedback,
get_subject_files,
)
# Import views from staffviews.py
from app.staffviews import (
teacherDashboard,
request_staff_leave,
submit_staff_institute_feedback,
save_attendance,
get_subject_schedule,
teacher_update_profile_picture,
teacher_change_password,
)
from django.contrib.auth.hashers import check_password
from django.urls import reverse
# Load environment variables
load_dotenv()
# Create your views here.
@login_required
def dashboard(request):
"""Main dashboard view that redirects to appropriate dashboard based on user role.
Args:
request: HttpRequest object containing user information
Returns:
HttpResponse: Redirects to appropriate dashboard or renders admin dashboard
"""
user = request.user
user_groups = user.groups.values_list("name", flat=True)
# Redirect based on user role
if "Student" in user_groups:
return redirect("studentDashboard")
elif "Teacher" in user_groups:
return redirect("teacherDashboard")
elif "Parent" in user_groups:
return redirect("parentDashboard")
elif "Admission Officer" in user_groups:
return redirect("admissionOfficerDashboard")
elif "HOD" in user_groups:
return redirect("hodDashboard")
else:
# 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()
male_students = Student.objects.filter(gender="Male").count()
female_students = Student.objects.filter(gender="Female").count()
inactive_students = total_students - active_students
# Staff Statistics
total_staff = Staff.objects.count()
active_staff = Staff.objects.filter(is_active=True).count()
total_teachers = Staff.objects.filter(designation="Teacher").count()
non_teaching_staff = total_staff - total_teachers
inactive_staff = total_staff - active_staff
# Parent Statistics
total_parents = Parent.objects.count()
active_parents = Parent.objects.filter(is_active=True).count()
parents_with_students = (
Parent.objects.filter(students__isnull=False).distinct().count()
)
parents_with_multiple_students = (
Parent.objects.annotate(student_count=Count("students"))
.filter(student_count__gt=1)
.count()
)
inactive_parents = total_parents - active_parents
# Course Statistics
total_courses = Course.objects.count()
active_courses = (
Course.objects.filter(
student_trackings__progress_status__in=["Not Started", "In Progress"]
)
.distinct()
.count()
)
total_subjects = Subject.objects.count()
total_batches = Batch.objects.count()
inactive_courses = total_courses - active_courses
# Recent Activities
recent_notices = Notice.objects.all().order_by("-created_at")[:5]
recent_feedback = (
list(InstituteFeedback.objects.all().order_by("-created_at")[:5])
+ list(StaffInstituteFeedback.objects.all().order_by("-created_at")[:5])
+ list(ParentInstituteFeedback.objects.all().order_by("-created_at")[:5])
)
recent_feedback = sorted(
recent_feedback, key=lambda x: x.created_at, reverse=True
)[:5]
# Leave Requests
staff_leaves = StaffLeave.objects.filter(status=0).order_by("-created_at")[
:5
] # 0 = Pending
student_leaves = StudentLeave.objects.filter(status=0).order_by("-created_at")[
:5
] # 0 = Pending
# Teacher Rating Statistics
teacher_ratings = (
Staff.objects.filter(designation="Teacher")
.annotate(
avg_student_rating=Avg("feedbacks__rating"),
avg_parent_rating=Avg("parent_feedbacks__rating"),
total_feedbacks=Count("feedbacks") + Count("parent_feedbacks"),
)
.order_by("-avg_student_rating")[:5]
)
# Get all meetings
meetings = TeacherParentMeeting.objects.all().order_by(
"-meeting_date", "-meeting_time"
)
context = {
"title": "Dashboard",
**custom_admin_site.each_context(request),
"total_students": total_students,
"active_students": active_students,
"male_students": male_students,
"female_students": female_students,
"inactive_students": inactive_students,
"total_staff": total_staff,
"active_staff": active_staff,
"total_teachers": total_teachers,
"non_teaching_staff": non_teaching_staff,
"inactive_staff": inactive_staff,
"total_parents": total_parents,
"active_parents": active_parents,
"parents_with_students": parents_with_students,
"parents_with_multiple_students": parents_with_multiple_students,
"inactive_parents": inactive_parents,
"total_courses": total_courses,
"active_courses": active_courses,
"total_subjects": total_subjects,
"total_batches": total_batches,
"inactive_courses": inactive_courses,
"recent_notices": recent_notices,
"recent_feedback": recent_feedback,
"staff_leaves": staff_leaves,
"student_leaves": student_leaves,
"teacher_ratings": teacher_ratings,
"meetings": meetings,
}
return render(request, "admin/dashboard.html", context)
@login_required
def update_profile(request):
"""Update user profile picture based on user role.
Args:
request: HttpRequest object containing user information and uploaded file
Returns:
HttpResponse: Redirects to appropriate dashboard with success/error message
"""
if request.method == "POST":
try:
user = request.user
user_groups = user.groups.values_list("name", flat=True)
# Handle profile image update
if "profile_image" in request.FILES:
try:
image = request.FILES["profile_image"]
# Handle file upload with security checks
try:
file_path = handle_file_upload(
image,
"profile_images",
ALLOWED_IMAGE_TYPES,
MAX_IMAGE_SIZE
)
except FileUploadError as e:
messages.error(request, str(e))
return redirect("dashboard")
# Update user's profile image
with transaction.atomic():
if "Student" in user_groups:
user.image = file_path
user.save()
elif "Teacher" in user_groups:
user.image = file_path
user.save()
elif "Parent" in user_groups:
user.image = file_path
user.save()
messages.success(request, "Profile picture updated successfully")
except Exception as e:
messages.error(request, f"Error updating profile picture: {str(e)}")
return redirect("dashboard")
return redirect("dashboard")
except Exception as e:
messages.error(request, f"An unexpected error occurred: {str(e)}")
return redirect("dashboard")
return redirect("dashboard")
@login_required
def change_password(request):
"""Change user password."""
if request.method == "POST":
try:
user = request.user
current_password = request.POST.get("current_password")
new_password = request.POST.get("new_password")
confirm_password = request.POST.get("confirm_password")
# Validate input
if not all([current_password, new_password, confirm_password]):
messages.error(request, "All fields are required")
return redirect("dashboard")
if new_password != confirm_password:
messages.error(request, "New passwords do not match")
return redirect("dashboard")
# Check current password
if not user.check_password(current_password):
messages.error(request, "Current password is incorrect")
return redirect("dashboard")
# Validate new password strength
if len(new_password) < 8:
messages.error(request, "Password must be at least 8 characters long")
return redirect("dashboard")
# Update password
with transaction.atomic():
user.set_password(new_password)
user.save()
messages.success(request, "Password changed successfully")
return redirect("dashboard")
except Exception as e:
messages.error(request, f"An error occurred while changing password: {str(e)}")
return redirect("dashboard")
return redirect("dashboard")
@login_required
@require_GET
def get_subjects(request):
"""Get subjects for a course."""
try:
course_id = request.GET.get("course_id")
if not course_id:
return JsonResponse({"error": "Course ID is required"}, status=400)
subjects = Subject.objects.filter(course_id=course_id).values(
"id", "name", "code", "period_or_year"
)
return JsonResponse(list(subjects), safe=False)
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)
@login_required
@require_GET
def get_teachers(request):
"""Get teachers for a subject."""
try:
subject_id = request.GET.get("subject_id")
if not subject_id:
return JsonResponse({"error": "Subject ID is required"}, status=400)
teachers = Staff.objects.filter(
designation="Teacher",
routine__subject_id=subject_id
).distinct().values("id", "name")
return JsonResponse(list(teachers), safe=False)
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)
@login_required
@require_GET
def get_course_duration(request):
"""Get course duration information."""
try:
course_id = request.GET.get("course_id")
if not course_id:
return JsonResponse({"error": "Course ID is required"}, status=400)
course = Course.objects.get(id=course_id)
return JsonResponse({
"duration": course.duration,
"duration_type": course.duration_type
})
except Course.DoesNotExist:
return JsonResponse({"error": "Course not found"}, status=404)
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)
@login_required
def approve_student_leave(request, leave_id):
"""View to approve a student leave request"""
if request.method == "POST":
leave = get_object_or_404(StudentLeave, id=leave_id)
leave.status = 1 # Approved
leave.save()
messages.success(
request, f"Leave request for {leave.student.name} has been approved."
)
return redirect("dashboard")
@login_required
def reject_student_leave(request, leave_id):
"""View to reject a student leave request"""
if request.method == "POST":
leave = get_object_or_404(StudentLeave, id=leave_id)
leave.status = 2 # Rejected
leave.save()
messages.warning(
request, f"Leave request for {leave.student.name} has been rejected."
)
return redirect("dashboard")
@login_required
def approve_staff_leave(request, leave_id):
"""View to approve a staff leave request"""
if request.method == "POST":
leave = get_object_or_404(StaffLeave, id=leave_id)
leave.status = 1 # Approved
leave.save()
messages.success(
request, f"Leave request for {leave.staff.name} has been approved."
)
return redirect("dashboard")
@login_required
def reject_staff_leave(request, leave_id):
"""View to reject a staff leave request"""
if request.method == "POST":
leave = get_object_or_404(StaffLeave, id=leave_id)
leave.status = 2 # Rejected
leave.save()
messages.warning(
request, f"Leave request for {leave.staff.name} has been rejected."
)
return redirect("dashboard")
@login_required
def add_notice(request):
if request.method == "POST":
try:
title = request.POST.get("title")
message = request.POST.get("message")
image = request.FILES.get("image")
file = request.FILES.get("file")
if not title or not message:
return JsonResponse(
{"success": False, "error": "Title and message are required"}
)
# Handle image upload if provided
image_path = None
if image:
try:
image_path = handle_file_upload(
image,
"notice_images",
ALLOWED_IMAGE_TYPES,
MAX_IMAGE_SIZE
)
except FileUploadError as e:
return JsonResponse({"success": False, "error": str(e)})
# Handle file upload if provided
file_path = None
if file:
try:
file_path = handle_file_upload(
file,
"notice_files",
ALLOWED_DOCUMENT_TYPES,
MAX_DOCUMENT_SIZE
)
except FileUploadError as e:
# Clean up image if file upload fails
if image_path:
cleanup_failed_upload(image_path)
return JsonResponse({"success": False, "error": str(e)})
# Create notice with uploaded files
try:
notice = Notice.objects.create(
title=title,
message=message,
image=image_path,
file=file_path
)
return JsonResponse(
{"success": True, "message": "Notice added successfully"}
)
except Exception as e:
# Clean up uploaded files if database operation fails
if image_path:
cleanup_failed_upload(image_path)
if file_path:
cleanup_failed_upload(file_path)
return JsonResponse({"success": False, "error": str(e)})
except Exception as e:
return JsonResponse({"success": False, "error": str(e)})
return JsonResponse({"success": False, "error": "Invalid request method"})
@login_required
def delete_notice(request, notice_id):
if request.method == "POST":
try:
notice = Notice.objects.get(id=notice_id)
notice.delete()
return JsonResponse({"success": True})
except Notice.DoesNotExist:
return JsonResponse({"success": False, "error": "Notice not found"})
return JsonResponse({"success": False, "error": "Invalid request method"})
@require_POST
@csrf_exempt
def save_fcm_token(request):
"""Save FCM token to database"""
try:
# Get token from request body
data = json.loads(request.body)
token = data.get('token')
if not token:
return JsonResponse({"error": "Token is required"}, status=400)
# Check if this is a fallback token
is_fallback = token.startswith("fcm-token-") or token.startswith("fallback-token-")
# Determine user type if a user is authenticated
user_type = "unknown"
if request.user.is_authenticated:
user_groups = request.user.groups.values_list("name", flat=True)
if "Student" in user_groups:
user_type = "student"
elif "Parent" in user_groups:
user_type = "parent"
elif "Teacher" in user_groups:
user_type = "teacher"
elif request.user.is_superuser:
user_type = "admin"
# Use update_or_create to avoid unnecessary deletes
device, created = FCMDevice.objects.update_or_create(
token=token,
defaults={
"is_fallback": is_fallback,
"is_active": True,
"user_type": user_type,
},
)
return JsonResponse({"status": "success"})
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid JSON data"}, status=400)
except Exception as e:
print(f"Error saving FCM token: {str(e)}")
return JsonResponse({"error": "Internal server error"}, status=500)
def serve_firebase_sw(request):
"""Serve the Firebase service worker file with correct content type and security headers"""
try:
# Get the path to the service worker in the static directory
sw_path = os.path.join(settings.STATICFILES_DIRS[0], "firebase-messaging-sw.js")
# If the file doesn't exist in static directory, try staticfiles
if not os.path.exists(sw_path):
sw_path = os.path.join(settings.STATIC_ROOT, "firebase-messaging-sw.js")
if not os.path.exists(sw_path):
print(f"Service worker file not found at: {sw_path}")
return HttpResponse("Service worker file not found", status=404)
with open(sw_path, "r") as f:
content = f.read()
# Create response with correct content type
response = HttpResponse(content, content_type="application/javascript")
# Set all necessary security headers for service worker
response["Service-Worker-Allowed"] = "/"
response["Cache-Control"] = "no-cache, no-store, must-revalidate"
response["Pragma"] = "no-cache"
response["Expires"] = "0"
# Set proper CORS headers for service worker
response["Access-Control-Allow-Origin"] = "*"
response["Access-Control-Allow-Methods"] = "GET, OPTIONS"
response["Access-Control-Allow-Headers"] = "Content-Type"
print(f"Serving service worker file successfully, size: {len(content)}")
return response
except Exception as e:
print(f"Error serving service worker: {str(e)}")
return HttpResponse("Internal server error", status=500)
@login_required
def check_weak_password(request):
"""Check if the current user's password is '123'"""
user = request.user
is_weak = check_password("123", user.password)
return JsonResponse({"is_weak": is_weak})
@login_required
def get_staff_leaves(request):
"""View to get staff leave requests for AJAX updates"""
staff_leaves = StaffLeave.objects.filter(status=0).order_by("-created_at")[:5]
leaves_data = []
for leave in staff_leaves:
leaves_data.append(
{
"id": leave.id,
"staff_name": leave.staff.name,
"start_date": leave.start_date.strftime("%b %d, %Y"),
"end_date": leave.end_date.strftime("%b %d, %Y"),
"message": leave.message[:50] + "..."
if len(leave.message) > 50
else leave.message,
}
)
return JsonResponse({"staff_leaves": leaves_data})
@login_required
def get_student_leaves(request):
"""View to get student leave requests for AJAX updates"""
student_leaves = StudentLeave.objects.filter(status=0).order_by("-created_at")[:5]
leaves_data = []
for leave in student_leaves:
leaves_data.append(
{
"id": leave.id,
"student_name": leave.student.name,
"start_date": leave.start_date.strftime("%b %d, %Y"),
"end_date": leave.end_date.strftime("%b %d, %Y"),
"message": leave.message[:50] + "..."
if len(leave.message) > 50
else leave.message,
}
)
return JsonResponse({"student_leaves": leaves_data})
@login_required
@require_GET
def get_meeting_cancellation_reason(request, meeting_id):
"""API endpoint to get the cancellation reason for a meeting"""
try:
meeting = get_object_or_404(TeacherParentMeeting, id=meeting_id)
if meeting.status != "cancelled":
return JsonResponse({"success": False, "error": "Meeting is not cancelled"})
return JsonResponse({"success": True, "reason": meeting.cancellation_reason})
except Exception as e:
return JsonResponse({"success": False, "error": str(e)})