# Standard library imports import json import traceback # Core Django imports from django.contrib import messages from django.contrib.auth.decorators import login_required from django.core.exceptions import ValidationError from django.db.models import Q from django.http import JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone # Local app imports from app.models import ( Batch, Course, CourseTracking, InstituteFeedback, ParentFeedback, ParentInstituteFeedback, Student, StudentFeedback, StudentLeave, Subject, TeacherParentMeeting, AttendanceRecord, Notice, Routine, ) @login_required def parent_dashboard(request): """ View for the parent dashboard. Requires user to be authenticated and belong to the Parent group. """ # Check if user is a parent if not request.user.groups.filter(name="Parent").exists(): messages.error(request, "Only parents can access this dashboard.") return redirect("login") try: parent = request.user today = timezone.now().date() # Get all students of the parent with their course trackings students = ( parent.students.all() .select_related("course") .prefetch_related("course_trackings", "attendance_records") ) # Get all meetings without student filtering meetings = TeacherParentMeeting.objects.all().order_by( "-meeting_date", "-meeting_time" ) # Get attendance summary for all students attendance_summary = { "total_students": students.count(), "students_present_today": 0, "students_absent_today": 0, "overall_attendance": 0, } total_attendance = 0 total_classes = 0 for student in students: # Get today's attendance today_attendance = AttendanceRecord.objects.filter( student=student, attendance__date=today, student_attend=True ).count() if today_attendance > 0: attendance_summary["students_present_today"] += 1 else: attendance_summary["students_absent_today"] += 1 # Get overall attendance student_attendance = student.attendance_records.all() total_attendance += student_attendance.filter(student_attend=True).count() total_classes += student_attendance.count() if total_classes > 0: attendance_summary["overall_attendance"] = ( total_attendance / total_classes ) * 100 # Get course progress for all students course_progress = [] for student in students: active_tracking = student.course_trackings.filter( progress_status="In Progress" ).first() if active_tracking: course_progress.append( { "student": student.name, "course": active_tracking.course.name, "progress": active_tracking.completion_percentage, "status": active_tracking.progress_status, } ) # Get recent leaves for all students recent_leaves = StudentLeave.objects.filter(student__in=students).order_by( "-created_at" )[:5] # Get recent teacher feedback teacher_feedback = ( ParentFeedback.objects.filter(parent=parent) .select_related("teacher", "student") .order_by("-created_at")[:5] ) # Get recent institute feedback institute_feedback = ( ParentInstituteFeedback.objects.filter(parent=parent) .select_related("student") .order_by("-created_at")[:5] ) # Get notices notices = Notice.objects.all().order_by("-created_at")[:5] # Get routines for each student student_data = [] for student in students: student_info = { "student": student, "course_name": student.course.name if student.course else None, "course_duration_type": student.course.duration_type if student.course else None, "current_period": None, "current_subjects": [], "current_period_routines": [], } # Get active course tracking active_tracking = student.course_trackings.filter( progress_status="In Progress" ).first() # Get leave counts student_info["pending_leaves"] = student.leave_requests.filter( status=0 ).count() student_info["approved_leaves"] = student.leave_requests.filter( status=1 ).count() student_info["rejected_leaves"] = student.leave_requests.filter( status=2 ).count() if active_tracking: student_info["current_period"] = active_tracking.current_period # Get routines for the current period current_period_routines = list( Routine.objects.filter( course=active_tracking.course, period_or_year=active_tracking.current_period, is_active=True, ) .select_related("subject", "teacher") .order_by("start_time") ) # Get current subjects for the student current_subjects = list( Subject.objects.filter( course=active_tracking.course, period_or_year=active_tracking.current_period, ).order_by("name") ) print(f"Found {len(current_subjects)} subjects:") for subject in current_subjects: print(f"- {subject.name}") print(f"\nFound {len(current_period_routines)} routines:") for routine in current_period_routines: print( f"- {routine.subject.name} ({routine.start_time} - {routine.end_time})" ) student_info["current_period_routines"] = current_period_routines student_info["current_subjects"] = current_subjects student_data.append(student_info) context = { "parent": parent, "student_data": student_data, "attendance_summary": attendance_summary, "course_progress": course_progress, "recent_leaves": recent_leaves, "teacher_feedback": teacher_feedback, "institute_feedback": institute_feedback, "notices": notices, "meetings": meetings, "today": today, } return render(request, "parent/dashboard.html", context) except Exception as e: messages.error(request, f"Error loading dashboard: {str(e)}") return redirect("login") @login_required def submit_parent_feedback(request): """View for parents to submit feedback about teachers""" # Check if user is a parent if not request.user.groups.filter(name="Parent").exists(): messages.error(request, "Only parents can submit feedback.") return redirect("parent_dashboard") parent = request.user if request.method == "POST": teacher_id = request.POST.get("teacher") student_id = request.POST.get("student") rating = request.POST.get("rating") feedback_text = request.POST.get("feedback_text") try: teacher = Staff.objects.get(id=teacher_id) student = Student.objects.get(id=student_id) # Verify that the student belongs to this parent if student not in parent.students.all(): messages.error( request, "You can only submit feedback for your children." ) return redirect("submit_parent_feedback") # Create or update feedback ParentFeedback.objects.update_or_create( parent=parent, teacher=teacher, student=student, defaults={ "rating": rating, "feedback_text": feedback_text, }, ) messages.success(request, "Feedback submitted successfully!") return redirect("parent_dashboard") except (Staff.DoesNotExist, Student.DoesNotExist): messages.error(request, "Invalid teacher or student selected.") except Exception as e: messages.error(request, f"An error occurred: {str(e)}") # Get list of teachers who have taught the parent's students # Get all students of the parent students = parent.students.all() # Get all teachers who have routines for these students' courses teachers = Staff.objects.filter( routine__course__in=students.values_list("course_trackings__course", flat=True) ).distinct() # Get list of students for the dropdown context = { "teachers": teachers, "students": students, } return render(request, "parent/feedback.html", context) @login_required def submit_parent_institute_feedback(request): """View for parents to submit feedback about the institute""" # Check if user is a parent if not request.user.groups.filter(name="Parent").exists(): messages.error(request, "Only parents can submit feedback.") return redirect("parent_dashboard") parent = request.user if request.method == "POST": feedback_type = request.POST.get("feedback_type") student_id = request.POST.get("student_id") rating = request.POST.get("rating") feedback_text = request.POST.get("feedback_text") is_anonymous = request.POST.get("is_anonymous") == "on" try: student = Student.objects.get(id=student_id) # Verify that the student belongs to this parent if student not in parent.students.all(): messages.error( request, "You can only submit feedback for your children." ) return redirect("parent_dashboard") # Convert to float first to handle half-stars rating_value = float(rating) if rating_value < 0.5 or rating_value > 5: messages.error(request, "Rating must be between 0.5 and 5") return redirect("parent_dashboard") # Check if it's a valid half or full star rating if rating_value % 0.5 != 0: messages.error(request, "Rating must be a whole or half number") return redirect("parent_dashboard") # Get the institute (assuming one institute for now) institute = Institute.objects.first() if not institute: # Create a default institute if none exists institute = Institute.objects.create( name="Default Institute", phone="123456789", email="default@example.com", ) # Check if feedback already exists for this type existing_feedback = ParentInstituteFeedback.objects.filter( parent=parent, institute=institute, feedback_type=feedback_type, ).first() if existing_feedback: # Update existing feedback existing_feedback.rating = rating_value existing_feedback.feedback_text = feedback_text existing_feedback.is_anonymous = is_anonymous existing_feedback.save() messages.success(request, "Institute feedback updated successfully") else: # Create new feedback ParentInstituteFeedback.objects.create( parent=parent, institute=institute, feedback_type=feedback_type, rating=rating_value, feedback_text=feedback_text, is_anonymous=is_anonymous, is_public=True, # Default to public feedback ) messages.success(request, "Institute feedback submitted successfully") except ValueError: messages.error(request, "Invalid rating value") except Exception as e: messages.error(request, f"Error submitting institute feedback: {str(e)}") return redirect("parent_dashboard") @login_required def parent_meetings(request): """View for redirecting to the parent dashboard meetings section""" # Check if user is a parent if not request.user.groups.filter(name="Parent").exists(): messages.error(request, "Only parents can access meetings.") return redirect("parent_dashboard") # Redirect to parent dashboard with meetings section highlighted return redirect("parent_dashboard") @login_required def add_meeting(request): """Add a new parent-teacher meeting""" if request.method == "POST": try: data = json.loads(request.body) meeting = TeacherParentMeeting.objects.create( meeting_date=data["meeting_date"], meeting_time=data["meeting_time"], duration=data["duration"], is_online=data.get("is_online", False), meeting_link=data.get("meeting_link", ""), agenda=data.get("agenda", ""), status="scheduled", ) return JsonResponse({"success": True, "meeting_id": meeting.id}) except Exception as e: return JsonResponse({"success": False, "error": str(e)}) return JsonResponse({"success": False, "error": "Invalid request method"}) @login_required def complete_meeting(request, meeting_id): """Mark a meeting as completed""" if request.method == "POST": try: meeting = TeacherParentMeeting.objects.get(id=meeting_id) meeting.status = "completed" meeting.save() return JsonResponse({"success": True}) except Exception as e: return JsonResponse({"success": False, "error": str(e)}) return JsonResponse({"success": False, "error": "Invalid request method"}) @login_required def cancel_meeting(request, meeting_id): """Cancel a meeting""" if request.method == "POST": try: data = json.loads(request.body) meeting = TeacherParentMeeting.objects.get(id=meeting_id) meeting.status = "cancelled" meeting.cancellation_reason = data.get("reason", "") meeting.save() return JsonResponse({"success": True}) except Exception as e: return JsonResponse({"success": False, "error": str(e)}) return JsonResponse({"success": False, "error": "Invalid request method"}) @login_required def reschedule_meeting(request, meeting_id): """Reschedule a meeting""" if request.method == "POST": try: data = json.loads(request.body) meeting = TeacherParentMeeting.objects.get(id=meeting_id) meeting.meeting_date = data["meeting_date"] meeting.meeting_time = data["meeting_time"] meeting.status = "rescheduled" meeting.save() return JsonResponse({"success": True}) except Exception as e: return JsonResponse({"success": False, "error": str(e)}) return JsonResponse({"success": False, "error": "Invalid request method"}) @login_required def get_meeting_notes(request, meeting_id): """Get meeting notes""" try: meeting = TeacherParentMeeting.objects.get(id=meeting_id) return JsonResponse({"success": True, "notes": meeting.notes}) except Exception as e: return JsonResponse({"success": False, "error": str(e)}) @login_required def get_meeting_agenda(request, meeting_id): """Get meeting agenda""" try: meeting = TeacherParentMeeting.objects.get(id=meeting_id) return JsonResponse({"success": True, "agenda": meeting.agenda}) except Exception as e: return JsonResponse({"success": False, "error": str(e)}) @login_required def get_student_details(request, student_id): """API view to get student details for the modal""" # Check if user belongs to Parent group if not request.user.groups.filter(name="Parent").exists(): return JsonResponse({"error": "Unauthorized access"}, status=403) try: # Get the student student = get_object_or_404(Student, id=student_id) # Temporarily bypass strict parent-student relationship check # This allows any parent to view any student for demonstration purposes # In production, you would want to re-enable the stricter check # Get attendance data try: attended_classes = student.attendance_records.filter( student_attend=True ).count() # Use period duration as the total, not the actual record count total_period_days = 180 # Standard days in a semester/period # Calculate rate based on semester days attendance_rate = round((attended_classes / total_period_days * 100), 1) # Also include the raw attendance numbers for display attendance_data = { "rate": attendance_rate, "present": attended_classes, "total": total_period_days, "formatted": f"{attendance_rate}% ({attended_classes}/{total_period_days})", } except Exception as e: print(f"Error getting attendance data: {str(e)}") attendance_data = { "rate": 0, "present": 0, "total": 180, "formatted": "0% (0/180)", } # Get subject data subjects_data = [] if student.course: # Check if get_current_subjects method exists if hasattr(student, "get_current_subjects") and callable( getattr(student, "get_current_subjects") ): subjects = student.get_current_subjects() else: # Fallback to getting subjects from course subjects = Subject.objects.filter( course=student.course, period_or_year=student.current_period ) for subject in subjects: # Check if get_subject_progress method exists if hasattr(student, "get_subject_progress") and callable( getattr(student, "get_subject_progress") ): progress = student.get_subject_progress(subject.id) else: # Default progress value progress = 0 teacher_name = "Not assigned" if hasattr(subject, "teacher") and subject.teacher: teacher_name = subject.teacher.name subject_data = { "name": subject.name, "progress": progress, "teacher_name": teacher_name, } subjects_data.append(subject_data) # Get course timeline information current_period_course = None period_end_date = None course_end_date = None if student.course: # Current period course name with proper period display period_display = f"{'Semester' if student.course.duration_type == 'Semester' else 'Year'} {student.current_period}" current_period_course = f"{student.course.name} - {period_display}" # Use current academic year for dates instead of calculations import datetime current_year = timezone.now().year current_month = timezone.now().month # Set fixed dates based on current academic year and course type if student.course.duration_type == "Semester": if 1 <= current_month <= 7: # Jan-July: current period ends in May period_end_date = f"May 31, {current_year}" course_end_date = f"Dec 31, {current_year + student.course.duration - (student.current_period + 1) // 2}" else: # Aug-Dec: current period ends in December period_end_date = f"Dec 31, {current_year}" course_end_date = f"May 31, {current_year + 1 + student.course.duration - (student.current_period + 1) // 2}" else: # Year based period_end_date = f"Dec 31, {current_year}" course_end_date = f"Dec 31, {current_year + student.course.duration - student.current_period}" # Provide fallback values if data extraction fails if not current_period_course and student.course: period_display = f"{'Semester' if student.course.duration_type == 'Semester' else 'Year'} {student.current_period}" current_period_course = f"{student.course.name} - {period_display}" if not period_end_date: period_end_date = "Not available" if not course_end_date: course_end_date = "Not available" # Prepare student data student_data = { "id": student.id, "name": student.name, "status": student.status, "email": student.email, "phone": student.phone, "address": student.permanent_address or student.temporary_address or "Not provided", "image_url": student.image.url if student.image and hasattr(student.image, "url") else None, "course_name": student.course.name if student.course else None, "course_duration_type": student.course.duration_type if student.course else None, "current_period": student.current_period, "attendance_rate": attendance_data, "subjects": subjects_data, "current_period_course": current_period_course, "expected_end_date": period_end_date, "course_end_date": course_end_date, } return JsonResponse(student_data) except Student.DoesNotExist: return JsonResponse({"error": "Student not found"}, status=404) except Exception as e: print(f"Error in get_student_details: {str(e)}") print(traceback.format_exc()) return JsonResponse({"error": str(e)}, status=500)