""" Appreciation signals - Signal handlers for appreciation events This module handles: - Sending notifications when appreciations are sent - Updating statistics when appreciations are created - Checking and awarding badges """ from django.db.models import Q from django.db.models.signals import post_save from django.dispatch import receiver from django.utils import timezone from apps.appreciation.models import ( Appreciation, AppreciationBadge, AppreciationStats, AppreciationStatus, UserBadge, ) @receiver(post_save, sender=Appreciation) def handle_appreciation_sent(sender, instance, created, **kwargs): """ Handle appreciation sent events. This signal triggers: 1. Send notification to recipient 2. Update statistics 3. Check for badge awards """ # Only process when appreciation is sent (status changes to 'sent') if instance.status == AppreciationStatus.SENT and not instance.notification_sent: # Send notification send_appreciation_notification(instance) # Update statistics update_appreciation_stats(instance) # Check for badge awards check_and_award_badges(instance) def send_appreciation_notification(appreciation): """ Send notification to recipient when appreciation is sent. Uses the notification system to send email/SMS/WhatsApp. Also sends email to department heads. """ try: from apps.notifications.services import send_email, send_sms recipient_email = appreciation.get_recipient_email() recipient_phone = appreciation.get_recipient_phone() sender_name = "Anonymous" if appreciation.is_anonymous else ( appreciation.sender.get_full_name() if appreciation.sender else (appreciation.metadata or {}).get("submitted_by_name", "Anonymous") ) html_message = f"""

🌟 Staff Appreciation

