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.

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
- if _has_active_session(tx_obj):
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", "description")
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",
@@ -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
- if requires_login and not request.user.is_authenticated:
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(Entity):
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
 
@@ -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