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.

Files changed (112) hide show
  1. arthexis-0.1.26.dist-info/METADATA +272 -0
  2. arthexis-0.1.26.dist-info/RECORD +111 -0
  3. {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +674 -674
  4. config/__init__.py +5 -5
  5. config/active_app.py +15 -15
  6. config/asgi.py +29 -29
  7. config/auth_app.py +7 -7
  8. config/celery.py +32 -25
  9. config/context_processors.py +67 -68
  10. config/horologia_app.py +7 -7
  11. config/loadenv.py +11 -11
  12. config/logging.py +59 -48
  13. config/middleware.py +71 -25
  14. config/offline.py +49 -49
  15. config/settings.py +676 -492
  16. config/settings_helpers.py +109 -0
  17. config/urls.py +228 -159
  18. config/wsgi.py +17 -17
  19. core/admin.py +4052 -2066
  20. core/admin_history.py +50 -50
  21. core/admindocs.py +192 -151
  22. core/apps.py +350 -223
  23. core/auto_upgrade.py +72 -0
  24. core/backends.py +311 -124
  25. core/changelog.py +403 -0
  26. core/entity.py +149 -133
  27. core/environment.py +60 -43
  28. core/fields.py +168 -75
  29. core/form_fields.py +75 -0
  30. core/github_helper.py +188 -25
  31. core/github_issues.py +183 -172
  32. core/github_repos.py +72 -0
  33. core/lcd_screen.py +78 -78
  34. core/liveupdate.py +25 -25
  35. core/log_paths.py +114 -100
  36. core/mailer.py +89 -83
  37. core/middleware.py +91 -91
  38. core/models.py +5041 -2195
  39. core/notifications.py +105 -105
  40. core/public_wifi.py +267 -227
  41. core/reference_utils.py +107 -0
  42. core/release.py +940 -346
  43. core/rfid_import_export.py +113 -0
  44. core/sigil_builder.py +149 -131
  45. core/sigil_context.py +20 -20
  46. core/sigil_resolver.py +250 -284
  47. core/system.py +1425 -230
  48. core/tasks.py +538 -199
  49. core/temp_passwords.py +181 -0
  50. core/test_system_info.py +202 -43
  51. core/tests.py +2673 -1069
  52. core/tests_liveupdate.py +17 -17
  53. core/urls.py +11 -11
  54. core/user_data.py +681 -495
  55. core/views.py +2484 -789
  56. core/widgets.py +213 -51
  57. nodes/admin.py +2236 -445
  58. nodes/apps.py +98 -70
  59. nodes/backends.py +160 -53
  60. nodes/dns.py +203 -0
  61. nodes/feature_checks.py +133 -0
  62. nodes/lcd.py +165 -165
  63. nodes/models.py +2375 -870
  64. nodes/reports.py +411 -0
  65. nodes/rfid_sync.py +210 -0
  66. nodes/signals.py +18 -0
  67. nodes/tasks.py +141 -46
  68. nodes/tests.py +5045 -1489
  69. nodes/urls.py +29 -13
  70. nodes/utils.py +172 -73
  71. nodes/views.py +1768 -304
  72. ocpp/admin.py +1775 -481
  73. ocpp/apps.py +25 -25
  74. ocpp/consumers.py +1843 -630
  75. ocpp/evcs.py +844 -928
  76. ocpp/evcs_discovery.py +158 -0
  77. ocpp/models.py +1417 -640
  78. ocpp/network.py +398 -0
  79. ocpp/reference_utils.py +42 -0
  80. ocpp/routing.py +11 -9
  81. ocpp/simulator.py +745 -368
  82. ocpp/status_display.py +26 -0
  83. ocpp/store.py +603 -403
  84. ocpp/tasks.py +479 -31
  85. ocpp/test_export_import.py +131 -130
  86. ocpp/test_rfid.py +1072 -540
  87. ocpp/tests.py +5494 -2296
  88. ocpp/transactions_io.py +197 -165
  89. ocpp/urls.py +50 -50
  90. ocpp/views.py +2024 -912
  91. pages/admin.py +1123 -396
  92. pages/apps.py +45 -10
  93. pages/checks.py +40 -40
  94. pages/context_processors.py +151 -85
  95. pages/defaults.py +13 -0
  96. pages/forms.py +221 -0
  97. pages/middleware.py +213 -153
  98. pages/models.py +720 -252
  99. pages/module_defaults.py +156 -0
  100. pages/site_config.py +137 -0
  101. pages/tasks.py +74 -0
  102. pages/tests.py +4009 -1389
  103. pages/urls.py +38 -20
  104. pages/utils.py +93 -12
  105. pages/views.py +1736 -762
  106. arthexis-0.1.9.dist-info/METADATA +0 -168
  107. arthexis-0.1.9.dist-info/RECORD +0 -92
  108. core/workgroup_urls.py +0 -17
  109. core/workgroup_views.py +0 -94
  110. nodes/actions.py +0 -70
  111. {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
  112. {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/top_level.txt +0 -0
@@ -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