arthexis 0.1.8__py3-none-any.whl → 0.1.10__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.8.dist-info → arthexis-0.1.10.dist-info}/METADATA +87 -6
- arthexis-0.1.10.dist-info/RECORD +95 -0
- arthexis-0.1.10.dist-info/licenses/LICENSE +674 -0
- config/__init__.py +0 -1
- config/auth_app.py +0 -1
- config/celery.py +1 -2
- config/context_processors.py +1 -1
- config/offline.py +2 -0
- config/settings.py +352 -37
- config/urls.py +71 -6
- core/admin.py +1601 -200
- core/admin_history.py +50 -0
- core/admindocs.py +108 -1
- core/apps.py +161 -3
- core/auto_upgrade.py +57 -0
- core/backends.py +123 -7
- core/entity.py +62 -48
- core/fields.py +98 -0
- core/github_helper.py +25 -0
- core/github_issues.py +172 -0
- core/lcd_screen.py +1 -0
- core/liveupdate.py +25 -0
- core/log_paths.py +100 -0
- core/mailer.py +83 -0
- core/middleware.py +57 -0
- core/models.py +1279 -267
- core/notifications.py +11 -1
- core/public_wifi.py +227 -0
- core/reference_utils.py +97 -0
- core/release.py +27 -20
- core/sigil_builder.py +144 -0
- core/sigil_context.py +20 -0
- core/sigil_resolver.py +284 -0
- core/system.py +162 -29
- core/tasks.py +269 -27
- core/test_system_info.py +59 -1
- core/tests.py +644 -73
- core/tests_liveupdate.py +17 -0
- core/urls.py +2 -2
- core/user_data.py +425 -168
- core/views.py +627 -59
- core/widgets.py +51 -0
- core/workgroup_urls.py +7 -3
- core/workgroup_views.py +43 -6
- nodes/actions.py +0 -2
- nodes/admin.py +168 -285
- nodes/apps.py +9 -15
- nodes/backends.py +145 -0
- nodes/lcd.py +24 -10
- nodes/models.py +579 -179
- nodes/tasks.py +1 -5
- nodes/tests.py +894 -130
- nodes/utils.py +13 -2
- nodes/views.py +204 -28
- ocpp/admin.py +212 -63
- ocpp/apps.py +1 -1
- ocpp/consumers.py +642 -68
- ocpp/evcs.py +30 -10
- ocpp/models.py +452 -70
- ocpp/simulator.py +75 -11
- ocpp/store.py +288 -30
- ocpp/tasks.py +11 -7
- ocpp/test_export_import.py +8 -7
- ocpp/test_rfid.py +211 -16
- ocpp/tests.py +1576 -137
- ocpp/transactions_io.py +68 -22
- ocpp/urls.py +35 -2
- ocpp/views.py +701 -123
- pages/admin.py +173 -13
- pages/checks.py +0 -1
- pages/context_processors.py +39 -6
- pages/forms.py +131 -0
- pages/middleware.py +153 -0
- pages/models.py +37 -9
- pages/tests.py +1182 -42
- pages/urls.py +4 -0
- pages/utils.py +0 -1
- pages/views.py +844 -51
- arthexis-0.1.8.dist-info/RECORD +0 -80
- arthexis-0.1.8.dist-info/licenses/LICENSE +0 -21
- config/workgroup_app.py +0 -7
- core/checks.py +0 -29
- {arthexis-0.1.8.dist-info → arthexis-0.1.10.dist-info}/WHEEL +0 -0
- {arthexis-0.1.8.dist-info → arthexis-0.1.10.dist-info}/top_level.txt +0 -0
core/sigil_resolver.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from django.apps import apps
|
|
9
|
+
from django.conf import settings
|
|
10
|
+
from django.core import serializers
|
|
11
|
+
from django.db import models
|
|
12
|
+
|
|
13
|
+
from .sigil_context import get_context
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("core.entity")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _is_wizard_mode() -> bool:
|
|
19
|
+
"""Return ``True`` when the application is running in wizard mode."""
|
|
20
|
+
|
|
21
|
+
flag = getattr(settings, "WIZARD_MODE", False)
|
|
22
|
+
if isinstance(flag, str):
|
|
23
|
+
return flag.lower() in {"1", "true", "yes", "on"}
|
|
24
|
+
return bool(flag)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _first_instance(model: type[models.Model]) -> Optional[models.Model]:
|
|
28
|
+
qs = model.objects
|
|
29
|
+
ordering = list(getattr(model._meta, "ordering", []))
|
|
30
|
+
if ordering:
|
|
31
|
+
qs = qs.order_by(*ordering)
|
|
32
|
+
else:
|
|
33
|
+
qs = qs.order_by("?")
|
|
34
|
+
return qs.first()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@lru_cache(maxsize=1)
|
|
38
|
+
def _find_gway_command() -> Optional[str]:
|
|
39
|
+
path = shutil.which("gway")
|
|
40
|
+
if path:
|
|
41
|
+
return path
|
|
42
|
+
for candidate in ("~/.local/bin/gway", "/usr/local/bin/gway"):
|
|
43
|
+
expanded = os.path.expanduser(candidate)
|
|
44
|
+
if os.path.isfile(expanded) and os.access(expanded, os.X_OK):
|
|
45
|
+
return expanded
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _resolve_with_gway(sigil: str) -> Optional[str]:
|
|
50
|
+
command = _find_gway_command()
|
|
51
|
+
if not command:
|
|
52
|
+
return None
|
|
53
|
+
timeout = 60 if _is_wizard_mode() else 1
|
|
54
|
+
try:
|
|
55
|
+
result = subprocess.run(
|
|
56
|
+
[command, "-e", sigil],
|
|
57
|
+
check=False,
|
|
58
|
+
stdout=subprocess.PIPE,
|
|
59
|
+
stderr=subprocess.PIPE,
|
|
60
|
+
text=True,
|
|
61
|
+
timeout=timeout,
|
|
62
|
+
)
|
|
63
|
+
except subprocess.TimeoutExpired:
|
|
64
|
+
logger.warning(
|
|
65
|
+
"gway timed out after %s seconds while resolving sigil %s",
|
|
66
|
+
timeout,
|
|
67
|
+
sigil,
|
|
68
|
+
)
|
|
69
|
+
return None
|
|
70
|
+
except Exception:
|
|
71
|
+
logger.exception("Failed executing gway for sigil %s", sigil)
|
|
72
|
+
return None
|
|
73
|
+
if result.returncode != 0:
|
|
74
|
+
logger.warning(
|
|
75
|
+
"gway exited with status %s while resolving sigil %s",
|
|
76
|
+
result.returncode,
|
|
77
|
+
sigil,
|
|
78
|
+
)
|
|
79
|
+
return None
|
|
80
|
+
return result.stdout.strip()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _failed_resolution(token: str) -> str:
|
|
84
|
+
sigil = f"[{token}]"
|
|
85
|
+
resolved = _resolve_with_gway(sigil)
|
|
86
|
+
if resolved is not None:
|
|
87
|
+
return resolved
|
|
88
|
+
return sigil
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _resolve_token(token: str, current: Optional[models.Model] = None) -> str:
|
|
92
|
+
original_token = token
|
|
93
|
+
i = 0
|
|
94
|
+
n = len(token)
|
|
95
|
+
root_name = ""
|
|
96
|
+
while i < n and token[i] not in ":=.":
|
|
97
|
+
root_name += token[i]
|
|
98
|
+
i += 1
|
|
99
|
+
if not root_name:
|
|
100
|
+
return _failed_resolution(original_token)
|
|
101
|
+
filter_field = None
|
|
102
|
+
if i < n and token[i] == ":":
|
|
103
|
+
i += 1
|
|
104
|
+
field = ""
|
|
105
|
+
while i < n and token[i] != "=":
|
|
106
|
+
field += token[i]
|
|
107
|
+
i += 1
|
|
108
|
+
if i == n:
|
|
109
|
+
return _failed_resolution(original_token)
|
|
110
|
+
filter_field = field.replace("-", "_")
|
|
111
|
+
instance_id = None
|
|
112
|
+
if i < n and token[i] == "=":
|
|
113
|
+
i += 1
|
|
114
|
+
start = i
|
|
115
|
+
depth = 0
|
|
116
|
+
while i < n:
|
|
117
|
+
ch = token[i]
|
|
118
|
+
if ch == "[":
|
|
119
|
+
depth += 1
|
|
120
|
+
elif ch == "]" and depth:
|
|
121
|
+
depth -= 1
|
|
122
|
+
elif ch == "." and depth == 0:
|
|
123
|
+
break
|
|
124
|
+
i += 1
|
|
125
|
+
instance_id = token[start:i]
|
|
126
|
+
key = None
|
|
127
|
+
if i < n and token[i] == ".":
|
|
128
|
+
i += 1
|
|
129
|
+
start = i
|
|
130
|
+
while i < n and token[i] != "=":
|
|
131
|
+
i += 1
|
|
132
|
+
key = token[start:i]
|
|
133
|
+
param = None
|
|
134
|
+
if i < n and token[i] == "=":
|
|
135
|
+
param = token[i + 1 :]
|
|
136
|
+
normalized_root = root_name.replace("-", "_")
|
|
137
|
+
lookup_root = normalized_root.upper()
|
|
138
|
+
raw_key = key
|
|
139
|
+
normalized_key = None
|
|
140
|
+
key_upper = None
|
|
141
|
+
key_lower = None
|
|
142
|
+
if key:
|
|
143
|
+
normalized_key = key.replace("-", "_")
|
|
144
|
+
key_upper = normalized_key.upper()
|
|
145
|
+
key_lower = normalized_key.lower()
|
|
146
|
+
if param:
|
|
147
|
+
param = resolve_sigils(param, current)
|
|
148
|
+
if instance_id:
|
|
149
|
+
instance_id = resolve_sigils(instance_id, current)
|
|
150
|
+
SigilRoot = apps.get_model("core", "SigilRoot")
|
|
151
|
+
try:
|
|
152
|
+
root = SigilRoot.objects.get(prefix__iexact=lookup_root)
|
|
153
|
+
if root.context_type == SigilRoot.Context.CONFIG:
|
|
154
|
+
if not normalized_key:
|
|
155
|
+
return ""
|
|
156
|
+
if root.prefix.upper() == "ENV":
|
|
157
|
+
candidates = []
|
|
158
|
+
if raw_key:
|
|
159
|
+
candidates.append(raw_key.replace("-", "_"))
|
|
160
|
+
if normalized_key:
|
|
161
|
+
candidates.append(normalized_key)
|
|
162
|
+
if key_upper:
|
|
163
|
+
candidates.append(key_upper)
|
|
164
|
+
if key_lower:
|
|
165
|
+
candidates.append(key_lower)
|
|
166
|
+
seen_candidates: set[str] = set()
|
|
167
|
+
for candidate in candidates:
|
|
168
|
+
if not candidate or candidate in seen_candidates:
|
|
169
|
+
continue
|
|
170
|
+
seen_candidates.add(candidate)
|
|
171
|
+
val = os.environ.get(candidate)
|
|
172
|
+
if val is not None:
|
|
173
|
+
return val
|
|
174
|
+
logger.warning(
|
|
175
|
+
"Missing environment variable for sigil [ENV.%s]",
|
|
176
|
+
key_upper or normalized_key or raw_key or "",
|
|
177
|
+
)
|
|
178
|
+
return _failed_resolution(original_token)
|
|
179
|
+
if root.prefix.upper() == "SYS":
|
|
180
|
+
for candidate in [normalized_key, key_upper, key_lower]:
|
|
181
|
+
if not candidate:
|
|
182
|
+
continue
|
|
183
|
+
sentinel = object()
|
|
184
|
+
value = getattr(settings, candidate, sentinel)
|
|
185
|
+
if value is not sentinel:
|
|
186
|
+
return str(value)
|
|
187
|
+
fallback = _resolve_with_gway(f"[{original_token}]")
|
|
188
|
+
if fallback is not None:
|
|
189
|
+
return fallback
|
|
190
|
+
return ""
|
|
191
|
+
elif root.context_type == SigilRoot.Context.ENTITY:
|
|
192
|
+
model = root.content_type.model_class() if root.content_type else None
|
|
193
|
+
instance = None
|
|
194
|
+
if model:
|
|
195
|
+
if instance_id:
|
|
196
|
+
try:
|
|
197
|
+
if filter_field:
|
|
198
|
+
field_name = filter_field.lower()
|
|
199
|
+
try:
|
|
200
|
+
field_obj = model._meta.get_field(field_name)
|
|
201
|
+
except Exception:
|
|
202
|
+
field_obj = None
|
|
203
|
+
lookup: dict[str, str] = {}
|
|
204
|
+
if field_obj and isinstance(field_obj, models.CharField):
|
|
205
|
+
lookup = {f"{field_name}__iexact": instance_id}
|
|
206
|
+
else:
|
|
207
|
+
lookup = {field_name: instance_id}
|
|
208
|
+
instance = model.objects.filter(**lookup).first()
|
|
209
|
+
else:
|
|
210
|
+
instance = model.objects.filter(pk=instance_id).first()
|
|
211
|
+
except Exception:
|
|
212
|
+
instance = None
|
|
213
|
+
if instance is None and not filter_field:
|
|
214
|
+
for field in model._meta.fields:
|
|
215
|
+
if field.unique and isinstance(field, models.CharField):
|
|
216
|
+
instance = model.objects.filter(
|
|
217
|
+
**{f"{field.name}__iexact": instance_id}
|
|
218
|
+
).first()
|
|
219
|
+
if instance:
|
|
220
|
+
break
|
|
221
|
+
elif current and isinstance(current, model):
|
|
222
|
+
instance = current
|
|
223
|
+
else:
|
|
224
|
+
ctx = get_context()
|
|
225
|
+
inst_pk = ctx.get(model)
|
|
226
|
+
if inst_pk is not None:
|
|
227
|
+
instance = model.objects.filter(pk=inst_pk).first()
|
|
228
|
+
if instance is None:
|
|
229
|
+
instance = _first_instance(model)
|
|
230
|
+
if instance:
|
|
231
|
+
if normalized_key:
|
|
232
|
+
field = next(
|
|
233
|
+
(
|
|
234
|
+
f
|
|
235
|
+
for f in model._meta.fields
|
|
236
|
+
if f.name.lower() == (key_lower or "")
|
|
237
|
+
),
|
|
238
|
+
None,
|
|
239
|
+
)
|
|
240
|
+
if field:
|
|
241
|
+
val = getattr(instance, field.attname)
|
|
242
|
+
return "" if val is None else str(val)
|
|
243
|
+
return _failed_resolution(original_token)
|
|
244
|
+
return serializers.serialize("json", [instance])
|
|
245
|
+
return _failed_resolution(original_token)
|
|
246
|
+
except SigilRoot.DoesNotExist:
|
|
247
|
+
logger.warning("Unknown sigil root [%s]", lookup_root)
|
|
248
|
+
except Exception:
|
|
249
|
+
logger.exception(
|
|
250
|
+
"Error resolving sigil [%s.%s]",
|
|
251
|
+
lookup_root,
|
|
252
|
+
key_upper or normalized_key or raw_key,
|
|
253
|
+
)
|
|
254
|
+
return _failed_resolution(original_token)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def resolve_sigils(text: str, current: Optional[models.Model] = None) -> str:
|
|
258
|
+
result = ""
|
|
259
|
+
i = 0
|
|
260
|
+
while i < len(text):
|
|
261
|
+
if text[i] == "[":
|
|
262
|
+
depth = 1
|
|
263
|
+
j = i + 1
|
|
264
|
+
while j < len(text) and depth:
|
|
265
|
+
if text[j] == "[":
|
|
266
|
+
depth += 1
|
|
267
|
+
elif text[j] == "]":
|
|
268
|
+
depth -= 1
|
|
269
|
+
j += 1
|
|
270
|
+
if depth:
|
|
271
|
+
result += text[i]
|
|
272
|
+
i += 1
|
|
273
|
+
continue
|
|
274
|
+
token = text[i + 1 : j - 1]
|
|
275
|
+
result += _resolve_token(token, current)
|
|
276
|
+
i = j
|
|
277
|
+
else:
|
|
278
|
+
result += text[i]
|
|
279
|
+
i += 1
|
|
280
|
+
return result
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def resolve_sigil(sigil: str, current: Optional[models.Model] = None) -> str:
|
|
284
|
+
return resolve_sigils(sigil, current)
|
core/system.py
CHANGED
|
@@ -1,44 +1,182 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from contextlib import closing
|
|
3
4
|
from pathlib import Path
|
|
5
|
+
import re
|
|
4
6
|
import socket
|
|
5
7
|
import subprocess
|
|
6
8
|
import shutil
|
|
7
9
|
|
|
8
10
|
from django.conf import settings
|
|
9
11
|
from django.contrib import admin
|
|
10
|
-
from django.shortcuts import redirect
|
|
11
12
|
from django.template.response import TemplateResponse
|
|
12
|
-
from django.urls import path
|
|
13
|
+
from django.urls import path
|
|
13
14
|
from django.utils.translation import gettext_lazy as _
|
|
14
15
|
|
|
16
|
+
from utils import revision
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_RUNSERVER_PORT_PATTERN = re.compile(r":(\d{2,5})(?:\D|$)")
|
|
20
|
+
_RUNSERVER_PORT_FLAG_PATTERN = re.compile(r"--port(?:=|\s+)(\d{2,5})", re.IGNORECASE)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _parse_runserver_port(command_line: str) -> int | None:
|
|
24
|
+
"""Extract the HTTP port from a runserver command line."""
|
|
25
|
+
|
|
26
|
+
for pattern in (_RUNSERVER_PORT_PATTERN, _RUNSERVER_PORT_FLAG_PATTERN):
|
|
27
|
+
match = pattern.search(command_line)
|
|
28
|
+
if match:
|
|
29
|
+
try:
|
|
30
|
+
return int(match.group(1))
|
|
31
|
+
except ValueError:
|
|
32
|
+
continue
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _detect_runserver_process() -> tuple[bool, int | None]:
|
|
37
|
+
"""Return whether the dev server is running and the port if available."""
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
result = subprocess.run(
|
|
41
|
+
["pgrep", "-af", "manage.py runserver"],
|
|
42
|
+
capture_output=True,
|
|
43
|
+
text=True,
|
|
44
|
+
check=False,
|
|
45
|
+
)
|
|
46
|
+
except FileNotFoundError:
|
|
47
|
+
return False, None
|
|
48
|
+
except Exception:
|
|
49
|
+
return False, None
|
|
50
|
+
|
|
51
|
+
if result.returncode != 0:
|
|
52
|
+
return False, None
|
|
53
|
+
|
|
54
|
+
output = result.stdout.strip()
|
|
55
|
+
if not output:
|
|
56
|
+
return False, None
|
|
57
|
+
|
|
58
|
+
port = None
|
|
59
|
+
for line in output.splitlines():
|
|
60
|
+
port = _parse_runserver_port(line)
|
|
61
|
+
if port is not None:
|
|
62
|
+
break
|
|
63
|
+
|
|
64
|
+
if port is None:
|
|
65
|
+
port = 8000
|
|
66
|
+
|
|
67
|
+
return True, port
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _probe_ports(candidates: list[int]) -> tuple[bool, int | None]:
|
|
71
|
+
"""Attempt to connect to localhost on the provided ports."""
|
|
72
|
+
|
|
73
|
+
for port in candidates:
|
|
74
|
+
try:
|
|
75
|
+
with closing(socket.create_connection(("localhost", port), timeout=0.25)):
|
|
76
|
+
return True, port
|
|
77
|
+
except OSError:
|
|
78
|
+
continue
|
|
79
|
+
return False, None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _port_candidates(default_port: int) -> list[int]:
|
|
83
|
+
"""Return a prioritized list of ports to probe for the HTTP service."""
|
|
84
|
+
|
|
85
|
+
candidates = [default_port]
|
|
86
|
+
for port in (8000, 8888):
|
|
87
|
+
if port not in candidates:
|
|
88
|
+
candidates.append(port)
|
|
89
|
+
return candidates
|
|
90
|
+
|
|
15
91
|
|
|
16
92
|
def _gather_info() -> dict:
|
|
17
|
-
"""Collect basic system information similar to status
|
|
93
|
+
"""Collect basic system information similar to status.sh."""
|
|
18
94
|
base_dir = Path(settings.BASE_DIR)
|
|
19
95
|
lock_dir = base_dir / "locks"
|
|
20
96
|
info: dict[str, object] = {}
|
|
21
97
|
|
|
22
98
|
info["installed"] = (base_dir / ".venv").exists()
|
|
99
|
+
info["revision"] = revision.get_revision()
|
|
23
100
|
|
|
24
101
|
service_file = lock_dir / "service.lck"
|
|
25
|
-
info["service"] = (
|
|
26
|
-
service_file.read_text().strip() if service_file.exists() else ""
|
|
27
|
-
)
|
|
102
|
+
info["service"] = service_file.read_text().strip() if service_file.exists() else ""
|
|
28
103
|
|
|
29
104
|
mode_file = lock_dir / "nginx_mode.lck"
|
|
30
105
|
mode = mode_file.read_text().strip() if mode_file.exists() else "internal"
|
|
31
106
|
info["mode"] = mode
|
|
32
|
-
|
|
107
|
+
default_port = 8000 if mode == "public" else 8888
|
|
108
|
+
detected_port: int | None = None
|
|
109
|
+
|
|
110
|
+
screen_file = lock_dir / "screen_mode.lck"
|
|
111
|
+
info["screen_mode"] = (
|
|
112
|
+
screen_file.read_text().strip() if screen_file.exists() else ""
|
|
113
|
+
)
|
|
33
114
|
|
|
34
115
|
# Use settings.NODE_ROLE as the single source of truth for the node role.
|
|
35
116
|
info["role"] = getattr(settings, "NODE_ROLE", "Terminal")
|
|
36
117
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
118
|
+
features: list[dict[str, object]] = []
|
|
119
|
+
try:
|
|
120
|
+
from nodes.models import Node, NodeFeature
|
|
121
|
+
except Exception:
|
|
122
|
+
info["features"] = features
|
|
123
|
+
else:
|
|
124
|
+
feature_map: dict[str, dict[str, object]] = {}
|
|
125
|
+
|
|
126
|
+
def _add_feature(feature: NodeFeature, flag: str) -> None:
|
|
127
|
+
slug = getattr(feature, "slug", "") or ""
|
|
128
|
+
if not slug:
|
|
129
|
+
return
|
|
130
|
+
display = (getattr(feature, "display", "") or "").strip()
|
|
131
|
+
normalized = display or slug.replace("-", " ").title()
|
|
132
|
+
entry = feature_map.setdefault(
|
|
133
|
+
slug,
|
|
134
|
+
{
|
|
135
|
+
"slug": slug,
|
|
136
|
+
"display": normalized,
|
|
137
|
+
"expected": False,
|
|
138
|
+
"actual": False,
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
if display:
|
|
142
|
+
entry["display"] = display
|
|
143
|
+
entry[flag] = True
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
expected_features = (
|
|
147
|
+
NodeFeature.objects.filter(roles__name=info["role"]).only("slug", "display").distinct()
|
|
148
|
+
)
|
|
149
|
+
except Exception:
|
|
150
|
+
expected_features = []
|
|
151
|
+
try:
|
|
152
|
+
for feature in expected_features:
|
|
153
|
+
_add_feature(feature, "expected")
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
local_node = Node.get_local()
|
|
159
|
+
except Exception:
|
|
160
|
+
local_node = None
|
|
161
|
+
|
|
162
|
+
actual_features = []
|
|
163
|
+
if local_node:
|
|
164
|
+
try:
|
|
165
|
+
actual_features = list(local_node.features.only("slug", "display"))
|
|
166
|
+
except Exception:
|
|
167
|
+
actual_features = []
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
for feature in actual_features:
|
|
171
|
+
_add_feature(feature, "actual")
|
|
172
|
+
except Exception:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
features = sorted(
|
|
176
|
+
feature_map.values(),
|
|
177
|
+
key=lambda item: str(item.get("display", "")).lower(),
|
|
178
|
+
)
|
|
179
|
+
info["features"] = features
|
|
42
180
|
|
|
43
181
|
running = False
|
|
44
182
|
service_status = ""
|
|
@@ -56,17 +194,20 @@ def _gather_info() -> dict:
|
|
|
56
194
|
except Exception:
|
|
57
195
|
pass
|
|
58
196
|
else:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
["pgrep", "-f", "manage.py runserver"],
|
|
62
|
-
check=True,
|
|
63
|
-
stdout=subprocess.PIPE,
|
|
64
|
-
stderr=subprocess.PIPE,
|
|
65
|
-
)
|
|
197
|
+
process_running, process_port = _detect_runserver_process()
|
|
198
|
+
if process_running:
|
|
66
199
|
running = True
|
|
67
|
-
|
|
68
|
-
|
|
200
|
+
detected_port = process_port
|
|
201
|
+
|
|
202
|
+
if not running or detected_port is None:
|
|
203
|
+
probe_running, probe_port = _probe_ports(_port_candidates(default_port))
|
|
204
|
+
if probe_running:
|
|
205
|
+
running = True
|
|
206
|
+
if detected_port is None:
|
|
207
|
+
detected_port = probe_port
|
|
208
|
+
|
|
69
209
|
info["running"] = running
|
|
210
|
+
info["port"] = detected_port if detected_port is not None else default_port
|
|
70
211
|
info["service_status"] = service_status
|
|
71
212
|
|
|
72
213
|
try:
|
|
@@ -83,14 +224,6 @@ def _gather_info() -> dict:
|
|
|
83
224
|
|
|
84
225
|
def _system_view(request):
|
|
85
226
|
info = _gather_info()
|
|
86
|
-
if request.method == "POST" and request.user.is_superuser:
|
|
87
|
-
action = request.POST.get("action")
|
|
88
|
-
stop_script = Path(settings.BASE_DIR) / "stop.sh"
|
|
89
|
-
args = [str(stop_script)]
|
|
90
|
-
if action == "stop" and info["service"]:
|
|
91
|
-
args.append("--all")
|
|
92
|
-
subprocess.Popen(args)
|
|
93
|
-
return redirect(reverse("admin:index"))
|
|
94
227
|
|
|
95
228
|
context = admin.site.each_context(request)
|
|
96
229
|
context.update({"title": _("System"), "info": info})
|