init
Some checks failed
CI/CD / Backend & Frontend Checks (push) Has been cancelled
CI/CD / Deploy to Production (push) Has been cancelled

This commit is contained in:
2026-05-18 11:34:07 +03:30
commit 7a8ddeabed
279 changed files with 37390 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
from django import forms
from django.contrib import admin
from django.utils import timezone
from simplemde.widgets import SimpleMDEEditor
from import_export.admin import ImportExportModelAdmin
from utils.admin import SoftDeleteListFilter, BaseModelAdmin
from communications.models import Announcement, NewsletterSubscription, PushNotificationDevice
class AnnouncementAdminForm(forms.ModelForm):
content = forms.CharField(
widget=SimpleMDEEditor(),
help_text="Announcement content in Markdown format with live preview"
)
class Meta:
model = Announcement
fields = '__all__'
@admin.register(Announcement)
class AnnouncementAdmin(BaseModelAdmin, ImportExportModelAdmin):
form = AnnouncementAdminForm
list_display = [
'title', 'announcement_type', 'priority', 'author',
'is_published', 'publish_date', 'email_sent', 'push_sent', 'created_at'
]
list_filter = [
'announcement_type', 'priority', 'is_published',
'send_email', 'send_push', 'target_audience',
SoftDeleteListFilter, 'created_at'
]
search_fields = ['title', 'content', 'author__username']
readonly_fields = ['email_sent', 'push_sent', 'created_at', 'updated_at']
fieldsets = (
('Content', {
'fields': ('title', 'content', 'author')
}),
('Settings', {
'fields': ('announcement_type', 'priority', 'target_audience', 'is_published', 'publish_date')
}),
('Notifications', {
'fields': ('send_email', 'send_push', 'email_sent', 'push_sent')
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
actions = BaseModelAdmin.actions + ['publish_announcements', 'send_notifications']
def publish_announcements(self, request, queryset):
queryset.update(is_published=True, publish_date=timezone.now())
self.message_user(request, f"{queryset.count()} announcements published.")
publish_announcements.short_description = "Publish selected announcements"
def send_notifications(self, request, queryset):
# This will be implemented with Celery tasks
for announcement in queryset:
if announcement.send_email and not announcement.email_sent:
# Trigger email task
pass
if announcement.send_push and not announcement.push_sent:
# Trigger push notification task
pass
self.message_user(request, f"Notifications queued for {queryset.count()} announcements.")
send_notifications.short_description = "Send notifications for selected announcements"
@admin.register(NewsletterSubscription)
class NewsletterSubscriptionAdmin(BaseModelAdmin, ImportExportModelAdmin):
list_display = ['email', 'user', 'is_active', 'confirmed_at', 'created_at']
list_filter = ['is_active', SoftDeleteListFilter, 'created_at', 'confirmed_at']
search_fields = ['email', 'user__username', 'user__email']
readonly_fields = ['confirmation_token', 'unsubscribe_token', 'created_at', 'updated_at']
fieldsets = (
('Subscription', {
'fields': ('email', 'user', 'is_active', 'subscribed_categories')
}),
('Confirmation', {
'fields': ('confirmed_at', 'confirmation_token', 'unsubscribe_token')
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
actions = BaseModelAdmin.actions + ['activate_subscriptions', 'deactivate_subscriptions']
def activate_subscriptions(self, request, queryset):
queryset.update(is_active=True)
self.message_user(request, f"{queryset.count()} subscriptions activated.")
activate_subscriptions.short_description = "Activate selected subscriptions"
def deactivate_subscriptions(self, request, queryset):
queryset.update(is_active=False)
self.message_user(request, f"{queryset.count()} subscriptions deactivated.")
deactivate_subscriptions.short_description = "Deactivate selected subscriptions"
@admin.register(PushNotificationDevice)
class PushNotificationDeviceAdmin(BaseModelAdmin, ImportExportModelAdmin):
list_display = ['user', 'device_type', 'is_active', 'created_at']
list_filter = ['device_type', 'is_active', SoftDeleteListFilter, 'created_at']
search_fields = ['user__username', 'user__email', 'device_token']
readonly_fields = ['created_at', 'updated_at']
fieldsets = (
('Device', {
'fields': ('user', 'device_token', 'device_type', 'is_active')
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)

View File

@@ -0,0 +1,7 @@
from django.apps import AppConfig
class CommunicationsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'communications'
verbose_name = 'Communications'

View File

@@ -0,0 +1,536 @@
[
{
"model": "communications.announcement",
"pk": 1,
"fields": {
"created_at": "2024-03-01T10:00:00Z",
"updated_at": "2024-03-01T10:00:00Z",
"is_deleted": false,
"title": "شروع ثبت‌نام کارگاه یادگیری ماشین",
"content": "# شروع ثبت‌نام کارگاه یادگیری ماشین\n\nبا سلام و احترام\n\nثبتنام کارگاه یادگیری ماشین پیشرفته از امروز آغاز شد.\n\n## جزئیات:\n- تاریخ: ۱۵ اسفند ۱۴۰۲\n- مدت: ۴ ساعت\n- هزینه: ۱۵۰ هزار تومان\n- ظرفیت: ۵۰ نفر\n\nبرای ثبت‌نام به وب‌سایت انجمن مراجعه کنید.",
"announcement_type": "event",
"priority": "high",
"author": 1,
"is_published": true,
"publish_date": "2024-03-01T10:00:00Z",
"send_email": true,
"send_push": true,
"email_sent": true,
"push_sent": true,
"target_audience": "all"
}
},
{
"model": "communications.announcement",
"pk": 2,
"fields": {
"created_at": "2024-03-10T14:30:00Z",
"updated_at": "2024-03-10T14:30:00Z",
"is_deleted": false,
"title": "تغییر زمان مسابقه برنامه‌نویسی",
"content": "# تغییر زمان مسابقه برنامه‌نویسی\n\nبه اطلاع شرکت‌کنندگان محترم می‌رساند که زمان مسابقه برنامه‌نویسی بهاری به دلیل تعطیلات از ۲۲ اسفند به ۲۹ اسفند تغییر یافت.\n\nعذرخواهی بابت این تغییر و لطفاً برنامه‌ریزی خود را بر این اساس انجام دهید.",
"announcement_type": "urgent",
"priority": "urgent",
"author": 2,
"is_published": true,
"publish_date": "2024-03-10T14:30:00Z",
"send_email": true,
"send_push": true,
"email_sent": true,
"push_sent": true,
"target_audience": "all"
}
},
{
"model": "communications.announcement",
"pk": 3,
"fields": {
"created_at": "2024-03-15T09:00:00Z",
"updated_at": "2024-03-15T09:00:00Z",
"is_deleted": false,
"title": "وبینار امنیت سایبری - رایگان",
"content": "# وبینار امنیت سایبری\n\nانجمن علمی مهندسی کامپیوتر برگزار می‌کند:\n\n**وبینار امنیت سایبری**\n\n- تاریخ: ۷ فروردین ۱۴۰۳\n- ساعت: ۱۹:۰۰ الی ۲۱:۰۰\n- مدرس: دکتر محمد رضایی\n- شرکت: رایگان\n\nلینک ورود یک ساعت قبل از شروع ارسال خواهد شد.",
"announcement_type": "event",
"priority": "normal",
"author": 5,
"is_published": true,
"publish_date": "2024-03-15T09:00:00Z",
"send_email": true,
"send_push": false,
"email_sent": true,
"push_sent": false,
"target_audience": "members"
}
},
{
"model": "communications.announcement",
"pk": 4,
"fields": {
"created_at": "2024-03-20T11:15:00Z",
"updated_at": "2024-03-20T11:15:00Z",
"is_deleted": false,
"title": "فراخوان مقاله برای نشریه انجمن",
"content": "# فراخوان مقاله برای نشریه انجمن\n\nدانشجویان و اساتید محترم می‌توانند مقالات خود را در زمینه‌های زیر برای چاپ در نشریه انجمن ارسال کنند:\n\n## موضوعات:\n- هوش مصنوعی\n- امنیت سایبری\n- مهندسی نرم‌افزار\n- شبکه‌های کامپیوتری\n- علم داده\n\n## مهلت ارسال:\n۳۰ فروردین ۱۴۰۳\n\nایمیل ارسال: journal@cs-association.ac.ir",
"announcement_type": "academic",
"priority": "normal",
"author": 1,
"is_published": true,
"publish_date": "2024-03-20T11:15:00Z",
"send_email": true,
"send_push": false,
"email_sent": true,
"push_sent": false,
"target_audience": "all"
}
},
{
"model": "communications.announcement",
"pk": 5,
"fields": {
"created_at": "2024-04-01T08:00:00Z",
"updated_at": "2024-04-01T08:00:00Z",
"is_deleted": false,
"title": "هکاتون هوش مصنوعی - ثبت‌نام آغاز شد",
"content": "# هکاتون هوش مصنوعی\n\nبزرگترین رویداد سال انجمن!\n\n## جزئیات:\n- تاریخ: ۳۰ فروردین تا ۲ اردیبهشت\n- مدت: ۴۸ ساعت\n- جایزه کل: ۲۰ میلیون تومان\n- ظرفیت: ۶۰ نفر (۲۰ تیم)\n\n## امکانات:\n- غذا و نوشیدنی رایگان\n- منتورینگ اساتید\n- فضای کار ۲۴ ساعته\n\nثبتنام تیمی (۳ نفره) الزامی است.",
"announcement_type": "event",
"priority": "high",
"author": 9,
"is_published": true,
"publish_date": "2024-04-01T08:00:00Z",
"send_email": true,
"send_push": true,
"email_sent": true,
"push_sent": true,
"target_audience": "all"
}
},
{
"model": "communications.announcement",
"pk": 6,
"fields": {
"created_at": "2024-04-05T16:00:00Z",
"updated_at": "2024-04-05T16:00:00Z",
"is_deleted": false,
"title": "جلسه کمیته اجرایی انجمن",
"content": "# جلسه کمیته اجرایی انجمن\n\nاعضای محترم کمیته اجرایی\n\nجلسه ماهانه کمیته اجرایی:\n\n- تاریخ: ۱۰ اردیبهشت ۱۴۰۳\n- ساعت: ۱۴:۰۰\n- مکان: دفتر انجمن\n\n## دستور جلسه:\n1. بررسی گزارش مالی\n2. برنامه‌ریزی رویدادهای آتی\n3. بررسی درخواست‌های عضویت\n4. سایر موارد\n\nحضور همه اعضا الزامی است.",
"announcement_type": "general",
"priority": "normal",
"author": 1,
"is_published": true,
"publish_date": "2024-04-05T16:00:00Z",
"send_email": true,
"send_push": false,
"email_sent": true,
"push_sent": false,
"target_audience": "committee"
}
},
{
"model": "communications.announcement",
"pk": 7,
"fields": {
"created_at": "2024-04-15T12:30:00Z",
"updated_at": "2024-04-15T12:30:00Z",
"is_deleted": false,
"title": "سمینار کارآفرینی فناوری",
"content": "# سمینار کارآفرینی فناوری\n\nبا حضور کارآفرینان موفق صنعت فناوری\n\n## سخنرانان:\n- دکتر علی احمدی (موسس تپسی)\n- خانم سارا محمدی (مدیرعامل کافه‌بازار)\n- مهندس رضا کریمی (سرمایه‌گذار)\n\n## موضوعات:\n- از ایده تا محصول\n- جذب سرمایه\n- چالش‌های استارتاپی\n- آینده فناوری در ایران\n\nشرکت رایگان - ظرفیت محدود",
"announcement_type": "event",
"priority": "high",
"author": 2,
"is_published": true,
"publish_date": "2024-04-15T12:30:00Z",
"send_email": true,
"send_push": true,
"email_sent": true,
"push_sent": true,
"target_audience": "all"
}
},
{
"model": "communications.announcement",
"pk": 8,
"fields": {
"created_at": "2024-04-20T10:45:00Z",
"updated_at": "2024-04-20T10:45:00Z",
"is_deleted": false,
"title": "کارگاه DevOps - ثبت‌نام محدود",
"content": "# کارگاه DevOps و Docker\n\nآموزش عملی ابزارهای DevOps\n\n## محتوا:\n- Docker و Containerization\n- CI/CD Pipeline\n- Kubernetes مقدماتی\n- پروژه عملی\n\n## جزئیات:\n- تاریخ: ۱۴ اردیبهشت\n- مدت: ۸ ساعت\n- هزینه: ۳۰۰ هزار تومان\n- ظرفیت: ۲۵ نفر\n\n⚠ ظرفیت بسیار محدود - عجله کنید!",
"announcement_type": "event",
"priority": "high",
"author": 8,
"is_published": true,
"publish_date": "2024-04-20T10:45:00Z",
"send_email": true,
"send_push": true,
"email_sent": true,
"push_sent": true,
"target_audience": "members"
}
},
{
"model": "communications.announcement",
"pk": 9,
"fields": {
"created_at": "2024-04-25T13:20:00Z",
"updated_at": "2024-04-25T13:20:00Z",
"is_deleted": false,
"title": "مسابقه طراحی UI/UX - جوایز جذاب",
"content": "# مسابقه طراحی UI/UX\n\nفرصتی برای نمایش خلاقیت شما!\n\n## موضوع:\nطراحی اپلیکیشن مدیریت تسک دانشجویی\n\n## جوایز:\n- نفر اول: iPad Air\n- نفر دوم: AirPods Pro\n- نفر سوم: پاوربانک ۲۰۰۰۰ میلی‌آمپر\n\n## مهلت ارسال:\n۲۰ اردیبهشت ۱۴۰۳\n\nفایلهای Figma یا Adobe XD قابل قبول هستند.",
"announcement_type": "event",
"priority": "normal",
"author": 12,
"is_published": true,
"publish_date": "2024-04-25T13:20:00Z",
"send_email": true,
"send_push": false,
"email_sent": true,
"push_sent": false,
"target_audience": "all"
}
},
{
"model": "communications.announcement",
"pk": 10,
"fields": {
"created_at": "2024-05-01T15:00:00Z",
"updated_at": "2024-05-01T15:00:00Z",
"is_deleted": false,
"title": "نشست فارغ‌التحصیلان - دعوت ویژه",
"content": "# نشست فارغ‌التحصیلان\n\nدیدار با فارغ‌التحصیلان موفق\n\n## مهمانان ویژه:\n- دکتر حسن زارع (مدیر فنی گوگل)\n- مهندس مریم حسینی (بنیان‌گذار استارتاپ)\n- دکتر امیر قربانی (استاد MIT)\n\n## برنامه:\n- ۱۷:۰۰ - پذیرایی\n- ۱۸:۰۰ - سخنرانی‌ها\n- ۱۹:۳۰ - پرسش و پاسخ\n- ۲۰:۳۰ - ضیافت شام\n\nشرکت رایگان - ثبت‌نام الزامی",
"announcement_type": "event",
"priority": "normal",
"author": 5,
"is_published": true,
"publish_date": "2024-05-01T15:00:00Z",
"send_email": true,
"send_push": false,
"email_sent": true,
"push_sent": false,
"target_audience": "all"
}
},
{
"model": "communications.newslettersubscription",
"pk": 1,
"fields": {
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z",
"is_deleted": false,
"email": "sara.mohammadi@student.ac.ir",
"user": 2,
"is_active": true,
"subscribed_categories": ["event", "academic", "general"],
"confirmed_at": "2024-01-15T10:30:00Z"
}
},
{
"model": "communications.newslettersubscription",
"pk": 2,
"fields": {
"created_at": "2024-01-20T14:15:00Z",
"updated_at": "2024-01-20T14:15:00Z",
"is_deleted": false,
"email": "reza.karimi@student.ac.ir",
"user": 3,
"is_active": true,
"subscribed_categories": ["event", "urgent"],
"confirmed_at": "2024-01-20T14:15:00Z"
}
},
{
"model": "communications.newslettersubscription",
"pk": 3,
"fields": {
"created_at": "2024-02-01T09:45:00Z",
"updated_at": "2024-02-01T09:45:00Z",
"is_deleted": false,
"email": "maryam.hosseini@student.ac.ir",
"user": 4,
"is_active": true,
"subscribed_categories": ["event", "academic"],
"confirmed_at": "2024-02-01T09:45:00Z"
}
},
{
"model": "communications.newslettersubscription",
"pk": 4,
"fields": {
"created_at": "2024-02-05T16:20:00Z",
"updated_at": "2024-02-05T16:20:00Z",
"is_deleted": false,
"email": "hassan.zare@student.ac.ir",
"user": 5,
"is_active": true,
"subscribed_categories": ["general", "event", "academic", "urgent"],
"confirmed_at": "2024-02-05T16:20:00Z"
}
},
{
"model": "communications.newslettersubscription",
"pk": 5,
"fields": {
"created_at": "2024-02-10T11:30:00Z",
"updated_at": "2024-02-10T11:30:00Z",
"is_deleted": false,
"email": "zahra.safari@student.ac.ir",
"user": 6,
"is_active": true,
"subscribed_categories": ["event", "academic"],
"confirmed_at": "2024-02-10T11:30:00Z"
}
},
{
"model": "communications.newslettersubscription",
"pk": 6,
"fields": {
"created_at": "2024-02-15T13:45:00Z",
"updated_at": "2024-02-15T13:45:00Z",
"is_deleted": false,
"email": "fateme.moradi@student.ac.ir",
"user": 8,
"is_active": true,
"subscribed_categories": ["event"],
"confirmed_at": "2024-02-15T13:45:00Z"
}
},
{
"model": "communications.newslettersubscription",
"pk": 7,
"fields": {
"created_at": "2024-02-20T08:15:00Z",
"updated_at": "2024-02-20T08:15:00Z",
"is_deleted": false,
"email": "amir.ghorbani@student.ac.ir",
"user": 9,
"is_active": true,
"subscribed_categories": ["general", "event", "academic"],
"confirmed_at": "2024-02-20T08:15:00Z"
}
},
{
"model": "communications.newslettersubscription",
"pk": 8,
"fields": {
"created_at": "2024-02-25T15:30:00Z",
"updated_at": "2024-02-25T15:30:00Z",
"is_deleted": false,
"email": "nasrin.jafari@student.ac.ir",
"user": 10,
"is_active": true,
"subscribed_categories": ["academic", "event"],
"confirmed_at": "2024-02-25T15:30:00Z"
}
},
{
"model": "communications.newslettersubscription",
"pk": 9,
"fields": {
"created_at": "2024-03-01T12:00:00Z",
"updated_at": "2024-03-01T12:00:00Z",
"is_deleted": false,
"email": "mehdi.bagheri@student.ac.ir",
"user": 11,
"is_active": true,
"subscribed_categories": ["event"],
"confirmed_at": "2024-03-01T12:00:00Z"
}
},
{
"model": "communications.newslettersubscription",
"pk": 10,
"fields": {
"created_at": "2024-03-05T14:45:00Z",
"updated_at": "2024-03-05T14:45:00Z",
"is_deleted": false,
"email": "leila.mousavi@student.ac.ir",
"user": 12,
"is_active": true,
"subscribed_categories": ["event", "academic"],
"confirmed_at": "2024-03-05T14:45:00Z"
}
},
{
"model": "communications.newslettersubscription",
"pk": 11,
"fields": {
"created_at": "2024-03-10T10:20:00Z",
"updated_at": "2024-03-10T10:20:00Z",
"is_deleted": false,
"email": "external.user1@gmail.com",
"user": null,
"is_active": true,
"subscribed_categories": ["event"],
"confirmed_at": "2024-03-10T10:20:00Z"
}
},
{
"model": "communications.newslettersubscription",
"pk": 12,
"fields": {
"created_at": "2024-03-15T16:30:00Z",
"updated_at": "2024-03-15T16:30:00Z",
"is_deleted": false,
"email": "external.user2@yahoo.com",
"user": null,
"is_active": false,
"subscribed_categories": ["general"],
"confirmed_at": null
}
},
{
"model": "communications.pushnotificationdevice",
"pk": 1,
"fields": {
"created_at": "2024-01-10T08:00:00Z",
"updated_at": "2024-01-10T08:00:00Z",
"is_deleted": false,
"user": 1,
"device_token": "web_push_token_admin_chrome",
"device_type": "web",
"is_active": true
}
},
{
"model": "communications.pushnotificationdevice",
"pk": 2,
"fields": {
"created_at": "2024-01-15T12:30:00Z",
"updated_at": "2024-01-15T12:30:00Z",
"is_deleted": false,
"user": 2,
"device_token": "web_push_token_sara_firefox",
"device_type": "web",
"is_active": true
}
},
{
"model": "communications.pushnotificationdevice",
"pk": 3,
"fields": {
"created_at": "2024-01-20T16:45:00Z",
"updated_at": "2024-01-20T16:45:00Z",
"is_deleted": false,
"user": 3,
"device_token": "web_push_token_reza_chrome",
"device_type": "web",
"is_active": true
}
},
{
"model": "communications.pushnotificationdevice",
"pk": 4,
"fields": {
"created_at": "2024-02-01T11:20:00Z",
"updated_at": "2024-02-01T11:20:00Z",
"is_deleted": false,
"user": 4,
"device_token": "android_token_maryam_phone",
"device_type": "android",
"is_active": true
}
},
{
"model": "communications.pushnotificationdevice",
"pk": 5,
"fields": {
"created_at": "2024-02-05T18:10:00Z",
"updated_at": "2024-02-05T18:10:00Z",
"is_deleted": false,
"user": 5,
"device_token": "web_push_token_hassan_edge",
"device_type": "web",
"is_active": true
}
},
{
"model": "communications.pushnotificationdevice",
"pk": 6,
"fields": {
"created_at": "2024-02-10T13:25:00Z",
"updated_at": "2024-02-10T13:25:00Z",
"is_deleted": false,
"user": 6,
"device_token": "ios_token_zahra_iphone",
"device_type": "ios",
"is_active": true
}
},
{
"model": "communications.pushnotificationdevice",
"pk": 7,
"fields": {
"created_at": "2024-02-15T15:40:00Z",
"updated_at": "2024-02-15T15:40:00Z",
"is_deleted": false,
"user": 8,
"device_token": "web_push_token_fateme_chrome",
"device_type": "web",
"is_active": true
}
},
{
"model": "communications.pushnotificationdevice",
"pk": 8,
"fields": {
"created_at": "2024-02-20T10:15:00Z",
"updated_at": "2024-02-20T10:15:00Z",
"is_deleted": false,
"user": 9,
"device_token": "web_push_token_amir_firefox",
"device_type": "web",
"is_active": true
}
},
{
"model": "communications.pushnotificationdevice",
"pk": 9,
"fields": {
"created_at": "2024-02-25T17:30:00Z",
"updated_at": "2024-02-25T17:30:00Z",
"is_deleted": false,
"user": 10,
"device_token": "android_token_nasrin_phone",
"device_type": "android",
"is_active": true
}
},
{
"model": "communications.pushnotificationdevice",
"pk": 10,
"fields": {
"created_at": "2024-03-01T14:00:00Z",
"updated_at": "2024-03-01T14:00:00Z",
"is_deleted": false,
"user": 11,
"device_token": "web_push_token_mehdi_chrome",
"device_type": "web",
"is_active": true
}
},
{
"model": "communications.pushnotificationdevice",
"pk": 11,
"fields": {
"created_at": "2024-03-05T16:50:00Z",
"updated_at": "2024-03-05T16:50:00Z",
"is_deleted": false,
"user": 12,
"device_token": "ios_token_leila_iphone",
"device_type": "ios",
"is_active": true
}
},
{
"model": "communications.pushnotificationdevice",
"pk": 12,
"fields": {
"created_at": "2024-01-10T08:00:00Z",
"updated_at": "2024-03-10T12:00:00Z",
"is_deleted": false,
"user": 1,
"device_token": "android_token_admin_phone",
"device_type": "android",
"is_active": false
}
}
]

View File

@@ -0,0 +1,78 @@
# Generated by Django 5.2.5 on 2025-10-16 12:07
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Announcement',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('is_deleted', models.BooleanField(default=False)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('title', models.CharField(max_length=200, verbose_name='Title')),
('content', models.TextField(verbose_name='Content')),
('announcement_type', models.CharField(choices=[('general', 'General'), ('event', 'Event'), ('academic', 'Academic'), ('urgent', 'Urgent'), ('newsletter', 'Newsletter')], default='general', max_length=20, verbose_name='Type')),
('priority', models.CharField(choices=[('low', 'Low'), ('normal', 'Normal'), ('high', 'High'), ('urgent', 'Urgent')], default='normal', max_length=10, verbose_name='Priority')),
('is_published', models.BooleanField(default=False, verbose_name='Published')),
('publish_date', models.DateTimeField(blank=True, null=True, verbose_name='Publish Date')),
('send_email', models.BooleanField(default=False, verbose_name='Send Email Notification')),
('send_push', models.BooleanField(default=False, verbose_name='Send Push Notification')),
('email_sent', models.BooleanField(default=False, verbose_name='Email Sent')),
('push_sent', models.BooleanField(default=False, verbose_name='Push Sent')),
('target_audience', models.CharField(choices=[('all', 'All Users'), ('members', 'Members Only'), ('committee', 'Committee Only'), ('subscribers', 'Newsletter Subscribers Only')], default='all', max_length=20, verbose_name='Target Audience')),
],
options={
'verbose_name': 'Announcement',
'verbose_name_plural': 'Announcements',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='NewsletterSubscription',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('is_deleted', models.BooleanField(default=False)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')),
('is_active', models.BooleanField(default=True, verbose_name='Active')),
('subscribed_categories', models.JSONField(blank=True, default=list, help_text='List of announcement types to receive', verbose_name='Subscribed Categories')),
('confirmation_token', models.CharField(blank=True, max_length=100, verbose_name='Confirmation Token')),
('confirmed_at', models.DateTimeField(blank=True, null=True, verbose_name='Confirmed At')),
('unsubscribe_token', models.CharField(blank=True, max_length=100, verbose_name='Unsubscribe Token')),
],
options={
'verbose_name': 'Newsletter Subscription',
'verbose_name_plural': 'Newsletter Subscriptions',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='PushNotificationDevice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('is_deleted', models.BooleanField(default=False)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('device_token', models.TextField(verbose_name='Device Token')),
('device_type', models.CharField(choices=[('web', 'Web'), ('android', 'Android'), ('ios', 'iOS')], max_length=10, verbose_name='Device Type')),
('is_active', models.BooleanField(default=True, verbose_name='Active')),
],
options={
'verbose_name': 'Push Notification Device',
'verbose_name_plural': 'Push Notification Devices',
},
),
]

View File

@@ -0,0 +1,37 @@
# Generated by Django 5.2.5 on 2025-10-16 12:07
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('communications', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='announcement',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='announcements', to=settings.AUTH_USER_MODEL, verbose_name='Author'),
),
migrations.AddField(
model_name='newslettersubscription',
name='user',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='newsletter_subscription', to=settings.AUTH_USER_MODEL, verbose_name='User'),
),
migrations.AddField(
model_name='pushnotificationdevice',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='push_devices', to=settings.AUTH_USER_MODEL, verbose_name='User'),
),
migrations.AlterUniqueTogether(
name='pushnotificationdevice',
unique_together={('user', 'device_token')},
),
]

View File

@@ -0,0 +1,142 @@
from django.db import models
from django.contrib.auth import get_user_model
from utils.models import BaseModel
User = get_user_model()
class AnnouncementType(models.TextChoices):
GENERAL = 'general', 'General'
EVENT = 'event', 'Event'
ACADEMIC = 'academic', 'Academic'
URGENT = 'urgent', 'Urgent'
NEWSLETTER = 'newsletter', 'Newsletter'
class AnnouncementPriority(models.TextChoices):
LOW = 'low', 'Low'
NORMAL = 'normal', 'Normal'
HIGH = 'high', 'High'
URGENT = 'urgent', 'Urgent'
class Announcement(BaseModel):
title = models.CharField(max_length=200, verbose_name='Title')
content = models.TextField(verbose_name='Content')
announcement_type = models.CharField(
max_length=20,
choices=AnnouncementType.choices,
default=AnnouncementType.GENERAL,
verbose_name='Type'
)
priority = models.CharField(
max_length=10,
choices=AnnouncementPriority.choices,
default=AnnouncementPriority.NORMAL,
verbose_name='Priority'
)
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='announcements',
verbose_name='Author'
)
is_published = models.BooleanField(default=False, verbose_name='Published')
publish_date = models.DateTimeField(null=True, blank=True, verbose_name='Publish Date')
send_email = models.BooleanField(default=False, verbose_name='Send Email Notification')
send_push = models.BooleanField(default=False, verbose_name='Send Push Notification')
email_sent = models.BooleanField(default=False, verbose_name='Email Sent')
push_sent = models.BooleanField(default=False, verbose_name='Push Sent')
target_audience = models.CharField(
max_length=20,
choices=[
('all', 'All Users'),
('members', 'Members Only'),
('committee', 'Committee Only'),
('subscribers', 'Newsletter Subscribers Only'),
],
default='all',
verbose_name='Target Audience'
)
class Meta:
verbose_name = 'Announcement'
verbose_name_plural = 'Announcements'
ordering = ['-created_at']
def __str__(self):
return self.title
@property
def content_html(self):
"""Convert markdown content to HTML"""
import markdown
return markdown.markdown(self.content)
class NewsletterSubscription(BaseModel):
email = models.EmailField(unique=True, verbose_name='Email')
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='newsletter_subscription',
verbose_name='User'
)
is_active = models.BooleanField(default=True, verbose_name='Active')
subscribed_categories = models.JSONField(
default=list,
blank=True,
verbose_name='Subscribed Categories',
help_text='List of announcement types to receive'
)
confirmation_token = models.CharField(max_length=100, blank=True, verbose_name='Confirmation Token')
confirmed_at = models.DateTimeField(null=True, blank=True, verbose_name='Confirmed At')
unsubscribe_token = models.CharField(max_length=100, blank=True, verbose_name='Unsubscribe Token')
class Meta:
verbose_name = 'Newsletter Subscription'
verbose_name_plural = 'Newsletter Subscriptions'
ordering = ['-created_at']
def __str__(self):
return self.email
def save(self, *args, **kwargs):
if not self.confirmation_token:
import uuid
self.confirmation_token = str(uuid.uuid4())
if not self.unsubscribe_token:
import uuid
self.unsubscribe_token = str(uuid.uuid4())
super().save(*args, **kwargs)
class PushNotificationDevice(BaseModel):
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='push_devices',
verbose_name='User'
)
device_token = models.TextField(verbose_name='Device Token')
device_type = models.CharField(
max_length=10,
choices=[
('web', 'Web'),
('android', 'Android'),
('ios', 'iOS'),
],
verbose_name='Device Type'
)
is_active = models.BooleanField(default=True, verbose_name='Active')
class Meta:
verbose_name = 'Push Notification Device'
verbose_name_plural = 'Push Notification Devices'
unique_together = ['user', 'device_token']
def __str__(self):
return f"{self.user.username} - {self.device_type}"

