arthexis 0.1.16__py3-none-any.whl → 0.1.18__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.18.dist-info}/METADATA +1 -1
- {arthexis-0.1.16.dist-info → arthexis-0.1.18.dist-info}/RECORD +26 -25
- config/middleware.py +47 -1
- config/settings.py +4 -0
- config/urls.py +5 -0
- core/admin.py +69 -9
- core/backends.py +2 -0
- core/changelog.py +66 -5
- core/models.py +88 -7
- core/release.py +55 -2
- core/system.py +1 -1
- core/tasks.py +0 -6
- core/tests.py +131 -0
- core/views.py +112 -24
- ocpp/admin.py +92 -10
- ocpp/consumers.py +63 -19
- ocpp/test_rfid.py +118 -3
- ocpp/tests.py +225 -0
- ocpp/views.py +46 -7
- pages/admin.py +87 -5
- pages/apps.py +3 -0
- pages/site_config.py +137 -0
- pages/tests.py +206 -2
- {arthexis-0.1.16.dist-info → arthexis-0.1.18.dist-info}/WHEEL +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.18.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.18.dist-info}/top_level.txt +0 -0
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
|
|
|
@@ -711,8 +726,46 @@ def publish(
|
|
|
711
726
|
uploaded.append(target.name)
|
|
712
727
|
|
|
713
728
|
tag_name = f"v{version}"
|
|
714
|
-
|
|
715
|
-
|
|
729
|
+
try:
|
|
730
|
+
_run(["git", "tag", tag_name])
|
|
731
|
+
except subprocess.CalledProcessError as exc:
|
|
732
|
+
details = _format_subprocess_error(exc)
|
|
733
|
+
if uploaded:
|
|
734
|
+
uploads = ", ".join(uploaded)
|
|
735
|
+
if details:
|
|
736
|
+
message = (
|
|
737
|
+
f"Upload to {uploads} completed, but creating git tag {tag_name} failed: {details}"
|
|
738
|
+
)
|
|
739
|
+
else:
|
|
740
|
+
message = (
|
|
741
|
+
f"Upload to {uploads} completed, but creating git tag {tag_name} failed."
|
|
742
|
+
)
|
|
743
|
+
followups = [f"Create and push git tag {tag_name} manually once the repository is ready."]
|
|
744
|
+
raise PostPublishWarning(
|
|
745
|
+
message,
|
|
746
|
+
uploaded=uploaded,
|
|
747
|
+
followups=followups,
|
|
748
|
+
) from exc
|
|
749
|
+
raise ReleaseError(
|
|
750
|
+
f"Failed to create git tag {tag_name}: {details or exc}"
|
|
751
|
+
) from exc
|
|
752
|
+
|
|
753
|
+
try:
|
|
754
|
+
_push_tag(tag_name, package)
|
|
755
|
+
except ReleaseError as exc:
|
|
756
|
+
if uploaded:
|
|
757
|
+
uploads = ", ".join(uploaded)
|
|
758
|
+
message = f"Upload to {uploads} completed, but {exc}"
|
|
759
|
+
followups = [
|
|
760
|
+
f"Push git tag {tag_name} to origin after resolving the reported issue."
|
|
761
|
+
]
|
|
762
|
+
warning = PostPublishWarning(
|
|
763
|
+
message,
|
|
764
|
+
uploaded=uploaded,
|
|
765
|
+
followups=followups,
|
|
766
|
+
)
|
|
767
|
+
raise warning from exc
|
|
768
|
+
raise
|
|
716
769
|
return uploaded
|
|
717
770
|
|
|
718
771
|
|
core/system.py
CHANGED
|
@@ -171,7 +171,7 @@ def _regenerate_changelog() -> None:
|
|
|
171
171
|
previous_text = (
|
|
172
172
|
changelog_path.read_text(encoding="utf-8") if changelog_path.exists() else None
|
|
173
173
|
)
|
|
174
|
-
range_spec = changelog_utils.determine_range_spec()
|
|
174
|
+
range_spec = changelog_utils.determine_range_spec(previous_text=previous_text)
|
|
175
175
|
sections = changelog_utils.collect_sections(
|
|
176
176
|
range_spec=range_spec, previous_text=previous_text
|
|
177
177
|
)
|
core/tasks.py
CHANGED
|
@@ -230,12 +230,6 @@ def check_github_updates() -> None:
|
|
|
230
230
|
|
|
231
231
|
subprocess.run(args, cwd=base_dir, check=True)
|
|
232
232
|
|
|
233
|
-
if shutil.which("gway"):
|
|
234
|
-
try:
|
|
235
|
-
subprocess.run(["gway", "upgrade"], check=True)
|
|
236
|
-
except subprocess.CalledProcessError:
|
|
237
|
-
logger.warning("gway upgrade failed; continuing anyway", exc_info=True)
|
|
238
|
-
|
|
239
233
|
service_file = base_dir / "locks/service.lck"
|
|
240
234
|
if service_file.exists():
|
|
241
235
|
service = service_file.read_text().strip()
|
core/tests.py
CHANGED
|
@@ -20,6 +20,7 @@ import types
|
|
|
20
20
|
from glob import glob
|
|
21
21
|
from datetime import datetime, timedelta, timezone as datetime_timezone
|
|
22
22
|
import tempfile
|
|
23
|
+
from io import StringIO
|
|
23
24
|
from urllib.parse import quote
|
|
24
25
|
|
|
25
26
|
from django.utils import timezone
|
|
@@ -57,6 +58,7 @@ from nodes.models import ContentSample
|
|
|
57
58
|
|
|
58
59
|
from django.core.exceptions import ValidationError
|
|
59
60
|
from django.core.management import call_command
|
|
61
|
+
from django.core.management.base import CommandError
|
|
60
62
|
from django.db import IntegrityError
|
|
61
63
|
from .backends import LocalhostAdminBackend
|
|
62
64
|
from core.views import (
|
|
@@ -548,6 +550,15 @@ class RFIDValidationTests(TestCase):
|
|
|
548
550
|
tag = RFID.objects.create(rfid="DEADBEEF10")
|
|
549
551
|
self.assertEqual(tag.rfid, "DEADBEEF10")
|
|
550
552
|
|
|
553
|
+
def test_reversed_uid_updates_with_rfid(self):
|
|
554
|
+
tag = RFID.objects.create(rfid="A1B2C3D4")
|
|
555
|
+
self.assertEqual(tag.reversed_uid, "D4C3B2A1")
|
|
556
|
+
|
|
557
|
+
tag.rfid = "112233"
|
|
558
|
+
tag.save(update_fields=["rfid"])
|
|
559
|
+
tag.refresh_from_db()
|
|
560
|
+
self.assertEqual(tag.reversed_uid, "332211")
|
|
561
|
+
|
|
551
562
|
def test_find_user_by_rfid(self):
|
|
552
563
|
user = User.objects.create_user(username="finder", password="pwd")
|
|
553
564
|
acc = EnergyAccount.objects.create(user=user, name="FINDER")
|
|
@@ -983,6 +994,40 @@ class RFIDImportExportCommandTests(TestCase):
|
|
|
983
994
|
self.assertTrue(tag.energy_accounts.filter(pk=account.pk).exists())
|
|
984
995
|
|
|
985
996
|
|
|
997
|
+
class CheckRFIDCommandTests(TestCase):
|
|
998
|
+
def test_successful_validation_outputs_json(self):
|
|
999
|
+
out = StringIO()
|
|
1000
|
+
|
|
1001
|
+
call_command("check_rfid", "abcd1234", stdout=out)
|
|
1002
|
+
|
|
1003
|
+
payload = json.loads(out.getvalue())
|
|
1004
|
+
self.assertEqual(payload["rfid"], "ABCD1234")
|
|
1005
|
+
self.assertTrue(payload["created"])
|
|
1006
|
+
self.assertTrue(RFID.objects.filter(rfid="ABCD1234").exists())
|
|
1007
|
+
|
|
1008
|
+
def test_invalid_value_raises_error(self):
|
|
1009
|
+
with self.assertRaises(CommandError):
|
|
1010
|
+
call_command("check_rfid", "invalid!")
|
|
1011
|
+
|
|
1012
|
+
def test_kind_option_updates_existing_tag(self):
|
|
1013
|
+
tag = RFID.objects.create(rfid="EXISTING", allowed=False, kind=RFID.CLASSIC)
|
|
1014
|
+
out = StringIO()
|
|
1015
|
+
|
|
1016
|
+
call_command(
|
|
1017
|
+
"check_rfid",
|
|
1018
|
+
"existing",
|
|
1019
|
+
"--kind",
|
|
1020
|
+
RFID.NTAG215,
|
|
1021
|
+
stdout=out,
|
|
1022
|
+
)
|
|
1023
|
+
|
|
1024
|
+
payload = json.loads(out.getvalue())
|
|
1025
|
+
tag.refresh_from_db()
|
|
1026
|
+
self.assertFalse(payload["created"])
|
|
1027
|
+
self.assertEqual(payload["kind"], RFID.NTAG215)
|
|
1028
|
+
self.assertEqual(tag.kind, RFID.NTAG215)
|
|
1029
|
+
|
|
1030
|
+
|
|
986
1031
|
class RFIDKeyVerificationFlagTests(TestCase):
|
|
987
1032
|
def test_flags_reset_on_key_change(self):
|
|
988
1033
|
tag = RFID.objects.create(
|
|
@@ -1037,6 +1082,44 @@ class ReleaseProcessTests(TestCase):
|
|
|
1037
1082
|
)
|
|
1038
1083
|
sync_main.assert_called_once_with(Path("rel.log"))
|
|
1039
1084
|
|
|
1085
|
+
def test_step_check_todos_logs_instruction_when_pending(self):
|
|
1086
|
+
log_path = Path("rel.log")
|
|
1087
|
+
log_path.unlink(missing_ok=True)
|
|
1088
|
+
Todo.objects.create(request="Review checklist")
|
|
1089
|
+
ctx: dict[str, object] = {}
|
|
1090
|
+
|
|
1091
|
+
try:
|
|
1092
|
+
with self.assertRaises(core_views.PendingTodos):
|
|
1093
|
+
core_views._step_check_todos(self.release, ctx, log_path)
|
|
1094
|
+
|
|
1095
|
+
contents = log_path.read_text(encoding="utf-8")
|
|
1096
|
+
message = "Release checklist requires acknowledgment before continuing."
|
|
1097
|
+
self.assertIn(message, contents)
|
|
1098
|
+
self.assertIn("Review outstanding TODO items", contents)
|
|
1099
|
+
|
|
1100
|
+
with self.assertRaises(core_views.PendingTodos):
|
|
1101
|
+
core_views._step_check_todos(self.release, ctx, log_path)
|
|
1102
|
+
|
|
1103
|
+
contents = log_path.read_text(encoding="utf-8")
|
|
1104
|
+
self.assertEqual(contents.count(message), 1)
|
|
1105
|
+
finally:
|
|
1106
|
+
log_path.unlink(missing_ok=True)
|
|
1107
|
+
|
|
1108
|
+
def test_step_check_todos_auto_ack_when_no_pending(self):
|
|
1109
|
+
log_path = Path("rel.log")
|
|
1110
|
+
log_path.unlink(missing_ok=True)
|
|
1111
|
+
ctx: dict[str, object] = {}
|
|
1112
|
+
|
|
1113
|
+
try:
|
|
1114
|
+
with mock.patch("core.views._refresh_changelog_once"):
|
|
1115
|
+
core_views._step_check_todos(self.release, ctx, log_path)
|
|
1116
|
+
finally:
|
|
1117
|
+
log_path.unlink(missing_ok=True)
|
|
1118
|
+
|
|
1119
|
+
self.assertTrue(ctx.get("todos_ack"))
|
|
1120
|
+
self.assertNotIn("todos_required", ctx)
|
|
1121
|
+
self.assertIsNone(ctx.get("todos"))
|
|
1122
|
+
|
|
1040
1123
|
@mock.patch("core.views._sync_with_origin_main")
|
|
1041
1124
|
@mock.patch("core.views.release_utils._git_clean", return_value=True)
|
|
1042
1125
|
@mock.patch("core.views.release_utils.network_available", return_value=False)
|
|
@@ -1527,6 +1610,54 @@ class ReleaseProcessTests(TestCase):
|
|
|
1527
1610
|
ctx = session.get(f"release_publish_{self.release.pk}")
|
|
1528
1611
|
self.assertTrue(ctx.get("dry_run"))
|
|
1529
1612
|
|
|
1613
|
+
def test_resume_button_shown_when_credentials_missing(self):
|
|
1614
|
+
user = User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
1615
|
+
url = reverse("release-progress", args=[self.release.pk, "publish"])
|
|
1616
|
+
self.client.force_login(user)
|
|
1617
|
+
|
|
1618
|
+
self.client.get(f"{url}?start=1")
|
|
1619
|
+
|
|
1620
|
+
session = self.client.session
|
|
1621
|
+
ctx = session.get(f"release_publish_{self.release.pk}") or {}
|
|
1622
|
+
ctx.update({"step": 7, "started": True, "paused": False})
|
|
1623
|
+
session[f"release_publish_{self.release.pk}"] = ctx
|
|
1624
|
+
session.save()
|
|
1625
|
+
|
|
1626
|
+
response = self.client.get(f"{url}?step=7")
|
|
1627
|
+
self.assertEqual(response.status_code, 200)
|
|
1628
|
+
context = response.context
|
|
1629
|
+
if isinstance(context, list):
|
|
1630
|
+
context = context[-1]
|
|
1631
|
+
self.assertTrue(context["resume_available"])
|
|
1632
|
+
self.assertIn(b"Resume Publish", response.content)
|
|
1633
|
+
|
|
1634
|
+
def test_resume_without_step_parameter_defaults_to_current_progress(self):
|
|
1635
|
+
run: list[str] = []
|
|
1636
|
+
|
|
1637
|
+
def step_fn(release, ctx, log_path):
|
|
1638
|
+
run.append("step")
|
|
1639
|
+
|
|
1640
|
+
steps = [("Only step", step_fn)]
|
|
1641
|
+
user = User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
1642
|
+
url = reverse("release-progress", args=[self.release.pk, "publish"])
|
|
1643
|
+
with mock.patch("core.views.PUBLISH_STEPS", steps):
|
|
1644
|
+
self.client.force_login(user)
|
|
1645
|
+
session = self.client.session
|
|
1646
|
+
session[f"release_publish_{self.release.pk}"] = {
|
|
1647
|
+
"step": 0,
|
|
1648
|
+
"started": True,
|
|
1649
|
+
"paused": False,
|
|
1650
|
+
}
|
|
1651
|
+
session.save()
|
|
1652
|
+
|
|
1653
|
+
response = self.client.get(f"{url}?resume=1")
|
|
1654
|
+
self.assertEqual(response.status_code, 200)
|
|
1655
|
+
self.assertEqual(run, ["step"])
|
|
1656
|
+
|
|
1657
|
+
session = self.client.session
|
|
1658
|
+
ctx = session.get(f"release_publish_{self.release.pk}")
|
|
1659
|
+
self.assertEqual(ctx.get("step"), 1)
|
|
1660
|
+
|
|
1530
1661
|
def test_new_todo_does_not_reset_pending_flow(self):
|
|
1531
1662
|
user = User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
1532
1663
|
url = reverse("release-progress", args=[self.release.pk, "publish"])
|
core/views.py
CHANGED
|
@@ -43,6 +43,7 @@ logger = logging.getLogger(__name__)
|
|
|
43
43
|
PYPI_REQUEST_TIMEOUT = 10
|
|
44
44
|
|
|
45
45
|
from . import changelog as changelog_utils
|
|
46
|
+
from . import temp_passwords
|
|
46
47
|
from .models import OdooProfile, Product, EnergyAccount, PackageRelease, Todo
|
|
47
48
|
from .models import RFID
|
|
48
49
|
|
|
@@ -336,6 +337,35 @@ def odoo_quote_report(request):
|
|
|
336
337
|
return TemplateResponse(request, "admin/core/odoo_quote_report.html", context)
|
|
337
338
|
|
|
338
339
|
|
|
340
|
+
@staff_member_required
|
|
341
|
+
@require_GET
|
|
342
|
+
def request_temp_password(request):
|
|
343
|
+
"""Generate a temporary password for the authenticated staff member."""
|
|
344
|
+
|
|
345
|
+
user = request.user
|
|
346
|
+
username = user.get_username()
|
|
347
|
+
password = temp_passwords.generate_password()
|
|
348
|
+
entry = temp_passwords.store_temp_password(
|
|
349
|
+
username,
|
|
350
|
+
password,
|
|
351
|
+
allow_change=True,
|
|
352
|
+
)
|
|
353
|
+
context = {
|
|
354
|
+
**admin_site.each_context(request),
|
|
355
|
+
"title": _("Temporary password"),
|
|
356
|
+
"username": username,
|
|
357
|
+
"password": password,
|
|
358
|
+
"expires_at": timezone.localtime(entry.expires_at),
|
|
359
|
+
"allow_change": entry.allow_change,
|
|
360
|
+
"return_url": reverse("admin:password_change"),
|
|
361
|
+
}
|
|
362
|
+
return TemplateResponse(
|
|
363
|
+
request,
|
|
364
|
+
"admin/core/request_temp_password.html",
|
|
365
|
+
context,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
339
369
|
@require_GET
|
|
340
370
|
def version_info(request):
|
|
341
371
|
"""Return the running application version and Git revision."""
|
|
@@ -656,8 +686,8 @@ def _should_use_python_changelog(exc: OSError) -> bool:
|
|
|
656
686
|
def _generate_changelog_with_python(log_path: Path) -> None:
|
|
657
687
|
_append_log(log_path, "Falling back to Python changelog generator")
|
|
658
688
|
changelog_path = Path("CHANGELOG.rst")
|
|
659
|
-
range_spec = changelog_utils.determine_range_spec()
|
|
660
689
|
previous = changelog_path.read_text(encoding="utf-8") if changelog_path.exists() else None
|
|
690
|
+
range_spec = changelog_utils.determine_range_spec(previous_text=previous)
|
|
661
691
|
sections = changelog_utils.collect_sections(range_spec=range_spec, previous_text=previous)
|
|
662
692
|
content = changelog_utils.render_changelog(sections)
|
|
663
693
|
if not content.endswith("\n"):
|
|
@@ -897,7 +927,18 @@ def _step_check_todos(release, ctx, log_path: Path) -> None:
|
|
|
897
927
|
pending_values = list(
|
|
898
928
|
pending_qs.values("id", "request", "url", "request_details")
|
|
899
929
|
)
|
|
930
|
+
if not pending_values:
|
|
931
|
+
ctx["todos_ack"] = True
|
|
932
|
+
|
|
900
933
|
if not ctx.get("todos_ack"):
|
|
934
|
+
if not ctx.get("todos_block_logged"):
|
|
935
|
+
_append_log(
|
|
936
|
+
log_path,
|
|
937
|
+
"Release checklist requires acknowledgment before continuing. "
|
|
938
|
+
"Review outstanding TODO items and confirm the checklist; "
|
|
939
|
+
"publishing will resume automatically afterward.",
|
|
940
|
+
)
|
|
941
|
+
ctx["todos_block_logged"] = True
|
|
901
942
|
ctx["todos"] = pending_values
|
|
902
943
|
ctx["todos_required"] = True
|
|
903
944
|
raise PendingTodos()
|
|
@@ -1451,12 +1492,29 @@ def _step_publish(release, ctx, log_path: Path) -> None:
|
|
|
1451
1492
|
)
|
|
1452
1493
|
else:
|
|
1453
1494
|
_append_log(log_path, "Uploading distribution")
|
|
1454
|
-
release_utils.
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1495
|
+
publish_warning: release_utils.PostPublishWarning | None = None
|
|
1496
|
+
try:
|
|
1497
|
+
release_utils.publish(
|
|
1498
|
+
package=release.to_package(),
|
|
1499
|
+
version=release.version,
|
|
1500
|
+
creds=release.to_credentials(),
|
|
1501
|
+
repositories=targets,
|
|
1502
|
+
)
|
|
1503
|
+
except release_utils.PostPublishWarning as warning:
|
|
1504
|
+
publish_warning = warning
|
|
1505
|
+
|
|
1506
|
+
if publish_warning is not None:
|
|
1507
|
+
message = str(publish_warning)
|
|
1508
|
+
followups = _dedupe_preserve_order(publish_warning.followups)
|
|
1509
|
+
warning_entries = ctx.setdefault("warnings", [])
|
|
1510
|
+
if not any(entry.get("message") == message for entry in warning_entries):
|
|
1511
|
+
entry: dict[str, object] = {"message": message}
|
|
1512
|
+
if followups:
|
|
1513
|
+
entry["followups"] = followups
|
|
1514
|
+
warning_entries.append(entry)
|
|
1515
|
+
_append_log(log_path, message)
|
|
1516
|
+
for note in followups:
|
|
1517
|
+
_append_log(log_path, f"Follow-up: {note}")
|
|
1460
1518
|
release.pypi_url = (
|
|
1461
1519
|
f"https://pypi.org/project/{release.package.name}/{release.version}/"
|
|
1462
1520
|
)
|
|
@@ -1804,8 +1862,16 @@ def release_progress(request, pk: int, action: str):
|
|
|
1804
1862
|
ctx["release_approval"] = "approved"
|
|
1805
1863
|
if request.GET.get("reject"):
|
|
1806
1864
|
ctx["release_approval"] = "rejected"
|
|
1865
|
+
resume_requested = bool(request.GET.get("resume"))
|
|
1866
|
+
|
|
1807
1867
|
if request.GET.get("pause") and ctx.get("started"):
|
|
1808
1868
|
ctx["paused"] = True
|
|
1869
|
+
|
|
1870
|
+
if resume_requested:
|
|
1871
|
+
if not ctx.get("started"):
|
|
1872
|
+
ctx["started"] = True
|
|
1873
|
+
if ctx.get("paused"):
|
|
1874
|
+
ctx["paused"] = False
|
|
1809
1875
|
restart_count = 0
|
|
1810
1876
|
if restart_path.exists():
|
|
1811
1877
|
try:
|
|
@@ -1814,26 +1880,39 @@ def release_progress(request, pk: int, action: str):
|
|
|
1814
1880
|
restart_count = 0
|
|
1815
1881
|
step_count = ctx.get("step", 0)
|
|
1816
1882
|
step_param = request.GET.get("step")
|
|
1883
|
+
if resume_requested and step_param is None:
|
|
1884
|
+
step_param = str(step_count)
|
|
1817
1885
|
|
|
1818
1886
|
pending_qs = Todo.objects.filter(is_deleted=False, done_on__isnull=True)
|
|
1819
1887
|
pending_items = list(pending_qs)
|
|
1820
|
-
if
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
if
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1888
|
+
if not pending_items:
|
|
1889
|
+
ctx["todos_ack"] = True
|
|
1890
|
+
ctx["todos_ack_auto"] = True
|
|
1891
|
+
elif ack_todos_requested:
|
|
1892
|
+
failures = []
|
|
1893
|
+
for todo in pending_items:
|
|
1894
|
+
result = todo.check_on_done_condition()
|
|
1895
|
+
if not result.passed:
|
|
1896
|
+
failures.append((todo, result))
|
|
1897
|
+
if failures:
|
|
1898
|
+
ctx["todos_ack"] = False
|
|
1899
|
+
ctx.pop("todos_ack_auto", None)
|
|
1900
|
+
for todo, result in failures:
|
|
1901
|
+
messages.error(request, _format_condition_failure(todo, result))
|
|
1833
1902
|
else:
|
|
1834
1903
|
ctx["todos_ack"] = True
|
|
1904
|
+
ctx.pop("todos_ack_auto", None)
|
|
1905
|
+
else:
|
|
1906
|
+
if ctx.pop("todos_ack_auto", None):
|
|
1907
|
+
ctx["todos_ack"] = False
|
|
1908
|
+
else:
|
|
1909
|
+
ctx.setdefault("todos_ack", False)
|
|
1835
1910
|
|
|
1836
|
-
if
|
|
1911
|
+
if ctx.get("todos_ack"):
|
|
1912
|
+
ctx.pop("todos_block_logged", None)
|
|
1913
|
+
ctx.pop("todos", None)
|
|
1914
|
+
ctx.pop("todos_required", None)
|
|
1915
|
+
else:
|
|
1837
1916
|
ctx["todos"] = [
|
|
1838
1917
|
{
|
|
1839
1918
|
"id": todo.pk,
|
|
@@ -1844,9 +1923,6 @@ def release_progress(request, pk: int, action: str):
|
|
|
1844
1923
|
for todo in pending_items
|
|
1845
1924
|
]
|
|
1846
1925
|
ctx["todos_required"] = True
|
|
1847
|
-
else:
|
|
1848
|
-
ctx.pop("todos", None)
|
|
1849
|
-
ctx.pop("todos_required", None)
|
|
1850
1926
|
|
|
1851
1927
|
log_name = _release_log_name(release.package.name, release.version)
|
|
1852
1928
|
if ctx.get("log") != log_name:
|
|
@@ -1856,6 +1932,8 @@ def release_progress(request, pk: int, action: str):
|
|
|
1856
1932
|
"started": ctx.get("started", False),
|
|
1857
1933
|
}
|
|
1858
1934
|
step_count = 0
|
|
1935
|
+
if not pending_items:
|
|
1936
|
+
ctx["todos_ack"] = True
|
|
1859
1937
|
log_path = log_dir / log_name
|
|
1860
1938
|
ctx.setdefault("log", log_name)
|
|
1861
1939
|
ctx.setdefault("paused", False)
|
|
@@ -2049,6 +2127,14 @@ def release_progress(request, pk: int, action: str):
|
|
|
2049
2127
|
)
|
|
2050
2128
|
|
|
2051
2129
|
is_running = ctx.get("started") and not paused and not done and not ctx.get("error")
|
|
2130
|
+
resume_available = (
|
|
2131
|
+
ctx.get("started")
|
|
2132
|
+
and not paused
|
|
2133
|
+
and not done
|
|
2134
|
+
and not ctx.get("error")
|
|
2135
|
+
and step_count < len(steps)
|
|
2136
|
+
and next_step is None
|
|
2137
|
+
)
|
|
2052
2138
|
can_resume = ctx.get("started") and paused and not done and not ctx.get("error")
|
|
2053
2139
|
release_manager_owner = manager.owner_display() if manager else ""
|
|
2054
2140
|
try:
|
|
@@ -2103,9 +2189,11 @@ def release_progress(request, pk: int, action: str):
|
|
|
2103
2189
|
"has_release_manager": bool(manager),
|
|
2104
2190
|
"current_user_admin_url": current_user_admin_url,
|
|
2105
2191
|
"is_running": is_running,
|
|
2192
|
+
"resume_available": resume_available,
|
|
2106
2193
|
"can_resume": can_resume,
|
|
2107
2194
|
"dry_run": dry_run_active,
|
|
2108
2195
|
"dry_run_toggle_enabled": dry_run_toggle_enabled,
|
|
2196
|
+
"warnings": ctx.get("warnings", []),
|
|
2109
2197
|
}
|
|
2110
2198
|
request.session[session_key] = ctx
|
|
2111
2199
|
if done or ctx.get("error"):
|
ocpp/admin.py
CHANGED
|
@@ -29,6 +29,7 @@ from .transactions_io import (
|
|
|
29
29
|
import_transactions as import_transactions_data,
|
|
30
30
|
)
|
|
31
31
|
from .status_display import STATUS_BADGE_MAP, ERROR_OK_VALUES
|
|
32
|
+
from .views import _charger_state, _live_sessions
|
|
32
33
|
from core.admin import SaveBeforeChangeAction
|
|
33
34
|
from core.user_data import EntityModelAdmin
|
|
34
35
|
|
|
@@ -253,6 +254,8 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
253
254
|
actions = [
|
|
254
255
|
"purge_data",
|
|
255
256
|
"fetch_cp_configuration",
|
|
257
|
+
"toggle_rfid_authentication",
|
|
258
|
+
"recheck_charger_status",
|
|
256
259
|
"change_availability_operative",
|
|
257
260
|
"change_availability_inoperative",
|
|
258
261
|
"set_availability_state_operative",
|
|
@@ -317,16 +320,14 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
317
320
|
"charger-status-connector",
|
|
318
321
|
args=[obj.charger_id, obj.connector_slug],
|
|
319
322
|
)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
)
|
|
328
|
-
label = STATUS_BADGE_MAP["charging"][0]
|
|
329
|
-
return format_html('<a href="{}" target="_blank">{}</a>', url, label)
|
|
323
|
+
tx_obj = store.get_transaction(obj.charger_id, obj.connector_id)
|
|
324
|
+
state, _ = _charger_state(
|
|
325
|
+
obj,
|
|
326
|
+
tx_obj
|
|
327
|
+
if obj.connector_id is not None
|
|
328
|
+
else (_live_sessions(obj) or None),
|
|
329
|
+
)
|
|
330
|
+
return format_html('<a href="{}" target="_blank">{}</a>', url, state)
|
|
330
331
|
|
|
331
332
|
status_link.short_description = "Status"
|
|
332
333
|
|
|
@@ -359,6 +360,63 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
359
360
|
|
|
360
361
|
purge_data.short_description = "Purge data"
|
|
361
362
|
|
|
363
|
+
@admin.action(description="Re-check Charger Status")
|
|
364
|
+
def recheck_charger_status(self, request, queryset):
|
|
365
|
+
requested = 0
|
|
366
|
+
for charger in queryset:
|
|
367
|
+
connector_value = charger.connector_id
|
|
368
|
+
ws = store.get_connection(charger.charger_id, connector_value)
|
|
369
|
+
if ws is None:
|
|
370
|
+
self.message_user(
|
|
371
|
+
request,
|
|
372
|
+
f"{charger}: no active connection",
|
|
373
|
+
level=messages.ERROR,
|
|
374
|
+
)
|
|
375
|
+
continue
|
|
376
|
+
payload: dict[str, object] = {"requestedMessage": "StatusNotification"}
|
|
377
|
+
trigger_connector: int | None = None
|
|
378
|
+
if connector_value is not None:
|
|
379
|
+
payload["connectorId"] = connector_value
|
|
380
|
+
trigger_connector = connector_value
|
|
381
|
+
message_id = uuid.uuid4().hex
|
|
382
|
+
msg = json.dumps([2, message_id, "TriggerMessage", payload])
|
|
383
|
+
try:
|
|
384
|
+
async_to_sync(ws.send)(msg)
|
|
385
|
+
except Exception as exc: # pragma: no cover - network error
|
|
386
|
+
self.message_user(
|
|
387
|
+
request,
|
|
388
|
+
f"{charger}: failed to send TriggerMessage ({exc})",
|
|
389
|
+
level=messages.ERROR,
|
|
390
|
+
)
|
|
391
|
+
continue
|
|
392
|
+
log_key = store.identity_key(charger.charger_id, connector_value)
|
|
393
|
+
store.add_log(log_key, f"< {msg}", log_type="charger")
|
|
394
|
+
store.register_pending_call(
|
|
395
|
+
message_id,
|
|
396
|
+
{
|
|
397
|
+
"action": "TriggerMessage",
|
|
398
|
+
"charger_id": charger.charger_id,
|
|
399
|
+
"connector_id": connector_value,
|
|
400
|
+
"log_key": log_key,
|
|
401
|
+
"trigger_target": "StatusNotification",
|
|
402
|
+
"trigger_connector": trigger_connector,
|
|
403
|
+
"requested_at": timezone.now(),
|
|
404
|
+
},
|
|
405
|
+
)
|
|
406
|
+
store.schedule_call_timeout(
|
|
407
|
+
message_id,
|
|
408
|
+
timeout=5.0,
|
|
409
|
+
action="TriggerMessage",
|
|
410
|
+
log_key=log_key,
|
|
411
|
+
message="TriggerMessage StatusNotification timed out",
|
|
412
|
+
)
|
|
413
|
+
requested += 1
|
|
414
|
+
if requested:
|
|
415
|
+
self.message_user(
|
|
416
|
+
request,
|
|
417
|
+
f"Requested status update from {requested} charger(s)",
|
|
418
|
+
)
|
|
419
|
+
|
|
362
420
|
@admin.action(description="Fetch CP configuration")
|
|
363
421
|
def fetch_cp_configuration(self, request, queryset):
|
|
364
422
|
fetched = 0
|
|
@@ -413,6 +471,30 @@ class ChargerAdmin(LogViewAdminMixin, EntityModelAdmin):
|
|
|
413
471
|
f"Requested configuration from {fetched} charger(s)",
|
|
414
472
|
)
|
|
415
473
|
|
|
474
|
+
@admin.action(description="Toggle RFID Authentication")
|
|
475
|
+
def toggle_rfid_authentication(self, request, queryset):
|
|
476
|
+
enabled = 0
|
|
477
|
+
disabled = 0
|
|
478
|
+
for charger in queryset:
|
|
479
|
+
new_value = not charger.require_rfid
|
|
480
|
+
Charger.objects.filter(pk=charger.pk).update(require_rfid=new_value)
|
|
481
|
+
charger.require_rfid = new_value
|
|
482
|
+
if new_value:
|
|
483
|
+
enabled += 1
|
|
484
|
+
else:
|
|
485
|
+
disabled += 1
|
|
486
|
+
if enabled or disabled:
|
|
487
|
+
changes = []
|
|
488
|
+
if enabled:
|
|
489
|
+
changes.append(f"enabled for {enabled} charger(s)")
|
|
490
|
+
if disabled:
|
|
491
|
+
changes.append(f"disabled for {disabled} charger(s)")
|
|
492
|
+
summary = "; ".join(changes)
|
|
493
|
+
self.message_user(
|
|
494
|
+
request,
|
|
495
|
+
f"Updated RFID authentication: {summary}",
|
|
496
|
+
)
|
|
497
|
+
|
|
416
498
|
def _dispatch_change_availability(self, request, queryset, availability_type: str):
|
|
417
499
|
sent = 0
|
|
418
500
|
for charger in queryset:
|