arthexis 0.1.3__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.3.dist-info/METADATA +126 -0
- arthexis-0.1.3.dist-info/RECORD +73 -0
- arthexis-0.1.3.dist-info/WHEEL +5 -0
- arthexis-0.1.3.dist-info/licenses/LICENSE +21 -0
- arthexis-0.1.3.dist-info/top_level.txt +5 -0
- config/__init__.py +6 -0
- config/active_app.py +15 -0
- config/asgi.py +29 -0
- config/auth_app.py +8 -0
- config/celery.py +19 -0
- config/context_processors.py +68 -0
- config/loadenv.py +11 -0
- config/logging.py +43 -0
- config/middleware.py +25 -0
- config/offline.py +47 -0
- config/settings.py +374 -0
- config/urls.py +91 -0
- config/wsgi.py +17 -0
- core/__init__.py +0 -0
- core/admin.py +830 -0
- core/apps.py +67 -0
- core/backends.py +82 -0
- core/entity.py +97 -0
- core/environment.py +43 -0
- core/fields.py +70 -0
- core/lcd_screen.py +77 -0
- core/middleware.py +34 -0
- core/models.py +1277 -0
- core/notifications.py +95 -0
- core/release.py +451 -0
- core/system.py +111 -0
- core/tasks.py +100 -0
- core/tests.py +483 -0
- core/urls.py +11 -0
- core/user_data.py +333 -0
- core/views.py +431 -0
- nodes/__init__.py +0 -0
- nodes/actions.py +72 -0
- nodes/admin.py +347 -0
- nodes/apps.py +76 -0
- nodes/lcd.py +151 -0
- nodes/models.py +577 -0
- nodes/tasks.py +50 -0
- nodes/tests.py +1072 -0
- nodes/urls.py +13 -0
- nodes/utils.py +62 -0
- nodes/views.py +262 -0
- ocpp/__init__.py +0 -0
- ocpp/admin.py +392 -0
- ocpp/apps.py +24 -0
- ocpp/consumers.py +267 -0
- ocpp/evcs.py +911 -0
- ocpp/models.py +300 -0
- ocpp/routing.py +9 -0
- ocpp/simulator.py +357 -0
- ocpp/store.py +175 -0
- ocpp/tasks.py +27 -0
- ocpp/test_export_import.py +129 -0
- ocpp/test_rfid.py +345 -0
- ocpp/tests.py +1229 -0
- ocpp/transactions_io.py +119 -0
- ocpp/urls.py +17 -0
- ocpp/views.py +359 -0
- pages/__init__.py +0 -0
- pages/admin.py +231 -0
- pages/apps.py +10 -0
- pages/checks.py +41 -0
- pages/context_processors.py +72 -0
- pages/models.py +224 -0
- pages/tests.py +628 -0
- pages/urls.py +17 -0
- pages/utils.py +13 -0
- pages/views.py +191 -0
core/user_data.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
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.conf import settings
|
|
9
|
+
from django.contrib import admin
|
|
10
|
+
from django.contrib import messages
|
|
11
|
+
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
12
|
+
from django.contrib.contenttypes.models import ContentType
|
|
13
|
+
from django.core.exceptions import ValidationError
|
|
14
|
+
from django.core.management import call_command
|
|
15
|
+
from django.db import IntegrityError, models
|
|
16
|
+
from django.db.models.signals import post_delete, post_save
|
|
17
|
+
from django.dispatch import receiver
|
|
18
|
+
from django.template.response import TemplateResponse
|
|
19
|
+
from django.urls import path, reverse
|
|
20
|
+
from django.http import HttpResponse, HttpResponseRedirect
|
|
21
|
+
from django.utils.translation import gettext as _
|
|
22
|
+
|
|
23
|
+
from .entity import Entity
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class UserDatum(models.Model):
|
|
27
|
+
"""Link an :class:`Entity` instance to a user for persistence."""
|
|
28
|
+
|
|
29
|
+
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
|
30
|
+
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
|
31
|
+
object_id = models.PositiveIntegerField()
|
|
32
|
+
entity = GenericForeignKey("content_type", "object_id")
|
|
33
|
+
created_on = models.DateTimeField(auto_now_add=True)
|
|
34
|
+
|
|
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
|
+
# ---- Fixture utilities ---------------------------------------------------
|
|
42
|
+
|
|
43
|
+
def _data_dir() -> Path:
|
|
44
|
+
path = Path(settings.BASE_DIR) / "data"
|
|
45
|
+
path.mkdir(exist_ok=True)
|
|
46
|
+
return path
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _fixture_path(user, instance) -> Path:
|
|
50
|
+
ct = ContentType.objects.get_for_model(instance)
|
|
51
|
+
filename = f"{user.pk}_{ct.app_label}_{ct.model}_{instance.pk}.json"
|
|
52
|
+
return _data_dir() / filename
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_seed_fixture_cache: dict[tuple[str, int], Path] | None = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _seed_fixture_path(instance) -> Path | None:
|
|
59
|
+
"""Return the fixture file containing the given seed datum."""
|
|
60
|
+
global _seed_fixture_cache
|
|
61
|
+
if _seed_fixture_cache is None:
|
|
62
|
+
_seed_fixture_cache = {}
|
|
63
|
+
base = Path(settings.BASE_DIR)
|
|
64
|
+
for path in base.rglob("fixtures/*.json"):
|
|
65
|
+
try:
|
|
66
|
+
with path.open() as f:
|
|
67
|
+
data = json.load(f)
|
|
68
|
+
except Exception:
|
|
69
|
+
continue
|
|
70
|
+
for obj in data:
|
|
71
|
+
model = obj.get("model")
|
|
72
|
+
pk = obj.get("pk")
|
|
73
|
+
if model and pk is not None:
|
|
74
|
+
_seed_fixture_cache[(model, pk)] = path
|
|
75
|
+
key = (f"{instance._meta.app_label}.{instance._meta.model_name}", instance.pk)
|
|
76
|
+
return _seed_fixture_cache.get(key)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def dump_user_fixture(instance, user) -> None:
|
|
80
|
+
path = _fixture_path(user, instance)
|
|
81
|
+
app_label = instance._meta.app_label
|
|
82
|
+
model_name = instance._meta.model_name
|
|
83
|
+
call_command(
|
|
84
|
+
"dumpdata",
|
|
85
|
+
f"{app_label}.{model_name}",
|
|
86
|
+
indent=2,
|
|
87
|
+
pks=str(instance.pk),
|
|
88
|
+
output=str(path),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def delete_user_fixture(instance, user) -> None:
|
|
93
|
+
_fixture_path(user, instance).unlink(missing_ok=True)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---- Signals -------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
@receiver(post_save)
|
|
99
|
+
def _entity_saved(sender, instance, **kwargs):
|
|
100
|
+
if isinstance(instance, UserDatum):
|
|
101
|
+
return
|
|
102
|
+
try:
|
|
103
|
+
ct = ContentType.objects.get_for_model(instance)
|
|
104
|
+
qs = list(UserDatum.objects.filter(content_type=ct, object_id=instance.pk))
|
|
105
|
+
except Exception:
|
|
106
|
+
return
|
|
107
|
+
for ud in qs:
|
|
108
|
+
dump_user_fixture(instance, ud.user)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@receiver(post_delete)
|
|
112
|
+
def _entity_deleted(sender, instance, **kwargs):
|
|
113
|
+
if isinstance(instance, UserDatum):
|
|
114
|
+
return
|
|
115
|
+
try:
|
|
116
|
+
ct = ContentType.objects.get_for_model(instance)
|
|
117
|
+
qs = list(UserDatum.objects.filter(content_type=ct, object_id=instance.pk))
|
|
118
|
+
except Exception:
|
|
119
|
+
return
|
|
120
|
+
for ud in qs:
|
|
121
|
+
delete_user_fixture(instance, ud.user)
|
|
122
|
+
ud.delete()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@receiver(post_save, sender=UserDatum)
|
|
126
|
+
def _userdatum_saved(sender, instance, **kwargs):
|
|
127
|
+
dump_user_fixture(instance.entity, instance.user)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@receiver(post_delete, sender=UserDatum)
|
|
131
|
+
def _userdatum_deleted(sender, instance, **kwargs):
|
|
132
|
+
delete_user_fixture(instance.entity, instance.user)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ---- Admin integration ---------------------------------------------------
|
|
136
|
+
|
|
137
|
+
class UserDatumAdminMixin(admin.ModelAdmin):
|
|
138
|
+
"""Mixin adding a *User Datum* checkbox to change forms."""
|
|
139
|
+
|
|
140
|
+
def render_change_form(
|
|
141
|
+
self, request, context, add=False, change=False, form_url="", obj=None
|
|
142
|
+
):
|
|
143
|
+
context["show_user_datum"] = True
|
|
144
|
+
context["show_save_as_copy"] = issubclass(self.model, Entity) or hasattr(
|
|
145
|
+
self.model, "clone"
|
|
146
|
+
)
|
|
147
|
+
if obj is not None:
|
|
148
|
+
ct = ContentType.objects.get_for_model(obj)
|
|
149
|
+
context["is_user_datum"] = UserDatum.objects.filter(
|
|
150
|
+
user=request.user, content_type=ct, object_id=obj.pk
|
|
151
|
+
).exists()
|
|
152
|
+
else:
|
|
153
|
+
context["is_user_datum"] = False
|
|
154
|
+
return super().render_change_form(request, context, add, change, form_url, obj)
|
|
155
|
+
|
|
156
|
+
def save_model(self, request, obj, form, change):
|
|
157
|
+
copied = "_saveacopy" in request.POST
|
|
158
|
+
if copied:
|
|
159
|
+
obj = obj.clone() if hasattr(obj, "clone") else obj
|
|
160
|
+
obj.pk = None
|
|
161
|
+
form.instance = obj
|
|
162
|
+
try:
|
|
163
|
+
super().save_model(request, obj, form, False)
|
|
164
|
+
except IntegrityError:
|
|
165
|
+
self.message_user(
|
|
166
|
+
request,
|
|
167
|
+
_("Unable to save copy. Adjust unique fields and try again."),
|
|
168
|
+
messages.ERROR,
|
|
169
|
+
)
|
|
170
|
+
raise ValidationError("save_as_copy")
|
|
171
|
+
else:
|
|
172
|
+
super().save_model(request, obj, form, change)
|
|
173
|
+
if copied:
|
|
174
|
+
return
|
|
175
|
+
ct = ContentType.objects.get_for_model(obj)
|
|
176
|
+
if request.POST.get("_user_datum") == "on":
|
|
177
|
+
UserDatum.objects.get_or_create(
|
|
178
|
+
user=request.user, content_type=ct, object_id=obj.pk
|
|
179
|
+
)
|
|
180
|
+
dump_user_fixture(obj, request.user)
|
|
181
|
+
path = _fixture_path(request.user, obj)
|
|
182
|
+
self.message_user(request, f"User datum saved to {path}")
|
|
183
|
+
else:
|
|
184
|
+
qs = UserDatum.objects.filter(
|
|
185
|
+
user=request.user, content_type=ct, object_id=obj.pk
|
|
186
|
+
)
|
|
187
|
+
if qs.exists():
|
|
188
|
+
qs.delete()
|
|
189
|
+
delete_user_fixture(obj, request.user)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def patch_admin_user_datum() -> None:
|
|
193
|
+
"""Mixin all registered admin classes."""
|
|
194
|
+
for model, model_admin in list(admin.site._registry.items()):
|
|
195
|
+
if model is UserDatum:
|
|
196
|
+
continue
|
|
197
|
+
if isinstance(model_admin, UserDatumAdminMixin):
|
|
198
|
+
continue
|
|
199
|
+
admin.site.unregister(model)
|
|
200
|
+
template = (
|
|
201
|
+
getattr(model_admin, "change_form_template", None)
|
|
202
|
+
or "admin/user_datum_change_form.html"
|
|
203
|
+
)
|
|
204
|
+
attrs = {"change_form_template": template}
|
|
205
|
+
Patched = type(
|
|
206
|
+
f"Patched{model_admin.__class__.__name__}",
|
|
207
|
+
(UserDatumAdminMixin, model_admin.__class__),
|
|
208
|
+
attrs,
|
|
209
|
+
)
|
|
210
|
+
admin.site.register(model, Patched)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _seed_data_view(request):
|
|
214
|
+
"""Display all entities marked as seed data."""
|
|
215
|
+
sections = []
|
|
216
|
+
for model, model_admin in admin.site._registry.items():
|
|
217
|
+
if not issubclass(model, Entity):
|
|
218
|
+
continue
|
|
219
|
+
objs = model.objects.filter(is_seed_data=True)
|
|
220
|
+
if not objs.exists():
|
|
221
|
+
continue
|
|
222
|
+
items = []
|
|
223
|
+
for obj in objs:
|
|
224
|
+
url = reverse(
|
|
225
|
+
f"admin:{obj._meta.app_label}_{obj._meta.model_name}_change",
|
|
226
|
+
args=[obj.pk],
|
|
227
|
+
)
|
|
228
|
+
fixture_path = _seed_fixture_path(obj)
|
|
229
|
+
items.append(
|
|
230
|
+
{
|
|
231
|
+
"url": url,
|
|
232
|
+
"label": str(obj),
|
|
233
|
+
"fixture": fixture_path.name if fixture_path else "",
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
sections.append({"opts": model._meta, "items": items})
|
|
237
|
+
context = admin.site.each_context(request)
|
|
238
|
+
context.update({"title": _("Seed Data"), "sections": sections})
|
|
239
|
+
return TemplateResponse(request, "admin/data_list.html", context)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _user_data_view(request):
|
|
243
|
+
"""Display all user datum entities for the current user."""
|
|
244
|
+
sections = {}
|
|
245
|
+
qs = UserDatum.objects.filter(user=request.user).select_related("content_type")
|
|
246
|
+
for ud in qs:
|
|
247
|
+
model = ud.content_type.model_class()
|
|
248
|
+
obj = ud.entity
|
|
249
|
+
url = reverse(
|
|
250
|
+
f"admin:{obj._meta.app_label}_{obj._meta.model_name}_change",
|
|
251
|
+
args=[obj.pk],
|
|
252
|
+
)
|
|
253
|
+
section = sections.setdefault(model._meta, [])
|
|
254
|
+
fixture = _fixture_path(request.user, obj)
|
|
255
|
+
section.append(
|
|
256
|
+
{"url": url, "label": str(obj), "fixture": fixture.name}
|
|
257
|
+
)
|
|
258
|
+
section_list = [{"opts": opts, "items": items} for opts, items in sections.items()]
|
|
259
|
+
context = admin.site.each_context(request)
|
|
260
|
+
context.update(
|
|
261
|
+
{
|
|
262
|
+
"title": _("User Data"),
|
|
263
|
+
"sections": section_list,
|
|
264
|
+
"import_export": True,
|
|
265
|
+
}
|
|
266
|
+
)
|
|
267
|
+
return TemplateResponse(request, "admin/data_list.html", context)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _user_data_export(request):
|
|
271
|
+
"""Return a zip file containing all fixtures for the current user."""
|
|
272
|
+
buffer = BytesIO()
|
|
273
|
+
with ZipFile(buffer, "w") as zf:
|
|
274
|
+
for path in _data_dir().glob(f"{request.user.pk}_*.json"):
|
|
275
|
+
zf.write(path, arcname=path.name)
|
|
276
|
+
buffer.seek(0)
|
|
277
|
+
response = HttpResponse(buffer.getvalue(), content_type="application/zip")
|
|
278
|
+
response["Content-Disposition"] = (
|
|
279
|
+
f"attachment; filename=user_data_{request.user.pk}.zip"
|
|
280
|
+
)
|
|
281
|
+
return response
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _user_data_import(request):
|
|
285
|
+
"""Import fixtures from an uploaded zip file."""
|
|
286
|
+
if request.method == "POST" and request.FILES.get("data_zip"):
|
|
287
|
+
with ZipFile(request.FILES["data_zip"]) as zf:
|
|
288
|
+
paths = []
|
|
289
|
+
data_dir = _data_dir()
|
|
290
|
+
for name in zf.namelist():
|
|
291
|
+
if not name.endswith(".json"):
|
|
292
|
+
continue
|
|
293
|
+
target = data_dir / name
|
|
294
|
+
with target.open("wb") as f:
|
|
295
|
+
f.write(zf.read(name))
|
|
296
|
+
paths.append(target)
|
|
297
|
+
if paths:
|
|
298
|
+
call_command("loaddata", *[str(p) for p in paths])
|
|
299
|
+
for p in paths:
|
|
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
|
|
308
|
+
return HttpResponseRedirect(reverse("admin:user_data"))
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def patch_admin_user_data_views() -> None:
|
|
312
|
+
"""Add custom admin views for seed and user data listings."""
|
|
313
|
+
original_get_urls = admin.site.get_urls
|
|
314
|
+
|
|
315
|
+
def get_urls():
|
|
316
|
+
urls = original_get_urls()
|
|
317
|
+
custom = [
|
|
318
|
+
path("seed-data/", admin.site.admin_view(_seed_data_view), name="seed_data"),
|
|
319
|
+
path("user-data/", admin.site.admin_view(_user_data_view), name="user_data"),
|
|
320
|
+
path(
|
|
321
|
+
"user-data/export/",
|
|
322
|
+
admin.site.admin_view(_user_data_export),
|
|
323
|
+
name="user_data_export",
|
|
324
|
+
),
|
|
325
|
+
path(
|
|
326
|
+
"user-data/import/",
|
|
327
|
+
admin.site.admin_view(_user_data_import),
|
|
328
|
+
name="user_data_import",
|
|
329
|
+
),
|
|
330
|
+
]
|
|
331
|
+
return custom + urls
|
|
332
|
+
|
|
333
|
+
admin.site.get_urls = get_urls
|