feat(notifications): add scheduled email notification system
- NotificationSchedule + NotificationRecipient models with initial migration - emails.py BUILDERS registry + events_expiring_this_week HTML email builder (IST week bounds) - send_scheduled_notifications management command (croniter due-check + select_for_update(skip_locked)) - 6 admin API endpoints under /api/v1/notifications/ (types, schedules CRUD, recipients CRUD, send-now) - date_from/date_to filters on EventListView for dashboard card - croniter>=2.0.0 added to requirements Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
93
notifications/migrations/0001_initial.py
Normal file
93
notifications/migrations/0001_initial.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Notification',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255)),
|
||||
('message', models.TextField()),
|
||||
('notification_type', models.CharField(
|
||||
choices=[
|
||||
('event', 'Event'),
|
||||
('promo', 'Promotion'),
|
||||
('system', 'System'),
|
||||
('booking', 'Booking'),
|
||||
],
|
||||
default='system', max_length=20,
|
||||
)),
|
||||
('is_read', models.BooleanField(default=False)),
|
||||
('action_url', models.URLField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='notifications',
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
)),
|
||||
],
|
||||
options={'ordering': ['-created_at']},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='NotificationSchedule',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('notification_type', models.CharField(
|
||||
choices=[('events_expiring_this_week', 'Events Expiring This Week')],
|
||||
db_index=True, max_length=64,
|
||||
)),
|
||||
('cron_expression', models.CharField(
|
||||
default='0 0 * * 1',
|
||||
help_text=(
|
||||
'Standard 5-field cron (minute hour dom month dow). '
|
||||
'Evaluated in Asia/Kolkata.'
|
||||
),
|
||||
max_length=100,
|
||||
)),
|
||||
('is_active', models.BooleanField(db_index=True, default=True)),
|
||||
('last_run_at', models.DateTimeField(blank=True, null=True)),
|
||||
('last_status', models.CharField(blank=True, default='', max_length=20)),
|
||||
('last_error', models.TextField(blank=True, default='')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={'ordering': ['-created_at']},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='notificationschedule',
|
||||
index=models.Index(
|
||||
fields=['is_active', 'notification_type'],
|
||||
name='notificatio_is_acti_26dfb5_idx',
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='NotificationRecipient',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('email', models.EmailField(max_length=254)),
|
||||
('display_name', models.CharField(blank=True, default='', max_length=200)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('schedule', models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='recipients',
|
||||
to='notifications.notificationschedule',
|
||||
)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['display_name', 'email'],
|
||||
'unique_together': {('schedule', 'email')},
|
||||
},
|
||||
),
|
||||
]
|
||||
0
notifications/migrations/__init__.py
Normal file
0
notifications/migrations/__init__.py
Normal file
Reference in New Issue
Block a user