HH/apps/appreciation/signals.py
2026-01-18 14:04:23 +03:00

346 lines
11 KiB
Python

"""
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.services import send_email, send_sms
# 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_email(
email=recipient_email,
subject=f"New Appreciation Received - {appreciation.hospital.name}",
message=message_en,
related_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_sms(
phone=recipient_phone,
message=message_en,
related_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