View File

@@ -0,0 +1,194 @@
from django.conf import settings
import json
import logging
from typing import List, Dict, Any, Optional
from pywebpush import webpush, WebPushException
from communications.models import PushNotificationDevice
from events.models import Registration
logger = logging.getLogger(__name__)
class PushNotificationService:
"""Service for handling web push notifications"""
def __init__(self):
self.vapid_private_key = getattr(settings, 'VAPID_PRIVATE_KEY', None)
self.vapid_public_key = getattr(settings, 'VAPID_PUBLIC_KEY', None)
self.vapid_claims = getattr(settings, 'VAPID_CLAIMS', {})
def send_notification(
self,
subscription_info: Dict[str, Any],
data: Dict[str, Any],
ttl: int = 86400
) -> bool:
"""
Send a push notification to a single device
Args:
subscription_info: Device subscription information
data: Notification payload
ttl: Time to live in seconds (default 24 hours)
Returns:
bool: True if successful, False otherwise
"""
try:
webpush(
subscription_info=subscription_info,
data=json.dumps(data),
vapid_private_key=self.vapid_private_key,
vapid_claims=self.vapid_claims,
ttl=ttl
)
return True
except WebPushException as e:
logger.error(f"Push notification failed: {e}")
if e.response and e.response.status_code in [410, 413]:
# Subscription is no longer valid, should be removed
self._remove_invalid_subscription(subscription_info)
return False
except Exception as e:
logger.error(f"Unexpected error sending push notification: {e}")
return False
def send_to_multiple(
self,
devices: List[PushNotificationDevice],
data: Dict[str, Any],
ttl: int = 86400
) -> Dict[str, int]:
"""
Send push notification to multiple devices
Args:
devices: List of PushNotificationDevice objects
data: Notification payload
ttl: Time to live in seconds
Returns:
dict: Statistics of sent/failed notifications
"""
stats = {'sent': 0, 'failed': 0}
for device in devices:
subscription_info = {
'endpoint': device.endpoint,
'keys': {
'p256dh': device.p256dh_key,
'auth': device.auth_key
}
}
if self.send_notification(subscription_info, data, ttl):
stats['sent'] += 1
else:
stats['failed'] += 1
return stats
def send_announcement_notification(
self,
announcement,
devices: Optional[List[PushNotificationDevice]] = None
) -> Dict[str, int]:
"""
Send push notification for an announcement
Args:
announcement: Announcement model instance
devices: Optional list of specific devices to send to
Returns:
dict: Statistics of sent/failed notifications
"""
if devices is None:
# Get devices based on announcement audience
if announcement.audience == 'all':
devices = PushNotificationDevice.objects.filter(is_active=True)
elif announcement.audience == 'members':
devices = PushNotificationDevice.objects.filter(
user__is_member=True,
is_active=True
)
elif announcement.audience == 'committee':
devices = PushNotificationDevice.objects.filter(
user__is_committee_member=True,
is_active=True
)
else:
devices = PushNotificationDevice.objects.none()
# Prepare notification data
data = {
'title': announcement.title,
'body': announcement.content[:100] + '...' if len(announcement.content) > 100 else announcement.content,
'icon': '/static/images/logo.png',
'badge': '/static/images/badge.png',
'data': {
'type': 'announcement',
'id': announcement.id,
'url': f'/announcements/{announcement.id}/'
}
}
return self.send_to_multiple(devices, data)
def send_event_reminder_notification(
self,
event,
devices: Optional[List[PushNotificationDevice]] = None
) -> Dict[str, int]:
"""
Send push notification for event reminder
Args:
event: Event model instance
devices: Optional list of specific devices to send to
Returns:
dict: Statistics of sent/failed notifications
"""
if devices is None:
# Get devices of registered users
registered_users = Registration.objects.filter(
event=event,
status='confirmed'
).values_list('user_id', flat=True)
devices = PushNotificationDevice.objects.filter(
user_id__in=registered_users,
is_active=True
)
# Prepare notification data
data = {
'title': f'Event Reminder: {event.title}',
'body': f'Your event "{event.title}" starts in 24 hours!',
'icon': '/static/images/logo.png',
'badge': '/static/images/badge.png',
'data': {
'type': 'event_reminder',
'id': event.id,
'url': f'/events/{event.id}/'
}
}
return self.send_to_multiple(devices, data)
def _remove_invalid_subscription(self, subscription_info: Dict[str, Any]):
"""Remove invalid subscription from database"""
try:
PushNotificationDevice.objects.filter(
endpoint=subscription_info['endpoint']
).delete()
logger.info(f"Removed invalid subscription: {subscription_info['endpoint']}")
except Exception as e:
logger.error(f"Error removing invalid subscription: {e}")
# Create a singleton instance
push_service = PushNotificationService()

