arthexis 0.1.9__py3-none-any.whl → 0.1.11__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.

Files changed (51) hide show
  1. {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/METADATA +76 -23
  2. arthexis-0.1.11.dist-info/RECORD +99 -0
  3. config/context_processors.py +1 -0
  4. config/settings.py +245 -26
  5. config/urls.py +11 -4
  6. core/admin.py +585 -57
  7. core/apps.py +29 -1
  8. core/auto_upgrade.py +57 -0
  9. core/backends.py +115 -3
  10. core/environment.py +23 -5
  11. core/fields.py +93 -0
  12. core/mailer.py +3 -1
  13. core/models.py +482 -38
  14. core/reference_utils.py +108 -0
  15. core/sigil_builder.py +23 -5
  16. core/sigil_resolver.py +35 -4
  17. core/system.py +400 -140
  18. core/tasks.py +151 -8
  19. core/temp_passwords.py +181 -0
  20. core/test_system_info.py +97 -1
  21. core/tests.py +393 -15
  22. core/user_data.py +154 -16
  23. core/views.py +499 -20
  24. nodes/admin.py +149 -6
  25. nodes/backends.py +125 -18
  26. nodes/dns.py +203 -0
  27. nodes/models.py +498 -9
  28. nodes/tests.py +682 -3
  29. nodes/views.py +154 -7
  30. ocpp/admin.py +63 -3
  31. ocpp/consumers.py +255 -41
  32. ocpp/evcs.py +6 -3
  33. ocpp/models.py +52 -7
  34. ocpp/reference_utils.py +42 -0
  35. ocpp/simulator.py +62 -5
  36. ocpp/store.py +30 -0
  37. ocpp/test_rfid.py +169 -7
  38. ocpp/tests.py +414 -8
  39. ocpp/views.py +109 -76
  40. pages/admin.py +9 -1
  41. pages/context_processors.py +24 -4
  42. pages/defaults.py +14 -0
  43. pages/forms.py +131 -0
  44. pages/models.py +53 -14
  45. pages/tests.py +450 -14
  46. pages/urls.py +4 -0
  47. pages/views.py +419 -110
  48. arthexis-0.1.9.dist-info/RECORD +0 -92
  49. {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/WHEEL +0 -0
  50. {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/licenses/LICENSE +0 -0
  51. {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/top_level.txt +0 -0
core/user_data.py CHANGED
@@ -16,6 +16,7 @@ from django.dispatch import receiver
16
16
  from django.http import HttpResponse, HttpResponseRedirect
17
17
  from django.template.response import TemplateResponse
18
18
  from django.urls import path, reverse
19
+ from django.utils.functional import LazyObject
19
20
  from django.utils.translation import gettext as _
20
21
 
21
22
  from .entity import Entity
@@ -39,7 +40,14 @@ def _username_for(user) -> str:
39
40
 
40
41
 
41
42
  def _user_allows_user_data(user) -> bool:
42
- return bool(user) and not getattr(user, "is_profile_restricted", False)
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)
43
51
 
44
52
 
45
53
  def _data_dir(user) -> Path:
@@ -93,18 +101,68 @@ def _seed_fixture_path(instance) -> Path | None:
93
101
  return None
94
102
 
95
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
+
96
148
  def _resolve_fixture_user(instance, fallback=None):
97
149
  UserModel = get_user_model()
98
150
  owner = getattr(instance, "user", None)
99
- if isinstance(owner, UserModel):
100
- return owner
151
+ selected = _select_fixture_user(owner, UserModel)
152
+ if selected is not None:
153
+ return selected
101
154
  if hasattr(instance, "owner"):
102
155
  try:
103
156
  owner_value = instance.owner
104
157
  except Exception:
105
158
  owner_value = None
106
- if isinstance(owner_value, UserModel):
107
- return owner_value
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
108
166
  return fallback
109
167
 
110
168
 
@@ -180,6 +238,36 @@ def _mark_fixture_user_data(path: Path) -> None:
180
238
  model.all_objects.filter(pk=pk).update(is_user_data=True)
181
239
 
182
240
 
241
+ def _fixture_targets_installed_apps(data) -> bool:
242
+ """Return ``True`` when *data* only targets installed apps and models."""
243
+
244
+ if not isinstance(data, list):
245
+ return True
246
+
247
+ labels = {
248
+ obj.get("model")
249
+ for obj in data
250
+ if isinstance(obj, dict) and obj.get("model")
251
+ }
252
+
253
+ for label in labels:
254
+ if not isinstance(label, str):
255
+ continue
256
+ if "." not in label:
257
+ continue
258
+ app_label, model_name = label.split(".", 1)
259
+ if not app_label or not model_name:
260
+ continue
261
+ if not apps.is_installed(app_label):
262
+ return False
263
+ try:
264
+ apps.get_model(label)
265
+ except LookupError:
266
+ return False
267
+
268
+ return True
269
+
270
+
183
271
  def _load_fixture(path: Path, *, mark_user_data: bool = True) -> bool:
184
272
  """Load a fixture from *path* and optionally flag loaded entities."""
185
273
 
@@ -203,9 +291,12 @@ def _load_fixture(path: Path, *, mark_user_data: bool = True) -> bool:
203
291
  except Exception:
204
292
  data = None
205
293
  else:
206
- if isinstance(data, list) and not data:
207
- path.unlink(missing_ok=True)
208
- return False
294
+ if isinstance(data, list):
295
+ if not data:
296
+ path.unlink(missing_ok=True)
297
+ return False
298
+ if not _fixture_targets_installed_apps(data):
299
+ return False
209
300
 
210
301
  try:
211
302
  call_command("loaddata", str(path), ignorenonexistent=True)
@@ -230,6 +321,30 @@ def _is_user_fixture(path: Path) -> bool:
230
321
  return len(parts) >= 2 and parts[1].lower() == "user"
231
322
 
232
323
 
324
+ def _get_request_ip(request) -> str:
325
+ """Return the best-effort client IP for ``request``."""
326
+
327
+ if request is None:
328
+ return ""
329
+
330
+ meta = getattr(request, "META", None)
331
+ if not getattr(meta, "get", None):
332
+ return ""
333
+
334
+ forwarded = meta.get("HTTP_X_FORWARDED_FOR")
335
+ if forwarded:
336
+ for value in str(forwarded).split(","):
337
+ candidate = value.strip()
338
+ if candidate:
339
+ return candidate
340
+
341
+ remote = meta.get("REMOTE_ADDR")
342
+ if remote:
343
+ return str(remote).strip()
344
+
345
+ return ""
346
+
347
+
233
348
  _shared_fixtures_loaded = False
234
349
 
235
350
 
@@ -260,6 +375,18 @@ def load_user_fixtures(user, *, include_shared: bool = False) -> None:
260
375
  def _on_login(sender, request, user, **kwargs):
261
376
  load_user_fixtures(user, include_shared=not _shared_fixtures_loaded)
262
377
 
378
+ if not (
379
+ getattr(user, "is_staff", False) or getattr(user, "is_superuser", False)
380
+ ):
381
+ return
382
+
383
+ username = _username_for(user) or "unknown"
384
+ ip_address = _get_request_ip(request) or "unknown"
385
+
386
+ from nodes.models import NetMessage
387
+
388
+ NetMessage.broadcast(subject=f"login {username}", body=f"@ {ip_address}")
389
+
263
390
 
264
391
  @receiver(post_save, sender=settings.AUTH_USER_MODEL)
265
392
  def _on_user_created(sender, instance, created, **kwargs):
@@ -274,9 +401,7 @@ class UserDatumAdminMixin(admin.ModelAdmin):
274
401
  def render_change_form(
275
402
  self, request, context, add=False, change=False, form_url="", obj=None
276
403
  ):
277
- context["show_user_datum"] = issubclass(
278
- self.model, Entity
279
- ) and _user_allows_user_data(request.user)
404
+ context["show_user_datum"] = issubclass(self.model, Entity)
280
405
  context["show_seed_datum"] = issubclass(self.model, Entity)
281
406
  context["show_save_as_copy"] = issubclass(self.model, Entity) or hasattr(
282
407
  self.model, "clone"
@@ -317,6 +442,9 @@ class EntityModelAdmin(UserDatumAdminMixin, admin.ModelAdmin):
317
442
  )
318
443
  if copied:
319
444
  return
445
+ if getattr(self, "_skip_entity_user_datum", False):
446
+ return
447
+
320
448
  target_user = _resolve_fixture_user(obj, request.user)
321
449
  allow_user_data = _user_allows_user_data(target_user)
322
450
  if request.POST.get("_user_datum") == "on":
@@ -389,11 +517,23 @@ def patch_admin_user_datum() -> None:
389
517
  admin.site._user_datum_patched = True
390
518
 
391
519
 
392
- def _seed_data_view(request):
393
- sections = []
520
+ def _iter_entity_admin_models():
521
+ """Yield registered :class:`Entity` admin models without proxy duplicates."""
522
+
523
+ seen: set[type] = set()
394
524
  for model, model_admin in admin.site._registry.items():
395
525
  if not issubclass(model, Entity):
396
526
  continue
527
+ concrete_model = model._meta.concrete_model
528
+ if concrete_model in seen:
529
+ continue
530
+ seen.add(concrete_model)
531
+ yield model, model_admin
532
+
533
+
534
+ def _seed_data_view(request):
535
+ sections = []
536
+ for model, model_admin in _iter_entity_admin_models():
397
537
  objs = model.objects.filter(is_seed_data=True)
398
538
  if not objs.exists():
399
539
  continue
@@ -413,9 +553,7 @@ def _seed_data_view(request):
413
553
 
414
554
  def _user_data_view(request):
415
555
  sections = []
416
- for model, model_admin in admin.site._registry.items():
417
- if not issubclass(model, Entity):
418
- continue
556
+ for model, model_admin in _iter_entity_admin_models():
419
557
  objs = model.objects.filter(is_user_data=True)
420
558
  if not objs.exists():
421
559
  continue