arthexis 0.1.14__py3-none-any.whl → 0.1.16__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.14.dist-info → arthexis-0.1.16.dist-info}/METADATA +3 -2
- {arthexis-0.1.14.dist-info → arthexis-0.1.16.dist-info}/RECORD +41 -39
- config/urls.py +5 -0
- core/admin.py +200 -9
- core/admindocs.py +44 -3
- core/apps.py +1 -1
- core/backends.py +44 -8
- core/entity.py +17 -1
- core/github_issues.py +12 -7
- core/log_paths.py +24 -10
- core/mailer.py +9 -5
- core/models.py +92 -23
- core/release.py +173 -2
- core/system.py +411 -4
- core/tasks.py +5 -1
- core/test_system_info.py +16 -0
- core/tests.py +280 -0
- core/views.py +252 -38
- nodes/admin.py +25 -1
- nodes/models.py +99 -6
- nodes/rfid_sync.py +15 -0
- nodes/tests.py +142 -3
- nodes/utils.py +3 -0
- ocpp/consumers.py +38 -0
- ocpp/models.py +19 -4
- ocpp/tasks.py +156 -2
- ocpp/test_rfid.py +44 -2
- ocpp/tests.py +111 -1
- pages/admin.py +188 -5
- pages/context_processors.py +20 -1
- pages/middleware.py +4 -0
- pages/models.py +39 -1
- pages/module_defaults.py +156 -0
- pages/tasks.py +74 -0
- pages/tests.py +629 -8
- pages/urls.py +2 -0
- pages/utils.py +11 -0
- pages/views.py +106 -38
- {arthexis-0.1.14.dist-info → arthexis-0.1.16.dist-info}/WHEEL +0 -0
- {arthexis-0.1.14.dist-info → arthexis-0.1.16.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.14.dist-info → arthexis-0.1.16.dist-info}/top_level.txt +0 -0
core/system.py
CHANGED
|
@@ -10,17 +10,28 @@ import re
|
|
|
10
10
|
import socket
|
|
11
11
|
import subprocess
|
|
12
12
|
import shutil
|
|
13
|
+
import logging
|
|
13
14
|
from typing import Callable, Iterable, Optional
|
|
14
15
|
|
|
15
16
|
from django.conf import settings
|
|
16
|
-
from django.contrib import admin
|
|
17
|
+
from django.contrib import admin, messages
|
|
17
18
|
from django.template.response import TemplateResponse
|
|
18
|
-
from django.
|
|
19
|
+
from django.http import HttpResponseRedirect
|
|
20
|
+
from django.urls import path, reverse
|
|
19
21
|
from django.utils import timezone
|
|
20
22
|
from django.utils.formats import date_format
|
|
21
|
-
from django.utils.
|
|
23
|
+
from django.utils.html import format_html, format_html_join
|
|
24
|
+
from django.utils.translation import gettext_lazy as _, ngettext
|
|
22
25
|
|
|
23
26
|
from core.auto_upgrade import AUTO_UPGRADE_TASK_NAME, AUTO_UPGRADE_TASK_PATH
|
|
27
|
+
from core import changelog as changelog_utils
|
|
28
|
+
from core.release import (
|
|
29
|
+
_git_authentication_missing,
|
|
30
|
+
_git_remote_url,
|
|
31
|
+
_manager_git_credentials,
|
|
32
|
+
_remote_with_credentials,
|
|
33
|
+
)
|
|
34
|
+
from core.tasks import check_github_updates
|
|
24
35
|
from utils import revision
|
|
25
36
|
|
|
26
37
|
|
|
@@ -29,6 +40,9 @@ AUTO_UPGRADE_SKIP_LOCK_NAME = "auto_upgrade_skip_revisions.lck"
|
|
|
29
40
|
AUTO_UPGRADE_LOG_NAME = "auto-upgrade.log"
|
|
30
41
|
|
|
31
42
|
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
32
46
|
def _auto_upgrade_mode_file(base_dir: Path) -> Path:
|
|
33
47
|
return base_dir / "locks" / AUTO_UPGRADE_LOCK_NAME
|
|
34
48
|
|
|
@@ -41,6 +55,246 @@ def _auto_upgrade_log_file(base_dir: Path) -> Path:
|
|
|
41
55
|
return base_dir / "logs" / AUTO_UPGRADE_LOG_NAME
|
|
42
56
|
|
|
43
57
|
|
|
58
|
+
def _open_changelog_entries() -> list[dict[str, str]]:
|
|
59
|
+
"""Return changelog entries that are not yet part of a tagged release."""
|
|
60
|
+
|
|
61
|
+
changelog_path = Path("CHANGELOG.rst")
|
|
62
|
+
try:
|
|
63
|
+
text = changelog_path.read_text(encoding="utf-8")
|
|
64
|
+
except FileNotFoundError:
|
|
65
|
+
return []
|
|
66
|
+
except OSError:
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
collecting = False
|
|
70
|
+
entries: list[dict[str, str]] = []
|
|
71
|
+
for raw_line in text.splitlines():
|
|
72
|
+
line = raw_line.strip()
|
|
73
|
+
if not collecting:
|
|
74
|
+
if line == "Unreleased":
|
|
75
|
+
collecting = True
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
if not line:
|
|
79
|
+
if entries:
|
|
80
|
+
break
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
if set(line) == {"-"}:
|
|
84
|
+
# Underline immediately following the section heading.
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
if not line.startswith("- "):
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
trimmed = line[2:].strip()
|
|
91
|
+
if not trimmed:
|
|
92
|
+
continue
|
|
93
|
+
parts = trimmed.split(" ", 1)
|
|
94
|
+
sha = parts[0]
|
|
95
|
+
message = parts[1] if len(parts) > 1 else ""
|
|
96
|
+
entries.append({"sha": sha, "message": message})
|
|
97
|
+
|
|
98
|
+
return entries
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _exclude_changelog_entries(shas: Iterable[str]) -> int:
|
|
102
|
+
"""Remove entries matching ``shas`` from the changelog.
|
|
103
|
+
|
|
104
|
+
Returns the number of entries removed. Only entries within the
|
|
105
|
+
``Unreleased`` section are considered.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
normalized_shas = {sha.strip() for sha in shas if sha and sha.strip()}
|
|
109
|
+
if not normalized_shas:
|
|
110
|
+
return 0
|
|
111
|
+
|
|
112
|
+
changelog_path = Path("CHANGELOG.rst")
|
|
113
|
+
try:
|
|
114
|
+
text = changelog_path.read_text(encoding="utf-8")
|
|
115
|
+
except (FileNotFoundError, OSError):
|
|
116
|
+
return 0
|
|
117
|
+
|
|
118
|
+
lines = text.splitlines(keepends=True)
|
|
119
|
+
new_lines: list[str] = []
|
|
120
|
+
collecting = False
|
|
121
|
+
removed = 0
|
|
122
|
+
|
|
123
|
+
for raw_line in lines:
|
|
124
|
+
stripped = raw_line.strip()
|
|
125
|
+
|
|
126
|
+
if not collecting:
|
|
127
|
+
new_lines.append(raw_line)
|
|
128
|
+
if stripped == "Unreleased":
|
|
129
|
+
collecting = True
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
if not stripped:
|
|
133
|
+
new_lines.append(raw_line)
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
if set(stripped) == {"-"}:
|
|
137
|
+
new_lines.append(raw_line)
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
if not stripped.startswith("- "):
|
|
141
|
+
new_lines.append(raw_line)
|
|
142
|
+
collecting = False
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
trimmed = stripped[2:].strip()
|
|
146
|
+
if not trimmed:
|
|
147
|
+
new_lines.append(raw_line)
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
sha = trimmed.split(" ", 1)[0]
|
|
151
|
+
if sha in normalized_shas:
|
|
152
|
+
removed += 1
|
|
153
|
+
normalized_shas.remove(sha)
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
new_lines.append(raw_line)
|
|
157
|
+
|
|
158
|
+
if removed:
|
|
159
|
+
new_text = "".join(new_lines)
|
|
160
|
+
if not new_text.endswith("\n"):
|
|
161
|
+
new_text += "\n"
|
|
162
|
+
changelog_path.write_text(new_text, encoding="utf-8")
|
|
163
|
+
|
|
164
|
+
return removed
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _regenerate_changelog() -> None:
|
|
168
|
+
"""Rebuild the changelog file using recent git commits."""
|
|
169
|
+
|
|
170
|
+
changelog_path = Path("CHANGELOG.rst")
|
|
171
|
+
previous_text = (
|
|
172
|
+
changelog_path.read_text(encoding="utf-8") if changelog_path.exists() else None
|
|
173
|
+
)
|
|
174
|
+
range_spec = changelog_utils.determine_range_spec()
|
|
175
|
+
sections = changelog_utils.collect_sections(
|
|
176
|
+
range_spec=range_spec, previous_text=previous_text
|
|
177
|
+
)
|
|
178
|
+
content = changelog_utils.render_changelog(sections)
|
|
179
|
+
if not content.endswith("\n"):
|
|
180
|
+
content += "\n"
|
|
181
|
+
changelog_path.write_text(content, encoding="utf-8")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _format_git_command_output(
|
|
185
|
+
command: list[str], result: subprocess.CompletedProcess[str]
|
|
186
|
+
) -> str:
|
|
187
|
+
"""Return a readable summary of a git command execution."""
|
|
188
|
+
|
|
189
|
+
command_display = "$ " + " ".join(command)
|
|
190
|
+
message_parts = []
|
|
191
|
+
if result.stdout:
|
|
192
|
+
message_parts.append(result.stdout.strip())
|
|
193
|
+
if result.stderr:
|
|
194
|
+
message_parts.append(result.stderr.strip())
|
|
195
|
+
if result.returncode != 0:
|
|
196
|
+
message_parts.append(f"[exit status {result.returncode}]")
|
|
197
|
+
if message_parts:
|
|
198
|
+
return command_display + "\n" + "\n".join(part for part in message_parts if part)
|
|
199
|
+
return command_display
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _git_status() -> str:
|
|
203
|
+
"""Return the repository status after attempting to commit."""
|
|
204
|
+
|
|
205
|
+
status_result = subprocess.run(
|
|
206
|
+
["git", "status", "--short", "--branch"],
|
|
207
|
+
capture_output=True,
|
|
208
|
+
text=True,
|
|
209
|
+
check=False,
|
|
210
|
+
)
|
|
211
|
+
stdout = status_result.stdout.strip()
|
|
212
|
+
stderr = status_result.stderr.strip()
|
|
213
|
+
if stdout and stderr:
|
|
214
|
+
return stdout + "\n" + stderr
|
|
215
|
+
return stdout or stderr
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _commit_changelog() -> tuple[bool, str, str]:
|
|
219
|
+
"""Stage, commit, and push the changelog file."""
|
|
220
|
+
|
|
221
|
+
def _retry_push_with_release_credentials(
|
|
222
|
+
command: list[str],
|
|
223
|
+
result: subprocess.CompletedProcess[str],
|
|
224
|
+
) -> bool:
|
|
225
|
+
exc = subprocess.CalledProcessError(
|
|
226
|
+
result.returncode,
|
|
227
|
+
command,
|
|
228
|
+
output=result.stdout,
|
|
229
|
+
stderr=result.stderr,
|
|
230
|
+
)
|
|
231
|
+
if not _git_authentication_missing(exc):
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
creds = _manager_git_credentials()
|
|
235
|
+
if not creds or not creds.has_auth():
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
remote_url = _git_remote_url("origin")
|
|
239
|
+
if not remote_url:
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
authed_url = _remote_with_credentials(remote_url, creds)
|
|
243
|
+
if not authed_url:
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
retry_command = ["git", "push", authed_url]
|
|
247
|
+
retry_result = subprocess.run(
|
|
248
|
+
retry_command,
|
|
249
|
+
capture_output=True,
|
|
250
|
+
text=True,
|
|
251
|
+
check=False,
|
|
252
|
+
)
|
|
253
|
+
formatted_retry = _format_git_command_output(retry_command, retry_result)
|
|
254
|
+
if formatted_retry:
|
|
255
|
+
outputs.append(formatted_retry)
|
|
256
|
+
logger.info(
|
|
257
|
+
"Executed %s with exit code %s",
|
|
258
|
+
retry_command,
|
|
259
|
+
retry_result.returncode,
|
|
260
|
+
)
|
|
261
|
+
return retry_result.returncode == 0
|
|
262
|
+
|
|
263
|
+
git_commands: list[list[str]] = [
|
|
264
|
+
["git", "add", "CHANGELOG.rst"],
|
|
265
|
+
[
|
|
266
|
+
"git",
|
|
267
|
+
"commit",
|
|
268
|
+
"-m",
|
|
269
|
+
"chore: update changelog",
|
|
270
|
+
"--",
|
|
271
|
+
"CHANGELOG.rst",
|
|
272
|
+
],
|
|
273
|
+
["git", "push"],
|
|
274
|
+
]
|
|
275
|
+
outputs: list[str] = []
|
|
276
|
+
success = True
|
|
277
|
+
|
|
278
|
+
for command in git_commands:
|
|
279
|
+
result = subprocess.run(
|
|
280
|
+
command, capture_output=True, text=True, check=False
|
|
281
|
+
)
|
|
282
|
+
formatted = _format_git_command_output(command, result)
|
|
283
|
+
outputs.append(formatted)
|
|
284
|
+
logger.info("Executed %s with exit code %s", command, result.returncode)
|
|
285
|
+
if result.returncode != 0:
|
|
286
|
+
if command[:2] == ["git", "push"] and _retry_push_with_release_credentials(
|
|
287
|
+
command, result
|
|
288
|
+
):
|
|
289
|
+
continue
|
|
290
|
+
success = False
|
|
291
|
+
break
|
|
292
|
+
|
|
293
|
+
command_output = "\n\n".join(output for output in outputs if output)
|
|
294
|
+
repo_status = _git_status()
|
|
295
|
+
return success, command_output, repo_status
|
|
296
|
+
|
|
297
|
+
|
|
44
298
|
@dataclass(frozen=True)
|
|
45
299
|
class SystemField:
|
|
46
300
|
"""Metadata describing a single entry on the system admin page."""
|
|
@@ -584,7 +838,14 @@ def _gather_info() -> dict:
|
|
|
584
838
|
info["service"] = service_file.read_text().strip() if service_file.exists() else ""
|
|
585
839
|
|
|
586
840
|
mode_file = lock_dir / "nginx_mode.lck"
|
|
587
|
-
|
|
841
|
+
if mode_file.exists():
|
|
842
|
+
try:
|
|
843
|
+
raw_mode = mode_file.read_text().strip()
|
|
844
|
+
except OSError:
|
|
845
|
+
raw_mode = ""
|
|
846
|
+
else:
|
|
847
|
+
raw_mode = ""
|
|
848
|
+
mode = raw_mode.lower() or "internal"
|
|
588
849
|
info["mode"] = mode
|
|
589
850
|
default_port = 8000 if mode == "public" else 8888
|
|
590
851
|
detected_port: int | None = None
|
|
@@ -721,6 +982,99 @@ def _system_view(request):
|
|
|
721
982
|
return TemplateResponse(request, "admin/system.html", context)
|
|
722
983
|
|
|
723
984
|
|
|
985
|
+
def _system_changelog_report_view(request):
|
|
986
|
+
if request.method == "POST":
|
|
987
|
+
action = request.POST.get("action")
|
|
988
|
+
if action == "exclude":
|
|
989
|
+
selected_shas = request.POST.getlist("selected_shas")
|
|
990
|
+
removed = _exclude_changelog_entries(selected_shas)
|
|
991
|
+
if removed:
|
|
992
|
+
messages.success(
|
|
993
|
+
request,
|
|
994
|
+
ngettext(
|
|
995
|
+
"Excluded %(count)d changelog entry.",
|
|
996
|
+
"Excluded %(count)d changelog entries.",
|
|
997
|
+
removed,
|
|
998
|
+
)
|
|
999
|
+
% {"count": removed},
|
|
1000
|
+
)
|
|
1001
|
+
else:
|
|
1002
|
+
if selected_shas:
|
|
1003
|
+
messages.info(
|
|
1004
|
+
request,
|
|
1005
|
+
_(
|
|
1006
|
+
"The selected changelog entries were not found or have already been excluded."
|
|
1007
|
+
),
|
|
1008
|
+
)
|
|
1009
|
+
else:
|
|
1010
|
+
messages.info(
|
|
1011
|
+
request,
|
|
1012
|
+
_("Select at least one changelog entry to exclude."),
|
|
1013
|
+
)
|
|
1014
|
+
elif action == "commit":
|
|
1015
|
+
success, command_output, repo_status = _commit_changelog()
|
|
1016
|
+
details: list[str] = []
|
|
1017
|
+
if command_output:
|
|
1018
|
+
details.append(
|
|
1019
|
+
format_html(
|
|
1020
|
+
"<div class=\"changelog-git-output\"><strong>{}</strong><pre>{}</pre></div>",
|
|
1021
|
+
_("Command log"),
|
|
1022
|
+
command_output,
|
|
1023
|
+
)
|
|
1024
|
+
)
|
|
1025
|
+
if repo_status:
|
|
1026
|
+
details.append(
|
|
1027
|
+
format_html(
|
|
1028
|
+
"<div class=\"changelog-git-status\"><strong>{}</strong><pre>{}</pre></div>",
|
|
1029
|
+
_("Repository status"),
|
|
1030
|
+
repo_status,
|
|
1031
|
+
)
|
|
1032
|
+
)
|
|
1033
|
+
details_html = (
|
|
1034
|
+
format_html_join("", "{}", ((detail,) for detail in details))
|
|
1035
|
+
if details
|
|
1036
|
+
else ""
|
|
1037
|
+
)
|
|
1038
|
+
if success:
|
|
1039
|
+
base_message = _("Committed the changelog and pushed to the current branch.")
|
|
1040
|
+
messages.success(request, format_html("{}{}", base_message, details_html))
|
|
1041
|
+
else:
|
|
1042
|
+
base_message = _("Unable to commit the changelog.")
|
|
1043
|
+
messages.error(request, format_html("{}{}", base_message, details_html))
|
|
1044
|
+
else:
|
|
1045
|
+
try:
|
|
1046
|
+
_regenerate_changelog()
|
|
1047
|
+
except subprocess.CalledProcessError as exc:
|
|
1048
|
+
logger.exception("Changelog regeneration failed")
|
|
1049
|
+
messages.error(
|
|
1050
|
+
request,
|
|
1051
|
+
_("Unable to recalculate the changelog: %(error)s")
|
|
1052
|
+
% {"error": exc.stderr.strip() if exc.stderr else str(exc)},
|
|
1053
|
+
)
|
|
1054
|
+
except Exception as exc: # pragma: no cover - unexpected failure
|
|
1055
|
+
logger.exception("Unexpected error while regenerating changelog")
|
|
1056
|
+
messages.error(
|
|
1057
|
+
request,
|
|
1058
|
+
_("Unable to recalculate the changelog: %(error)s")
|
|
1059
|
+
% {"error": str(exc)},
|
|
1060
|
+
)
|
|
1061
|
+
else:
|
|
1062
|
+
messages.success(
|
|
1063
|
+
request,
|
|
1064
|
+
_("Successfully recalculated the changelog from recent commits."),
|
|
1065
|
+
)
|
|
1066
|
+
return HttpResponseRedirect(reverse("admin:system-changelog-report"))
|
|
1067
|
+
|
|
1068
|
+
context = admin.site.each_context(request)
|
|
1069
|
+
context.update(
|
|
1070
|
+
{
|
|
1071
|
+
"title": _("Changelog Report"),
|
|
1072
|
+
"open_changelog_entries": _open_changelog_entries(),
|
|
1073
|
+
}
|
|
1074
|
+
)
|
|
1075
|
+
return TemplateResponse(request, "admin/system_changelog_report.html", context)
|
|
1076
|
+
|
|
1077
|
+
|
|
724
1078
|
def _system_upgrade_report_view(request):
|
|
725
1079
|
context = admin.site.each_context(request)
|
|
726
1080
|
context.update(
|
|
@@ -732,6 +1086,49 @@ def _system_upgrade_report_view(request):
|
|
|
732
1086
|
return TemplateResponse(request, "admin/system_upgrade_report.html", context)
|
|
733
1087
|
|
|
734
1088
|
|
|
1089
|
+
def _trigger_upgrade_check() -> bool:
|
|
1090
|
+
"""Return ``True`` when the upgrade check was queued asynchronously."""
|
|
1091
|
+
|
|
1092
|
+
try:
|
|
1093
|
+
check_github_updates.delay()
|
|
1094
|
+
except Exception:
|
|
1095
|
+
logger.exception("Failed to enqueue upgrade check; running synchronously instead")
|
|
1096
|
+
check_github_updates()
|
|
1097
|
+
return False
|
|
1098
|
+
return True
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
def _system_trigger_upgrade_check_view(request):
|
|
1102
|
+
if request.method != "POST":
|
|
1103
|
+
return HttpResponseRedirect(reverse("admin:system-upgrade-report"))
|
|
1104
|
+
|
|
1105
|
+
try:
|
|
1106
|
+
queued = _trigger_upgrade_check()
|
|
1107
|
+
except Exception as exc: # pragma: no cover - unexpected failure
|
|
1108
|
+
logger.exception("Unable to trigger upgrade check")
|
|
1109
|
+
messages.error(
|
|
1110
|
+
request,
|
|
1111
|
+
_("Unable to trigger an upgrade check: %(error)s")
|
|
1112
|
+
% {"error": str(exc)},
|
|
1113
|
+
)
|
|
1114
|
+
else:
|
|
1115
|
+
if queued:
|
|
1116
|
+
messages.success(
|
|
1117
|
+
request,
|
|
1118
|
+
_("Upgrade check requested. The task will run shortly."),
|
|
1119
|
+
)
|
|
1120
|
+
else:
|
|
1121
|
+
messages.success(
|
|
1122
|
+
request,
|
|
1123
|
+
_(
|
|
1124
|
+
"Upgrade check started locally. Review the auto-upgrade log for"
|
|
1125
|
+
" progress."
|
|
1126
|
+
),
|
|
1127
|
+
)
|
|
1128
|
+
|
|
1129
|
+
return HttpResponseRedirect(reverse("admin:system-upgrade-report"))
|
|
1130
|
+
|
|
1131
|
+
|
|
735
1132
|
def patch_admin_system_view() -> None:
|
|
736
1133
|
"""Add custom admin view for system information."""
|
|
737
1134
|
original_get_urls = admin.site.get_urls
|
|
@@ -740,11 +1137,21 @@ def patch_admin_system_view() -> None:
|
|
|
740
1137
|
urls = original_get_urls()
|
|
741
1138
|
custom = [
|
|
742
1139
|
path("system/", admin.site.admin_view(_system_view), name="system"),
|
|
1140
|
+
path(
|
|
1141
|
+
"system/changelog-report/",
|
|
1142
|
+
admin.site.admin_view(_system_changelog_report_view),
|
|
1143
|
+
name="system-changelog-report",
|
|
1144
|
+
),
|
|
743
1145
|
path(
|
|
744
1146
|
"system/upgrade-report/",
|
|
745
1147
|
admin.site.admin_view(_system_upgrade_report_view),
|
|
746
1148
|
name="system-upgrade-report",
|
|
747
1149
|
),
|
|
1150
|
+
path(
|
|
1151
|
+
"system/upgrade-report/run-check/",
|
|
1152
|
+
admin.site.admin_view(_system_trigger_upgrade_check_view),
|
|
1153
|
+
name="system-upgrade-run-check",
|
|
1154
|
+
),
|
|
748
1155
|
]
|
|
749
1156
|
return custom + urls
|
|
750
1157
|
|
core/tasks.py
CHANGED
|
@@ -132,11 +132,15 @@ def check_github_updates() -> None:
|
|
|
132
132
|
mode = "version"
|
|
133
133
|
if mode_file.exists():
|
|
134
134
|
try:
|
|
135
|
-
|
|
135
|
+
raw_mode = mode_file.read_text().strip()
|
|
136
136
|
except (OSError, UnicodeDecodeError):
|
|
137
137
|
logger.warning(
|
|
138
138
|
"Failed to read auto-upgrade mode lockfile", exc_info=True
|
|
139
139
|
)
|
|
140
|
+
else:
|
|
141
|
+
cleaned_mode = raw_mode.lower()
|
|
142
|
+
if cleaned_mode:
|
|
143
|
+
mode = cleaned_mode
|
|
140
144
|
|
|
141
145
|
branch = "main"
|
|
142
146
|
subprocess.run(["git", "fetch", "origin", branch], cwd=base_dir, check=True)
|
core/test_system_info.py
CHANGED
|
@@ -55,6 +55,22 @@ class SystemInfoScreenModeTests(SimpleTestCase):
|
|
|
55
55
|
lock_dir.rmdir()
|
|
56
56
|
|
|
57
57
|
|
|
58
|
+
class SystemInfoModeTests(SimpleTestCase):
|
|
59
|
+
def test_public_mode_case_insensitive(self):
|
|
60
|
+
lock_dir = Path(settings.BASE_DIR) / "locks"
|
|
61
|
+
lock_dir.mkdir(exist_ok=True)
|
|
62
|
+
lock_file = lock_dir / "nginx_mode.lck"
|
|
63
|
+
lock_file.write_text("PUBLIC", encoding="utf-8")
|
|
64
|
+
try:
|
|
65
|
+
info = _gather_info()
|
|
66
|
+
self.assertEqual(info["mode"], "public")
|
|
67
|
+
self.assertEqual(info["port"], 8000)
|
|
68
|
+
finally:
|
|
69
|
+
lock_file.unlink()
|
|
70
|
+
if not any(lock_dir.iterdir()):
|
|
71
|
+
lock_dir.rmdir()
|
|
72
|
+
|
|
73
|
+
|
|
58
74
|
class SystemInfoRevisionTests(SimpleTestCase):
|
|
59
75
|
@patch("core.system.revision.get_revision", return_value="abcdef1234567890")
|
|
60
76
|
def test_includes_full_revision(self, mock_revision):
|