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): pass @admin.register(PushSubscription) class PushSubscriptionAdmin(admin.ModelAdmin): """Devices receiving reminders. Delete a row to stop pushing to that device.""" 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) class ApiTokenAdmin(admin.ModelAdmin): """Tokens are created by the Flutter app via /api/auth/login; this is for revoking.""" list_display = ["name", "user", "key", "created_at", "last_used_at"] readonly_fields = ["key", "created_at", "last_used_at"] list_filter = ["user"]