arthexis 0.1.13__py3-none-any.whl → 0.1.15__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.
- {arthexis-0.1.13.dist-info → arthexis-0.1.15.dist-info}/METADATA +224 -221
- arthexis-0.1.15.dist-info/RECORD +110 -0
- {arthexis-0.1.13.dist-info → arthexis-0.1.15.dist-info}/licenses/LICENSE +674 -674
- config/__init__.py +5 -5
- config/active_app.py +15 -15
- config/asgi.py +43 -43
- config/auth_app.py +7 -7
- config/celery.py +32 -32
- config/context_processors.py +67 -69
- config/horologia_app.py +7 -7
- config/loadenv.py +11 -11
- config/logging.py +59 -48
- config/middleware.py +25 -25
- config/offline.py +49 -49
- config/settings.py +691 -682
- config/settings_helpers.py +109 -109
- config/urls.py +171 -166
- config/wsgi.py +17 -17
- core/admin.py +3795 -2809
- core/admin_history.py +50 -50
- core/admindocs.py +151 -151
- core/apps.py +356 -272
- core/auto_upgrade.py +57 -57
- core/backends.py +265 -236
- core/changelog.py +342 -0
- core/entity.py +149 -133
- core/environment.py +61 -61
- core/fields.py +168 -168
- core/form_fields.py +75 -75
- core/github_helper.py +188 -25
- core/github_issues.py +178 -172
- core/github_repos.py +72 -0
- core/lcd_screen.py +78 -78
- core/liveupdate.py +25 -25
- core/log_paths.py +114 -100
- core/mailer.py +85 -85
- core/middleware.py +91 -91
- core/models.py +3637 -2795
- core/notifications.py +105 -105
- core/public_wifi.py +267 -227
- core/reference_utils.py +108 -108
- core/release.py +840 -368
- core/rfid_import_export.py +113 -0
- core/sigil_builder.py +149 -149
- core/sigil_context.py +20 -20
- core/sigil_resolver.py +315 -315
- core/system.py +952 -493
- core/tasks.py +408 -394
- core/temp_passwords.py +181 -181
- core/test_system_info.py +186 -139
- core/tests.py +2168 -1521
- core/tests_liveupdate.py +17 -17
- core/urls.py +11 -11
- core/user_data.py +641 -633
- core/views.py +2201 -1417
- core/widgets.py +213 -94
- core/workgroup_urls.py +17 -17
- core/workgroup_views.py +94 -94
- nodes/admin.py +1720 -1161
- nodes/apps.py +87 -85
- nodes/backends.py +160 -160
- nodes/dns.py +203 -203
- nodes/feature_checks.py +133 -133
- nodes/lcd.py +165 -165
- nodes/models.py +1764 -1597
- nodes/reports.py +411 -411
- nodes/rfid_sync.py +195 -0
- nodes/signals.py +18 -0
- nodes/tasks.py +46 -46
- nodes/tests.py +3830 -3116
- nodes/urls.py +15 -14
- nodes/utils.py +121 -105
- nodes/views.py +683 -619
- ocpp/admin.py +948 -948
- ocpp/apps.py +25 -25
- ocpp/consumers.py +1565 -1459
- ocpp/evcs.py +844 -844
- ocpp/evcs_discovery.py +158 -158
- ocpp/models.py +917 -917
- ocpp/reference_utils.py +42 -42
- ocpp/routing.py +11 -11
- ocpp/simulator.py +745 -745
- ocpp/status_display.py +26 -26
- ocpp/store.py +601 -541
- ocpp/tasks.py +31 -31
- ocpp/test_export_import.py +130 -130
- ocpp/test_rfid.py +913 -702
- ocpp/tests.py +4445 -4094
- ocpp/transactions_io.py +189 -189
- ocpp/urls.py +50 -50
- ocpp/views.py +1479 -1251
- pages/admin.py +769 -539
- pages/apps.py +10 -10
- pages/checks.py +40 -40
- pages/context_processors.py +127 -119
- pages/defaults.py +13 -13
- pages/forms.py +198 -198
- pages/middleware.py +209 -153
- pages/models.py +643 -426
- pages/tasks.py +74 -0
- pages/tests.py +3025 -2200
- pages/urls.py +26 -25
- pages/utils.py +23 -12
- pages/views.py +1176 -1128
- arthexis-0.1.13.dist-info/RECORD +0 -105
- nodes/actions.py +0 -70
- {arthexis-0.1.13.dist-info → arthexis-0.1.15.dist-info}/WHEEL +0 -0
- {arthexis-0.1.13.dist-info → arthexis-0.1.15.dist-info}/top_level.txt +0 -0
pages/admin.py
CHANGED
|
@@ -1,539 +1,769 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
from django.contrib
|
|
5
|
-
from django.contrib.sites.
|
|
6
|
-
from django import
|
|
7
|
-
from django
|
|
8
|
-
from
|
|
9
|
-
from django.
|
|
10
|
-
from django.
|
|
11
|
-
from django.
|
|
12
|
-
from django.
|
|
13
|
-
from django.
|
|
14
|
-
from django.
|
|
15
|
-
from django.db.models import
|
|
16
|
-
from
|
|
17
|
-
|
|
18
|
-
import
|
|
19
|
-
from django.
|
|
20
|
-
from django.
|
|
21
|
-
from django.
|
|
22
|
-
|
|
23
|
-
from nodes.models import Node
|
|
24
|
-
from nodes.utils import capture_screenshot, save_screenshot
|
|
25
|
-
|
|
26
|
-
from .forms import UserManualAdminForm
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
"
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
)
|
|
364
|
-
list_filter = ("
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
"path",
|
|
372
|
-
"
|
|
373
|
-
"
|
|
374
|
-
"
|
|
375
|
-
"
|
|
376
|
-
|
|
377
|
-
)
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
"
|
|
381
|
-
"
|
|
382
|
-
"
|
|
383
|
-
"
|
|
384
|
-
"
|
|
385
|
-
"
|
|
386
|
-
"
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from django.contrib import admin, messages
|
|
5
|
+
from django.contrib.sites.admin import SiteAdmin as DjangoSiteAdmin
|
|
6
|
+
from django.contrib.sites.models import Site
|
|
7
|
+
from django import forms
|
|
8
|
+
from django.shortcuts import redirect, render, get_object_or_404
|
|
9
|
+
from django.urls import path, reverse
|
|
10
|
+
from django.utils.html import format_html
|
|
11
|
+
from django.template.response import TemplateResponse
|
|
12
|
+
from django.http import JsonResponse
|
|
13
|
+
from django.utils import timezone
|
|
14
|
+
from django.db.models import Count
|
|
15
|
+
from django.db.models.functions import TruncDate
|
|
16
|
+
from datetime import datetime, time, timedelta
|
|
17
|
+
import ipaddress
|
|
18
|
+
from django.apps import apps as django_apps
|
|
19
|
+
from django.conf import settings
|
|
20
|
+
from django.utils.translation import gettext_lazy as _, ngettext
|
|
21
|
+
from django.core.management import CommandError, call_command
|
|
22
|
+
|
|
23
|
+
from nodes.models import Node
|
|
24
|
+
from nodes.utils import capture_screenshot, save_screenshot
|
|
25
|
+
|
|
26
|
+
from .forms import UserManualAdminForm
|
|
27
|
+
from .utils import landing_leads_supported
|
|
28
|
+
|
|
29
|
+
from .models import (
|
|
30
|
+
SiteBadge,
|
|
31
|
+
Application,
|
|
32
|
+
SiteProxy,
|
|
33
|
+
Module,
|
|
34
|
+
Landing,
|
|
35
|
+
LandingLead,
|
|
36
|
+
RoleLanding,
|
|
37
|
+
Favorite,
|
|
38
|
+
ViewHistory,
|
|
39
|
+
UserManual,
|
|
40
|
+
UserStory,
|
|
41
|
+
)
|
|
42
|
+
from django.contrib.contenttypes.models import ContentType
|
|
43
|
+
from core.user_data import EntityModelAdmin
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_local_app_choices():
|
|
50
|
+
choices = []
|
|
51
|
+
for app_label in getattr(settings, "LOCAL_APPS", []):
|
|
52
|
+
try:
|
|
53
|
+
config = django_apps.get_app_config(app_label)
|
|
54
|
+
except LookupError:
|
|
55
|
+
continue
|
|
56
|
+
choices.append((config.label, config.verbose_name))
|
|
57
|
+
return choices
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SiteBadgeInline(admin.StackedInline):
|
|
61
|
+
model = SiteBadge
|
|
62
|
+
can_delete = False
|
|
63
|
+
extra = 0
|
|
64
|
+
fields = ("favicon", "landing_override")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SiteForm(forms.ModelForm):
|
|
68
|
+
name = forms.CharField(required=False)
|
|
69
|
+
|
|
70
|
+
class Meta:
|
|
71
|
+
model = Site
|
|
72
|
+
fields = "__all__"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SiteAdmin(DjangoSiteAdmin):
|
|
76
|
+
form = SiteForm
|
|
77
|
+
inlines = [SiteBadgeInline]
|
|
78
|
+
change_list_template = "admin/sites/site/change_list.html"
|
|
79
|
+
fields = ("domain", "name")
|
|
80
|
+
list_display = ("domain", "name")
|
|
81
|
+
actions = ["capture_screenshot"]
|
|
82
|
+
|
|
83
|
+
@admin.action(description="Capture screenshot")
|
|
84
|
+
def capture_screenshot(self, request, queryset):
|
|
85
|
+
node = Node.get_local()
|
|
86
|
+
for site in queryset:
|
|
87
|
+
url = f"http://{site.domain}/"
|
|
88
|
+
try:
|
|
89
|
+
path = capture_screenshot(url)
|
|
90
|
+
screenshot = save_screenshot(path, node=node, method="ADMIN")
|
|
91
|
+
except Exception as exc: # pragma: no cover - browser issues
|
|
92
|
+
self.message_user(request, f"{site.domain}: {exc}", messages.ERROR)
|
|
93
|
+
continue
|
|
94
|
+
if screenshot:
|
|
95
|
+
link = reverse("admin:nodes_contentsample_change", args=[screenshot.pk])
|
|
96
|
+
self.message_user(
|
|
97
|
+
request,
|
|
98
|
+
format_html(
|
|
99
|
+
'Screenshot for {} saved. <a href="{}">View</a>',
|
|
100
|
+
site.domain,
|
|
101
|
+
link,
|
|
102
|
+
),
|
|
103
|
+
messages.SUCCESS,
|
|
104
|
+
)
|
|
105
|
+
else:
|
|
106
|
+
self.message_user(
|
|
107
|
+
request,
|
|
108
|
+
f"{site.domain}: duplicate screenshot; not saved",
|
|
109
|
+
messages.INFO,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def _reload_site_fixtures(self, request):
|
|
113
|
+
fixtures_dir = Path(settings.BASE_DIR) / "core" / "fixtures"
|
|
114
|
+
fixture_paths = sorted(fixtures_dir.glob("references__00_site_*.json"))
|
|
115
|
+
sigil_fixture = fixtures_dir / "sigil_roots__site.json"
|
|
116
|
+
if sigil_fixture.exists():
|
|
117
|
+
fixture_paths.append(sigil_fixture)
|
|
118
|
+
|
|
119
|
+
if not fixture_paths:
|
|
120
|
+
self.message_user(request, _("No site fixtures found."), messages.WARNING)
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
loaded = 0
|
|
124
|
+
for path in fixture_paths:
|
|
125
|
+
try:
|
|
126
|
+
call_command("loaddata", str(path), verbosity=0)
|
|
127
|
+
except CommandError as exc:
|
|
128
|
+
self.message_user(
|
|
129
|
+
request,
|
|
130
|
+
_("%(fixture)s: %(error)s")
|
|
131
|
+
% {"fixture": path.name, "error": exc},
|
|
132
|
+
messages.ERROR,
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
loaded += 1
|
|
136
|
+
|
|
137
|
+
if loaded:
|
|
138
|
+
message = ngettext(
|
|
139
|
+
"Reloaded %(count)d site fixture.",
|
|
140
|
+
"Reloaded %(count)d site fixtures.",
|
|
141
|
+
loaded,
|
|
142
|
+
) % {"count": loaded}
|
|
143
|
+
self.message_user(request, message, messages.SUCCESS)
|
|
144
|
+
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
def reload_site_fixtures(self, request):
|
|
148
|
+
if request.method != "POST":
|
|
149
|
+
return redirect("..")
|
|
150
|
+
|
|
151
|
+
self._reload_site_fixtures(request)
|
|
152
|
+
|
|
153
|
+
return redirect("..")
|
|
154
|
+
|
|
155
|
+
def get_urls(self):
|
|
156
|
+
urls = super().get_urls()
|
|
157
|
+
custom = [
|
|
158
|
+
path(
|
|
159
|
+
"register-current/",
|
|
160
|
+
self.admin_site.admin_view(self.register_current),
|
|
161
|
+
name="pages_siteproxy_register_current",
|
|
162
|
+
),
|
|
163
|
+
path(
|
|
164
|
+
"reload-site-fixtures/",
|
|
165
|
+
self.admin_site.admin_view(self.reload_site_fixtures),
|
|
166
|
+
name="pages_siteproxy_reload_site_fixtures",
|
|
167
|
+
),
|
|
168
|
+
]
|
|
169
|
+
return custom + urls
|
|
170
|
+
|
|
171
|
+
def register_current(self, request):
|
|
172
|
+
domain = request.get_host().split(":")[0]
|
|
173
|
+
try:
|
|
174
|
+
ipaddress.ip_address(domain)
|
|
175
|
+
except ValueError:
|
|
176
|
+
name = domain
|
|
177
|
+
else:
|
|
178
|
+
name = ""
|
|
179
|
+
site, created = Site.objects.get_or_create(
|
|
180
|
+
domain=domain, defaults={"name": name}
|
|
181
|
+
)
|
|
182
|
+
if created:
|
|
183
|
+
self.message_user(request, "Current domain registered", messages.SUCCESS)
|
|
184
|
+
else:
|
|
185
|
+
self.message_user(
|
|
186
|
+
request, "Current domain already registered", messages.INFO
|
|
187
|
+
)
|
|
188
|
+
return redirect("..")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
admin.site.unregister(Site)
|
|
192
|
+
admin.site.register(SiteProxy, SiteAdmin)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class ApplicationForm(forms.ModelForm):
|
|
196
|
+
name = forms.ChoiceField(choices=[])
|
|
197
|
+
|
|
198
|
+
class Meta:
|
|
199
|
+
model = Application
|
|
200
|
+
fields = "__all__"
|
|
201
|
+
|
|
202
|
+
def __init__(self, *args, **kwargs):
|
|
203
|
+
super().__init__(*args, **kwargs)
|
|
204
|
+
self.fields["name"].choices = get_local_app_choices()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class ApplicationModuleInline(admin.TabularInline):
|
|
208
|
+
model = Module
|
|
209
|
+
fk_name = "application"
|
|
210
|
+
extra = 0
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@admin.register(Application)
|
|
214
|
+
class ApplicationAdmin(EntityModelAdmin):
|
|
215
|
+
form = ApplicationForm
|
|
216
|
+
list_display = ("name", "app_verbose_name", "description", "installed")
|
|
217
|
+
readonly_fields = ("installed",)
|
|
218
|
+
inlines = [ApplicationModuleInline]
|
|
219
|
+
|
|
220
|
+
@admin.display(description="Verbose name")
|
|
221
|
+
def app_verbose_name(self, obj):
|
|
222
|
+
return obj.verbose_name
|
|
223
|
+
|
|
224
|
+
@admin.display(boolean=True)
|
|
225
|
+
def installed(self, obj):
|
|
226
|
+
return obj.installed
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class LandingInline(admin.TabularInline):
|
|
230
|
+
model = Landing
|
|
231
|
+
extra = 0
|
|
232
|
+
fields = ("path", "label", "enabled", "description")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@admin.register(Module)
|
|
236
|
+
class ModuleAdmin(EntityModelAdmin):
|
|
237
|
+
list_display = ("application", "node_role", "path", "menu", "is_default")
|
|
238
|
+
list_filter = ("node_role", "application")
|
|
239
|
+
fields = ("node_role", "application", "path", "menu", "is_default", "favicon")
|
|
240
|
+
inlines = [LandingInline]
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@admin.register(LandingLead)
|
|
244
|
+
class LandingLeadAdmin(EntityModelAdmin):
|
|
245
|
+
list_display = (
|
|
246
|
+
"landing_label",
|
|
247
|
+
"landing_path",
|
|
248
|
+
"status",
|
|
249
|
+
"user",
|
|
250
|
+
"referer_display",
|
|
251
|
+
"created_on",
|
|
252
|
+
)
|
|
253
|
+
list_filter = (
|
|
254
|
+
"status",
|
|
255
|
+
"landing__module__node_role",
|
|
256
|
+
"landing__module__application",
|
|
257
|
+
)
|
|
258
|
+
search_fields = (
|
|
259
|
+
"landing__label",
|
|
260
|
+
"landing__path",
|
|
261
|
+
"referer",
|
|
262
|
+
"path",
|
|
263
|
+
"user__username",
|
|
264
|
+
"user__email",
|
|
265
|
+
)
|
|
266
|
+
readonly_fields = (
|
|
267
|
+
"landing",
|
|
268
|
+
"user",
|
|
269
|
+
"path",
|
|
270
|
+
"referer",
|
|
271
|
+
"user_agent",
|
|
272
|
+
"ip_address",
|
|
273
|
+
"created_on",
|
|
274
|
+
)
|
|
275
|
+
fields = (
|
|
276
|
+
"landing",
|
|
277
|
+
"user",
|
|
278
|
+
"path",
|
|
279
|
+
"referer",
|
|
280
|
+
"user_agent",
|
|
281
|
+
"ip_address",
|
|
282
|
+
"status",
|
|
283
|
+
"assign_to",
|
|
284
|
+
"created_on",
|
|
285
|
+
)
|
|
286
|
+
list_select_related = ("landing", "landing__module", "landing__module__application")
|
|
287
|
+
ordering = ("-created_on",)
|
|
288
|
+
date_hierarchy = "created_on"
|
|
289
|
+
|
|
290
|
+
def changelist_view(self, request, extra_context=None):
|
|
291
|
+
if not landing_leads_supported():
|
|
292
|
+
self.message_user(
|
|
293
|
+
request,
|
|
294
|
+
_(
|
|
295
|
+
"Landing leads are not being recorded because Celery is not running on this node."
|
|
296
|
+
),
|
|
297
|
+
messages.WARNING,
|
|
298
|
+
)
|
|
299
|
+
return super().changelist_view(request, extra_context=extra_context)
|
|
300
|
+
|
|
301
|
+
@admin.display(description=_("Landing"), ordering="landing__label")
|
|
302
|
+
def landing_label(self, obj):
|
|
303
|
+
return obj.landing.label
|
|
304
|
+
|
|
305
|
+
@admin.display(description=_("Path"), ordering="landing__path")
|
|
306
|
+
def landing_path(self, obj):
|
|
307
|
+
return obj.landing.path
|
|
308
|
+
|
|
309
|
+
@admin.display(description=_("Referrer"))
|
|
310
|
+
def referer_display(self, obj):
|
|
311
|
+
return obj.referer or ""
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@admin.register(RoleLanding)
|
|
315
|
+
class RoleLandingAdmin(EntityModelAdmin):
|
|
316
|
+
list_display = (
|
|
317
|
+
"target_display",
|
|
318
|
+
"landing_path",
|
|
319
|
+
"landing_label",
|
|
320
|
+
"priority",
|
|
321
|
+
"is_seed_data",
|
|
322
|
+
)
|
|
323
|
+
list_filter = ("node_role", "security_group")
|
|
324
|
+
search_fields = (
|
|
325
|
+
"node_role__name",
|
|
326
|
+
"security_group__name",
|
|
327
|
+
"user__username",
|
|
328
|
+
"landing__path",
|
|
329
|
+
"landing__label",
|
|
330
|
+
)
|
|
331
|
+
fields = ("node_role", "security_group", "user", "priority", "landing")
|
|
332
|
+
list_select_related = (
|
|
333
|
+
"node_role",
|
|
334
|
+
"security_group",
|
|
335
|
+
"user",
|
|
336
|
+
"landing",
|
|
337
|
+
"landing__module",
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
@admin.display(description="Landing Path")
|
|
341
|
+
def landing_path(self, obj):
|
|
342
|
+
return obj.landing.path if obj.landing_id else ""
|
|
343
|
+
|
|
344
|
+
@admin.display(description="Landing Label")
|
|
345
|
+
def landing_label(self, obj):
|
|
346
|
+
return obj.landing.label if obj.landing_id else ""
|
|
347
|
+
|
|
348
|
+
@admin.display(description="Target", ordering="priority")
|
|
349
|
+
def target_display(self, obj):
|
|
350
|
+
if obj.node_role_id:
|
|
351
|
+
return obj.node_role.name
|
|
352
|
+
if obj.security_group_id:
|
|
353
|
+
return obj.security_group.name
|
|
354
|
+
if obj.user_id:
|
|
355
|
+
return obj.user.get_username()
|
|
356
|
+
return ""
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@admin.register(UserManual)
|
|
360
|
+
class UserManualAdmin(EntityModelAdmin):
|
|
361
|
+
form = UserManualAdminForm
|
|
362
|
+
list_display = ("title", "slug", "languages", "is_seed_data", "is_user_data")
|
|
363
|
+
search_fields = ("title", "slug", "description")
|
|
364
|
+
list_filter = ("is_seed_data", "is_user_data")
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
@admin.register(ViewHistory)
|
|
368
|
+
class ViewHistoryAdmin(EntityModelAdmin):
|
|
369
|
+
date_hierarchy = "visited_at"
|
|
370
|
+
list_display = (
|
|
371
|
+
"path",
|
|
372
|
+
"status_code",
|
|
373
|
+
"status_text",
|
|
374
|
+
"method",
|
|
375
|
+
"visited_at",
|
|
376
|
+
)
|
|
377
|
+
list_filter = ("method", "status_code")
|
|
378
|
+
search_fields = ("path", "error_message", "view_name", "status_text")
|
|
379
|
+
readonly_fields = (
|
|
380
|
+
"path",
|
|
381
|
+
"method",
|
|
382
|
+
"status_code",
|
|
383
|
+
"status_text",
|
|
384
|
+
"error_message",
|
|
385
|
+
"view_name",
|
|
386
|
+
"visited_at",
|
|
387
|
+
)
|
|
388
|
+
ordering = ("-visited_at",)
|
|
389
|
+
change_list_template = "admin/pages/viewhistory/change_list.html"
|
|
390
|
+
actions = ["view_traffic_graph"]
|
|
391
|
+
|
|
392
|
+
def has_add_permission(self, request):
|
|
393
|
+
return False
|
|
394
|
+
|
|
395
|
+
@admin.action(description="View traffic graph")
|
|
396
|
+
def view_traffic_graph(self, request, queryset):
|
|
397
|
+
return redirect("admin:pages_viewhistory_traffic_graph")
|
|
398
|
+
|
|
399
|
+
def get_urls(self):
|
|
400
|
+
urls = super().get_urls()
|
|
401
|
+
custom = [
|
|
402
|
+
path(
|
|
403
|
+
"traffic-graph/",
|
|
404
|
+
self.admin_site.admin_view(self.traffic_graph_view),
|
|
405
|
+
name="pages_viewhistory_traffic_graph",
|
|
406
|
+
),
|
|
407
|
+
path(
|
|
408
|
+
"traffic-data/",
|
|
409
|
+
self.admin_site.admin_view(self.traffic_data_view),
|
|
410
|
+
name="pages_viewhistory_traffic_data",
|
|
411
|
+
),
|
|
412
|
+
]
|
|
413
|
+
return custom + urls
|
|
414
|
+
|
|
415
|
+
def traffic_graph_view(self, request):
|
|
416
|
+
context = {
|
|
417
|
+
**self.admin_site.each_context(request),
|
|
418
|
+
"opts": self.model._meta,
|
|
419
|
+
"title": "Public site traffic",
|
|
420
|
+
"chart_endpoint": reverse("admin:pages_viewhistory_traffic_data"),
|
|
421
|
+
}
|
|
422
|
+
return TemplateResponse(
|
|
423
|
+
request,
|
|
424
|
+
"admin/pages/viewhistory/traffic_graph.html",
|
|
425
|
+
context,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
def traffic_data_view(self, request):
|
|
429
|
+
return JsonResponse(self._build_chart_data())
|
|
430
|
+
|
|
431
|
+
def _build_chart_data(self, days: int = 30, max_pages: int = 8) -> dict:
|
|
432
|
+
end_date = timezone.localdate()
|
|
433
|
+
start_date = end_date - timedelta(days=days - 1)
|
|
434
|
+
|
|
435
|
+
start_at = datetime.combine(start_date, time.min)
|
|
436
|
+
end_at = datetime.combine(end_date + timedelta(days=1), time.min)
|
|
437
|
+
|
|
438
|
+
if settings.USE_TZ:
|
|
439
|
+
current_tz = timezone.get_current_timezone()
|
|
440
|
+
start_at = timezone.make_aware(start_at, current_tz)
|
|
441
|
+
end_at = timezone.make_aware(end_at, current_tz)
|
|
442
|
+
trunc_expression = TruncDate("visited_at", tzinfo=current_tz)
|
|
443
|
+
else:
|
|
444
|
+
trunc_expression = TruncDate("visited_at")
|
|
445
|
+
|
|
446
|
+
queryset = ViewHistory.objects.filter(
|
|
447
|
+
visited_at__gte=start_at, visited_at__lt=end_at
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
meta = {
|
|
451
|
+
"start": start_date.isoformat(),
|
|
452
|
+
"end": end_date.isoformat(),
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if not queryset.exists():
|
|
456
|
+
meta["pages"] = []
|
|
457
|
+
return {"labels": [], "datasets": [], "meta": meta}
|
|
458
|
+
|
|
459
|
+
top_paths = list(
|
|
460
|
+
queryset.values("path")
|
|
461
|
+
.annotate(total=Count("id"))
|
|
462
|
+
.order_by("-total")[:max_pages]
|
|
463
|
+
)
|
|
464
|
+
paths = [entry["path"] for entry in top_paths]
|
|
465
|
+
meta["pages"] = paths
|
|
466
|
+
|
|
467
|
+
labels = [
|
|
468
|
+
(start_date + timedelta(days=offset)).isoformat() for offset in range(days)
|
|
469
|
+
]
|
|
470
|
+
|
|
471
|
+
aggregates = (
|
|
472
|
+
queryset.filter(path__in=paths)
|
|
473
|
+
.annotate(day=trunc_expression)
|
|
474
|
+
.values("day", "path")
|
|
475
|
+
.order_by("day")
|
|
476
|
+
.annotate(total=Count("id"))
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
counts: dict[str, dict[str, int]] = {
|
|
480
|
+
path: {label: 0 for label in labels} for path in paths
|
|
481
|
+
}
|
|
482
|
+
for row in aggregates:
|
|
483
|
+
day = row["day"].isoformat()
|
|
484
|
+
path = row["path"]
|
|
485
|
+
if day in counts.get(path, {}):
|
|
486
|
+
counts[path][day] = row["total"]
|
|
487
|
+
|
|
488
|
+
palette = [
|
|
489
|
+
"#1f77b4",
|
|
490
|
+
"#ff7f0e",
|
|
491
|
+
"#2ca02c",
|
|
492
|
+
"#d62728",
|
|
493
|
+
"#9467bd",
|
|
494
|
+
"#8c564b",
|
|
495
|
+
"#e377c2",
|
|
496
|
+
"#7f7f7f",
|
|
497
|
+
"#bcbd22",
|
|
498
|
+
"#17becf",
|
|
499
|
+
]
|
|
500
|
+
datasets = []
|
|
501
|
+
for index, path in enumerate(paths):
|
|
502
|
+
color = palette[index % len(palette)]
|
|
503
|
+
datasets.append(
|
|
504
|
+
{
|
|
505
|
+
"label": path,
|
|
506
|
+
"data": [counts[path][label] for label in labels],
|
|
507
|
+
"borderColor": color,
|
|
508
|
+
"backgroundColor": color,
|
|
509
|
+
"fill": False,
|
|
510
|
+
"tension": 0.3,
|
|
511
|
+
}
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
return {"labels": labels, "datasets": datasets, "meta": meta}
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
@admin.register(UserStory)
|
|
518
|
+
class UserStoryAdmin(EntityModelAdmin):
|
|
519
|
+
date_hierarchy = "submitted_at"
|
|
520
|
+
actions = ["create_github_issues"]
|
|
521
|
+
list_display = (
|
|
522
|
+
"name",
|
|
523
|
+
"rating",
|
|
524
|
+
"path",
|
|
525
|
+
"submitted_at",
|
|
526
|
+
"github_issue_display",
|
|
527
|
+
"take_screenshot",
|
|
528
|
+
"owner",
|
|
529
|
+
)
|
|
530
|
+
list_filter = ("rating", "submitted_at", "take_screenshot")
|
|
531
|
+
search_fields = ("name", "comments", "path", "github_issue_url")
|
|
532
|
+
readonly_fields = (
|
|
533
|
+
"name",
|
|
534
|
+
"rating",
|
|
535
|
+
"comments",
|
|
536
|
+
"take_screenshot",
|
|
537
|
+
"path",
|
|
538
|
+
"user",
|
|
539
|
+
"owner",
|
|
540
|
+
"submitted_at",
|
|
541
|
+
"github_issue_number",
|
|
542
|
+
"github_issue_url",
|
|
543
|
+
)
|
|
544
|
+
ordering = ("-submitted_at",)
|
|
545
|
+
fields = (
|
|
546
|
+
"name",
|
|
547
|
+
"rating",
|
|
548
|
+
"comments",
|
|
549
|
+
"take_screenshot",
|
|
550
|
+
"path",
|
|
551
|
+
"user",
|
|
552
|
+
"owner",
|
|
553
|
+
"submitted_at",
|
|
554
|
+
"github_issue_number",
|
|
555
|
+
"github_issue_url",
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
@admin.display(description=_("GitHub issue"), ordering="github_issue_number")
|
|
559
|
+
def github_issue_display(self, obj):
|
|
560
|
+
if obj.github_issue_url:
|
|
561
|
+
label = (
|
|
562
|
+
f"#{obj.github_issue_number}"
|
|
563
|
+
if obj.github_issue_number is not None
|
|
564
|
+
else obj.github_issue_url
|
|
565
|
+
)
|
|
566
|
+
return format_html(
|
|
567
|
+
'<a href="{}" target="_blank" rel="noopener noreferrer">{}</a>',
|
|
568
|
+
obj.github_issue_url,
|
|
569
|
+
label,
|
|
570
|
+
)
|
|
571
|
+
if obj.github_issue_number is not None:
|
|
572
|
+
return f"#{obj.github_issue_number}"
|
|
573
|
+
return _("Not created")
|
|
574
|
+
|
|
575
|
+
@admin.action(description=_("Create GitHub issues"))
|
|
576
|
+
def create_github_issues(self, request, queryset):
|
|
577
|
+
created = 0
|
|
578
|
+
skipped = 0
|
|
579
|
+
|
|
580
|
+
for story in queryset:
|
|
581
|
+
if story.github_issue_url:
|
|
582
|
+
skipped += 1
|
|
583
|
+
continue
|
|
584
|
+
|
|
585
|
+
try:
|
|
586
|
+
issue_url = story.create_github_issue()
|
|
587
|
+
except Exception as exc: # pragma: no cover - network/runtime errors
|
|
588
|
+
logger.exception("Failed to create GitHub issue for UserStory %s", story.pk)
|
|
589
|
+
self.message_user(
|
|
590
|
+
request,
|
|
591
|
+
_("Unable to create a GitHub issue for %(story)s: %(error)s")
|
|
592
|
+
% {"story": story, "error": exc},
|
|
593
|
+
messages.ERROR,
|
|
594
|
+
)
|
|
595
|
+
continue
|
|
596
|
+
|
|
597
|
+
if issue_url:
|
|
598
|
+
created += 1
|
|
599
|
+
else:
|
|
600
|
+
skipped += 1
|
|
601
|
+
|
|
602
|
+
if created:
|
|
603
|
+
self.message_user(
|
|
604
|
+
request,
|
|
605
|
+
ngettext(
|
|
606
|
+
"Created %(count)d GitHub issue.",
|
|
607
|
+
"Created %(count)d GitHub issues.",
|
|
608
|
+
created,
|
|
609
|
+
)
|
|
610
|
+
% {"count": created},
|
|
611
|
+
messages.SUCCESS,
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
if skipped:
|
|
615
|
+
self.message_user(
|
|
616
|
+
request,
|
|
617
|
+
ngettext(
|
|
618
|
+
"Skipped %(count)d feedback item (issue already exists or was throttled).",
|
|
619
|
+
"Skipped %(count)d feedback items (issues already exist or were throttled).",
|
|
620
|
+
skipped,
|
|
621
|
+
)
|
|
622
|
+
% {"count": skipped},
|
|
623
|
+
messages.INFO,
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
def has_add_permission(self, request):
|
|
627
|
+
return False
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def favorite_toggle(request, ct_id):
|
|
631
|
+
ct = get_object_or_404(ContentType, pk=ct_id)
|
|
632
|
+
fav = Favorite.objects.filter(user=request.user, content_type=ct).first()
|
|
633
|
+
next_url = request.GET.get("next")
|
|
634
|
+
if fav:
|
|
635
|
+
return redirect(next_url or "admin:favorite_list")
|
|
636
|
+
if request.method == "POST":
|
|
637
|
+
label = request.POST.get("custom_label", "").strip()
|
|
638
|
+
user_data = request.POST.get("user_data") == "on"
|
|
639
|
+
Favorite.objects.create(
|
|
640
|
+
user=request.user,
|
|
641
|
+
content_type=ct,
|
|
642
|
+
custom_label=label,
|
|
643
|
+
user_data=user_data,
|
|
644
|
+
)
|
|
645
|
+
return redirect(next_url or "admin:index")
|
|
646
|
+
return render(
|
|
647
|
+
request,
|
|
648
|
+
"admin/favorite_confirm.html",
|
|
649
|
+
{"content_type": ct, "next": next_url},
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def favorite_list(request):
|
|
654
|
+
favorites = Favorite.objects.filter(user=request.user).select_related(
|
|
655
|
+
"content_type"
|
|
656
|
+
)
|
|
657
|
+
if request.method == "POST":
|
|
658
|
+
selected = request.POST.getlist("user_data")
|
|
659
|
+
for fav in favorites:
|
|
660
|
+
fav.user_data = str(fav.pk) in selected
|
|
661
|
+
fav.save(update_fields=["user_data"])
|
|
662
|
+
return redirect("admin:favorite_list")
|
|
663
|
+
return render(request, "admin/favorite_list.html", {"favorites": favorites})
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def favorite_delete(request, pk):
|
|
667
|
+
fav = get_object_or_404(Favorite, pk=pk, user=request.user)
|
|
668
|
+
fav.delete()
|
|
669
|
+
return redirect("admin:favorite_list")
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def favorite_clear(request):
|
|
673
|
+
Favorite.objects.filter(user=request.user).delete()
|
|
674
|
+
return redirect("admin:favorite_list")
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def log_viewer(request):
|
|
678
|
+
logs_dir = Path(settings.BASE_DIR) / "logs"
|
|
679
|
+
logs_exist = logs_dir.exists() and logs_dir.is_dir()
|
|
680
|
+
available_logs = []
|
|
681
|
+
if logs_exist:
|
|
682
|
+
available_logs = sorted(
|
|
683
|
+
[
|
|
684
|
+
entry.name
|
|
685
|
+
for entry in logs_dir.iterdir()
|
|
686
|
+
if entry.is_file() and not entry.name.startswith(".")
|
|
687
|
+
],
|
|
688
|
+
key=str.lower,
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
selected_log = request.GET.get("log", "")
|
|
692
|
+
log_content = ""
|
|
693
|
+
log_error = ""
|
|
694
|
+
|
|
695
|
+
if selected_log:
|
|
696
|
+
if selected_log in available_logs:
|
|
697
|
+
selected_path = logs_dir / selected_log
|
|
698
|
+
try:
|
|
699
|
+
log_content = selected_path.read_text(encoding="utf-8")
|
|
700
|
+
except UnicodeDecodeError:
|
|
701
|
+
log_content = selected_path.read_text(
|
|
702
|
+
encoding="utf-8", errors="replace"
|
|
703
|
+
)
|
|
704
|
+
except OSError as exc: # pragma: no cover - filesystem edge cases
|
|
705
|
+
logger.warning("Unable to read log file %s", selected_path, exc_info=exc)
|
|
706
|
+
log_error = _(
|
|
707
|
+
"The log file could not be read. Check server permissions and try again."
|
|
708
|
+
)
|
|
709
|
+
else:
|
|
710
|
+
log_error = _("The requested log could not be found.")
|
|
711
|
+
|
|
712
|
+
if not logs_exist:
|
|
713
|
+
log_notice = _("The logs directory could not be found at %(path)s.") % {
|
|
714
|
+
"path": logs_dir,
|
|
715
|
+
}
|
|
716
|
+
elif not available_logs:
|
|
717
|
+
log_notice = _("No log files were found in %(path)s.") % {"path": logs_dir}
|
|
718
|
+
else:
|
|
719
|
+
log_notice = ""
|
|
720
|
+
|
|
721
|
+
context = {**admin.site.each_context(request)}
|
|
722
|
+
context.update(
|
|
723
|
+
{
|
|
724
|
+
"title": _("Log viewer"),
|
|
725
|
+
"available_logs": available_logs,
|
|
726
|
+
"selected_log": selected_log,
|
|
727
|
+
"log_content": log_content,
|
|
728
|
+
"log_error": log_error,
|
|
729
|
+
"log_notice": log_notice,
|
|
730
|
+
"logs_directory": logs_dir,
|
|
731
|
+
}
|
|
732
|
+
)
|
|
733
|
+
return TemplateResponse(request, "admin/log_viewer.html", context)
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def get_admin_urls(original_get_urls):
|
|
737
|
+
def get_urls():
|
|
738
|
+
urls = original_get_urls()
|
|
739
|
+
my_urls = [
|
|
740
|
+
path(
|
|
741
|
+
"logs/viewer/",
|
|
742
|
+
admin.site.admin_view(log_viewer),
|
|
743
|
+
name="log_viewer",
|
|
744
|
+
),
|
|
745
|
+
path(
|
|
746
|
+
"favorites/<int:ct_id>/",
|
|
747
|
+
admin.site.admin_view(favorite_toggle),
|
|
748
|
+
name="favorite_toggle",
|
|
749
|
+
),
|
|
750
|
+
path(
|
|
751
|
+
"favorites/", admin.site.admin_view(favorite_list), name="favorite_list"
|
|
752
|
+
),
|
|
753
|
+
path(
|
|
754
|
+
"favorites/delete/<int:pk>/",
|
|
755
|
+
admin.site.admin_view(favorite_delete),
|
|
756
|
+
name="favorite_delete",
|
|
757
|
+
),
|
|
758
|
+
path(
|
|
759
|
+
"favorites/clear/",
|
|
760
|
+
admin.site.admin_view(favorite_clear),
|
|
761
|
+
name="favorite_clear",
|
|
762
|
+
),
|
|
763
|
+
]
|
|
764
|
+
return my_urls + original_get_urls()
|
|
765
|
+
|
|
766
|
+
return get_urls
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
admin.site.get_urls = get_admin_urls(admin.site.get_urls)
|