arthexis 0.1.16__py3-none-any.whl → 0.1.28__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 (67) hide show
  1. {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/METADATA +95 -41
  2. arthexis-0.1.28.dist-info/RECORD +112 -0
  3. config/asgi.py +1 -15
  4. config/middleware.py +47 -1
  5. config/settings.py +21 -30
  6. config/settings_helpers.py +176 -1
  7. config/urls.py +69 -1
  8. core/admin.py +805 -473
  9. core/apps.py +6 -8
  10. core/auto_upgrade.py +19 -4
  11. core/backends.py +13 -3
  12. core/celery_utils.py +73 -0
  13. core/changelog.py +66 -5
  14. core/environment.py +4 -5
  15. core/models.py +1825 -218
  16. core/notifications.py +1 -1
  17. core/reference_utils.py +10 -11
  18. core/release.py +55 -7
  19. core/sigil_builder.py +2 -2
  20. core/sigil_resolver.py +1 -66
  21. core/system.py +285 -4
  22. core/tasks.py +439 -138
  23. core/test_system_info.py +43 -5
  24. core/tests.py +516 -18
  25. core/user_data.py +94 -21
  26. core/views.py +348 -186
  27. nodes/admin.py +904 -67
  28. nodes/apps.py +12 -1
  29. nodes/feature_checks.py +30 -0
  30. nodes/models.py +800 -127
  31. nodes/rfid_sync.py +1 -1
  32. nodes/tasks.py +98 -3
  33. nodes/tests.py +1381 -152
  34. nodes/urls.py +15 -1
  35. nodes/utils.py +51 -3
  36. nodes/views.py +1382 -152
  37. ocpp/admin.py +1970 -152
  38. ocpp/consumers.py +839 -34
  39. ocpp/models.py +968 -17
  40. ocpp/network.py +398 -0
  41. ocpp/store.py +411 -43
  42. ocpp/tasks.py +261 -3
  43. ocpp/test_export_import.py +1 -0
  44. ocpp/test_rfid.py +194 -6
  45. ocpp/tests.py +1918 -87
  46. ocpp/transactions_io.py +9 -1
  47. ocpp/urls.py +8 -3
  48. ocpp/views.py +700 -53
  49. pages/admin.py +262 -30
  50. pages/apps.py +35 -0
  51. pages/context_processors.py +28 -21
  52. pages/defaults.py +1 -1
  53. pages/forms.py +31 -8
  54. pages/middleware.py +6 -2
  55. pages/models.py +86 -2
  56. pages/module_defaults.py +5 -5
  57. pages/site_config.py +137 -0
  58. pages/tests.py +1050 -126
  59. pages/urls.py +14 -2
  60. pages/utils.py +70 -0
  61. pages/views.py +622 -56
  62. arthexis-0.1.16.dist-info/RECORD +0 -111
  63. core/workgroup_urls.py +0 -17
  64. core/workgroup_views.py +0 -94
  65. {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/WHEEL +0 -0
  66. {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/licenses/LICENSE +0 -0
  67. {arthexis-0.1.16.dist-info → arthexis-0.1.28.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
@@ -53,6 +56,11 @@ class Application(Entity):
53
56
  return self.name
54
57
 
55
58
 
59
+ class Meta:
60
+ verbose_name = _("Application")
61
+ verbose_name_plural = _("Applications")
62
+
63
+
56
64
  class ModuleManager(models.Manager):
57
65
  def get_by_natural_key(self, role: str, path: str):
58
66
  return self.get(node_role__name=role, path=path)
@@ -212,12 +220,15 @@ class Landing(Entity):
212
220
  path = models.CharField(max_length=200)
213
221
  label = models.CharField(max_length=100)
214
222
  enabled = models.BooleanField(default=True)
223
+ track_leads = models.BooleanField(default=False)
215
224
  description = models.TextField(blank=True)
216
225
 
217
226
  objects = LandingManager()
218
227
 
219
228
  class Meta:
220
229
  unique_together = ("module", "path")
230
+ verbose_name = _("Landing")
231
+ verbose_name_plural = _("Landings")
221
232
 
222
233
  def __str__(self) -> str: # pragma: no cover - simple representation
223
234
  return f"{self.label} ({self.path})"
@@ -435,6 +446,39 @@ class UserManual(Entity):
435
446
  def natural_key(self): # pragma: no cover - simple representation
436
447
  return (self.slug,)
437
448
 
449
+ def _ensure_pdf_is_base64(self) -> None:
450
+ """Normalize ``content_pdf`` so stored values are base64 strings."""
451
+
452
+ value = self.content_pdf
453
+ if value in {None, ""}:
454
+ self.content_pdf = "" if value is None else value
455
+ return
456
+
457
+ if isinstance(value, (bytes, bytearray, memoryview)):
458
+ self.content_pdf = base64.b64encode(bytes(value)).decode("ascii")
459
+ return
460
+
461
+ reader = getattr(value, "read", None)
462
+ if callable(reader):
463
+ data = reader()
464
+ if hasattr(value, "seek"):
465
+ try:
466
+ value.seek(0)
467
+ except Exception: # pragma: no cover - best effort reset
468
+ pass
469
+ self.content_pdf = base64.b64encode(data).decode("ascii")
470
+ return
471
+
472
+ if isinstance(value, str):
473
+ stripped = value.strip()
474
+ if stripped.startswith("data:"):
475
+ _, _, encoded = stripped.partition(",")
476
+ self.content_pdf = encoded.strip()
477
+
478
+ def save(self, *args, **kwargs):
479
+ self._ensure_pdf_is_base64()
480
+ super().save(*args, **kwargs)
481
+
438
482
 
439
483
  class ViewHistory(Entity):
440
484
  """Record of public site visits."""
@@ -455,6 +499,14 @@ class ViewHistory(Entity):
455
499
  def __str__(self) -> str: # pragma: no cover - simple representation
456
500
  return f"{self.method} {self.path} ({self.status_code})"
457
501
 
502
+ @classmethod
503
+ def purge_older_than(cls, *, days: int) -> int:
504
+ """Delete history entries recorded more than ``days`` days ago."""
505
+
506
+ cutoff = timezone.now() - timedelta(days=days)
507
+ deleted, _ = cls.objects.filter(visited_at__lt=cutoff).delete()
508
+ return deleted
509
+
458
510
 
459
511
  class Favorite(Entity):
460
512
  user = models.ForeignKey(
@@ -465,9 +517,13 @@ class Favorite(Entity):
465
517
  content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
466
518
  custom_label = models.CharField(max_length=100, blank=True)
467
519
  user_data = models.BooleanField(default=False)
520
+ priority = models.IntegerField(default=0)
468
521
 
469
522
  class Meta:
470
523
  unique_together = ("user", "content_type")
524
+ ordering = ["priority", "pk"]
525
+ verbose_name = _("Favorite")
526
+ verbose_name_plural = _("Favorites")
471
527
 
472
528
 
473
529
  class UserStory(Lead):
@@ -510,6 +566,19 @@ class UserStory(Lead):
510
566
  blank=True,
511
567
  help_text=_("Link to the GitHub issue created for this feedback."),
512
568
  )
569
+ screenshot = models.ForeignKey(
570
+ ContentSample,
571
+ on_delete=models.SET_NULL,
572
+ blank=True,
573
+ null=True,
574
+ related_name="user_stories",
575
+ help_text=_("Screenshot captured for this feedback."),
576
+ )
577
+ language_code = models.CharField(
578
+ max_length=15,
579
+ blank=True,
580
+ help_text=_("Language selected when the feedback was submitted."),
581
+ )
513
582
 
514
583
  class Meta:
515
584
  ordering = ["-submitted_at"]
@@ -555,6 +624,21 @@ class UserStory(Lead):
555
624
  f"**Screenshot requested:** {screenshot_requested}",
556
625
  ]
557
626
 
627
+ language_code = (self.language_code or "").strip()
628
+ if language_code:
629
+ normalized = language_code.replace("_", "-").lower()
630
+ try:
631
+ info = get_language_info(normalized)
632
+ except KeyError:
633
+ language_display = ""
634
+ else:
635
+ language_display = info.get("name_local") or info.get("name") or ""
636
+
637
+ if language_display:
638
+ lines.append(f"**Language:** {language_display} ({normalized})")
639
+ else:
640
+ lines.append(f"**Language:** {normalized}")
641
+
558
642
  if self.submitted_at:
559
643
  lines.append(f"**Submitted at:** {self.submitted_at.isoformat()}")
560
644
 
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
+ )