Files
2025-04-15 04:20:26 +05:45

767 lines
27 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
# 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":
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"]
# Validate image file type
allowed_types = ["image/jpeg", "image/png", "image/gif"]
if image.content_type not in allowed_types:
messages.error(
request,
"Invalid file type. Please upload a JPEG, PNG, or GIF image.",
)
return redirect("dashboard")
# Validate image size (max 5MB)
if image.size > 5 * 1024 * 1024:
messages.error(
request,
"Image size too large. Please upload an image smaller than 5MB.",
)
return redirect("dashboard")
if "Student" in user_groups:
student = Student.objects.get(id=user.id)
if student.image:
# Delete old image if updating
if os.path.isfile(student.image.path):
os.remove(student.image.path)
# Save new image
student.image = image
student.save()
messages.success(request, "Profile picture updated successfully.")
elif "Teacher" in user_groups:
staff = Staff.objects.get(id=user.id)
if staff.image:
# Delete old image if updating
if os.path.isfile(staff.image.path):
os.remove(staff.image.path)
# Save new image
staff.image = image
staff.save()
messages.success(request, "Profile picture updated successfully.")
elif "Parent" in user_groups:
parent = Parent.objects.get(id=user.id)
if parent.image:
# Delete old image if updating
if os.path.isfile(parent.image.path):
os.remove(parent.image.path)
# Save new image
parent.image = image
parent.save()
messages.success(request, "Profile picture updated successfully.")
elif "HOD" in user_groups or any(
group.lower().startswith("hod") for group in user_groups
):
# HOD is typically a Staff member with special privileges
staff = Staff.objects.get(id=user.id)
if staff.image:
# Delete old image if updating
if os.path.isfile(staff.image.path):
os.remove(staff.image.path)
# Save new image
staff.image = image
staff.save()
messages.success(request, "Profile picture updated successfully.")
else:
messages.error(
request,
"User profile not found. Please make sure you are logged in as a student, staff, or parent member.",
)
return redirect("dashboard")
except (Student.DoesNotExist, Staff.DoesNotExist, Parent.DoesNotExist) as e:
messages.error(request, "User profile not found in the database.")
return redirect("dashboard")
except Exception as e:
messages.error(request, f"Error updating profile picture: {str(e)}")
return redirect("dashboard")
else:
messages.error(request, "No profile picture uploaded.")
return redirect("dashboard")
# Redirect based on user type
if "Student" in user_groups:
return redirect("studentDashboard")
elif "Teacher" in user_groups:
return redirect("teacherDashboard")
elif "Parent" in user_groups:
return redirect("parentDashboard")
elif "Admission Officer" in user_groups:
return redirect("admissionOfficerDashboard")
elif "HOD" in user_groups:
return redirect("hodDashboard")
else:
return redirect("dashboard")
return redirect("dashboard")
@login_required
def change_password(request):
if request.method == "POST":
user = request.user
user_groups = user.groups.values_list("name", flat=True)
current_password = request.POST.get("current_password")
new_password = request.POST.get("new_password")
confirm_password = request.POST.get("confirm_password")
# Check if this is an AJAX request
is_ajax = request.headers.get("X-Requested-With") == "XMLHttpRequest"
# Determine redirect URL based on user role
redirect_url = "dashboard"
if "Student" in user_groups:
redirect_url = "studentDashboard"
elif "Teacher" in user_groups:
redirect_url = "teacherDashboard"
elif "Parent" in user_groups:
redirect_url = "parentDashboard"
elif "Admission Officer" in user_groups:
redirect_url = "admissionOfficerDashboard"
elif "HOD" in user_groups:
redirect_url = "hodDashboard"
# Validate input
if not all([current_password, new_password, confirm_password]):
if is_ajax:
return JsonResponse(
{"success": False, "error": "All password fields are required."}
)
messages.error(request, "All password fields are required.")
return redirect(redirect_url)
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(redirect_url)
# 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(redirect_url)
# Set new password
try:
user.set_password(new_password)
user.save()
# For both AJAX and regular requests, we'll return success and indicate need for logout
if is_ajax:
return JsonResponse(
{
"success": True,
"redirect": "/login/",
"message": "Password changed successfully. Please login again.",
}
)
messages.success(
request, "Password changed successfully. Please login again."
)
return redirect("login")
except Exception as e:
if is_ajax:
return JsonResponse(
{"success": False, "error": f"Error changing password: {str(e)}"}
)
messages.error(request, f"Error changing password: {str(e)}")
return redirect(redirect_url)
return redirect("dashboard")
@login_required
@require_GET
def get_subjects(request):
"""Get subjects for a course and period"""
course_id = request.GET.get("course_id")
period = request.GET.get("period", 1)
if not course_id:
return JsonResponse({"success": False, "message": "Course ID is required"})
try:
subjects = Subject.objects.filter(
course_id=course_id, period_or_year=period
).values("id", "name")
return JsonResponse({"success": True, "subjects": list(subjects)})
except Exception as e:
return JsonResponse({"success": False, "message": str(e)})
@login_required
@require_GET
def get_teachers(request):
"""Get teachers for the institute"""
try:
teachers = Staff.objects.filter(
Q(designation__icontains="Teacher")
| Q(designation__icontains="Staff")
| Q(designation__icontains="Principal")
| Q(designation__icontains="HOD")
| Q(designation__icontains="Admission Officer")
| Q(designation__isnull=True)
| Q(designation="")
).values("id", "name")
return JsonResponse({"success": True, "teachers": list(teachers)})
except Exception as e:
return JsonResponse({"success": False, "message": str(e)})
@login_required
@require_GET
def get_course_duration(request):
"""Get course duration details"""
course_id = request.GET.get("course_id")
if not course_id:
return JsonResponse({"success": False, "message": "Course ID is required"})
try:
course = Course.objects.get(id=course_id)
# Calculate number of semesters or years
total_periods = course.duration
if course.duration_type == "Semester":
total_periods = course.duration * 2 # 2 semesters per year
# Create a list of periods
periods = []
for i in range(1, total_periods + 1):
if course.duration_type == "Semester":
periods.append({"value": i, "label": f"Semester {i}"})
else:
periods.append({"value": i, "label": f"Year {i}"})
return JsonResponse(
{
"success": True,
"course": {
"id": course.id,
"name": course.name,
"duration": course.duration,
"duration_type": course.duration_type,
"total_periods": total_periods,
"periods": periods,
},
}
)
except Course.DoesNotExist:
return JsonResponse({"success": False, "message": "Course not found"})
except Exception as e:
return JsonResponse({"success": False, "message": str(e)})
@login_required
def approve_student_leave(request, leave_id):
"""View to approve a student leave request"""
if request.method == "POST":
leave = get_object_or_404(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")
if not title or not message:
return JsonResponse(
{"success": False, "error": "Title and message are required"}
)
notice = Notice.objects.create(title=title, message=message, image=image)
return JsonResponse(
{"success": True, "message": "Notice added successfully"}
)
except Exception as e:
return JsonResponse({"success": False, "error": str(e)})
return JsonResponse({"success": False, "error": "Invalid request method"})
@login_required
def delete_notice(request, notice_id):
if request.method == "POST":
try:
notice = Notice.objects.get(id=notice_id)
notice.delete()
return JsonResponse({"success": True})
except Notice.DoesNotExist:
return JsonResponse({"success": False, "error": "Notice not found"})
return JsonResponse({"success": False, "error": "Invalid request method"})
@login_required
@require_POST
def save_fcm_token(request):
try:
print("Received FCM token save request")
data = json.loads(request.body)
token = data.get("token")
timestamp = data.get("timestamp")
vapid_key_used = data.get("vapidKeyUsed", False)
# Log additional information for debugging
auth_header = request.headers.get("Authorization", "")
using_vapid_auth = auth_header.startswith("VAPID ")
print(f"Auth method: {'VAPID' if using_vapid_auth else 'Cookie-based'}")
print(f"VAPID key used for token generation: {vapid_key_used}")
if not token:
print("No token provided in request")
return JsonResponse({"error": "Token is required"}, status=400)
user = request.user
print(f"User authenticated: {user.id} ({user.username})")
user_groups = user.groups.values_list("name", flat=True)
print(f"FCM token saving for user: {user.id}, groups: {list(user_groups)}")
# Determine user type based on group membership
user_type = "unknown"
if user.is_superuser:
user_type = "admin"
elif "Student" in user_groups:
user_type = "student"
elif "Teacher" in user_groups:
user_type = "teacher"
elif "Parent" in user_groups:
user_type = "parent"
# Save token directly to the user model
user.fcm_token = token
user.save(update_fields=["fcm_token"])
print(f"FCM token saved directly to user {user.name}")
# Update or create FCMDevice
try:
# Check if this is a fallback token
is_fallback = token.startswith("fcm-token-") or token.startswith(
"fallback-token-"
)
# Create or update the device
device, created = FCMDevice.objects.update_or_create(
token=token,
defaults={
"is_active": True,
"is_fallback": is_fallback,
"user_type": user_type,
},
)
print(
f"FCMDevice {'created' if created else 'updated'} with ID: {device.id}, user_type: {user_type}"
)
except Exception as e:
print(f"Error creating/updating FCMDevice: {str(e)}")
raise
return JsonResponse(
{
"status": "success",
"device_id": device.id,
"created": created,
"auth_method": "vapid" if using_vapid_auth else "cookie",
"user_type": user_type,
}
)
except json.JSONDecodeError:
print("Invalid JSON in request body")
return JsonResponse({"error": "Invalid JSON"}, status=400)
except Exception as e:
print(f"Error in save_fcm_token: {str(e)}")
return JsonResponse({"error": str(e)}, 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)})