arthexis 0.1.15__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.15.dist-info → arthexis-0.1.16.dist-info}/METADATA +1 -2
- {arthexis-0.1.15.dist-info → arthexis-0.1.16.dist-info}/RECORD +36 -35
- config/urls.py +5 -0
- core/admin.py +174 -7
- core/admindocs.py +44 -3
- core/apps.py +1 -1
- core/backends.py +44 -8
- core/github_issues.py +12 -7
- core/mailer.py +9 -5
- core/models.py +64 -23
- core/release.py +52 -0
- core/system.py +208 -1
- core/tasks.py +5 -1
- core/test_system_info.py +16 -0
- core/tests.py +207 -0
- core/views.py +221 -33
- nodes/admin.py +25 -1
- nodes/models.py +70 -4
- nodes/rfid_sync.py +15 -0
- nodes/tests.py +119 -0
- 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 +126 -4
- pages/context_processors.py +20 -1
- pages/models.py +3 -1
- pages/module_defaults.py +156 -0
- pages/tests.py +215 -7
- pages/urls.py +1 -0
- pages/views.py +61 -4
- {arthexis-0.1.15.dist-info → arthexis-0.1.16.dist-info}/WHEEL +0 -0
- {arthexis-0.1.15.dist-info → arthexis-0.1.16.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.15.dist-info → arthexis-0.1.16.dist-info}/top_level.txt +0 -0
core/mailer.py
CHANGED
|
@@ -45,11 +45,15 @@ def send(
|
|
|
45
45
|
)
|
|
46
46
|
if attachments:
|
|
47
47
|
for attachment in attachments:
|
|
48
|
-
if
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
if isinstance(attachment, (list, tuple)):
|
|
49
|
+
length = len(attachment)
|
|
50
|
+
if length not in {2, 3}:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
"attachments must contain 2- or 3-item (name, content, mimetype) tuples"
|
|
53
|
+
)
|
|
54
|
+
email.attach(*attachment)
|
|
55
|
+
else:
|
|
56
|
+
email.attach(attachment)
|
|
53
57
|
if content_subtype:
|
|
54
58
|
email.content_subtype = content_subtype
|
|
55
59
|
email.send(fail_silently=fail_silently)
|
core/models.py
CHANGED
|
@@ -586,10 +586,10 @@ class OdooProfile(Profile):
|
|
|
586
586
|
"""Return the display label for this profile."""
|
|
587
587
|
|
|
588
588
|
username = self._resolved_field_value("username")
|
|
589
|
+
if username:
|
|
590
|
+
return username
|
|
589
591
|
database = self._resolved_field_value("database")
|
|
590
|
-
|
|
591
|
-
return f"{username}@{database}"
|
|
592
|
-
return username or database or ""
|
|
592
|
+
return database or ""
|
|
593
593
|
|
|
594
594
|
def save(self, *args, **kwargs):
|
|
595
595
|
if self.pk:
|
|
@@ -1816,17 +1816,22 @@ class RFID(Entity):
|
|
|
1816
1816
|
blank=True,
|
|
1817
1817
|
help_text="Optional command executed during validation.",
|
|
1818
1818
|
)
|
|
1819
|
+
post_auth_command = models.TextField(
|
|
1820
|
+
default="",
|
|
1821
|
+
blank=True,
|
|
1822
|
+
help_text="Optional command executed after successful validation.",
|
|
1823
|
+
)
|
|
1819
1824
|
BLACK = "B"
|
|
1820
1825
|
WHITE = "W"
|
|
1821
1826
|
BLUE = "U"
|
|
1822
1827
|
RED = "R"
|
|
1823
1828
|
GREEN = "G"
|
|
1824
1829
|
COLOR_CHOICES = [
|
|
1825
|
-
(BLACK, "Black"),
|
|
1826
|
-
(WHITE, "White"),
|
|
1827
|
-
(BLUE, "Blue"),
|
|
1828
|
-
(RED, "Red"),
|
|
1829
|
-
(GREEN, "Green"),
|
|
1830
|
+
(BLACK, _("Black")),
|
|
1831
|
+
(WHITE, _("White")),
|
|
1832
|
+
(BLUE, _("Blue")),
|
|
1833
|
+
(RED, _("Red")),
|
|
1834
|
+
(GREEN, _("Green")),
|
|
1830
1835
|
]
|
|
1831
1836
|
SCAN_LABEL_STEP = 10
|
|
1832
1837
|
COPY_LABEL_STEP = 1
|
|
@@ -1838,8 +1843,8 @@ class RFID(Entity):
|
|
|
1838
1843
|
CLASSIC = "CLASSIC"
|
|
1839
1844
|
NTAG215 = "NTAG215"
|
|
1840
1845
|
KIND_CHOICES = [
|
|
1841
|
-
(CLASSIC, "MIFARE Classic"),
|
|
1842
|
-
(NTAG215, "NTAG215"),
|
|
1846
|
+
(CLASSIC, _("MIFARE Classic")),
|
|
1847
|
+
(NTAG215, _("NTAG215")),
|
|
1843
1848
|
]
|
|
1844
1849
|
kind = models.CharField(
|
|
1845
1850
|
max_length=8,
|
|
@@ -3057,7 +3062,16 @@ class ReleaseManager(Profile):
|
|
|
3057
3062
|
),
|
|
3058
3063
|
)
|
|
3059
3064
|
pypi_password = SigilShortAutoField("PyPI password", max_length=200, blank=True)
|
|
3060
|
-
pypi_url = SigilShortAutoField(
|
|
3065
|
+
pypi_url = SigilShortAutoField(
|
|
3066
|
+
"PyPI URL",
|
|
3067
|
+
max_length=200,
|
|
3068
|
+
blank=True,
|
|
3069
|
+
help_text=(
|
|
3070
|
+
"Link to the PyPI user profile (for example, https://pypi.org/user/username/). "
|
|
3071
|
+
"Use the account's user page, not a project-specific URL. "
|
|
3072
|
+
"This value is informational and not used for uploads."
|
|
3073
|
+
),
|
|
3074
|
+
)
|
|
3061
3075
|
secondary_pypi_url = SigilShortAutoField(
|
|
3062
3076
|
"Secondary PyPI URL",
|
|
3063
3077
|
max_length=200,
|
|
@@ -3103,6 +3117,13 @@ class ReleaseManager(Profile):
|
|
|
3103
3117
|
username = (self.git_username or "").strip()
|
|
3104
3118
|
password_source = self.git_password or self.github_token or ""
|
|
3105
3119
|
password = password_source.strip()
|
|
3120
|
+
|
|
3121
|
+
if password and not username and password_source == self.github_token:
|
|
3122
|
+
# GitHub personal access tokens require a username when used for
|
|
3123
|
+
# HTTPS pushes. Default to the recommended ``x-access-token`` so
|
|
3124
|
+
# release managers only need to provide their token.
|
|
3125
|
+
username = "x-access-token"
|
|
3126
|
+
|
|
3106
3127
|
if username and password:
|
|
3107
3128
|
return GitCredentials(username=username, password=password)
|
|
3108
3129
|
return None
|
|
@@ -3217,13 +3238,22 @@ class PackageRelease(Entity):
|
|
|
3217
3238
|
def dump_fixture(cls) -> None:
|
|
3218
3239
|
base = Path("core/fixtures")
|
|
3219
3240
|
base.mkdir(parents=True, exist_ok=True)
|
|
3220
|
-
for
|
|
3221
|
-
|
|
3241
|
+
existing = {path.name: path for path in base.glob("releases__*.json")}
|
|
3242
|
+
expected: set[str] = set()
|
|
3222
3243
|
for release in cls.objects.all():
|
|
3223
3244
|
name = f"releases__packagerelease_{release.version.replace('.', '_')}.json"
|
|
3224
3245
|
path = base / name
|
|
3225
3246
|
data = serializers.serialize("json", [release])
|
|
3226
|
-
|
|
3247
|
+
expected.add(name)
|
|
3248
|
+
try:
|
|
3249
|
+
current = path.read_text(encoding="utf-8")
|
|
3250
|
+
except FileNotFoundError:
|
|
3251
|
+
current = None
|
|
3252
|
+
if current != data:
|
|
3253
|
+
path.write_text(data, encoding="utf-8")
|
|
3254
|
+
for old_name, old_path in existing.items():
|
|
3255
|
+
if old_name not in expected and old_path.exists():
|
|
3256
|
+
old_path.unlink()
|
|
3227
3257
|
|
|
3228
3258
|
def __str__(self) -> str: # pragma: no cover - trivial
|
|
3229
3259
|
return f"{self.package.name} {self.version}"
|
|
@@ -3236,7 +3266,18 @@ class PackageRelease(Entity):
|
|
|
3236
3266
|
"""Return :class:`Credentials` from the associated release manager."""
|
|
3237
3267
|
manager = self.release_manager or self.package.release_manager
|
|
3238
3268
|
if manager:
|
|
3239
|
-
|
|
3269
|
+
creds = manager.to_credentials()
|
|
3270
|
+
if creds and creds.has_auth():
|
|
3271
|
+
return creds
|
|
3272
|
+
|
|
3273
|
+
token = (os.environ.get("PYPI_API_TOKEN") or "").strip()
|
|
3274
|
+
username = (os.environ.get("PYPI_USERNAME") or "").strip()
|
|
3275
|
+
password = (os.environ.get("PYPI_PASSWORD") or "").strip()
|
|
3276
|
+
|
|
3277
|
+
if token:
|
|
3278
|
+
return Credentials(token=token)
|
|
3279
|
+
if username and password:
|
|
3280
|
+
return Credentials(username=username, password=password)
|
|
3240
3281
|
return None
|
|
3241
3282
|
|
|
3242
3283
|
def get_github_token(self) -> str | None:
|
|
@@ -3252,12 +3293,8 @@ class PackageRelease(Entity):
|
|
|
3252
3293
|
manager = self.release_manager or self.package.release_manager
|
|
3253
3294
|
targets: list[RepositoryTarget] = []
|
|
3254
3295
|
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
primary_url = manager.pypi_url.strip()
|
|
3258
|
-
if not primary_url:
|
|
3259
|
-
env_primary = os.environ.get("PYPI_REPOSITORY_URL", "")
|
|
3260
|
-
primary_url = env_primary.strip()
|
|
3296
|
+
env_primary = os.environ.get("PYPI_REPOSITORY_URL", "")
|
|
3297
|
+
primary_url = env_primary.strip()
|
|
3261
3298
|
|
|
3262
3299
|
primary_creds = self.to_credentials()
|
|
3263
3300
|
targets.append(
|
|
@@ -3381,10 +3418,12 @@ class PackageRelease(Entity):
|
|
|
3381
3418
|
|
|
3382
3419
|
@classmethod
|
|
3383
3420
|
def latest(cls):
|
|
3384
|
-
"""Return the latest release by version."""
|
|
3421
|
+
"""Return the latest release by version, preferring active packages."""
|
|
3385
3422
|
from packaging.version import Version
|
|
3386
3423
|
|
|
3387
|
-
releases = list(cls.objects.
|
|
3424
|
+
releases = list(cls.objects.filter(package__is_active=True))
|
|
3425
|
+
if not releases:
|
|
3426
|
+
releases = list(cls.objects.all())
|
|
3388
3427
|
if not releases:
|
|
3389
3428
|
return None
|
|
3390
3429
|
return max(releases, key=lambda r: Version(r.version))
|
|
@@ -3572,6 +3611,8 @@ class Todo(Entity):
|
|
|
3572
3611
|
max_length=200, blank=True, default="", validators=[validate_relative_url]
|
|
3573
3612
|
)
|
|
3574
3613
|
request_details = models.TextField(blank=True, default="")
|
|
3614
|
+
generated_for_version = models.CharField(max_length=20, blank=True, default="")
|
|
3615
|
+
generated_for_revision = models.CharField(max_length=40, blank=True, default="")
|
|
3575
3616
|
done_on = models.DateTimeField(null=True, blank=True)
|
|
3576
3617
|
on_done_condition = ConditionTextField(blank=True, default="")
|
|
3577
3618
|
|
core/release.py
CHANGED
|
@@ -303,6 +303,47 @@ def _git_remote_url(remote: str = "origin") -> Optional[str]:
|
|
|
303
303
|
return (proc.stdout or "").strip() or None
|
|
304
304
|
|
|
305
305
|
|
|
306
|
+
def _git_tag_commit(tag_name: str) -> Optional[str]:
|
|
307
|
+
"""Return the commit referenced by ``tag_name`` in the local repository."""
|
|
308
|
+
|
|
309
|
+
for ref in (f"{tag_name}^{{}}", tag_name):
|
|
310
|
+
proc = subprocess.run(
|
|
311
|
+
["git", "rev-parse", ref],
|
|
312
|
+
capture_output=True,
|
|
313
|
+
text=True,
|
|
314
|
+
check=False,
|
|
315
|
+
)
|
|
316
|
+
if proc.returncode == 0:
|
|
317
|
+
commit = (proc.stdout or "").strip()
|
|
318
|
+
if commit:
|
|
319
|
+
return commit
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _git_remote_tag_commit(remote: str, tag_name: str) -> Optional[str]:
|
|
324
|
+
"""Return the commit referenced by ``tag_name`` on ``remote`` if it exists."""
|
|
325
|
+
|
|
326
|
+
proc = subprocess.run(
|
|
327
|
+
["git", "ls-remote", "--tags", remote, tag_name],
|
|
328
|
+
capture_output=True,
|
|
329
|
+
text=True,
|
|
330
|
+
check=False,
|
|
331
|
+
)
|
|
332
|
+
if proc.returncode != 0:
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
commit = None
|
|
336
|
+
for line in (proc.stdout or "").splitlines():
|
|
337
|
+
parts = line.strip().split()
|
|
338
|
+
if len(parts) != 2:
|
|
339
|
+
continue
|
|
340
|
+
sha, ref = parts
|
|
341
|
+
commit = sha
|
|
342
|
+
if ref.endswith("^{}"):
|
|
343
|
+
return sha
|
|
344
|
+
return commit
|
|
345
|
+
|
|
346
|
+
|
|
306
347
|
def _remote_with_credentials(url: str, creds: GitCredentials) -> Optional[str]:
|
|
307
348
|
if not creds.has_auth():
|
|
308
349
|
return None
|
|
@@ -335,6 +376,17 @@ def _push_tag(tag_name: str, package: Package) -> None:
|
|
|
335
376
|
_run(["git", "push", "origin", tag_name])
|
|
336
377
|
return
|
|
337
378
|
except subprocess.CalledProcessError as exc:
|
|
379
|
+
remote_commit = _git_remote_tag_commit("origin", tag_name)
|
|
380
|
+
local_commit = _git_tag_commit(tag_name)
|
|
381
|
+
if remote_commit:
|
|
382
|
+
if local_commit and remote_commit == local_commit:
|
|
383
|
+
# Another process already pushed the tag; treat as success.
|
|
384
|
+
return
|
|
385
|
+
message = (
|
|
386
|
+
"Git rejected tag {tag} because it already exists on the remote. "
|
|
387
|
+
"Delete the remote tag or choose a new version before retrying."
|
|
388
|
+
).format(tag=tag_name)
|
|
389
|
+
raise ReleaseError(message) from exc
|
|
338
390
|
if not _git_authentication_missing(exc):
|
|
339
391
|
raise
|
|
340
392
|
auth_error = exc
|
core/system.py
CHANGED
|
@@ -20,10 +20,18 @@ from django.http import HttpResponseRedirect
|
|
|
20
20
|
from django.urls import path, reverse
|
|
21
21
|
from django.utils import timezone
|
|
22
22
|
from django.utils.formats import date_format
|
|
23
|
+
from django.utils.html import format_html, format_html_join
|
|
23
24
|
from django.utils.translation import gettext_lazy as _, ngettext
|
|
24
25
|
|
|
25
26
|
from core.auto_upgrade import AUTO_UPGRADE_TASK_NAME, AUTO_UPGRADE_TASK_PATH
|
|
26
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
|
|
27
35
|
from utils import revision
|
|
28
36
|
|
|
29
37
|
|
|
@@ -173,6 +181,120 @@ def _regenerate_changelog() -> None:
|
|
|
173
181
|
changelog_path.write_text(content, encoding="utf-8")
|
|
174
182
|
|
|
175
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
|
+
|
|
176
298
|
@dataclass(frozen=True)
|
|
177
299
|
class SystemField:
|
|
178
300
|
"""Metadata describing a single entry on the system admin page."""
|
|
@@ -716,7 +838,14 @@ def _gather_info() -> dict:
|
|
|
716
838
|
info["service"] = service_file.read_text().strip() if service_file.exists() else ""
|
|
717
839
|
|
|
718
840
|
mode_file = lock_dir / "nginx_mode.lck"
|
|
719
|
-
|
|
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"
|
|
720
849
|
info["mode"] = mode
|
|
721
850
|
default_port = 8000 if mode == "public" else 8888
|
|
722
851
|
detected_port: int | None = None
|
|
@@ -882,6 +1011,36 @@ def _system_changelog_report_view(request):
|
|
|
882
1011
|
request,
|
|
883
1012
|
_("Select at least one changelog entry to exclude."),
|
|
884
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))
|
|
885
1044
|
else:
|
|
886
1045
|
try:
|
|
887
1046
|
_regenerate_changelog()
|
|
@@ -927,6 +1086,49 @@ def _system_upgrade_report_view(request):
|
|
|
927
1086
|
return TemplateResponse(request, "admin/system_upgrade_report.html", context)
|
|
928
1087
|
|
|
929
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
|
+
|
|
930
1132
|
def patch_admin_system_view() -> None:
|
|
931
1133
|
"""Add custom admin view for system information."""
|
|
932
1134
|
original_get_urls = admin.site.get_urls
|
|
@@ -945,6 +1147,11 @@ def patch_admin_system_view() -> None:
|
|
|
945
1147
|
admin.site.admin_view(_system_upgrade_report_view),
|
|
946
1148
|
name="system-upgrade-report",
|
|
947
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
|
+
),
|
|
948
1155
|
]
|
|
949
1156
|
return custom + urls
|
|
950
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):
|