arthexis 0.1.16__py3-none-any.whl → 0.1.26__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.

Files changed (63) hide show
  1. {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/METADATA +84 -35
  2. arthexis-0.1.26.dist-info/RECORD +111 -0
  3. config/asgi.py +1 -15
  4. config/middleware.py +47 -1
  5. config/settings.py +15 -30
  6. config/urls.py +53 -1
  7. core/admin.py +540 -450
  8. core/apps.py +0 -6
  9. core/auto_upgrade.py +19 -4
  10. core/backends.py +13 -3
  11. core/changelog.py +66 -5
  12. core/environment.py +4 -5
  13. core/models.py +1566 -203
  14. core/notifications.py +1 -1
  15. core/reference_utils.py +10 -11
  16. core/release.py +55 -7
  17. core/sigil_builder.py +2 -2
  18. core/sigil_resolver.py +1 -66
  19. core/system.py +268 -2
  20. core/tasks.py +174 -48
  21. core/tests.py +314 -16
  22. core/user_data.py +42 -2
  23. core/views.py +278 -183
  24. nodes/admin.py +557 -65
  25. nodes/apps.py +11 -0
  26. nodes/models.py +658 -113
  27. nodes/rfid_sync.py +1 -1
  28. nodes/tasks.py +97 -2
  29. nodes/tests.py +1212 -116
  30. nodes/urls.py +15 -1
  31. nodes/utils.py +51 -3
  32. nodes/views.py +1239 -154
  33. ocpp/admin.py +979 -152
  34. ocpp/consumers.py +268 -28
  35. ocpp/models.py +488 -3
  36. ocpp/network.py +398 -0
  37. ocpp/store.py +6 -4
  38. ocpp/tasks.py +296 -2
  39. ocpp/test_export_import.py +1 -0
  40. ocpp/test_rfid.py +121 -4
  41. ocpp/tests.py +950 -11
  42. ocpp/transactions_io.py +9 -1
  43. ocpp/urls.py +3 -3
  44. ocpp/views.py +596 -51
  45. pages/admin.py +262 -30
  46. pages/apps.py +35 -0
  47. pages/context_processors.py +26 -21
  48. pages/defaults.py +1 -1
  49. pages/forms.py +31 -8
  50. pages/middleware.py +6 -2
  51. pages/models.py +77 -2
  52. pages/module_defaults.py +5 -5
  53. pages/site_config.py +137 -0
  54. pages/tests.py +885 -109
  55. pages/urls.py +13 -2
  56. pages/utils.py +70 -0
  57. pages/views.py +558 -55
  58. arthexis-0.1.16.dist-info/RECORD +0 -111
  59. core/workgroup_urls.py +0 -17
  60. core/workgroup_views.py +0 -94
  61. {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
  62. {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +0 -0
  63. {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/top_level.txt +0 -0
pages/middleware.py CHANGED
@@ -10,7 +10,7 @@ from django.conf import settings
10
10
  from django.urls import Resolver404, resolve
11
11
 
12
12
  from .models import Landing, LandingLead, ViewHistory
13
- from .utils import landing_leads_supported
13
+ from .utils import cache_original_referer, get_original_referer, landing_leads_supported
14
14
 
15
15
 
16
16
  logger = logging.getLogger(__name__)
@@ -30,6 +30,7 @@ class ViewHistoryMiddleware:
30
30
  )
31
31
 
32
32
  def __call__(self, request):
33
+ cache_original_referer(request)
33
34
  should_track = self._should_track(request)
34
35
  if not should_track:
35
36
  return self.get_response(request)
@@ -126,10 +127,13 @@ class ViewHistoryMiddleware:
126
127
  if request.method.upper() != "GET":
127
128
  return
128
129
 
130
+ if not getattr(landing, "track_leads", False):
131
+ return
132
+
129
133
  if not landing_leads_supported():
130
134
  return
131
135
 
132
- referer = request.META.get("HTTP_REFERER", "") or ""
136
+ referer = get_original_referer(request)
133
137
  user_agent = request.META.get("HTTP_USER_AGENT", "") or ""
134
138
  ip_address = self._extract_client_ip(request) or None
135
139
  user = getattr(request, "user", None)
pages/models.py CHANGED
@@ -1,4 +1,6 @@
1
+ import base64
1
2
  import logging
3
+ from datetime import timedelta
2
4
  from pathlib import Path
3
5
 
4
6
  from django.db import models
@@ -6,10 +8,11 @@ from django.db.models import Q
6
8
  from core.entity import Entity
7
9
  from core.models import Lead, SecurityGroup
8
10
  from django.contrib.sites.models import Site
9
- from nodes.models import NodeRole
11
+ from nodes.models import ContentSample, NodeRole
10
12
  from django.apps import apps as django_apps
13
+ from django.utils import timezone
11
14
  from django.utils.text import slugify
12
- from django.utils.translation import gettext, gettext_lazy as _
15
+ from django.utils.translation import gettext, gettext_lazy as _, get_language_info
13
16
  from importlib import import_module
14
17
  from django.urls import URLPattern
15
18
  from django.conf import settings
@@ -212,6 +215,7 @@ class Landing(Entity):
212
215
  path = models.CharField(max_length=200)
213
216
  label = models.CharField(max_length=100)
214
217
  enabled = models.BooleanField(default=True)
218
+ track_leads = models.BooleanField(default=False)
215
219
  description = models.TextField(blank=True)
216
220
 
217
221
  objects = LandingManager()
@@ -435,6 +439,39 @@ class UserManual(Entity):
435
439
  def natural_key(self): # pragma: no cover - simple representation
436
440
  return (self.slug,)
437
441
 
442
+ def _ensure_pdf_is_base64(self) -> None:
443
+ """Normalize ``content_pdf`` so stored values are base64 strings."""
444
+
445
+ value = self.content_pdf
446
+ if value in {None, ""}:
447
+ self.content_pdf = "" if value is None else value
448
+ return
449
+
450
+ if isinstance(value, (bytes, bytearray, memoryview)):
451
+ self.content_pdf = base64.b64encode(bytes(value)).decode("ascii")
452
+ return
453
+
454
+ reader = getattr(value, "read", None)
455
+ if callable(reader):
456
+ data = reader()
457
+ if hasattr(value, "seek"):
458
+ try:
459
+ value.seek(0)
460
+ except Exception: # pragma: no cover - best effort reset
461
+ pass
462
+ self.content_pdf = base64.b64encode(data).decode("ascii")
463
+ return
464
+
465
+ if isinstance(value, str):
466
+ stripped = value.strip()
467
+ if stripped.startswith("data:"):
468
+ _, _, encoded = stripped.partition(",")
469
+ self.content_pdf = encoded.strip()
470
+
471
+ def save(self, *args, **kwargs):
472
+ self._ensure_pdf_is_base64()
473
+ super().save(*args, **kwargs)
474
+
438
475
 
439
476
  class ViewHistory(Entity):
440
477
  """Record of public site visits."""
@@ -455,6 +492,14 @@ class ViewHistory(Entity):
455
492
  def __str__(self) -> str: # pragma: no cover - simple representation
456
493
  return f"{self.method} {self.path} ({self.status_code})"
457
494
 
495
+ @classmethod
496
+ def purge_older_than(cls, *, days: int) -> int:
497
+ """Delete history entries recorded more than ``days`` days ago."""
498
+
499
+ cutoff = timezone.now() - timedelta(days=days)
500
+ deleted, _ = cls.objects.filter(visited_at__lt=cutoff).delete()
501
+ return deleted
502
+
458
503
 
459
504
  class Favorite(Entity):
460
505
  user = models.ForeignKey(
@@ -465,9 +510,11 @@ class Favorite(Entity):
465
510
  content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
466
511
  custom_label = models.CharField(max_length=100, blank=True)
467
512
  user_data = models.BooleanField(default=False)
513
+ priority = models.IntegerField(default=0)
468
514
 
469
515
  class Meta:
470
516
  unique_together = ("user", "content_type")
517
+ ordering = ["priority", "pk"]
471
518
 
472
519
 
473
520
  class UserStory(Lead):
@@ -510,6 +557,19 @@ class UserStory(Lead):
510
557
  blank=True,
511
558
  help_text=_("Link to the GitHub issue created for this feedback."),
512
559
  )
560
+ screenshot = models.ForeignKey(
561
+ ContentSample,
562
+ on_delete=models.SET_NULL,
563
+ blank=True,
564
+ null=True,
565
+ related_name="user_stories",
566
+ help_text=_("Screenshot captured for this feedback."),
567
+ )
568
+ language_code = models.CharField(
569
+ max_length=15,
570
+ blank=True,
571
+ help_text=_("Language selected when the feedback was submitted."),
572
+ )
513
573
 
514
574
  class Meta:
515
575
  ordering = ["-submitted_at"]
@@ -555,6 +615,21 @@ class UserStory(Lead):
555
615
  f"**Screenshot requested:** {screenshot_requested}",
556
616
  ]
557
617
 
618
+ language_code = (self.language_code or "").strip()
619
+ if language_code:
620
+ normalized = language_code.replace("_", "-").lower()
621
+ try:
622
+ info = get_language_info(normalized)
623
+ except KeyError:
624
+ language_display = ""
625
+ else:
626
+ language_display = info.get("name_local") or info.get("name") or ""
627
+
628
+ if language_display:
629
+ lines.append(f"**Language:** {language_display} ({normalized})")
630
+ else:
631
+ lines.append(f"**Language:** {normalized}")
632
+
558
633
  if self.submitted_at:
559
634
  lines.append(f"**Submitted at:** {self.submitted_at.isoformat()}")
560
635
 
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
  },
pages/site_config.py ADDED
@@ -0,0 +1,137 @@
1
+ """Customizations for :mod:`django.contrib.sites`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ from pathlib import Path
8
+
9
+ from django.apps import apps
10
+ from django.conf import settings
11
+ from django.contrib.sites.models import Site
12
+ from django.db import DatabaseError, models
13
+ from django.db.models.signals import post_delete, post_migrate, post_save
14
+ from django.dispatch import receiver
15
+ from django.utils.translation import gettext_lazy as _
16
+
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ _FIELD_DEFINITIONS: tuple[tuple[str, models.Field], ...] = (
22
+ (
23
+ "managed",
24
+ models.BooleanField(
25
+ default=False,
26
+ db_default=False,
27
+ verbose_name=_("Managed by local NGINX"),
28
+ help_text=_(
29
+ "Include this site when staging the local NGINX configuration."
30
+ ),
31
+ ),
32
+ ),
33
+ (
34
+ "require_https",
35
+ models.BooleanField(
36
+ default=False,
37
+ db_default=False,
38
+ verbose_name=_("Require HTTPS"),
39
+ help_text=_(
40
+ "Redirect HTTP traffic to HTTPS when the staged NGINX configuration is applied."
41
+ ),
42
+ ),
43
+ ),
44
+ )
45
+
46
+
47
+ def _sites_config_path() -> Path:
48
+ return Path(settings.BASE_DIR) / "scripts" / "generated" / "nginx-sites.json"
49
+
50
+
51
+ def _ensure_directories(path: Path) -> bool:
52
+ try:
53
+ path.parent.mkdir(parents=True, exist_ok=True)
54
+ except OSError as exc: # pragma: no cover - filesystem errors
55
+ logger.warning("Unable to create directory for %s: %s", path, exc)
56
+ return False
57
+ return True
58
+
59
+
60
+ def update_local_nginx_scripts() -> None:
61
+ """Serialize managed site configuration for the network setup script."""
62
+
63
+ SiteModel = apps.get_model("sites", "Site")
64
+ data: list[dict[str, object]] = []
65
+ seen_domains: set[str] = set()
66
+
67
+ try:
68
+ sites = list(
69
+ SiteModel.objects.filter(managed=True)
70
+ .only("domain", "require_https")
71
+ .order_by("domain")
72
+ )
73
+ except DatabaseError: # pragma: no cover - database not ready
74
+ return
75
+
76
+ for site in sites:
77
+ domain = (site.domain or "").strip()
78
+ if not domain:
79
+ continue
80
+ if domain.lower() in seen_domains:
81
+ continue
82
+ seen_domains.add(domain.lower())
83
+ data.append({"domain": domain, "require_https": bool(site.require_https)})
84
+
85
+ output_path = _sites_config_path()
86
+ if not _ensure_directories(output_path):
87
+ return
88
+
89
+ if data:
90
+ try:
91
+ output_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
92
+ except OSError as exc: # pragma: no cover - filesystem errors
93
+ logger.warning("Failed to write managed site configuration: %s", exc)
94
+ else:
95
+ try:
96
+ output_path.unlink()
97
+ except FileNotFoundError:
98
+ pass
99
+ except OSError as exc: # pragma: no cover - filesystem errors
100
+ logger.warning("Failed to remove managed site configuration: %s", exc)
101
+
102
+
103
+ def _install_fields() -> None:
104
+ for name, field in _FIELD_DEFINITIONS:
105
+ if hasattr(Site, name):
106
+ continue
107
+ Site.add_to_class(name, field.clone())
108
+
109
+
110
+ def ensure_site_fields() -> None:
111
+ """Ensure the custom ``Site`` fields are installed."""
112
+
113
+ _install_fields()
114
+
115
+
116
+ @receiver(post_save, sender=Site, dispatch_uid="pages_site_save_update_nginx")
117
+ def _site_saved(sender, **kwargs) -> None: # pragma: no cover - signal wrapper
118
+ update_local_nginx_scripts()
119
+
120
+
121
+ @receiver(post_delete, sender=Site, dispatch_uid="pages_site_delete_update_nginx")
122
+ def _site_deleted(sender, **kwargs) -> None: # pragma: no cover - signal wrapper
123
+ update_local_nginx_scripts()
124
+
125
+
126
+ def _run_post_migrate_update(**kwargs) -> None: # pragma: no cover - signal wrapper
127
+ update_local_nginx_scripts()
128
+
129
+
130
+ def ready() -> None:
131
+ """Apply customizations and connect signal handlers."""
132
+
133
+ ensure_site_fields()
134
+ post_migrate.connect(
135
+ _run_post_migrate_update,
136
+ dispatch_uid="pages_site_post_migrate_update",
137
+ )