HH/apps/appreciation/signals.py
ismail c5f76b3855
Some checks are pending
Build and Push Docker Image / build (push) Waiting to run
updates
2026-05-11 14:45:30 +03:00

481 lines
17 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.
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"""
<!DOCTYPE html>
<html>
<head><style>
body {{ font-family: 'Segoe UI', Tahoma, sans-serif; background: #f8fafc; margin: 0; padding: 20px; }}
.container {{ max-width: 600px; margin: 0 auto; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }}
.header {{ background: linear-gradient(135deg, #005696 0%, #007bbd 100%); padding: 30px; text-align: center; color: white; }}
.content {{ padding: 30px; }}
.appreciation-box {{ background: #eef6fb; border-left: 4px solid #005696; padding: 20px; margin: 20px 0; }}
.personal-note {{ background: #fffbeb; border-left: 4px solid #f59e0b; padding: 20px; margin: 20px 0; }}
</style></head>
<body>
<div class="container">
<div class="header">
<h1>&#127775; Staff Appreciation</h1>
</div>
<div class="content">
<div class="appreciation-box">
<strong>From:</strong> {sender_name}<br>
<strong>Hospital:</strong> {appreciation.hospital.name}<br>
{f'<strong>Category:</strong> {appreciation.category.name_en}<br>' if appreciation.category else ''}
<br>
<strong>Message:</strong><br>
{appreciation.message_en}
</div>
{f'<div class="personal-note"><strong>Personal Note from {sender_name}:</strong><br>{appreciation.custom_message}</div>' if appreciation.custom_message else ''}
<p>Congratulations on this recognition! Your dedication is truly appreciated.</p>
</div>
</div>
</body>
</html>
"""
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"""
<!DOCTYPE html>
<html>
<head><style>
body {{ font-family: 'Segoe UI', Tahoma, sans-serif; background: #f8fafc; margin: 0; padding: 20px; }}
.container {{ max-width: 600px; margin: 0 auto; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }}
.header {{ background: linear-gradient(135deg, #10b981 0%, #059669 100%); padding: 30px; text-align: center; color: white; }}
.content {{ padding: 30px; }}
.info-box {{ background: #ecfdf5; border-left: 4px solid #10b981; padding: 20px; margin: 20px 0; }}
</style></head>
<body>
<div class="container">
<div class="header">
<h1>&#127775; New Appreciation in Your Department</h1>
</div>
<div class="content">
<div class="info-box">
<strong>Staff Member:</strong> {recipient_name}<br>
<strong>Department:</strong> {dept.name_en or dept.name}<br>
<strong>From:</strong> {sender_name}<br>
{f'<strong>Category:</strong> {appreciation.category.name_en}<br>' if appreciation.category else ''}
<br>
<strong>Message:</strong><br>
{appreciation.message_en}
</div>
<p>A staff member in your department has received an appreciation. Please acknowledge their good work.</p>
</div>
</div>
</body>
</html>
"""
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