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.
- {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/METADATA +84 -35
- arthexis-0.1.26.dist-info/RECORD +111 -0
- config/asgi.py +1 -15
- config/middleware.py +47 -1
- config/settings.py +15 -30
- config/urls.py +53 -1
- core/admin.py +540 -450
- core/apps.py +0 -6
- core/auto_upgrade.py +19 -4
- core/backends.py +13 -3
- core/changelog.py +66 -5
- core/environment.py +4 -5
- core/models.py +1566 -203
- 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 +268 -2
- core/tasks.py +174 -48
- core/tests.py +314 -16
- core/user_data.py +42 -2
- core/views.py +278 -183
- nodes/admin.py +557 -65
- nodes/apps.py +11 -0
- nodes/models.py +658 -113
- nodes/rfid_sync.py +1 -1
- nodes/tasks.py +97 -2
- nodes/tests.py +1212 -116
- nodes/urls.py +15 -1
- nodes/utils.py +51 -3
- nodes/views.py +1239 -154
- ocpp/admin.py +979 -152
- ocpp/consumers.py +268 -28
- ocpp/models.py +488 -3
- ocpp/network.py +398 -0
- ocpp/store.py +6 -4
- ocpp/tasks.py +296 -2
- ocpp/test_export_import.py +1 -0
- ocpp/test_rfid.py +121 -4
- ocpp/tests.py +950 -11
- ocpp/transactions_io.py +9 -1
- ocpp/urls.py +3 -3
- ocpp/views.py +596 -51
- pages/admin.py +262 -30
- pages/apps.py +35 -0
- pages/context_processors.py +26 -21
- pages/defaults.py +1 -1
- pages/forms.py +31 -8
- pages/middleware.py +6 -2
- pages/models.py +77 -2
- pages/module_defaults.py +5 -5
- pages/site_config.py +137 -0
- pages/tests.py +885 -109
- pages/urls.py +13 -2
- pages/utils.py +70 -0
- pages/views.py +558 -55
- 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.26.dist-info}/WHEEL +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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
|
-
"
|
|
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
|
+
)
|