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.
- {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/METADATA +95 -41
- arthexis-0.1.28.dist-info/RECORD +112 -0
- config/asgi.py +1 -15
- config/middleware.py +47 -1
- config/settings.py +21 -30
- config/settings_helpers.py +176 -1
- config/urls.py +69 -1
- core/admin.py +805 -473
- core/apps.py +6 -8
- core/auto_upgrade.py +19 -4
- core/backends.py +13 -3
- core/celery_utils.py +73 -0
- core/changelog.py +66 -5
- core/environment.py +4 -5
- core/models.py +1825 -218
- core/notifications.py +1 -1
- core/reference_utils.py +10 -11
- core/release.py +55 -7
- core/sigil_builder.py +2 -2
- core/sigil_resolver.py +1 -66
- core/system.py +285 -4
- core/tasks.py +439 -138
- core/test_system_info.py +43 -5
- core/tests.py +516 -18
- core/user_data.py +94 -21
- core/views.py +348 -186
- nodes/admin.py +904 -67
- nodes/apps.py +12 -1
- nodes/feature_checks.py +30 -0
- nodes/models.py +800 -127
- nodes/rfid_sync.py +1 -1
- nodes/tasks.py +98 -3
- nodes/tests.py +1381 -152
- nodes/urls.py +15 -1
- nodes/utils.py +51 -3
- nodes/views.py +1382 -152
- ocpp/admin.py +1970 -152
- ocpp/consumers.py +839 -34
- ocpp/models.py +968 -17
- ocpp/network.py +398 -0
- ocpp/store.py +411 -43
- ocpp/tasks.py +261 -3
- ocpp/test_export_import.py +1 -0
- ocpp/test_rfid.py +194 -6
- ocpp/tests.py +1918 -87
- ocpp/transactions_io.py +9 -1
- ocpp/urls.py +8 -3
- ocpp/views.py +700 -53
- pages/admin.py +262 -30
- pages/apps.py +35 -0
- pages/context_processors.py +28 -21
- pages/defaults.py +1 -1
- pages/forms.py +31 -8
- pages/middleware.py +6 -2
- pages/models.py +86 -2
- pages/module_defaults.py +5 -5
- pages/site_config.py +137 -0
- pages/tests.py +1050 -126
- pages/urls.py +14 -2
- pages/utils.py +70 -0
- pages/views.py +622 -56
- arthexis-0.1.16.dist-info/RECORD +0 -111
- core/workgroup_urls.py +0 -17
- core/workgroup_views.py +0 -94
- {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/WHEEL +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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
|
-
"
|
|
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
|
+
)
|