diff --git a/apps/push_notifications/migrations/0007_alter_notificationlink_action_code_and_more.py b/apps/push_notifications/migrations/0007_alter_notificationlink_action_code_and_more.py new file mode 100644 index 0000000..75d5d1e --- /dev/null +++ b/apps/push_notifications/migrations/0007_alter_notificationlink_action_code_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.6 on 2026-09-01 12:23 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('push_notifications', '0006_rename_code_notificationlink_action_code_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='notificationlink', + name='action_code', + field=models.CharField(db_index=True, max_length=100, unique=True, validators=[django.core.validators.RegexValidator(message='Enter a valid action code: letters, numbers, underscores, hyphens, or dots.', regex='^[-a-zA-Z0-9_.]+\\Z')]), + ), + migrations.AlterField( + model_name='pushmessage', + name='action_code', + field=models.CharField(blank=True, db_index=True, max_length=100, null=True, validators=[django.core.validators.RegexValidator(message='Enter a valid action code: letters, numbers, underscores, hyphens, or dots.', regex='^[-a-zA-Z0-9_.]+\\Z')]), + ), + ] diff --git a/apps/push_notifications/models.py b/apps/push_notifications/models.py index 64f49c1..2995e6a 100644 --- a/apps/push_notifications/models.py +++ b/apps/push_notifications/models.py @@ -1,3 +1,4 @@ +from django.core.validators import RegexValidator from django.db import models from oauth2_provider.settings import oauth2_settings @@ -6,6 +7,15 @@ from utils.models import BaseModel from utils.clients import gotify from django.utils.translation import gettext_lazy as _ +# action_code values are dotted, e.g. "billboard.approved", "escrow.timeout" +# (see docs/frontend-notification-click-actions.md) — a plain SlugField +# rejects the dot, so every real call using the documented catalog gets a +# 400 from the serializer. This mirrors slug's allowed charset but adds ".". +validate_action_code = RegexValidator( + regex=r'^[-a-zA-Z0-9_.]+\Z', + message="Enter a valid action code: letters, numbers, underscores, hyphens, or dots.", +) + class PushUserQuerySet(models.QuerySet): def submit(self, user): @@ -51,7 +61,7 @@ class NotificationLink(BaseModel): type) instead of a raw URL, so where a tap should navigate is a config change here, not a code deploy across every producer service. """ - action_code = models.SlugField(max_length=100, unique=True, db_index=True) + action_code = models.CharField(max_length=100, unique=True, db_index=True, validators=[validate_action_code]) description = models.CharField(max_length=255, blank=True) url_template = models.CharField( max_length=1000, @@ -93,7 +103,7 @@ class PushMessage(BaseModel): # action_code, resolved against NotificationLink at send time (see # get_gotify_extras). click_object_id_value is substituted into that # action_code's {object_id} placeholder. - action_code = models.SlugField(max_length=100, null=True, blank=True, db_index=True) + action_code = models.CharField(max_length=100, null=True, blank=True, db_index=True, validators=[validate_action_code]) click_object_id_value = models.CharField(max_length=255, null=True, blank=True) stats = models.IntegerField(default=StateChoices.INIT, choices=StateChoices.choices) diff --git a/apps/push_notifications/tasks.py b/apps/push_notifications/tasks.py index 9577403..f321fbf 100644 --- a/apps/push_notifications/tasks.py +++ b/apps/push_notifications/tasks.py @@ -11,8 +11,8 @@ def send_push_notification(push_message_uuid): message=push_message.message, priority=push_message.priority, extras=push_message.get_gotify_extras()) - push_message.state = push_message.StateChoices.DONE - push_message.save() + push_message.stats = push_message.StateChoices.DONE + push_message.save(update_fields=["stats"]) @celery_app.task