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.
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 = ("enabled", "module__node_role", "module__application")
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
- log_content = selected_path.read_text(encoding="utf-8")
904
- except UnicodeDecodeError:
905
- log_content = selected_path.read_text(
906
- encoding="utf-8", errors="replace"
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)
@@ -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
- "Constellation": "favicon_constellation.txt",
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
- continue
80
- if not user_is_superuser and not (
80
+ blocked_reason = "login"
81
+ elif not user_is_superuser and not (
81
82
  user_group_names & set(required_groups)
82
83
  ):
83
- continue
84
+ blocked_reason = "permission"
84
85
  elif requires_login and not user_is_authenticated:
85
- setattr(landing, "requires_login", True)
86
- if staff_only and not request.user.is_staff:
87
- continue
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 = "Calculate"
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
@@ -126,6 +126,9 @@ class ViewHistoryMiddleware:
126
126
  if request.method.upper() != "GET":
127
127
  return
128
128
 
129
+ if not getattr(landing, "track_leads", False):
130
+ return
131
+
129
132
  if not landing_leads_supported():
130
133
  return
131
134
 
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
- "Constellation": (
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
  },