arthexis 0.1.8__py3-none-any.whl → 0.1.9__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.8.dist-info → arthexis-0.1.9.dist-info}/METADATA +42 -4
- arthexis-0.1.9.dist-info/RECORD +92 -0
- arthexis-0.1.9.dist-info/licenses/LICENSE +674 -0
- config/__init__.py +0 -1
- config/auth_app.py +0 -1
- config/celery.py +1 -2
- config/context_processors.py +1 -1
- config/offline.py +2 -0
- config/settings.py +133 -16
- config/urls.py +65 -6
- core/admin.py +1226 -191
- core/admin_history.py +50 -0
- core/admindocs.py +108 -1
- core/apps.py +158 -3
- core/backends.py +46 -4
- core/entity.py +62 -48
- core/fields.py +6 -1
- core/github_helper.py +25 -0
- core/github_issues.py +172 -0
- core/lcd_screen.py +1 -0
- core/liveupdate.py +25 -0
- core/log_paths.py +100 -0
- core/mailer.py +83 -0
- core/middleware.py +57 -0
- core/models.py +1071 -264
- core/notifications.py +11 -1
- core/public_wifi.py +227 -0
- core/release.py +27 -20
- core/sigil_builder.py +131 -0
- core/sigil_context.py +20 -0
- core/sigil_resolver.py +284 -0
- core/system.py +129 -10
- core/tasks.py +118 -19
- core/test_system_info.py +22 -0
- core/tests.py +358 -63
- core/tests_liveupdate.py +17 -0
- core/urls.py +2 -2
- core/user_data.py +329 -167
- core/views.py +383 -57
- core/widgets.py +51 -0
- core/workgroup_urls.py +7 -3
- core/workgroup_views.py +43 -6
- nodes/actions.py +0 -2
- nodes/admin.py +159 -284
- nodes/apps.py +9 -15
- nodes/backends.py +53 -0
- nodes/lcd.py +24 -10
- nodes/models.py +375 -178
- nodes/tasks.py +1 -5
- nodes/tests.py +524 -129
- nodes/utils.py +13 -2
- nodes/views.py +66 -23
- ocpp/admin.py +150 -61
- ocpp/apps.py +1 -1
- ocpp/consumers.py +432 -69
- ocpp/evcs.py +25 -8
- ocpp/models.py +408 -68
- ocpp/simulator.py +13 -6
- ocpp/store.py +258 -30
- ocpp/tasks.py +11 -7
- ocpp/test_export_import.py +8 -7
- ocpp/test_rfid.py +211 -16
- ocpp/tests.py +1198 -135
- ocpp/transactions_io.py +68 -22
- ocpp/urls.py +35 -2
- ocpp/views.py +654 -101
- pages/admin.py +173 -13
- pages/checks.py +0 -1
- pages/context_processors.py +19 -6
- pages/middleware.py +153 -0
- pages/models.py +37 -9
- pages/tests.py +759 -40
- pages/urls.py +3 -0
- pages/utils.py +0 -1
- pages/views.py +576 -25
- arthexis-0.1.8.dist-info/RECORD +0 -80
- arthexis-0.1.8.dist-info/licenses/LICENSE +0 -21
- config/workgroup_app.py +0 -7
- core/checks.py +0 -29
- {arthexis-0.1.8.dist-info → arthexis-0.1.9.dist-info}/WHEEL +0 -0
- {arthexis-0.1.8.dist-info → arthexis-0.1.9.dist-info}/top_level.txt +0 -0
core/user_data.py
CHANGED
|
@@ -5,134 +5,268 @@ from io import BytesIO
|
|
|
5
5
|
from zipfile import ZipFile
|
|
6
6
|
import json
|
|
7
7
|
|
|
8
|
+
from django.apps import apps
|
|
8
9
|
from django.conf import settings
|
|
9
|
-
from django.contrib import admin
|
|
10
|
-
from django.contrib import
|
|
11
|
-
from django.contrib.
|
|
12
|
-
from django.contrib.contenttypes.models import ContentType
|
|
13
|
-
from django.core.exceptions import ValidationError
|
|
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
|
|
14
13
|
from django.core.management import call_command
|
|
15
|
-
from django.db import
|
|
16
|
-
from django.db.models.signals import post_delete, post_save
|
|
14
|
+
from django.db.models.signals import post_save
|
|
17
15
|
from django.dispatch import receiver
|
|
16
|
+
from django.http import HttpResponse, HttpResponseRedirect
|
|
18
17
|
from django.template.response import TemplateResponse
|
|
19
18
|
from django.urls import path, reverse
|
|
20
|
-
from django.http import HttpResponse, HttpResponseRedirect
|
|
21
19
|
from django.utils.translation import gettext as _
|
|
22
20
|
|
|
23
21
|
from .entity import Entity
|
|
24
22
|
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
def _data_root(user=None) -> Path:
|
|
25
|
+
path = Path(getattr(user, "data_path", "") or Path(settings.BASE_DIR) / "data")
|
|
26
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
return path
|
|
28
|
+
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
def _username_for(user) -> str:
|
|
31
|
+
username = ""
|
|
32
|
+
if hasattr(user, "get_username"):
|
|
33
|
+
username = user.get_username()
|
|
34
|
+
if not username and hasattr(user, "username"):
|
|
35
|
+
username = user.username
|
|
36
|
+
if not username and getattr(user, "pk", None):
|
|
37
|
+
username = str(user.pk)
|
|
38
|
+
return username
|
|
34
39
|
|
|
35
|
-
class Meta:
|
|
36
|
-
unique_together = ("user", "content_type", "object_id")
|
|
37
|
-
verbose_name = "User Datum"
|
|
38
|
-
verbose_name_plural = "User Data"
|
|
39
40
|
|
|
41
|
+
def _user_allows_user_data(user) -> bool:
|
|
42
|
+
return bool(user) and not getattr(user, "is_profile_restricted", False)
|
|
40
43
|
|
|
41
|
-
# ---- Fixture utilities ---------------------------------------------------
|
|
42
44
|
|
|
43
|
-
def _data_dir() -> Path:
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
def _data_dir(user) -> Path:
|
|
46
|
+
username = _username_for(user)
|
|
47
|
+
if not username:
|
|
48
|
+
raise ValueError("Cannot determine username for fixture directory")
|
|
49
|
+
path = _data_root(user) / username
|
|
50
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
46
51
|
return path
|
|
47
52
|
|
|
48
53
|
|
|
49
54
|
def _fixture_path(user, instance) -> Path:
|
|
50
|
-
|
|
51
|
-
filename = f"{
|
|
52
|
-
return _data_dir() / filename
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
_seed_fixture_cache: dict[tuple[str, int], Path] | None = None
|
|
55
|
+
model_meta = instance._meta.concrete_model._meta
|
|
56
|
+
filename = f"{model_meta.app_label}_{model_meta.model_name}_{instance.pk}.json"
|
|
57
|
+
return _data_dir(user) / filename
|
|
56
58
|
|
|
57
59
|
|
|
58
60
|
def _seed_fixture_path(instance) -> Path | None:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
61
|
+
label = f"{instance._meta.app_label}.{instance._meta.model_name}"
|
|
62
|
+
base = Path(settings.BASE_DIR)
|
|
63
|
+
for path in base.glob("**/fixtures/*.json"):
|
|
64
|
+
try:
|
|
65
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
66
|
+
except Exception:
|
|
67
|
+
continue
|
|
68
|
+
if not isinstance(data, list) or not data:
|
|
69
|
+
continue
|
|
70
|
+
obj = data[0]
|
|
71
|
+
if obj.get("model") != label:
|
|
72
|
+
continue
|
|
73
|
+
pk = obj.get("pk")
|
|
74
|
+
if pk is not None and pk == instance.pk:
|
|
75
|
+
return path
|
|
76
|
+
fields = obj.get("fields", {}) or {}
|
|
77
|
+
comparable_fields = {
|
|
78
|
+
key: value
|
|
79
|
+
for key, value in fields.items()
|
|
80
|
+
if key not in {"is_seed_data", "is_deleted", "is_user_data"}
|
|
81
|
+
}
|
|
82
|
+
if comparable_fields:
|
|
83
|
+
match = True
|
|
84
|
+
for field_name, value in comparable_fields.items():
|
|
85
|
+
if not hasattr(instance, field_name):
|
|
86
|
+
match = False
|
|
87
|
+
break
|
|
88
|
+
if getattr(instance, field_name) != value:
|
|
89
|
+
match = False
|
|
90
|
+
break
|
|
91
|
+
if match:
|
|
92
|
+
return path
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _resolve_fixture_user(instance, fallback=None):
|
|
97
|
+
UserModel = get_user_model()
|
|
98
|
+
owner = getattr(instance, "user", None)
|
|
99
|
+
if isinstance(owner, UserModel):
|
|
100
|
+
return owner
|
|
101
|
+
if hasattr(instance, "owner"):
|
|
102
|
+
try:
|
|
103
|
+
owner_value = instance.owner
|
|
104
|
+
except Exception:
|
|
105
|
+
owner_value = None
|
|
106
|
+
if isinstance(owner_value, UserModel):
|
|
107
|
+
return owner_value
|
|
108
|
+
return fallback
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def dump_user_fixture(instance, user=None) -> None:
|
|
112
|
+
model = instance._meta.concrete_model
|
|
113
|
+
UserModel = get_user_model()
|
|
114
|
+
if issubclass(UserModel, Entity) and isinstance(instance, UserModel):
|
|
115
|
+
return
|
|
116
|
+
target_user = user or _resolve_fixture_user(instance)
|
|
117
|
+
if target_user is None:
|
|
118
|
+
return
|
|
119
|
+
allow_user_data = _user_allows_user_data(target_user)
|
|
120
|
+
if not allow_user_data:
|
|
121
|
+
is_user_data = getattr(instance, "is_user_data", False)
|
|
122
|
+
if not is_user_data and instance.pk:
|
|
123
|
+
stored_flag = (
|
|
124
|
+
type(instance)
|
|
125
|
+
.all_objects.filter(pk=instance.pk)
|
|
126
|
+
.values_list("is_user_data", flat=True)
|
|
127
|
+
.first()
|
|
128
|
+
)
|
|
129
|
+
is_user_data = bool(stored_flag)
|
|
130
|
+
if not is_user_data:
|
|
131
|
+
return
|
|
132
|
+
meta = model._meta
|
|
133
|
+
path = _fixture_path(target_user, instance)
|
|
83
134
|
call_command(
|
|
84
135
|
"dumpdata",
|
|
85
|
-
f"{app_label}.{model_name}",
|
|
136
|
+
f"{meta.app_label}.{meta.model_name}",
|
|
86
137
|
indent=2,
|
|
87
138
|
pks=str(instance.pk),
|
|
88
139
|
output=str(path),
|
|
140
|
+
use_natural_foreign_keys=True,
|
|
89
141
|
)
|
|
90
142
|
|
|
91
143
|
|
|
92
|
-
def delete_user_fixture(instance, user) -> None:
|
|
93
|
-
|
|
94
|
-
|
|
144
|
+
def delete_user_fixture(instance, user=None) -> None:
|
|
145
|
+
target_user = user or _resolve_fixture_user(instance)
|
|
146
|
+
if target_user is None:
|
|
147
|
+
return
|
|
148
|
+
_fixture_path(target_user, instance).unlink(missing_ok=True)
|
|
95
149
|
|
|
96
|
-
# ---- Signals -------------------------------------------------------------
|
|
97
150
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
151
|
+
def _mark_fixture_user_data(path: Path) -> None:
|
|
152
|
+
try:
|
|
153
|
+
content = path.read_text(encoding="utf-8")
|
|
154
|
+
except UnicodeDecodeError:
|
|
155
|
+
try:
|
|
156
|
+
content = path.read_bytes().decode("latin-1")
|
|
157
|
+
except Exception:
|
|
158
|
+
return
|
|
159
|
+
except Exception:
|
|
101
160
|
return
|
|
102
161
|
try:
|
|
103
|
-
|
|
104
|
-
qs = list(UserDatum.objects.filter(content_type=ct, object_id=instance.pk))
|
|
162
|
+
data = json.loads(content)
|
|
105
163
|
except Exception:
|
|
106
164
|
return
|
|
107
|
-
|
|
108
|
-
|
|
165
|
+
if not isinstance(data, list):
|
|
166
|
+
return
|
|
167
|
+
for obj in data:
|
|
168
|
+
label = obj.get("model")
|
|
169
|
+
if not label:
|
|
170
|
+
continue
|
|
171
|
+
try:
|
|
172
|
+
model = apps.get_model(label)
|
|
173
|
+
except LookupError:
|
|
174
|
+
continue
|
|
175
|
+
if not issubclass(model, Entity):
|
|
176
|
+
continue
|
|
177
|
+
pk = obj.get("pk")
|
|
178
|
+
if pk is None:
|
|
179
|
+
continue
|
|
180
|
+
model.all_objects.filter(pk=pk).update(is_user_data=True)
|
|
109
181
|
|
|
110
182
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
183
|
+
def _load_fixture(path: Path, *, mark_user_data: bool = True) -> bool:
|
|
184
|
+
"""Load a fixture from *path* and optionally flag loaded entities."""
|
|
185
|
+
|
|
186
|
+
text = None
|
|
115
187
|
try:
|
|
116
|
-
|
|
117
|
-
|
|
188
|
+
text = path.read_text(encoding="utf-8")
|
|
189
|
+
except UnicodeDecodeError:
|
|
190
|
+
try:
|
|
191
|
+
text = path.read_bytes().decode("latin-1")
|
|
192
|
+
except Exception:
|
|
193
|
+
return False
|
|
194
|
+
path.write_text(text, encoding="utf-8")
|
|
118
195
|
except Exception:
|
|
196
|
+
# Continue without cached text so ``call_command`` can surface the
|
|
197
|
+
# underlying error just as before.
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
if text is not None:
|
|
201
|
+
try:
|
|
202
|
+
data = json.loads(text)
|
|
203
|
+
except Exception:
|
|
204
|
+
data = None
|
|
205
|
+
else:
|
|
206
|
+
if isinstance(data, list) and not data:
|
|
207
|
+
path.unlink(missing_ok=True)
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
call_command("loaddata", str(path), ignorenonexistent=True)
|
|
212
|
+
except Exception:
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
if mark_user_data:
|
|
216
|
+
_mark_fixture_user_data(path)
|
|
217
|
+
|
|
218
|
+
return True
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _fixture_sort_key(path: Path) -> tuple[int, str]:
|
|
222
|
+
parts = path.name.split("_", 2)
|
|
223
|
+
model_part = parts[1].lower() if len(parts) >= 2 else ""
|
|
224
|
+
is_user = model_part == "user"
|
|
225
|
+
return (0 if is_user else 1, path.name)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _is_user_fixture(path: Path) -> bool:
|
|
229
|
+
parts = path.name.split("_", 2)
|
|
230
|
+
return len(parts) >= 2 and parts[1].lower() == "user"
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
_shared_fixtures_loaded = False
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def load_shared_user_fixtures(*, force: bool = False, user=None) -> None:
|
|
237
|
+
global _shared_fixtures_loaded
|
|
238
|
+
if _shared_fixtures_loaded and not force:
|
|
119
239
|
return
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
240
|
+
root = _data_root(user)
|
|
241
|
+
paths = sorted(root.glob("*.json"), key=_fixture_sort_key)
|
|
242
|
+
for path in paths:
|
|
243
|
+
if _is_user_fixture(path):
|
|
244
|
+
continue
|
|
245
|
+
_load_fixture(path)
|
|
246
|
+
_shared_fixtures_loaded = True
|
|
123
247
|
|
|
124
248
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
249
|
+
def load_user_fixtures(user, *, include_shared: bool = False) -> None:
|
|
250
|
+
if include_shared:
|
|
251
|
+
load_shared_user_fixtures(user=user)
|
|
252
|
+
paths = sorted(_data_dir(user).glob("*.json"), key=_fixture_sort_key)
|
|
253
|
+
for path in paths:
|
|
254
|
+
if _is_user_fixture(path):
|
|
255
|
+
continue
|
|
256
|
+
_load_fixture(path)
|
|
128
257
|
|
|
129
258
|
|
|
130
|
-
@receiver(
|
|
131
|
-
def
|
|
132
|
-
|
|
259
|
+
@receiver(user_logged_in)
|
|
260
|
+
def _on_login(sender, request, user, **kwargs):
|
|
261
|
+
load_user_fixtures(user, include_shared=not _shared_fixtures_loaded)
|
|
133
262
|
|
|
134
263
|
|
|
135
|
-
|
|
264
|
+
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
|
|
265
|
+
def _on_user_created(sender, instance, created, **kwargs):
|
|
266
|
+
if created:
|
|
267
|
+
load_shared_user_fixtures(force=True, user=instance)
|
|
268
|
+
load_user_fixtures(instance)
|
|
269
|
+
|
|
136
270
|
|
|
137
271
|
class UserDatumAdminMixin(admin.ModelAdmin):
|
|
138
272
|
"""Mixin adding a *User Datum* checkbox to change forms."""
|
|
@@ -140,19 +274,27 @@ class UserDatumAdminMixin(admin.ModelAdmin):
|
|
|
140
274
|
def render_change_form(
|
|
141
275
|
self, request, context, add=False, change=False, form_url="", obj=None
|
|
142
276
|
):
|
|
143
|
-
context["show_user_datum"] =
|
|
277
|
+
context["show_user_datum"] = issubclass(
|
|
278
|
+
self.model, Entity
|
|
279
|
+
) and _user_allows_user_data(request.user)
|
|
280
|
+
context["show_seed_datum"] = issubclass(self.model, Entity)
|
|
144
281
|
context["show_save_as_copy"] = issubclass(self.model, Entity) or hasattr(
|
|
145
282
|
self.model, "clone"
|
|
146
283
|
)
|
|
147
284
|
if obj is not None:
|
|
148
|
-
|
|
149
|
-
context["
|
|
150
|
-
user=request.user, content_type=ct, object_id=obj.pk
|
|
151
|
-
).exists()
|
|
285
|
+
context["is_user_datum"] = getattr(obj, "is_user_data", False)
|
|
286
|
+
context["is_seed_datum"] = getattr(obj, "is_seed_data", False)
|
|
152
287
|
else:
|
|
153
288
|
context["is_user_datum"] = False
|
|
289
|
+
context["is_seed_datum"] = False
|
|
154
290
|
return super().render_change_form(request, context, add, change, form_url, obj)
|
|
155
291
|
|
|
292
|
+
|
|
293
|
+
class EntityModelAdmin(UserDatumAdminMixin, admin.ModelAdmin):
|
|
294
|
+
"""ModelAdmin base class for :class:`Entity` models."""
|
|
295
|
+
|
|
296
|
+
change_form_template = "admin/user_datum_change_form.html"
|
|
297
|
+
|
|
156
298
|
def save_model(self, request, obj, form, change):
|
|
157
299
|
copied = "_saveacopy" in request.POST
|
|
158
300
|
if copied:
|
|
@@ -161,57 +303,93 @@ class UserDatumAdminMixin(admin.ModelAdmin):
|
|
|
161
303
|
form.instance = obj
|
|
162
304
|
try:
|
|
163
305
|
super().save_model(request, obj, form, False)
|
|
164
|
-
except
|
|
165
|
-
|
|
306
|
+
except Exception:
|
|
307
|
+
messages.error(
|
|
166
308
|
request,
|
|
167
309
|
_("Unable to save copy. Adjust unique fields and try again."),
|
|
168
|
-
messages.ERROR,
|
|
169
310
|
)
|
|
170
|
-
raise
|
|
311
|
+
raise
|
|
171
312
|
else:
|
|
172
313
|
super().save_model(request, obj, form, change)
|
|
314
|
+
if isinstance(obj, Entity):
|
|
315
|
+
type(obj).all_objects.filter(pk=obj.pk).update(
|
|
316
|
+
is_seed_data=obj.is_seed_data, is_user_data=obj.is_user_data
|
|
317
|
+
)
|
|
173
318
|
if copied:
|
|
174
319
|
return
|
|
175
|
-
|
|
320
|
+
target_user = _resolve_fixture_user(obj, request.user)
|
|
321
|
+
allow_user_data = _user_allows_user_data(target_user)
|
|
176
322
|
if request.POST.get("_user_datum") == "on":
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
323
|
+
if allow_user_data:
|
|
324
|
+
if not obj.is_user_data:
|
|
325
|
+
type(obj).all_objects.filter(pk=obj.pk).update(is_user_data=True)
|
|
326
|
+
obj.is_user_data = True
|
|
327
|
+
dump_user_fixture(obj, target_user)
|
|
328
|
+
handler = getattr(self, "user_datum_saved", None)
|
|
329
|
+
if callable(handler):
|
|
330
|
+
handler(request, obj)
|
|
331
|
+
path = _fixture_path(target_user, obj)
|
|
332
|
+
self.message_user(request, f"User datum saved to {path}")
|
|
333
|
+
else:
|
|
334
|
+
if obj.is_user_data:
|
|
335
|
+
type(obj).all_objects.filter(pk=obj.pk).update(is_user_data=False)
|
|
336
|
+
obj.is_user_data = False
|
|
337
|
+
delete_user_fixture(obj, target_user)
|
|
338
|
+
messages.warning(
|
|
339
|
+
request,
|
|
340
|
+
_("User data is not available for this account."),
|
|
341
|
+
)
|
|
342
|
+
elif obj.is_user_data:
|
|
343
|
+
type(obj).all_objects.filter(pk=obj.pk).update(is_user_data=False)
|
|
344
|
+
obj.is_user_data = False
|
|
345
|
+
delete_user_fixture(obj, target_user)
|
|
346
|
+
handler = getattr(self, "user_datum_deleted", None)
|
|
347
|
+
if callable(handler):
|
|
348
|
+
handler(request, obj)
|
|
190
349
|
|
|
191
350
|
|
|
192
351
|
def patch_admin_user_datum() -> None:
|
|
193
|
-
"""Mixin all registered admin classes."""
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
admin.site.unregister(model)
|
|
352
|
+
"""Mixin all registered entity admin classes and future registrations."""
|
|
353
|
+
|
|
354
|
+
if getattr(admin.site, "_user_datum_patched", False):
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
def _patched(admin_class):
|
|
200
358
|
template = (
|
|
201
|
-
getattr(
|
|
202
|
-
or
|
|
359
|
+
getattr(admin_class, "change_form_template", None)
|
|
360
|
+
or EntityModelAdmin.change_form_template
|
|
203
361
|
)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
attrs,
|
|
362
|
+
return type(
|
|
363
|
+
f"Patched{admin_class.__name__}",
|
|
364
|
+
(EntityModelAdmin, admin_class),
|
|
365
|
+
{"change_form_template": template},
|
|
209
366
|
)
|
|
210
|
-
|
|
367
|
+
|
|
368
|
+
for model, model_admin in list(admin.site._registry.items()):
|
|
369
|
+
if issubclass(model, Entity) and not isinstance(model_admin, EntityModelAdmin):
|
|
370
|
+
admin.site.unregister(model)
|
|
371
|
+
admin.site.register(model, _patched(model_admin.__class__))
|
|
372
|
+
|
|
373
|
+
original_register = admin.site.register
|
|
374
|
+
|
|
375
|
+
def register(model_or_iterable, admin_class=None, **options):
|
|
376
|
+
models = model_or_iterable
|
|
377
|
+
if not isinstance(models, (list, tuple, set)):
|
|
378
|
+
models = [models]
|
|
379
|
+
admin_class = admin_class or admin.ModelAdmin
|
|
380
|
+
patched_class = admin_class
|
|
381
|
+
for model in models:
|
|
382
|
+
if issubclass(model, Entity) and not issubclass(
|
|
383
|
+
patched_class, EntityModelAdmin
|
|
384
|
+
):
|
|
385
|
+
patched_class = _patched(patched_class)
|
|
386
|
+
return original_register(model_or_iterable, patched_class, **options)
|
|
387
|
+
|
|
388
|
+
admin.site.register = register
|
|
389
|
+
admin.site._user_datum_patched = True
|
|
211
390
|
|
|
212
391
|
|
|
213
392
|
def _seed_data_view(request):
|
|
214
|
-
"""Display all entities marked as seed data."""
|
|
215
393
|
sections = []
|
|
216
394
|
for model, model_admin in admin.site._registry.items():
|
|
217
395
|
if not issubclass(model, Entity):
|
|
@@ -225,14 +403,8 @@ def _seed_data_view(request):
|
|
|
225
403
|
f"admin:{obj._meta.app_label}_{obj._meta.model_name}_change",
|
|
226
404
|
args=[obj.pk],
|
|
227
405
|
)
|
|
228
|
-
|
|
229
|
-
items.append(
|
|
230
|
-
{
|
|
231
|
-
"url": url,
|
|
232
|
-
"label": str(obj),
|
|
233
|
-
"fixture": fixture_path.name if fixture_path else "",
|
|
234
|
-
}
|
|
235
|
-
)
|
|
406
|
+
fixture = _seed_fixture_path(obj)
|
|
407
|
+
items.append({"url": url, "label": str(obj), "fixture": fixture})
|
|
236
408
|
sections.append({"opts": model._meta, "items": items})
|
|
237
409
|
context = admin.site.each_context(request)
|
|
238
410
|
context.update({"title": _("Seed Data"), "sections": sections})
|
|
@@ -240,38 +412,33 @@ def _seed_data_view(request):
|
|
|
240
412
|
|
|
241
413
|
|
|
242
414
|
def _user_data_view(request):
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
415
|
+
sections = []
|
|
416
|
+
for model, model_admin in admin.site._registry.items():
|
|
417
|
+
if not issubclass(model, Entity):
|
|
418
|
+
continue
|
|
419
|
+
objs = model.objects.filter(is_user_data=True)
|
|
420
|
+
if not objs.exists():
|
|
421
|
+
continue
|
|
422
|
+
items = []
|
|
423
|
+
for obj in objs:
|
|
424
|
+
url = reverse(
|
|
425
|
+
f"admin:{obj._meta.app_label}_{obj._meta.model_name}_change",
|
|
426
|
+
args=[obj.pk],
|
|
427
|
+
)
|
|
428
|
+
fixture = _fixture_path(request.user, obj)
|
|
429
|
+
items.append({"url": url, "label": str(obj), "fixture": fixture})
|
|
430
|
+
sections.append({"opts": model._meta, "items": items})
|
|
259
431
|
context = admin.site.each_context(request)
|
|
260
432
|
context.update(
|
|
261
|
-
{
|
|
262
|
-
"title": _("User Data"),
|
|
263
|
-
"sections": section_list,
|
|
264
|
-
"import_export": True,
|
|
265
|
-
}
|
|
433
|
+
{"title": _("User Data"), "sections": sections, "import_export": True}
|
|
266
434
|
)
|
|
267
435
|
return TemplateResponse(request, "admin/data_list.html", context)
|
|
268
436
|
|
|
269
437
|
|
|
270
438
|
def _user_data_export(request):
|
|
271
|
-
"""Return a zip file containing all fixtures for the current user."""
|
|
272
439
|
buffer = BytesIO()
|
|
273
440
|
with ZipFile(buffer, "w") as zf:
|
|
274
|
-
for path in _data_dir().glob(
|
|
441
|
+
for path in _data_dir(request.user).glob("*.json"):
|
|
275
442
|
zf.write(path, arcname=path.name)
|
|
276
443
|
buffer.seek(0)
|
|
277
444
|
response = HttpResponse(buffer.getvalue(), content_type="application/zip")
|
|
@@ -282,41 +449,36 @@ def _user_data_export(request):
|
|
|
282
449
|
|
|
283
450
|
|
|
284
451
|
def _user_data_import(request):
|
|
285
|
-
"""Import fixtures from an uploaded zip file."""
|
|
286
452
|
if request.method == "POST" and request.FILES.get("data_zip"):
|
|
287
453
|
with ZipFile(request.FILES["data_zip"]) as zf:
|
|
288
454
|
paths = []
|
|
289
|
-
data_dir = _data_dir()
|
|
455
|
+
data_dir = _data_dir(request.user)
|
|
290
456
|
for name in zf.namelist():
|
|
291
457
|
if not name.endswith(".json"):
|
|
292
458
|
continue
|
|
459
|
+
content = zf.read(name)
|
|
293
460
|
target = data_dir / name
|
|
294
461
|
with target.open("wb") as f:
|
|
295
|
-
f.write(
|
|
462
|
+
f.write(content)
|
|
296
463
|
paths.append(target)
|
|
297
464
|
if paths:
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
try:
|
|
301
|
-
user_id, app_label, model, obj_id = p.stem.split("_", 3)
|
|
302
|
-
ct = ContentType.objects.get_by_natural_key(app_label, model)
|
|
303
|
-
UserDatum.objects.get_or_create(
|
|
304
|
-
user_id=int(user_id), content_type=ct, object_id=int(obj_id)
|
|
305
|
-
)
|
|
306
|
-
except Exception:
|
|
307
|
-
continue
|
|
465
|
+
for path in paths:
|
|
466
|
+
_load_fixture(path)
|
|
308
467
|
return HttpResponseRedirect(reverse("admin:user_data"))
|
|
309
468
|
|
|
310
469
|
|
|
311
470
|
def patch_admin_user_data_views() -> None:
|
|
312
|
-
"""Add custom admin views for seed and user data listings."""
|
|
313
471
|
original_get_urls = admin.site.get_urls
|
|
314
472
|
|
|
315
473
|
def get_urls():
|
|
316
474
|
urls = original_get_urls()
|
|
317
475
|
custom = [
|
|
318
|
-
path(
|
|
319
|
-
|
|
476
|
+
path(
|
|
477
|
+
"seed-data/", admin.site.admin_view(_seed_data_view), name="seed_data"
|
|
478
|
+
),
|
|
479
|
+
path(
|
|
480
|
+
"user-data/", admin.site.admin_view(_user_data_view), name="user_data"
|
|
481
|
+
),
|
|
320
482
|
path(
|
|
321
483
|
"user-data/export/",
|
|
322
484
|
admin.site.admin_view(_user_data_export),
|