View File

@@ -0,0 +1,56 @@
from django.contrib.auth import get_user_model
from import_export import resources, fields
from import_export.widgets import ForeignKeyWidget
from communications.models import Announcement, NewsletterSubscription, PushNotificationDevice
User = get_user_model()
class AnnouncementResource(resources.ModelResource):
author = fields.Field(
column_name='author',
attribute='author',
widget=ForeignKeyWidget(User, 'username')
)
class Meta:
model = Announcement
fields = (
'id', 'title', 'content', 'announcement_type', 'priority',
'author', 'is_published', 'publish_date', 'send_email', 'send_push',
'target_audience', 'created_at', 'updated_at'
)
export_order = fields
class NewsletterSubscriptionResource(resources.ModelResource):
user = fields.Field(
column_name='user',
attribute='user',
widget=ForeignKeyWidget(User, 'username')
)
class Meta:
model = NewsletterSubscription
fields = (
'id', 'email', 'user', 'is_active', 'subscribed_categories',
'confirmed_at', 'created_at', 'updated_at'
)
export_order = fields
class PushNotificationDeviceResource(resources.ModelResource):
user = fields.Field(
column_name='user',
attribute='user',
widget=ForeignKeyWidget(User, 'username')
)
class Meta:
model = PushNotificationDevice
fields = (
'id', 'user', 'device_type', 'is_active', 'created_at', 'updated_at'
)
export_order = fields

