arthexis 0.1.9__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.26.dist-info/METADATA +272 -0
- arthexis-0.1.26.dist-info/RECORD +111 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +674 -674
- config/__init__.py +5 -5
- config/active_app.py +15 -15
- config/asgi.py +29 -29
- config/auth_app.py +7 -7
- config/celery.py +32 -25
- config/context_processors.py +67 -68
- config/horologia_app.py +7 -7
- config/loadenv.py +11 -11
- config/logging.py +59 -48
- config/middleware.py +71 -25
- config/offline.py +49 -49
- config/settings.py +676 -492
- config/settings_helpers.py +109 -0
- config/urls.py +228 -159
- config/wsgi.py +17 -17
- core/admin.py +4052 -2066
- core/admin_history.py +50 -50
- core/admindocs.py +192 -151
- core/apps.py +350 -223
- core/auto_upgrade.py +72 -0
- core/backends.py +311 -124
- core/changelog.py +403 -0
- core/entity.py +149 -133
- core/environment.py +60 -43
- core/fields.py +168 -75
- core/form_fields.py +75 -0
- core/github_helper.py +188 -25
- core/github_issues.py +183 -172
- core/github_repos.py +72 -0
- core/lcd_screen.py +78 -78
- core/liveupdate.py +25 -25
- core/log_paths.py +114 -100
- core/mailer.py +89 -83
- core/middleware.py +91 -91
- core/models.py +5041 -2195
- core/notifications.py +105 -105
- core/public_wifi.py +267 -227
- core/reference_utils.py +107 -0
- core/release.py +940 -346
- core/rfid_import_export.py +113 -0
- core/sigil_builder.py +149 -131
- core/sigil_context.py +20 -20
- core/sigil_resolver.py +250 -284
- core/system.py +1425 -230
- core/tasks.py +538 -199
- core/temp_passwords.py +181 -0
- core/test_system_info.py +202 -43
- core/tests.py +2673 -1069
- core/tests_liveupdate.py +17 -17
- core/urls.py +11 -11
- core/user_data.py +681 -495
- core/views.py +2484 -789
- core/widgets.py +213 -51
- nodes/admin.py +2236 -445
- nodes/apps.py +98 -70
- nodes/backends.py +160 -53
- nodes/dns.py +203 -0
- nodes/feature_checks.py +133 -0
- nodes/lcd.py +165 -165
- nodes/models.py +2375 -870
- nodes/reports.py +411 -0
- nodes/rfid_sync.py +210 -0
- nodes/signals.py +18 -0
- nodes/tasks.py +141 -46
- nodes/tests.py +5045 -1489
- nodes/urls.py +29 -13
- nodes/utils.py +172 -73
- nodes/views.py +1768 -304
- ocpp/admin.py +1775 -481
- ocpp/apps.py +25 -25
- ocpp/consumers.py +1843 -630
- ocpp/evcs.py +844 -928
- ocpp/evcs_discovery.py +158 -0
- ocpp/models.py +1417 -640
- ocpp/network.py +398 -0
- ocpp/reference_utils.py +42 -0
- ocpp/routing.py +11 -9
- ocpp/simulator.py +745 -368
- ocpp/status_display.py +26 -0
- ocpp/store.py +603 -403
- ocpp/tasks.py +479 -31
- ocpp/test_export_import.py +131 -130
- ocpp/test_rfid.py +1072 -540
- ocpp/tests.py +5494 -2296
- ocpp/transactions_io.py +197 -165
- ocpp/urls.py +50 -50
- ocpp/views.py +2024 -912
- pages/admin.py +1123 -396
- pages/apps.py +45 -10
- pages/checks.py +40 -40
- pages/context_processors.py +151 -85
- pages/defaults.py +13 -0
- pages/forms.py +221 -0
- pages/middleware.py +213 -153
- pages/models.py +720 -252
- pages/module_defaults.py +156 -0
- pages/site_config.py +137 -0
- pages/tasks.py +74 -0
- pages/tests.py +4009 -1389
- pages/urls.py +38 -20
- pages/utils.py +93 -12
- pages/views.py +1736 -762
- arthexis-0.1.9.dist-info/METADATA +0 -168
- arthexis-0.1.9.dist-info/RECORD +0 -92
- core/workgroup_urls.py +0 -17
- core/workgroup_views.py +0 -94
- nodes/actions.py +0 -70
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/top_level.txt +0 -0
pages/module_defaults.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Utilities to restore default navigation modules."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Iterable, Mapping
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
ModuleDefinition = Mapping[str, object]
|
|
9
|
+
LandingDefinition = tuple[str, str]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
ROLE_MODULE_DEFAULTS: Mapping[str, tuple[ModuleDefinition, ...]] = {
|
|
13
|
+
"Watchtower": (
|
|
14
|
+
{
|
|
15
|
+
"application": "ocpp",
|
|
16
|
+
"path": "/ocpp/",
|
|
17
|
+
"menu": "Chargers",
|
|
18
|
+
"landings": (
|
|
19
|
+
("/ocpp/cpms/dashboard/", "CPMS Online Dashboard"),
|
|
20
|
+
("/ocpp/evcs/simulator/", "Charge Point Simulator"),
|
|
21
|
+
("/ocpp/rfid/validator/", "RFID Tag Validator"),
|
|
22
|
+
),
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"application": "awg",
|
|
26
|
+
"path": "/awg/",
|
|
27
|
+
"menu": "",
|
|
28
|
+
"landings": (
|
|
29
|
+
("/awg/", "AWG Cable Calculator"),
|
|
30
|
+
("/awg/energy-tariff/", "Energy Tariff Calculator"),
|
|
31
|
+
),
|
|
32
|
+
},
|
|
33
|
+
),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class ReloadSummary:
|
|
39
|
+
"""Report about the changes performed while restoring defaults."""
|
|
40
|
+
|
|
41
|
+
roles_processed: int = 0
|
|
42
|
+
modules_created: int = 0
|
|
43
|
+
modules_updated: int = 0
|
|
44
|
+
landings_created: int = 0
|
|
45
|
+
landings_updated: int = 0
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def has_changes(self) -> bool:
|
|
49
|
+
return any(
|
|
50
|
+
(
|
|
51
|
+
self.modules_created,
|
|
52
|
+
self.modules_updated,
|
|
53
|
+
self.landings_created,
|
|
54
|
+
self.landings_updated,
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _manager(model, name: str):
|
|
60
|
+
manager = getattr(model, name, None)
|
|
61
|
+
if manager is not None:
|
|
62
|
+
return manager
|
|
63
|
+
return model.objects
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def reload_default_modules(Application, Module, Landing, NodeRole) -> ReloadSummary:
|
|
67
|
+
"""Ensure default navigation modules exist for the configured roles."""
|
|
68
|
+
|
|
69
|
+
summary = ReloadSummary()
|
|
70
|
+
application_manager = _manager(Application, "all_objects")
|
|
71
|
+
module_manager = _manager(Module, "all_objects")
|
|
72
|
+
landing_manager = _manager(Landing, "all_objects")
|
|
73
|
+
role_manager = _manager(NodeRole, "all_objects")
|
|
74
|
+
|
|
75
|
+
for role_name, module_definitions in ROLE_MODULE_DEFAULTS.items():
|
|
76
|
+
try:
|
|
77
|
+
role = role_manager.get(name=role_name)
|
|
78
|
+
except NodeRole.DoesNotExist:
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
summary.roles_processed += 1
|
|
82
|
+
|
|
83
|
+
for definition in module_definitions:
|
|
84
|
+
app_name: str = definition["application"] # type: ignore[assignment]
|
|
85
|
+
try:
|
|
86
|
+
application = application_manager.get(name=app_name)
|
|
87
|
+
except Application.DoesNotExist:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
module, created = module_manager.get_or_create(
|
|
91
|
+
node_role=role,
|
|
92
|
+
path=definition["path"],
|
|
93
|
+
defaults={
|
|
94
|
+
"application": application,
|
|
95
|
+
"menu": definition["menu"],
|
|
96
|
+
"is_seed_data": True,
|
|
97
|
+
"is_deleted": False,
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
if created:
|
|
101
|
+
summary.modules_created += 1
|
|
102
|
+
|
|
103
|
+
module_updates: list[str] = []
|
|
104
|
+
if module.application_id != application.id:
|
|
105
|
+
module.application = application
|
|
106
|
+
module_updates.append("application")
|
|
107
|
+
if module.menu != definition["menu"]:
|
|
108
|
+
module.menu = definition["menu"] # type: ignore[index]
|
|
109
|
+
module_updates.append("menu")
|
|
110
|
+
if getattr(module, "is_deleted", False):
|
|
111
|
+
module.is_deleted = False
|
|
112
|
+
module_updates.append("is_deleted")
|
|
113
|
+
if not getattr(module, "is_seed_data", False):
|
|
114
|
+
module.is_seed_data = True
|
|
115
|
+
module_updates.append("is_seed_data")
|
|
116
|
+
if module_updates:
|
|
117
|
+
module.save(update_fields=module_updates)
|
|
118
|
+
if not created:
|
|
119
|
+
summary.modules_updated += 1
|
|
120
|
+
|
|
121
|
+
landings: Iterable[tuple[str, str]] = definition["landings"] # type: ignore[index]
|
|
122
|
+
for path, label in landings:
|
|
123
|
+
landing, landing_created = landing_manager.get_or_create(
|
|
124
|
+
module=module,
|
|
125
|
+
path=path,
|
|
126
|
+
defaults={
|
|
127
|
+
"label": label,
|
|
128
|
+
"description": "",
|
|
129
|
+
"enabled": True,
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
if landing_created:
|
|
133
|
+
summary.landings_created += 1
|
|
134
|
+
|
|
135
|
+
landing_updates: list[str] = []
|
|
136
|
+
if landing.label != label:
|
|
137
|
+
landing.label = label
|
|
138
|
+
landing_updates.append("label")
|
|
139
|
+
if landing.description:
|
|
140
|
+
landing.description = ""
|
|
141
|
+
landing_updates.append("description")
|
|
142
|
+
if not landing.enabled:
|
|
143
|
+
landing.enabled = True
|
|
144
|
+
landing_updates.append("enabled")
|
|
145
|
+
if getattr(landing, "is_deleted", False):
|
|
146
|
+
landing.is_deleted = False
|
|
147
|
+
landing_updates.append("is_deleted")
|
|
148
|
+
if not getattr(landing, "is_seed_data", False):
|
|
149
|
+
landing.is_seed_data = True
|
|
150
|
+
landing_updates.append("is_seed_data")
|
|
151
|
+
if landing_updates:
|
|
152
|
+
landing.save(update_fields=landing_updates)
|
|
153
|
+
if not landing_created:
|
|
154
|
+
summary.landings_updated += 1
|
|
155
|
+
|
|
156
|
+
return summary
|
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
|
+
)
|
pages/tasks.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Celery tasks for the pages application."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from datetime import timedelta
|
|
7
|
+
|
|
8
|
+
from celery import shared_task
|
|
9
|
+
|
|
10
|
+
from django.utils import timezone
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@shared_task
|
|
17
|
+
def create_user_story_github_issue(user_story_id: int) -> str | None:
|
|
18
|
+
"""Create a GitHub issue for the provided ``UserStory`` instance."""
|
|
19
|
+
|
|
20
|
+
from .models import UserStory
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
story = UserStory.objects.get(pk=user_story_id)
|
|
24
|
+
except UserStory.DoesNotExist: # pragma: no cover - defensive guard
|
|
25
|
+
logger.warning(
|
|
26
|
+
"User story %s no longer exists; skipping GitHub issue creation",
|
|
27
|
+
user_story_id,
|
|
28
|
+
)
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
if story.rating >= 5:
|
|
32
|
+
logger.info(
|
|
33
|
+
"Skipping GitHub issue creation for user story %s with rating %s",
|
|
34
|
+
story.pk,
|
|
35
|
+
story.rating,
|
|
36
|
+
)
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
if story.github_issue_url:
|
|
40
|
+
logger.info(
|
|
41
|
+
"GitHub issue already recorded for user story %s: %s",
|
|
42
|
+
story.pk,
|
|
43
|
+
story.github_issue_url,
|
|
44
|
+
)
|
|
45
|
+
return story.github_issue_url
|
|
46
|
+
|
|
47
|
+
issue_url = story.create_github_issue()
|
|
48
|
+
|
|
49
|
+
if issue_url:
|
|
50
|
+
logger.info(
|
|
51
|
+
"Created GitHub issue %s for user story %s", issue_url, story.pk
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
logger.info(
|
|
55
|
+
"No GitHub issue created for user story %s", story.pk
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return issue_url
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@shared_task
|
|
62
|
+
def purge_expired_landing_leads(days: int = 30) -> int:
|
|
63
|
+
"""Remove landing leads older than ``days`` days."""
|
|
64
|
+
|
|
65
|
+
from .models import LandingLead
|
|
66
|
+
|
|
67
|
+
cutoff = timezone.now() - timedelta(days=days)
|
|
68
|
+
queryset = LandingLead.objects.filter(created_on__lt=cutoff)
|
|
69
|
+
deleted, _ = queryset.delete()
|
|
70
|
+
if deleted:
|
|
71
|
+
logger.info(
|
|
72
|
+
"Purged %s landing leads older than %s days", deleted, days
|
|
73
|
+
)
|
|
74
|
+
return deleted
|