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.

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
- if _has_active_session(tx_obj):
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