django-webhook-subscriber 0.4.0__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_webhook_subscriber/__init__.py +7 -1
- django_webhook_subscriber/admin.py +831 -182
- django_webhook_subscriber/apps.py +3 -20
- django_webhook_subscriber/conf.py +11 -24
- django_webhook_subscriber/delivery.py +414 -159
- django_webhook_subscriber/http.py +51 -0
- django_webhook_subscriber/management/commands/webhook.py +169 -0
- django_webhook_subscriber/management/commands/webhook_cache.py +173 -0
- django_webhook_subscriber/management/commands/webhook_logs.py +226 -0
- django_webhook_subscriber/management/commands/webhook_performance_test.py +469 -0
- django_webhook_subscriber/management/commands/webhook_send.py +96 -0
- django_webhook_subscriber/management/commands/webhook_status.py +139 -0
- django_webhook_subscriber/managers.py +36 -14
- django_webhook_subscriber/migrations/0002_remove_webhookregistry_content_type_and_more.py +192 -0
- django_webhook_subscriber/models.py +291 -114
- django_webhook_subscriber/serializers.py +16 -50
- django_webhook_subscriber/tasks.py +434 -56
- django_webhook_subscriber/tests/factories.py +40 -0
- django_webhook_subscriber/tests/settings.py +27 -8
- django_webhook_subscriber/tests/test_delivery.py +453 -190
- django_webhook_subscriber/tests/test_http.py +32 -0
- django_webhook_subscriber/tests/test_managers.py +26 -37
- django_webhook_subscriber/tests/test_models.py +341 -251
- django_webhook_subscriber/tests/test_serializers.py +22 -56
- django_webhook_subscriber/tests/test_tasks.py +477 -189
- django_webhook_subscriber/tests/test_utils.py +98 -94
- django_webhook_subscriber/utils.py +87 -69
- django_webhook_subscriber/validators.py +53 -0
- django_webhook_subscriber-2.0.0.dist-info/METADATA +774 -0
- django_webhook_subscriber-2.0.0.dist-info/RECORD +38 -0
- django_webhook_subscriber/management/commands/check_webhook_tasks.py +0 -113
- django_webhook_subscriber/management/commands/clean_webhook_logs.py +0 -65
- django_webhook_subscriber/management/commands/test_webhook.py +0 -96
- django_webhook_subscriber/signals.py +0 -152
- django_webhook_subscriber/testing.py +0 -14
- django_webhook_subscriber/tests/test_signals.py +0 -268
- django_webhook_subscriber-0.4.0.dist-info/METADATA +0 -448
- django_webhook_subscriber-0.4.0.dist-info/RECORD +0 -33
- {django_webhook_subscriber-0.4.0.dist-info → django_webhook_subscriber-2.0.0.dist-info}/WHEEL +0 -0
- {django_webhook_subscriber-0.4.0.dist-info → django_webhook_subscriber-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {django_webhook_subscriber-0.4.0.dist-info → django_webhook_subscriber-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -1,266 +1,915 @@
|
|
|
1
|
-
"""Admin configuration for Django Webhook Subscriber."""
|
|
1
|
+
"""Updated Admin configuration for Django Webhook Subscriber."""
|
|
2
2
|
|
|
3
3
|
from django.contrib import admin
|
|
4
|
-
from django.
|
|
4
|
+
from django.db import models
|
|
5
|
+
from django.urls import reverse
|
|
5
6
|
from django.utils.html import format_html
|
|
6
|
-
from django.
|
|
7
|
+
from django.utils.safestring import mark_safe
|
|
7
8
|
from django.utils.translation import gettext_lazy as _
|
|
9
|
+
from django.contrib import messages
|
|
10
|
+
|
|
11
|
+
from .models import WebhookDeliveryLog, WebhookSubscriber, WebhookSubscription
|
|
12
|
+
from .delivery import clear_webhook_cache, get_webhook_cache_stats
|
|
13
|
+
from .tasks import test_webhook_connectivity, cleanup_webhook_logs
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Inline Admin Classes
|
|
17
|
+
class WebhookSubscriptionInline(admin.TabularInline):
|
|
18
|
+
"""Inline for managing subscriptions within subscriber admin."""
|
|
19
|
+
|
|
20
|
+
model = WebhookSubscription
|
|
21
|
+
extra = 1
|
|
22
|
+
fields = [
|
|
23
|
+
"event_name",
|
|
24
|
+
"custom_endpoint",
|
|
25
|
+
"is_active",
|
|
26
|
+
"success_rate_display",
|
|
27
|
+
"total_deliveries",
|
|
28
|
+
"consecutive_failures",
|
|
29
|
+
]
|
|
30
|
+
readonly_fields = ["success_rate_display"]
|
|
31
|
+
|
|
32
|
+
def success_rate_display(self, obj):
|
|
33
|
+
"""Display success rate with color coding."""
|
|
34
|
+
if obj.pk and obj.success_rate is not None:
|
|
35
|
+
rate = obj.success_rate
|
|
36
|
+
if rate >= 90:
|
|
37
|
+
color = "green"
|
|
38
|
+
elif rate >= 70:
|
|
39
|
+
color = "orange"
|
|
40
|
+
else:
|
|
41
|
+
color = "red"
|
|
42
|
+
return format_html(
|
|
43
|
+
'<span style="color: {};">{:.1f}%</span>', color, rate
|
|
44
|
+
)
|
|
45
|
+
return "-"
|
|
8
46
|
|
|
9
|
-
|
|
10
|
-
WebhookRegistry,
|
|
11
|
-
WebhookDeliveryLog,
|
|
12
|
-
)
|
|
13
|
-
from django_webhook_subscriber.utils import _webhook_registry
|
|
47
|
+
success_rate_display.short_description = _("Success Rate")
|
|
14
48
|
|
|
15
|
-
|
|
16
|
-
|
|
49
|
+
def get_queryset(self, request):
|
|
50
|
+
return super().get_queryset(request).select_related("subscriber")
|
|
17
51
|
|
|
18
52
|
|
|
19
|
-
class
|
|
20
|
-
"""Inline
|
|
53
|
+
class WebhookDeliveryLogInline(admin.StackedInline):
|
|
54
|
+
"""Inline for recent delivery logs."""
|
|
21
55
|
|
|
22
56
|
model = WebhookDeliveryLog
|
|
23
57
|
extra = 0
|
|
24
|
-
max_num =
|
|
25
|
-
fields = [
|
|
58
|
+
max_num = 5 # Show last 5 deliveries
|
|
59
|
+
fields = [
|
|
60
|
+
"created_at",
|
|
61
|
+
"response_status_display",
|
|
62
|
+
"attempt_number",
|
|
63
|
+
"delivery_duration_ms",
|
|
64
|
+
"error_message_short",
|
|
65
|
+
]
|
|
26
66
|
readonly_fields = [
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
67
|
+
"created_at",
|
|
68
|
+
"response_status_display",
|
|
69
|
+
"attempt_number",
|
|
70
|
+
"delivery_duration_ms",
|
|
71
|
+
"error_message_short",
|
|
31
72
|
]
|
|
32
73
|
can_delete = False
|
|
33
|
-
|
|
74
|
+
ordering = ["-created_at"]
|
|
34
75
|
|
|
35
76
|
def has_add_permission(self, request, obj=None):
|
|
36
77
|
return False
|
|
37
78
|
|
|
38
|
-
def
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
79
|
+
def response_status_display(self, obj):
|
|
80
|
+
"""Display response status with color coding."""
|
|
81
|
+
if obj.response_status:
|
|
82
|
+
if obj.is_success:
|
|
83
|
+
return format_html(
|
|
84
|
+
'<span style="color: green;">✓ {}</span>',
|
|
85
|
+
obj.response_status,
|
|
86
|
+
)
|
|
87
|
+
elif obj.is_client_error:
|
|
88
|
+
return format_html(
|
|
89
|
+
'<span style="color: orange;">⚠ {}</span>',
|
|
90
|
+
obj.response_status,
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
return format_html(
|
|
94
|
+
'<span style="color: red;">✗ {}</span>',
|
|
95
|
+
obj.response_status,
|
|
96
|
+
)
|
|
97
|
+
elif obj.error_message:
|
|
98
|
+
return format_html('<span style="color: red;">✗ Exception</span>')
|
|
99
|
+
return "?"
|
|
100
|
+
|
|
101
|
+
response_status_display.short_description = _("Status")
|
|
102
|
+
|
|
103
|
+
def error_message_short(self, obj):
|
|
104
|
+
"""Truncated error message for inline display."""
|
|
105
|
+
if obj.error_message:
|
|
106
|
+
return (
|
|
107
|
+
obj.error_message[:100] + "..."
|
|
108
|
+
if len(obj.error_message) > 100
|
|
109
|
+
else obj.error_message
|
|
110
|
+
)
|
|
111
|
+
return "-"
|
|
43
112
|
|
|
44
|
-
|
|
45
|
-
return queryset
|
|
113
|
+
error_message_short.short_description = _("Error")
|
|
46
114
|
|
|
47
|
-
ids = queryset.order_by('-created_at').values('pk')[
|
|
48
|
-
:MAX_DISPLAYED_LOGS
|
|
49
|
-
]
|
|
50
115
|
|
|
51
|
-
|
|
52
|
-
|
|
116
|
+
# Main Admin Classes
|
|
117
|
+
@admin.register(WebhookSubscriber)
|
|
118
|
+
class WebhookSubscriberAdmin(admin.ModelAdmin):
|
|
119
|
+
"""Enhanced admin interface for WebhookSubscriber."""
|
|
53
120
|
|
|
121
|
+
list_display = [
|
|
122
|
+
"name",
|
|
123
|
+
"model_display",
|
|
124
|
+
"target_url_display",
|
|
125
|
+
"health_indicator",
|
|
126
|
+
"subscriptions_count",
|
|
127
|
+
"consecutive_failures",
|
|
128
|
+
"last_activity_display",
|
|
129
|
+
"created_at",
|
|
130
|
+
]
|
|
131
|
+
list_display_links = ["name"]
|
|
132
|
+
list_filter = [
|
|
133
|
+
"is_active",
|
|
134
|
+
"content_type",
|
|
135
|
+
"consecutive_failures",
|
|
136
|
+
"created_at",
|
|
137
|
+
"last_success",
|
|
138
|
+
"last_failure",
|
|
139
|
+
]
|
|
140
|
+
search_fields = ["name", "description", "target_url"]
|
|
141
|
+
readonly_fields = [
|
|
142
|
+
"created_at",
|
|
143
|
+
"updated_at",
|
|
144
|
+
"last_success",
|
|
145
|
+
"last_failure",
|
|
146
|
+
"consecutive_failures",
|
|
147
|
+
"model_class_display",
|
|
148
|
+
"subscriptions_summary",
|
|
149
|
+
]
|
|
54
150
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
151
|
+
fieldsets = (
|
|
152
|
+
(
|
|
153
|
+
_("Basic Information"),
|
|
154
|
+
{"fields": ("name", "description", "content_type", "is_active")},
|
|
155
|
+
),
|
|
156
|
+
(
|
|
157
|
+
_("Endpoint Configuration"),
|
|
158
|
+
{
|
|
159
|
+
"fields": (
|
|
160
|
+
"target_url",
|
|
161
|
+
"secret",
|
|
162
|
+
"headers",
|
|
163
|
+
"serializer_class",
|
|
164
|
+
),
|
|
165
|
+
},
|
|
166
|
+
),
|
|
167
|
+
(
|
|
168
|
+
_("Delivery Settings"),
|
|
169
|
+
{
|
|
170
|
+
"fields": (
|
|
171
|
+
"max_retries",
|
|
172
|
+
"retry_delay",
|
|
173
|
+
"timeout",
|
|
174
|
+
"auto_disable_after_failures",
|
|
175
|
+
),
|
|
176
|
+
"classes": ("collapse",),
|
|
177
|
+
},
|
|
178
|
+
),
|
|
179
|
+
(
|
|
180
|
+
_("Status Information"),
|
|
181
|
+
{
|
|
182
|
+
"fields": (
|
|
183
|
+
"consecutive_failures",
|
|
184
|
+
"last_success",
|
|
185
|
+
"last_failure",
|
|
186
|
+
"model_class_display",
|
|
187
|
+
"subscriptions_summary",
|
|
188
|
+
),
|
|
189
|
+
"classes": ("collapse",),
|
|
190
|
+
},
|
|
191
|
+
),
|
|
192
|
+
(
|
|
193
|
+
_("Metadata"),
|
|
194
|
+
{
|
|
195
|
+
"fields": ("created_at", "updated_at"),
|
|
196
|
+
"classes": ("collapse",),
|
|
197
|
+
},
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
inlines = [WebhookSubscriptionInline]
|
|
202
|
+
actions = [
|
|
203
|
+
"activate_subscribers",
|
|
204
|
+
"deactivate_subscribers",
|
|
205
|
+
"test_connectivity_action",
|
|
206
|
+
"reset_failure_counters",
|
|
207
|
+
"clear_cache_for_subscribers",
|
|
208
|
+
]
|
|
58
209
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
210
|
+
def get_queryset(self, request):
|
|
211
|
+
return (
|
|
212
|
+
super()
|
|
213
|
+
.get_queryset(request)
|
|
214
|
+
.select_related("content_type")
|
|
215
|
+
.prefetch_related("subscriptions")
|
|
216
|
+
.annotate(
|
|
217
|
+
active_subscriptions_count=models.Count(
|
|
218
|
+
"subscriptions",
|
|
219
|
+
filter=models.Q(subscriptions__is_active=True),
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
)
|
|
62
223
|
|
|
63
|
-
def
|
|
64
|
-
|
|
224
|
+
def model_display(self, obj):
|
|
225
|
+
"""Display model information with app label."""
|
|
226
|
+
return f"{obj.content_type.app_label}.{obj.content_type.model}"
|
|
227
|
+
|
|
228
|
+
model_display.short_description = _("Model")
|
|
229
|
+
model_display.admin_order_field = "content_type"
|
|
230
|
+
|
|
231
|
+
def target_url_display(self, obj):
|
|
232
|
+
"""Display target URL with link."""
|
|
233
|
+
if obj.target_url:
|
|
234
|
+
return format_html(
|
|
235
|
+
'<a href="{}" target="_blank" title="Open endpoint">{}</a>',
|
|
236
|
+
obj.target_url,
|
|
237
|
+
(
|
|
238
|
+
obj.target_url[:50] + "..."
|
|
239
|
+
if len(obj.target_url) > 50
|
|
240
|
+
else obj.target_url
|
|
241
|
+
),
|
|
242
|
+
)
|
|
243
|
+
return "-"
|
|
65
244
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return content_type
|
|
245
|
+
target_url_display.short_description = _("Target URL")
|
|
246
|
+
target_url_display.admin_order_field = "target_url"
|
|
69
247
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if
|
|
73
|
-
return
|
|
248
|
+
def health_indicator(self, obj):
|
|
249
|
+
"""Visual health indicator with status and failure count."""
|
|
250
|
+
if not obj.is_active:
|
|
251
|
+
return format_html('<span style="color: #999;">● Disabled</span>')
|
|
74
252
|
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
253
|
+
# Health based on consecutive failures and recent activity
|
|
254
|
+
if obj.consecutive_failures == 0:
|
|
255
|
+
if obj.last_success:
|
|
256
|
+
return format_html(
|
|
257
|
+
'<span style="color: green;">● Healthy</span>'
|
|
258
|
+
)
|
|
259
|
+
else:
|
|
260
|
+
return format_html(
|
|
261
|
+
'<span style="color: orange;">● Untested</span>'
|
|
262
|
+
)
|
|
263
|
+
elif obj.consecutive_failures < 5:
|
|
264
|
+
return format_html(
|
|
265
|
+
'<span style="color: orange;">● Warning ({})</span>',
|
|
266
|
+
obj.consecutive_failures,
|
|
267
|
+
)
|
|
268
|
+
else:
|
|
269
|
+
return format_html(
|
|
270
|
+
'<span style="color: red;">● Critical ({})</span>',
|
|
271
|
+
obj.consecutive_failures,
|
|
80
272
|
)
|
|
81
|
-
)
|
|
82
273
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
event_signals = self.cleaned_data.get('event_signals')
|
|
274
|
+
health_indicator.short_description = _("Health")
|
|
275
|
+
health_indicator.admin_order_field = "consecutive_failures"
|
|
86
276
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
277
|
+
def subscriptions_count(self, obj):
|
|
278
|
+
"""Count of active subscriptions with link."""
|
|
279
|
+
count = getattr(obj, "active_subscriptions_count", 0)
|
|
280
|
+
if count > 0:
|
|
281
|
+
url = reverse(
|
|
282
|
+
"admin:django_webhook_subscriber_webhooksubscription_changelist" # noqa: E501
|
|
283
|
+
)
|
|
284
|
+
return format_html(
|
|
285
|
+
'<a href="{}?subscriber__id__exact={}">{}</a>',
|
|
286
|
+
url,
|
|
287
|
+
obj.pk,
|
|
288
|
+
count,
|
|
289
|
+
)
|
|
290
|
+
return "0"
|
|
291
|
+
|
|
292
|
+
subscriptions_count.short_description = _("Active Subscriptions")
|
|
293
|
+
subscriptions_count.admin_order_field = "active_subscriptions_count"
|
|
294
|
+
|
|
295
|
+
def last_activity_display(self, obj):
|
|
296
|
+
"""Display most recent activity (success or failure)."""
|
|
297
|
+
last_activity = None
|
|
298
|
+
activity_type = None
|
|
299
|
+
|
|
300
|
+
if obj.last_success and obj.last_failure:
|
|
301
|
+
if obj.last_success > obj.last_failure:
|
|
302
|
+
last_activity = obj.last_success
|
|
303
|
+
activity_type = "success"
|
|
304
|
+
else:
|
|
305
|
+
last_activity = obj.last_failure
|
|
306
|
+
activity_type = "failure"
|
|
307
|
+
elif obj.last_success:
|
|
308
|
+
last_activity = obj.last_success
|
|
309
|
+
activity_type = "success"
|
|
310
|
+
elif obj.last_failure:
|
|
311
|
+
last_activity = obj.last_failure
|
|
312
|
+
activity_type = "failure"
|
|
313
|
+
|
|
314
|
+
if last_activity:
|
|
315
|
+
color = "green" if activity_type == "success" else "red"
|
|
316
|
+
icon = "✓" if activity_type == "success" else "✗"
|
|
317
|
+
return format_html(
|
|
318
|
+
'<span style="color: {};" title="{}">{} {}</span>',
|
|
319
|
+
color,
|
|
320
|
+
last_activity.strftime("%Y-%m-%d %H:%M:%S"),
|
|
321
|
+
icon,
|
|
322
|
+
last_activity.strftime("%m/%d %H:%M"),
|
|
91
323
|
)
|
|
92
324
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
325
|
+
return format_html('<span style="color: #999;">No activity</span>')
|
|
326
|
+
|
|
327
|
+
last_activity_display.short_description = _("Last Activity")
|
|
328
|
+
|
|
329
|
+
def model_class_display(self, obj):
|
|
330
|
+
"""Display the actual model class information."""
|
|
331
|
+
model_class = obj.model_class
|
|
332
|
+
if model_class:
|
|
333
|
+
return f"{model_class.__module__}.{model_class.__name__}"
|
|
334
|
+
return "Model not found"
|
|
335
|
+
|
|
336
|
+
model_class_display.short_description = _("Model Class")
|
|
337
|
+
|
|
338
|
+
def subscriptions_summary(self, obj):
|
|
339
|
+
"""Summary of all subscriptions with success rates."""
|
|
340
|
+
subscriptions = obj.subscriptions.all()
|
|
341
|
+
if not subscriptions:
|
|
342
|
+
return "No subscriptions"
|
|
343
|
+
|
|
344
|
+
summary_lines = []
|
|
345
|
+
for sub in subscriptions:
|
|
346
|
+
status = "✓" if sub.is_active else "✗"
|
|
347
|
+
rate = (
|
|
348
|
+
f" ({sub.success_rate:.1f}%)"
|
|
349
|
+
if sub.success_rate is not None
|
|
350
|
+
else ""
|
|
115
351
|
)
|
|
352
|
+
summary_lines.append(f"{status} {sub.event_name}{rate}")
|
|
353
|
+
|
|
354
|
+
return mark_safe("<br>".join(summary_lines))
|
|
355
|
+
|
|
356
|
+
subscriptions_summary.short_description = _("Subscriptions Summary")
|
|
357
|
+
|
|
358
|
+
# Custom Actions
|
|
359
|
+
@admin.action(description=_("Activate selected subscribers"))
|
|
360
|
+
def activate_subscribers(self, request, queryset):
|
|
361
|
+
"""Bulk activate subscribers."""
|
|
362
|
+
updated = queryset.update(is_active=True)
|
|
363
|
+
self.message_user(request, f"{updated} subscriber(s) activated.")
|
|
364
|
+
|
|
365
|
+
@admin.action(description=_("Deactivate selected subscribers"))
|
|
366
|
+
def deactivate_subscribers(self, request, queryset):
|
|
367
|
+
"""Bulk deactivate subscribers."""
|
|
368
|
+
updated = queryset.update(is_active=False)
|
|
369
|
+
self.message_user(request, f"{updated} subscriber(s) deactivated.")
|
|
370
|
+
|
|
371
|
+
@admin.action(description=_("Test endpoint connectivity"))
|
|
372
|
+
def test_connectivity_action(self, request, queryset):
|
|
373
|
+
"""Test connectivity to subscriber endpoints."""
|
|
374
|
+
subscriber_ids = list(queryset.values_list("id", flat=True))
|
|
375
|
+
|
|
376
|
+
# Queue the test task
|
|
377
|
+
result = test_webhook_connectivity.delay(subscriber_ids)
|
|
378
|
+
|
|
379
|
+
self.message_user(
|
|
380
|
+
request,
|
|
381
|
+
f"Connectivity test queued for {len(subscriber_ids)} "
|
|
382
|
+
f"subscriber(s). Task ID: {result.id}",
|
|
383
|
+
messages.INFO,
|
|
384
|
+
)
|
|
116
385
|
|
|
117
|
-
|
|
386
|
+
@admin.action(description=_("Reset failure counters"))
|
|
387
|
+
def reset_failure_counters(self, request, queryset):
|
|
388
|
+
"""Reset consecutive failure counters."""
|
|
389
|
+
updated = queryset.update(consecutive_failures=0)
|
|
390
|
+
self.message_user(
|
|
391
|
+
request, f"Reset failure counters for {updated} subscriber(s)."
|
|
392
|
+
)
|
|
118
393
|
|
|
394
|
+
@admin.action(description=_("Clear cache for selected subscribers"))
|
|
395
|
+
def clear_cache_for_subscribers(self, request, queryset):
|
|
396
|
+
"""Clear cache for selected subscribers."""
|
|
397
|
+
for subscriber in queryset:
|
|
398
|
+
clear_webhook_cache(content_type=subscriber.content_type)
|
|
119
399
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
400
|
+
self.message_user(
|
|
401
|
+
request, f"Cache cleared for {queryset.count()} subscriber(s)."
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
@admin.register(WebhookSubscription)
|
|
406
|
+
class WebhookSubscriptionAdmin(admin.ModelAdmin):
|
|
407
|
+
"""Admin interface for WebhookSubscription."""
|
|
123
408
|
|
|
124
|
-
form = WebhookRegistryAdminForm
|
|
125
409
|
list_display = [
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
'webhooks_sent',
|
|
134
|
-
'use_async',
|
|
410
|
+
"subscriber_name",
|
|
411
|
+
"event_name",
|
|
412
|
+
"endpoint_display",
|
|
413
|
+
"status_indicator",
|
|
414
|
+
"performance_display",
|
|
415
|
+
"recent_deliveries",
|
|
416
|
+
"created_at",
|
|
135
417
|
]
|
|
136
|
-
list_display_links = [
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
418
|
+
list_display_links = ["subscriber_name", "event_name"]
|
|
419
|
+
list_filter = [
|
|
420
|
+
"is_active",
|
|
421
|
+
"event_name",
|
|
422
|
+
"subscriber__content_type",
|
|
423
|
+
"subscriber__is_active",
|
|
424
|
+
"consecutive_failures",
|
|
425
|
+
"created_at",
|
|
426
|
+
]
|
|
427
|
+
search_fields = [
|
|
428
|
+
"subscriber__name",
|
|
429
|
+
"event_name",
|
|
430
|
+
"custom_endpoint",
|
|
431
|
+
"subscriber__target_url",
|
|
141
432
|
]
|
|
142
|
-
list_filter = ['is_active', 'content_type', 'created_at']
|
|
143
|
-
search_fields = ['name', 'endpoint']
|
|
144
433
|
readonly_fields = [
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
434
|
+
"created_at",
|
|
435
|
+
"updated_at",
|
|
436
|
+
"full_endpoint_display",
|
|
437
|
+
"success_rate",
|
|
438
|
+
"total_deliveries",
|
|
439
|
+
"successful_deliveries",
|
|
440
|
+
"consecutive_failures",
|
|
150
441
|
]
|
|
151
442
|
|
|
152
|
-
|
|
153
|
-
fieldsets = [
|
|
443
|
+
fieldsets = (
|
|
154
444
|
(
|
|
155
|
-
|
|
445
|
+
_("Subscription Details"),
|
|
446
|
+
{"fields": ("subscriber", "event_name", "is_active")},
|
|
447
|
+
),
|
|
448
|
+
(
|
|
449
|
+
_("Endpoint Configuration"),
|
|
450
|
+
{
|
|
451
|
+
"fields": ("custom_endpoint", "full_endpoint_display"),
|
|
452
|
+
},
|
|
453
|
+
),
|
|
454
|
+
(
|
|
455
|
+
_("Performance Statistics"),
|
|
156
456
|
{
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
457
|
+
"fields": (
|
|
458
|
+
"success_rate",
|
|
459
|
+
"total_deliveries",
|
|
460
|
+
"successful_deliveries",
|
|
461
|
+
"consecutive_failures",
|
|
462
|
+
),
|
|
463
|
+
"classes": ("collapse",),
|
|
164
464
|
},
|
|
165
465
|
),
|
|
166
|
-
(_('Authentication'), {'fields': ['secret']}),
|
|
167
466
|
(
|
|
168
|
-
_(
|
|
467
|
+
_("Response Handling"),
|
|
169
468
|
{
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
'keep_last_response',
|
|
173
|
-
'use_async',
|
|
174
|
-
'max_retries',
|
|
175
|
-
'retry_delay',
|
|
176
|
-
],
|
|
177
|
-
'classes': ['collapse'],
|
|
469
|
+
"fields": ("keep_last_response", "last_response"),
|
|
470
|
+
"classes": ("collapse",),
|
|
178
471
|
},
|
|
179
472
|
),
|
|
180
473
|
(
|
|
181
|
-
_(
|
|
182
|
-
{
|
|
474
|
+
_("Metadata"),
|
|
475
|
+
{
|
|
476
|
+
"fields": ("created_at", "updated_at"),
|
|
477
|
+
"classes": ("collapse",),
|
|
478
|
+
},
|
|
183
479
|
),
|
|
184
|
-
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
inlines = [WebhookDeliveryLogInline]
|
|
483
|
+
actions = [
|
|
484
|
+
"activate_subscriptions",
|
|
485
|
+
"deactivate_subscriptions",
|
|
486
|
+
"reset_subscription_stats",
|
|
487
|
+
"clear_cache_action",
|
|
185
488
|
]
|
|
186
|
-
inlines = [WebhookRegistryDeliverLogInline]
|
|
187
489
|
|
|
188
|
-
def
|
|
189
|
-
return
|
|
490
|
+
def get_queryset(self, request):
|
|
491
|
+
return (
|
|
492
|
+
super()
|
|
493
|
+
.get_queryset(request)
|
|
494
|
+
.select_related("subscriber", "subscriber__content_type")
|
|
495
|
+
.prefetch_related("delivery_logs")
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
def subscriber_name(self, obj):
|
|
499
|
+
"""Display subscriber name with link."""
|
|
500
|
+
url = reverse(
|
|
501
|
+
"admin:django_webhook_subscriber_webhooksubscriber_change",
|
|
502
|
+
args=[obj.subscriber.pk],
|
|
503
|
+
)
|
|
504
|
+
return format_html('<a href="{}">{}</a>', url, obj.subscriber.name)
|
|
505
|
+
|
|
506
|
+
subscriber_name.short_description = _("Subscriber")
|
|
507
|
+
subscriber_name.admin_order_field = "subscriber__name"
|
|
508
|
+
|
|
509
|
+
def endpoint_display(self, obj):
|
|
510
|
+
"""Display the effective endpoint."""
|
|
511
|
+
endpoint = obj.endpoint
|
|
512
|
+
return format_html(
|
|
513
|
+
'<a href="{}" target="_blank" title="Test endpoint">{}</a>',
|
|
514
|
+
endpoint,
|
|
515
|
+
endpoint[:40] + "..." if len(endpoint) > 40 else endpoint,
|
|
516
|
+
)
|
|
190
517
|
|
|
191
|
-
|
|
518
|
+
endpoint_display.short_description = _("Endpoint")
|
|
192
519
|
|
|
193
520
|
def status_indicator(self, obj):
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
521
|
+
"""Visual status indicator."""
|
|
522
|
+
if not obj.is_active:
|
|
523
|
+
return format_html('<span style="color: #999;">● Disabled</span>')
|
|
524
|
+
elif not obj.subscriber.is_active:
|
|
525
|
+
return format_html(
|
|
526
|
+
'<span style="color: orange;">● Subscriber Disabled</span>'
|
|
527
|
+
)
|
|
528
|
+
elif obj.consecutive_failures == 0:
|
|
529
|
+
return format_html('<span style="color: green;">● Active</span>')
|
|
530
|
+
elif obj.consecutive_failures < 3:
|
|
531
|
+
return format_html('<span style="color: orange;">● Warning</span>')
|
|
202
532
|
else:
|
|
203
|
-
return format_html('<span style="color:
|
|
533
|
+
return format_html('<span style="color: red;">● Failing</span>')
|
|
204
534
|
|
|
205
|
-
status_indicator.short_description = _(
|
|
535
|
+
status_indicator.short_description = _("Status")
|
|
206
536
|
|
|
207
|
-
def
|
|
208
|
-
"""
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
537
|
+
def performance_display(self, obj):
|
|
538
|
+
"""Display performance metrics."""
|
|
539
|
+
if obj.total_deliveries == 0:
|
|
540
|
+
return format_html(
|
|
541
|
+
'<span style="color: #999;">No deliveries</span>'
|
|
542
|
+
)
|
|
212
543
|
|
|
213
|
-
|
|
544
|
+
success_rate = obj.success_rate or 0
|
|
545
|
+
if success_rate >= 95:
|
|
546
|
+
color = "green"
|
|
547
|
+
elif success_rate >= 80:
|
|
548
|
+
color = "orange"
|
|
549
|
+
else:
|
|
550
|
+
color = "red"
|
|
551
|
+
|
|
552
|
+
return format_html(
|
|
553
|
+
'<span style="color: {};" title="{} successful / {} total">{}% '
|
|
554
|
+
"({}/{})</span>",
|
|
555
|
+
color,
|
|
556
|
+
obj.successful_deliveries,
|
|
557
|
+
obj.total_deliveries,
|
|
558
|
+
success_rate,
|
|
559
|
+
obj.successful_deliveries,
|
|
560
|
+
obj.total_deliveries,
|
|
561
|
+
)
|
|
214
562
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
563
|
+
performance_display.short_description = _("Performance")
|
|
564
|
+
|
|
565
|
+
def recent_deliveries(self, obj):
|
|
566
|
+
"""Show recent delivery status with timing."""
|
|
567
|
+
recent_logs = obj.delivery_logs.order_by("-created_at")[:5]
|
|
568
|
+
if not recent_logs:
|
|
569
|
+
return format_html('<span style="color: #999;">None</span>')
|
|
570
|
+
|
|
571
|
+
status_icons = []
|
|
572
|
+
for log in recent_logs:
|
|
573
|
+
if log.is_success:
|
|
574
|
+
icon = "✅"
|
|
575
|
+
title = f"Success ({log.response_status})"
|
|
576
|
+
elif log.error_message:
|
|
577
|
+
icon = "❌"
|
|
578
|
+
title = f"Exception: {log.error_message[:50]}"
|
|
579
|
+
elif log.response_status:
|
|
580
|
+
if log.is_client_error:
|
|
581
|
+
icon = "⚠️"
|
|
582
|
+
title = f"Client Error ({log.response_status})"
|
|
583
|
+
else:
|
|
584
|
+
icon = "❌"
|
|
585
|
+
title = f"Server Error ({log.response_status})"
|
|
586
|
+
else:
|
|
587
|
+
icon = "❓"
|
|
588
|
+
title = "Unknown status"
|
|
589
|
+
|
|
590
|
+
# Add timing info if available
|
|
591
|
+
if log.delivery_duration_ms:
|
|
592
|
+
title += f" - {log.delivery_duration_ms}ms"
|
|
593
|
+
|
|
594
|
+
status_icons.append(f'<span title="{title}">{icon}</span>')
|
|
595
|
+
|
|
596
|
+
return format_html(" ".join(status_icons))
|
|
597
|
+
|
|
598
|
+
recent_deliveries.short_description = _("Recent Deliveries")
|
|
599
|
+
|
|
600
|
+
def full_endpoint_display(self, obj):
|
|
601
|
+
"""Display the complete endpoint URL."""
|
|
602
|
+
return obj.endpoint
|
|
603
|
+
|
|
604
|
+
full_endpoint_display.short_description = _("Full Endpoint URL")
|
|
605
|
+
|
|
606
|
+
# Custom Actions
|
|
607
|
+
@admin.action(description=_("Activate selected subscriptions"))
|
|
608
|
+
def activate_subscriptions(self, request, queryset):
|
|
609
|
+
"""Bulk activate subscriptions."""
|
|
610
|
+
updated = queryset.update(is_active=True)
|
|
611
|
+
self.message_user(request, f"{updated} subscription(s) activated.")
|
|
612
|
+
|
|
613
|
+
@admin.action(description=_("Deactivate selected subscriptions"))
|
|
614
|
+
def deactivate_subscriptions(self, request, queryset):
|
|
615
|
+
"""Bulk deactivate subscriptions."""
|
|
616
|
+
updated = queryset.update(is_active=False)
|
|
617
|
+
self.message_user(request, f"{updated} subscription(s) deactivated.")
|
|
618
|
+
|
|
619
|
+
@admin.action(description=_("Reset performance statistics"))
|
|
620
|
+
def reset_subscription_stats(self, request, queryset):
|
|
621
|
+
"""Reset performance statistics for subscriptions."""
|
|
622
|
+
updated = queryset.update(
|
|
623
|
+
consecutive_failures=0, total_deliveries=0, successful_deliveries=0
|
|
624
|
+
)
|
|
625
|
+
self.message_user(
|
|
626
|
+
request, f"Reset statistics for {updated} subscription(s)."
|
|
627
|
+
)
|
|
220
628
|
|
|
221
|
-
@admin.action(description=_(
|
|
222
|
-
def
|
|
223
|
-
"""
|
|
224
|
-
|
|
225
|
-
|
|
629
|
+
@admin.action(description=_("Clear cache for selected subscriptions"))
|
|
630
|
+
def clear_cache_action(self, request, queryset):
|
|
631
|
+
"""Clear cache for selected subscriptions."""
|
|
632
|
+
for sub in queryset:
|
|
633
|
+
clear_webhook_cache(
|
|
634
|
+
content_type=sub.subscriber.content_type,
|
|
635
|
+
event_name=sub.event_name,
|
|
636
|
+
)
|
|
637
|
+
self.message_user(
|
|
638
|
+
request, f"Cache cleared for {queryset.count()} subscription(s)."
|
|
639
|
+
)
|
|
226
640
|
|
|
227
641
|
|
|
228
642
|
@admin.register(WebhookDeliveryLog)
|
|
229
643
|
class WebhookDeliveryLogAdmin(admin.ModelAdmin):
|
|
230
|
-
"""
|
|
644
|
+
"""Comprehensive admin interface for WebhookDeliveryLog."""
|
|
231
645
|
|
|
232
646
|
list_display = [
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
647
|
+
"__str__",
|
|
648
|
+
"subscription_display",
|
|
649
|
+
"event_name_display",
|
|
650
|
+
"response_status_display",
|
|
651
|
+
"performance_display",
|
|
652
|
+
"created_at_display",
|
|
653
|
+
]
|
|
654
|
+
list_display_links = ["__str__", "subscription_display"]
|
|
655
|
+
list_filter = [
|
|
656
|
+
"response_status",
|
|
657
|
+
"subscription__event_name",
|
|
658
|
+
"subscription__subscriber__content_type",
|
|
659
|
+
"is_retry",
|
|
660
|
+
"attempt_number",
|
|
661
|
+
"created_at",
|
|
662
|
+
("delivery_duration_ms", admin.EmptyFieldListFilter),
|
|
238
663
|
]
|
|
239
|
-
list_filter = ['webhook', 'event_signal', 'response_status', 'created_at']
|
|
240
664
|
search_fields = [
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
665
|
+
"subscription__subscriber__name",
|
|
666
|
+
"subscription__event_name",
|
|
667
|
+
"delivery_url",
|
|
668
|
+
"error_message",
|
|
245
669
|
]
|
|
246
670
|
readonly_fields = [
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
671
|
+
"subscription",
|
|
672
|
+
"attempt_number",
|
|
673
|
+
"is_retry",
|
|
674
|
+
"payload_display",
|
|
675
|
+
"response_body_display",
|
|
676
|
+
"response_headers_display",
|
|
677
|
+
"error_message",
|
|
678
|
+
"delivery_url",
|
|
679
|
+
"delivery_duration_ms",
|
|
680
|
+
"created_at",
|
|
254
681
|
]
|
|
255
682
|
|
|
256
|
-
|
|
257
|
-
|
|
683
|
+
fieldsets = (
|
|
684
|
+
(
|
|
685
|
+
_("Delivery Information"),
|
|
686
|
+
{
|
|
687
|
+
"fields": (
|
|
688
|
+
"subscription",
|
|
689
|
+
"delivery_url",
|
|
690
|
+
"attempt_number",
|
|
691
|
+
"is_retry",
|
|
692
|
+
"delivery_duration_ms",
|
|
693
|
+
"created_at",
|
|
694
|
+
)
|
|
695
|
+
},
|
|
696
|
+
),
|
|
697
|
+
(
|
|
698
|
+
_("Request Data"),
|
|
699
|
+
{
|
|
700
|
+
"fields": ("payload_display",),
|
|
701
|
+
"classes": ("collapse",),
|
|
702
|
+
},
|
|
703
|
+
),
|
|
704
|
+
(
|
|
705
|
+
_("Response Data"),
|
|
706
|
+
{
|
|
707
|
+
"fields": (
|
|
708
|
+
"response_status",
|
|
709
|
+
"response_body_display",
|
|
710
|
+
"response_headers_display",
|
|
711
|
+
),
|
|
712
|
+
"classes": ("collapse",),
|
|
713
|
+
},
|
|
714
|
+
),
|
|
715
|
+
(
|
|
716
|
+
_("Error Information"),
|
|
717
|
+
{
|
|
718
|
+
"fields": ("error_message",),
|
|
719
|
+
"classes": ("collapse",),
|
|
720
|
+
},
|
|
721
|
+
),
|
|
722
|
+
)
|
|
258
723
|
|
|
259
|
-
|
|
260
|
-
|
|
724
|
+
actions = ["cleanup_old_logs", "show_cache_stats"]
|
|
725
|
+
date_hierarchy = "created_at"
|
|
261
726
|
|
|
262
727
|
def has_add_permission(self, request):
|
|
728
|
+
"""Delivery logs are created automatically."""
|
|
263
729
|
return False
|
|
264
730
|
|
|
265
731
|
def has_change_permission(self, request, obj=None):
|
|
732
|
+
"""Delivery logs are read-only."""
|
|
266
733
|
return False
|
|
734
|
+
|
|
735
|
+
def subscription_display(self, obj):
|
|
736
|
+
"""Display subscription with link."""
|
|
737
|
+
url = reverse(
|
|
738
|
+
"admin:django_webhook_subscriber_webhooksubscription_change",
|
|
739
|
+
args=[obj.subscription.pk],
|
|
740
|
+
)
|
|
741
|
+
return format_html(
|
|
742
|
+
'<a href="{}">{} - {}</a>',
|
|
743
|
+
url,
|
|
744
|
+
obj.subscription.subscriber.name,
|
|
745
|
+
obj.subscription.event_name,
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
subscription_display.short_description = _("Subscription")
|
|
749
|
+
subscription_display.admin_order_field = "subscription__subscriber__name"
|
|
750
|
+
|
|
751
|
+
def event_name_display(self, obj):
|
|
752
|
+
"""Display event name."""
|
|
753
|
+
return obj.subscription.event_name
|
|
754
|
+
|
|
755
|
+
event_name_display.short_description = _("Event")
|
|
756
|
+
event_name_display.admin_order_field = "subscription__event_name"
|
|
757
|
+
|
|
758
|
+
def response_status_display(self, obj):
|
|
759
|
+
"""Display response status with detailed color coding."""
|
|
760
|
+
if obj.is_success:
|
|
761
|
+
return format_html(
|
|
762
|
+
'<span style="color: green; font-weight: bold;">✅ {} Success'
|
|
763
|
+
"</span>",
|
|
764
|
+
obj.response_status,
|
|
765
|
+
)
|
|
766
|
+
elif obj.error_message:
|
|
767
|
+
return format_html(
|
|
768
|
+
'<span style="color: red; font-weight: bold;">❌ Exception'
|
|
769
|
+
"</span>"
|
|
770
|
+
)
|
|
771
|
+
elif obj.response_status:
|
|
772
|
+
if obj.is_client_error:
|
|
773
|
+
return format_html(
|
|
774
|
+
'<span style="color: orange; font-weight: bold;">⚠️ {} '
|
|
775
|
+
"Client Error</span>",
|
|
776
|
+
obj.response_status,
|
|
777
|
+
)
|
|
778
|
+
elif obj.is_server_error:
|
|
779
|
+
return format_html(
|
|
780
|
+
'<span style="color: red; font-weight: bold;">❌ {} Server '
|
|
781
|
+
"Error</span>",
|
|
782
|
+
obj.response_status,
|
|
783
|
+
)
|
|
784
|
+
else:
|
|
785
|
+
return format_html(
|
|
786
|
+
'<span style="color: purple;">❓ {} Unknown</span>',
|
|
787
|
+
obj.response_status,
|
|
788
|
+
)
|
|
789
|
+
else:
|
|
790
|
+
return format_html(
|
|
791
|
+
'<span style="color: #999;">❓ No Response</span>'
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
response_status_display.short_description = _("Status")
|
|
795
|
+
response_status_display.admin_order_field = "response_status"
|
|
796
|
+
|
|
797
|
+
def performance_display(self, obj):
|
|
798
|
+
"""Display performance metrics."""
|
|
799
|
+
parts = []
|
|
800
|
+
|
|
801
|
+
# Retry info
|
|
802
|
+
if obj.is_retry:
|
|
803
|
+
parts.append(f"Retry #{obj.attempt_number}")
|
|
804
|
+
elif obj.attempt_number > 1:
|
|
805
|
+
parts.append(f"Attempt #{obj.attempt_number}")
|
|
806
|
+
|
|
807
|
+
# Duration
|
|
808
|
+
if obj.delivery_duration_ms is not None:
|
|
809
|
+
if obj.delivery_duration_ms < 1000:
|
|
810
|
+
parts.append(f"{obj.delivery_duration_ms}ms")
|
|
811
|
+
else:
|
|
812
|
+
parts.append(f"{obj.delivery_duration_ms/1000:.1f}s")
|
|
813
|
+
|
|
814
|
+
return " | ".join(parts) if parts else "-"
|
|
815
|
+
|
|
816
|
+
performance_display.short_description = _("Performance")
|
|
817
|
+
|
|
818
|
+
def created_at_display(self, obj):
|
|
819
|
+
"""Display creation time with relative info."""
|
|
820
|
+
return obj.created_at.strftime("%Y-%m-%d %H:%M:%S")
|
|
821
|
+
|
|
822
|
+
created_at_display.short_description = _("Delivered At")
|
|
823
|
+
created_at_display.admin_order_field = "created_at"
|
|
824
|
+
|
|
825
|
+
def payload_display(self, obj):
|
|
826
|
+
"""Display formatted payload."""
|
|
827
|
+
import json
|
|
828
|
+
|
|
829
|
+
try:
|
|
830
|
+
formatted = json.dumps(obj.payload, indent=2, ensure_ascii=False)
|
|
831
|
+
return format_html(
|
|
832
|
+
'<pre style="white-space: pre-wrap; max-height: 300px; '
|
|
833
|
+
'overflow-y: auto; font-size: 12px;">{}</pre>',
|
|
834
|
+
formatted,
|
|
835
|
+
)
|
|
836
|
+
except Exception:
|
|
837
|
+
return str(obj.payload)
|
|
838
|
+
|
|
839
|
+
payload_display.short_description = _("Payload")
|
|
840
|
+
|
|
841
|
+
def response_body_display(self, obj):
|
|
842
|
+
"""Display formatted response body."""
|
|
843
|
+
if obj.response_body:
|
|
844
|
+
# Try to format as JSON if possible
|
|
845
|
+
try:
|
|
846
|
+
import json
|
|
847
|
+
|
|
848
|
+
parsed = json.loads(obj.response_body)
|
|
849
|
+
formatted = json.dumps(parsed, indent=2, ensure_ascii=False)
|
|
850
|
+
content = formatted
|
|
851
|
+
except (json.JSONDecodeError, TypeError):
|
|
852
|
+
content = obj.response_body
|
|
853
|
+
|
|
854
|
+
# Truncate if too long
|
|
855
|
+
if len(content) > 3000:
|
|
856
|
+
content = content[:3000] + "\n... [TRUNCATED]"
|
|
857
|
+
|
|
858
|
+
return format_html(
|
|
859
|
+
'<pre style="white-space: pre-wrap; max-height: 300px; '
|
|
860
|
+
"overflow-y: auto; font-size: 12px; "
|
|
861
|
+
'padding: 10px; border-radius: 4px;">{}</pre>',
|
|
862
|
+
content,
|
|
863
|
+
)
|
|
864
|
+
return "-"
|
|
865
|
+
|
|
866
|
+
response_body_display.short_description = _("Response Body")
|
|
867
|
+
|
|
868
|
+
def response_headers_display(self, obj):
|
|
869
|
+
"""Display formatted response headers."""
|
|
870
|
+
if obj.response_headers:
|
|
871
|
+
import json
|
|
872
|
+
|
|
873
|
+
try:
|
|
874
|
+
formatted = json.dumps(
|
|
875
|
+
obj.response_headers, indent=2, ensure_ascii=False
|
|
876
|
+
)
|
|
877
|
+
return format_html(
|
|
878
|
+
'<pre style="white-space: pre-wrap; font-size: 12px;">{}'
|
|
879
|
+
"</pre>",
|
|
880
|
+
formatted,
|
|
881
|
+
)
|
|
882
|
+
except Exception:
|
|
883
|
+
return str(obj.response_headers)
|
|
884
|
+
return "-"
|
|
885
|
+
|
|
886
|
+
response_headers_display.short_description = _("Response Headers")
|
|
887
|
+
|
|
888
|
+
# Custom Actions
|
|
889
|
+
@admin.action(description=_("Clean up old delivery logs"))
|
|
890
|
+
def cleanup_old_logs(self, request, queryset):
|
|
891
|
+
"""Clean up old delivery logs."""
|
|
892
|
+
result = cleanup_webhook_logs.delay()
|
|
893
|
+
self.message_user(
|
|
894
|
+
request,
|
|
895
|
+
f"Log cleanup task queued. Task ID: {result.id}",
|
|
896
|
+
messages.INFO,
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
@admin.action(description=_("Show webhook cache statistics"))
|
|
900
|
+
def show_cache_stats(self, request, queryset):
|
|
901
|
+
"""Show cache statistics."""
|
|
902
|
+
try:
|
|
903
|
+
stats = get_webhook_cache_stats()
|
|
904
|
+
message = (
|
|
905
|
+
f"Cache Statistics: "
|
|
906
|
+
f"{stats['cached_keys']}/{stats['total_possible_keys']} keys "
|
|
907
|
+
f"cached ({stats['cache_hit_ratio']:.1f}% hit ratio), "
|
|
908
|
+
f"{stats['total_cached_subscriptions']} total subscriptions "
|
|
909
|
+
"cached"
|
|
910
|
+
)
|
|
911
|
+
self.message_user(request, message, messages.INFO)
|
|
912
|
+
except Exception as e:
|
|
913
|
+
self.message_user(
|
|
914
|
+
request, f"Error getting cache stats: {e}", messages.ERROR
|
|
915
|
+
)
|