""" 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. """ try: from apps.notifications.models import NotificationLog, NotificationChannel, NotificationStatus from apps.notifications.services import send_notification # Get recipient details recipient_email = appreciation.get_recipient_email() recipient_phone = appreciation.get_recipient_phone() # Get sender name sender_name = "Anonymous" if appreciation.is_anonymous else appreciation.sender.get_full_name() # Build message 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}" # Send email if available if recipient_email: try: send_notification( channel=NotificationChannel.EMAIL, recipient=recipient_email, subject=f"New Appreciation Received - {appreciation.hospital.name}", message=message_en, content_object=appreciation, ) except Exception as e: # Log error but don't fail print(f"Failed to send appreciation email: {e}") # Send SMS if available if recipient_phone: try: send_notification( channel=NotificationChannel.SMS, recipient=recipient_phone, message=message_en, content_object=appreciation, ) except Exception as e: # Log error but don't fail print(f"Failed to send appreciation SMS: {e}") except ImportError as e: # Notification service not available - skip notification print(f"Notification service not available: {e}") except Exception as e: # Any other error - log but don't fail print(f"Error sending appreciation notification: {e}") # Mark notification as sent (even if notification failed) appreciation.notification_sent = True appreciation.notification_sent_at = timezone.now() appreciation.save(update_fields=['notification_sent', 'notification_sent_at']) 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