diff --git a/accounts/admin.py b/accounts/admin.py index c4ca930..9f5c9b1 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -1,8 +1,50 @@ -from django.contrib import admin +from django.contrib import admin, messages from django.contrib.auth.admin import UserAdmin +from django.http import HttpResponseRedirect +from django.urls import reverse from accounts.models import ApiToken, PushSubscription, User +TEST_PAYLOAD = { + "title": "Osiris — test", + "body": "Reminders are working. This is what a dose reminder looks like.", + "url": "/admin/today/", + "tag": "test", +} + + +def send_test_push(request, subscription: PushSubscription) -> bool: + """Push a test notification and report the outcome as an admin message.""" + # Imported here rather than at module scope: tracker.push imports this module's + # models, so a top-level import would be circular. + from tracker.push import push_is_configured, send_to_subscription + + if not push_is_configured(): + messages.error( + request, + "VAPID keys are not set on the server — run manage.py generate_vapid_keys.", + ) + return False + + pk = subscription.pk + if send_to_subscription(subscription, TEST_PAYLOAD): + messages.success(request, f"Test notification sent to {subscription}.") + return True + + # send_to_subscription deletes the row on a 404/410, which is the single most + # useful thing to know here: the device is gone, not merely unreachable. + if not PushSubscription.objects.filter(pk=pk).exists(): + messages.warning( + request, + "That device had discarded its subscription (the push service returned " + "404/410), so it has been removed. Re-enable reminders on the device.", + ) + else: + messages.error( + request, "The push service rejected the message. See the server log." + ) + return False + @admin.register(User) class OsirisUserAdmin(UserAdmin): @@ -15,6 +57,38 @@ class PushSubscriptionAdmin(admin.ModelAdmin): list_display = ["user", "user_agent", "created_at", "last_success_at"] readonly_fields = ["endpoint", "p256dh", "auth", "created_at", "last_success_at"] + actions = ["send_test_notification"] + change_form_template = "admin/accounts/pushsubscription/change_form.html" + + @admin.action(description="Send a test notification to the selected devices") + def send_test_notification(self, request, queryset): + for subscription in queryset: + send_test_push(request, subscription) + + def change_view(self, request, object_id, form_url="", extra_context=None): + """Handle the 'Send test notification' button. + + Intercepted before the form is validated or saved: sending a test is not an + edit, and it should not be blocked by an unrelated validation error. + """ + if request.method == "POST" and "_send_test" in request.POST: + subscription = self.get_object(request, object_id) + if subscription is None: + return self._get_obj_does_not_exist_redirect( + request, self.opts, object_id + ) + + send_test_push(request, subscription) + + # Back to this object so the message is read in context — unless the push + # came back 404/410, in which case the row was deleted as dead. + if PushSubscription.objects.filter(pk=subscription.pk).exists(): + return HttpResponseRedirect(request.get_full_path()) + return HttpResponseRedirect( + reverse("admin:accounts_pushsubscription_changelist") + ) + + return super().change_view(request, object_id, form_url, extra_context) @admin.register(ApiToken) diff --git a/templates/admin/accounts/pushsubscription/change_form.html b/templates/admin/accounts/pushsubscription/change_form.html new file mode 100644 index 0000000..dfe8a47 --- /dev/null +++ b/templates/admin/accounts/pushsubscription/change_form.html @@ -0,0 +1,9 @@ +{% extends "admin/change_form.html" %} + +{% block submit_buttons_bottom %} + {{ block.super }} +