Files
Student-Management-System-I…/app/signals.py
T
2025-04-20 17:46:30 +05:45

423 lines
16 KiB
Python

# Standard library imports
import random
from datetime import date, timedelta
# Core Django imports
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import Group, Permission
from django.core.cache import cache
from django.core.signals import request_started
from django.db import transaction
from django.db.models import Q
from django.db.models.signals import m2m_changed, post_migrate, post_save
from django.dispatch import receiver
# Local app imports
from app.firebase import FCMDevice, send_push_notification
from app.models import (
Batch,
CourseTracking,
Notice,
Parent,
Staff,
Student,
TeacherParentMeeting,
)
# --------------------------------------------------------------------
# Course Tracking Signals
# --------------------------------------------------------------------
@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(request_started)
def periodic_update_course_completion(sender, **kwargs):
"""
Signal that updates course completion percentages randomly on request start.
This ensures periodic updates without overwhelming the system.
Only runs with 1% probability per request to avoid performance impact.
"""
# Run this signal with 1% probability to avoid running on every request
if random.random() <= 0.01: # 1% chance to run on any request
# Get all active course trackings (not completed or dropped)
active_trackings = CourseTracking.objects.filter(
Q(progress_status="In Progress") | Q(progress_status="Not Started")
)
updated_count = 0
for tracking in active_trackings:
old_percentage = tracking.completion_percentage
new_percentage = tracking.update_completion_percentage()
# If percentage changed, it was updated
if old_percentage != new_percentage:
updated_count += 1
# Check if course is now complete (100%)
if new_percentage >= 100 and tracking.progress_status != "Completed":
tracking.progress_status = "Completed"
tracking.actual_end_date = date.today()
tracking.save()
# Cache the last update timestamp
if updated_count > 0:
cache.set(
"last_course_percentage_update", date.today().isoformat(), 86400
) # 24 hours
# --------------------------------------------------------------------
# Student and Course Signals
# --------------------------------------------------------------------
@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
"""
# 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_period=instance.current_period,
period_start_date=start_date,
period_end_date=start_date
+ timedelta(days=180), # 6 months for first semester
)
# Force initial percentage calculation
course_tracking.force_update_percentage()
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_period(sender, instance, created, **kwargs):
"""Signal handler to sync student's current_period with course tracking"""
if instance.course:
try:
course_tracking = CourseTracking.objects.get(
student=instance, course=instance.course
)
if course_tracking.current_period != instance.current_period:
course_tracking.current_period = instance.current_period
course_tracking.save()
except CourseTracking.DoesNotExist:
pass
@receiver(post_save, sender=CourseTracking)
def sync_course_tracking_period(sender, instance, created, **kwargs):
"""Signal handler to sync course tracking's current_period with student"""
if instance.student.current_period != instance.current_period:
instance.student.current_period = instance.current_period
instance.student.save()
# --------------------------------------------------------------------
# Parent and Student Relationship Signals
# --------------------------------------------------------------------
@receiver(post_save, sender=Student)
def create_parent_for_student(sender, instance, created, **kwargs):
"""
Signal to create or update parent when a student is created or updated.
Also ensures parent is assigned to the Parent group.
"""
if instance.parent_name and instance.parent_phone:
try:
# Try to find existing parent
parent = Parent.objects.filter(phone=instance.parent_phone).first()
if not parent:
# Create new parent if doesn't exist
parent = Parent.objects.create(
name=instance.parent_name,
phone=instance.parent_phone,
password=make_password("123"), # Default password
)
# Get or create Parent group
parent_group, _ = Group.objects.get_or_create(name="Parent")
# Add parent to group
parent.groups.add(parent_group)
# Add necessary permissions
permissions = Permission.objects.filter(
codename__in=["view_student", "view_attendance", "view_notice"]
)
parent_group.permissions.add(*permissions)
# Add student to parent's students
parent.students.add(instance)
except Exception as e:
print(f"Error creating/updating parent: {str(e)}")
# Don't prevent student creation/update if parent creation fails
@receiver(post_save, sender=Parent)
def setup_parent_user(sender, instance, created, **kwargs):
"""
Signal handler to set up a new parent user:
1. Add to Parent group
2. Set up initial settings and permissions
"""
if created:
try:
# Add user to Parent group
parent_group, _ = Group.objects.get_or_create(name="Parent")
instance.groups.add(parent_group)
print(f"Successfully set up new parent user: {instance.name}")
except Exception as e:
print(f"Error setting up parent user {instance.name}: {str(e)}")
@receiver(m2m_changed, sender=Parent.students.through)
def handle_parent_student_relationship(
sender, instance, action, reverse, model, pk_set, **kwargs
):
"""
Signal handler to manage parent-student relationship changes:
- Update caches
- Handle notifications
- Sync permissions
"""
if action == "post_add":
try:
if reverse:
# This is triggered when students are added to a parent
parent = model.objects.get(pk=pk_set.pop())
student = instance
else:
# This is triggered when a parent adds students
parent = instance
student_ids = pk_set
# Clear parent dashboard cache using a more compatible approach
cache.delete(f"parent_dashboard_{parent.id}")
print(
f"Successfully updated parent-student relationship for parent: {parent.name}"
)
except Exception as e:
print(f"Error handling parent-student relationship: {str(e)}")
# --------------------------------------------------------------------
# User Group and Permission Signals
# --------------------------------------------------------------------
@receiver(post_migrate)
def create_user_groups(sender, **kwargs):
"""
Create initial user groups and permissions after migrations.
This ensures groups exist before any user is created.
"""
from django.contrib.auth.models import Group, Permission
# Create the necessary groups
groups_data = {
"Student": ["view_attendance", "view_notice", "view_subject"],
"Teacher": [
"view_student",
"view_attendance",
"add_attendance",
"change_attendance",
"view_notice",
"add_notice",
"view_subject",
"change_subject",
],
"Parent": ["view_student", "view_attendance", "view_notice"],
"Admission Officer": [
"view_student",
"add_student",
"change_student",
"view_parent",
"add_parent",
"change_parent",
"view_course",
"view_batch",
"view_notice",
],
}
for group_name, permissions in groups_data.items():
group, created = Group.objects.get_or_create(name=group_name)
if created:
print(f"Created user group: {group_name}")
# Add permissions to group
perms = Permission.objects.filter(codename__in=permissions)
group.permissions.add(*perms)
print(f"Added {perms.count()} permissions to {group_name} group")
# --------------------------------------------------------------------
# Batch and Student Enrollment Signals
# --------------------------------------------------------------------
@receiver(post_save, sender=Batch)
def update_student_joining_dates(sender, instance, **kwargs):
"""Update joining dates for all students in a batch when batch year changes"""
if instance.year:
# Update all students in this batch who don't have a joining date
for student in instance.students.filter(joining_date__isnull=True):
student.joining_date = instance.year
student.save(update_fields=["joining_date"])
@receiver(m2m_changed, sender=Student.batches.through)
def handle_student_batch_change(
sender, instance, action, reverse, model, pk_set, **kwargs
):
"""Handle joining date when a student is added to a batch"""
if action == "post_add" and not reverse:
# This is fired when batches are added to a student
for batch_id in pk_set:
try:
batch = Batch.objects.get(pk=batch_id)
if batch.year and not instance.joining_date:
instance.joining_date = batch.year
instance.save(update_fields=["joining_date"])
break # Use the first batch with a joining date
except Batch.DoesNotExist:
pass
# --------------------------------------------------------------------
# Notification Signals
# --------------------------------------------------------------------
@receiver(post_save, sender=Notice)
def send_notice_notification(sender, instance, created, **kwargs):
"""Send notifications when a notice is created or updated"""
# Get all tokens from active devices (all user types)
tokens = FCMDevice.objects.filter(is_active=True).values_list("token", flat=True)
send_push_notification(instance.title, instance.message, tokens)
@receiver(post_save, sender=TeacherParentMeeting)
def send_meeting_notification(sender, instance, created, **kwargs):
"""Send notifications when a meeting is created or updated"""
tokens = []
# Method 1: Get tokens from FCMDevice records based on user_type
fcm_device_tokens = FCMDevice.objects.filter(
is_active=True, user_type__in=["parent", "teacher"]
).values_list("token", flat=True)
tokens.extend(fcm_device_tokens)
# Method 2: Get tokens directly from user models
# Get tokens from Staff model
staff_tokens = (
Staff.objects.filter(fcm_token__isnull=False)
.exclude(fcm_token="")
.values_list("fcm_token", flat=True)
)
tokens.extend(staff_tokens)
# Get tokens from Parent model
parent_tokens = (
Parent.objects.filter(fcm_token__isnull=False)
.exclude(fcm_token="")
.values_list("fcm_token", flat=True)
)
tokens.extend(parent_tokens)
# Remove duplicates
unique_tokens = list(set(tokens))
if not unique_tokens:
return
if created:
title = "New Parent-Teacher Meeting Scheduled"
message = f"A parent-teacher meeting has been scheduled for {instance.meeting_date} at {instance.meeting_time}."
if instance.is_online:
message += " This is an online meeting."
send_push_notification(title, message, unique_tokens)
else:
# Access through instance.tracker to determine if status changed
# This requires django-model-utils or similar to track changes
if hasattr(instance, "tracker") and "status" in instance.tracker.changed():
old_status = instance.tracker.previous("status")
if instance.status == "cancelled":
title = "Parent-Teacher Meeting Cancelled"
message = f"The meeting for {instance.meeting_date} at {instance.meeting_time} has been cancelled."
if instance.cancellation_reason:
message += f" Reason: {instance.cancellation_reason}"
send_push_notification(title, message, unique_tokens)
elif instance.status == "rescheduled":
# Meeting rescheduled notification
title = "Parent-Teacher Meeting Rescheduled"
message = f"The parent-teacher meeting has been rescheduled to {instance.meeting_date} at {instance.meeting_time}."
send_push_notification(title, message, unique_tokens)
elif instance.status == "completed":
# Meeting completed notification
title = "Parent-Teacher Meeting Completed"
message = f"The parent-teacher meeting scheduled for {instance.meeting_date} at {instance.meeting_time} has been marked as completed."
send_push_notification(title, message, unique_tokens)