FIX(notifications): action_code accepts dots, PushMessage.stats actually updates

Found by an end-to-end run against a live Gotify container:

- action_code / NotificationLink.action_code were SlugField, which rejects
  dots. Every documented action_code (billboard.approved, escrow.timeout,
  ...) is dotted, so any real request using the catalog got a 400 before
  this fix -- only click_url ever worked. Switched both to CharField with
  a validator that keeps slug's charset but allows ".".
- send_push_notification set push_message.state instead of .stats after a
  successful send, so PushMessage.stats stayed stuck at INIT forever
  regardless of delivery outcome. Fixed the attribute name and scoped the
  save with update_fields.

Verified live: POST with a dotted action_code now resolves through
NotificationLink and lands in Gotify with the correct click.url, and the
PushMessage row flips to DONE.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Ali Asadi 2026-09-01 15:55:17 +03:30
parent 8a69de8ff2
commit a643fee027
3 changed files with 38 additions and 4 deletions

View file

@ -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')]),
),
]

View file

@ -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)

View file

@ -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