from django.utils import timezone from django.contrib.auth import get_user_model import logging from celery import shared_task from datetime import timedelta from apps.events.models import Event, Registration from apps.communications.models import Announcement, NewsletterSubscription from apps.communications.utils import send_announcement_email, send_event_reminder, get_announcement_recipients from apps.communications.push_notifications import push_service User = get_user_model() logger = logging.getLogger(__name__) SYSTEM_USER_ID = 1 @shared_task(bind=True, max_retries=3) def send_announcement_notifications(self, announcement_id): """Send email and push notifications for an announcement""" try: announcement = Announcement.objects.get(id=announcement_id) # Send email notifications if announcement.send_email and not announcement.email_sent: recipients = get_announcement_recipients(announcement) if recipients: success = send_announcement_email(announcement, recipients) if success: announcement.email_sent = True announcement.save() logger.info(f"Email notifications sent for announcement {announcement.id}") # Send push notifications if announcement.send_push and not announcement.push_sent: sent_count = push_service.send_announcement_notification(announcement) if sent_count > 0: announcement.push_sent = True announcement.save() logger.info(f"Push notifications sent to {sent_count} devices for announcement {announcement.id}") return f"Notifications sent for announcement: {announcement.title}" except Announcement.DoesNotExist: logger.error(f"Announcement {announcement_id} not found") return f"Announcement {announcement_id} not found" except Exception as exc: logger.error(f"Failed to send announcement notifications: {exc}") raise self.retry(exc=exc, countdown=60) @shared_task(bind=True, max_retries=3) def send_newsletter_confirmation_task(self, subscription_id): """Send newsletter confirmation email""" try: from .utils import send_newsletter_confirmation subscription = NewsletterSubscription.objects.get(id=subscription_id) success = send_newsletter_confirmation(subscription) if success: logger.info(f"Newsletter confirmation sent to {subscription.email}") return f"Newsletter confirmation sent to {subscription.email}" else: raise Exception("Failed to send newsletter confirmation") except NewsletterSubscription.DoesNotExist: logger.error(f"Newsletter subscription {subscription_id} not found") return f"Newsletter subscription {subscription_id} not found" except Exception as exc: logger.error(f"Failed to send newsletter confirmation: {exc}") raise self.retry(exc=exc, countdown=60) @shared_task def send_event_reminders(): """Send reminders for events starting about 24 hours from now within a 30-minute window.""" try: reminder_target = timezone.now() + timedelta(hours=24) window = timedelta(minutes=30) start_range = reminder_target - window end_range = reminder_target + window events = Event.objects.filter( start_time__range=(start_range, end_range), status='published', is_deleted=False ) total_sent = 0 for event in events: # Get confirmed registrations registrations = Registration.objects.filter( event=event, status='confirmed', is_deleted=False ).select_related('user') for registration in registrations: try: # Send email reminder send_event_reminder(event, registration.user) # Send push notification reminder push_service.send_event_reminder_notification(event, registration.user) total_sent += 1 except Exception as e: logger.error(f"Failed to send reminder to {registration.user.email}: {str(e)}") logger.info(f"Event reminders sent to {total_sent} users") return f"Event reminders sent to {total_sent} users" except Exception as exc: logger.error(f"Failed to send event reminders: {exc}") raise exc @shared_task def send_weekly_newsletter(): """Send the weekly newsletter as the system user with recent announcements and upcoming events.""" try: # Get active newsletter subscribers subscribers = NewsletterSubscription.objects.filter( is_active=True, confirmed_at__isnull=False, is_deleted=False ) if not subscribers.exists(): logger.info("No active newsletter subscribers found") return "No active newsletter subscribers found" # Get recent announcements (last 7 days) week_ago = timezone.now() - timedelta(days=7) recent_announcements = Announcement.objects.filter( is_published=True, publish_date__gte=week_ago, announcement_type__in=['general', 'academic', 'newsletter'], is_deleted=False ).order_by('-publish_date')[:5] # Get upcoming events (next 14 days) two_weeks_ahead = timezone.now() + timedelta(days=14) upcoming_events = Event.objects.filter( start_time__range=(timezone.now(), two_weeks_ahead), status='published', is_deleted=False ).order_by('start_time')[:5] newsletter_content = f""" # Weekly Newsletter - {timezone.now().strftime('%B %d, %Y')} ## Recent Announcements """ for announcement in recent_announcements: newsletter_content += f"- **{announcement.title}** ({announcement.publish_date.strftime('%B %d')})\n" newsletter_content += "\n## Upcoming Events\n" for event in upcoming_events: newsletter_content += f"- **{event.title}** - {event.start_time.strftime('%B %d, %Y at %I:%M %p')}\n" if not recent_announcements.exists() and not upcoming_events.exists(): newsletter_content += "\nNo recent announcements or upcoming events this week." newsletter = Announcement.objects.create( title=f"Weekly Newsletter - {timezone.now().strftime('%B %d, %Y')}", content=newsletter_content, announcement_type='newsletter', priority='normal', author_id=SYSTEM_USER_ID, is_published=True, publish_date=timezone.now(), send_email=True, target_audience='subscribers' ) # Send to subscribers subscriber_emails = list(subscribers.values_list('email', flat=True)) success = send_announcement_email(newsletter, subscriber_emails) if success: newsletter.email_sent = True newsletter.save() logger.info(f"Weekly newsletter sent to {len(subscriber_emails)} subscribers") return f"Weekly newsletter sent to {len(subscriber_emails)} subscribers" else: raise Exception("Failed to send weekly newsletter") except Exception as exc: logger.error(f"Failed to send weekly newsletter: {exc}") raise exc @shared_task def cleanup_expired_tokens(): """Clean up expired newsletter confirmation tokens""" try: # Remove unconfirmed subscriptions older than 7 days week_ago = timezone.now() - timedelta(days=7) expired_subscriptions = NewsletterSubscription.objects.filter( confirmed_at__isnull=True, created_at__lt=week_ago ) count = expired_subscriptions.count() expired_subscriptions.delete() logger.info(f"Cleaned up {count} expired newsletter subscriptions") return f"Cleaned up {count} expired newsletter subscriptions" except Exception as exc: logger.error(f"Failed to cleanup expired tokens: {exc}") raise exc @shared_task def send_bulk_announcement(announcement_id, recipient_emails): """Send announcement to a specific list of recipients""" try: announcement = Announcement.objects.get(id=announcement_id) # Split recipients into batches to avoid overwhelming the email server batch_size = 50 total_sent = 0 for i in range(0, len(recipient_emails), batch_size): batch = recipient_emails[i:i + batch_size] success = send_announcement_email(announcement, batch) if success: total_sent += len(batch) logger.info(f"Sent announcement to batch of {len(batch)} recipients") # Small delay between batches import time time.sleep(1) logger.info(f"Bulk announcement sent to {total_sent} recipients") return f"Bulk announcement sent to {total_sent} recipients" except Exception as exc: logger.error(f"Failed to send bulk announcement: {exc}") raise exc @shared_task def process_scheduled_announcements(): """Process announcements scheduled for publication""" try: now = timezone.now() # Get announcements scheduled for publication scheduled_announcements = Announcement.objects.filter( is_published=True, publish_date__lte=now, email_sent=False, send_email=True, is_deleted=False ) processed_count = 0 for announcement in scheduled_announcements: # Send notifications send_announcement_notifications.delay(announcement.id) processed_count += 1 logger.info(f"Processed {processed_count} scheduled announcements") return f"Processed {processed_count} scheduled announcements" except Exception as exc: logger.error(f"Failed to process scheduled announcements: {exc}") raise exc