arthexis 0.1.16__py3-none-any.whl → 0.1.28__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.16.dist-info → arthexis-0.1.28.dist-info}/METADATA +95 -41
- arthexis-0.1.28.dist-info/RECORD +112 -0
- config/asgi.py +1 -15
- config/middleware.py +47 -1
- config/settings.py +21 -30
- config/settings_helpers.py +176 -1
- config/urls.py +69 -1
- core/admin.py +805 -473
- core/apps.py +6 -8
- core/auto_upgrade.py +19 -4
- core/backends.py +13 -3
- core/celery_utils.py +73 -0
- core/changelog.py +66 -5
- core/environment.py +4 -5
- core/models.py +1825 -218
- core/notifications.py +1 -1
- core/reference_utils.py +10 -11
- core/release.py +55 -7
- core/sigil_builder.py +2 -2
- core/sigil_resolver.py +1 -66
- core/system.py +285 -4
- core/tasks.py +439 -138
- core/test_system_info.py +43 -5
- core/tests.py +516 -18
- core/user_data.py +94 -21
- core/views.py +348 -186
- nodes/admin.py +904 -67
- nodes/apps.py +12 -1
- nodes/feature_checks.py +30 -0
- nodes/models.py +800 -127
- nodes/rfid_sync.py +1 -1
- nodes/tasks.py +98 -3
- nodes/tests.py +1381 -152
- nodes/urls.py +15 -1
- nodes/utils.py +51 -3
- nodes/views.py +1382 -152
- ocpp/admin.py +1970 -152
- ocpp/consumers.py +839 -34
- ocpp/models.py +968 -17
- ocpp/network.py +398 -0
- ocpp/store.py +411 -43
- ocpp/tasks.py +261 -3
- ocpp/test_export_import.py +1 -0
- ocpp/test_rfid.py +194 -6
- ocpp/tests.py +1918 -87
- ocpp/transactions_io.py +9 -1
- ocpp/urls.py +8 -3
- ocpp/views.py +700 -53
- pages/admin.py +262 -30
- pages/apps.py +35 -0
- pages/context_processors.py +28 -21
- pages/defaults.py +1 -1
- pages/forms.py +31 -8
- pages/middleware.py +6 -2
- pages/models.py +86 -2
- pages/module_defaults.py +5 -5
- pages/site_config.py +137 -0
- pages/tests.py +1050 -126
- pages/urls.py +14 -2
- pages/utils.py +70 -0
- pages/views.py +622 -56
- arthexis-0.1.16.dist-info/RECORD +0 -111
- core/workgroup_urls.py +0 -17
- core/workgroup_views.py +0 -94
- {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/WHEEL +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.28.dist-info}/top_level.txt +0 -0
core/notifications.py
CHANGED
|
@@ -39,7 +39,7 @@ class NotificationManager:
|
|
|
39
39
|
self.lock_file.parent.mkdir(parents=True, exist_ok=True)
|
|
40
40
|
# ``plyer`` is only available on Windows and can fail when used in
|
|
41
41
|
# a non-interactive environment (e.g. service or CI).
|
|
42
|
-
# Any failure will
|
|
42
|
+
# Any failure will fall back to logging quietly.
|
|
43
43
|
|
|
44
44
|
def _write_lock_file(self, subject: str, body: str) -> None:
|
|
45
45
|
self.lock_file.write_text(f"{subject}\n{body}\n", encoding="utf-8")
|
core/reference_utils.py
CHANGED
|
@@ -70,17 +70,16 @@ def filter_visible_references(
|
|
|
70
70
|
required_sites = {current_site.pk for current_site in ref.sites.all()}
|
|
71
71
|
|
|
72
72
|
if required_roles or required_features or required_sites:
|
|
73
|
-
allowed =
|
|
74
|
-
if required_roles
|
|
75
|
-
allowed =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
allowed = True
|
|
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)
|
|
84
83
|
|
|
85
84
|
if not allowed:
|
|
86
85
|
continue
|
core/release.py
CHANGED
|
@@ -114,6 +114,21 @@ class ReleaseError(Exception):
|
|
|
114
114
|
pass
|
|
115
115
|
|
|
116
116
|
|
|
117
|
+
class PostPublishWarning(ReleaseError):
|
|
118
|
+
"""Raised when distribution uploads succeed but post-publish tasks need attention."""
|
|
119
|
+
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
message: str,
|
|
123
|
+
*,
|
|
124
|
+
uploaded: Sequence[str],
|
|
125
|
+
followups: Optional[Sequence[str]] = None,
|
|
126
|
+
) -> None:
|
|
127
|
+
super().__init__(message)
|
|
128
|
+
self.uploaded = list(uploaded)
|
|
129
|
+
self.followups = list(followups or [])
|
|
130
|
+
|
|
131
|
+
|
|
117
132
|
class TestsFailed(ReleaseError):
|
|
118
133
|
"""Raised when the test suite fails.
|
|
119
134
|
|
|
@@ -505,11 +520,6 @@ def build(
|
|
|
505
520
|
if not version_path.exists():
|
|
506
521
|
raise ReleaseError("VERSION file not found")
|
|
507
522
|
version = version_path.read_text().strip()
|
|
508
|
-
if bump:
|
|
509
|
-
major, minor, patch = map(int, version.split("."))
|
|
510
|
-
patch += 1
|
|
511
|
-
version = f"{major}.{minor}.{patch}"
|
|
512
|
-
version_path.write_text(version + "\n")
|
|
513
523
|
else:
|
|
514
524
|
# Ensure the VERSION file reflects the provided release version
|
|
515
525
|
if version_path.parent != Path("."):
|
|
@@ -711,8 +721,46 @@ def publish(
|
|
|
711
721
|
uploaded.append(target.name)
|
|
712
722
|
|
|
713
723
|
tag_name = f"v{version}"
|
|
714
|
-
|
|
715
|
-
|
|
724
|
+
try:
|
|
725
|
+
_run(["git", "tag", tag_name])
|
|
726
|
+
except subprocess.CalledProcessError as exc:
|
|
727
|
+
details = _format_subprocess_error(exc)
|
|
728
|
+
if uploaded:
|
|
729
|
+
uploads = ", ".join(uploaded)
|
|
730
|
+
if details:
|
|
731
|
+
message = (
|
|
732
|
+
f"Upload to {uploads} completed, but creating git tag {tag_name} failed: {details}"
|
|
733
|
+
)
|
|
734
|
+
else:
|
|
735
|
+
message = (
|
|
736
|
+
f"Upload to {uploads} completed, but creating git tag {tag_name} failed."
|
|
737
|
+
)
|
|
738
|
+
followups = [f"Create and push git tag {tag_name} manually once the repository is ready."]
|
|
739
|
+
raise PostPublishWarning(
|
|
740
|
+
message,
|
|
741
|
+
uploaded=uploaded,
|
|
742
|
+
followups=followups,
|
|
743
|
+
) from exc
|
|
744
|
+
raise ReleaseError(
|
|
745
|
+
f"Failed to create git tag {tag_name}: {details or exc}"
|
|
746
|
+
) from exc
|
|
747
|
+
|
|
748
|
+
try:
|
|
749
|
+
_push_tag(tag_name, package)
|
|
750
|
+
except ReleaseError as exc:
|
|
751
|
+
if uploaded:
|
|
752
|
+
uploads = ", ".join(uploaded)
|
|
753
|
+
message = f"Upload to {uploads} completed, but {exc}"
|
|
754
|
+
followups = [
|
|
755
|
+
f"Push git tag {tag_name} to origin after resolving the reported issue."
|
|
756
|
+
]
|
|
757
|
+
warning = PostPublishWarning(
|
|
758
|
+
message,
|
|
759
|
+
uploaded=uploaded,
|
|
760
|
+
followups=followups,
|
|
761
|
+
)
|
|
762
|
+
raise warning from exc
|
|
763
|
+
raise
|
|
716
764
|
return uploaded
|
|
717
765
|
|
|
718
766
|
|
core/sigil_builder.py
CHANGED
|
@@ -40,12 +40,12 @@ def _sigil_builder_view(request):
|
|
|
40
40
|
{
|
|
41
41
|
"prefix": "ENV",
|
|
42
42
|
"url": reverse("admin:environment"),
|
|
43
|
-
"label": _("
|
|
43
|
+
"label": _("Environment"),
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
46
|
"prefix": "CONF",
|
|
47
47
|
"url": reverse("admin:config"),
|
|
48
|
-
"label": _("Config"),
|
|
48
|
+
"label": _("Django Config"),
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
"prefix": "SYS",
|
core/sigil_resolver.py
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
-
import shutil
|
|
4
|
-
import subprocess
|
|
5
|
-
from functools import lru_cache
|
|
6
3
|
from typing import Optional
|
|
7
4
|
|
|
8
5
|
from django.apps import apps
|
|
@@ -16,15 +13,6 @@ from .system import get_system_sigil_values, resolve_system_namespace_value
|
|
|
16
13
|
logger = logging.getLogger("core.entity")
|
|
17
14
|
|
|
18
15
|
|
|
19
|
-
def _is_wizard_mode() -> bool:
|
|
20
|
-
"""Return ``True`` when the application is running in wizard mode."""
|
|
21
|
-
|
|
22
|
-
flag = getattr(settings, "WIZARD_MODE", False)
|
|
23
|
-
if isinstance(flag, str):
|
|
24
|
-
return flag.lower() in {"1", "true", "yes", "on"}
|
|
25
|
-
return bool(flag)
|
|
26
|
-
|
|
27
|
-
|
|
28
16
|
def _first_instance(model: type[models.Model]) -> Optional[models.Model]:
|
|
29
17
|
qs = model.objects
|
|
30
18
|
ordering = list(getattr(model._meta, "ordering", []))
|
|
@@ -35,58 +23,8 @@ def _first_instance(model: type[models.Model]) -> Optional[models.Model]:
|
|
|
35
23
|
return qs.first()
|
|
36
24
|
|
|
37
25
|
|
|
38
|
-
@lru_cache(maxsize=1)
|
|
39
|
-
def _find_gway_command() -> Optional[str]:
|
|
40
|
-
path = shutil.which("gway")
|
|
41
|
-
if path:
|
|
42
|
-
return path
|
|
43
|
-
for candidate in ("~/.local/bin/gway", "/usr/local/bin/gway"):
|
|
44
|
-
expanded = os.path.expanduser(candidate)
|
|
45
|
-
if os.path.isfile(expanded) and os.access(expanded, os.X_OK):
|
|
46
|
-
return expanded
|
|
47
|
-
return None
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _resolve_with_gway(sigil: str) -> Optional[str]:
|
|
51
|
-
command = _find_gway_command()
|
|
52
|
-
if not command:
|
|
53
|
-
return None
|
|
54
|
-
timeout = 60 if _is_wizard_mode() else 1
|
|
55
|
-
try:
|
|
56
|
-
result = subprocess.run(
|
|
57
|
-
[command, "-e", sigil],
|
|
58
|
-
check=False,
|
|
59
|
-
stdout=subprocess.PIPE,
|
|
60
|
-
stderr=subprocess.PIPE,
|
|
61
|
-
text=True,
|
|
62
|
-
timeout=timeout,
|
|
63
|
-
)
|
|
64
|
-
except subprocess.TimeoutExpired:
|
|
65
|
-
logger.warning(
|
|
66
|
-
"gway timed out after %s seconds while resolving sigil %s",
|
|
67
|
-
timeout,
|
|
68
|
-
sigil,
|
|
69
|
-
)
|
|
70
|
-
return None
|
|
71
|
-
except Exception:
|
|
72
|
-
logger.exception("Failed executing gway for sigil %s", sigil)
|
|
73
|
-
return None
|
|
74
|
-
if result.returncode != 0:
|
|
75
|
-
logger.warning(
|
|
76
|
-
"gway exited with status %s while resolving sigil %s",
|
|
77
|
-
result.returncode,
|
|
78
|
-
sigil,
|
|
79
|
-
)
|
|
80
|
-
return None
|
|
81
|
-
return result.stdout.strip()
|
|
82
|
-
|
|
83
|
-
|
|
84
26
|
def _failed_resolution(token: str) -> str:
|
|
85
|
-
|
|
86
|
-
resolved = _resolve_with_gway(sigil)
|
|
87
|
-
if resolved is not None:
|
|
88
|
-
return resolved
|
|
89
|
-
return sigil
|
|
27
|
+
return f"[{token}]"
|
|
90
28
|
|
|
91
29
|
|
|
92
30
|
def _resolve_token(token: str, current: Optional[models.Model] = None) -> str:
|
|
@@ -197,9 +135,6 @@ def _resolve_token(token: str, current: Optional[models.Model] = None) -> str:
|
|
|
197
135
|
value = getattr(settings, candidate, sentinel)
|
|
198
136
|
if value is not sentinel:
|
|
199
137
|
return str(value)
|
|
200
|
-
fallback = _resolve_with_gway(f"[{original_token}]")
|
|
201
|
-
if fallback is not None:
|
|
202
|
-
return fallback
|
|
203
138
|
return ""
|
|
204
139
|
if root.prefix.upper() == "SYS":
|
|
205
140
|
values = get_system_sigil_values()
|
core/system.py
CHANGED
|
@@ -4,6 +4,7 @@ from collections import deque
|
|
|
4
4
|
from contextlib import closing
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from datetime import datetime
|
|
7
|
+
from functools import lru_cache
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
import json
|
|
9
10
|
import re
|
|
@@ -12,9 +13,12 @@ import subprocess
|
|
|
12
13
|
import shutil
|
|
13
14
|
import logging
|
|
14
15
|
from typing import Callable, Iterable, Optional
|
|
16
|
+
from urllib.parse import urlparse
|
|
15
17
|
|
|
18
|
+
from django import forms
|
|
16
19
|
from django.conf import settings
|
|
17
20
|
from django.contrib import admin, messages
|
|
21
|
+
from django.forms import modelformset_factory
|
|
18
22
|
from django.template.response import TemplateResponse
|
|
19
23
|
from django.http import HttpResponseRedirect
|
|
20
24
|
from django.urls import path, reverse
|
|
@@ -32,6 +36,7 @@ from core.release import (
|
|
|
32
36
|
_remote_with_credentials,
|
|
33
37
|
)
|
|
34
38
|
from core.tasks import check_github_updates
|
|
39
|
+
from core.models import Todo
|
|
35
40
|
from utils import revision
|
|
36
41
|
|
|
37
42
|
|
|
@@ -43,6 +48,69 @@ AUTO_UPGRADE_LOG_NAME = "auto-upgrade.log"
|
|
|
43
48
|
logger = logging.getLogger(__name__)
|
|
44
49
|
|
|
45
50
|
|
|
51
|
+
def _github_repo_path(remote_url: str | None) -> str:
|
|
52
|
+
"""Return the ``owner/repo`` path for a GitHub *remote_url* if possible."""
|
|
53
|
+
|
|
54
|
+
if not remote_url:
|
|
55
|
+
return ""
|
|
56
|
+
|
|
57
|
+
normalized = remote_url.strip()
|
|
58
|
+
if not normalized:
|
|
59
|
+
return ""
|
|
60
|
+
|
|
61
|
+
path = ""
|
|
62
|
+
if normalized.startswith("git@"):
|
|
63
|
+
host, _, remainder = normalized.partition(":")
|
|
64
|
+
if "github.com" not in host.lower():
|
|
65
|
+
return ""
|
|
66
|
+
path = remainder
|
|
67
|
+
else:
|
|
68
|
+
parsed = urlparse(normalized)
|
|
69
|
+
if "github.com" not in parsed.netloc.lower():
|
|
70
|
+
return ""
|
|
71
|
+
path = parsed.path
|
|
72
|
+
|
|
73
|
+
path = path.strip("/")
|
|
74
|
+
if path.endswith(".git"):
|
|
75
|
+
path = path[: -len(".git")]
|
|
76
|
+
|
|
77
|
+
if not path:
|
|
78
|
+
return ""
|
|
79
|
+
|
|
80
|
+
segments = [segment for segment in path.split("/") if segment]
|
|
81
|
+
if len(segments) < 2:
|
|
82
|
+
return ""
|
|
83
|
+
|
|
84
|
+
owner, repo = segments[-2], segments[-1]
|
|
85
|
+
return f"{owner}/{repo}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@lru_cache()
|
|
89
|
+
def _github_commit_url_base() -> str:
|
|
90
|
+
"""Return the GitHub commit URL template for the configured repository."""
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
remote_url = _git_remote_url()
|
|
94
|
+
except FileNotFoundError: # pragma: no cover - depends on environment setup
|
|
95
|
+
logger.debug("Skipping GitHub commit URL generation; git executable not found")
|
|
96
|
+
remote_url = None
|
|
97
|
+
|
|
98
|
+
repo_path = _github_repo_path(remote_url)
|
|
99
|
+
if not repo_path:
|
|
100
|
+
return ""
|
|
101
|
+
return f"https://github.com/{repo_path}/commit/{{sha}}"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _github_commit_url(sha: str) -> str:
|
|
105
|
+
"""Return the GitHub commit URL for *sha* when available."""
|
|
106
|
+
|
|
107
|
+
base = _github_commit_url_base()
|
|
108
|
+
clean_sha = (sha or "").strip()
|
|
109
|
+
if not base or not clean_sha:
|
|
110
|
+
return ""
|
|
111
|
+
return base.replace("{sha}", clean_sha)
|
|
112
|
+
|
|
113
|
+
|
|
46
114
|
def _auto_upgrade_mode_file(base_dir: Path) -> Path:
|
|
47
115
|
return base_dir / "locks" / AUTO_UPGRADE_LOCK_NAME
|
|
48
116
|
|
|
@@ -93,11 +161,77 @@ def _open_changelog_entries() -> list[dict[str, str]]:
|
|
|
93
161
|
parts = trimmed.split(" ", 1)
|
|
94
162
|
sha = parts[0]
|
|
95
163
|
message = parts[1] if len(parts) > 1 else ""
|
|
96
|
-
entries.append({"sha": sha, "message": message})
|
|
164
|
+
entries.append({"sha": sha, "message": message, "url": _github_commit_url(sha)})
|
|
97
165
|
|
|
98
166
|
return entries
|
|
99
167
|
|
|
100
168
|
|
|
169
|
+
def _latest_release_changelog() -> dict[str, object]:
|
|
170
|
+
"""Return the most recent tagged release entries for display."""
|
|
171
|
+
|
|
172
|
+
changelog_path = Path("CHANGELOG.rst")
|
|
173
|
+
try:
|
|
174
|
+
text = changelog_path.read_text(encoding="utf-8")
|
|
175
|
+
except (FileNotFoundError, OSError):
|
|
176
|
+
return {"title": "", "entries": []}
|
|
177
|
+
|
|
178
|
+
lines = text.splitlines()
|
|
179
|
+
state = "before"
|
|
180
|
+
release_title = ""
|
|
181
|
+
entries: list[dict[str, str]] = []
|
|
182
|
+
|
|
183
|
+
for raw_line in lines:
|
|
184
|
+
stripped = raw_line.strip()
|
|
185
|
+
|
|
186
|
+
if state == "before":
|
|
187
|
+
if stripped == "Unreleased":
|
|
188
|
+
state = "unreleased-heading"
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
if state == "unreleased-heading":
|
|
192
|
+
if set(stripped) == {"-"}:
|
|
193
|
+
state = "unreleased-body"
|
|
194
|
+
else:
|
|
195
|
+
state = "unreleased-body"
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
if state == "unreleased-body":
|
|
199
|
+
if not stripped:
|
|
200
|
+
state = "after-unreleased"
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
if state == "after-unreleased":
|
|
204
|
+
if not stripped:
|
|
205
|
+
continue
|
|
206
|
+
release_title = stripped
|
|
207
|
+
state = "release-heading"
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
if state == "release-heading":
|
|
211
|
+
if set(stripped) == {"-"}:
|
|
212
|
+
state = "release-body"
|
|
213
|
+
else:
|
|
214
|
+
state = "release-body"
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
if state == "release-body":
|
|
218
|
+
if not stripped:
|
|
219
|
+
if entries:
|
|
220
|
+
break
|
|
221
|
+
continue
|
|
222
|
+
if not stripped.startswith("- "):
|
|
223
|
+
break
|
|
224
|
+
trimmed = stripped[2:].strip()
|
|
225
|
+
if not trimmed:
|
|
226
|
+
continue
|
|
227
|
+
parts = trimmed.split(" ", 1)
|
|
228
|
+
sha = parts[0]
|
|
229
|
+
message = parts[1] if len(parts) > 1 else ""
|
|
230
|
+
entries.append({"sha": sha, "message": message, "url": _github_commit_url(sha)})
|
|
231
|
+
|
|
232
|
+
return {"title": release_title, "entries": entries}
|
|
233
|
+
|
|
234
|
+
|
|
101
235
|
def _exclude_changelog_entries(shas: Iterable[str]) -> int:
|
|
102
236
|
"""Remove entries matching ``shas`` from the changelog.
|
|
103
237
|
|
|
@@ -171,7 +305,7 @@ def _regenerate_changelog() -> None:
|
|
|
171
305
|
previous_text = (
|
|
172
306
|
changelog_path.read_text(encoding="utf-8") if changelog_path.exists() else None
|
|
173
307
|
)
|
|
174
|
-
range_spec = changelog_utils.determine_range_spec()
|
|
308
|
+
range_spec = changelog_utils.determine_range_spec(previous_text=previous_text)
|
|
175
309
|
sections = changelog_utils.collect_sections(
|
|
176
310
|
range_spec=range_spec, previous_text=previous_text
|
|
177
311
|
)
|
|
@@ -769,6 +903,21 @@ def _parse_runserver_port(command_line: str) -> int | None:
|
|
|
769
903
|
return None
|
|
770
904
|
|
|
771
905
|
|
|
906
|
+
def _configured_backend_port(base_dir: Path) -> int:
|
|
907
|
+
lock_file = base_dir / "locks" / "backend_port.lck"
|
|
908
|
+
try:
|
|
909
|
+
raw = lock_file.read_text().strip()
|
|
910
|
+
except OSError:
|
|
911
|
+
return 8888
|
|
912
|
+
try:
|
|
913
|
+
value = int(raw)
|
|
914
|
+
except (TypeError, ValueError):
|
|
915
|
+
return 8888
|
|
916
|
+
if 1 <= value <= 65535:
|
|
917
|
+
return value
|
|
918
|
+
return 8888
|
|
919
|
+
|
|
920
|
+
|
|
772
921
|
def _detect_runserver_process() -> tuple[bool, int | None]:
|
|
773
922
|
"""Return whether the dev server is running and the port if available."""
|
|
774
923
|
|
|
@@ -798,7 +947,7 @@ def _detect_runserver_process() -> tuple[bool, int | None]:
|
|
|
798
947
|
break
|
|
799
948
|
|
|
800
949
|
if port is None:
|
|
801
|
-
port =
|
|
950
|
+
port = _configured_backend_port(Path(settings.BASE_DIR))
|
|
802
951
|
|
|
803
952
|
return True, port
|
|
804
953
|
|
|
@@ -847,7 +996,7 @@ def _gather_info() -> dict:
|
|
|
847
996
|
raw_mode = ""
|
|
848
997
|
mode = raw_mode.lower() or "internal"
|
|
849
998
|
info["mode"] = mode
|
|
850
|
-
default_port =
|
|
999
|
+
default_port = _configured_backend_port(base_dir)
|
|
851
1000
|
detected_port: int | None = None
|
|
852
1001
|
|
|
853
1002
|
screen_file = lock_dir / "screen_mode.lck"
|
|
@@ -1070,6 +1219,7 @@ def _system_changelog_report_view(request):
|
|
|
1070
1219
|
{
|
|
1071
1220
|
"title": _("Changelog Report"),
|
|
1072
1221
|
"open_changelog_entries": _open_changelog_entries(),
|
|
1222
|
+
"latest_release_changelog": _latest_release_changelog(),
|
|
1073
1223
|
}
|
|
1074
1224
|
)
|
|
1075
1225
|
return TemplateResponse(request, "admin/system_changelog_report.html", context)
|
|
@@ -1086,6 +1236,132 @@ def _system_upgrade_report_view(request):
|
|
|
1086
1236
|
return TemplateResponse(request, "admin/system_upgrade_report.html", context)
|
|
1087
1237
|
|
|
1088
1238
|
|
|
1239
|
+
class PendingTodoForm(forms.ModelForm):
|
|
1240
|
+
mark_done = forms.BooleanField(required=False, label=_("Approve"))
|
|
1241
|
+
|
|
1242
|
+
class Meta:
|
|
1243
|
+
model = Todo
|
|
1244
|
+
fields = [
|
|
1245
|
+
"request",
|
|
1246
|
+
"request_details",
|
|
1247
|
+
"url",
|
|
1248
|
+
"generated_for_version",
|
|
1249
|
+
"generated_for_revision",
|
|
1250
|
+
"on_done_condition",
|
|
1251
|
+
]
|
|
1252
|
+
widgets = {
|
|
1253
|
+
"request_details": forms.Textarea(attrs={"rows": 3}),
|
|
1254
|
+
"on_done_condition": forms.Textarea(attrs={"rows": 2}),
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
def __init__(self, *args, **kwargs):
|
|
1258
|
+
super().__init__(*args, **kwargs)
|
|
1259
|
+
for name in [
|
|
1260
|
+
"request",
|
|
1261
|
+
"url",
|
|
1262
|
+
"generated_for_version",
|
|
1263
|
+
"generated_for_revision",
|
|
1264
|
+
]:
|
|
1265
|
+
self.fields[name].widget.attrs.setdefault("class", "vTextField")
|
|
1266
|
+
for name in ["request_details", "on_done_condition"]:
|
|
1267
|
+
self.fields[name].widget.attrs.setdefault("class", "vLargeTextField")
|
|
1268
|
+
|
|
1269
|
+
mark_done_widget = self.fields["mark_done"].widget
|
|
1270
|
+
existing_classes = mark_done_widget.attrs.get("class", "").split()
|
|
1271
|
+
if "approve-checkbox" not in existing_classes:
|
|
1272
|
+
existing_classes.append("approve-checkbox")
|
|
1273
|
+
mark_done_widget.attrs["class"] = " ".join(
|
|
1274
|
+
class_name for class_name in existing_classes if class_name
|
|
1275
|
+
)
|
|
1276
|
+
|
|
1277
|
+
|
|
1278
|
+
PendingTodoFormSet = modelformset_factory(Todo, form=PendingTodoForm, extra=0)
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
def _system_pending_todos_report_view(request):
|
|
1282
|
+
queryset = (
|
|
1283
|
+
Todo.objects.filter(is_deleted=False, done_on__isnull=True)
|
|
1284
|
+
.order_by("request")
|
|
1285
|
+
)
|
|
1286
|
+
formset = PendingTodoFormSet(
|
|
1287
|
+
request.POST or None,
|
|
1288
|
+
queryset=queryset,
|
|
1289
|
+
prefix="todos",
|
|
1290
|
+
)
|
|
1291
|
+
|
|
1292
|
+
if request.method == "POST":
|
|
1293
|
+
if formset.is_valid():
|
|
1294
|
+
approved_count = 0
|
|
1295
|
+
edited_count = 0
|
|
1296
|
+
for form in formset.forms:
|
|
1297
|
+
mark_done = form.cleaned_data.get("mark_done")
|
|
1298
|
+
todo = form.save(commit=False)
|
|
1299
|
+
has_changes = form.has_changed()
|
|
1300
|
+
if mark_done and todo.done_on is None:
|
|
1301
|
+
todo.done_on = timezone.now()
|
|
1302
|
+
todo.populate_done_metadata(request.user)
|
|
1303
|
+
approved_count += 1
|
|
1304
|
+
has_changes = True
|
|
1305
|
+
if has_changes:
|
|
1306
|
+
todo.save()
|
|
1307
|
+
if form.has_changed():
|
|
1308
|
+
edited_count += 1
|
|
1309
|
+
if has_changes and form.has_changed():
|
|
1310
|
+
form.save_m2m()
|
|
1311
|
+
|
|
1312
|
+
if approved_count or edited_count:
|
|
1313
|
+
message_parts: list[str] = []
|
|
1314
|
+
if edited_count:
|
|
1315
|
+
message_parts.append(
|
|
1316
|
+
ngettext(
|
|
1317
|
+
"%(count)d TODO updated.",
|
|
1318
|
+
"%(count)d TODOs updated.",
|
|
1319
|
+
edited_count,
|
|
1320
|
+
)
|
|
1321
|
+
% {"count": edited_count}
|
|
1322
|
+
)
|
|
1323
|
+
if approved_count:
|
|
1324
|
+
message_parts.append(
|
|
1325
|
+
ngettext(
|
|
1326
|
+
"%(count)d TODO approved.",
|
|
1327
|
+
"%(count)d TODOs approved.",
|
|
1328
|
+
approved_count,
|
|
1329
|
+
)
|
|
1330
|
+
% {"count": approved_count}
|
|
1331
|
+
)
|
|
1332
|
+
messages.success(request, " ".join(message_parts))
|
|
1333
|
+
else:
|
|
1334
|
+
messages.info(
|
|
1335
|
+
request,
|
|
1336
|
+
_("No changes were applied to the pending TODOs."),
|
|
1337
|
+
)
|
|
1338
|
+
return HttpResponseRedirect(reverse("admin:system-pending-todos-report"))
|
|
1339
|
+
else:
|
|
1340
|
+
messages.error(request, _("Please correct the errors below."))
|
|
1341
|
+
|
|
1342
|
+
rows = [
|
|
1343
|
+
{
|
|
1344
|
+
"form": form,
|
|
1345
|
+
"todo": form.instance,
|
|
1346
|
+
}
|
|
1347
|
+
for form in formset.forms
|
|
1348
|
+
]
|
|
1349
|
+
|
|
1350
|
+
context = admin.site.each_context(request)
|
|
1351
|
+
context.update(
|
|
1352
|
+
{
|
|
1353
|
+
"title": _("Pending TODOs Report"),
|
|
1354
|
+
"formset": formset,
|
|
1355
|
+
"rows": rows,
|
|
1356
|
+
}
|
|
1357
|
+
)
|
|
1358
|
+
return TemplateResponse(
|
|
1359
|
+
request,
|
|
1360
|
+
"admin/system_pending_todos_report.html",
|
|
1361
|
+
context,
|
|
1362
|
+
)
|
|
1363
|
+
|
|
1364
|
+
|
|
1089
1365
|
def _trigger_upgrade_check() -> bool:
|
|
1090
1366
|
"""Return ``True`` when the upgrade check was queued asynchronously."""
|
|
1091
1367
|
|
|
@@ -1142,6 +1418,11 @@ def patch_admin_system_view() -> None:
|
|
|
1142
1418
|
admin.site.admin_view(_system_changelog_report_view),
|
|
1143
1419
|
name="system-changelog-report",
|
|
1144
1420
|
),
|
|
1421
|
+
path(
|
|
1422
|
+
"system/pending-todos-report/",
|
|
1423
|
+
admin.site.admin_view(_system_pending_todos_report_view),
|
|
1424
|
+
name="system-pending-todos-report",
|
|
1425
|
+
),
|
|
1145
1426
|
path(
|
|
1146
1427
|
"system/upgrade-report/",
|
|
1147
1428
|
admin.site.admin_view(_system_upgrade_report_view),
|