From: {sender_name}
Hospital: {appreciation.hospital.name}
{f'Category: {appreciation.category.name_en}
' if appreciation.category else ''}
Message:
{appreciation.message_en}
{f'
Personal Note from {sender_name}:
{appreciation.custom_message}
' if appreciation.custom_message else ''}

Congratulations on this recognition! Your dedication is truly appreciated.

""" message_en = f"You've received an appreciation from {sender_name}!" message_ar = f"لقد تلقيت تكريماً من {sender_name}!" if appreciation.category: message_en += f"\n\nCategory: {appreciation.category.name_en}" message_ar += f"\n\nالفئة: {appreciation.category.name_ar}" message_en += f"\n\nMessage: {appreciation.message_en}" message_ar += f"\n\nالرسالة: {appreciation.message_ar}" if appreciation.custom_message: message_en += f"\n\nPersonal note: {appreciation.custom_message}" message_ar += f"\n\nملاحظة شخصية: {appreciation.custom_message}" if recipient_email: try: send_email( email=recipient_email, subject=f"New Appreciation Received - {appreciation.hospital.name}", message=message_en, html_message=html_message, related_object=appreciation, ) except Exception as e: print(f"Failed to send appreciation email: {e}") if recipient_phone: try: send_sms( phone=recipient_phone, message=message_en, related_object=appreciation, ) except Exception as e: print(f"Failed to send appreciation SMS: {e}") _send_department_head_notification(appreciation, sender_name) _send_cc_notifications(appreciation, sender_name, html_message, message_en) except ImportError as e: print(f"Notification service not available: {e}") except Exception as e: print(f"Error sending appreciation notification: {e}") appreciation.notification_sent = True appreciation.notification_sent_at = timezone.now() appreciation.save(update_fields=['notification_sent', 'notification_sent_at']) def _send_department_head_notification(appreciation, sender_name): from apps.notifications.services import send_email as notify_send_email from apps.organizations.models import Staff if not appreciation.department: return dept = appreciation.department recipient_emails = set() if dept.manager and dept.manager.email: recipient_emails.add(dept.manager.email) heads = Staff.objects.filter( department=dept, is_head=True ).select_related("user") for head in heads: if head.user and head.user.email: recipient_emails.add(head.user.email) if not recipient_emails: return recipient_name = appreciation.get_recipient_name() if appreciation.recipient else "N/A" html = f"""

🌟 New Appreciation in Your Department

Staff Member: {recipient_name}
Department: {dept.name_en or dept.name}
From: {sender_name}
{f'Category: {appreciation.category.name_en}
' if appreciation.category else ''}
Message:
{appreciation.message_en}

A staff member in your department has received an appreciation. Please acknowledge their good work.

""" plain_message = ( f"New appreciation for {recipient_name} in {dept.name_en or dept.name}.\n" f"From: {sender_name}\n\n" f"Message: {appreciation.message_en}" ) for email in recipient_emails: try: notify_send_email( email=email, subject=f"New Appreciation for {recipient_name} - {dept.name_en or dept.name}", message=plain_message, html_message=html, related_object=appreciation, ) except Exception as e: print(f"Failed to send department head notification: {e}") def _send_cc_notifications(appreciation, sender_name, html_message, plain_message): from apps.notifications.services import send_email as notify_send_email cc_list = appreciation.cc_list or [] if not cc_list: return recipient_name = appreciation.get_recipient_name() if appreciation.recipient else "N/A" for cc_email in cc_list: if not cc_email: continue try: notify_send_email( email=cc_email.strip(), subject=f"CC: Appreciation sent to {recipient_name} - {appreciation.hospital.name}", message=plain_message, html_message=html_message, related_object=appreciation, ) except Exception as e: print(f"Failed to send CC notification to {cc_email}: {e}") def update_appreciation_stats(instance): """ Update appreciation statistics for the recipient. Creates or updates monthly statistics. """ # Get current year and month now = timezone.now() year = now.year month = now.month # Get or create stats record stats, created = AppreciationStats.objects.get_or_create( recipient_content_type=instance.recipient_content_type, recipient_object_id=instance.recipient_object_id, year=year, month=month, defaults={ 'hospital': instance.hospital, 'department': instance.department, 'received_count': 0, 'sent_count': 0, 'acknowledged_count': 0, 'category_breakdown': {}, } ) # Update received count stats.received_count += 1 # Update category breakdown if instance.category: category_breakdown = stats.category_breakdown or {} category_id_str = str(instance.category.id) category_breakdown[category_id_str] = category_breakdown.get(category_id_str, 0) + 1 stats.category_breakdown = category_breakdown # Save stats stats.save() # Recalculate rankings recalculate_rankings(instance.hospital, year, month, instance.department) def recalculate_rankings(hospital, year, month, department=None): """ Recalculate rankings for a given period. Updates hospital_rank and department_rank for all recipients. """ # Get all stats for the period if department: stats_queryset = AppreciationStats.objects.filter( hospital=hospital, department=department, year=year, month=month, ) else: stats_queryset = AppreciationStats.objects.filter( hospital=hospital, year=year, month=month, ) # Order by received count stats_queryset = stats_queryset.order_by('-received_count') # Update hospital rank for rank, stat in enumerate(stats_queryset, start=1): stat.hospital_rank = rank stat.save(update_fields=['hospital_rank']) # Update department rank if department is specified if department: dept_stats = stats_queryset.filter(department=department) for rank, stat in enumerate(dept_stats, start=1): stat.department_rank = rank stat.save(update_fields=['department_rank']) def check_and_award_badges(instance): """ Check if recipient qualifies for any badges and award them. Checks all active badge criteria and awards badges if criteria are met. """ from django.contrib.contenttypes.models import ContentType # Get recipient recipient_content_type = instance.recipient_content_type recipient_object_id = instance.recipient_object_id # Get all active badges for the hospital badges = AppreciationBadge.objects.filter( Q(hospital=instance.hospital) | Q(hospital__isnull=True), is_active=True, ) for badge in badges: # Check if badge already earned if UserBadge.objects.filter( recipient_content_type=recipient_content_type, recipient_object_id=recipient_object_id, badge=badge, ).exists(): continue # Already earned # Check badge criteria qualifies = check_badge_criteria( badge, recipient_content_type, recipient_object_id, instance.hospital, ) if qualifies: # Calculate current count count = get_appreciation_count( recipient_content_type, recipient_object_id, badge.criteria_type ) # Award badge UserBadge.objects.create( recipient_content_type=recipient_content_type, recipient_object_id=recipient_object_id, badge=badge, appreciation_count=count, ) def check_badge_criteria(badge, content_type, object_id, hospital): """ Check if recipient meets badge criteria. Returns True if criteria is met, False otherwise. """ criteria_type = badge.criteria_type criteria_value = badge.criteria_value if criteria_type == 'received_count': # Check total appreciation count count = Appreciation.objects.filter( recipient_content_type=content_type, recipient_object_id=object_id, status=AppreciationStatus.SENT, ).count() return count >= criteria_value elif criteria_type == 'received_month': # Check appreciation count in current month now = timezone.now() count = Appreciation.objects.filter( recipient_content_type=content_type, recipient_object_id=object_id, status=AppreciationStatus.SENT, sent_at__year=now.year, sent_at__month=now.month, ).count() return count >= criteria_value elif criteria_type == 'streak_weeks': # Check consecutive weeks with appreciation return check_appreciation_streak( content_type, object_id, criteria_value ) elif criteria_type == 'diverse_senders': # Check appreciation from different senders sender_count = Appreciation.objects.filter( recipient_content_type=content_type, recipient_object_id=object_id, status=AppreciationStatus.SENT, ).values('sender').distinct().count() return sender_count >= criteria_value return False def get_appreciation_count(content_type, object_id, criteria_type): """ Get appreciation count based on criteria type. """ if criteria_type == 'received_count': return Appreciation.objects.filter( recipient_content_type=content_type, recipient_object_id=object_id, status=AppreciationStatus.SENT, ).count() elif criteria_type == 'received_month': now = timezone.now() return Appreciation.objects.filter( recipient_content_type=content_type, recipient_object_id=object_id, status=AppreciationStatus.SENT, sent_at__year=now.year, sent_at__month=now.month, ).count() return 0 def check_appreciation_streak(content_type, object_id, required_weeks): """ Check if recipient has appreciation streak for required weeks. Returns True if streak meets or exceeds required_weeks. """ from datetime import timedelta now = timezone.now() current_week = 0 # Check week by week going backwards for i in range(required_weeks): week_start = now - timedelta(weeks=i+1) week_end = now - timedelta(weeks=i) # Check if there's any appreciation in this week has_appreciation = Appreciation.objects.filter( recipient_content_type=content_type, recipient_object_id=object_id, status=AppreciationStatus.SENT, sent_at__gte=week_start, sent_at__lt=week_end, ).exists() if has_appreciation: current_week += 1 else: break return current_week >= required_weeks