View File

@@ -0,0 +1,278 @@
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 events.models import Event, Registration
from communications.models import Announcement, NewsletterSubscription
from communications.utils import send_announcement_email, send_event_reminder, get_announcement_recipients
from 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

View File

@@ -0,0 +1,140 @@
from django.contrib.auth import get_user_model
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
import logging
from communications.models import NewsletterSubscription
logger = logging.getLogger(__name__)
def send_announcement_email(announcement, recipients):
"""Send announcement email to recipients"""
try:
template_name = f'emails/announcement_email.html'
context = {
'announcement': announcement,
'unsubscribe_url': f"{settings.FRONTEND_ROOT}newsletter/unsubscribe/",
'manage_subscription_url': f"{settings.FRONTEND_ROOT}newsletter/manage-subscription",
}
html_message = render_to_string(template_name, context)
plain_message = strip_tags(html_message)
subject = f"انجمن علمی کامپیوتر گیلان | {announcement.title}"
send_mail(
subject=subject,
message=plain_message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=recipients,
html_message=html_message,
fail_silently=False,
)
logger.info(f"Announcement email sent to {len(recipients)} recipients")
return True
except Exception as e:
logger.error(f"Failed to send announcement email: {str(e)}")
return False
def send_newsletter_confirmation(subscription):
"""Send newsletter confirmation email"""
try:
template_name = f'emails/newsletter_confirmation.html'
confirmation_url = f"{settings.FRONTEND_ROOT}confirm-subscription/{subscription.confirmation_token}"
context = {
'subscription': subscription,
'confirmation_url': confirmation_url,
}
html_message = render_to_string(template_name, context)
plain_message = strip_tags(html_message)
subject = "تأیید اشتراک خبرنامه"
send_mail(
subject=subject,
message=plain_message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[subscription.email],
html_message=html_message,
fail_silently=False,
)
logger.info(f"Newsletter confirmation sent to {subscription.email}")
return True
except Exception as e:
logger.error(f"Failed to send newsletter confirmation: {str(e)}")
return False
def send_event_reminder(event, user):
"""Send event reminder email"""
try:
template_name = f'emails/event_reminder.html'
event_url = f"{settings.FRONTEND_ROOT}events/{event.slug}"
context = {
'event': event,
'user': user,
'event_url': event_url,
}
html_message = render_to_string(template_name, context)
plain_message = strip_tags(html_message)
subject = f"یادآوری رویداد: {event.title}"
send_mail(
subject=subject,
message=plain_message,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[user.email],
html_message=html_message,
fail_silently=False,
)
logger.info(f"Event reminder sent to {user.email} for event {event.title}")
return True
except Exception as e:
logger.error(f"Failed to send event reminder: {str(e)}")
return False
def get_announcement_recipients(announcement):
"""Get list of email addresses based on announcement target audience"""
User = get_user_model()
recipients = []
if announcement.target_audience == 'all':
# All users with email
recipients = list(User.objects.filter(email__isnull=False).values_list('email', flat=True))
elif announcement.target_audience == 'members':
# Only members (users with is_member=True)
recipients = list(User.objects.filter(is_member=True, email__isnull=False).values_list('email', flat=True))
elif announcement.target_audience == 'committee':
# Only committee members
recipients = list(User.objects.filter(is_committee=True, email__isnull=False).values_list('email', flat=True))
elif announcement.target_audience == 'subscribers':
# Only newsletter subscribers
recipients = list(NewsletterSubscription.objects.filter(
is_active=True,
confirmed_at__isnull=False
).values_list('email', flat=True))
return recipients