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/views.py
CHANGED
|
@@ -11,7 +11,7 @@ import requests
|
|
|
11
11
|
from django.conf import settings
|
|
12
12
|
from django.contrib.admin.sites import site as admin_site
|
|
13
13
|
from django.contrib.admin.views.decorators import staff_member_required
|
|
14
|
-
from django.contrib.auth import authenticate, login
|
|
14
|
+
from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login
|
|
15
15
|
from django.contrib import messages
|
|
16
16
|
from django.contrib.sites.models import Site
|
|
17
17
|
from django.http import Http404, JsonResponse, HttpResponse
|
|
@@ -19,7 +19,6 @@ from django.shortcuts import get_object_or_404, redirect, render, resolve_url
|
|
|
19
19
|
from django.template.response import TemplateResponse
|
|
20
20
|
from django.utils import timezone
|
|
21
21
|
from django.utils.html import strip_tags
|
|
22
|
-
from django.utils.text import slugify
|
|
23
22
|
from django.utils.translation import gettext as _
|
|
24
23
|
from django.urls import NoReverseMatch, reverse
|
|
25
24
|
from django.views.decorators.csrf import csrf_exempt
|
|
@@ -43,6 +42,7 @@ logger = logging.getLogger(__name__)
|
|
|
43
42
|
PYPI_REQUEST_TIMEOUT = 10
|
|
44
43
|
|
|
45
44
|
from . import changelog as changelog_utils
|
|
45
|
+
from . import temp_passwords
|
|
46
46
|
from .models import OdooProfile, Product, EnergyAccount, PackageRelease, Todo
|
|
47
47
|
from .models import RFID
|
|
48
48
|
|
|
@@ -58,7 +58,6 @@ def odoo_products(request):
|
|
|
58
58
|
products = profile.execute(
|
|
59
59
|
"product.product",
|
|
60
60
|
"search_read",
|
|
61
|
-
[[]],
|
|
62
61
|
fields=["name"],
|
|
63
62
|
limit=50,
|
|
64
63
|
)
|
|
@@ -100,7 +99,7 @@ def odoo_quote_report(request):
|
|
|
100
99
|
|
|
101
100
|
if not profile or not profile.is_verified:
|
|
102
101
|
context["error"] = _(
|
|
103
|
-
"Configure and verify your
|
|
102
|
+
"Configure and verify your CRM employee credentials before generating the report."
|
|
104
103
|
)
|
|
105
104
|
return TemplateResponse(
|
|
106
105
|
request, "admin/core/odoo_quote_report.html", context
|
|
@@ -137,7 +136,6 @@ def odoo_quote_report(request):
|
|
|
137
136
|
templates = profile.execute(
|
|
138
137
|
"sale.order.template",
|
|
139
138
|
"search_read",
|
|
140
|
-
[[]],
|
|
141
139
|
fields=["name"],
|
|
142
140
|
order="name asc",
|
|
143
141
|
)
|
|
@@ -287,7 +285,6 @@ def odoo_quote_report(request):
|
|
|
287
285
|
products = profile.execute(
|
|
288
286
|
"product.product",
|
|
289
287
|
"search_read",
|
|
290
|
-
[[]],
|
|
291
288
|
fields=["name", "default_code", "write_date", "create_date"],
|
|
292
289
|
limit=10,
|
|
293
290
|
order="write_date desc, create_date desc",
|
|
@@ -336,6 +333,36 @@ def odoo_quote_report(request):
|
|
|
336
333
|
return TemplateResponse(request, "admin/core/odoo_quote_report.html", context)
|
|
337
334
|
|
|
338
335
|
|
|
336
|
+
@staff_member_required
|
|
337
|
+
@require_GET
|
|
338
|
+
def request_temp_password(request):
|
|
339
|
+
"""Generate a temporary password for the authenticated staff member."""
|
|
340
|
+
|
|
341
|
+
user = request.user
|
|
342
|
+
username = user.get_username()
|
|
343
|
+
password = temp_passwords.generate_password()
|
|
344
|
+
entry = temp_passwords.store_temp_password(
|
|
345
|
+
username,
|
|
346
|
+
password,
|
|
347
|
+
allow_change=True,
|
|
348
|
+
)
|
|
349
|
+
context = {
|
|
350
|
+
**admin_site.each_context(request),
|
|
351
|
+
"title": _("Temporary password"),
|
|
352
|
+
"username": username,
|
|
353
|
+
"password": password,
|
|
354
|
+
"expires_at": timezone.localtime(entry.expires_at),
|
|
355
|
+
"allow_change": entry.allow_change,
|
|
356
|
+
"return_url": reverse("admin:password_change"),
|
|
357
|
+
}
|
|
358
|
+
return TemplateResponse(
|
|
359
|
+
request,
|
|
360
|
+
"admin/core/request_temp_password.html",
|
|
361
|
+
context,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@staff_member_required
|
|
339
366
|
@require_GET
|
|
340
367
|
def version_info(request):
|
|
341
368
|
"""Return the running application version and Git revision."""
|
|
@@ -418,8 +445,11 @@ def _resolve_release_log_dir(preferred: Path) -> tuple[Path, str | None]:
|
|
|
418
445
|
|
|
419
446
|
env_override = os.environ.pop("ARTHEXIS_LOG_DIR", None)
|
|
420
447
|
fallback = select_log_dir(Path(settings.BASE_DIR))
|
|
421
|
-
if env_override
|
|
422
|
-
|
|
448
|
+
if env_override is not None:
|
|
449
|
+
if Path(env_override) == fallback:
|
|
450
|
+
os.environ["ARTHEXIS_LOG_DIR"] = env_override
|
|
451
|
+
else:
|
|
452
|
+
os.environ["ARTHEXIS_LOG_DIR"] = str(fallback)
|
|
423
453
|
|
|
424
454
|
if fallback == preferred:
|
|
425
455
|
if error:
|
|
@@ -440,9 +470,60 @@ def _resolve_release_log_dir(preferred: Path) -> tuple[Path, str | None]:
|
|
|
440
470
|
return fallback, warning
|
|
441
471
|
|
|
442
472
|
|
|
473
|
+
def _commit_todo_fixtures_if_needed(log_path: Path) -> None:
|
|
474
|
+
"""Stage and commit modified TODO fixtures before syncing with origin/main."""
|
|
475
|
+
|
|
476
|
+
try:
|
|
477
|
+
status = subprocess.run(
|
|
478
|
+
["git", "status", "--porcelain"],
|
|
479
|
+
check=True,
|
|
480
|
+
capture_output=True,
|
|
481
|
+
text=True,
|
|
482
|
+
)
|
|
483
|
+
except subprocess.CalledProcessError:
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
todo_paths: set[Path] = set()
|
|
487
|
+
for line in (status.stdout or "").splitlines():
|
|
488
|
+
if not line:
|
|
489
|
+
continue
|
|
490
|
+
path_fragment = line[3:].strip()
|
|
491
|
+
if "->" in path_fragment:
|
|
492
|
+
path_fragment = path_fragment.split("->", 1)[1].strip()
|
|
493
|
+
path = Path(path_fragment)
|
|
494
|
+
if path.suffix == ".json" and path.parent == Path("core/fixtures"):
|
|
495
|
+
if path.name.startswith("todo__"):
|
|
496
|
+
todo_paths.add(path)
|
|
497
|
+
|
|
498
|
+
if not todo_paths:
|
|
499
|
+
return
|
|
500
|
+
|
|
501
|
+
sorted_paths = sorted(todo_paths)
|
|
502
|
+
subprocess.run(
|
|
503
|
+
["git", "add", *[str(path) for path in sorted_paths]],
|
|
504
|
+
check=True,
|
|
505
|
+
)
|
|
506
|
+
formatted = ", ".join(_format_path(path) for path in sorted_paths)
|
|
507
|
+
_append_log(log_path, f"Staged TODO fixtures {formatted}")
|
|
508
|
+
|
|
509
|
+
diff = subprocess.run(
|
|
510
|
+
["git", "diff", "--cached", "--name-only", "--", *[str(path) for path in sorted_paths]],
|
|
511
|
+
check=True,
|
|
512
|
+
capture_output=True,
|
|
513
|
+
text=True,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
if (diff.stdout or "").strip():
|
|
517
|
+
message = "chore: update TODO fixtures"
|
|
518
|
+
subprocess.run(["git", "commit", "-m", message], check=True)
|
|
519
|
+
_append_log(log_path, f"Committed TODO fixtures ({message})")
|
|
520
|
+
|
|
521
|
+
|
|
443
522
|
def _sync_with_origin_main(log_path: Path) -> None:
|
|
444
523
|
"""Ensure the current branch is rebased onto ``origin/main``."""
|
|
445
524
|
|
|
525
|
+
_commit_todo_fixtures_if_needed(log_path)
|
|
526
|
+
|
|
446
527
|
if not _has_remote("origin"):
|
|
447
528
|
_append_log(log_path, "No git remote configured; skipping sync with origin/main")
|
|
448
529
|
return
|
|
@@ -463,6 +544,16 @@ def _sync_with_origin_main(log_path: Path) -> None:
|
|
|
463
544
|
if stderr:
|
|
464
545
|
_append_log(log_path, "git errors:\n" + stderr)
|
|
465
546
|
|
|
547
|
+
status = subprocess.run(
|
|
548
|
+
["git", "status"], capture_output=True, text=True, check=False
|
|
549
|
+
)
|
|
550
|
+
status_output = (status.stdout or "").strip()
|
|
551
|
+
status_errors = (status.stderr or "").strip()
|
|
552
|
+
if status_output:
|
|
553
|
+
_append_log(log_path, "git status:\n" + status_output)
|
|
554
|
+
if status_errors:
|
|
555
|
+
_append_log(log_path, "git status errors:\n" + status_errors)
|
|
556
|
+
|
|
466
557
|
branch = _current_branch() or "(detached HEAD)"
|
|
467
558
|
instructions = [
|
|
468
559
|
"Manual intervention required to finish syncing with origin/main.",
|
|
@@ -578,6 +669,43 @@ def _git_authentication_missing(exc: subprocess.CalledProcessError) -> bool:
|
|
|
578
669
|
return any(marker in message for marker in auth_markers)
|
|
579
670
|
|
|
580
671
|
|
|
672
|
+
def _push_release_changes(log_path: Path) -> bool:
|
|
673
|
+
"""Push release commits to ``origin`` and log the outcome."""
|
|
674
|
+
|
|
675
|
+
if not _has_remote("origin"):
|
|
676
|
+
_append_log(
|
|
677
|
+
log_path, "No git remote configured; skipping push of release changes"
|
|
678
|
+
)
|
|
679
|
+
return False
|
|
680
|
+
|
|
681
|
+
try:
|
|
682
|
+
branch = _current_branch()
|
|
683
|
+
if branch is None:
|
|
684
|
+
push_cmd = ["git", "push", "origin", "HEAD"]
|
|
685
|
+
elif _has_upstream(branch):
|
|
686
|
+
push_cmd = ["git", "push"]
|
|
687
|
+
else:
|
|
688
|
+
push_cmd = ["git", "push", "--set-upstream", "origin", branch]
|
|
689
|
+
subprocess.run(push_cmd, check=True, capture_output=True, text=True)
|
|
690
|
+
except subprocess.CalledProcessError as exc:
|
|
691
|
+
details = _format_subprocess_error(exc)
|
|
692
|
+
if _git_authentication_missing(exc):
|
|
693
|
+
_append_log(
|
|
694
|
+
log_path,
|
|
695
|
+
"Authentication is required to push release changes to origin; skipping push",
|
|
696
|
+
)
|
|
697
|
+
if details:
|
|
698
|
+
_append_log(log_path, details)
|
|
699
|
+
return False
|
|
700
|
+
_append_log(
|
|
701
|
+
log_path, f"Failed to push release changes to origin: {details}"
|
|
702
|
+
)
|
|
703
|
+
raise Exception("Failed to push release changes") from exc
|
|
704
|
+
|
|
705
|
+
_append_log(log_path, "Pushed release changes to origin")
|
|
706
|
+
return True
|
|
707
|
+
|
|
708
|
+
|
|
581
709
|
def _ensure_origin_main_unchanged(log_path: Path) -> None:
|
|
582
710
|
"""Verify that ``origin/main`` has not advanced during the release."""
|
|
583
711
|
|
|
@@ -610,42 +738,20 @@ def _ensure_origin_main_unchanged(log_path: Path) -> None:
|
|
|
610
738
|
def _next_patch_version(version: str) -> str:
|
|
611
739
|
from packaging.version import InvalidVersion, Version
|
|
612
740
|
|
|
741
|
+
cleaned = version.rstrip("+")
|
|
613
742
|
try:
|
|
614
|
-
parsed = Version(
|
|
743
|
+
parsed = Version(cleaned)
|
|
615
744
|
except InvalidVersion:
|
|
616
|
-
parts =
|
|
745
|
+
parts = cleaned.split(".") if cleaned else []
|
|
617
746
|
for index in range(len(parts) - 1, -1, -1):
|
|
618
747
|
segment = parts[index]
|
|
619
748
|
if segment.isdigit():
|
|
620
749
|
parts[index] = str(int(segment) + 1)
|
|
621
750
|
return ".".join(parts)
|
|
622
|
-
return version
|
|
751
|
+
return cleaned or version
|
|
623
752
|
return f"{parsed.major}.{parsed.minor}.{parsed.micro + 1}"
|
|
624
753
|
|
|
625
754
|
|
|
626
|
-
def _write_todo_fixture(todo: Todo) -> Path:
|
|
627
|
-
safe_request = todo.request.replace(".", " ")
|
|
628
|
-
slug = slugify(safe_request).replace("-", "_")
|
|
629
|
-
if not slug:
|
|
630
|
-
slug = "todo"
|
|
631
|
-
path = TODO_FIXTURE_DIR / f"todos__{slug}.json"
|
|
632
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
633
|
-
data = [
|
|
634
|
-
{
|
|
635
|
-
"model": "core.todo",
|
|
636
|
-
"fields": {
|
|
637
|
-
"request": todo.request,
|
|
638
|
-
"url": todo.url,
|
|
639
|
-
"request_details": todo.request_details,
|
|
640
|
-
"generated_for_version": todo.generated_for_version,
|
|
641
|
-
"generated_for_revision": todo.generated_for_revision,
|
|
642
|
-
},
|
|
643
|
-
}
|
|
644
|
-
]
|
|
645
|
-
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
646
|
-
return path
|
|
647
|
-
|
|
648
|
-
|
|
649
755
|
def _should_use_python_changelog(exc: OSError) -> bool:
|
|
650
756
|
winerror = getattr(exc, "winerror", None)
|
|
651
757
|
if winerror in {193}:
|
|
@@ -656,8 +762,8 @@ def _should_use_python_changelog(exc: OSError) -> bool:
|
|
|
656
762
|
def _generate_changelog_with_python(log_path: Path) -> None:
|
|
657
763
|
_append_log(log_path, "Falling back to Python changelog generator")
|
|
658
764
|
changelog_path = Path("CHANGELOG.rst")
|
|
659
|
-
range_spec = changelog_utils.determine_range_spec()
|
|
660
765
|
previous = changelog_path.read_text(encoding="utf-8") if changelog_path.exists() else None
|
|
766
|
+
range_spec = changelog_utils.determine_range_spec(previous_text=previous)
|
|
661
767
|
sections = changelog_utils.collect_sections(range_spec=range_spec, previous_text=previous)
|
|
662
768
|
content = changelog_utils.render_changelog(sections)
|
|
663
769
|
if not content.endswith("\n"):
|
|
@@ -666,44 +772,32 @@ def _generate_changelog_with_python(log_path: Path) -> None:
|
|
|
666
772
|
_append_log(log_path, "Regenerated CHANGELOG.rst using Python fallback")
|
|
667
773
|
|
|
668
774
|
|
|
669
|
-
def
|
|
670
|
-
|
|
671
|
-
) -> tuple[Todo, Path]:
|
|
672
|
-
previous_version = (previous_version or "").strip()
|
|
673
|
-
target_version = _next_patch_version(release.version)
|
|
674
|
-
if previous_version:
|
|
675
|
-
try:
|
|
676
|
-
from packaging.version import InvalidVersion, Version
|
|
775
|
+
def _todo_blocks_publish(todo: Todo, release: PackageRelease) -> bool:
|
|
776
|
+
"""Return ``True`` when ``todo`` should block the release workflow."""
|
|
677
777
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
"done_on": None,
|
|
702
|
-
"on_done_condition": "",
|
|
703
|
-
},
|
|
704
|
-
)
|
|
705
|
-
fixture_path = _write_todo_fixture(todo)
|
|
706
|
-
return todo, fixture_path
|
|
778
|
+
request = (todo.request or "").strip()
|
|
779
|
+
release_name = (release.package.name or "").strip()
|
|
780
|
+
if not request or not release_name:
|
|
781
|
+
return True
|
|
782
|
+
|
|
783
|
+
prefix = f"create release {release_name.lower()} "
|
|
784
|
+
if not request.lower().startswith(prefix):
|
|
785
|
+
return True
|
|
786
|
+
|
|
787
|
+
release_version = (release.version or "").strip()
|
|
788
|
+
generated_version = (todo.generated_for_version or "").strip()
|
|
789
|
+
if not release_version or release_version != generated_version:
|
|
790
|
+
return True
|
|
791
|
+
|
|
792
|
+
generated_revision = (todo.generated_for_revision or "").strip()
|
|
793
|
+
release_revision = (release.revision or "").strip()
|
|
794
|
+
if generated_revision and release_revision and generated_revision != release_revision:
|
|
795
|
+
return True
|
|
796
|
+
|
|
797
|
+
if not todo.is_seed_data:
|
|
798
|
+
return True
|
|
799
|
+
|
|
800
|
+
return False
|
|
707
801
|
|
|
708
802
|
|
|
709
803
|
def _sync_release_with_revision(release: PackageRelease) -> tuple[bool, str]:
|
|
@@ -723,7 +817,9 @@ def _sync_release_with_revision(release: PackageRelease) -> tuple[bool, str]:
|
|
|
723
817
|
version_path = Path("VERSION")
|
|
724
818
|
if version_path.exists():
|
|
725
819
|
try:
|
|
726
|
-
|
|
820
|
+
raw_version = version_path.read_text(encoding="utf-8").strip()
|
|
821
|
+
cleaned_version = raw_version.rstrip("+") or "0.0.0"
|
|
822
|
+
repo_version = Version(cleaned_version)
|
|
727
823
|
except InvalidVersion:
|
|
728
824
|
repo_version = None
|
|
729
825
|
|
|
@@ -890,14 +986,25 @@ def _refresh_changelog_once(ctx, log_path: Path) -> None:
|
|
|
890
986
|
ctx["changelog_refreshed"] = True
|
|
891
987
|
|
|
892
988
|
|
|
893
|
-
def _step_check_todos(release, ctx, log_path: Path) -> None:
|
|
989
|
+
def _step_check_todos(release, ctx, log_path: Path, *, user=None) -> None:
|
|
894
990
|
_refresh_changelog_once(ctx, log_path)
|
|
895
991
|
|
|
896
992
|
pending_qs = Todo.objects.filter(is_deleted=False, done_on__isnull=True)
|
|
897
993
|
pending_values = list(
|
|
898
994
|
pending_qs.values("id", "request", "url", "request_details")
|
|
899
995
|
)
|
|
996
|
+
if not pending_values:
|
|
997
|
+
ctx["todos_ack"] = True
|
|
998
|
+
|
|
900
999
|
if not ctx.get("todos_ack"):
|
|
1000
|
+
if not ctx.get("todos_block_logged"):
|
|
1001
|
+
_append_log(
|
|
1002
|
+
log_path,
|
|
1003
|
+
"Release checklist requires acknowledgment before continuing. "
|
|
1004
|
+
"Review outstanding TODO items and confirm the checklist; "
|
|
1005
|
+
"publishing will resume automatically afterward.",
|
|
1006
|
+
)
|
|
1007
|
+
ctx["todos_block_logged"] = True
|
|
901
1008
|
ctx["todos"] = pending_values
|
|
902
1009
|
ctx["todos_required"] = True
|
|
903
1010
|
raise PendingTodos()
|
|
@@ -919,7 +1026,7 @@ def _step_check_todos(release, ctx, log_path: Path) -> None:
|
|
|
919
1026
|
ctx["todos_ack"] = True
|
|
920
1027
|
|
|
921
1028
|
|
|
922
|
-
def _step_check_version(release, ctx, log_path: Path) -> None:
|
|
1029
|
+
def _step_check_version(release, ctx, log_path: Path, *, user=None) -> None:
|
|
923
1030
|
from . import release as release_utils
|
|
924
1031
|
from packaging.version import InvalidVersion, Version
|
|
925
1032
|
|
|
@@ -1054,10 +1161,12 @@ def _step_check_version(release, ctx, log_path: Path) -> None:
|
|
|
1054
1161
|
version_path = Path("VERSION")
|
|
1055
1162
|
if version_path.exists():
|
|
1056
1163
|
current = version_path.read_text(encoding="utf-8").strip()
|
|
1057
|
-
if current
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1164
|
+
if current:
|
|
1165
|
+
current_clean = current.rstrip("+") or "0.0.0"
|
|
1166
|
+
if Version(release.version) < Version(current_clean):
|
|
1167
|
+
raise Exception(
|
|
1168
|
+
f"Version {release.version} is older than existing {current}"
|
|
1169
|
+
)
|
|
1061
1170
|
|
|
1062
1171
|
_append_log(log_path, f"Checking if version {release.version} exists on PyPI")
|
|
1063
1172
|
if release_utils.network_available():
|
|
@@ -1107,47 +1216,17 @@ def _step_check_version(release, ctx, log_path: Path) -> None:
|
|
|
1107
1216
|
_append_log(log_path, "Network unavailable, skipping PyPI check")
|
|
1108
1217
|
|
|
1109
1218
|
|
|
1110
|
-
def _step_handle_migrations(release, ctx, log_path: Path) -> None:
|
|
1219
|
+
def _step_handle_migrations(release, ctx, log_path: Path, *, user=None) -> None:
|
|
1111
1220
|
_append_log(log_path, "Freeze, squash and approve migrations")
|
|
1112
1221
|
_append_log(log_path, "Migration review acknowledged (manual step)")
|
|
1113
1222
|
|
|
1114
1223
|
|
|
1115
|
-
def _step_changelog_docs(release, ctx, log_path: Path) -> None:
|
|
1224
|
+
def _step_changelog_docs(release, ctx, log_path: Path, *, user=None) -> None:
|
|
1116
1225
|
_append_log(log_path, "Compose CHANGELOG and documentation")
|
|
1117
1226
|
_append_log(log_path, "CHANGELOG and documentation review recorded")
|
|
1118
1227
|
|
|
1119
1228
|
|
|
1120
|
-
def
|
|
1121
|
-
release, ctx, log_path: Path, *, previous_version: str | None = None
|
|
1122
|
-
) -> None:
|
|
1123
|
-
previous_version = previous_version or ctx.pop(
|
|
1124
|
-
"release_todo_previous_version",
|
|
1125
|
-
getattr(release, "_repo_version_before_sync", ""),
|
|
1126
|
-
)
|
|
1127
|
-
todo, fixture_path = _ensure_release_todo(
|
|
1128
|
-
release, previous_version=previous_version
|
|
1129
|
-
)
|
|
1130
|
-
fixture_display = _format_path(fixture_path)
|
|
1131
|
-
_append_log(log_path, f"Added TODO: {todo.request}")
|
|
1132
|
-
_append_log(log_path, f"Wrote TODO fixture {fixture_display}")
|
|
1133
|
-
subprocess.run(["git", "add", str(fixture_path)], check=True)
|
|
1134
|
-
_append_log(log_path, f"Staged TODO fixture {fixture_display}")
|
|
1135
|
-
fixture_diff = subprocess.run(
|
|
1136
|
-
["git", "diff", "--cached", "--quiet", "--", str(fixture_path)],
|
|
1137
|
-
check=False,
|
|
1138
|
-
)
|
|
1139
|
-
if fixture_diff.returncode != 0:
|
|
1140
|
-
commit_message = f"chore: add release TODO for {release.package.name}"
|
|
1141
|
-
subprocess.run(["git", "commit", "-m", commit_message], check=True)
|
|
1142
|
-
_append_log(log_path, f"Committed TODO fixture {fixture_display}")
|
|
1143
|
-
else:
|
|
1144
|
-
_append_log(
|
|
1145
|
-
log_path,
|
|
1146
|
-
f"No changes detected for TODO fixture {fixture_display}; skipping commit",
|
|
1147
|
-
)
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
def _step_pre_release_actions(release, ctx, log_path: Path) -> None:
|
|
1229
|
+
def _step_pre_release_actions(release, ctx, log_path: Path, *, user=None) -> None:
|
|
1151
1230
|
_append_log(log_path, "Execute pre-release actions")
|
|
1152
1231
|
if ctx.get("dry_run"):
|
|
1153
1232
|
_append_log(log_path, "Dry run: skipping pre-release actions")
|
|
@@ -1220,16 +1299,15 @@ def _step_pre_release_actions(release, ctx, log_path: Path) -> None:
|
|
|
1220
1299
|
for path in staged_release_fixtures:
|
|
1221
1300
|
subprocess.run(["git", "reset", "HEAD", str(path)], check=False)
|
|
1222
1301
|
_append_log(log_path, f"Unstaged release fixture {_format_path(path)}")
|
|
1223
|
-
ctx["release_todo_previous_version"] = repo_version_before_sync
|
|
1224
1302
|
_append_log(log_path, "Pre-release actions complete")
|
|
1225
1303
|
|
|
1226
1304
|
|
|
1227
|
-
def _step_run_tests(release, ctx, log_path: Path) -> None:
|
|
1305
|
+
def _step_run_tests(release, ctx, log_path: Path, *, user=None) -> None:
|
|
1228
1306
|
_append_log(log_path, "Complete test suite with --all flag")
|
|
1229
1307
|
_append_log(log_path, "Test suite completion acknowledged")
|
|
1230
1308
|
|
|
1231
1309
|
|
|
1232
|
-
def _step_promote_build(release, ctx, log_path: Path) -> None:
|
|
1310
|
+
def _step_promote_build(release, ctx, log_path: Path, *, user=None) -> None:
|
|
1233
1311
|
from . import release as release_utils
|
|
1234
1312
|
|
|
1235
1313
|
_append_log(log_path, "Generating build files")
|
|
@@ -1241,7 +1319,7 @@ def _step_promote_build(release, ctx, log_path: Path) -> None:
|
|
|
1241
1319
|
release_utils.promote(
|
|
1242
1320
|
package=release.to_package(),
|
|
1243
1321
|
version=release.version,
|
|
1244
|
-
creds=release.to_credentials(),
|
|
1322
|
+
creds=release.to_credentials(user=user),
|
|
1245
1323
|
)
|
|
1246
1324
|
_append_log(
|
|
1247
1325
|
log_path,
|
|
@@ -1271,40 +1349,9 @@ def _step_promote_build(release, ctx, log_path: Path) -> None:
|
|
|
1271
1349
|
log_path,
|
|
1272
1350
|
f"Committed release metadata for v{release.version}",
|
|
1273
1351
|
)
|
|
1274
|
-
|
|
1275
|
-
try:
|
|
1276
|
-
branch = _current_branch()
|
|
1277
|
-
if branch is None:
|
|
1278
|
-
push_cmd = ["git", "push", "origin", "HEAD"]
|
|
1279
|
-
elif _has_upstream(branch):
|
|
1280
|
-
push_cmd = ["git", "push"]
|
|
1281
|
-
else:
|
|
1282
|
-
push_cmd = ["git", "push", "--set-upstream", "origin", branch]
|
|
1283
|
-
subprocess.run(push_cmd, check=True, capture_output=True, text=True)
|
|
1284
|
-
except subprocess.CalledProcessError as exc:
|
|
1285
|
-
details = _format_subprocess_error(exc)
|
|
1286
|
-
if _git_authentication_missing(exc):
|
|
1287
|
-
_append_log(
|
|
1288
|
-
log_path,
|
|
1289
|
-
"Authentication is required to push release changes to origin; skipping push",
|
|
1290
|
-
)
|
|
1291
|
-
if details:
|
|
1292
|
-
_append_log(log_path, details)
|
|
1293
|
-
else:
|
|
1294
|
-
_append_log(
|
|
1295
|
-
log_path, f"Failed to push release changes to origin: {details}"
|
|
1296
|
-
)
|
|
1297
|
-
raise Exception("Failed to push release changes") from exc
|
|
1298
|
-
else:
|
|
1299
|
-
_append_log(log_path, "Pushed release changes to origin")
|
|
1300
|
-
else:
|
|
1301
|
-
_append_log(
|
|
1302
|
-
log_path,
|
|
1303
|
-
"No git remote configured; skipping push of release changes",
|
|
1304
|
-
)
|
|
1352
|
+
_push_release_changes(log_path)
|
|
1305
1353
|
PackageRelease.dump_fixture()
|
|
1306
1354
|
_append_log(log_path, "Updated release fixtures")
|
|
1307
|
-
_record_release_todo(release, ctx, log_path)
|
|
1308
1355
|
except Exception:
|
|
1309
1356
|
_clean_repo()
|
|
1310
1357
|
raise
|
|
@@ -1320,8 +1367,10 @@ def _step_promote_build(release, ctx, log_path: Path) -> None:
|
|
|
1320
1367
|
_append_log(new_log, "Build complete")
|
|
1321
1368
|
|
|
1322
1369
|
|
|
1323
|
-
def _step_release_manager_approval(
|
|
1324
|
-
|
|
1370
|
+
def _step_release_manager_approval(
|
|
1371
|
+
release, ctx, log_path: Path, *, user=None
|
|
1372
|
+
) -> None:
|
|
1373
|
+
if release.to_credentials(user=user) is None:
|
|
1325
1374
|
ctx.pop("release_approval", None)
|
|
1326
1375
|
if not ctx.get("approval_credentials_missing"):
|
|
1327
1376
|
_append_log(log_path, "Release manager publishing credentials missing")
|
|
@@ -1355,14 +1404,14 @@ def _step_release_manager_approval(release, ctx, log_path: Path) -> None:
|
|
|
1355
1404
|
raise ApprovalRequired()
|
|
1356
1405
|
|
|
1357
1406
|
|
|
1358
|
-
def _step_publish(release, ctx, log_path: Path) -> None:
|
|
1407
|
+
def _step_publish(release, ctx, log_path: Path, *, user=None) -> None:
|
|
1359
1408
|
from . import release as release_utils
|
|
1360
1409
|
|
|
1361
1410
|
if ctx.get("dry_run"):
|
|
1362
1411
|
test_repository_url = os.environ.get(
|
|
1363
1412
|
"PYPI_TEST_REPOSITORY_URL", "https://test.pypi.org/legacy/"
|
|
1364
1413
|
)
|
|
1365
|
-
test_creds = release.to_credentials()
|
|
1414
|
+
test_creds = release.to_credentials(user=user)
|
|
1366
1415
|
if not (test_creds and test_creds.has_auth()):
|
|
1367
1416
|
test_creds = release_utils.Credentials(
|
|
1368
1417
|
token=os.environ.get("PYPI_TEST_API_TOKEN"),
|
|
@@ -1402,7 +1451,7 @@ def _step_publish(release, ctx, log_path: Path) -> None:
|
|
|
1402
1451
|
release_utils.build(
|
|
1403
1452
|
package=package,
|
|
1404
1453
|
version=release.version,
|
|
1405
|
-
creds=release.to_credentials(),
|
|
1454
|
+
creds=release.to_credentials(user=user),
|
|
1406
1455
|
dist=True,
|
|
1407
1456
|
tests=False,
|
|
1408
1457
|
twine=False,
|
|
@@ -1431,13 +1480,13 @@ def _step_publish(release, ctx, log_path: Path) -> None:
|
|
|
1431
1480
|
release_utils.publish(
|
|
1432
1481
|
package=release.to_package(),
|
|
1433
1482
|
version=release.version,
|
|
1434
|
-
creds=target.credentials or release.to_credentials(),
|
|
1483
|
+
creds=target.credentials or release.to_credentials(user=user),
|
|
1435
1484
|
repositories=[target],
|
|
1436
1485
|
)
|
|
1437
1486
|
_append_log(log_path, "Dry run: skipped release metadata updates")
|
|
1438
1487
|
return
|
|
1439
1488
|
|
|
1440
|
-
targets = release.build_publish_targets()
|
|
1489
|
+
targets = release.build_publish_targets(user=user)
|
|
1441
1490
|
repo_labels = []
|
|
1442
1491
|
for target in targets:
|
|
1443
1492
|
label = target.name
|
|
@@ -1451,12 +1500,29 @@ def _step_publish(release, ctx, log_path: Path) -> None:
|
|
|
1451
1500
|
)
|
|
1452
1501
|
else:
|
|
1453
1502
|
_append_log(log_path, "Uploading distribution")
|
|
1454
|
-
release_utils.
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1503
|
+
publish_warning: release_utils.PostPublishWarning | None = None
|
|
1504
|
+
try:
|
|
1505
|
+
release_utils.publish(
|
|
1506
|
+
package=release.to_package(),
|
|
1507
|
+
version=release.version,
|
|
1508
|
+
creds=release.to_credentials(user=user),
|
|
1509
|
+
repositories=targets,
|
|
1510
|
+
)
|
|
1511
|
+
except release_utils.PostPublishWarning as warning:
|
|
1512
|
+
publish_warning = warning
|
|
1513
|
+
|
|
1514
|
+
if publish_warning is not None:
|
|
1515
|
+
message = str(publish_warning)
|
|
1516
|
+
followups = _dedupe_preserve_order(publish_warning.followups)
|
|
1517
|
+
warning_entries = ctx.setdefault("warnings", [])
|
|
1518
|
+
if not any(entry.get("message") == message for entry in warning_entries):
|
|
1519
|
+
entry: dict[str, object] = {"message": message}
|
|
1520
|
+
if followups:
|
|
1521
|
+
entry["followups"] = followups
|
|
1522
|
+
warning_entries.append(entry)
|
|
1523
|
+
_append_log(log_path, message)
|
|
1524
|
+
for note in followups:
|
|
1525
|
+
_append_log(log_path, f"Follow-up: {note}")
|
|
1460
1526
|
release.pypi_url = (
|
|
1461
1527
|
f"https://pypi.org/project/{release.package.name}/{release.version}/"
|
|
1462
1528
|
)
|
|
@@ -1475,6 +1541,30 @@ def _step_publish(release, ctx, log_path: Path) -> None:
|
|
|
1475
1541
|
_append_log(log_path, f"Recorded PyPI URL: {release.pypi_url}")
|
|
1476
1542
|
if release.github_url:
|
|
1477
1543
|
_append_log(log_path, f"Recorded GitHub URL: {release.github_url}")
|
|
1544
|
+
fixture_paths = [
|
|
1545
|
+
str(path) for path in Path("core/fixtures").glob("releases__*.json")
|
|
1546
|
+
]
|
|
1547
|
+
if fixture_paths:
|
|
1548
|
+
status = subprocess.run(
|
|
1549
|
+
["git", "status", "--porcelain", "--", *fixture_paths],
|
|
1550
|
+
capture_output=True,
|
|
1551
|
+
text=True,
|
|
1552
|
+
check=True,
|
|
1553
|
+
)
|
|
1554
|
+
if status.stdout.strip():
|
|
1555
|
+
subprocess.run(["git", "add", *fixture_paths], check=True)
|
|
1556
|
+
_append_log(log_path, "Staged publish metadata updates")
|
|
1557
|
+
commit_message = f"chore: record publish metadata for v{release.version}"
|
|
1558
|
+
subprocess.run(["git", "commit", "-m", commit_message], check=True)
|
|
1559
|
+
_append_log(
|
|
1560
|
+
log_path, f"Committed publish metadata for v{release.version}"
|
|
1561
|
+
)
|
|
1562
|
+
_push_release_changes(log_path)
|
|
1563
|
+
else:
|
|
1564
|
+
_append_log(
|
|
1565
|
+
log_path,
|
|
1566
|
+
"No release metadata updates detected after publish; skipping commit",
|
|
1567
|
+
)
|
|
1478
1568
|
_append_log(log_path, "Upload complete")
|
|
1479
1569
|
|
|
1480
1570
|
|
|
@@ -1510,12 +1600,28 @@ def rfid_login(request):
|
|
|
1510
1600
|
if not rfid:
|
|
1511
1601
|
return JsonResponse({"detail": "rfid required"}, status=400)
|
|
1512
1602
|
|
|
1603
|
+
redirect_to = data.get(REDIRECT_FIELD_NAME) or data.get("next")
|
|
1604
|
+
if redirect_to and not url_has_allowed_host_and_scheme(
|
|
1605
|
+
redirect_to,
|
|
1606
|
+
allowed_hosts={request.get_host()},
|
|
1607
|
+
require_https=request.is_secure(),
|
|
1608
|
+
):
|
|
1609
|
+
redirect_to = ""
|
|
1610
|
+
|
|
1513
1611
|
user = authenticate(request, rfid=rfid)
|
|
1514
1612
|
if user is None:
|
|
1515
1613
|
return JsonResponse({"detail": "invalid RFID"}, status=401)
|
|
1516
1614
|
|
|
1517
1615
|
login(request, user)
|
|
1518
|
-
|
|
1616
|
+
if redirect_to:
|
|
1617
|
+
target = redirect_to
|
|
1618
|
+
elif user.is_staff:
|
|
1619
|
+
target = reverse("admin:index")
|
|
1620
|
+
else:
|
|
1621
|
+
target = "/"
|
|
1622
|
+
return JsonResponse(
|
|
1623
|
+
{"id": user.id, "username": user.username, "redirect": target}
|
|
1624
|
+
)
|
|
1519
1625
|
|
|
1520
1626
|
|
|
1521
1627
|
@api_login_required
|
|
@@ -1668,9 +1774,9 @@ def rfid_batch(request):
|
|
|
1668
1774
|
else:
|
|
1669
1775
|
post_auth_command = post_auth_command.strip()
|
|
1670
1776
|
|
|
1671
|
-
tag, _ = RFID.
|
|
1672
|
-
rfid
|
|
1673
|
-
|
|
1777
|
+
tag, _ = RFID.update_or_create_from_code(
|
|
1778
|
+
rfid,
|
|
1779
|
+
{
|
|
1674
1780
|
"allowed": allowed,
|
|
1675
1781
|
"color": color,
|
|
1676
1782
|
"released": released,
|
|
@@ -1784,7 +1890,7 @@ def release_progress(request, pk: int, action: str):
|
|
|
1784
1890
|
return redirect(request.path)
|
|
1785
1891
|
|
|
1786
1892
|
manager = release.release_manager or release.package.release_manager
|
|
1787
|
-
credentials_ready = bool(release.to_credentials())
|
|
1893
|
+
credentials_ready = bool(release.to_credentials(user=request.user))
|
|
1788
1894
|
if credentials_ready and ctx.get("approval_credentials_missing"):
|
|
1789
1895
|
ctx.pop("approval_credentials_missing", None)
|
|
1790
1896
|
|
|
@@ -1804,8 +1910,16 @@ def release_progress(request, pk: int, action: str):
|
|
|
1804
1910
|
ctx["release_approval"] = "approved"
|
|
1805
1911
|
if request.GET.get("reject"):
|
|
1806
1912
|
ctx["release_approval"] = "rejected"
|
|
1913
|
+
resume_requested = bool(request.GET.get("resume"))
|
|
1914
|
+
|
|
1807
1915
|
if request.GET.get("pause") and ctx.get("started"):
|
|
1808
1916
|
ctx["paused"] = True
|
|
1917
|
+
|
|
1918
|
+
if resume_requested:
|
|
1919
|
+
if not ctx.get("started"):
|
|
1920
|
+
ctx["started"] = True
|
|
1921
|
+
if ctx.get("paused"):
|
|
1922
|
+
ctx["paused"] = False
|
|
1809
1923
|
restart_count = 0
|
|
1810
1924
|
if restart_path.exists():
|
|
1811
1925
|
try:
|
|
@@ -1814,26 +1928,42 @@ def release_progress(request, pk: int, action: str):
|
|
|
1814
1928
|
restart_count = 0
|
|
1815
1929
|
step_count = ctx.get("step", 0)
|
|
1816
1930
|
step_param = request.GET.get("step")
|
|
1931
|
+
if resume_requested and step_param is None:
|
|
1932
|
+
step_param = str(step_count)
|
|
1817
1933
|
|
|
1818
1934
|
pending_qs = Todo.objects.filter(is_deleted=False, done_on__isnull=True)
|
|
1819
1935
|
pending_items = list(pending_qs)
|
|
1820
|
-
|
|
1821
|
-
if
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1936
|
+
blocking_todos = [
|
|
1937
|
+
todo for todo in pending_items if _todo_blocks_publish(todo, release)
|
|
1938
|
+
]
|
|
1939
|
+
if not blocking_todos:
|
|
1940
|
+
ctx["todos_ack"] = True
|
|
1941
|
+
ctx["todos_ack_auto"] = True
|
|
1942
|
+
elif ack_todos_requested:
|
|
1943
|
+
failures = []
|
|
1944
|
+
for todo in blocking_todos:
|
|
1945
|
+
result = todo.check_on_done_condition()
|
|
1946
|
+
if not result.passed:
|
|
1947
|
+
failures.append((todo, result))
|
|
1948
|
+
if failures:
|
|
1949
|
+
ctx["todos_ack"] = False
|
|
1950
|
+
ctx.pop("todos_ack_auto", None)
|
|
1951
|
+
for todo, result in failures:
|
|
1952
|
+
messages.error(request, _format_condition_failure(todo, result))
|
|
1833
1953
|
else:
|
|
1834
1954
|
ctx["todos_ack"] = True
|
|
1955
|
+
ctx.pop("todos_ack_auto", None)
|
|
1956
|
+
else:
|
|
1957
|
+
if ctx.pop("todos_ack_auto", None):
|
|
1958
|
+
ctx["todos_ack"] = False
|
|
1959
|
+
else:
|
|
1960
|
+
ctx.setdefault("todos_ack", False)
|
|
1835
1961
|
|
|
1836
|
-
if
|
|
1962
|
+
if ctx.get("todos_ack"):
|
|
1963
|
+
ctx.pop("todos_block_logged", None)
|
|
1964
|
+
ctx.pop("todos", None)
|
|
1965
|
+
ctx.pop("todos_required", None)
|
|
1966
|
+
else:
|
|
1837
1967
|
ctx["todos"] = [
|
|
1838
1968
|
{
|
|
1839
1969
|
"id": todo.pk,
|
|
@@ -1841,12 +1971,9 @@ def release_progress(request, pk: int, action: str):
|
|
|
1841
1971
|
"url": todo.url,
|
|
1842
1972
|
"request_details": todo.request_details,
|
|
1843
1973
|
}
|
|
1844
|
-
for todo in
|
|
1974
|
+
for todo in blocking_todos
|
|
1845
1975
|
]
|
|
1846
1976
|
ctx["todos_required"] = True
|
|
1847
|
-
else:
|
|
1848
|
-
ctx.pop("todos", None)
|
|
1849
|
-
ctx.pop("todos_required", None)
|
|
1850
1977
|
|
|
1851
1978
|
log_name = _release_log_name(release.package.name, release.version)
|
|
1852
1979
|
if ctx.get("log") != log_name:
|
|
@@ -1856,6 +1983,8 @@ def release_progress(request, pk: int, action: str):
|
|
|
1856
1983
|
"started": ctx.get("started", False),
|
|
1857
1984
|
}
|
|
1858
1985
|
step_count = 0
|
|
1986
|
+
if not blocking_todos:
|
|
1987
|
+
ctx["todos_ack"] = True
|
|
1859
1988
|
log_path = log_dir / log_name
|
|
1860
1989
|
ctx.setdefault("log", log_name)
|
|
1861
1990
|
ctx.setdefault("paused", False)
|
|
@@ -1932,7 +2061,7 @@ def release_progress(request, pk: int, action: str):
|
|
|
1932
2061
|
if to_run == step_count:
|
|
1933
2062
|
name, func = steps[to_run]
|
|
1934
2063
|
try:
|
|
1935
|
-
func(release, ctx, log_path)
|
|
2064
|
+
func(release, ctx, log_path, user=request.user)
|
|
1936
2065
|
except PendingTodos:
|
|
1937
2066
|
pass
|
|
1938
2067
|
except ApprovalRequired:
|
|
@@ -2049,6 +2178,14 @@ def release_progress(request, pk: int, action: str):
|
|
|
2049
2178
|
)
|
|
2050
2179
|
|
|
2051
2180
|
is_running = ctx.get("started") and not paused and not done and not ctx.get("error")
|
|
2181
|
+
resume_available = (
|
|
2182
|
+
ctx.get("started")
|
|
2183
|
+
and not paused
|
|
2184
|
+
and not done
|
|
2185
|
+
and not ctx.get("error")
|
|
2186
|
+
and step_count < len(steps)
|
|
2187
|
+
and next_step is None
|
|
2188
|
+
)
|
|
2052
2189
|
can_resume = ctx.get("started") and paused and not done and not ctx.get("error")
|
|
2053
2190
|
release_manager_owner = manager.owner_display() if manager else ""
|
|
2054
2191
|
try:
|
|
@@ -2103,9 +2240,11 @@ def release_progress(request, pk: int, action: str):
|
|
|
2103
2240
|
"has_release_manager": bool(manager),
|
|
2104
2241
|
"current_user_admin_url": current_user_admin_url,
|
|
2105
2242
|
"is_running": is_running,
|
|
2243
|
+
"resume_available": resume_available,
|
|
2106
2244
|
"can_resume": can_resume,
|
|
2107
2245
|
"dry_run": dry_run_active,
|
|
2108
2246
|
"dry_run_toggle_enabled": dry_run_toggle_enabled,
|
|
2247
|
+
"warnings": ctx.get("warnings", []),
|
|
2109
2248
|
}
|
|
2110
2249
|
request.session[session_key] = ctx
|
|
2111
2250
|
if done or ctx.get("error"):
|
|
@@ -2303,6 +2442,7 @@ def todo_focus(request, pk: int):
|
|
|
2303
2442
|
"focus_auth": focus_auth,
|
|
2304
2443
|
"next_url": _get_return_url(request),
|
|
2305
2444
|
"done_url": reverse("todo-done", args=[todo.pk]),
|
|
2445
|
+
"delete_url": reverse("todo-delete", args=[todo.pk]),
|
|
2306
2446
|
"snapshot_url": reverse("todo-snapshot", args=[todo.pk]),
|
|
2307
2447
|
}
|
|
2308
2448
|
return render(request, "core/todo_focus.html", context)
|
|
@@ -2321,7 +2461,29 @@ def todo_done(request, pk: int):
|
|
|
2321
2461
|
messages.error(request, _format_condition_failure(todo, result))
|
|
2322
2462
|
return redirect(redirect_to)
|
|
2323
2463
|
todo.done_on = timezone.now()
|
|
2324
|
-
todo.
|
|
2464
|
+
todo.populate_done_metadata(request.user)
|
|
2465
|
+
todo.save(
|
|
2466
|
+
update_fields=[
|
|
2467
|
+
"done_on",
|
|
2468
|
+
"done_node",
|
|
2469
|
+
"done_version",
|
|
2470
|
+
"done_revision",
|
|
2471
|
+
"done_username",
|
|
2472
|
+
]
|
|
2473
|
+
)
|
|
2474
|
+
return redirect(redirect_to)
|
|
2475
|
+
|
|
2476
|
+
|
|
2477
|
+
@staff_member_required
|
|
2478
|
+
@require_POST
|
|
2479
|
+
def todo_delete(request, pk: int):
|
|
2480
|
+
redirect_to = reverse("admin:index")
|
|
2481
|
+
try:
|
|
2482
|
+
todo = Todo.objects.get(pk=pk, is_deleted=False)
|
|
2483
|
+
except Todo.DoesNotExist:
|
|
2484
|
+
return redirect(redirect_to)
|
|
2485
|
+
todo.is_deleted = True
|
|
2486
|
+
todo.save(update_fields=["is_deleted"])
|
|
2325
2487
|
return redirect(redirect_to)
|
|
2326
2488
|
|
|
2327
2489
|
|