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
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
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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)
|
core/reference_utils.py
ADDED
|
@@ -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
|
+
|