346 lines
11 KiB
Python
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
|