arthexis 0.1.18__py3-none-any.whl → 0.1.20__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.18.dist-info → arthexis-0.1.20.dist-info}/METADATA +39 -12
- {arthexis-0.1.18.dist-info → arthexis-0.1.20.dist-info}/RECORD +44 -44
- config/settings.py +1 -5
- core/admin.py +142 -1
- core/backends.py +8 -2
- core/environment.py +221 -4
- core/models.py +124 -25
- core/notifications.py +1 -1
- core/reference_utils.py +10 -11
- core/sigil_builder.py +2 -2
- core/system.py +125 -0
- core/tasks.py +24 -23
- core/tests.py +1 -0
- core/views.py +105 -40
- nodes/admin.py +134 -3
- nodes/models.py +310 -69
- nodes/rfid_sync.py +1 -1
- nodes/tasks.py +100 -2
- nodes/tests.py +573 -48
- nodes/urls.py +4 -1
- nodes/views.py +498 -106
- ocpp/admin.py +124 -5
- ocpp/consumers.py +106 -9
- ocpp/models.py +90 -1
- ocpp/store.py +6 -4
- ocpp/tasks.py +4 -0
- ocpp/test_export_import.py +1 -0
- ocpp/test_rfid.py +3 -1
- ocpp/tests.py +114 -10
- ocpp/transactions_io.py +9 -1
- ocpp/urls.py +3 -3
- ocpp/views.py +166 -40
- pages/admin.py +63 -10
- pages/context_processors.py +26 -9
- pages/defaults.py +1 -1
- pages/middleware.py +3 -0
- pages/models.py +35 -0
- pages/module_defaults.py +5 -5
- pages/tests.py +280 -65
- pages/urls.py +3 -1
- pages/views.py +176 -29
- {arthexis-0.1.18.dist-info → arthexis-0.1.20.dist-info}/WHEEL +0 -0
- {arthexis-0.1.18.dist-info → arthexis-0.1.20.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.18.dist-info → arthexis-0.1.20.dist-info}/top_level.txt +0 -0
pages/admin.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from collections import deque
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
4
5
|
from django.contrib import admin, messages
|
|
@@ -8,8 +9,9 @@ from django import forms
|
|
|
8
9
|
from django.shortcuts import redirect, render, get_object_or_404
|
|
9
10
|
from django.urls import NoReverseMatch, path, reverse
|
|
10
11
|
from django.utils.html import format_html
|
|
12
|
+
|
|
11
13
|
from django.template.response import TemplateResponse
|
|
12
|
-
from django.http import JsonResponse
|
|
14
|
+
from django.http import FileResponse, JsonResponse
|
|
13
15
|
from django.utils import timezone
|
|
14
16
|
from django.db.models import Count
|
|
15
17
|
from django.core.exceptions import FieldError
|
|
@@ -289,14 +291,19 @@ class ApplicationAdmin(EntityModelAdmin):
|
|
|
289
291
|
class LandingInline(admin.TabularInline):
|
|
290
292
|
model = Landing
|
|
291
293
|
extra = 0
|
|
292
|
-
fields = ("path", "label", "enabled")
|
|
294
|
+
fields = ("path", "label", "enabled", "track_leads")
|
|
293
295
|
show_change_link = True
|
|
294
296
|
|
|
295
297
|
|
|
296
298
|
@admin.register(Landing)
|
|
297
299
|
class LandingAdmin(EntityModelAdmin):
|
|
298
|
-
list_display = ("label", "path", "module", "enabled")
|
|
299
|
-
list_filter = (
|
|
300
|
+
list_display = ("label", "path", "module", "enabled", "track_leads")
|
|
301
|
+
list_filter = (
|
|
302
|
+
"enabled",
|
|
303
|
+
"track_leads",
|
|
304
|
+
"module__node_role",
|
|
305
|
+
"module__application",
|
|
306
|
+
)
|
|
300
307
|
search_fields = (
|
|
301
308
|
"label",
|
|
302
309
|
"path",
|
|
@@ -305,7 +312,7 @@ class LandingAdmin(EntityModelAdmin):
|
|
|
305
312
|
"module__application__name",
|
|
306
313
|
"module__node_role__name",
|
|
307
314
|
)
|
|
308
|
-
fields = ("module", "path", "label", "enabled", "description")
|
|
315
|
+
fields = ("module", "path", "label", "enabled", "track_leads", "description")
|
|
309
316
|
list_select_related = ("module", "module__application", "module__node_role")
|
|
310
317
|
|
|
311
318
|
|
|
@@ -878,6 +885,13 @@ def favorite_clear(request):
|
|
|
878
885
|
return redirect("admin:favorite_list")
|
|
879
886
|
|
|
880
887
|
|
|
888
|
+
def _read_log_tail(path: Path, limit: int) -> str:
|
|
889
|
+
"""Return the last ``limit`` lines from ``path`` preserving newlines."""
|
|
890
|
+
|
|
891
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
892
|
+
return "".join(deque(handle, maxlen=limit))
|
|
893
|
+
|
|
894
|
+
|
|
881
895
|
def log_viewer(request):
|
|
882
896
|
logs_dir = Path(settings.BASE_DIR) / "logs"
|
|
883
897
|
logs_exist = logs_dir.exists() and logs_dir.is_dir()
|
|
@@ -895,16 +909,50 @@ def log_viewer(request):
|
|
|
895
909
|
selected_log = request.GET.get("log", "")
|
|
896
910
|
log_content = ""
|
|
897
911
|
log_error = ""
|
|
912
|
+
limit_options = [
|
|
913
|
+
{"value": "20", "label": "20"},
|
|
914
|
+
{"value": "40", "label": "40"},
|
|
915
|
+
{"value": "100", "label": "100"},
|
|
916
|
+
{"value": "all", "label": _("All")},
|
|
917
|
+
]
|
|
918
|
+
allowed_limits = [item["value"] for item in limit_options]
|
|
919
|
+
limit_choice = request.GET.get("limit", "20")
|
|
920
|
+
if limit_choice not in allowed_limits:
|
|
921
|
+
limit_choice = "20"
|
|
922
|
+
limit_index = allowed_limits.index(limit_choice)
|
|
923
|
+
download_requested = request.GET.get("download") == "1"
|
|
898
924
|
|
|
899
925
|
if selected_log:
|
|
900
926
|
if selected_log in available_logs:
|
|
901
927
|
selected_path = logs_dir / selected_log
|
|
902
928
|
try:
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
929
|
+
if download_requested:
|
|
930
|
+
return FileResponse(
|
|
931
|
+
selected_path.open("rb"),
|
|
932
|
+
as_attachment=True,
|
|
933
|
+
filename=selected_log,
|
|
934
|
+
)
|
|
935
|
+
if limit_choice == "all":
|
|
936
|
+
try:
|
|
937
|
+
log_content = selected_path.read_text(encoding="utf-8")
|
|
938
|
+
except UnicodeDecodeError:
|
|
939
|
+
log_content = selected_path.read_text(
|
|
940
|
+
encoding="utf-8", errors="replace"
|
|
941
|
+
)
|
|
942
|
+
else:
|
|
943
|
+
try:
|
|
944
|
+
limit_value = int(limit_choice)
|
|
945
|
+
except (TypeError, ValueError):
|
|
946
|
+
limit_value = 20
|
|
947
|
+
limit_choice = "20"
|
|
948
|
+
limit_index = allowed_limits.index(limit_choice)
|
|
949
|
+
try:
|
|
950
|
+
log_content = _read_log_tail(selected_path, limit_value)
|
|
951
|
+
except UnicodeDecodeError:
|
|
952
|
+
with selected_path.open(
|
|
953
|
+
"r", encoding="utf-8", errors="replace"
|
|
954
|
+
) as handle:
|
|
955
|
+
log_content = "".join(deque(handle, maxlen=limit_value))
|
|
908
956
|
except OSError as exc: # pragma: no cover - filesystem edge cases
|
|
909
957
|
logger.warning("Unable to read log file %s", selected_path, exc_info=exc)
|
|
910
958
|
log_error = _(
|
|
@@ -922,6 +970,7 @@ def log_viewer(request):
|
|
|
922
970
|
else:
|
|
923
971
|
log_notice = ""
|
|
924
972
|
|
|
973
|
+
limit_label = limit_options[limit_index]["label"]
|
|
925
974
|
context = {**admin.site.each_context(request)}
|
|
926
975
|
context.update(
|
|
927
976
|
{
|
|
@@ -932,6 +981,10 @@ def log_viewer(request):
|
|
|
932
981
|
"log_error": log_error,
|
|
933
982
|
"log_notice": log_notice,
|
|
934
983
|
"logs_directory": logs_dir,
|
|
984
|
+
"log_limit_options": limit_options,
|
|
985
|
+
"log_limit_index": limit_index,
|
|
986
|
+
"log_limit_choice": limit_choice,
|
|
987
|
+
"log_limit_label": limit_label,
|
|
935
988
|
}
|
|
936
989
|
)
|
|
937
990
|
return TemplateResponse(request, "admin/log_viewer.html", context)
|
pages/context_processors.py
CHANGED
|
@@ -11,7 +11,8 @@ from .models import Module
|
|
|
11
11
|
_FAVICON_DIR = Path(settings.BASE_DIR) / "pages" / "fixtures" / "data"
|
|
12
12
|
_FAVICON_FILENAMES = {
|
|
13
13
|
"default": "favicon.txt",
|
|
14
|
-
"
|
|
14
|
+
"Watchtower": "favicon_watchtower.txt",
|
|
15
|
+
"Constellation": "favicon_watchtower.txt",
|
|
15
16
|
"Control": "favicon_control.txt",
|
|
16
17
|
"Satellite": "favicon_satellite.txt",
|
|
17
18
|
}
|
|
@@ -72,24 +73,40 @@ def nav_links(request):
|
|
|
72
73
|
required_groups = getattr(
|
|
73
74
|
view_func, "required_security_groups", frozenset()
|
|
74
75
|
)
|
|
76
|
+
blocked_reason = None
|
|
75
77
|
if required_groups:
|
|
76
78
|
requires_login = True
|
|
77
|
-
setattr(landing, "requires_login", True)
|
|
78
79
|
if not user_is_authenticated:
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
blocked_reason = "login"
|
|
81
|
+
elif not user_is_superuser and not (
|
|
81
82
|
user_group_names & set(required_groups)
|
|
82
83
|
):
|
|
83
|
-
|
|
84
|
+
blocked_reason = "permission"
|
|
84
85
|
elif requires_login and not user_is_authenticated:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
blocked_reason = "login"
|
|
87
|
+
|
|
88
|
+
if staff_only and not getattr(request.user, "is_staff", False):
|
|
89
|
+
if blocked_reason != "login":
|
|
90
|
+
blocked_reason = "permission"
|
|
91
|
+
|
|
92
|
+
landing.nav_is_locked = bool(blocked_reason)
|
|
93
|
+
landing.nav_lock_reason = blocked_reason
|
|
88
94
|
landings.append(landing)
|
|
89
95
|
if landings:
|
|
96
|
+
normalized_module_path = module.path.rstrip("/") or "/"
|
|
97
|
+
if normalized_module_path == "/read":
|
|
98
|
+
primary_landings = [
|
|
99
|
+
landing
|
|
100
|
+
for landing in landings
|
|
101
|
+
if landing.path.rstrip("/") == normalized_module_path
|
|
102
|
+
]
|
|
103
|
+
if primary_landings:
|
|
104
|
+
landings = primary_landings
|
|
105
|
+
else:
|
|
106
|
+
landings = [landings[0]]
|
|
90
107
|
app_name = getattr(module.application, "name", "").lower()
|
|
91
108
|
if app_name == "awg":
|
|
92
|
-
module.menu = "
|
|
109
|
+
module.menu = "Calculators"
|
|
93
110
|
elif module.path.rstrip("/").lower() == "/man":
|
|
94
111
|
module.menu = "Manual"
|
|
95
112
|
module.enabled_landings = landings
|
pages/defaults.py
CHANGED
|
@@ -7,7 +7,7 @@ DEFAULT_APPLICATION_DESCRIPTIONS: Dict[str, str] = {
|
|
|
7
7
|
"awg": "Power, Energy and Cost calculations.",
|
|
8
8
|
"core": "Support for Business Processes and monetization.",
|
|
9
9
|
"ocpp": "Compatibility with Standards and Good Practices.",
|
|
10
|
-
"nodes": "System and Node-level operations
|
|
10
|
+
"nodes": "System and Node-level operations.",
|
|
11
11
|
"pages": "User QA, Continuity Design and Chaos Testing.",
|
|
12
12
|
"teams": "Identity, Entitlements and Access Controls.",
|
|
13
13
|
}
|
pages/middleware.py
CHANGED
pages/models.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import base64
|
|
1
2
|
import logging
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
@@ -212,6 +213,7 @@ class Landing(Entity):
|
|
|
212
213
|
path = models.CharField(max_length=200)
|
|
213
214
|
label = models.CharField(max_length=100)
|
|
214
215
|
enabled = models.BooleanField(default=True)
|
|
216
|
+
track_leads = models.BooleanField(default=False)
|
|
215
217
|
description = models.TextField(blank=True)
|
|
216
218
|
|
|
217
219
|
objects = LandingManager()
|
|
@@ -435,6 +437,39 @@ class UserManual(Entity):
|
|
|
435
437
|
def natural_key(self): # pragma: no cover - simple representation
|
|
436
438
|
return (self.slug,)
|
|
437
439
|
|
|
440
|
+
def _ensure_pdf_is_base64(self) -> None:
|
|
441
|
+
"""Normalize ``content_pdf`` so stored values are base64 strings."""
|
|
442
|
+
|
|
443
|
+
value = self.content_pdf
|
|
444
|
+
if value in {None, ""}:
|
|
445
|
+
self.content_pdf = "" if value is None else value
|
|
446
|
+
return
|
|
447
|
+
|
|
448
|
+
if isinstance(value, (bytes, bytearray, memoryview)):
|
|
449
|
+
self.content_pdf = base64.b64encode(bytes(value)).decode("ascii")
|
|
450
|
+
return
|
|
451
|
+
|
|
452
|
+
reader = getattr(value, "read", None)
|
|
453
|
+
if callable(reader):
|
|
454
|
+
data = reader()
|
|
455
|
+
if hasattr(value, "seek"):
|
|
456
|
+
try:
|
|
457
|
+
value.seek(0)
|
|
458
|
+
except Exception: # pragma: no cover - best effort reset
|
|
459
|
+
pass
|
|
460
|
+
self.content_pdf = base64.b64encode(data).decode("ascii")
|
|
461
|
+
return
|
|
462
|
+
|
|
463
|
+
if isinstance(value, str):
|
|
464
|
+
stripped = value.strip()
|
|
465
|
+
if stripped.startswith("data:"):
|
|
466
|
+
_, _, encoded = stripped.partition(",")
|
|
467
|
+
self.content_pdf = encoded.strip()
|
|
468
|
+
|
|
469
|
+
def save(self, *args, **kwargs):
|
|
470
|
+
self._ensure_pdf_is_base64()
|
|
471
|
+
super().save(*args, **kwargs)
|
|
472
|
+
|
|
438
473
|
|
|
439
474
|
class ViewHistory(Entity):
|
|
440
475
|
"""Record of public site visits."""
|
pages/module_defaults.py
CHANGED
|
@@ -10,15 +10,15 @@ LandingDefinition = tuple[str, str]
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
ROLE_MODULE_DEFAULTS: Mapping[str, tuple[ModuleDefinition, ...]] = {
|
|
13
|
-
"
|
|
13
|
+
"Watchtower": (
|
|
14
14
|
{
|
|
15
15
|
"application": "ocpp",
|
|
16
16
|
"path": "/ocpp/",
|
|
17
17
|
"menu": "Chargers",
|
|
18
18
|
"landings": (
|
|
19
|
-
("/ocpp/", "CPMS Online Dashboard"),
|
|
20
|
-
("/ocpp/simulator/", "Charge Point Simulator"),
|
|
21
|
-
("/ocpp/rfid/", "RFID Tag Validator"),
|
|
19
|
+
("/ocpp/cpms/dashboard/", "CPMS Online Dashboard"),
|
|
20
|
+
("/ocpp/evcs/simulator/", "Charge Point Simulator"),
|
|
21
|
+
("/ocpp/rfid/validator/", "RFID Tag Validator"),
|
|
22
22
|
),
|
|
23
23
|
},
|
|
24
24
|
{
|
|
@@ -26,7 +26,7 @@ ROLE_MODULE_DEFAULTS: Mapping[str, tuple[ModuleDefinition, ...]] = {
|
|
|
26
26
|
"path": "/awg/",
|
|
27
27
|
"menu": "",
|
|
28
28
|
"landings": (
|
|
29
|
-
("/awg/", "AWG Calculator"),
|
|
29
|
+
("/awg/", "AWG Cable Calculator"),
|
|
30
30
|
("/awg/energy-tariff/", "Energy Tariff Calculator"),
|
|
31
31
|
),
|
|
32
32
|
},
|