arthexis 0.1.16__py3-none-any.whl → 0.1.17__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.17.dist-info}/METADATA +1 -1
- {arthexis-0.1.16.dist-info → arthexis-0.1.17.dist-info}/RECORD +20 -20
- config/settings.py +3 -0
- core/admin.py +68 -8
- core/backends.py +2 -0
- core/changelog.py +66 -5
- core/models.py +57 -6
- core/release.py +55 -2
- core/system.py +1 -1
- core/tasks.py +0 -6
- core/tests.py +122 -0
- core/views.py +58 -7
- ocpp/admin.py +92 -10
- ocpp/test_rfid.py +48 -3
- ocpp/tests.py +132 -0
- ocpp/views.py +23 -5
- pages/tests.py +26 -1
- {arthexis-0.1.16.dist-info → arthexis-0.1.17.dist-info}/WHEEL +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.17.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.16.dist-info → arthexis-0.1.17.dist-info}/top_level.txt +0 -0
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 (
|
|
@@ -983,6 +985,40 @@ class RFIDImportExportCommandTests(TestCase):
|
|
|
983
985
|
self.assertTrue(tag.energy_accounts.filter(pk=account.pk).exists())
|
|
984
986
|
|
|
985
987
|
|
|
988
|
+
class CheckRFIDCommandTests(TestCase):
|
|
989
|
+
def test_successful_validation_outputs_json(self):
|
|
990
|
+
out = StringIO()
|
|
991
|
+
|
|
992
|
+
call_command("check_rfid", "abcd1234", stdout=out)
|
|
993
|
+
|
|
994
|
+
payload = json.loads(out.getvalue())
|
|
995
|
+
self.assertEqual(payload["rfid"], "ABCD1234")
|
|
996
|
+
self.assertTrue(payload["created"])
|
|
997
|
+
self.assertTrue(RFID.objects.filter(rfid="ABCD1234").exists())
|
|
998
|
+
|
|
999
|
+
def test_invalid_value_raises_error(self):
|
|
1000
|
+
with self.assertRaises(CommandError):
|
|
1001
|
+
call_command("check_rfid", "invalid!")
|
|
1002
|
+
|
|
1003
|
+
def test_kind_option_updates_existing_tag(self):
|
|
1004
|
+
tag = RFID.objects.create(rfid="EXISTING", allowed=False, kind=RFID.CLASSIC)
|
|
1005
|
+
out = StringIO()
|
|
1006
|
+
|
|
1007
|
+
call_command(
|
|
1008
|
+
"check_rfid",
|
|
1009
|
+
"existing",
|
|
1010
|
+
"--kind",
|
|
1011
|
+
RFID.NTAG215,
|
|
1012
|
+
stdout=out,
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
payload = json.loads(out.getvalue())
|
|
1016
|
+
tag.refresh_from_db()
|
|
1017
|
+
self.assertFalse(payload["created"])
|
|
1018
|
+
self.assertEqual(payload["kind"], RFID.NTAG215)
|
|
1019
|
+
self.assertEqual(tag.kind, RFID.NTAG215)
|
|
1020
|
+
|
|
1021
|
+
|
|
986
1022
|
class RFIDKeyVerificationFlagTests(TestCase):
|
|
987
1023
|
def test_flags_reset_on_key_change(self):
|
|
988
1024
|
tag = RFID.objects.create(
|
|
@@ -1037,6 +1073,44 @@ class ReleaseProcessTests(TestCase):
|
|
|
1037
1073
|
)
|
|
1038
1074
|
sync_main.assert_called_once_with(Path("rel.log"))
|
|
1039
1075
|
|
|
1076
|
+
def test_step_check_todos_logs_instruction_when_pending(self):
|
|
1077
|
+
log_path = Path("rel.log")
|
|
1078
|
+
log_path.unlink(missing_ok=True)
|
|
1079
|
+
Todo.objects.create(request="Review checklist")
|
|
1080
|
+
ctx: dict[str, object] = {}
|
|
1081
|
+
|
|
1082
|
+
try:
|
|
1083
|
+
with self.assertRaises(core_views.PendingTodos):
|
|
1084
|
+
core_views._step_check_todos(self.release, ctx, log_path)
|
|
1085
|
+
|
|
1086
|
+
contents = log_path.read_text(encoding="utf-8")
|
|
1087
|
+
message = "Release checklist requires acknowledgment before continuing."
|
|
1088
|
+
self.assertIn(message, contents)
|
|
1089
|
+
self.assertIn("Review outstanding TODO items", contents)
|
|
1090
|
+
|
|
1091
|
+
with self.assertRaises(core_views.PendingTodos):
|
|
1092
|
+
core_views._step_check_todos(self.release, ctx, log_path)
|
|
1093
|
+
|
|
1094
|
+
contents = log_path.read_text(encoding="utf-8")
|
|
1095
|
+
self.assertEqual(contents.count(message), 1)
|
|
1096
|
+
finally:
|
|
1097
|
+
log_path.unlink(missing_ok=True)
|
|
1098
|
+
|
|
1099
|
+
def test_step_check_todos_auto_ack_when_no_pending(self):
|
|
1100
|
+
log_path = Path("rel.log")
|
|
1101
|
+
log_path.unlink(missing_ok=True)
|
|
1102
|
+
ctx: dict[str, object] = {}
|
|
1103
|
+
|
|
1104
|
+
try:
|
|
1105
|
+
with mock.patch("core.views._refresh_changelog_once"):
|
|
1106
|
+
core_views._step_check_todos(self.release, ctx, log_path)
|
|
1107
|
+
finally:
|
|
1108
|
+
log_path.unlink(missing_ok=True)
|
|
1109
|
+
|
|
1110
|
+
self.assertTrue(ctx.get("todos_ack"))
|
|
1111
|
+
self.assertNotIn("todos_required", ctx)
|
|
1112
|
+
self.assertIsNone(ctx.get("todos"))
|
|
1113
|
+
|
|
1040
1114
|
@mock.patch("core.views._sync_with_origin_main")
|
|
1041
1115
|
@mock.patch("core.views.release_utils._git_clean", return_value=True)
|
|
1042
1116
|
@mock.patch("core.views.release_utils.network_available", return_value=False)
|
|
@@ -1527,6 +1601,54 @@ class ReleaseProcessTests(TestCase):
|
|
|
1527
1601
|
ctx = session.get(f"release_publish_{self.release.pk}")
|
|
1528
1602
|
self.assertTrue(ctx.get("dry_run"))
|
|
1529
1603
|
|
|
1604
|
+
def test_resume_button_shown_when_credentials_missing(self):
|
|
1605
|
+
user = User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
1606
|
+
url = reverse("release-progress", args=[self.release.pk, "publish"])
|
|
1607
|
+
self.client.force_login(user)
|
|
1608
|
+
|
|
1609
|
+
self.client.get(f"{url}?start=1")
|
|
1610
|
+
|
|
1611
|
+
session = self.client.session
|
|
1612
|
+
ctx = session.get(f"release_publish_{self.release.pk}") or {}
|
|
1613
|
+
ctx.update({"step": 7, "started": True, "paused": False})
|
|
1614
|
+
session[f"release_publish_{self.release.pk}"] = ctx
|
|
1615
|
+
session.save()
|
|
1616
|
+
|
|
1617
|
+
response = self.client.get(f"{url}?step=7")
|
|
1618
|
+
self.assertEqual(response.status_code, 200)
|
|
1619
|
+
context = response.context
|
|
1620
|
+
if isinstance(context, list):
|
|
1621
|
+
context = context[-1]
|
|
1622
|
+
self.assertTrue(context["resume_available"])
|
|
1623
|
+
self.assertIn(b"Resume Publish", response.content)
|
|
1624
|
+
|
|
1625
|
+
def test_resume_without_step_parameter_defaults_to_current_progress(self):
|
|
1626
|
+
run: list[str] = []
|
|
1627
|
+
|
|
1628
|
+
def step_fn(release, ctx, log_path):
|
|
1629
|
+
run.append("step")
|
|
1630
|
+
|
|
1631
|
+
steps = [("Only step", step_fn)]
|
|
1632
|
+
user = User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
1633
|
+
url = reverse("release-progress", args=[self.release.pk, "publish"])
|
|
1634
|
+
with mock.patch("core.views.PUBLISH_STEPS", steps):
|
|
1635
|
+
self.client.force_login(user)
|
|
1636
|
+
session = self.client.session
|
|
1637
|
+
session[f"release_publish_{self.release.pk}"] = {
|
|
1638
|
+
"step": 0,
|
|
1639
|
+
"started": True,
|
|
1640
|
+
"paused": False,
|
|
1641
|
+
}
|
|
1642
|
+
session.save()
|
|
1643
|
+
|
|
1644
|
+
response = self.client.get(f"{url}?resume=1")
|
|
1645
|
+
self.assertEqual(response.status_code, 200)
|
|
1646
|
+
self.assertEqual(run, ["step"])
|
|
1647
|
+
|
|
1648
|
+
session = self.client.session
|
|
1649
|
+
ctx = session.get(f"release_publish_{self.release.pk}")
|
|
1650
|
+
self.assertEqual(ctx.get("step"), 1)
|
|
1651
|
+
|
|
1530
1652
|
def test_new_todo_does_not_reset_pending_flow(self):
|
|
1531
1653
|
user = User.objects.create_superuser("admin", "admin@example.com", "pw")
|
|
1532
1654
|
url = reverse("release-progress", args=[self.release.pk, "publish"])
|
core/views.py
CHANGED
|
@@ -656,8 +656,8 @@ def _should_use_python_changelog(exc: OSError) -> bool:
|
|
|
656
656
|
def _generate_changelog_with_python(log_path: Path) -> None:
|
|
657
657
|
_append_log(log_path, "Falling back to Python changelog generator")
|
|
658
658
|
changelog_path = Path("CHANGELOG.rst")
|
|
659
|
-
range_spec = changelog_utils.determine_range_spec()
|
|
660
659
|
previous = changelog_path.read_text(encoding="utf-8") if changelog_path.exists() else None
|
|
660
|
+
range_spec = changelog_utils.determine_range_spec(previous_text=previous)
|
|
661
661
|
sections = changelog_utils.collect_sections(range_spec=range_spec, previous_text=previous)
|
|
662
662
|
content = changelog_utils.render_changelog(sections)
|
|
663
663
|
if not content.endswith("\n"):
|
|
@@ -897,7 +897,18 @@ def _step_check_todos(release, ctx, log_path: Path) -> None:
|
|
|
897
897
|
pending_values = list(
|
|
898
898
|
pending_qs.values("id", "request", "url", "request_details")
|
|
899
899
|
)
|
|
900
|
+
if not pending_values:
|
|
901
|
+
ctx["todos_ack"] = True
|
|
902
|
+
|
|
900
903
|
if not ctx.get("todos_ack"):
|
|
904
|
+
if not ctx.get("todos_block_logged"):
|
|
905
|
+
_append_log(
|
|
906
|
+
log_path,
|
|
907
|
+
"Release checklist requires acknowledgment before continuing. "
|
|
908
|
+
"Review outstanding TODO items and confirm the checklist; "
|
|
909
|
+
"publishing will resume automatically afterward.",
|
|
910
|
+
)
|
|
911
|
+
ctx["todos_block_logged"] = True
|
|
901
912
|
ctx["todos"] = pending_values
|
|
902
913
|
ctx["todos_required"] = True
|
|
903
914
|
raise PendingTodos()
|
|
@@ -1451,12 +1462,29 @@ def _step_publish(release, ctx, log_path: Path) -> None:
|
|
|
1451
1462
|
)
|
|
1452
1463
|
else:
|
|
1453
1464
|
_append_log(log_path, "Uploading distribution")
|
|
1454
|
-
release_utils.
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1465
|
+
publish_warning: release_utils.PostPublishWarning | None = None
|
|
1466
|
+
try:
|
|
1467
|
+
release_utils.publish(
|
|
1468
|
+
package=release.to_package(),
|
|
1469
|
+
version=release.version,
|
|
1470
|
+
creds=release.to_credentials(),
|
|
1471
|
+
repositories=targets,
|
|
1472
|
+
)
|
|
1473
|
+
except release_utils.PostPublishWarning as warning:
|
|
1474
|
+
publish_warning = warning
|
|
1475
|
+
|
|
1476
|
+
if publish_warning is not None:
|
|
1477
|
+
message = str(publish_warning)
|
|
1478
|
+
followups = _dedupe_preserve_order(publish_warning.followups)
|
|
1479
|
+
warning_entries = ctx.setdefault("warnings", [])
|
|
1480
|
+
if not any(entry.get("message") == message for entry in warning_entries):
|
|
1481
|
+
entry: dict[str, object] = {"message": message}
|
|
1482
|
+
if followups:
|
|
1483
|
+
entry["followups"] = followups
|
|
1484
|
+
warning_entries.append(entry)
|
|
1485
|
+
_append_log(log_path, message)
|
|
1486
|
+
for note in followups:
|
|
1487
|
+
_append_log(log_path, f"Follow-up: {note}")
|
|
1460
1488
|
release.pypi_url = (
|
|
1461
1489
|
f"https://pypi.org/project/{release.package.name}/{release.version}/"
|
|
1462
1490
|
)
|
|
@@ -1804,8 +1832,16 @@ def release_progress(request, pk: int, action: str):
|
|
|
1804
1832
|
ctx["release_approval"] = "approved"
|
|
1805
1833
|
if request.GET.get("reject"):
|
|
1806
1834
|
ctx["release_approval"] = "rejected"
|
|
1835
|
+
resume_requested = bool(request.GET.get("resume"))
|
|
1836
|
+
|
|
1807
1837
|
if request.GET.get("pause") and ctx.get("started"):
|
|
1808
1838
|
ctx["paused"] = True
|
|
1839
|
+
|
|
1840
|
+
if resume_requested:
|
|
1841
|
+
if not ctx.get("started"):
|
|
1842
|
+
ctx["started"] = True
|
|
1843
|
+
if ctx.get("paused"):
|
|
1844
|
+
ctx["paused"] = False
|
|
1809
1845
|
restart_count = 0
|
|
1810
1846
|
if restart_path.exists():
|
|
1811
1847
|
try:
|
|
@@ -1814,6 +1850,8 @@ def release_progress(request, pk: int, action: str):
|
|
|
1814
1850
|
restart_count = 0
|
|
1815
1851
|
step_count = ctx.get("step", 0)
|
|
1816
1852
|
step_param = request.GET.get("step")
|
|
1853
|
+
if resume_requested and step_param is None:
|
|
1854
|
+
step_param = str(step_count)
|
|
1817
1855
|
|
|
1818
1856
|
pending_qs = Todo.objects.filter(is_deleted=False, done_on__isnull=True)
|
|
1819
1857
|
pending_items = list(pending_qs)
|
|
@@ -1833,6 +1871,9 @@ def release_progress(request, pk: int, action: str):
|
|
|
1833
1871
|
else:
|
|
1834
1872
|
ctx["todos_ack"] = True
|
|
1835
1873
|
|
|
1874
|
+
if ctx.get("todos_ack"):
|
|
1875
|
+
ctx.pop("todos_block_logged", None)
|
|
1876
|
+
|
|
1836
1877
|
if not ctx.get("todos_ack"):
|
|
1837
1878
|
ctx["todos"] = [
|
|
1838
1879
|
{
|
|
@@ -2049,6 +2090,14 @@ def release_progress(request, pk: int, action: str):
|
|
|
2049
2090
|
)
|
|
2050
2091
|
|
|
2051
2092
|
is_running = ctx.get("started") and not paused and not done and not ctx.get("error")
|
|
2093
|
+
resume_available = (
|
|
2094
|
+
ctx.get("started")
|
|
2095
|
+
and not paused
|
|
2096
|
+
and not done
|
|
2097
|
+
and not ctx.get("error")
|
|
2098
|
+
and step_count < len(steps)
|
|
2099
|
+
and next_step is None
|
|
2100
|
+
)
|
|
2052
2101
|
can_resume = ctx.get("started") and paused and not done and not ctx.get("error")
|
|
2053
2102
|
release_manager_owner = manager.owner_display() if manager else ""
|
|
2054
2103
|
try:
|
|
@@ -2103,9 +2152,11 @@ def release_progress(request, pk: int, action: str):
|
|
|
2103
2152
|
"has_release_manager": bool(manager),
|
|
2104
2153
|
"current_user_admin_url": current_user_admin_url,
|
|
2105
2154
|
"is_running": is_running,
|
|
2155
|
+
"resume_available": resume_available,
|
|
2106
2156
|
"can_resume": can_resume,
|
|
2107
2157
|
"dry_run": dry_run_active,
|
|
2108
2158
|
"dry_run_toggle_enabled": dry_run_toggle_enabled,
|
|
2159
|
+
"warnings": ctx.get("warnings", []),
|
|
2109
2160
|
}
|
|
2110
2161
|
request.session[session_key] = ctx
|
|
2111
2162
|
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:
|
ocpp/test_rfid.py
CHANGED
|
@@ -131,7 +131,7 @@ class ScanNextViewTests(TestCase):
|
|
|
131
131
|
self.assertEqual(
|
|
132
132
|
resp.json(), {"rfid": "ABCD1234", "label_id": 1, "created": False}
|
|
133
133
|
)
|
|
134
|
-
mock_validate.assert_called_once_with("ABCD1234", kind=None)
|
|
134
|
+
mock_validate.assert_called_once_with("ABCD1234", kind=None, endianness=None)
|
|
135
135
|
|
|
136
136
|
@patch("config.middleware.Node.get_local", return_value=None)
|
|
137
137
|
@patch("config.middleware.get_site")
|
|
@@ -342,16 +342,20 @@ class ValidateRfidValueTests(SimpleTestCase):
|
|
|
342
342
|
tag.released = False
|
|
343
343
|
tag.reference = None
|
|
344
344
|
tag.kind = RFID.CLASSIC
|
|
345
|
+
tag.endianness = RFID.BIG_ENDIAN
|
|
345
346
|
mock_register.return_value = (tag, True)
|
|
346
347
|
|
|
347
348
|
result = validate_rfid_value("abcd1234")
|
|
348
349
|
|
|
349
|
-
mock_register.assert_called_once_with(
|
|
350
|
+
mock_register.assert_called_once_with(
|
|
351
|
+
"ABCD1234", kind=None, endianness=RFID.BIG_ENDIAN
|
|
352
|
+
)
|
|
350
353
|
tag.save.assert_called_once_with(update_fields=["last_seen_on"])
|
|
351
354
|
self.assertIs(tag.last_seen_on, fake_now)
|
|
352
355
|
mock_notify.assert_called_once_with("RFID 1 OK", "ABCD1234 B")
|
|
353
356
|
self.assertTrue(result["created"])
|
|
354
357
|
self.assertEqual(result["rfid"], "ABCD1234")
|
|
358
|
+
self.assertEqual(result["endianness"], RFID.BIG_ENDIAN)
|
|
355
359
|
|
|
356
360
|
@patch("ocpp.rfid.reader.timezone.now")
|
|
357
361
|
@patch("ocpp.rfid.reader.notify_async")
|
|
@@ -367,11 +371,14 @@ class ValidateRfidValueTests(SimpleTestCase):
|
|
|
367
371
|
tag.released = True
|
|
368
372
|
tag.reference = None
|
|
369
373
|
tag.kind = RFID.CLASSIC
|
|
374
|
+
tag.endianness = RFID.BIG_ENDIAN
|
|
370
375
|
mock_register.return_value = (tag, False)
|
|
371
376
|
|
|
372
377
|
result = validate_rfid_value("abcd", kind=RFID.NTAG215)
|
|
373
378
|
|
|
374
|
-
mock_register.assert_called_once_with(
|
|
379
|
+
mock_register.assert_called_once_with(
|
|
380
|
+
"ABCD", kind=RFID.NTAG215, endianness=RFID.BIG_ENDIAN
|
|
381
|
+
)
|
|
375
382
|
tag.save.assert_called_once_with(update_fields=["kind", "last_seen_on"])
|
|
376
383
|
self.assertIs(tag.last_seen_on, fake_now)
|
|
377
384
|
self.assertEqual(tag.kind, RFID.NTAG215)
|
|
@@ -379,6 +386,36 @@ class ValidateRfidValueTests(SimpleTestCase):
|
|
|
379
386
|
self.assertFalse(result["allowed"])
|
|
380
387
|
self.assertFalse(result["created"])
|
|
381
388
|
self.assertEqual(result["kind"], RFID.NTAG215)
|
|
389
|
+
self.assertEqual(result["endianness"], RFID.BIG_ENDIAN)
|
|
390
|
+
|
|
391
|
+
@patch("ocpp.rfid.reader.timezone.now")
|
|
392
|
+
@patch("ocpp.rfid.reader.notify_async")
|
|
393
|
+
@patch("ocpp.rfid.reader.RFID.register_scan")
|
|
394
|
+
def test_registers_little_endian_value(
|
|
395
|
+
self, mock_register, mock_notify, mock_now
|
|
396
|
+
):
|
|
397
|
+
fake_now = object()
|
|
398
|
+
mock_now.return_value = fake_now
|
|
399
|
+
tag = MagicMock()
|
|
400
|
+
tag.pk = 7
|
|
401
|
+
tag.label_id = 7
|
|
402
|
+
tag.allowed = True
|
|
403
|
+
tag.color = "B"
|
|
404
|
+
tag.released = False
|
|
405
|
+
tag.reference = None
|
|
406
|
+
tag.kind = RFID.CLASSIC
|
|
407
|
+
tag.endianness = RFID.LITTLE_ENDIAN
|
|
408
|
+
mock_register.return_value = (tag, True)
|
|
409
|
+
|
|
410
|
+
result = validate_rfid_value("A1B2C3D4", endianness=RFID.LITTLE_ENDIAN)
|
|
411
|
+
|
|
412
|
+
mock_register.assert_called_once_with(
|
|
413
|
+
"D4C3B2A1", kind=None, endianness=RFID.LITTLE_ENDIAN
|
|
414
|
+
)
|
|
415
|
+
tag.save.assert_called_once_with(update_fields=["last_seen_on"])
|
|
416
|
+
self.assertEqual(result["rfid"], "D4C3B2A1")
|
|
417
|
+
self.assertEqual(result["endianness"], RFID.LITTLE_ENDIAN)
|
|
418
|
+
mock_notify.assert_called_once()
|
|
382
419
|
|
|
383
420
|
def test_rejects_invalid_value(self):
|
|
384
421
|
result = validate_rfid_value("invalid!")
|
|
@@ -412,6 +449,7 @@ class ValidateRfidValueTests(SimpleTestCase):
|
|
|
412
449
|
tag.released = False
|
|
413
450
|
tag.reference = None
|
|
414
451
|
tag.kind = RFID.CLASSIC
|
|
452
|
+
tag.endianness = RFID.BIG_ENDIAN
|
|
415
453
|
mock_register.return_value = (tag, False)
|
|
416
454
|
mock_run.return_value = types.SimpleNamespace(
|
|
417
455
|
returncode=0, stdout="ok\n", stderr=""
|
|
@@ -427,6 +465,7 @@ class ValidateRfidValueTests(SimpleTestCase):
|
|
|
427
465
|
env = run_kwargs.get("env", {})
|
|
428
466
|
self.assertEqual(env.get("RFID_VALUE"), "ABCD1234")
|
|
429
467
|
self.assertEqual(env.get("RFID_LABEL_ID"), "1")
|
|
468
|
+
self.assertEqual(env.get("RFID_ENDIANNESS"), RFID.BIG_ENDIAN)
|
|
430
469
|
mock_popen.assert_not_called()
|
|
431
470
|
mock_notify.assert_called_once_with("RFID 1 OK", "ABCD1234 B")
|
|
432
471
|
tag.save.assert_called_once_with(update_fields=["last_seen_on"])
|
|
@@ -437,6 +476,7 @@ class ValidateRfidValueTests(SimpleTestCase):
|
|
|
437
476
|
self.assertEqual(output.get("stderr"), "")
|
|
438
477
|
self.assertEqual(output.get("returncode"), 0)
|
|
439
478
|
self.assertEqual(output.get("error"), "")
|
|
479
|
+
self.assertEqual(result["endianness"], RFID.BIG_ENDIAN)
|
|
440
480
|
|
|
441
481
|
@patch("ocpp.rfid.reader.timezone.now")
|
|
442
482
|
@patch("ocpp.rfid.reader.notify_async")
|
|
@@ -457,6 +497,7 @@ class ValidateRfidValueTests(SimpleTestCase):
|
|
|
457
497
|
tag.released = False
|
|
458
498
|
tag.reference = None
|
|
459
499
|
tag.kind = RFID.CLASSIC
|
|
500
|
+
tag.endianness = RFID.BIG_ENDIAN
|
|
460
501
|
mock_register.return_value = (tag, False)
|
|
461
502
|
mock_run.return_value = types.SimpleNamespace(
|
|
462
503
|
returncode=1, stdout="", stderr="failure"
|
|
@@ -476,6 +517,7 @@ class ValidateRfidValueTests(SimpleTestCase):
|
|
|
476
517
|
self.assertEqual(output.get("stderr"), "failure")
|
|
477
518
|
self.assertEqual(output.get("error"), "")
|
|
478
519
|
mock_popen.assert_not_called()
|
|
520
|
+
self.assertEqual(result["endianness"], RFID.BIG_ENDIAN)
|
|
479
521
|
|
|
480
522
|
@patch("ocpp.rfid.reader.timezone.now")
|
|
481
523
|
@patch("ocpp.rfid.reader.notify_async")
|
|
@@ -497,6 +539,7 @@ class ValidateRfidValueTests(SimpleTestCase):
|
|
|
497
539
|
tag.released = False
|
|
498
540
|
tag.reference = None
|
|
499
541
|
tag.kind = RFID.CLASSIC
|
|
542
|
+
tag.endianness = RFID.BIG_ENDIAN
|
|
500
543
|
mock_register.return_value = (tag, False)
|
|
501
544
|
result = validate_rfid_value("abcdef")
|
|
502
545
|
|
|
@@ -507,10 +550,12 @@ class ValidateRfidValueTests(SimpleTestCase):
|
|
|
507
550
|
env = kwargs.get("env", {})
|
|
508
551
|
self.assertEqual(env.get("RFID_VALUE"), "ABCDEF")
|
|
509
552
|
self.assertEqual(env.get("RFID_LABEL_ID"), "3")
|
|
553
|
+
self.assertEqual(env.get("RFID_ENDIANNESS"), RFID.BIG_ENDIAN)
|
|
510
554
|
self.assertIs(kwargs.get("stdout"), subprocess.DEVNULL)
|
|
511
555
|
self.assertIs(kwargs.get("stderr"), subprocess.DEVNULL)
|
|
512
556
|
self.assertTrue(result["allowed"])
|
|
513
557
|
mock_notify.assert_called_once_with("RFID 3 OK", "ABCDEF B")
|
|
558
|
+
self.assertEqual(result["endianness"], RFID.BIG_ENDIAN)
|
|
514
559
|
|
|
515
560
|
|
|
516
561
|
class CardTypeDetectionTests(TestCase):
|