arthexis 0.1.10__py3-none-any.whl → 0.1.12__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.10.dist-info → arthexis-0.1.12.dist-info}/METADATA +36 -26
- arthexis-0.1.12.dist-info/RECORD +102 -0
- config/context_processors.py +1 -0
- config/settings.py +31 -5
- config/urls.py +5 -4
- core/admin.py +430 -90
- core/apps.py +48 -2
- core/backends.py +38 -0
- core/environment.py +23 -5
- core/mailer.py +3 -1
- core/models.py +303 -31
- core/reference_utils.py +20 -9
- core/release.py +4 -0
- core/sigil_builder.py +7 -2
- core/sigil_resolver.py +35 -4
- core/system.py +250 -1
- core/tasks.py +92 -40
- core/temp_passwords.py +181 -0
- core/test_system_info.py +62 -2
- core/tests.py +169 -3
- core/user_data.py +51 -8
- core/views.py +371 -20
- nodes/admin.py +453 -8
- nodes/backends.py +21 -6
- nodes/dns.py +203 -0
- nodes/feature_checks.py +133 -0
- nodes/models.py +374 -31
- nodes/reports.py +411 -0
- nodes/tests.py +677 -38
- nodes/utils.py +32 -0
- nodes/views.py +14 -0
- ocpp/admin.py +278 -15
- ocpp/consumers.py +517 -16
- ocpp/evcs_discovery.py +158 -0
- ocpp/models.py +237 -4
- ocpp/reference_utils.py +42 -0
- ocpp/simulator.py +321 -22
- ocpp/store.py +110 -2
- ocpp/test_rfid.py +169 -7
- ocpp/tests.py +819 -6
- ocpp/transactions_io.py +17 -3
- ocpp/views.py +233 -19
- pages/admin.py +144 -4
- pages/context_processors.py +21 -7
- pages/defaults.py +13 -0
- pages/forms.py +38 -0
- pages/models.py +189 -15
- pages/tests.py +281 -8
- pages/urls.py +4 -0
- pages/views.py +137 -21
- arthexis-0.1.10.dist-info/RECORD +0 -95
- {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/WHEEL +0 -0
- {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.10.dist-info → arthexis-0.1.12.dist-info}/top_level.txt +0 -0
core/views.py
CHANGED
|
@@ -11,13 +11,15 @@ from django.contrib.sites.models import Site
|
|
|
11
11
|
from django.http import Http404, JsonResponse
|
|
12
12
|
from django.shortcuts import get_object_or_404, redirect, render, resolve_url
|
|
13
13
|
from django.utils import timezone
|
|
14
|
+
from django.utils.text import slugify
|
|
14
15
|
from django.utils.translation import gettext as _
|
|
15
16
|
from django.urls import NoReverseMatch, reverse
|
|
16
17
|
from django.views.decorators.csrf import csrf_exempt
|
|
17
18
|
from django.views.decorators.http import require_GET, require_POST
|
|
18
19
|
from django.utils.http import url_has_allowed_host_and_scheme
|
|
19
20
|
from pathlib import Path
|
|
20
|
-
from urllib.parse import urlsplit, urlunsplit
|
|
21
|
+
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
|
22
|
+
import errno
|
|
21
23
|
import subprocess
|
|
22
24
|
|
|
23
25
|
from utils import revision
|
|
@@ -38,7 +40,7 @@ def odoo_products(request):
|
|
|
38
40
|
products = profile.execute(
|
|
39
41
|
"product.product",
|
|
40
42
|
"search_read",
|
|
41
|
-
[],
|
|
43
|
+
[[]],
|
|
42
44
|
{"fields": ["name"], "limit": 50},
|
|
43
45
|
)
|
|
44
46
|
except Exception:
|
|
@@ -81,6 +83,126 @@ def _clean_repo() -> None:
|
|
|
81
83
|
subprocess.run(["git", "clean", "-fd"], check=False)
|
|
82
84
|
|
|
83
85
|
|
|
86
|
+
def _format_path(path: Path) -> str:
|
|
87
|
+
try:
|
|
88
|
+
return str(path.resolve().relative_to(Path.cwd()))
|
|
89
|
+
except ValueError:
|
|
90
|
+
return str(path)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _next_patch_version(version: str) -> str:
|
|
94
|
+
from packaging.version import InvalidVersion, Version
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
parsed = Version(version)
|
|
98
|
+
except InvalidVersion:
|
|
99
|
+
parts = version.split(".")
|
|
100
|
+
for index in range(len(parts) - 1, -1, -1):
|
|
101
|
+
segment = parts[index]
|
|
102
|
+
if segment.isdigit():
|
|
103
|
+
parts[index] = str(int(segment) + 1)
|
|
104
|
+
return ".".join(parts)
|
|
105
|
+
return version
|
|
106
|
+
return f"{parsed.major}.{parsed.minor}.{parsed.micro + 1}"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _write_todo_fixture(todo: Todo) -> Path:
|
|
110
|
+
safe_request = todo.request.replace(".", " ")
|
|
111
|
+
slug = slugify(safe_request).replace("-", "_")
|
|
112
|
+
if not slug:
|
|
113
|
+
slug = "todo"
|
|
114
|
+
path = TODO_FIXTURE_DIR / f"todos__{slug}.json"
|
|
115
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
data = [
|
|
117
|
+
{
|
|
118
|
+
"model": "core.todo",
|
|
119
|
+
"fields": {
|
|
120
|
+
"request": todo.request,
|
|
121
|
+
"url": todo.url,
|
|
122
|
+
"request_details": todo.request_details,
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
127
|
+
return path
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _should_use_python_changelog(exc: OSError) -> bool:
|
|
131
|
+
winerror = getattr(exc, "winerror", None)
|
|
132
|
+
if winerror in {193}:
|
|
133
|
+
return True
|
|
134
|
+
return exc.errno in {errno.ENOEXEC, errno.EACCES, errno.ENOENT}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _generate_changelog_with_python(log_path: Path) -> None:
|
|
138
|
+
_append_log(log_path, "Falling back to Python changelog generator")
|
|
139
|
+
describe = subprocess.run(
|
|
140
|
+
["git", "describe", "--tags", "--abbrev=0"],
|
|
141
|
+
capture_output=True,
|
|
142
|
+
text=True,
|
|
143
|
+
check=False,
|
|
144
|
+
)
|
|
145
|
+
start_tag = describe.stdout.strip() if describe.returncode == 0 else ""
|
|
146
|
+
range_spec = f"{start_tag}..HEAD" if start_tag else "HEAD"
|
|
147
|
+
log_proc = subprocess.run(
|
|
148
|
+
["git", "log", range_spec, "--no-merges", "--pretty=format:- %h %s"],
|
|
149
|
+
capture_output=True,
|
|
150
|
+
text=True,
|
|
151
|
+
check=True,
|
|
152
|
+
)
|
|
153
|
+
entries = [line for line in log_proc.stdout.splitlines() if line]
|
|
154
|
+
changelog_path = Path("CHANGELOG.rst")
|
|
155
|
+
previous_lines: list[str] = []
|
|
156
|
+
if changelog_path.exists():
|
|
157
|
+
previous_lines = changelog_path.read_text(encoding="utf-8").splitlines()
|
|
158
|
+
if len(previous_lines) > 6:
|
|
159
|
+
previous_lines = previous_lines[6:]
|
|
160
|
+
else:
|
|
161
|
+
previous_lines = []
|
|
162
|
+
lines = [
|
|
163
|
+
"Changelog",
|
|
164
|
+
"=========",
|
|
165
|
+
"",
|
|
166
|
+
"Unreleased",
|
|
167
|
+
"----------",
|
|
168
|
+
"",
|
|
169
|
+
]
|
|
170
|
+
if entries:
|
|
171
|
+
lines.extend(entries)
|
|
172
|
+
if previous_lines:
|
|
173
|
+
lines.append("")
|
|
174
|
+
lines.extend(previous_lines)
|
|
175
|
+
content = "\n".join(lines)
|
|
176
|
+
if not content.endswith("\n"):
|
|
177
|
+
content += "\n"
|
|
178
|
+
changelog_path.write_text(content, encoding="utf-8")
|
|
179
|
+
_append_log(log_path, "Regenerated CHANGELOG.rst using Python fallback")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _ensure_release_todo(release) -> tuple[Todo, Path]:
|
|
183
|
+
target_version = _next_patch_version(release.version)
|
|
184
|
+
request = f"Create release {release.package.name} {target_version}"
|
|
185
|
+
try:
|
|
186
|
+
url = reverse("admin:core_packagerelease_changelist")
|
|
187
|
+
except NoReverseMatch:
|
|
188
|
+
url = ""
|
|
189
|
+
todo, _ = Todo.all_objects.update_or_create(
|
|
190
|
+
request__iexact=request,
|
|
191
|
+
defaults={
|
|
192
|
+
"request": request,
|
|
193
|
+
"url": url,
|
|
194
|
+
"request_details": "",
|
|
195
|
+
"is_seed_data": True,
|
|
196
|
+
"is_deleted": False,
|
|
197
|
+
"is_user_data": False,
|
|
198
|
+
"done_on": None,
|
|
199
|
+
"on_done_condition": "",
|
|
200
|
+
},
|
|
201
|
+
)
|
|
202
|
+
fixture_path = _write_todo_fixture(todo)
|
|
203
|
+
return todo, fixture_path
|
|
204
|
+
|
|
205
|
+
|
|
84
206
|
def _sync_release_with_revision(release: PackageRelease) -> tuple[bool, str]:
|
|
85
207
|
"""Ensure ``release`` matches the repository revision and version.
|
|
86
208
|
|
|
@@ -125,7 +247,22 @@ def _sync_release_with_revision(release: PackageRelease) -> tuple[bool, str]:
|
|
|
125
247
|
release.save(update_fields=list(updated_fields))
|
|
126
248
|
PackageRelease.dump_fixture()
|
|
127
249
|
|
|
128
|
-
|
|
250
|
+
package_updated = False
|
|
251
|
+
if release.package_id and not release.package.is_active:
|
|
252
|
+
release.package.is_active = True
|
|
253
|
+
release.package.save(update_fields=["is_active"])
|
|
254
|
+
package_updated = True
|
|
255
|
+
|
|
256
|
+
version_updated = False
|
|
257
|
+
if release.version:
|
|
258
|
+
current = ""
|
|
259
|
+
if version_path.exists():
|
|
260
|
+
current = version_path.read_text(encoding="utf-8").strip()
|
|
261
|
+
if current != release.version:
|
|
262
|
+
version_path.write_text(f"{release.version}\n", encoding="utf-8")
|
|
263
|
+
version_updated = True
|
|
264
|
+
|
|
265
|
+
return bool(updated_fields or version_updated or package_updated), previous_version
|
|
129
266
|
|
|
130
267
|
|
|
131
268
|
def _changelog_notes(version: str) -> str:
|
|
@@ -215,12 +352,12 @@ def _step_check_todos(release, ctx, log_path: Path) -> None:
|
|
|
215
352
|
check=False,
|
|
216
353
|
)
|
|
217
354
|
ctx.pop("todos", None)
|
|
218
|
-
ctx
|
|
355
|
+
ctx["todos_ack"] = True
|
|
219
356
|
|
|
220
357
|
|
|
221
358
|
def _step_check_version(release, ctx, log_path: Path) -> None:
|
|
222
359
|
from . import release as release_utils
|
|
223
|
-
from packaging.version import Version
|
|
360
|
+
from packaging.version import InvalidVersion, Version
|
|
224
361
|
|
|
225
362
|
if not release_utils._git_clean():
|
|
226
363
|
proc = subprocess.run(
|
|
@@ -266,6 +403,7 @@ def _step_check_version(release, ctx, log_path: Path) -> None:
|
|
|
266
403
|
)
|
|
267
404
|
subprocess.run(["git", "add", *fixture_files], check=True)
|
|
268
405
|
subprocess.run(["git", "commit", "-m", "chore: update fixtures"], check=True)
|
|
406
|
+
_append_log(log_path, "Fixture changes committed")
|
|
269
407
|
|
|
270
408
|
version_path = Path("VERSION")
|
|
271
409
|
if version_path.exists():
|
|
@@ -279,32 +417,91 @@ def _step_check_version(release, ctx, log_path: Path) -> None:
|
|
|
279
417
|
if release_utils.network_available():
|
|
280
418
|
try:
|
|
281
419
|
resp = requests.get(f"https://pypi.org/pypi/{release.package.name}/json")
|
|
282
|
-
if resp.ok
|
|
283
|
-
|
|
420
|
+
if resp.ok:
|
|
421
|
+
data = resp.json()
|
|
422
|
+
releases = data.get("releases", {})
|
|
423
|
+
try:
|
|
424
|
+
target_version = Version(release.version)
|
|
425
|
+
except InvalidVersion:
|
|
426
|
+
target_version = None
|
|
427
|
+
|
|
428
|
+
for candidate, files in releases.items():
|
|
429
|
+
same_version = candidate == release.version
|
|
430
|
+
if target_version is not None and not same_version:
|
|
431
|
+
try:
|
|
432
|
+
same_version = Version(candidate) == target_version
|
|
433
|
+
except InvalidVersion:
|
|
434
|
+
same_version = False
|
|
435
|
+
if not same_version:
|
|
436
|
+
continue
|
|
437
|
+
|
|
438
|
+
has_available_files = any(
|
|
439
|
+
isinstance(file_data, dict)
|
|
440
|
+
and not file_data.get("yanked", False)
|
|
441
|
+
for file_data in files or []
|
|
442
|
+
)
|
|
443
|
+
if has_available_files:
|
|
444
|
+
raise Exception(
|
|
445
|
+
f"Version {release.version} already on PyPI"
|
|
446
|
+
)
|
|
284
447
|
except Exception as exc:
|
|
285
448
|
# network errors should be logged but not crash
|
|
286
449
|
if "already on PyPI" in str(exc):
|
|
287
450
|
raise
|
|
288
451
|
_append_log(log_path, f"PyPI check failed: {exc}")
|
|
452
|
+
else:
|
|
453
|
+
_append_log(
|
|
454
|
+
log_path,
|
|
455
|
+
f"Version {release.version} not published on PyPI",
|
|
456
|
+
)
|
|
289
457
|
else:
|
|
290
458
|
_append_log(log_path, "Network unavailable, skipping PyPI check")
|
|
291
459
|
|
|
292
460
|
|
|
293
461
|
def _step_handle_migrations(release, ctx, log_path: Path) -> None:
|
|
294
462
|
_append_log(log_path, "Freeze, squash and approve migrations")
|
|
463
|
+
_append_log(log_path, "Migration review acknowledged (manual step)")
|
|
295
464
|
|
|
296
465
|
|
|
297
466
|
def _step_changelog_docs(release, ctx, log_path: Path) -> None:
|
|
298
467
|
_append_log(log_path, "Compose CHANGELOG and documentation")
|
|
468
|
+
_append_log(log_path, "CHANGELOG and documentation review recorded")
|
|
299
469
|
|
|
300
470
|
|
|
301
471
|
def _step_pre_release_actions(release, ctx, log_path: Path) -> None:
|
|
302
472
|
_append_log(log_path, "Execute pre-release actions")
|
|
473
|
+
try:
|
|
474
|
+
subprocess.run(["scripts/generate-changelog.sh"], check=True)
|
|
475
|
+
except OSError as exc:
|
|
476
|
+
if _should_use_python_changelog(exc):
|
|
477
|
+
_append_log(
|
|
478
|
+
log_path,
|
|
479
|
+
f"scripts/generate-changelog.sh failed: {exc}",
|
|
480
|
+
)
|
|
481
|
+
_generate_changelog_with_python(log_path)
|
|
482
|
+
else: # pragma: no cover - unexpected OSError
|
|
483
|
+
raise
|
|
484
|
+
else:
|
|
485
|
+
_append_log(
|
|
486
|
+
log_path, "Regenerated CHANGELOG.rst using scripts/generate-changelog.sh"
|
|
487
|
+
)
|
|
488
|
+
subprocess.run(["git", "add", "CHANGELOG.rst"], check=True)
|
|
489
|
+
_append_log(log_path, "Staged CHANGELOG.rst for commit")
|
|
303
490
|
version_path = Path("VERSION")
|
|
304
491
|
version_path.write_text(f"{release.version}\n", encoding="utf-8")
|
|
492
|
+
_append_log(log_path, f"Updated VERSION file to {release.version}")
|
|
305
493
|
subprocess.run(["git", "add", "VERSION"], check=True)
|
|
494
|
+
_append_log(log_path, "Staged VERSION for commit")
|
|
306
495
|
diff = subprocess.run(
|
|
307
|
-
[
|
|
496
|
+
[
|
|
497
|
+
"git",
|
|
498
|
+
"diff",
|
|
499
|
+
"--cached",
|
|
500
|
+
"--quiet",
|
|
501
|
+
"--",
|
|
502
|
+
"CHANGELOG.rst",
|
|
503
|
+
"VERSION",
|
|
504
|
+
],
|
|
308
505
|
check=False,
|
|
309
506
|
)
|
|
310
507
|
if diff.returncode != 0:
|
|
@@ -312,13 +509,40 @@ def _step_pre_release_actions(release, ctx, log_path: Path) -> None:
|
|
|
312
509
|
["git", "commit", "-m", f"pre-release commit {release.version}"],
|
|
313
510
|
check=True,
|
|
314
511
|
)
|
|
512
|
+
_append_log(log_path, f"Committed VERSION update for {release.version}")
|
|
315
513
|
else:
|
|
316
|
-
_append_log(
|
|
514
|
+
_append_log(
|
|
515
|
+
log_path, "No changes detected for VERSION or CHANGELOG; skipping commit"
|
|
516
|
+
)
|
|
517
|
+
subprocess.run(["git", "reset", "HEAD", "CHANGELOG.rst"], check=False)
|
|
518
|
+
_append_log(log_path, "Unstaged CHANGELOG.rst")
|
|
317
519
|
subprocess.run(["git", "reset", "HEAD", "VERSION"], check=False)
|
|
520
|
+
_append_log(log_path, "Unstaged VERSION file")
|
|
521
|
+
todo, fixture_path = _ensure_release_todo(release)
|
|
522
|
+
fixture_display = _format_path(fixture_path)
|
|
523
|
+
_append_log(log_path, f"Added TODO: {todo.request}")
|
|
524
|
+
_append_log(log_path, f"Wrote TODO fixture {fixture_display}")
|
|
525
|
+
subprocess.run(["git", "add", str(fixture_path)], check=True)
|
|
526
|
+
_append_log(log_path, f"Staged TODO fixture {fixture_display}")
|
|
527
|
+
fixture_diff = subprocess.run(
|
|
528
|
+
["git", "diff", "--cached", "--quiet", "--", str(fixture_path)],
|
|
529
|
+
check=False,
|
|
530
|
+
)
|
|
531
|
+
if fixture_diff.returncode != 0:
|
|
532
|
+
commit_message = f"chore: add release TODO for {release.package.name}"
|
|
533
|
+
subprocess.run(["git", "commit", "-m", commit_message], check=True)
|
|
534
|
+
_append_log(log_path, f"Committed TODO fixture {fixture_display}")
|
|
535
|
+
else:
|
|
536
|
+
_append_log(
|
|
537
|
+
log_path,
|
|
538
|
+
f"No changes detected for TODO fixture {fixture_display}; skipping commit",
|
|
539
|
+
)
|
|
540
|
+
_append_log(log_path, "Pre-release actions complete")
|
|
318
541
|
|
|
319
542
|
|
|
320
543
|
def _step_run_tests(release, ctx, log_path: Path) -> None:
|
|
321
544
|
_append_log(log_path, "Complete test suite with --all flag")
|
|
545
|
+
_append_log(log_path, "Test suite completion acknowledged")
|
|
322
546
|
|
|
323
547
|
|
|
324
548
|
def _step_promote_build(release, ctx, log_path: Path) -> None:
|
|
@@ -328,15 +552,22 @@ def _step_promote_build(release, ctx, log_path: Path) -> None:
|
|
|
328
552
|
try:
|
|
329
553
|
try:
|
|
330
554
|
subprocess.run(["git", "fetch", "origin", "main"], check=True)
|
|
555
|
+
_append_log(log_path, "Fetched latest changes from origin/main")
|
|
331
556
|
subprocess.run(["git", "rebase", "origin/main"], check=True)
|
|
557
|
+
_append_log(log_path, "Rebased current branch onto origin/main")
|
|
332
558
|
except subprocess.CalledProcessError as exc:
|
|
333
559
|
subprocess.run(["git", "rebase", "--abort"], check=False)
|
|
560
|
+
_append_log(log_path, "Rebase onto origin/main failed; aborted rebase")
|
|
334
561
|
raise Exception("Rebase onto main failed") from exc
|
|
335
562
|
release_utils.promote(
|
|
336
563
|
package=release.to_package(),
|
|
337
564
|
version=release.version,
|
|
338
565
|
creds=release.to_credentials(),
|
|
339
566
|
)
|
|
567
|
+
_append_log(
|
|
568
|
+
log_path,
|
|
569
|
+
f"Generated release artifacts for v{release.version}",
|
|
570
|
+
)
|
|
340
571
|
from glob import glob
|
|
341
572
|
|
|
342
573
|
paths = ["VERSION", *glob("core/fixtures/releases__*.json")]
|
|
@@ -347,6 +578,7 @@ def _step_promote_build(release, ctx, log_path: Path) -> None:
|
|
|
347
578
|
)
|
|
348
579
|
if diff.stdout.strip():
|
|
349
580
|
subprocess.run(["git", "add", *paths], check=True)
|
|
581
|
+
_append_log(log_path, "Staged release metadata updates")
|
|
350
582
|
subprocess.run(
|
|
351
583
|
[
|
|
352
584
|
"git",
|
|
@@ -356,8 +588,14 @@ def _step_promote_build(release, ctx, log_path: Path) -> None:
|
|
|
356
588
|
],
|
|
357
589
|
check=True,
|
|
358
590
|
)
|
|
591
|
+
_append_log(
|
|
592
|
+
log_path,
|
|
593
|
+
f"Committed release metadata for v{release.version}",
|
|
594
|
+
)
|
|
359
595
|
subprocess.run(["git", "push"], check=True)
|
|
596
|
+
_append_log(log_path, "Pushed release changes to origin")
|
|
360
597
|
PackageRelease.dump_fixture()
|
|
598
|
+
_append_log(log_path, "Updated release fixtures")
|
|
361
599
|
except Exception:
|
|
362
600
|
_clean_repo()
|
|
363
601
|
raise
|
|
@@ -415,8 +653,10 @@ def _step_publish(release, ctx, log_path: Path) -> None:
|
|
|
415
653
|
release.pypi_url = (
|
|
416
654
|
f"https://pypi.org/project/{release.package.name}/{release.version}/"
|
|
417
655
|
)
|
|
418
|
-
release.
|
|
656
|
+
release.release_on = timezone.now()
|
|
657
|
+
release.save(update_fields=["pypi_url", "release_on"])
|
|
419
658
|
PackageRelease.dump_fixture()
|
|
659
|
+
_append_log(log_path, f"Recorded PyPI URL: {release.pypi_url}")
|
|
420
660
|
_append_log(log_path, "Upload complete")
|
|
421
661
|
|
|
422
662
|
|
|
@@ -644,6 +884,8 @@ def release_progress(request, pk: int, action: str):
|
|
|
644
884
|
f"{release.package.name}-{previous_version}*.log"
|
|
645
885
|
):
|
|
646
886
|
log_file.unlink()
|
|
887
|
+
if not release.is_current:
|
|
888
|
+
raise Http404("Release is not current")
|
|
647
889
|
|
|
648
890
|
if request.GET.get("restart"):
|
|
649
891
|
count = 0
|
|
@@ -656,7 +898,8 @@ def release_progress(request, pk: int, action: str):
|
|
|
656
898
|
restart_path.write_text(str(count + 1), encoding="utf-8")
|
|
657
899
|
_clean_repo()
|
|
658
900
|
release.pypi_url = ""
|
|
659
|
-
release.
|
|
901
|
+
release.release_on = None
|
|
902
|
+
release.save(update_fields=["pypi_url", "release_on"])
|
|
660
903
|
request.session.pop(session_key, None)
|
|
661
904
|
if lock_path.exists():
|
|
662
905
|
lock_path.unlink()
|
|
@@ -946,20 +1189,121 @@ def release_progress(request, pk: int, action: str):
|
|
|
946
1189
|
return render(request, "core/release_progress.html", context)
|
|
947
1190
|
|
|
948
1191
|
|
|
949
|
-
def
|
|
950
|
-
|
|
1192
|
+
def _dedupe_preserve_order(values):
|
|
1193
|
+
seen = set()
|
|
1194
|
+
result = []
|
|
1195
|
+
for value in values:
|
|
1196
|
+
if value in seen:
|
|
1197
|
+
continue
|
|
1198
|
+
seen.add(value)
|
|
1199
|
+
result.append(value)
|
|
1200
|
+
return result
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
def _parse_todo_auth_directives(query: str):
|
|
1204
|
+
directives = {
|
|
1205
|
+
"require_logout": False,
|
|
1206
|
+
"users": [],
|
|
1207
|
+
"permissions": [],
|
|
1208
|
+
"notes": [],
|
|
1209
|
+
}
|
|
1210
|
+
if not query:
|
|
1211
|
+
return "", directives
|
|
1212
|
+
|
|
1213
|
+
remaining = []
|
|
1214
|
+
for key, value in parse_qsl(query, keep_blank_values=True):
|
|
1215
|
+
if key != "_todo_auth":
|
|
1216
|
+
remaining.append((key, value))
|
|
1217
|
+
continue
|
|
1218
|
+
token = (value or "").strip()
|
|
1219
|
+
if not token:
|
|
1220
|
+
continue
|
|
1221
|
+
kind, _, payload = token.partition(":")
|
|
1222
|
+
kind = kind.strip().lower()
|
|
1223
|
+
payload = payload.strip()
|
|
1224
|
+
if kind in {"logout", "anonymous", "anon"}:
|
|
1225
|
+
directives["require_logout"] = True
|
|
1226
|
+
elif kind in {"user", "username"} and payload:
|
|
1227
|
+
directives["users"].append(payload)
|
|
1228
|
+
elif kind in {"perm", "permission"} and payload:
|
|
1229
|
+
directives["permissions"].append(payload)
|
|
1230
|
+
else:
|
|
1231
|
+
directives["notes"].append(token)
|
|
1232
|
+
|
|
1233
|
+
sanitized_query = urlencode(remaining, doseq=True)
|
|
1234
|
+
return sanitized_query, directives
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
def _todo_iframe_url(request, todo: Todo):
|
|
1238
|
+
"""Return a safe iframe URL and auth context for ``todo``."""
|
|
951
1239
|
|
|
952
1240
|
fallback = reverse("admin:core_todo_change", args=[todo.pk])
|
|
953
1241
|
raw_url = (todo.url or "").strip()
|
|
1242
|
+
|
|
1243
|
+
auth_context = {
|
|
1244
|
+
"require_logout": False,
|
|
1245
|
+
"users": [],
|
|
1246
|
+
"permissions": [],
|
|
1247
|
+
"notes": [],
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
def _final_context(target_url: str):
|
|
1251
|
+
return {
|
|
1252
|
+
"target_url": target_url or fallback,
|
|
1253
|
+
"require_logout": auth_context["require_logout"],
|
|
1254
|
+
"users": _dedupe_preserve_order(auth_context["users"]),
|
|
1255
|
+
"permissions": _dedupe_preserve_order(auth_context["permissions"]),
|
|
1256
|
+
"notes": _dedupe_preserve_order(auth_context["notes"]),
|
|
1257
|
+
"has_requirements": bool(
|
|
1258
|
+
auth_context["require_logout"]
|
|
1259
|
+
or auth_context["users"]
|
|
1260
|
+
or auth_context["permissions"]
|
|
1261
|
+
or auth_context["notes"]
|
|
1262
|
+
),
|
|
1263
|
+
}
|
|
1264
|
+
|
|
954
1265
|
if not raw_url:
|
|
955
|
-
return fallback
|
|
1266
|
+
return fallback, _final_context(fallback)
|
|
1267
|
+
|
|
1268
|
+
focus_path = reverse("todo-focus", args=[todo.pk])
|
|
1269
|
+
focus_norm = focus_path.strip("/").lower()
|
|
1270
|
+
|
|
1271
|
+
def _is_focus_target(target: str) -> bool:
|
|
1272
|
+
if not target:
|
|
1273
|
+
return False
|
|
1274
|
+
parsed_target = urlsplit(target)
|
|
1275
|
+
path = parsed_target.path
|
|
1276
|
+
if not path and not parsed_target.scheme and not parsed_target.netloc:
|
|
1277
|
+
path = target.split("?", 1)[0].split("#", 1)[0]
|
|
1278
|
+
normalized = path.strip("/").lower()
|
|
1279
|
+
return normalized == focus_norm if normalized else False
|
|
1280
|
+
|
|
1281
|
+
if _is_focus_target(raw_url):
|
|
1282
|
+
return fallback, _final_context(fallback)
|
|
956
1283
|
|
|
957
1284
|
parsed = urlsplit(raw_url)
|
|
1285
|
+
|
|
1286
|
+
def _merge_directives(parsed_result):
|
|
1287
|
+
sanitized_query, directives = _parse_todo_auth_directives(parsed_result.query)
|
|
1288
|
+
if directives["require_logout"]:
|
|
1289
|
+
auth_context["require_logout"] = True
|
|
1290
|
+
auth_context["users"].extend(directives["users"])
|
|
1291
|
+
auth_context["permissions"].extend(directives["permissions"])
|
|
1292
|
+
auth_context["notes"].extend(directives["notes"])
|
|
1293
|
+
return parsed_result._replace(query=sanitized_query)
|
|
1294
|
+
|
|
958
1295
|
if not parsed.scheme and not parsed.netloc:
|
|
959
|
-
|
|
1296
|
+
sanitized = _merge_directives(parsed)
|
|
1297
|
+
path = sanitized.path or "/"
|
|
1298
|
+
if not path.startswith("/"):
|
|
1299
|
+
path = f"/{path}"
|
|
1300
|
+
relative_url = urlunsplit(("", "", path, sanitized.query, sanitized.fragment))
|
|
1301
|
+
if _is_focus_target(relative_url):
|
|
1302
|
+
return fallback, _final_context(fallback)
|
|
1303
|
+
return relative_url or fallback, _final_context(relative_url)
|
|
960
1304
|
|
|
961
1305
|
if parsed.scheme and parsed.scheme.lower() not in {"http", "https"}:
|
|
962
|
-
return fallback
|
|
1306
|
+
return fallback, _final_context(fallback)
|
|
963
1307
|
|
|
964
1308
|
request_host = request.get_host().strip().lower()
|
|
965
1309
|
host_without_port = request_host.split(":", 1)[0]
|
|
@@ -993,12 +1337,16 @@ def _todo_iframe_url(request, todo: Todo) -> str:
|
|
|
993
1337
|
hostname = (parsed.hostname or "").strip().lower()
|
|
994
1338
|
netloc = parsed.netloc.strip().lower()
|
|
995
1339
|
if hostname in allowed_hosts or netloc in allowed_hosts:
|
|
996
|
-
|
|
1340
|
+
sanitized = _merge_directives(parsed)
|
|
1341
|
+
path = sanitized.path or "/"
|
|
997
1342
|
if not path.startswith("/"):
|
|
998
1343
|
path = f"/{path}"
|
|
999
|
-
|
|
1344
|
+
relative_url = urlunsplit(("", "", path, sanitized.query, sanitized.fragment))
|
|
1345
|
+
if _is_focus_target(relative_url):
|
|
1346
|
+
return fallback, _final_context(fallback)
|
|
1347
|
+
return relative_url or fallback, _final_context(relative_url)
|
|
1000
1348
|
|
|
1001
|
-
return fallback
|
|
1349
|
+
return fallback, _final_context(fallback)
|
|
1002
1350
|
|
|
1003
1351
|
|
|
1004
1352
|
@staff_member_required
|
|
@@ -1007,10 +1355,13 @@ def todo_focus(request, pk: int):
|
|
|
1007
1355
|
if todo.done_on:
|
|
1008
1356
|
return redirect(_get_return_url(request))
|
|
1009
1357
|
|
|
1010
|
-
iframe_url = _todo_iframe_url(request, todo)
|
|
1358
|
+
iframe_url, focus_auth = _todo_iframe_url(request, todo)
|
|
1359
|
+
focus_target_url = focus_auth.get("target_url", iframe_url) if focus_auth else iframe_url
|
|
1011
1360
|
context = {
|
|
1012
1361
|
"todo": todo,
|
|
1013
1362
|
"iframe_url": iframe_url,
|
|
1363
|
+
"focus_target_url": focus_target_url,
|
|
1364
|
+
"focus_auth": focus_auth,
|
|
1014
1365
|
"next_url": _get_return_url(request),
|
|
1015
1366
|
"done_url": reverse("todo-done", args=[todo.pk]),
|
|
1016
1367
|
}
|