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
core/public_wifi.py CHANGED
@@ -1,227 +1,267 @@
1
- """Utilities for managing public Wi-Fi access control."""
2
-
3
- from __future__ import annotations
4
-
5
- import logging
6
- import re
7
- import shutil
8
- import subprocess
9
- from pathlib import Path
10
- from typing import Iterable
11
-
12
- from django.conf import settings
13
- from django.utils import timezone
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
- _MAC_RE = re.compile(r"(?P<mac>([0-9a-f]{2}:){5}[0-9a-f]{2})", re.IGNORECASE)
18
-
19
-
20
- def _lock_dir() -> Path:
21
- return Path(settings.BASE_DIR) / "locks"
22
-
23
-
24
- def _mode_lock() -> Path:
25
- return _lock_dir() / "public_wifi_mode.lck"
26
-
27
-
28
- def _allowlist_path() -> Path:
29
- return _lock_dir() / "public_wifi_allow.list"
30
-
31
-
32
- def _normalize_mac(mac: str) -> str:
33
- return mac.strip().lower()
34
-
35
-
36
- def resolve_mac_address(ip_address: str | None) -> str | None:
37
- """Attempt to resolve the MAC address for ``ip_address``.
38
-
39
- The lookup prefers ``ip neigh`` and falls back to ``arp`` when available.
40
- Returns ``None`` when the MAC cannot be determined.
41
- """
42
-
43
- if not ip_address:
44
- return None
45
-
46
- commands: Iterable[list[str]] = []
47
- ip_cmd = shutil.which("ip")
48
- if ip_cmd:
49
- commands = [[ip_cmd, "neigh", "show", ip_address]]
50
- arp_cmd = shutil.which("arp")
51
- if arp_cmd:
52
- commands = list(commands) + [[arp_cmd, "-n", ip_address]]
53
- for command in commands:
54
- try:
55
- result = subprocess.run(
56
- command,
57
- capture_output=True,
58
- text=True,
59
- check=False,
60
- timeout=1,
61
- )
62
- except Exception: # pragma: no cover - defensive
63
- continue
64
- if result.returncode != 0:
65
- continue
66
- match = _MAC_RE.search(result.stdout)
67
- if match:
68
- mac = match.group("mac")
69
- return _normalize_mac(mac)
70
- return None
71
-
72
-
73
- def _iptables_available() -> bool:
74
- return shutil.which("iptables") is not None
75
-
76
-
77
- def _run_iptables(args: list[str]) -> None:
78
- try:
79
- subprocess.run(["iptables", *args], check=False, timeout=2)
80
- except Exception: # pragma: no cover - defensive
81
- logger.exception("iptables command failed: %s", " ".join(args))
82
-
83
-
84
- def _load_allowlist() -> set[str]:
85
- path = _allowlist_path()
86
- if not path.exists():
87
- return set()
88
- try:
89
- content = path.read_text().splitlines()
90
- except OSError: # pragma: no cover - defensive
91
- logger.exception("Unable to read public Wi-Fi allow list")
92
- return set()
93
- return {line.strip().lower() for line in content if line.strip()}
94
-
95
-
96
- def _save_allowlist(macs: Iterable[str]) -> None:
97
- path = _allowlist_path()
98
- path.parent.mkdir(parents=True, exist_ok=True)
99
- lines = sorted({m.lower() for m in macs if m})
100
- try:
101
- path.write_text("\n".join(lines) + ("\n" if lines else ""))
102
- except OSError: # pragma: no cover - defensive
103
- logger.exception("Unable to write public Wi-Fi allow list")
104
-
105
-
106
- def allow_mac(mac: str) -> None:
107
- mac = _normalize_mac(mac)
108
- if not mac:
109
- return
110
- allowlist = _load_allowlist()
111
- if mac not in allowlist:
112
- allowlist.add(mac)
113
- _save_allowlist(allowlist)
114
- if _iptables_available():
115
- check_args = [
116
- "-C",
117
- "FORWARD",
118
- "-i",
119
- "wlan0",
120
- "-m",
121
- "mac",
122
- "--mac-source",
123
- mac,
124
- "-j",
125
- "ACCEPT",
126
- ]
127
- try:
128
- result = subprocess.run(
129
- ["iptables", *check_args],
130
- capture_output=True,
131
- text=True,
132
- check=False,
133
- timeout=2,
134
- )
135
- exists = result.returncode == 0
136
- except Exception: # pragma: no cover - defensive
137
- logger.exception("iptables check failed for %s", mac)
138
- exists = True
139
- if not exists:
140
- _run_iptables(
141
- [
142
- "-I",
143
- "FORWARD",
144
- "1",
145
- "-i",
146
- "wlan0",
147
- "-m",
148
- "mac",
149
- "--mac-source",
150
- mac,
151
- "-j",
152
- "ACCEPT",
153
- ]
154
- )
155
-
156
-
157
- def revoke_mac(mac: str) -> None:
158
- mac = _normalize_mac(mac)
159
- if not mac:
160
- return
161
- allowlist = _load_allowlist()
162
- if mac in allowlist:
163
- allowlist.remove(mac)
164
- _save_allowlist(allowlist)
165
- if _iptables_available():
166
- while True:
167
- try:
168
- result = subprocess.run(
169
- [
170
- "iptables",
171
- "-D",
172
- "FORWARD",
173
- "-i",
174
- "wlan0",
175
- "-m",
176
- "mac",
177
- "--mac-source",
178
- mac,
179
- "-j",
180
- "ACCEPT",
181
- ],
182
- capture_output=True,
183
- text=True,
184
- check=False,
185
- timeout=2,
186
- )
187
- except Exception: # pragma: no cover - defensive
188
- logger.exception("iptables delete failed for %s", mac)
189
- break
190
- if result.returncode != 0:
191
- break
192
-
193
-
194
- def public_mode_lock_exists() -> bool:
195
- return _mode_lock().exists()
196
-
197
-
198
- def grant_public_access(user, mac: str):
199
- from core.models import PublicWifiAccess
200
-
201
- mac = _normalize_mac(mac)
202
- if not mac:
203
- return None
204
- access, created = PublicWifiAccess.objects.get_or_create(
205
- user=user,
206
- mac_address=mac,
207
- defaults={"revoked_on": None},
208
- )
209
- if access.revoked_on is not None:
210
- access.revoked_on = None
211
- access.save(update_fields=["revoked_on", "updated_on"])
212
- allow_mac(mac)
213
- return access
214
-
215
-
216
- def revoke_public_access(access) -> None:
217
- if access.revoked_on is None:
218
- access.revoked_on = timezone.now()
219
- access.save(update_fields=["revoked_on", "updated_on"])
220
- revoke_mac(access.mac_address)
221
-
222
-
223
- def revoke_public_access_for_user(user) -> None:
224
- from core.models import PublicWifiAccess
225
-
226
- for access in PublicWifiAccess.objects.filter(user=user, revoked_on__isnull=True):
227
- revoke_public_access(access)
1
+ """Utilities for managing public Wi-Fi access control."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import re
7
+ import shutil
8
+ import subprocess
9
+ from pathlib import Path
10
+ from typing import Iterable
11
+
12
+ from django.conf import settings
13
+ from django.utils import timezone
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ _MAC_RE = re.compile(r"(?P<mac>([0-9a-f]{2}:){5}[0-9a-f]{2})", re.IGNORECASE)
18
+
19
+
20
+ def _lock_dir() -> Path:
21
+ return Path(settings.BASE_DIR) / "locks"
22
+
23
+
24
+ def _mode_lock() -> Path:
25
+ return _lock_dir() / "public_wifi_mode.lck"
26
+
27
+
28
+ def _allowlist_path() -> Path:
29
+ return _lock_dir() / "public_wifi_allow.list"
30
+
31
+
32
+ def _normalize_mac(mac: str) -> str:
33
+ return mac.strip().lower()
34
+
35
+
36
+ def resolve_mac_address(ip_address: str | None) -> str | None:
37
+ """Attempt to resolve the MAC address for ``ip_address``.
38
+
39
+ The lookup prefers ``ip neigh`` and falls back to ``arp`` when available.
40
+ Returns ``None`` when the MAC cannot be determined.
41
+ """
42
+
43
+ if not ip_address:
44
+ return None
45
+
46
+ commands: Iterable[list[str]] = []
47
+ ip_cmd = shutil.which("ip")
48
+ if ip_cmd:
49
+ commands = [[ip_cmd, "neigh", "show", ip_address]]
50
+ arp_cmd = shutil.which("arp")
51
+ if arp_cmd:
52
+ commands = list(commands) + [[arp_cmd, "-n", ip_address]]
53
+ for command in commands:
54
+ try:
55
+ result = subprocess.run(
56
+ command,
57
+ capture_output=True,
58
+ text=True,
59
+ check=False,
60
+ timeout=1,
61
+ )
62
+ except Exception: # pragma: no cover - defensive
63
+ continue
64
+ if result.returncode != 0:
65
+ continue
66
+ match = _MAC_RE.search(result.stdout)
67
+ if match:
68
+ mac = match.group("mac")
69
+ return _normalize_mac(mac)
70
+ return None
71
+
72
+
73
+ def _iptables_available() -> bool:
74
+ return shutil.which("iptables") is not None
75
+
76
+
77
+ def _run_iptables(args: list[str]) -> None:
78
+ try:
79
+ subprocess.run(["iptables", *args], check=False, timeout=2)
80
+ except Exception: # pragma: no cover - defensive
81
+ logger.exception("iptables command failed: %s", " ".join(args))
82
+
83
+
84
+ def _ensure_wlan0_drop_rule() -> None:
85
+ if not _iptables_available():
86
+ return
87
+ check_args = [
88
+ "-C",
89
+ "FORWARD",
90
+ "-i",
91
+ "wlan0",
92
+ "-o",
93
+ "wlan1",
94
+ "-j",
95
+ "DROP",
96
+ ]
97
+ try:
98
+ result = subprocess.run(
99
+ ["iptables", *check_args],
100
+ capture_output=True,
101
+ text=True,
102
+ check=False,
103
+ timeout=2,
104
+ )
105
+ except Exception: # pragma: no cover - defensive
106
+ logger.exception("iptables check failed for wlan0 drop rule")
107
+ result = None
108
+ if result is None or result.returncode != 0:
109
+ _run_iptables(
110
+ [
111
+ "-A",
112
+ "FORWARD",
113
+ "-i",
114
+ "wlan0",
115
+ "-o",
116
+ "wlan1",
117
+ "-j",
118
+ "DROP",
119
+ ]
120
+ )
121
+
122
+
123
+ def _load_allowlist() -> set[str]:
124
+ path = _allowlist_path()
125
+ if not path.exists():
126
+ return set()
127
+ try:
128
+ content = path.read_text().splitlines()
129
+ except OSError: # pragma: no cover - defensive
130
+ logger.exception("Unable to read public Wi-Fi allow list")
131
+ return set()
132
+ return {line.strip().lower() for line in content if line.strip()}
133
+
134
+
135
+ def _save_allowlist(macs: Iterable[str]) -> None:
136
+ path = _allowlist_path()
137
+ path.parent.mkdir(parents=True, exist_ok=True)
138
+ lines = sorted({m.lower() for m in macs if m})
139
+ try:
140
+ path.write_text("\n".join(lines) + ("\n" if lines else ""))
141
+ except OSError: # pragma: no cover - defensive
142
+ logger.exception("Unable to write public Wi-Fi allow list")
143
+
144
+
145
+ def allow_mac(mac: str) -> None:
146
+ mac = _normalize_mac(mac)
147
+ if not mac:
148
+ return
149
+ allowlist = _load_allowlist()
150
+ if mac not in allowlist:
151
+ allowlist.add(mac)
152
+ _save_allowlist(allowlist)
153
+ if _iptables_available():
154
+ _ensure_wlan0_drop_rule()
155
+ check_args = [
156
+ "-C",
157
+ "FORWARD",
158
+ "-i",
159
+ "wlan0",
160
+ "-m",
161
+ "mac",
162
+ "--mac-source",
163
+ mac,
164
+ "-j",
165
+ "ACCEPT",
166
+ ]
167
+ try:
168
+ result = subprocess.run(
169
+ ["iptables", *check_args],
170
+ capture_output=True,
171
+ text=True,
172
+ check=False,
173
+ timeout=2,
174
+ )
175
+ exists = result.returncode == 0
176
+ except Exception: # pragma: no cover - defensive
177
+ logger.exception("iptables check failed for %s", mac)
178
+ exists = True
179
+ if not exists:
180
+ _run_iptables(
181
+ [
182
+ "-I",
183
+ "FORWARD",
184
+ "1",
185
+ "-i",
186
+ "wlan0",
187
+ "-m",
188
+ "mac",
189
+ "--mac-source",
190
+ mac,
191
+ "-j",
192
+ "ACCEPT",
193
+ ]
194
+ )
195
+
196
+
197
+ def revoke_mac(mac: str) -> None:
198
+ mac = _normalize_mac(mac)
199
+ if not mac:
200
+ return
201
+ allowlist = _load_allowlist()
202
+ if mac in allowlist:
203
+ allowlist.remove(mac)
204
+ _save_allowlist(allowlist)
205
+ if _iptables_available():
206
+ while True:
207
+ try:
208
+ result = subprocess.run(
209
+ [
210
+ "iptables",
211
+ "-D",
212
+ "FORWARD",
213
+ "-i",
214
+ "wlan0",
215
+ "-m",
216
+ "mac",
217
+ "--mac-source",
218
+ mac,
219
+ "-j",
220
+ "ACCEPT",
221
+ ],
222
+ capture_output=True,
223
+ text=True,
224
+ check=False,
225
+ timeout=2,
226
+ )
227
+ except Exception: # pragma: no cover - defensive
228
+ logger.exception("iptables delete failed for %s", mac)
229
+ break
230
+ if result.returncode != 0:
231
+ break
232
+
233
+
234
+ def public_mode_lock_exists() -> bool:
235
+ return _mode_lock().exists()
236
+
237
+
238
+ def grant_public_access(user, mac: str):
239
+ from core.models import PublicWifiAccess
240
+
241
+ mac = _normalize_mac(mac)
242
+ if not mac:
243
+ return None
244
+ access, created = PublicWifiAccess.objects.get_or_create(
245
+ user=user,
246
+ mac_address=mac,
247
+ defaults={"revoked_on": None},
248
+ )
249
+ if access.revoked_on is not None:
250
+ access.revoked_on = None
251
+ access.save(update_fields=["revoked_on", "updated_on"])
252
+ allow_mac(mac)
253
+ return access
254
+
255
+
256
+ def revoke_public_access(access) -> None:
257
+ if access.revoked_on is None:
258
+ access.revoked_on = timezone.now()
259
+ access.save(update_fields=["revoked_on", "updated_on"])
260
+ revoke_mac(access.mac_address)
261
+
262
+
263
+ def revoke_public_access_for_user(user) -> None:
264
+ from core.models import PublicWifiAccess
265
+
266
+ for access in PublicWifiAccess.objects.filter(user=user, revoked_on__isnull=True):
267
+ revoke_public_access(access)
@@ -0,0 +1,107 @@
1
+ """Utility helpers for working with Reference objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Iterable, TYPE_CHECKING
6
+
7
+ from django.contrib.sites.models import Site
8
+
9
+ if TYPE_CHECKING: # pragma: no cover - imported only for type checking
10
+ from django.http import HttpRequest
11
+ from nodes.models import Node
12
+ from .models import Reference
13
+
14
+
15
+ def filter_visible_references(
16
+ refs: Iterable["Reference"],
17
+ *,
18
+ request: "HttpRequest | None" = None,
19
+ site: Site | None = None,
20
+ node: "Node | None" = None,
21
+ respect_footer_visibility: bool = True,
22
+ ) -> list["Reference"]:
23
+ """Return references visible for the current context."""
24
+
25
+ if site is None and request is not None:
26
+ try:
27
+ host = request.get_host().split(":")[0]
28
+ except Exception:
29
+ host = ""
30
+ if host:
31
+ site = Site.objects.filter(domain__iexact=host).first()
32
+
33
+ site_id = getattr(site, "pk", None)
34
+
35
+ if node is None:
36
+ try:
37
+ from nodes.models import Node # imported lazily to avoid circular import
38
+
39
+ node = Node.get_local()
40
+ except Exception:
41
+ node = None
42
+
43
+ node_role_id = getattr(node, "role_id", None)
44
+ node_active_feature_ids: set[int] = set()
45
+ if node is not None:
46
+ assignments_manager = getattr(node, "feature_assignments", None)
47
+ if assignments_manager is not None:
48
+ try:
49
+ assignments = list(
50
+ assignments_manager.filter(is_deleted=False).select_related(
51
+ "feature"
52
+ )
53
+ )
54
+ except Exception:
55
+ assignments = []
56
+ for assignment in assignments:
57
+ feature = getattr(assignment, "feature", None)
58
+ if feature is None or getattr(feature, "is_deleted", False):
59
+ continue
60
+ try:
61
+ if feature.is_enabled:
62
+ node_active_feature_ids.add(feature.pk)
63
+ except Exception:
64
+ continue
65
+
66
+ visible_refs: list["Reference"] = []
67
+ for ref in refs:
68
+ required_roles = {role.pk for role in ref.roles.all()}
69
+ required_features = {feature.pk for feature in ref.features.all()}
70
+ required_sites = {current_site.pk for current_site in ref.sites.all()}
71
+
72
+ if required_roles or required_features or required_sites:
73
+ allowed = True
74
+ if required_roles:
75
+ allowed = bool(node_role_id and node_role_id in required_roles)
76
+ if allowed and required_features:
77
+ allowed = bool(
78
+ node_active_feature_ids
79
+ and node_active_feature_ids.intersection(required_features)
80
+ )
81
+ if allowed and required_sites:
82
+ allowed = bool(site_id and site_id in required_sites)
83
+
84
+ if not allowed:
85
+ continue
86
+
87
+ if respect_footer_visibility:
88
+ if ref.footer_visibility == ref.FOOTER_PUBLIC:
89
+ visible_refs.append(ref)
90
+ elif (
91
+ ref.footer_visibility == ref.FOOTER_PRIVATE
92
+ and request
93
+ and request.user.is_authenticated
94
+ ):
95
+ visible_refs.append(ref)
96
+ elif (
97
+ ref.footer_visibility == ref.FOOTER_STAFF
98
+ and request
99
+ and request.user.is_authenticated
100
+ and request.user.is_staff
101
+ ):
102
+ visible_refs.append(ref)
103
+ else:
104
+ visible_refs.append(ref)
105
+
106
+ return visible_refs
107
+