arthexis 0.1.18__py3-none-any.whl → 0.1.20__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.
- {arthexis-0.1.18.dist-info → arthexis-0.1.20.dist-info}/METADATA +39 -12
- {arthexis-0.1.18.dist-info → arthexis-0.1.20.dist-info}/RECORD +44 -44
- config/settings.py +1 -5
- core/admin.py +142 -1
- core/backends.py +8 -2
- core/environment.py +221 -4
- core/models.py +124 -25
- core/notifications.py +1 -1
- core/reference_utils.py +10 -11
- core/sigil_builder.py +2 -2
- core/system.py +125 -0
- core/tasks.py +24 -23
- core/tests.py +1 -0
- core/views.py +105 -40
- nodes/admin.py +134 -3
- nodes/models.py +310 -69
- nodes/rfid_sync.py +1 -1
- nodes/tasks.py +100 -2
- nodes/tests.py +573 -48
- nodes/urls.py +4 -1
- nodes/views.py +498 -106
- ocpp/admin.py +124 -5
- ocpp/consumers.py +106 -9
- ocpp/models.py +90 -1
- ocpp/store.py +6 -4
- ocpp/tasks.py +4 -0
- ocpp/test_export_import.py +1 -0
- ocpp/test_rfid.py +3 -1
- ocpp/tests.py +114 -10
- ocpp/transactions_io.py +9 -1
- ocpp/urls.py +3 -3
- ocpp/views.py +166 -40
- pages/admin.py +63 -10
- pages/context_processors.py +26 -9
- pages/defaults.py +1 -1
- pages/middleware.py +3 -0
- pages/models.py +35 -0
- pages/module_defaults.py +5 -5
- pages/tests.py +280 -65
- pages/urls.py +3 -1
- pages/views.py +176 -29
- {arthexis-0.1.18.dist-info → arthexis-0.1.20.dist-info}/WHEEL +0 -0
- {arthexis-0.1.18.dist-info → arthexis-0.1.20.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.18.dist-info → arthexis-0.1.20.dist-info}/top_level.txt +0 -0
pages/tests.py
CHANGED
|
@@ -3,12 +3,13 @@ import os
|
|
|
3
3
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
|
4
4
|
|
|
5
5
|
import django
|
|
6
|
+
import pytest
|
|
6
7
|
|
|
7
8
|
django.setup()
|
|
8
9
|
|
|
9
10
|
from django.test import Client, RequestFactory, TestCase, SimpleTestCase, override_settings
|
|
10
11
|
from django.test.utils import CaptureQueriesContext
|
|
11
|
-
from django.urls import
|
|
12
|
+
from django.urls import reverse
|
|
12
13
|
from django.templatetags.static import static
|
|
13
14
|
from urllib.parse import quote
|
|
14
15
|
from django.contrib.auth import get_user_model
|
|
@@ -33,6 +34,8 @@ from pages.models import (
|
|
|
33
34
|
UserManual,
|
|
34
35
|
UserStory,
|
|
35
36
|
)
|
|
37
|
+
from django.http import FileResponse
|
|
38
|
+
|
|
36
39
|
from pages.admin import (
|
|
37
40
|
ApplicationAdmin,
|
|
38
41
|
UserManualAdmin,
|
|
@@ -506,6 +509,7 @@ class InvitationTests(TestCase):
|
|
|
506
509
|
lead = InviteLead.objects.get()
|
|
507
510
|
self.assertEqual(lead.mac_address, "aa:bb:cc:dd:ee:ff")
|
|
508
511
|
|
|
512
|
+
@pytest.mark.feature("ap-router")
|
|
509
513
|
@patch("pages.views.public_wifi.grant_public_access")
|
|
510
514
|
@patch(
|
|
511
515
|
"pages.views.public_wifi.resolve_mac_address",
|
|
@@ -690,23 +694,6 @@ class AdminDashboardAppListTests(TestCase):
|
|
|
690
694
|
resp = self.client.get(reverse("admin:index"))
|
|
691
695
|
self.assertContains(resp, "5. Horologia MODELS")
|
|
692
696
|
|
|
693
|
-
def test_dashboard_handles_missing_last_net_message_url(self):
|
|
694
|
-
from pages.templatetags import admin_extras
|
|
695
|
-
|
|
696
|
-
real_reverse = admin_extras.reverse
|
|
697
|
-
|
|
698
|
-
def fake_reverse(name, *args, **kwargs):
|
|
699
|
-
if name == "last-net-message":
|
|
700
|
-
raise NoReverseMatch("missing")
|
|
701
|
-
return real_reverse(name, *args, **kwargs)
|
|
702
|
-
|
|
703
|
-
with patch("pages.templatetags.admin_extras.reverse", side_effect=fake_reverse):
|
|
704
|
-
resp = self.client.get(reverse("admin:index"))
|
|
705
|
-
|
|
706
|
-
self.assertEqual(resp.status_code, 200)
|
|
707
|
-
self.assertNotIn(b"last-net-message", resp.content)
|
|
708
|
-
|
|
709
|
-
|
|
710
697
|
class AdminSidebarTests(TestCase):
|
|
711
698
|
def setUp(self):
|
|
712
699
|
self.client = Client()
|
|
@@ -816,7 +803,8 @@ class ViewHistoryLoggingTests(TestCase):
|
|
|
816
803
|
)
|
|
817
804
|
landing = module.landings.get(path="/")
|
|
818
805
|
landing.label = "Home Landing"
|
|
819
|
-
landing.
|
|
806
|
+
landing.track_leads = True
|
|
807
|
+
landing.save(update_fields=["label", "track_leads"])
|
|
820
808
|
|
|
821
809
|
resp = self.client.get(
|
|
822
810
|
reverse("pages:index"), HTTP_REFERER="https://example.com/ref"
|
|
@@ -841,7 +829,8 @@ class ViewHistoryLoggingTests(TestCase):
|
|
|
841
829
|
)
|
|
842
830
|
landing = module.landings.get(path="/")
|
|
843
831
|
landing.label = "No Celery"
|
|
844
|
-
landing.
|
|
832
|
+
landing.track_leads = True
|
|
833
|
+
landing.save(update_fields=["label", "track_leads"])
|
|
845
834
|
|
|
846
835
|
resp = self.client.get(reverse("pages:index"))
|
|
847
836
|
|
|
@@ -861,7 +850,8 @@ class ViewHistoryLoggingTests(TestCase):
|
|
|
861
850
|
)
|
|
862
851
|
landing = module.landings.get(path="/")
|
|
863
852
|
landing.enabled = False
|
|
864
|
-
landing.
|
|
853
|
+
landing.track_leads = True
|
|
854
|
+
landing.save(update_fields=["enabled", "track_leads"])
|
|
865
855
|
|
|
866
856
|
resp = self.client.get(reverse("pages:index"))
|
|
867
857
|
|
|
@@ -1130,6 +1120,50 @@ class LogViewerAdminTests(SimpleTestCase):
|
|
|
1130
1120
|
self.assertEqual(context["selected_log"], "selected.log")
|
|
1131
1121
|
self.assertIn("hello world", context["log_content"])
|
|
1132
1122
|
|
|
1123
|
+
def test_log_viewer_applies_line_limit(self):
|
|
1124
|
+
content = "\n".join(f"line {i}" for i in range(50))
|
|
1125
|
+
self._create_log("limited.log", content)
|
|
1126
|
+
response = self._render({"log": "limited.log", "limit": "20"})
|
|
1127
|
+
context = response.context_data
|
|
1128
|
+
self.assertEqual(context["log_limit_choice"], "20")
|
|
1129
|
+
self.assertIn("line 49", context["log_content"])
|
|
1130
|
+
self.assertIn("line 30", context["log_content"])
|
|
1131
|
+
self.assertNotIn("line 29", context["log_content"])
|
|
1132
|
+
|
|
1133
|
+
def test_log_viewer_all_limit_returns_full_log(self):
|
|
1134
|
+
content = "first\nsecond\nthird"
|
|
1135
|
+
self._create_log("all.log", content)
|
|
1136
|
+
response = self._render({"log": "all.log", "limit": "all"})
|
|
1137
|
+
context = response.context_data
|
|
1138
|
+
self.assertEqual(context["log_limit_choice"], "all")
|
|
1139
|
+
self.assertIn("first", context["log_content"])
|
|
1140
|
+
self.assertIn("second", context["log_content"])
|
|
1141
|
+
|
|
1142
|
+
def test_log_viewer_invalid_limit_defaults_to_20(self):
|
|
1143
|
+
content = "\n".join(f"item {i}" for i in range(5))
|
|
1144
|
+
self._create_log("invalid-limit.log", content)
|
|
1145
|
+
response = self._render({"log": "invalid-limit.log", "limit": "oops"})
|
|
1146
|
+
context = response.context_data
|
|
1147
|
+
self.assertEqual(context["log_limit_choice"], "20")
|
|
1148
|
+
|
|
1149
|
+
def test_log_viewer_downloads_selected_log(self):
|
|
1150
|
+
self._create_log("download.log", "downloadable content")
|
|
1151
|
+
request = self._build_request({"log": "download.log", "download": "1"})
|
|
1152
|
+
context = {
|
|
1153
|
+
"site_title": "Constellation",
|
|
1154
|
+
"site_header": "Constellation",
|
|
1155
|
+
"site_url": "/",
|
|
1156
|
+
"available_apps": [],
|
|
1157
|
+
}
|
|
1158
|
+
with patch("pages.admin.admin.site.each_context", return_value=context), patch(
|
|
1159
|
+
"pages.context_processors.get_site", return_value=None
|
|
1160
|
+
):
|
|
1161
|
+
response = log_viewer(request)
|
|
1162
|
+
self.assertIsInstance(response, FileResponse)
|
|
1163
|
+
self.assertIn("attachment", response["Content-Disposition"])
|
|
1164
|
+
content = b"".join(response.streaming_content).decode()
|
|
1165
|
+
self.assertIn("downloadable content", content)
|
|
1166
|
+
|
|
1133
1167
|
def test_log_viewer_reports_missing_log(self):
|
|
1134
1168
|
response = self._render({"log": "missing.log"})
|
|
1135
1169
|
self.assertIn("requested log could not be found", response.context_data["log_error"])
|
|
@@ -1328,6 +1362,7 @@ class SiteAdminRegisterCurrentTests(TestCase):
|
|
|
1328
1362
|
self.assertEqual(site.name, "")
|
|
1329
1363
|
|
|
1330
1364
|
|
|
1365
|
+
@pytest.mark.feature("screenshot-poll")
|
|
1331
1366
|
class SiteAdminScreenshotTests(TestCase):
|
|
1332
1367
|
def setUp(self):
|
|
1333
1368
|
self.client = Client()
|
|
@@ -1447,17 +1482,17 @@ class NavAppsTests(TestCase):
|
|
|
1447
1482
|
)
|
|
1448
1483
|
app = Application.objects.create(name="Readme")
|
|
1449
1484
|
Module.objects.create(
|
|
1450
|
-
node_role=role, application=app, path="/", is_default=True
|
|
1485
|
+
node_role=role, application=app, path="/", is_default=True, menu="Cookbooks"
|
|
1451
1486
|
)
|
|
1452
1487
|
|
|
1453
1488
|
def test_nav_pill_renders(self):
|
|
1454
1489
|
resp = self.client.get(reverse("pages:index"))
|
|
1455
|
-
self.assertContains(resp, "
|
|
1490
|
+
self.assertContains(resp, "COOKBOOKS")
|
|
1456
1491
|
self.assertContains(resp, "badge rounded-pill")
|
|
1457
1492
|
|
|
1458
1493
|
def test_nav_pill_renders_with_port(self):
|
|
1459
1494
|
resp = self.client.get(reverse("pages:index"), HTTP_HOST="127.0.0.1:8000")
|
|
1460
|
-
self.assertContains(resp, "
|
|
1495
|
+
self.assertContains(resp, "COOKBOOKS")
|
|
1461
1496
|
|
|
1462
1497
|
def test_nav_pill_uses_menu_field(self):
|
|
1463
1498
|
site_app = Module.objects.get()
|
|
@@ -1465,7 +1500,7 @@ class NavAppsTests(TestCase):
|
|
|
1465
1500
|
site_app.save()
|
|
1466
1501
|
resp = self.client.get(reverse("pages:index"))
|
|
1467
1502
|
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">DOCS')
|
|
1468
|
-
self.assertNotContains(resp, 'badge rounded-pill text-bg-secondary">
|
|
1503
|
+
self.assertNotContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOKS')
|
|
1469
1504
|
|
|
1470
1505
|
def test_app_without_root_url_excluded(self):
|
|
1471
1506
|
role = NodeRole.objects.get(name="Terminal")
|
|
@@ -1530,20 +1565,22 @@ class RoleLandingRedirectTests(TestCase):
|
|
|
1530
1565
|
|
|
1531
1566
|
def test_satellite_redirects_to_dashboard(self):
|
|
1532
1567
|
target = self._configure_role_landing(
|
|
1533
|
-
"Satellite", "/ocpp/", "CPMS Online Dashboard"
|
|
1568
|
+
"Satellite", "/ocpp/cpms/dashboard/", "CPMS Online Dashboard"
|
|
1534
1569
|
)
|
|
1535
1570
|
resp = self.client.get(reverse("pages:index"))
|
|
1536
1571
|
self.assertRedirects(resp, target, fetch_redirect_response=False)
|
|
1537
1572
|
|
|
1538
1573
|
def test_control_redirects_to_rfid(self):
|
|
1539
1574
|
target = self._configure_role_landing(
|
|
1540
|
-
"Control", "/ocpp/rfid/", "RFID Tag Validator"
|
|
1575
|
+
"Control", "/ocpp/rfid/validator/", "RFID Tag Validator"
|
|
1541
1576
|
)
|
|
1542
1577
|
resp = self.client.get(reverse("pages:index"))
|
|
1543
1578
|
self.assertRedirects(resp, target, fetch_redirect_response=False)
|
|
1544
1579
|
|
|
1545
1580
|
def test_security_group_redirect_takes_priority(self):
|
|
1546
|
-
self._configure_role_landing(
|
|
1581
|
+
self._configure_role_landing(
|
|
1582
|
+
"Control", "/ocpp/rfid/validator/", "RFID Tag Validator"
|
|
1583
|
+
)
|
|
1547
1584
|
role = self.node.role
|
|
1548
1585
|
group = SecurityGroup.objects.create(name="Operators")
|
|
1549
1586
|
group_landing = self._ensure_landing(role, "/ocpp/group/", "Group Landing")
|
|
@@ -1560,7 +1597,9 @@ class RoleLandingRedirectTests(TestCase):
|
|
|
1560
1597
|
)
|
|
1561
1598
|
|
|
1562
1599
|
def test_user_redirect_overrides_group_with_higher_priority(self):
|
|
1563
|
-
self._configure_role_landing(
|
|
1600
|
+
self._configure_role_landing(
|
|
1601
|
+
"Control", "/ocpp/rfid/validator/", "RFID Tag Validator"
|
|
1602
|
+
)
|
|
1564
1603
|
role = self.node.role
|
|
1565
1604
|
group = SecurityGroup.objects.create(name="Operators")
|
|
1566
1605
|
group_landing = self._ensure_landing(role, "/ocpp/group/", "Group Landing")
|
|
@@ -1582,10 +1621,10 @@ class RoleLandingRedirectTests(TestCase):
|
|
|
1582
1621
|
)
|
|
1583
1622
|
|
|
1584
1623
|
|
|
1585
|
-
class
|
|
1624
|
+
class WatchtowerNavTests(TestCase):
|
|
1586
1625
|
def setUp(self):
|
|
1587
1626
|
self.client = Client()
|
|
1588
|
-
role, _ = NodeRole.objects.get_or_create(name="
|
|
1627
|
+
role, _ = NodeRole.objects.get_or_create(name="Watchtower")
|
|
1589
1628
|
Node.objects.update_or_create(
|
|
1590
1629
|
mac_address=Node.get_current_mac(),
|
|
1591
1630
|
defaults={
|
|
@@ -1595,38 +1634,56 @@ class ConstellationNavTests(TestCase):
|
|
|
1595
1634
|
},
|
|
1596
1635
|
)
|
|
1597
1636
|
Site.objects.update_or_create(
|
|
1598
|
-
id=1, defaults={"domain": "
|
|
1637
|
+
id=1, defaults={"domain": "arthexis.com", "name": "Arthexis"}
|
|
1599
1638
|
)
|
|
1600
1639
|
fixtures = [
|
|
1601
1640
|
Path(
|
|
1602
1641
|
settings.BASE_DIR,
|
|
1603
1642
|
"pages",
|
|
1604
1643
|
"fixtures",
|
|
1605
|
-
"
|
|
1644
|
+
"default__application_pages.json",
|
|
1645
|
+
),
|
|
1646
|
+
Path(
|
|
1647
|
+
settings.BASE_DIR,
|
|
1648
|
+
"pages",
|
|
1649
|
+
"fixtures",
|
|
1650
|
+
"watchtower__application_ocpp.json",
|
|
1606
1651
|
),
|
|
1607
1652
|
Path(
|
|
1608
1653
|
settings.BASE_DIR,
|
|
1609
1654
|
"pages",
|
|
1610
1655
|
"fixtures",
|
|
1611
|
-
"
|
|
1656
|
+
"watchtower__module_ocpp.json",
|
|
1612
1657
|
),
|
|
1613
1658
|
Path(
|
|
1614
1659
|
settings.BASE_DIR,
|
|
1615
1660
|
"pages",
|
|
1616
1661
|
"fixtures",
|
|
1617
|
-
"
|
|
1662
|
+
"watchtower__landing_ocpp_dashboard.json",
|
|
1618
1663
|
),
|
|
1619
1664
|
Path(
|
|
1620
1665
|
settings.BASE_DIR,
|
|
1621
1666
|
"pages",
|
|
1622
1667
|
"fixtures",
|
|
1623
|
-
"
|
|
1668
|
+
"watchtower__landing_ocpp_cp_simulator.json",
|
|
1624
1669
|
),
|
|
1625
1670
|
Path(
|
|
1626
1671
|
settings.BASE_DIR,
|
|
1627
1672
|
"pages",
|
|
1628
1673
|
"fixtures",
|
|
1629
|
-
"
|
|
1674
|
+
"watchtower__landing_ocpp_rfid.json",
|
|
1675
|
+
),
|
|
1676
|
+
Path(
|
|
1677
|
+
settings.BASE_DIR,
|
|
1678
|
+
"pages",
|
|
1679
|
+
"fixtures",
|
|
1680
|
+
"watchtower__module_readme.json",
|
|
1681
|
+
),
|
|
1682
|
+
Path(
|
|
1683
|
+
settings.BASE_DIR,
|
|
1684
|
+
"pages",
|
|
1685
|
+
"fixtures",
|
|
1686
|
+
"watchtower__landing_readme.json",
|
|
1630
1687
|
),
|
|
1631
1688
|
]
|
|
1632
1689
|
call_command("loaddata", *map(str, fixtures))
|
|
@@ -1639,13 +1696,13 @@ class ConstellationNavTests(TestCase):
|
|
|
1639
1696
|
self.assertNotIn("RFID", nav_labels)
|
|
1640
1697
|
self.assertTrue(
|
|
1641
1698
|
Module.objects.filter(
|
|
1642
|
-
path="/ocpp/", node_role__name="
|
|
1699
|
+
path="/ocpp/", node_role__name="Watchtower"
|
|
1643
1700
|
).exists()
|
|
1644
1701
|
)
|
|
1645
1702
|
self.assertFalse(
|
|
1646
1703
|
Module.objects.filter(
|
|
1647
1704
|
path="/ocpp/rfid/",
|
|
1648
|
-
node_role__name="
|
|
1705
|
+
node_role__name="Watchtower",
|
|
1649
1706
|
is_deleted=False,
|
|
1650
1707
|
).exists()
|
|
1651
1708
|
)
|
|
@@ -1657,9 +1714,16 @@ class ConstellationNavTests(TestCase):
|
|
|
1657
1714
|
landing_labels = [landing.label for landing in ocpp_module.enabled_landings]
|
|
1658
1715
|
self.assertIn("RFID Tag Validator", landing_labels)
|
|
1659
1716
|
|
|
1717
|
+
@override_settings(ALLOWED_HOSTS=["testserver", "arthexis.com"])
|
|
1718
|
+
def test_cookbooks_pill_visible_for_arthexis(self):
|
|
1719
|
+
resp = self.client.get(
|
|
1720
|
+
reverse("pages:index"), HTTP_HOST="arthexis.com"
|
|
1721
|
+
)
|
|
1722
|
+
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOKS')
|
|
1723
|
+
|
|
1660
1724
|
def test_ocpp_dashboard_visible(self):
|
|
1661
1725
|
resp = self.client.get(reverse("pages:index"))
|
|
1662
|
-
self.assertContains(resp, 'href="/ocpp/"')
|
|
1726
|
+
self.assertContains(resp, 'href="/ocpp/cpms/dashboard/"')
|
|
1663
1727
|
|
|
1664
1728
|
|
|
1665
1729
|
class ReleaseModuleNavTests(TestCase):
|
|
@@ -1801,7 +1865,7 @@ class ControlNavTests(TestCase):
|
|
|
1801
1865
|
self.client.force_login(user)
|
|
1802
1866
|
resp = self.client.get(reverse("pages:index"))
|
|
1803
1867
|
self.assertEqual(resp.status_code, 200)
|
|
1804
|
-
self.assertContains(resp, 'href="/ocpp/"')
|
|
1868
|
+
self.assertContains(resp, 'href="/ocpp/cpms/dashboard/"')
|
|
1805
1869
|
self.assertContains(
|
|
1806
1870
|
resp, 'badge rounded-pill text-bg-secondary">CHARGERS'
|
|
1807
1871
|
)
|
|
@@ -1833,10 +1897,76 @@ class ControlNavTests(TestCase):
|
|
|
1833
1897
|
self.assertFalse(resp.context["header_references"])
|
|
1834
1898
|
self.assertNotContains(resp, "https://example.com/hidden")
|
|
1835
1899
|
|
|
1900
|
+
def test_header_link_hidden_when_only_site_matches(self):
|
|
1901
|
+
terminal_role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
1902
|
+
site = Site.objects.get(domain="testserver")
|
|
1903
|
+
reference = Reference.objects.create(
|
|
1904
|
+
alt_text="Restricted",
|
|
1905
|
+
value="https://example.com/restricted",
|
|
1906
|
+
show_in_header=True,
|
|
1907
|
+
)
|
|
1908
|
+
reference.roles.add(terminal_role)
|
|
1909
|
+
reference.sites.add(site)
|
|
1910
|
+
|
|
1911
|
+
resp = self.client.get(reverse("pages:index"))
|
|
1912
|
+
|
|
1913
|
+
self.assertIn("header_references", resp.context)
|
|
1914
|
+
self.assertFalse(resp.context["header_references"])
|
|
1915
|
+
self.assertNotContains(resp, "https://example.com/restricted")
|
|
1916
|
+
|
|
1836
1917
|
def test_readme_pill_visible(self):
|
|
1837
1918
|
resp = self.client.get(reverse("pages:readme"))
|
|
1838
|
-
self.assertContains(resp, 'href="/
|
|
1839
|
-
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">
|
|
1919
|
+
self.assertContains(resp, 'href="/read/"')
|
|
1920
|
+
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOKS')
|
|
1921
|
+
|
|
1922
|
+
def test_cookbook_pill_has_no_dropdown(self):
|
|
1923
|
+
module = Module.objects.get(node_role__name="Control", path="/read/")
|
|
1924
|
+
Landing.objects.create(
|
|
1925
|
+
module=module,
|
|
1926
|
+
path="/man/",
|
|
1927
|
+
label="Manuals",
|
|
1928
|
+
enabled=True,
|
|
1929
|
+
)
|
|
1930
|
+
|
|
1931
|
+
resp = self.client.get(reverse("pages:readme"))
|
|
1932
|
+
|
|
1933
|
+
self.assertContains(
|
|
1934
|
+
resp,
|
|
1935
|
+
'<a class="nav-link" href="/read/"><span class="badge rounded-pill text-bg-secondary">COOKBOOKS</span></a>',
|
|
1936
|
+
html=True,
|
|
1937
|
+
)
|
|
1938
|
+
self.assertNotContains(resp, 'dropdown-item" href="/man/"')
|
|
1939
|
+
|
|
1940
|
+
def test_readme_page_includes_qr_share(self):
|
|
1941
|
+
resp = self.client.get(reverse("pages:readme"), {"section": "intro"})
|
|
1942
|
+
self.assertContains(resp, 'id="reader-qr"')
|
|
1943
|
+
self.assertContains(
|
|
1944
|
+
resp,
|
|
1945
|
+
'data-url="http://testserver/read/?section=intro"',
|
|
1946
|
+
)
|
|
1947
|
+
self.assertNotContains(resp, "Scan this page")
|
|
1948
|
+
self.assertNotContains(
|
|
1949
|
+
resp, 'class="small text-break text-muted mt-3 mb-0"'
|
|
1950
|
+
)
|
|
1951
|
+
|
|
1952
|
+
def test_readme_document_by_name(self):
|
|
1953
|
+
resp = self.client.get(reverse("pages:readme-document", args=["AGENTS.md"]))
|
|
1954
|
+
self.assertEqual(resp.status_code, 200)
|
|
1955
|
+
self.assertContains(resp, "Agent Guidelines")
|
|
1956
|
+
|
|
1957
|
+
def test_readme_document_by_relative_path(self):
|
|
1958
|
+
resp = self.client.get(
|
|
1959
|
+
reverse(
|
|
1960
|
+
"pages:readme-document",
|
|
1961
|
+
args=["docs/development/maintenance-roadmap.md"],
|
|
1962
|
+
)
|
|
1963
|
+
)
|
|
1964
|
+
self.assertEqual(resp.status_code, 200)
|
|
1965
|
+
self.assertContains(resp, "Maintenance Improvement Proposals")
|
|
1966
|
+
|
|
1967
|
+
def test_readme_document_rejects_traversal(self):
|
|
1968
|
+
resp = self.client.get("/read/../../SECRET.md")
|
|
1969
|
+
self.assertEqual(resp.status_code, 404)
|
|
1840
1970
|
|
|
1841
1971
|
|
|
1842
1972
|
class SatelliteNavTests(TestCase):
|
|
@@ -1908,8 +2038,8 @@ class SatelliteNavTests(TestCase):
|
|
|
1908
2038
|
|
|
1909
2039
|
def test_readme_pill_visible(self):
|
|
1910
2040
|
resp = self.client.get(reverse("pages:readme"))
|
|
1911
|
-
self.assertContains(resp, 'href="/
|
|
1912
|
-
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">
|
|
2041
|
+
self.assertContains(resp, 'href="/read/"')
|
|
2042
|
+
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOKS')
|
|
1913
2043
|
|
|
1914
2044
|
|
|
1915
2045
|
class PowerNavTests(TestCase):
|
|
@@ -1944,9 +2074,9 @@ class PowerNavTests(TestCase):
|
|
|
1944
2074
|
power_module = module
|
|
1945
2075
|
break
|
|
1946
2076
|
self.assertIsNotNone(power_module)
|
|
1947
|
-
self.assertEqual(power_module.menu_label.upper(), "
|
|
2077
|
+
self.assertEqual(power_module.menu_label.upper(), "CALCULATORS")
|
|
1948
2078
|
landing_labels = {landing.label for landing in power_module.enabled_landings}
|
|
1949
|
-
self.assertIn("AWG Calculator", landing_labels)
|
|
2079
|
+
self.assertIn("AWG Cable Calculator", landing_labels)
|
|
1950
2080
|
|
|
1951
2081
|
def test_manual_pill_label(self):
|
|
1952
2082
|
resp = self.client.get(reverse("pages:index"))
|
|
@@ -1970,9 +2100,26 @@ class PowerNavTests(TestCase):
|
|
|
1970
2100
|
break
|
|
1971
2101
|
self.assertIsNotNone(power_module)
|
|
1972
2102
|
landing_labels = {landing.label for landing in power_module.enabled_landings}
|
|
1973
|
-
self.assertIn("AWG Calculator", landing_labels)
|
|
2103
|
+
self.assertIn("AWG Cable Calculator", landing_labels)
|
|
1974
2104
|
self.assertIn("Energy Tariff Calculator", landing_labels)
|
|
1975
2105
|
|
|
2106
|
+
def test_locked_landing_shows_lock_icon(self):
|
|
2107
|
+
resp = self.client.get(reverse("pages:index"))
|
|
2108
|
+
html = resp.content.decode()
|
|
2109
|
+
energy_index = html.find("Energy Tariff Calculator")
|
|
2110
|
+
self.assertGreaterEqual(energy_index, 0)
|
|
2111
|
+
icon_index = html.find("dropdown-lock-icon", energy_index, energy_index + 300)
|
|
2112
|
+
self.assertGreaterEqual(icon_index, 0)
|
|
2113
|
+
|
|
2114
|
+
def test_lock_icon_disappears_after_login(self):
|
|
2115
|
+
self.client.force_login(self.user)
|
|
2116
|
+
resp = self.client.get(reverse("pages:index"))
|
|
2117
|
+
html = resp.content.decode()
|
|
2118
|
+
energy_index = html.find("Energy Tariff Calculator")
|
|
2119
|
+
self.assertGreaterEqual(energy_index, 0)
|
|
2120
|
+
icon_index = html.find("dropdown-lock-icon", energy_index, energy_index + 300)
|
|
2121
|
+
self.assertEqual(icon_index, -1)
|
|
2122
|
+
|
|
1976
2123
|
|
|
1977
2124
|
class StaffNavVisibilityTests(TestCase):
|
|
1978
2125
|
def setUp(self):
|
|
@@ -1994,12 +2141,12 @@ class StaffNavVisibilityTests(TestCase):
|
|
|
1994
2141
|
def test_nonstaff_pill_hidden(self):
|
|
1995
2142
|
self.client.login(username="user", password="pw")
|
|
1996
2143
|
resp = self.client.get(reverse("pages:index"))
|
|
1997
|
-
self.assertContains(resp, 'href="/ocpp/"')
|
|
2144
|
+
self.assertContains(resp, 'href="/ocpp/cpms/dashboard/"')
|
|
1998
2145
|
|
|
1999
2146
|
def test_staff_sees_pill(self):
|
|
2000
2147
|
self.client.login(username="staff", password="pw")
|
|
2001
2148
|
resp = self.client.get(reverse("pages:index"))
|
|
2002
|
-
self.assertContains(resp, 'href="/ocpp/"')
|
|
2149
|
+
self.assertContains(resp, 'href="/ocpp/cpms/dashboard/"')
|
|
2003
2150
|
|
|
2004
2151
|
|
|
2005
2152
|
class ModuleAdminReloadActionTests(TestCase):
|
|
@@ -2012,7 +2159,7 @@ class ModuleAdminReloadActionTests(TestCase):
|
|
|
2012
2159
|
password="pw",
|
|
2013
2160
|
)
|
|
2014
2161
|
self.client.force_login(self.superuser)
|
|
2015
|
-
self.role, _ = NodeRole.objects.get_or_create(name="
|
|
2162
|
+
self.role, _ = NodeRole.objects.get_or_create(name="Watchtower")
|
|
2016
2163
|
Application.objects.get_or_create(name="ocpp")
|
|
2017
2164
|
Application.objects.get_or_create(name="awg")
|
|
2018
2165
|
Site.objects.update_or_create(
|
|
@@ -2050,7 +2197,11 @@ class ModuleAdminReloadActionTests(TestCase):
|
|
|
2050
2197
|
)
|
|
2051
2198
|
self.assertSetEqual(
|
|
2052
2199
|
charger_landings,
|
|
2053
|
-
{
|
|
2200
|
+
{
|
|
2201
|
+
"/ocpp/cpms/dashboard/",
|
|
2202
|
+
"/ocpp/evcs/simulator/",
|
|
2203
|
+
"/ocpp/rfid/validator/",
|
|
2204
|
+
},
|
|
2054
2205
|
)
|
|
2055
2206
|
|
|
2056
2207
|
calculator_landings = set(
|
|
@@ -2188,6 +2339,47 @@ class UserManualAdminFormTests(TestCase):
|
|
|
2188
2339
|
self.assertEqual(form.cleaned_data["content_pdf"], self.manual.content_pdf)
|
|
2189
2340
|
|
|
2190
2341
|
|
|
2342
|
+
class UserManualModelTests(TestCase):
|
|
2343
|
+
def _build_manual(self, **overrides):
|
|
2344
|
+
defaults = {
|
|
2345
|
+
"slug": "manual-model-test",
|
|
2346
|
+
"title": "Manual Model",
|
|
2347
|
+
"description": "Manual description",
|
|
2348
|
+
"languages": "en",
|
|
2349
|
+
"content_html": "<p>Manual</p>",
|
|
2350
|
+
"content_pdf": base64.b64encode(b"initial").decode("ascii"),
|
|
2351
|
+
}
|
|
2352
|
+
defaults.update(overrides)
|
|
2353
|
+
return UserManual(**defaults)
|
|
2354
|
+
|
|
2355
|
+
def test_save_encodes_uploaded_file(self):
|
|
2356
|
+
upload = SimpleUploadedFile("manual.pdf", b"PDF data")
|
|
2357
|
+
manual = self._build_manual(slug="manual-upload", content_pdf=upload)
|
|
2358
|
+
manual.save()
|
|
2359
|
+
manual.refresh_from_db()
|
|
2360
|
+
self.assertEqual(
|
|
2361
|
+
manual.content_pdf,
|
|
2362
|
+
base64.b64encode(b"PDF data").decode("ascii"),
|
|
2363
|
+
)
|
|
2364
|
+
|
|
2365
|
+
def test_save_encodes_raw_bytes(self):
|
|
2366
|
+
manual = self._build_manual(slug="manual-bytes", content_pdf=b"PDF raw")
|
|
2367
|
+
manual.save()
|
|
2368
|
+
manual.refresh_from_db()
|
|
2369
|
+
self.assertEqual(
|
|
2370
|
+
manual.content_pdf,
|
|
2371
|
+
base64.b64encode(b"PDF raw").decode("ascii"),
|
|
2372
|
+
)
|
|
2373
|
+
|
|
2374
|
+
def test_save_strips_data_uri_prefix(self):
|
|
2375
|
+
encoded = base64.b64encode(b"PDF data").decode("ascii")
|
|
2376
|
+
data_uri = f"data:application/pdf;base64,{encoded}"
|
|
2377
|
+
manual = self._build_manual(slug="manual-data-uri", content_pdf=data_uri)
|
|
2378
|
+
manual.save()
|
|
2379
|
+
manual.refresh_from_db()
|
|
2380
|
+
self.assertEqual(manual.content_pdf, encoded)
|
|
2381
|
+
|
|
2382
|
+
|
|
2191
2383
|
class LandingCreationTests(TestCase):
|
|
2192
2384
|
def setUp(self):
|
|
2193
2385
|
role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
@@ -2209,12 +2401,12 @@ class LandingCreationTests(TestCase):
|
|
|
2209
2401
|
|
|
2210
2402
|
|
|
2211
2403
|
class LandingFixtureTests(TestCase):
|
|
2212
|
-
def
|
|
2404
|
+
def test_watchtower_fixture_loads_without_duplicates(self):
|
|
2213
2405
|
from glob import glob
|
|
2214
2406
|
|
|
2215
|
-
NodeRole.objects.get_or_create(name="
|
|
2407
|
+
NodeRole.objects.get_or_create(name="Watchtower")
|
|
2216
2408
|
fixtures = glob(
|
|
2217
|
-
str(Path(settings.BASE_DIR, "pages", "fixtures", "
|
|
2409
|
+
str(Path(settings.BASE_DIR, "pages", "fixtures", "watchtower__*.json"))
|
|
2218
2410
|
)
|
|
2219
2411
|
fixtures = sorted(
|
|
2220
2412
|
fixtures,
|
|
@@ -2224,9 +2416,11 @@ class LandingFixtureTests(TestCase):
|
|
|
2224
2416
|
)
|
|
2225
2417
|
call_command("loaddata", *fixtures)
|
|
2226
2418
|
call_command("loaddata", *fixtures)
|
|
2227
|
-
module = Module.objects.get(path="/ocpp/", node_role__name="
|
|
2419
|
+
module = Module.objects.get(path="/ocpp/", node_role__name="Watchtower")
|
|
2228
2420
|
module.create_landings()
|
|
2229
|
-
self.assertEqual(
|
|
2421
|
+
self.assertEqual(
|
|
2422
|
+
module.landings.filter(path="/ocpp/rfid/validator/").count(), 1
|
|
2423
|
+
)
|
|
2230
2424
|
|
|
2231
2425
|
|
|
2232
2426
|
class AllowedHostSubnetTests(TestCase):
|
|
@@ -2379,9 +2573,9 @@ class FaviconTests(TestCase):
|
|
|
2379
2573
|
)
|
|
2380
2574
|
self.assertContains(resp, b64)
|
|
2381
2575
|
|
|
2382
|
-
def
|
|
2576
|
+
def test_watchtower_nodes_use_goldenrod_favicon(self):
|
|
2383
2577
|
with override_settings(MEDIA_ROOT=self.tmpdir):
|
|
2384
|
-
role, _ = NodeRole.objects.get_or_create(name="
|
|
2578
|
+
role, _ = NodeRole.objects.get_or_create(name="Watchtower")
|
|
2385
2579
|
Node.objects.update_or_create(
|
|
2386
2580
|
mac_address=Node.get_current_mac(),
|
|
2387
2581
|
defaults={
|
|
@@ -2396,7 +2590,7 @@ class FaviconTests(TestCase):
|
|
|
2396
2590
|
resp = self.client.get(reverse("pages:index"))
|
|
2397
2591
|
b64 = (
|
|
2398
2592
|
Path(settings.BASE_DIR)
|
|
2399
|
-
.joinpath("pages", "fixtures", "data", "
|
|
2593
|
+
.joinpath("pages", "fixtures", "data", "favicon_watchtower.txt")
|
|
2400
2594
|
.read_text()
|
|
2401
2595
|
.strip()
|
|
2402
2596
|
)
|
|
@@ -2547,20 +2741,41 @@ class FavoriteTests(TestCase):
|
|
|
2547
2741
|
self.assertContains(resp, f'aria-label="{badge_label}"')
|
|
2548
2742
|
|
|
2549
2743
|
def test_dashboard_shows_charge_point_availability_badge(self):
|
|
2550
|
-
Charger.objects.create(charger_id="CP-001", last_status="Available")
|
|
2551
2744
|
Charger.objects.create(
|
|
2552
2745
|
charger_id="CP-001", connector_id=1, last_status="Available"
|
|
2553
2746
|
)
|
|
2747
|
+
Charger.objects.create(charger_id="CP-002", last_status="Available")
|
|
2748
|
+
Charger.objects.create(
|
|
2749
|
+
charger_id="CP-003", connector_id=1, last_status="Unavailable"
|
|
2750
|
+
)
|
|
2751
|
+
|
|
2752
|
+
resp = self.client.get(reverse("admin:index"))
|
|
2753
|
+
|
|
2754
|
+
expected = "1 / 2"
|
|
2755
|
+
badge_label = gettext(
|
|
2756
|
+
"%(available)s chargers reporting Available status with a CP number, out of %(total)s total Available chargers. %(missing)s Available chargers are missing a connector number."
|
|
2757
|
+
) % {"available": 1, "total": 2, "missing": 1}
|
|
2758
|
+
|
|
2759
|
+
self.assertContains(resp, expected)
|
|
2760
|
+
self.assertContains(resp, 'class="charger-availability-badge"')
|
|
2761
|
+
self.assertContains(resp, f'title="{badge_label}"')
|
|
2762
|
+
self.assertContains(resp, f'aria-label="{badge_label}"')
|
|
2763
|
+
|
|
2764
|
+
def test_dashboard_charge_point_badge_ignores_aggregator(self):
|
|
2765
|
+
Charger.objects.create(charger_id="CP-AGG", last_status="Available")
|
|
2766
|
+
Charger.objects.create(
|
|
2767
|
+
charger_id="CP-AGG", connector_id=1, last_status="Available"
|
|
2768
|
+
)
|
|
2554
2769
|
Charger.objects.create(
|
|
2555
|
-
charger_id="CP-
|
|
2770
|
+
charger_id="CP-AGG", connector_id=2, last_status="Available"
|
|
2556
2771
|
)
|
|
2557
2772
|
|
|
2558
2773
|
resp = self.client.get(reverse("admin:index"))
|
|
2559
2774
|
|
|
2560
|
-
expected = "2 /
|
|
2775
|
+
expected = "2 / 2"
|
|
2561
2776
|
badge_label = gettext(
|
|
2562
|
-
"%(
|
|
2563
|
-
) % {"
|
|
2777
|
+
"%(available)s chargers reporting Available status with a CP number."
|
|
2778
|
+
) % {"available": 2}
|
|
2564
2779
|
|
|
2565
2780
|
self.assertContains(resp, expected)
|
|
2566
2781
|
self.assertContains(resp, 'class="charger-availability-badge"')
|
pages/urls.py
CHANGED
|
@@ -6,7 +6,9 @@ app_name = "pages"
|
|
|
6
6
|
|
|
7
7
|
urlpatterns = [
|
|
8
8
|
path("", views.index, name="index"),
|
|
9
|
-
path("
|
|
9
|
+
path("read/<path:doc>/edit/", views.readme_edit, name="readme-edit"),
|
|
10
|
+
path("read/", views.readme, name="readme"),
|
|
11
|
+
path("read/<path:doc>", views.readme, name="readme-document"),
|
|
10
12
|
path("sitemap.xml", views.sitemap, name="pages-sitemap"),
|
|
11
13
|
path("release/", views.release_admin_redirect, name="release-admin"),
|
|
12
14
|
path("client-report/", views.client_report, name="client-report"),
|