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
ocpp/tests.py
CHANGED
|
@@ -1931,6 +1931,27 @@ class ChargerLandingTests(TestCase):
|
|
|
1931
1931
|
finally:
|
|
1932
1932
|
store.transactions.pop(key, None)
|
|
1933
1933
|
|
|
1934
|
+
def test_public_page_shows_available_when_status_stale(self):
|
|
1935
|
+
charger = Charger.objects.create(
|
|
1936
|
+
charger_id="STALEPUB",
|
|
1937
|
+
last_status="Charging",
|
|
1938
|
+
)
|
|
1939
|
+
response = self.client.get(reverse("charger-page", args=["STALEPUB"]))
|
|
1940
|
+
self.assertEqual(response.status_code, 200)
|
|
1941
|
+
self.assertContains(
|
|
1942
|
+
response,
|
|
1943
|
+
'style="background-color: #0d6efd; color: #fff;">Available</span>',
|
|
1944
|
+
)
|
|
1945
|
+
|
|
1946
|
+
def test_admin_status_shows_available_when_status_stale(self):
|
|
1947
|
+
charger = Charger.objects.create(
|
|
1948
|
+
charger_id="STALEADM",
|
|
1949
|
+
last_status="Charging",
|
|
1950
|
+
)
|
|
1951
|
+
response = self.client.get(reverse("charger-status", args=["STALEADM"]))
|
|
1952
|
+
self.assertEqual(response.status_code, 200)
|
|
1953
|
+
self.assertContains(response, 'id="charger-state">Available</strong>')
|
|
1954
|
+
|
|
1934
1955
|
def test_public_status_shows_rfid_link_for_known_tag(self):
|
|
1935
1956
|
aggregate = Charger.objects.create(charger_id="PUBRFID")
|
|
1936
1957
|
connector = Charger.objects.create(
|
|
@@ -2129,6 +2150,32 @@ class ChargerAdminTests(TestCase):
|
|
|
2129
2150
|
resp = self.client.get(url)
|
|
2130
2151
|
self.assertNotContains(resp, charger.reference.image.url)
|
|
2131
2152
|
|
|
2153
|
+
def test_toggle_rfid_authentication_action_toggles_value(self):
|
|
2154
|
+
charger_requires = Charger.objects.create(
|
|
2155
|
+
charger_id="RFIDON", require_rfid=True
|
|
2156
|
+
)
|
|
2157
|
+
charger_optional = Charger.objects.create(
|
|
2158
|
+
charger_id="RFIDOFF", require_rfid=False
|
|
2159
|
+
)
|
|
2160
|
+
url = reverse("admin:ocpp_charger_changelist")
|
|
2161
|
+
response = self.client.post(
|
|
2162
|
+
url,
|
|
2163
|
+
{
|
|
2164
|
+
"action": "toggle_rfid_authentication",
|
|
2165
|
+
"_selected_action": [
|
|
2166
|
+
charger_requires.pk,
|
|
2167
|
+
charger_optional.pk,
|
|
2168
|
+
],
|
|
2169
|
+
},
|
|
2170
|
+
follow=True,
|
|
2171
|
+
)
|
|
2172
|
+
self.assertEqual(response.status_code, 200)
|
|
2173
|
+
charger_requires.refresh_from_db()
|
|
2174
|
+
charger_optional.refresh_from_db()
|
|
2175
|
+
self.assertFalse(charger_requires.require_rfid)
|
|
2176
|
+
self.assertTrue(charger_optional.require_rfid)
|
|
2177
|
+
self.assertContains(response, "Updated RFID authentication")
|
|
2178
|
+
|
|
2132
2179
|
def test_admin_lists_log_link(self):
|
|
2133
2180
|
charger = Charger.objects.create(charger_id="LOG1")
|
|
2134
2181
|
url = reverse("admin:ocpp_charger_changelist")
|
|
@@ -2155,6 +2202,48 @@ class ChargerAdminTests(TestCase):
|
|
|
2155
2202
|
finally:
|
|
2156
2203
|
store.transactions.pop(key, None)
|
|
2157
2204
|
|
|
2205
|
+
def test_admin_status_shows_available_when_status_stale(self):
|
|
2206
|
+
charger = Charger.objects.create(
|
|
2207
|
+
charger_id="ADMINSTALE",
|
|
2208
|
+
last_status="Charging",
|
|
2209
|
+
)
|
|
2210
|
+
url = reverse("admin:ocpp_charger_changelist")
|
|
2211
|
+
resp = self.client.get(url)
|
|
2212
|
+
available_label = force_str(STATUS_BADGE_MAP["available"][0])
|
|
2213
|
+
self.assertContains(resp, f">{available_label}<")
|
|
2214
|
+
|
|
2215
|
+
def test_recheck_charger_status_action_sends_trigger(self):
|
|
2216
|
+
charger = Charger.objects.create(charger_id="RECHECK1")
|
|
2217
|
+
|
|
2218
|
+
class DummyConnection:
|
|
2219
|
+
def __init__(self):
|
|
2220
|
+
self.sent: list[str] = []
|
|
2221
|
+
|
|
2222
|
+
async def send(self, message):
|
|
2223
|
+
self.sent.append(message)
|
|
2224
|
+
|
|
2225
|
+
ws = DummyConnection()
|
|
2226
|
+
store.set_connection(charger.charger_id, charger.connector_id, ws)
|
|
2227
|
+
try:
|
|
2228
|
+
url = reverse("admin:ocpp_charger_changelist")
|
|
2229
|
+
response = self.client.post(
|
|
2230
|
+
url,
|
|
2231
|
+
{
|
|
2232
|
+
"action": "recheck_charger_status",
|
|
2233
|
+
"index": 0,
|
|
2234
|
+
"select_across": 0,
|
|
2235
|
+
"_selected_action": [charger.pk],
|
|
2236
|
+
},
|
|
2237
|
+
follow=True,
|
|
2238
|
+
)
|
|
2239
|
+
self.assertEqual(response.status_code, 200)
|
|
2240
|
+
self.assertTrue(ws.sent)
|
|
2241
|
+
self.assertIn("TriggerMessage", ws.sent[0])
|
|
2242
|
+
self.assertContains(response, "Requested status update")
|
|
2243
|
+
finally:
|
|
2244
|
+
store.pop_connection(charger.charger_id, charger.connector_id)
|
|
2245
|
+
store.clear_pending_calls(charger.charger_id)
|
|
2246
|
+
|
|
2158
2247
|
def test_admin_log_view_displays_entries(self):
|
|
2159
2248
|
charger = Charger.objects.create(charger_id="LOG2")
|
|
2160
2249
|
log_id = store.identity_key(charger.charger_id, charger.connector_id)
|
|
@@ -4442,6 +4531,49 @@ class LiveUpdateViewTests(TestCase):
|
|
|
4442
4531
|
)
|
|
4443
4532
|
self.assertEqual(aggregate_entry["state"], available_label)
|
|
4444
4533
|
|
|
4534
|
+
def test_dashboard_connector_treats_finishing_as_available_without_session(self):
|
|
4535
|
+
charger = Charger.objects.create(
|
|
4536
|
+
charger_id="FINISH-STATE",
|
|
4537
|
+
connector_id=1,
|
|
4538
|
+
last_status="Finishing",
|
|
4539
|
+
)
|
|
4540
|
+
|
|
4541
|
+
resp = self.client.get(reverse("ocpp-dashboard"))
|
|
4542
|
+
self.assertEqual(resp.status_code, 200)
|
|
4543
|
+
self.assertIsNotNone(resp.context)
|
|
4544
|
+
context = resp.context
|
|
4545
|
+
available_label = force_str(STATUS_BADGE_MAP["available"][0])
|
|
4546
|
+
entry = next(
|
|
4547
|
+
item
|
|
4548
|
+
for item in context["chargers"]
|
|
4549
|
+
if item["charger"].pk == charger.pk
|
|
4550
|
+
)
|
|
4551
|
+
self.assertEqual(entry["state"], available_label)
|
|
4552
|
+
|
|
4553
|
+
def test_dashboard_aggregate_treats_finishing_as_available_without_session(self):
|
|
4554
|
+
aggregate = Charger.objects.create(
|
|
4555
|
+
charger_id="FINISH-AGG",
|
|
4556
|
+
connector_id=None,
|
|
4557
|
+
last_status="Finishing",
|
|
4558
|
+
)
|
|
4559
|
+
Charger.objects.create(
|
|
4560
|
+
charger_id=aggregate.charger_id,
|
|
4561
|
+
connector_id=1,
|
|
4562
|
+
last_status="Finishing",
|
|
4563
|
+
)
|
|
4564
|
+
|
|
4565
|
+
resp = self.client.get(reverse("ocpp-dashboard"))
|
|
4566
|
+
self.assertEqual(resp.status_code, 200)
|
|
4567
|
+
self.assertIsNotNone(resp.context)
|
|
4568
|
+
context = resp.context
|
|
4569
|
+
available_label = force_str(STATUS_BADGE_MAP["available"][0])
|
|
4570
|
+
aggregate_entry = next(
|
|
4571
|
+
item
|
|
4572
|
+
for item in context["chargers"]
|
|
4573
|
+
if item["charger"].pk == aggregate.pk
|
|
4574
|
+
)
|
|
4575
|
+
self.assertEqual(aggregate_entry["state"], available_label)
|
|
4576
|
+
|
|
4445
4577
|
def test_dashboard_aggregate_uses_connection_when_status_missing(self):
|
|
4446
4578
|
aggregate = Charger.objects.create(
|
|
4447
4579
|
charger_id="DASHAGG-CONN", last_status="Charging"
|
ocpp/views.py
CHANGED
|
@@ -370,10 +370,6 @@ def _aggregate_dashboard_state(charger: Charger) -> tuple[str, str] | None:
|
|
|
370
370
|
)
|
|
371
371
|
statuses: list[str] = []
|
|
372
372
|
for sibling in siblings:
|
|
373
|
-
status_value = (sibling.last_status or "").strip()
|
|
374
|
-
if status_value:
|
|
375
|
-
statuses.append(status_value.casefold())
|
|
376
|
-
continue
|
|
377
373
|
tx_obj = store.get_transaction(sibling.charger_id, sibling.connector_id)
|
|
378
374
|
if not tx_obj:
|
|
379
375
|
tx_obj = (
|
|
@@ -381,9 +377,22 @@ def _aggregate_dashboard_state(charger: Charger) -> tuple[str, str] | None:
|
|
|
381
377
|
.order_by("-start_time")
|
|
382
378
|
.first()
|
|
383
379
|
)
|
|
384
|
-
|
|
380
|
+
has_session = _has_active_session(tx_obj)
|
|
381
|
+
status_value = (sibling.last_status or "").strip()
|
|
382
|
+
normalized_status = status_value.casefold() if status_value else ""
|
|
383
|
+
error_code_lower = (sibling.last_error_code or "").strip().lower()
|
|
384
|
+
if has_session:
|
|
385
385
|
statuses.append("charging")
|
|
386
386
|
continue
|
|
387
|
+
if (
|
|
388
|
+
normalized_status in {"charging", "finishing"}
|
|
389
|
+
and error_code_lower in ERROR_OK_VALUES
|
|
390
|
+
):
|
|
391
|
+
statuses.append("available")
|
|
392
|
+
continue
|
|
393
|
+
if normalized_status:
|
|
394
|
+
statuses.append(normalized_status)
|
|
395
|
+
continue
|
|
387
396
|
if store.is_connected(sibling.charger_id, sibling.connector_id):
|
|
388
397
|
statuses.append("available")
|
|
389
398
|
|
|
@@ -424,6 +433,15 @@ def _charger_state(charger: Charger, tx_obj: Transaction | list | None):
|
|
|
424
433
|
# while a session is active. Override the badge so the user can see
|
|
425
434
|
# the charger is actually busy.
|
|
426
435
|
label, color = STATUS_BADGE_MAP.get("charging", (_("Charging"), "#198754"))
|
|
436
|
+
elif (
|
|
437
|
+
not has_session
|
|
438
|
+
and key in {"charging", "finishing"}
|
|
439
|
+
and error_code_lower in ERROR_OK_VALUES
|
|
440
|
+
):
|
|
441
|
+
# Some chargers continue reporting "Charging" after a session ends.
|
|
442
|
+
# When no active transaction exists, surface the state as available
|
|
443
|
+
# so the UI reflects the actual behaviour at the site.
|
|
444
|
+
label, color = STATUS_BADGE_MAP.get("available", (_("Available"), "#0d6efd"))
|
|
427
445
|
elif error_code and error_code_lower not in ERROR_OK_VALUES:
|
|
428
446
|
label = _("%(status)s (%(error)s)") % {
|
|
429
447
|
"status": label,
|
pages/tests.py
CHANGED
|
@@ -8,7 +8,7 @@ django.setup()
|
|
|
8
8
|
|
|
9
9
|
from django.test import Client, RequestFactory, TestCase, SimpleTestCase, override_settings
|
|
10
10
|
from django.test.utils import CaptureQueriesContext
|
|
11
|
-
from django.urls import reverse
|
|
11
|
+
from django.urls import NoReverseMatch, reverse
|
|
12
12
|
from django.templatetags.static import static
|
|
13
13
|
from urllib.parse import quote
|
|
14
14
|
from django.contrib.auth import get_user_model
|
|
@@ -685,6 +685,22 @@ class AdminDashboardAppListTests(TestCase):
|
|
|
685
685
|
resp = self.client.get(reverse("admin:index"))
|
|
686
686
|
self.assertContains(resp, "5. Horologia MODELS")
|
|
687
687
|
|
|
688
|
+
def test_dashboard_handles_missing_last_net_message_url(self):
|
|
689
|
+
from pages.templatetags import admin_extras
|
|
690
|
+
|
|
691
|
+
real_reverse = admin_extras.reverse
|
|
692
|
+
|
|
693
|
+
def fake_reverse(name, *args, **kwargs):
|
|
694
|
+
if name == "last-net-message":
|
|
695
|
+
raise NoReverseMatch("missing")
|
|
696
|
+
return real_reverse(name, *args, **kwargs)
|
|
697
|
+
|
|
698
|
+
with patch("pages.templatetags.admin_extras.reverse", side_effect=fake_reverse):
|
|
699
|
+
resp = self.client.get(reverse("admin:index"))
|
|
700
|
+
|
|
701
|
+
self.assertEqual(resp.status_code, 200)
|
|
702
|
+
self.assertNotIn(b"last-net-message", resp.content)
|
|
703
|
+
|
|
688
704
|
|
|
689
705
|
class AdminSidebarTests(TestCase):
|
|
690
706
|
def setUp(self):
|
|
@@ -2568,6 +2584,15 @@ class FavoriteTests(TestCase):
|
|
|
2568
2584
|
resp, '<div class="todo-details">More info</div>', html=True
|
|
2569
2585
|
)
|
|
2570
2586
|
|
|
2587
|
+
def test_dashboard_hides_completed_todos(self):
|
|
2588
|
+
todo = Todo.objects.create(request="Completed task")
|
|
2589
|
+
Todo.objects.filter(pk=todo.pk).update(done_on=timezone.now())
|
|
2590
|
+
|
|
2591
|
+
resp = self.client.get(reverse("admin:index"))
|
|
2592
|
+
|
|
2593
|
+
self.assertNotContains(resp, todo.request)
|
|
2594
|
+
self.assertNotContains(resp, "Completed")
|
|
2595
|
+
|
|
2571
2596
|
def test_dashboard_shows_todos_when_node_unknown(self):
|
|
2572
2597
|
Todo.objects.create(request="Check fallback")
|
|
2573
2598
|
from nodes.models import Node
|
|
File without changes
|
|
File without changes
|
|
File without changes
|