arthexis 0.1.15__py3-none-any.whl → 0.1.17__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.15.dist-info → arthexis-0.1.17.dist-info}/METADATA +1 -2
- {arthexis-0.1.15.dist-info → arthexis-0.1.17.dist-info}/RECORD +40 -39
- config/settings.py +3 -0
- config/urls.py +5 -0
- core/admin.py +242 -15
- core/admindocs.py +44 -3
- core/apps.py +1 -1
- core/backends.py +46 -8
- core/changelog.py +66 -5
- core/github_issues.py +12 -7
- core/mailer.py +9 -5
- core/models.py +121 -29
- core/release.py +107 -2
- core/system.py +209 -2
- core/tasks.py +5 -7
- core/test_system_info.py +16 -0
- core/tests.py +329 -0
- core/views.py +279 -40
- nodes/admin.py +25 -1
- nodes/models.py +70 -4
- nodes/rfid_sync.py +15 -0
- nodes/tests.py +119 -0
- nodes/utils.py +3 -0
- ocpp/admin.py +92 -10
- ocpp/consumers.py +38 -0
- ocpp/models.py +19 -4
- ocpp/tasks.py +156 -2
- ocpp/test_rfid.py +92 -5
- ocpp/tests.py +243 -1
- ocpp/views.py +23 -5
- pages/admin.py +126 -4
- pages/context_processors.py +20 -1
- pages/models.py +3 -1
- pages/module_defaults.py +156 -0
- pages/tests.py +241 -8
- pages/urls.py +1 -0
- pages/views.py +61 -4
- {arthexis-0.1.15.dist-info → arthexis-0.1.17.dist-info}/WHEEL +0 -0
- {arthexis-0.1.15.dist-info → arthexis-0.1.17.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.15.dist-info → arthexis-0.1.17.dist-info}/top_level.txt +0 -0
ocpp/views.py
CHANGED
|
@@ -370,10 +370,6 @@ def _aggregate_dashboard_state(charger: Charger) -> tuple[str, str] | None:
|
|
|
370
370
|
)
|
|
371
371
|
statuses: list[str] = []
|
|
372
372
|
for sibling in siblings:
|
|
373
|
-
status_value = (sibling.last_status or "").strip()
|
|
374
|
-
if status_value:
|
|
375
|
-
statuses.append(status_value.casefold())
|
|
376
|
-
continue
|
|
377
373
|
tx_obj = store.get_transaction(sibling.charger_id, sibling.connector_id)
|
|
378
374
|
if not tx_obj:
|
|
379
375
|
tx_obj = (
|
|
@@ -381,9 +377,22 @@ def _aggregate_dashboard_state(charger: Charger) -> tuple[str, str] | None:
|
|
|
381
377
|
.order_by("-start_time")
|
|
382
378
|
.first()
|
|
383
379
|
)
|
|
384
|
-
|
|
380
|
+
has_session = _has_active_session(tx_obj)
|
|
381
|
+
status_value = (sibling.last_status or "").strip()
|
|
382
|
+
normalized_status = status_value.casefold() if status_value else ""
|
|
383
|
+
error_code_lower = (sibling.last_error_code or "").strip().lower()
|
|
384
|
+
if has_session:
|
|
385
385
|
statuses.append("charging")
|
|
386
386
|
continue
|
|
387
|
+
if (
|
|
388
|
+
normalized_status in {"charging", "finishing"}
|
|
389
|
+
and error_code_lower in ERROR_OK_VALUES
|
|
390
|
+
):
|
|
391
|
+
statuses.append("available")
|
|
392
|
+
continue
|
|
393
|
+
if normalized_status:
|
|
394
|
+
statuses.append(normalized_status)
|
|
395
|
+
continue
|
|
387
396
|
if store.is_connected(sibling.charger_id, sibling.connector_id):
|
|
388
397
|
statuses.append("available")
|
|
389
398
|
|
|
@@ -424,6 +433,15 @@ def _charger_state(charger: Charger, tx_obj: Transaction | list | None):
|
|
|
424
433
|
# while a session is active. Override the badge so the user can see
|
|
425
434
|
# the charger is actually busy.
|
|
426
435
|
label, color = STATUS_BADGE_MAP.get("charging", (_("Charging"), "#198754"))
|
|
436
|
+
elif (
|
|
437
|
+
not has_session
|
|
438
|
+
and key in {"charging", "finishing"}
|
|
439
|
+
and error_code_lower in ERROR_OK_VALUES
|
|
440
|
+
):
|
|
441
|
+
# Some chargers continue reporting "Charging" after a session ends.
|
|
442
|
+
# When no active transaction exists, surface the state as available
|
|
443
|
+
# so the UI reflects the actual behaviour at the site.
|
|
444
|
+
label, color = STATUS_BADGE_MAP.get("available", (_("Available"), "#0d6efd"))
|
|
427
445
|
elif error_code and error_code_lower not in ERROR_OK_VALUES:
|
|
428
446
|
label = _("%(status)s (%(error)s)") % {
|
|
429
447
|
"status": label,
|
pages/admin.py
CHANGED
|
@@ -20,10 +20,11 @@ from django.conf import settings
|
|
|
20
20
|
from django.utils.translation import gettext_lazy as _, ngettext
|
|
21
21
|
from django.core.management import CommandError, call_command
|
|
22
22
|
|
|
23
|
-
from nodes.models import Node
|
|
23
|
+
from nodes.models import Node, NodeRole
|
|
24
24
|
from nodes.utils import capture_screenshot, save_screenshot
|
|
25
25
|
|
|
26
26
|
from .forms import UserManualAdminForm
|
|
27
|
+
from .module_defaults import reload_default_modules as restore_default_modules
|
|
27
28
|
from .utils import landing_leads_supported
|
|
28
29
|
|
|
29
30
|
from .models import (
|
|
@@ -229,16 +230,118 @@ class ApplicationAdmin(EntityModelAdmin):
|
|
|
229
230
|
class LandingInline(admin.TabularInline):
|
|
230
231
|
model = Landing
|
|
231
232
|
extra = 0
|
|
232
|
-
fields = ("path", "label", "enabled"
|
|
233
|
+
fields = ("path", "label", "enabled")
|
|
234
|
+
show_change_link = True
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@admin.register(Landing)
|
|
238
|
+
class LandingAdmin(EntityModelAdmin):
|
|
239
|
+
list_display = ("label", "path", "module", "enabled")
|
|
240
|
+
list_filter = ("enabled", "module__node_role", "module__application")
|
|
241
|
+
search_fields = (
|
|
242
|
+
"label",
|
|
243
|
+
"path",
|
|
244
|
+
"description",
|
|
245
|
+
"module__path",
|
|
246
|
+
"module__application__name",
|
|
247
|
+
"module__node_role__name",
|
|
248
|
+
)
|
|
249
|
+
fields = ("module", "path", "label", "enabled", "description")
|
|
250
|
+
list_select_related = ("module", "module__application", "module__node_role")
|
|
233
251
|
|
|
234
252
|
|
|
235
253
|
@admin.register(Module)
|
|
236
254
|
class ModuleAdmin(EntityModelAdmin):
|
|
255
|
+
change_list_template = "admin/pages/module/change_list.html"
|
|
237
256
|
list_display = ("application", "node_role", "path", "menu", "is_default")
|
|
238
257
|
list_filter = ("node_role", "application")
|
|
239
258
|
fields = ("node_role", "application", "path", "menu", "is_default", "favicon")
|
|
240
259
|
inlines = [LandingInline]
|
|
241
260
|
|
|
261
|
+
def get_urls(self):
|
|
262
|
+
urls = super().get_urls()
|
|
263
|
+
custom = [
|
|
264
|
+
path(
|
|
265
|
+
"reload-default-modules/",
|
|
266
|
+
self.admin_site.admin_view(self.reload_default_modules_view),
|
|
267
|
+
name="pages_module_reload_default_modules",
|
|
268
|
+
),
|
|
269
|
+
]
|
|
270
|
+
return custom + urls
|
|
271
|
+
|
|
272
|
+
def reload_default_modules_view(self, request):
|
|
273
|
+
if request.method != "POST":
|
|
274
|
+
return redirect("..")
|
|
275
|
+
|
|
276
|
+
summary = restore_default_modules(Application, Module, Landing, NodeRole)
|
|
277
|
+
|
|
278
|
+
if summary.roles_processed == 0:
|
|
279
|
+
self.message_user(
|
|
280
|
+
request,
|
|
281
|
+
_("No default modules were reloaded because the required node roles are missing."),
|
|
282
|
+
messages.WARNING,
|
|
283
|
+
)
|
|
284
|
+
elif summary.has_changes:
|
|
285
|
+
parts: list[str] = []
|
|
286
|
+
if summary.modules_created:
|
|
287
|
+
parts.append(
|
|
288
|
+
ngettext(
|
|
289
|
+
"%(count)d module created",
|
|
290
|
+
"%(count)d modules created",
|
|
291
|
+
summary.modules_created,
|
|
292
|
+
)
|
|
293
|
+
% {"count": summary.modules_created}
|
|
294
|
+
)
|
|
295
|
+
if summary.modules_updated:
|
|
296
|
+
parts.append(
|
|
297
|
+
ngettext(
|
|
298
|
+
"%(count)d module updated",
|
|
299
|
+
"%(count)d modules updated",
|
|
300
|
+
summary.modules_updated,
|
|
301
|
+
)
|
|
302
|
+
% {"count": summary.modules_updated}
|
|
303
|
+
)
|
|
304
|
+
if summary.landings_created:
|
|
305
|
+
parts.append(
|
|
306
|
+
ngettext(
|
|
307
|
+
"%(count)d landing created",
|
|
308
|
+
"%(count)d landings created",
|
|
309
|
+
summary.landings_created,
|
|
310
|
+
)
|
|
311
|
+
% {"count": summary.landings_created}
|
|
312
|
+
)
|
|
313
|
+
if summary.landings_updated:
|
|
314
|
+
parts.append(
|
|
315
|
+
ngettext(
|
|
316
|
+
"%(count)d landing updated",
|
|
317
|
+
"%(count)d landings updated",
|
|
318
|
+
summary.landings_updated,
|
|
319
|
+
)
|
|
320
|
+
% {"count": summary.landings_updated}
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
details = "; ".join(parts)
|
|
324
|
+
if details:
|
|
325
|
+
message = _(
|
|
326
|
+
"Reloaded default modules for %(roles)d role(s). %(details)s."
|
|
327
|
+
) % {"roles": summary.roles_processed, "details": details}
|
|
328
|
+
else:
|
|
329
|
+
message = _(
|
|
330
|
+
"Reloaded default modules for %(roles)d role(s)."
|
|
331
|
+
) % {"roles": summary.roles_processed}
|
|
332
|
+
self.message_user(request, message, messages.SUCCESS)
|
|
333
|
+
else:
|
|
334
|
+
self.message_user(
|
|
335
|
+
request,
|
|
336
|
+
_(
|
|
337
|
+
"Default modules are already up to date for %(roles)d role(s)."
|
|
338
|
+
)
|
|
339
|
+
% {"roles": summary.roles_processed},
|
|
340
|
+
messages.INFO,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
return redirect("..")
|
|
344
|
+
|
|
242
345
|
|
|
243
346
|
@admin.register(LandingLead)
|
|
244
347
|
class LandingLeadAdmin(EntityModelAdmin):
|
|
@@ -522,13 +625,22 @@ class UserStoryAdmin(EntityModelAdmin):
|
|
|
522
625
|
"name",
|
|
523
626
|
"rating",
|
|
524
627
|
"path",
|
|
628
|
+
"status",
|
|
525
629
|
"submitted_at",
|
|
526
630
|
"github_issue_display",
|
|
527
631
|
"take_screenshot",
|
|
528
632
|
"owner",
|
|
633
|
+
"assign_to",
|
|
634
|
+
)
|
|
635
|
+
list_filter = ("rating", "status", "submitted_at", "take_screenshot")
|
|
636
|
+
search_fields = (
|
|
637
|
+
"name",
|
|
638
|
+
"comments",
|
|
639
|
+
"path",
|
|
640
|
+
"referer",
|
|
641
|
+
"github_issue_url",
|
|
642
|
+
"ip_address",
|
|
529
643
|
)
|
|
530
|
-
list_filter = ("rating", "submitted_at", "take_screenshot")
|
|
531
|
-
search_fields = ("name", "comments", "path", "github_issue_url")
|
|
532
644
|
readonly_fields = (
|
|
533
645
|
"name",
|
|
534
646
|
"rating",
|
|
@@ -537,6 +649,10 @@ class UserStoryAdmin(EntityModelAdmin):
|
|
|
537
649
|
"path",
|
|
538
650
|
"user",
|
|
539
651
|
"owner",
|
|
652
|
+
"referer",
|
|
653
|
+
"user_agent",
|
|
654
|
+
"ip_address",
|
|
655
|
+
"created_on",
|
|
540
656
|
"submitted_at",
|
|
541
657
|
"github_issue_number",
|
|
542
658
|
"github_issue_url",
|
|
@@ -550,6 +666,12 @@ class UserStoryAdmin(EntityModelAdmin):
|
|
|
550
666
|
"path",
|
|
551
667
|
"user",
|
|
552
668
|
"owner",
|
|
669
|
+
"status",
|
|
670
|
+
"assign_to",
|
|
671
|
+
"referer",
|
|
672
|
+
"user_agent",
|
|
673
|
+
"ip_address",
|
|
674
|
+
"created_on",
|
|
553
675
|
"submitted_at",
|
|
554
676
|
"github_issue_number",
|
|
555
677
|
"github_issue_url",
|
pages/context_processors.py
CHANGED
|
@@ -50,6 +50,13 @@ def nav_links(request):
|
|
|
50
50
|
valid_modules = []
|
|
51
51
|
datasette_enabled = False
|
|
52
52
|
current_module = None
|
|
53
|
+
user = getattr(request, "user", None)
|
|
54
|
+
user_is_authenticated = getattr(user, "is_authenticated", False)
|
|
55
|
+
user_is_superuser = getattr(user, "is_superuser", False)
|
|
56
|
+
if user_is_authenticated:
|
|
57
|
+
user_group_names = set(user.groups.values_list("name", flat=True))
|
|
58
|
+
else:
|
|
59
|
+
user_group_names = set()
|
|
53
60
|
for module in modules:
|
|
54
61
|
landings = []
|
|
55
62
|
for landing in module.landings.filter(enabled=True):
|
|
@@ -62,7 +69,19 @@ def nav_links(request):
|
|
|
62
69
|
if not requires_login and hasattr(view_func, "login_url"):
|
|
63
70
|
requires_login = True
|
|
64
71
|
staff_only = getattr(view_func, "staff_required", False)
|
|
65
|
-
|
|
72
|
+
required_groups = getattr(
|
|
73
|
+
view_func, "required_security_groups", frozenset()
|
|
74
|
+
)
|
|
75
|
+
if required_groups:
|
|
76
|
+
requires_login = True
|
|
77
|
+
setattr(landing, "requires_login", True)
|
|
78
|
+
if not user_is_authenticated:
|
|
79
|
+
continue
|
|
80
|
+
if not user_is_superuser and not (
|
|
81
|
+
user_group_names & set(required_groups)
|
|
82
|
+
):
|
|
83
|
+
continue
|
|
84
|
+
elif requires_login and not user_is_authenticated:
|
|
66
85
|
setattr(landing, "requires_login", True)
|
|
67
86
|
if staff_only and not request.user.is_staff:
|
|
68
87
|
continue
|
pages/models.py
CHANGED
|
@@ -470,7 +470,7 @@ class Favorite(Entity):
|
|
|
470
470
|
unique_together = ("user", "content_type")
|
|
471
471
|
|
|
472
472
|
|
|
473
|
-
class UserStory(
|
|
473
|
+
class UserStory(Lead):
|
|
474
474
|
path = models.CharField(max_length=500)
|
|
475
475
|
name = models.CharField(max_length=40, blank=True)
|
|
476
476
|
rating = models.PositiveSmallIntegerField(
|
|
@@ -624,6 +624,8 @@ def _queue_low_rating_user_story_issue(
|
|
|
624
624
|
return
|
|
625
625
|
if instance.github_issue_url:
|
|
626
626
|
return
|
|
627
|
+
if not instance.user_id:
|
|
628
|
+
return
|
|
627
629
|
if not _is_celery_enabled():
|
|
628
630
|
return
|
|
629
631
|
|
pages/module_defaults.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Utilities to restore default navigation modules."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Iterable, Mapping
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
ModuleDefinition = Mapping[str, object]
|
|
9
|
+
LandingDefinition = tuple[str, str]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
ROLE_MODULE_DEFAULTS: Mapping[str, tuple[ModuleDefinition, ...]] = {
|
|
13
|
+
"Constellation": (
|
|
14
|
+
{
|
|
15
|
+
"application": "ocpp",
|
|
16
|
+
"path": "/ocpp/",
|
|
17
|
+
"menu": "Chargers",
|
|
18
|
+
"landings": (
|
|
19
|
+
("/ocpp/", "CPMS Online Dashboard"),
|
|
20
|
+
("/ocpp/simulator/", "Charge Point Simulator"),
|
|
21
|
+
("/ocpp/rfid/", "RFID Tag Validator"),
|
|
22
|
+
),
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"application": "awg",
|
|
26
|
+
"path": "/awg/",
|
|
27
|
+
"menu": "",
|
|
28
|
+
"landings": (
|
|
29
|
+
("/awg/", "AWG Calculator"),
|
|
30
|
+
("/awg/energy-tariff/", "Energy Tariff Calculator"),
|
|
31
|
+
),
|
|
32
|
+
},
|
|
33
|
+
),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class ReloadSummary:
|
|
39
|
+
"""Report about the changes performed while restoring defaults."""
|
|
40
|
+
|
|
41
|
+
roles_processed: int = 0
|
|
42
|
+
modules_created: int = 0
|
|
43
|
+
modules_updated: int = 0
|
|
44
|
+
landings_created: int = 0
|
|
45
|
+
landings_updated: int = 0
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def has_changes(self) -> bool:
|
|
49
|
+
return any(
|
|
50
|
+
(
|
|
51
|
+
self.modules_created,
|
|
52
|
+
self.modules_updated,
|
|
53
|
+
self.landings_created,
|
|
54
|
+
self.landings_updated,
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _manager(model, name: str):
|
|
60
|
+
manager = getattr(model, name, None)
|
|
61
|
+
if manager is not None:
|
|
62
|
+
return manager
|
|
63
|
+
return model.objects
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def reload_default_modules(Application, Module, Landing, NodeRole) -> ReloadSummary:
|
|
67
|
+
"""Ensure default navigation modules exist for the configured roles."""
|
|
68
|
+
|
|
69
|
+
summary = ReloadSummary()
|
|
70
|
+
application_manager = _manager(Application, "all_objects")
|
|
71
|
+
module_manager = _manager(Module, "all_objects")
|
|
72
|
+
landing_manager = _manager(Landing, "all_objects")
|
|
73
|
+
role_manager = _manager(NodeRole, "all_objects")
|
|
74
|
+
|
|
75
|
+
for role_name, module_definitions in ROLE_MODULE_DEFAULTS.items():
|
|
76
|
+
try:
|
|
77
|
+
role = role_manager.get(name=role_name)
|
|
78
|
+
except NodeRole.DoesNotExist:
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
summary.roles_processed += 1
|
|
82
|
+
|
|
83
|
+
for definition in module_definitions:
|
|
84
|
+
app_name: str = definition["application"] # type: ignore[assignment]
|
|
85
|
+
try:
|
|
86
|
+
application = application_manager.get(name=app_name)
|
|
87
|
+
except Application.DoesNotExist:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
module, created = module_manager.get_or_create(
|
|
91
|
+
node_role=role,
|
|
92
|
+
path=definition["path"],
|
|
93
|
+
defaults={
|
|
94
|
+
"application": application,
|
|
95
|
+
"menu": definition["menu"],
|
|
96
|
+
"is_seed_data": True,
|
|
97
|
+
"is_deleted": False,
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
if created:
|
|
101
|
+
summary.modules_created += 1
|
|
102
|
+
|
|
103
|
+
module_updates: list[str] = []
|
|
104
|
+
if module.application_id != application.id:
|
|
105
|
+
module.application = application
|
|
106
|
+
module_updates.append("application")
|
|
107
|
+
if module.menu != definition["menu"]:
|
|
108
|
+
module.menu = definition["menu"] # type: ignore[index]
|
|
109
|
+
module_updates.append("menu")
|
|
110
|
+
if getattr(module, "is_deleted", False):
|
|
111
|
+
module.is_deleted = False
|
|
112
|
+
module_updates.append("is_deleted")
|
|
113
|
+
if not getattr(module, "is_seed_data", False):
|
|
114
|
+
module.is_seed_data = True
|
|
115
|
+
module_updates.append("is_seed_data")
|
|
116
|
+
if module_updates:
|
|
117
|
+
module.save(update_fields=module_updates)
|
|
118
|
+
if not created:
|
|
119
|
+
summary.modules_updated += 1
|
|
120
|
+
|
|
121
|
+
landings: Iterable[tuple[str, str]] = definition["landings"] # type: ignore[index]
|
|
122
|
+
for path, label in landings:
|
|
123
|
+
landing, landing_created = landing_manager.get_or_create(
|
|
124
|
+
module=module,
|
|
125
|
+
path=path,
|
|
126
|
+
defaults={
|
|
127
|
+
"label": label,
|
|
128
|
+
"description": "",
|
|
129
|
+
"enabled": True,
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
if landing_created:
|
|
133
|
+
summary.landings_created += 1
|
|
134
|
+
|
|
135
|
+
landing_updates: list[str] = []
|
|
136
|
+
if landing.label != label:
|
|
137
|
+
landing.label = label
|
|
138
|
+
landing_updates.append("label")
|
|
139
|
+
if landing.description:
|
|
140
|
+
landing.description = ""
|
|
141
|
+
landing_updates.append("description")
|
|
142
|
+
if not landing.enabled:
|
|
143
|
+
landing.enabled = True
|
|
144
|
+
landing_updates.append("enabled")
|
|
145
|
+
if getattr(landing, "is_deleted", False):
|
|
146
|
+
landing.is_deleted = False
|
|
147
|
+
landing_updates.append("is_deleted")
|
|
148
|
+
if not getattr(landing, "is_seed_data", False):
|
|
149
|
+
landing.is_seed_data = True
|
|
150
|
+
landing_updates.append("is_seed_data")
|
|
151
|
+
if landing_updates:
|
|
152
|
+
landing.save(update_fields=landing_updates)
|
|
153
|
+
if not landing_created:
|
|
154
|
+
summary.landings_updated += 1
|
|
155
|
+
|
|
156
|
+
return summary
|