arthexis 0.1.9__py3-none-any.whl → 0.1.26__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.
Potentially problematic release.
This version of arthexis might be problematic. Click here for more details.
- arthexis-0.1.26.dist-info/METADATA +272 -0
- arthexis-0.1.26.dist-info/RECORD +111 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +674 -674
- config/__init__.py +5 -5
- config/active_app.py +15 -15
- config/asgi.py +29 -29
- config/auth_app.py +7 -7
- config/celery.py +32 -25
- config/context_processors.py +67 -68
- config/horologia_app.py +7 -7
- config/loadenv.py +11 -11
- config/logging.py +59 -48
- config/middleware.py +71 -25
- config/offline.py +49 -49
- config/settings.py +676 -492
- config/settings_helpers.py +109 -0
- config/urls.py +228 -159
- config/wsgi.py +17 -17
- core/admin.py +4052 -2066
- core/admin_history.py +50 -50
- core/admindocs.py +192 -151
- core/apps.py +350 -223
- core/auto_upgrade.py +72 -0
- core/backends.py +311 -124
- core/changelog.py +403 -0
- core/entity.py +149 -133
- core/environment.py +60 -43
- core/fields.py +168 -75
- core/form_fields.py +75 -0
- core/github_helper.py +188 -25
- core/github_issues.py +183 -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 +89 -83
- core/middleware.py +91 -91
- core/models.py +5041 -2195
- core/notifications.py +105 -105
- core/public_wifi.py +267 -227
- core/reference_utils.py +107 -0
- core/release.py +940 -346
- core/rfid_import_export.py +113 -0
- core/sigil_builder.py +149 -131
- core/sigil_context.py +20 -20
- core/sigil_resolver.py +250 -284
- core/system.py +1425 -230
- core/tasks.py +538 -199
- core/temp_passwords.py +181 -0
- core/test_system_info.py +202 -43
- core/tests.py +2673 -1069
- core/tests_liveupdate.py +17 -17
- core/urls.py +11 -11
- core/user_data.py +681 -495
- core/views.py +2484 -789
- core/widgets.py +213 -51
- nodes/admin.py +2236 -445
- nodes/apps.py +98 -70
- nodes/backends.py +160 -53
- nodes/dns.py +203 -0
- nodes/feature_checks.py +133 -0
- nodes/lcd.py +165 -165
- nodes/models.py +2375 -870
- nodes/reports.py +411 -0
- nodes/rfid_sync.py +210 -0
- nodes/signals.py +18 -0
- nodes/tasks.py +141 -46
- nodes/tests.py +5045 -1489
- nodes/urls.py +29 -13
- nodes/utils.py +172 -73
- nodes/views.py +1768 -304
- ocpp/admin.py +1775 -481
- ocpp/apps.py +25 -25
- ocpp/consumers.py +1843 -630
- ocpp/evcs.py +844 -928
- ocpp/evcs_discovery.py +158 -0
- ocpp/models.py +1417 -640
- ocpp/network.py +398 -0
- ocpp/reference_utils.py +42 -0
- ocpp/routing.py +11 -9
- ocpp/simulator.py +745 -368
- ocpp/status_display.py +26 -0
- ocpp/store.py +603 -403
- ocpp/tasks.py +479 -31
- ocpp/test_export_import.py +131 -130
- ocpp/test_rfid.py +1072 -540
- ocpp/tests.py +5494 -2296
- ocpp/transactions_io.py +197 -165
- ocpp/urls.py +50 -50
- ocpp/views.py +2024 -912
- pages/admin.py +1123 -396
- pages/apps.py +45 -10
- pages/checks.py +40 -40
- pages/context_processors.py +151 -85
- pages/defaults.py +13 -0
- pages/forms.py +221 -0
- pages/middleware.py +213 -153
- pages/models.py +720 -252
- pages/module_defaults.py +156 -0
- pages/site_config.py +137 -0
- pages/tasks.py +74 -0
- pages/tests.py +4009 -1389
- pages/urls.py +38 -20
- pages/utils.py +93 -12
- pages/views.py +1736 -762
- arthexis-0.1.9.dist-info/METADATA +0 -168
- arthexis-0.1.9.dist-info/RECORD +0 -92
- core/workgroup_urls.py +0 -17
- core/workgroup_views.py +0 -94
- nodes/actions.py +0 -70
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/top_level.txt +0 -0
core/user_data.py
CHANGED
|
@@ -1,495 +1,681 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from io import BytesIO
|
|
5
|
-
from zipfile import ZipFile
|
|
6
|
-
import json
|
|
7
|
-
|
|
8
|
-
from django.apps import apps
|
|
9
|
-
from django.conf import settings
|
|
10
|
-
from django.contrib import admin, messages
|
|
11
|
-
from django.contrib.auth import get_user_model
|
|
12
|
-
from django.contrib.auth.signals import user_logged_in
|
|
13
|
-
from django.core.management import call_command
|
|
14
|
-
from django.db.models.signals import post_save
|
|
15
|
-
from django.dispatch import receiver
|
|
16
|
-
from django.http import HttpResponse, HttpResponseRedirect
|
|
17
|
-
from django.template.response import TemplateResponse
|
|
18
|
-
from django.urls import path, reverse
|
|
19
|
-
from django.utils.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
path
|
|
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
|
-
if
|
|
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
|
-
try:
|
|
156
|
-
|
|
157
|
-
except Exception:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
def
|
|
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
|
-
if
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
if
|
|
399
|
-
continue
|
|
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
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
from zipfile import ZipFile
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
from django.apps import apps
|
|
9
|
+
from django.conf import settings
|
|
10
|
+
from django.contrib import admin, messages
|
|
11
|
+
from django.contrib.auth import get_user_model
|
|
12
|
+
from django.contrib.auth.signals import user_logged_in
|
|
13
|
+
from django.core.management import call_command
|
|
14
|
+
from django.db.models.signals import post_save
|
|
15
|
+
from django.dispatch import receiver
|
|
16
|
+
from django.http import HttpResponse, HttpResponseRedirect
|
|
17
|
+
from django.template.response import TemplateResponse
|
|
18
|
+
from django.urls import path, reverse
|
|
19
|
+
from django.utils.functional import LazyObject
|
|
20
|
+
from django.utils.translation import gettext as _
|
|
21
|
+
|
|
22
|
+
from .entity import Entity
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _data_root(user=None) -> Path:
|
|
26
|
+
path = Path(getattr(user, "data_path", "") or Path(settings.BASE_DIR) / "data")
|
|
27
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
return path
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _username_for(user) -> str:
|
|
32
|
+
username = ""
|
|
33
|
+
if hasattr(user, "get_username"):
|
|
34
|
+
username = user.get_username()
|
|
35
|
+
if not username and hasattr(user, "username"):
|
|
36
|
+
username = user.username
|
|
37
|
+
if not username and getattr(user, "pk", None):
|
|
38
|
+
username = str(user.pk)
|
|
39
|
+
return username
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _user_allows_user_data(user) -> bool:
|
|
43
|
+
if not user:
|
|
44
|
+
return False
|
|
45
|
+
username = _username_for(user)
|
|
46
|
+
UserModel = get_user_model()
|
|
47
|
+
system_username = getattr(UserModel, "SYSTEM_USERNAME", "")
|
|
48
|
+
if system_username and username == system_username:
|
|
49
|
+
return True
|
|
50
|
+
return not getattr(user, "is_profile_restricted", False)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _data_dir(user) -> Path:
|
|
54
|
+
username = _username_for(user)
|
|
55
|
+
if not username:
|
|
56
|
+
raise ValueError("Cannot determine username for fixture directory")
|
|
57
|
+
path = _data_root(user) / username
|
|
58
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
return path
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _fixture_path(user, instance) -> Path:
|
|
63
|
+
model_meta = instance._meta.concrete_model._meta
|
|
64
|
+
filename = f"{model_meta.app_label}_{model_meta.model_name}_{instance.pk}.json"
|
|
65
|
+
return _data_dir(user) / filename
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _seed_fixture_path(instance) -> Path | None:
|
|
69
|
+
label = f"{instance._meta.app_label}.{instance._meta.model_name}"
|
|
70
|
+
base = Path(settings.BASE_DIR)
|
|
71
|
+
for path in base.glob("**/fixtures/*.json"):
|
|
72
|
+
try:
|
|
73
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
74
|
+
except Exception:
|
|
75
|
+
continue
|
|
76
|
+
if not isinstance(data, list) or not data:
|
|
77
|
+
continue
|
|
78
|
+
obj = data[0]
|
|
79
|
+
if obj.get("model") != label:
|
|
80
|
+
continue
|
|
81
|
+
pk = obj.get("pk")
|
|
82
|
+
if pk is not None and pk == instance.pk:
|
|
83
|
+
return path
|
|
84
|
+
fields = obj.get("fields", {}) or {}
|
|
85
|
+
comparable_fields = {
|
|
86
|
+
key: value
|
|
87
|
+
for key, value in fields.items()
|
|
88
|
+
if key not in {"is_seed_data", "is_deleted", "is_user_data"}
|
|
89
|
+
}
|
|
90
|
+
if comparable_fields:
|
|
91
|
+
match = True
|
|
92
|
+
for field_name, value in comparable_fields.items():
|
|
93
|
+
if not hasattr(instance, field_name):
|
|
94
|
+
match = False
|
|
95
|
+
break
|
|
96
|
+
if getattr(instance, field_name) != value:
|
|
97
|
+
match = False
|
|
98
|
+
break
|
|
99
|
+
if match:
|
|
100
|
+
return path
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _coerce_user(candidate, user_model):
|
|
105
|
+
if candidate is None:
|
|
106
|
+
return None
|
|
107
|
+
if isinstance(candidate, user_model):
|
|
108
|
+
return candidate
|
|
109
|
+
if isinstance(candidate, LazyObject):
|
|
110
|
+
try:
|
|
111
|
+
candidate._setup()
|
|
112
|
+
except Exception:
|
|
113
|
+
return None
|
|
114
|
+
return _coerce_user(candidate._wrapped, user_model)
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _select_fixture_user(candidate, user_model):
|
|
119
|
+
user = _coerce_user(candidate, user_model)
|
|
120
|
+
visited: set[int] = set()
|
|
121
|
+
while user is not None:
|
|
122
|
+
identifier = user.pk or id(user)
|
|
123
|
+
if identifier in visited:
|
|
124
|
+
break
|
|
125
|
+
visited.add(identifier)
|
|
126
|
+
username = _username_for(user)
|
|
127
|
+
admin_username = getattr(user_model, "ADMIN_USERNAME", "")
|
|
128
|
+
if admin_username and username == admin_username:
|
|
129
|
+
try:
|
|
130
|
+
delegate = getattr(user, "operate_as", None)
|
|
131
|
+
except user_model.DoesNotExist:
|
|
132
|
+
delegate = None
|
|
133
|
+
else:
|
|
134
|
+
delegate = _coerce_user(delegate, user_model)
|
|
135
|
+
if delegate is not None and delegate is not user:
|
|
136
|
+
user = delegate
|
|
137
|
+
continue
|
|
138
|
+
if _user_allows_user_data(user):
|
|
139
|
+
return user
|
|
140
|
+
try:
|
|
141
|
+
delegate = getattr(user, "operate_as", None)
|
|
142
|
+
except user_model.DoesNotExist:
|
|
143
|
+
delegate = None
|
|
144
|
+
user = _coerce_user(delegate, user_model)
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _resolve_fixture_user(instance, fallback=None):
|
|
149
|
+
UserModel = get_user_model()
|
|
150
|
+
owner = getattr(instance, "user", None)
|
|
151
|
+
selected = _select_fixture_user(owner, UserModel)
|
|
152
|
+
if selected is not None:
|
|
153
|
+
return selected
|
|
154
|
+
if hasattr(instance, "owner"):
|
|
155
|
+
try:
|
|
156
|
+
owner_value = instance.owner
|
|
157
|
+
except Exception:
|
|
158
|
+
owner_value = None
|
|
159
|
+
else:
|
|
160
|
+
selected = _select_fixture_user(owner_value, UserModel)
|
|
161
|
+
if selected is not None:
|
|
162
|
+
return selected
|
|
163
|
+
selected = _select_fixture_user(fallback, UserModel)
|
|
164
|
+
if selected is not None:
|
|
165
|
+
return selected
|
|
166
|
+
return fallback
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def dump_user_fixture(instance, user=None) -> None:
|
|
170
|
+
model = instance._meta.concrete_model
|
|
171
|
+
UserModel = get_user_model()
|
|
172
|
+
if issubclass(UserModel, Entity) and isinstance(instance, UserModel):
|
|
173
|
+
return
|
|
174
|
+
target_user = user or _resolve_fixture_user(instance)
|
|
175
|
+
if target_user is None:
|
|
176
|
+
return
|
|
177
|
+
allow_user_data = _user_allows_user_data(target_user)
|
|
178
|
+
if not allow_user_data:
|
|
179
|
+
is_user_data = getattr(instance, "is_user_data", False)
|
|
180
|
+
if not is_user_data and instance.pk:
|
|
181
|
+
stored_flag = (
|
|
182
|
+
type(instance)
|
|
183
|
+
.all_objects.filter(pk=instance.pk)
|
|
184
|
+
.values_list("is_user_data", flat=True)
|
|
185
|
+
.first()
|
|
186
|
+
)
|
|
187
|
+
is_user_data = bool(stored_flag)
|
|
188
|
+
if not is_user_data:
|
|
189
|
+
return
|
|
190
|
+
meta = model._meta
|
|
191
|
+
path = _fixture_path(target_user, instance)
|
|
192
|
+
call_command(
|
|
193
|
+
"dumpdata",
|
|
194
|
+
f"{meta.app_label}.{meta.model_name}",
|
|
195
|
+
indent=2,
|
|
196
|
+
pks=str(instance.pk),
|
|
197
|
+
output=str(path),
|
|
198
|
+
use_natural_foreign_keys=True,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def delete_user_fixture(instance, user=None) -> None:
|
|
203
|
+
target_user = user or _resolve_fixture_user(instance)
|
|
204
|
+
filename = (
|
|
205
|
+
f"{instance._meta.app_label}_{instance._meta.model_name}_{instance.pk}.json"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def _remove_for_user(candidate) -> None:
|
|
209
|
+
if candidate is None:
|
|
210
|
+
return
|
|
211
|
+
base_path = Path(
|
|
212
|
+
getattr(candidate, "data_path", "") or Path(settings.BASE_DIR) / "data"
|
|
213
|
+
)
|
|
214
|
+
username = _username_for(candidate)
|
|
215
|
+
if not username:
|
|
216
|
+
return
|
|
217
|
+
user_dir = base_path / username
|
|
218
|
+
if user_dir.exists():
|
|
219
|
+
(user_dir / filename).unlink(missing_ok=True)
|
|
220
|
+
|
|
221
|
+
if target_user is not None:
|
|
222
|
+
_remove_for_user(target_user)
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
root = Path(settings.BASE_DIR) / "data"
|
|
226
|
+
if root.exists():
|
|
227
|
+
(root / filename).unlink(missing_ok=True)
|
|
228
|
+
for path in root.iterdir():
|
|
229
|
+
if path.is_dir():
|
|
230
|
+
(path / filename).unlink(missing_ok=True)
|
|
231
|
+
|
|
232
|
+
UserModel = get_user_model()
|
|
233
|
+
manager = getattr(UserModel, "all_objects", UserModel._default_manager)
|
|
234
|
+
for candidate in manager.all():
|
|
235
|
+
data_path = getattr(candidate, "data_path", "")
|
|
236
|
+
if not data_path:
|
|
237
|
+
continue
|
|
238
|
+
base_path = Path(data_path)
|
|
239
|
+
if not base_path.exists():
|
|
240
|
+
continue
|
|
241
|
+
username = _username_for(candidate)
|
|
242
|
+
if not username:
|
|
243
|
+
continue
|
|
244
|
+
user_dir = base_path / username
|
|
245
|
+
if user_dir.exists():
|
|
246
|
+
(user_dir / filename).unlink(missing_ok=True)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _mark_fixture_user_data(path: Path) -> None:
|
|
250
|
+
try:
|
|
251
|
+
content = path.read_text(encoding="utf-8")
|
|
252
|
+
except UnicodeDecodeError:
|
|
253
|
+
try:
|
|
254
|
+
content = path.read_bytes().decode("latin-1")
|
|
255
|
+
except Exception:
|
|
256
|
+
return
|
|
257
|
+
except Exception:
|
|
258
|
+
return
|
|
259
|
+
try:
|
|
260
|
+
data = json.loads(content)
|
|
261
|
+
except Exception:
|
|
262
|
+
return
|
|
263
|
+
if not isinstance(data, list):
|
|
264
|
+
return
|
|
265
|
+
for obj in data:
|
|
266
|
+
label = obj.get("model")
|
|
267
|
+
if not label:
|
|
268
|
+
continue
|
|
269
|
+
try:
|
|
270
|
+
model = apps.get_model(label)
|
|
271
|
+
except LookupError:
|
|
272
|
+
continue
|
|
273
|
+
if not issubclass(model, Entity):
|
|
274
|
+
continue
|
|
275
|
+
pk = obj.get("pk")
|
|
276
|
+
if pk is None:
|
|
277
|
+
continue
|
|
278
|
+
model.all_objects.filter(pk=pk).update(is_user_data=True)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _fixture_targets_installed_apps(data) -> bool:
|
|
282
|
+
"""Return ``True`` when *data* only targets installed apps and models."""
|
|
283
|
+
|
|
284
|
+
if not isinstance(data, list):
|
|
285
|
+
return True
|
|
286
|
+
|
|
287
|
+
labels = {
|
|
288
|
+
obj.get("model")
|
|
289
|
+
for obj in data
|
|
290
|
+
if isinstance(obj, dict) and obj.get("model")
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
for label in labels:
|
|
294
|
+
if not isinstance(label, str):
|
|
295
|
+
continue
|
|
296
|
+
if "." not in label:
|
|
297
|
+
continue
|
|
298
|
+
app_label, model_name = label.split(".", 1)
|
|
299
|
+
if not app_label or not model_name:
|
|
300
|
+
continue
|
|
301
|
+
if not apps.is_installed(app_label):
|
|
302
|
+
return False
|
|
303
|
+
try:
|
|
304
|
+
apps.get_model(label)
|
|
305
|
+
except LookupError:
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
return True
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _load_fixture(path: Path, *, mark_user_data: bool = True) -> bool:
|
|
312
|
+
"""Load a fixture from *path* and optionally flag loaded entities."""
|
|
313
|
+
|
|
314
|
+
text = None
|
|
315
|
+
try:
|
|
316
|
+
text = path.read_text(encoding="utf-8")
|
|
317
|
+
except UnicodeDecodeError:
|
|
318
|
+
try:
|
|
319
|
+
text = path.read_bytes().decode("latin-1")
|
|
320
|
+
except Exception:
|
|
321
|
+
return False
|
|
322
|
+
path.write_text(text, encoding="utf-8")
|
|
323
|
+
except Exception:
|
|
324
|
+
# Continue without cached text so ``call_command`` can surface the
|
|
325
|
+
# underlying error just as before.
|
|
326
|
+
pass
|
|
327
|
+
|
|
328
|
+
if text is not None:
|
|
329
|
+
try:
|
|
330
|
+
data = json.loads(text)
|
|
331
|
+
except Exception:
|
|
332
|
+
data = None
|
|
333
|
+
else:
|
|
334
|
+
if isinstance(data, list):
|
|
335
|
+
if not data:
|
|
336
|
+
path.unlink(missing_ok=True)
|
|
337
|
+
return False
|
|
338
|
+
if not _fixture_targets_installed_apps(data):
|
|
339
|
+
return False
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
call_command("loaddata", str(path), ignorenonexistent=True)
|
|
343
|
+
except Exception:
|
|
344
|
+
return False
|
|
345
|
+
|
|
346
|
+
if mark_user_data:
|
|
347
|
+
_mark_fixture_user_data(path)
|
|
348
|
+
|
|
349
|
+
return True
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _fixture_sort_key(path: Path) -> tuple[int, str]:
|
|
353
|
+
parts = path.name.split("_", 2)
|
|
354
|
+
model_part = parts[1].lower() if len(parts) >= 2 else ""
|
|
355
|
+
is_user = model_part == "user"
|
|
356
|
+
return (0 if is_user else 1, path.name)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _is_user_fixture(path: Path) -> bool:
|
|
360
|
+
parts = path.name.split("_", 2)
|
|
361
|
+
return len(parts) >= 2 and parts[1].lower() == "user"
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _get_request_ip(request) -> str:
|
|
365
|
+
"""Return the best-effort client IP for ``request``."""
|
|
366
|
+
|
|
367
|
+
if request is None:
|
|
368
|
+
return ""
|
|
369
|
+
|
|
370
|
+
meta = getattr(request, "META", None)
|
|
371
|
+
if not getattr(meta, "get", None):
|
|
372
|
+
return ""
|
|
373
|
+
|
|
374
|
+
forwarded = meta.get("HTTP_X_FORWARDED_FOR")
|
|
375
|
+
if forwarded:
|
|
376
|
+
for value in str(forwarded).split(","):
|
|
377
|
+
candidate = value.strip()
|
|
378
|
+
if candidate:
|
|
379
|
+
return candidate
|
|
380
|
+
|
|
381
|
+
remote = meta.get("REMOTE_ADDR")
|
|
382
|
+
if remote:
|
|
383
|
+
return str(remote).strip()
|
|
384
|
+
|
|
385
|
+
return ""
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
_shared_fixtures_loaded = False
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def load_shared_user_fixtures(*, force: bool = False, user=None) -> None:
|
|
392
|
+
global _shared_fixtures_loaded
|
|
393
|
+
if _shared_fixtures_loaded and not force:
|
|
394
|
+
return
|
|
395
|
+
root = _data_root(user)
|
|
396
|
+
paths = sorted(root.glob("*.json"), key=_fixture_sort_key)
|
|
397
|
+
for path in paths:
|
|
398
|
+
if _is_user_fixture(path):
|
|
399
|
+
continue
|
|
400
|
+
_load_fixture(path)
|
|
401
|
+
_shared_fixtures_loaded = True
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def load_user_fixtures(user, *, include_shared: bool = False) -> None:
|
|
405
|
+
if include_shared:
|
|
406
|
+
load_shared_user_fixtures(user=user)
|
|
407
|
+
paths = sorted(_data_dir(user).glob("*.json"), key=_fixture_sort_key)
|
|
408
|
+
for path in paths:
|
|
409
|
+
if _is_user_fixture(path):
|
|
410
|
+
continue
|
|
411
|
+
_load_fixture(path)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@receiver(user_logged_in)
|
|
415
|
+
def _on_login(sender, request, user, **kwargs):
|
|
416
|
+
load_user_fixtures(user, include_shared=not _shared_fixtures_loaded)
|
|
417
|
+
|
|
418
|
+
if not (
|
|
419
|
+
getattr(user, "is_staff", False) or getattr(user, "is_superuser", False)
|
|
420
|
+
):
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
username = _username_for(user) or "unknown"
|
|
424
|
+
ip_address = _get_request_ip(request) or "unknown"
|
|
425
|
+
|
|
426
|
+
from nodes.models import NetMessage
|
|
427
|
+
|
|
428
|
+
NetMessage.broadcast(subject=f"login {username}", body=f"@ {ip_address}")
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
|
|
432
|
+
def _on_user_created(sender, instance, created, **kwargs):
|
|
433
|
+
if created:
|
|
434
|
+
load_shared_user_fixtures(force=True, user=instance)
|
|
435
|
+
load_user_fixtures(instance)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class UserDatumAdminMixin(admin.ModelAdmin):
|
|
439
|
+
"""Mixin adding a *User Datum* checkbox to change forms."""
|
|
440
|
+
|
|
441
|
+
def render_change_form(
|
|
442
|
+
self, request, context, add=False, change=False, form_url="", obj=None
|
|
443
|
+
):
|
|
444
|
+
supports_user_datum = issubclass(self.model, Entity) or getattr(
|
|
445
|
+
self.model, "supports_user_datum", False
|
|
446
|
+
)
|
|
447
|
+
supports_seed_datum = issubclass(self.model, Entity) or getattr(
|
|
448
|
+
self.model, "supports_seed_datum", supports_user_datum
|
|
449
|
+
)
|
|
450
|
+
context["show_user_datum"] = supports_user_datum
|
|
451
|
+
context["show_seed_datum"] = supports_seed_datum
|
|
452
|
+
context["show_save_as_copy"] = (
|
|
453
|
+
issubclass(self.model, Entity)
|
|
454
|
+
or getattr(self.model, "supports_save_as_copy", False)
|
|
455
|
+
or hasattr(self.model, "clone")
|
|
456
|
+
)
|
|
457
|
+
if obj is not None:
|
|
458
|
+
context["is_user_datum"] = getattr(obj, "is_user_data", False)
|
|
459
|
+
context["is_seed_datum"] = getattr(obj, "is_seed_data", False)
|
|
460
|
+
else:
|
|
461
|
+
context["is_user_datum"] = False
|
|
462
|
+
context["is_seed_datum"] = False
|
|
463
|
+
return super().render_change_form(request, context, add, change, form_url, obj)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class EntityModelAdmin(UserDatumAdminMixin, admin.ModelAdmin):
|
|
467
|
+
"""ModelAdmin base class for :class:`Entity` models."""
|
|
468
|
+
|
|
469
|
+
change_form_template = "admin/user_datum_change_form.html"
|
|
470
|
+
|
|
471
|
+
def save_model(self, request, obj, form, change):
|
|
472
|
+
copied = "_saveacopy" in request.POST
|
|
473
|
+
if copied:
|
|
474
|
+
obj = obj.clone() if hasattr(obj, "clone") else obj
|
|
475
|
+
obj.pk = None
|
|
476
|
+
form.instance = obj
|
|
477
|
+
try:
|
|
478
|
+
super().save_model(request, obj, form, False)
|
|
479
|
+
except Exception:
|
|
480
|
+
messages.error(
|
|
481
|
+
request,
|
|
482
|
+
_("Unable to save copy. Adjust unique fields and try again."),
|
|
483
|
+
)
|
|
484
|
+
raise
|
|
485
|
+
else:
|
|
486
|
+
super().save_model(request, obj, form, change)
|
|
487
|
+
if isinstance(obj, Entity):
|
|
488
|
+
type(obj).all_objects.filter(pk=obj.pk).update(
|
|
489
|
+
is_seed_data=obj.is_seed_data, is_user_data=obj.is_user_data
|
|
490
|
+
)
|
|
491
|
+
if copied:
|
|
492
|
+
return
|
|
493
|
+
if getattr(self, "_skip_entity_user_datum", False):
|
|
494
|
+
return
|
|
495
|
+
|
|
496
|
+
target_user = _resolve_fixture_user(obj, request.user)
|
|
497
|
+
allow_user_data = _user_allows_user_data(target_user)
|
|
498
|
+
if request.POST.get("_user_datum") == "on":
|
|
499
|
+
if allow_user_data:
|
|
500
|
+
if not obj.is_user_data:
|
|
501
|
+
type(obj).all_objects.filter(pk=obj.pk).update(is_user_data=True)
|
|
502
|
+
obj.is_user_data = True
|
|
503
|
+
dump_user_fixture(obj, target_user)
|
|
504
|
+
handler = getattr(self, "user_datum_saved", None)
|
|
505
|
+
if callable(handler):
|
|
506
|
+
handler(request, obj)
|
|
507
|
+
path = _fixture_path(target_user, obj)
|
|
508
|
+
self.message_user(request, f"User datum saved to {path}")
|
|
509
|
+
else:
|
|
510
|
+
if obj.is_user_data:
|
|
511
|
+
type(obj).all_objects.filter(pk=obj.pk).update(is_user_data=False)
|
|
512
|
+
obj.is_user_data = False
|
|
513
|
+
delete_user_fixture(obj, target_user)
|
|
514
|
+
messages.warning(
|
|
515
|
+
request,
|
|
516
|
+
_("User data is not available for this account."),
|
|
517
|
+
)
|
|
518
|
+
elif obj.is_user_data:
|
|
519
|
+
type(obj).all_objects.filter(pk=obj.pk).update(is_user_data=False)
|
|
520
|
+
obj.is_user_data = False
|
|
521
|
+
delete_user_fixture(obj, target_user)
|
|
522
|
+
handler = getattr(self, "user_datum_deleted", None)
|
|
523
|
+
if callable(handler):
|
|
524
|
+
handler(request, obj)
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def patch_admin_user_datum() -> None:
|
|
528
|
+
"""Mixin all registered entity admin classes and future registrations."""
|
|
529
|
+
|
|
530
|
+
if getattr(admin.site, "_user_datum_patched", False):
|
|
531
|
+
return
|
|
532
|
+
|
|
533
|
+
def _patched(admin_class):
|
|
534
|
+
template = (
|
|
535
|
+
getattr(admin_class, "change_form_template", None)
|
|
536
|
+
or EntityModelAdmin.change_form_template
|
|
537
|
+
)
|
|
538
|
+
return type(
|
|
539
|
+
f"Patched{admin_class.__name__}",
|
|
540
|
+
(EntityModelAdmin, admin_class),
|
|
541
|
+
{"change_form_template": template},
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
for model, model_admin in list(admin.site._registry.items()):
|
|
545
|
+
if issubclass(model, Entity) and not isinstance(model_admin, EntityModelAdmin):
|
|
546
|
+
admin.site.unregister(model)
|
|
547
|
+
admin.site.register(model, _patched(model_admin.__class__))
|
|
548
|
+
|
|
549
|
+
original_register = admin.site.register
|
|
550
|
+
|
|
551
|
+
def register(model_or_iterable, admin_class=None, **options):
|
|
552
|
+
models = model_or_iterable
|
|
553
|
+
if not isinstance(models, (list, tuple, set)):
|
|
554
|
+
models = [models]
|
|
555
|
+
admin_class = admin_class or admin.ModelAdmin
|
|
556
|
+
patched_class = admin_class
|
|
557
|
+
for model in models:
|
|
558
|
+
if issubclass(model, Entity) and not issubclass(
|
|
559
|
+
patched_class, EntityModelAdmin
|
|
560
|
+
):
|
|
561
|
+
patched_class = _patched(patched_class)
|
|
562
|
+
return original_register(model_or_iterable, patched_class, **options)
|
|
563
|
+
|
|
564
|
+
admin.site.register = register
|
|
565
|
+
admin.site._user_datum_patched = True
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _iter_entity_admin_models():
|
|
569
|
+
"""Yield registered :class:`Entity` admin models without proxy duplicates."""
|
|
570
|
+
|
|
571
|
+
seen: set[type] = set()
|
|
572
|
+
for model, model_admin in admin.site._registry.items():
|
|
573
|
+
if not issubclass(model, Entity):
|
|
574
|
+
continue
|
|
575
|
+
concrete_model = model._meta.concrete_model
|
|
576
|
+
if concrete_model in seen:
|
|
577
|
+
continue
|
|
578
|
+
seen.add(concrete_model)
|
|
579
|
+
yield model, model_admin
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _seed_data_view(request):
|
|
583
|
+
sections = []
|
|
584
|
+
for model, model_admin in _iter_entity_admin_models():
|
|
585
|
+
objs = model.objects.filter(is_seed_data=True)
|
|
586
|
+
if not objs.exists():
|
|
587
|
+
continue
|
|
588
|
+
items = []
|
|
589
|
+
for obj in objs:
|
|
590
|
+
url = reverse(
|
|
591
|
+
f"admin:{obj._meta.app_label}_{obj._meta.model_name}_change",
|
|
592
|
+
args=[obj.pk],
|
|
593
|
+
)
|
|
594
|
+
fixture = _seed_fixture_path(obj)
|
|
595
|
+
items.append({"url": url, "label": str(obj), "fixture": fixture})
|
|
596
|
+
sections.append({"opts": model._meta, "items": items})
|
|
597
|
+
context = admin.site.each_context(request)
|
|
598
|
+
context.update({"title": _("Seed Data"), "sections": sections})
|
|
599
|
+
return TemplateResponse(request, "admin/data_list.html", context)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _user_data_view(request):
|
|
603
|
+
sections = []
|
|
604
|
+
for model, model_admin in _iter_entity_admin_models():
|
|
605
|
+
objs = model.objects.filter(is_user_data=True)
|
|
606
|
+
if not objs.exists():
|
|
607
|
+
continue
|
|
608
|
+
items = []
|
|
609
|
+
for obj in objs:
|
|
610
|
+
url = reverse(
|
|
611
|
+
f"admin:{obj._meta.app_label}_{obj._meta.model_name}_change",
|
|
612
|
+
args=[obj.pk],
|
|
613
|
+
)
|
|
614
|
+
fixture = _fixture_path(request.user, obj)
|
|
615
|
+
items.append({"url": url, "label": str(obj), "fixture": fixture})
|
|
616
|
+
sections.append({"opts": model._meta, "items": items})
|
|
617
|
+
context = admin.site.each_context(request)
|
|
618
|
+
context.update(
|
|
619
|
+
{"title": _("User Data"), "sections": sections, "import_export": True}
|
|
620
|
+
)
|
|
621
|
+
return TemplateResponse(request, "admin/data_list.html", context)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _user_data_export(request):
|
|
625
|
+
buffer = BytesIO()
|
|
626
|
+
with ZipFile(buffer, "w") as zf:
|
|
627
|
+
for path in _data_dir(request.user).glob("*.json"):
|
|
628
|
+
zf.write(path, arcname=path.name)
|
|
629
|
+
buffer.seek(0)
|
|
630
|
+
response = HttpResponse(buffer.getvalue(), content_type="application/zip")
|
|
631
|
+
response["Content-Disposition"] = (
|
|
632
|
+
f"attachment; filename=user_data_{request.user.pk}.zip"
|
|
633
|
+
)
|
|
634
|
+
return response
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _user_data_import(request):
|
|
638
|
+
if request.method == "POST" and request.FILES.get("data_zip"):
|
|
639
|
+
with ZipFile(request.FILES["data_zip"]) as zf:
|
|
640
|
+
paths = []
|
|
641
|
+
data_dir = _data_dir(request.user)
|
|
642
|
+
for name in zf.namelist():
|
|
643
|
+
if not name.endswith(".json"):
|
|
644
|
+
continue
|
|
645
|
+
content = zf.read(name)
|
|
646
|
+
target = data_dir / name
|
|
647
|
+
with target.open("wb") as f:
|
|
648
|
+
f.write(content)
|
|
649
|
+
paths.append(target)
|
|
650
|
+
if paths:
|
|
651
|
+
for path in paths:
|
|
652
|
+
_load_fixture(path)
|
|
653
|
+
return HttpResponseRedirect(reverse("admin:user_data"))
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def patch_admin_user_data_views() -> None:
|
|
657
|
+
original_get_urls = admin.site.get_urls
|
|
658
|
+
|
|
659
|
+
def get_urls():
|
|
660
|
+
urls = original_get_urls()
|
|
661
|
+
custom = [
|
|
662
|
+
path(
|
|
663
|
+
"seed-data/", admin.site.admin_view(_seed_data_view), name="seed_data"
|
|
664
|
+
),
|
|
665
|
+
path(
|
|
666
|
+
"user-data/", admin.site.admin_view(_user_data_view), name="user_data"
|
|
667
|
+
),
|
|
668
|
+
path(
|
|
669
|
+
"user-data/export/",
|
|
670
|
+
admin.site.admin_view(_user_data_export),
|
|
671
|
+
name="user_data_export",
|
|
672
|
+
),
|
|
673
|
+
path(
|
|
674
|
+
"user-data/import/",
|
|
675
|
+
admin.site.admin_view(_user_data_import),
|
|
676
|
+
name="user_data_import",
|
|
677
|
+
),
|
|
678
|
+
]
|
|
679
|
+
return custom + urls
|
|
680
|
+
|
|
681
|
+
admin.site.get_urls = get_urls
|