feature/notification-actions #1
4 changed files with 107 additions and 8 deletions
|
|
@ -6,12 +6,19 @@ from django.contrib.admin.options import TO_FIELD_VAR
|
|||
from django.core.exceptions import PermissionDenied
|
||||
from django.template.response import TemplateResponse
|
||||
|
||||
from .models import PushUser, PushMessage, BulkPushMessage
|
||||
from .models import PushUser, PushMessage, BulkPushMessage, NotificationLink
|
||||
|
||||
|
||||
admin.site.register(PushUser)
|
||||
|
||||
|
||||
@admin.register(NotificationLink)
|
||||
class NotificationLinkAdmin(admin.ModelAdmin):
|
||||
list_display = ['code', 'url_template', 'is_active', 'description']
|
||||
list_filter = ['is_active']
|
||||
search_fields = ['code', 'description']
|
||||
|
||||
|
||||
from django import forms
|
||||
from django.shortcuts import render, redirect
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
# Generated by Django 5.2.6 on 2026-08-22 10:59
|
||||
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('push_notifications', '0004_pushmessage_click_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='NotificationLink',
|
||||
fields=[
|
||||
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
|
||||
('code', models.SlugField(max_length=100, unique=True)),
|
||||
('description', models.CharField(blank=True, max_length=255)),
|
||||
('url_template', models.CharField(help_text='Use {object_id} as a placeholder, e.g. https://app.gooyal.ir/ads/{object_id}', max_length=1000)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pushmessage',
|
||||
name='click_object_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pushmessage',
|
||||
name='code',
|
||||
field=models.SlugField(blank=True, max_length=100, null=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -44,6 +44,30 @@ class PushUser(BaseModel):
|
|||
return
|
||||
|
||||
|
||||
class NotificationLink(BaseModel):
|
||||
"""
|
||||
Admin-managed code -> URL mapping for notification click actions. Producer
|
||||
services send a `code` (documented per notification 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.
|
||||
"""
|
||||
code = models.SlugField(max_length=100, unique=True, db_index=True)
|
||||
description = models.CharField(max_length=255, blank=True)
|
||||
url_template = models.CharField(
|
||||
max_length=1000,
|
||||
help_text="Use {object_id} as a placeholder, e.g. https://app.gooyal.ir/ads/{object_id}",
|
||||
)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.code
|
||||
|
||||
def resolve(self, object_id=None):
|
||||
if object_id and '{object_id}' in self.url_template:
|
||||
return self.url_template.format(object_id=object_id)
|
||||
return self.url_template
|
||||
|
||||
|
||||
class PushMessage(BaseModel):
|
||||
# Gotify's reserved extras namespace for client-side actions. Official
|
||||
# clients (Android/iOS/web) open this URL when the notification is tapped:
|
||||
|
|
@ -65,17 +89,38 @@ class PushMessage(BaseModel):
|
|||
priority = models.IntegerField(default=5)
|
||||
extras = models.JSONField(default=dict)
|
||||
click_url = models.URLField(max_length=1000, null=True, blank=True)
|
||||
# Alternative to a raw click_url: a documented per-notification-type code,
|
||||
# resolved against NotificationLink at send time (see get_gotify_extras).
|
||||
# click_object_id is substituted into that code's {object_id} placeholder.
|
||||
code = models.SlugField(max_length=100, null=True, blank=True, db_index=True)
|
||||
click_object_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
stats = models.IntegerField(default=StateChoices.INIT, choices=StateChoices.choices)
|
||||
|
||||
bulk = models.ForeignKey("BulkPushMessage", null=True, blank=True, on_delete=models.PROTECT)
|
||||
|
||||
def get_gotify_extras(self):
|
||||
"""Merge click_url into extras using Gotify's click-action convention,
|
||||
without disturbing any other extras keys the caller already set."""
|
||||
extras = dict(self.extras or {})
|
||||
def resolve_click_url(self):
|
||||
"""An explicit click_url always wins; otherwise resolve via `code`
|
||||
against NotificationLink. Returns None (not an error) if `code` isn't
|
||||
set, or isn't configured/active yet — not every notification is
|
||||
clickable, and a not-yet-configured code shouldn't block delivery."""
|
||||
if self.click_url:
|
||||
return self.click_url
|
||||
if not self.code:
|
||||
return None
|
||||
link = NotificationLink.objects.filter(code=self.code, is_active=True).first()
|
||||
if not link:
|
||||
return None
|
||||
return link.resolve(self.click_object_id)
|
||||
|
||||
def get_gotify_extras(self):
|
||||
"""Merge the resolved click url into extras using Gotify's click-action
|
||||
convention, without disturbing any other extras keys the caller already
|
||||
set."""
|
||||
extras = dict(self.extras or {})
|
||||
click_url = self.resolve_click_url()
|
||||
if click_url:
|
||||
notification_extra = dict(extras.get(self.GOTIFY_CLICK_EXTRA_KEY) or {})
|
||||
notification_extra["click"] = {"url": self.click_url}
|
||||
notification_extra["click"] = {"url": click_url}
|
||||
extras[self.GOTIFY_CLICK_EXTRA_KEY] = notification_extra
|
||||
return extras
|
||||
|
||||
|
|
@ -115,7 +160,9 @@ class BulkPushMessage(BaseModel):
|
|||
message=data[user_uuid].get('message'),
|
||||
priority=data[user_uuid].get('priority', 5),
|
||||
extras=data[user_uuid].get("extras", {}),
|
||||
click_url=data[user_uuid].get("click_url") or None)
|
||||
click_url=data[user_uuid].get("click_url") or None,
|
||||
code=data[user_uuid].get("code") or None,
|
||||
click_object_id=data[user_uuid].get("click_object_id") or None)
|
||||
push_message.send_push()
|
||||
|
||||
self.success_count += 1
|
||||
|
|
@ -146,6 +193,8 @@ class BulkPushMessage(BaseModel):
|
|||
else:
|
||||
extras = {}
|
||||
click_url = str(sh.cell(rowx=row, colx=5).value).strip() if sh.ncols > 5 else ""
|
||||
code = str(sh.cell(rowx=row, colx=6).value).strip() if sh.ncols > 6 else ""
|
||||
click_object_id = str(sh.cell(rowx=row, colx=7).value).strip() if sh.ncols > 7 else ""
|
||||
|
||||
push_dict[user_uuid] = {
|
||||
"title" : title,
|
||||
|
|
@ -153,5 +202,7 @@ class BulkPushMessage(BaseModel):
|
|||
"priority" : priority,
|
||||
"extras" : extras,
|
||||
"click_url": click_url or None,
|
||||
"code": code or None,
|
||||
"click_object_id": click_object_id or None,
|
||||
}
|
||||
return push_dict
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ class PushMessageSerializer(serializers.ModelSerializer):
|
|||
"message",
|
||||
"priority",
|
||||
"extras",
|
||||
"click_url"
|
||||
"click_url",
|
||||
"code",
|
||||
"click_object_id",
|
||||
)
|
||||
read_only_fields = ("push_user","application")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue