arthexis 0.1.19__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.19.dist-info → arthexis-0.1.20.dist-info}/METADATA +3 -3
- {arthexis-0.1.19.dist-info → arthexis-0.1.20.dist-info}/RECORD +38 -38
- 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/tasks.py +24 -1
- core/tests.py +1 -0
- core/views.py +70 -36
- nodes/admin.py +133 -1
- nodes/models.py +294 -48
- nodes/rfid_sync.py +1 -1
- nodes/tasks.py +100 -2
- nodes/tests.py +532 -15
- nodes/urls.py +4 -0
- nodes/views.py +500 -95
- ocpp/admin.py +101 -3
- ocpp/consumers.py +106 -9
- ocpp/models.py +83 -1
- ocpp/tasks.py +4 -0
- ocpp/test_export_import.py +1 -0
- ocpp/test_rfid.py +3 -1
- ocpp/tests.py +100 -9
- ocpp/transactions_io.py +9 -1
- ocpp/urls.py +3 -3
- ocpp/views.py +101 -28
- pages/context_processors.py +15 -9
- pages/defaults.py +1 -1
- pages/module_defaults.py +5 -5
- pages/tests.py +110 -38
- pages/urls.py +1 -0
- pages/views.py +108 -8
- {arthexis-0.1.19.dist-info → arthexis-0.1.20.dist-info}/WHEEL +0 -0
- {arthexis-0.1.19.dist-info → arthexis-0.1.20.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.19.dist-info → arthexis-0.1.20.dist-info}/top_level.txt +0 -0
pages/tests.py
CHANGED
|
@@ -3,6 +3,7 @@ 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
|
|
|
@@ -508,6 +509,7 @@ class InvitationTests(TestCase):
|
|
|
508
509
|
lead = InviteLead.objects.get()
|
|
509
510
|
self.assertEqual(lead.mac_address, "aa:bb:cc:dd:ee:ff")
|
|
510
511
|
|
|
512
|
+
@pytest.mark.feature("ap-router")
|
|
511
513
|
@patch("pages.views.public_wifi.grant_public_access")
|
|
512
514
|
@patch(
|
|
513
515
|
"pages.views.public_wifi.resolve_mac_address",
|
|
@@ -1360,6 +1362,7 @@ class SiteAdminRegisterCurrentTests(TestCase):
|
|
|
1360
1362
|
self.assertEqual(site.name, "")
|
|
1361
1363
|
|
|
1362
1364
|
|
|
1365
|
+
@pytest.mark.feature("screenshot-poll")
|
|
1363
1366
|
class SiteAdminScreenshotTests(TestCase):
|
|
1364
1367
|
def setUp(self):
|
|
1365
1368
|
self.client = Client()
|
|
@@ -1479,17 +1482,17 @@ class NavAppsTests(TestCase):
|
|
|
1479
1482
|
)
|
|
1480
1483
|
app = Application.objects.create(name="Readme")
|
|
1481
1484
|
Module.objects.create(
|
|
1482
|
-
node_role=role, application=app, path="/", is_default=True, menu="
|
|
1485
|
+
node_role=role, application=app, path="/", is_default=True, menu="Cookbooks"
|
|
1483
1486
|
)
|
|
1484
1487
|
|
|
1485
1488
|
def test_nav_pill_renders(self):
|
|
1486
1489
|
resp = self.client.get(reverse("pages:index"))
|
|
1487
|
-
self.assertContains(resp, "
|
|
1490
|
+
self.assertContains(resp, "COOKBOOKS")
|
|
1488
1491
|
self.assertContains(resp, "badge rounded-pill")
|
|
1489
1492
|
|
|
1490
1493
|
def test_nav_pill_renders_with_port(self):
|
|
1491
1494
|
resp = self.client.get(reverse("pages:index"), HTTP_HOST="127.0.0.1:8000")
|
|
1492
|
-
self.assertContains(resp, "
|
|
1495
|
+
self.assertContains(resp, "COOKBOOKS")
|
|
1493
1496
|
|
|
1494
1497
|
def test_nav_pill_uses_menu_field(self):
|
|
1495
1498
|
site_app = Module.objects.get()
|
|
@@ -1497,7 +1500,7 @@ class NavAppsTests(TestCase):
|
|
|
1497
1500
|
site_app.save()
|
|
1498
1501
|
resp = self.client.get(reverse("pages:index"))
|
|
1499
1502
|
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">DOCS')
|
|
1500
|
-
self.assertNotContains(resp, 'badge rounded-pill text-bg-secondary">
|
|
1503
|
+
self.assertNotContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOKS')
|
|
1501
1504
|
|
|
1502
1505
|
def test_app_without_root_url_excluded(self):
|
|
1503
1506
|
role = NodeRole.objects.get(name="Terminal")
|
|
@@ -1562,20 +1565,22 @@ class RoleLandingRedirectTests(TestCase):
|
|
|
1562
1565
|
|
|
1563
1566
|
def test_satellite_redirects_to_dashboard(self):
|
|
1564
1567
|
target = self._configure_role_landing(
|
|
1565
|
-
"Satellite", "/ocpp/", "CPMS Online Dashboard"
|
|
1568
|
+
"Satellite", "/ocpp/cpms/dashboard/", "CPMS Online Dashboard"
|
|
1566
1569
|
)
|
|
1567
1570
|
resp = self.client.get(reverse("pages:index"))
|
|
1568
1571
|
self.assertRedirects(resp, target, fetch_redirect_response=False)
|
|
1569
1572
|
|
|
1570
1573
|
def test_control_redirects_to_rfid(self):
|
|
1571
1574
|
target = self._configure_role_landing(
|
|
1572
|
-
"Control", "/ocpp/rfid/", "RFID Tag Validator"
|
|
1575
|
+
"Control", "/ocpp/rfid/validator/", "RFID Tag Validator"
|
|
1573
1576
|
)
|
|
1574
1577
|
resp = self.client.get(reverse("pages:index"))
|
|
1575
1578
|
self.assertRedirects(resp, target, fetch_redirect_response=False)
|
|
1576
1579
|
|
|
1577
1580
|
def test_security_group_redirect_takes_priority(self):
|
|
1578
|
-
self._configure_role_landing(
|
|
1581
|
+
self._configure_role_landing(
|
|
1582
|
+
"Control", "/ocpp/rfid/validator/", "RFID Tag Validator"
|
|
1583
|
+
)
|
|
1579
1584
|
role = self.node.role
|
|
1580
1585
|
group = SecurityGroup.objects.create(name="Operators")
|
|
1581
1586
|
group_landing = self._ensure_landing(role, "/ocpp/group/", "Group Landing")
|
|
@@ -1592,7 +1597,9 @@ class RoleLandingRedirectTests(TestCase):
|
|
|
1592
1597
|
)
|
|
1593
1598
|
|
|
1594
1599
|
def test_user_redirect_overrides_group_with_higher_priority(self):
|
|
1595
|
-
self._configure_role_landing(
|
|
1600
|
+
self._configure_role_landing(
|
|
1601
|
+
"Control", "/ocpp/rfid/validator/", "RFID Tag Validator"
|
|
1602
|
+
)
|
|
1596
1603
|
role = self.node.role
|
|
1597
1604
|
group = SecurityGroup.objects.create(name="Operators")
|
|
1598
1605
|
group_landing = self._ensure_landing(role, "/ocpp/group/", "Group Landing")
|
|
@@ -1614,10 +1621,10 @@ class RoleLandingRedirectTests(TestCase):
|
|
|
1614
1621
|
)
|
|
1615
1622
|
|
|
1616
1623
|
|
|
1617
|
-
class
|
|
1624
|
+
class WatchtowerNavTests(TestCase):
|
|
1618
1625
|
def setUp(self):
|
|
1619
1626
|
self.client = Client()
|
|
1620
|
-
role, _ = NodeRole.objects.get_or_create(name="
|
|
1627
|
+
role, _ = NodeRole.objects.get_or_create(name="Watchtower")
|
|
1621
1628
|
Node.objects.update_or_create(
|
|
1622
1629
|
mac_address=Node.get_current_mac(),
|
|
1623
1630
|
defaults={
|
|
@@ -1627,38 +1634,56 @@ class ConstellationNavTests(TestCase):
|
|
|
1627
1634
|
},
|
|
1628
1635
|
)
|
|
1629
1636
|
Site.objects.update_or_create(
|
|
1630
|
-
id=1, defaults={"domain": "
|
|
1637
|
+
id=1, defaults={"domain": "arthexis.com", "name": "Arthexis"}
|
|
1631
1638
|
)
|
|
1632
1639
|
fixtures = [
|
|
1633
1640
|
Path(
|
|
1634
1641
|
settings.BASE_DIR,
|
|
1635
1642
|
"pages",
|
|
1636
1643
|
"fixtures",
|
|
1637
|
-
"
|
|
1644
|
+
"default__application_pages.json",
|
|
1638
1645
|
),
|
|
1639
1646
|
Path(
|
|
1640
1647
|
settings.BASE_DIR,
|
|
1641
1648
|
"pages",
|
|
1642
1649
|
"fixtures",
|
|
1643
|
-
"
|
|
1650
|
+
"watchtower__application_ocpp.json",
|
|
1644
1651
|
),
|
|
1645
1652
|
Path(
|
|
1646
1653
|
settings.BASE_DIR,
|
|
1647
1654
|
"pages",
|
|
1648
1655
|
"fixtures",
|
|
1649
|
-
"
|
|
1656
|
+
"watchtower__module_ocpp.json",
|
|
1650
1657
|
),
|
|
1651
1658
|
Path(
|
|
1652
1659
|
settings.BASE_DIR,
|
|
1653
1660
|
"pages",
|
|
1654
1661
|
"fixtures",
|
|
1655
|
-
"
|
|
1662
|
+
"watchtower__landing_ocpp_dashboard.json",
|
|
1656
1663
|
),
|
|
1657
1664
|
Path(
|
|
1658
1665
|
settings.BASE_DIR,
|
|
1659
1666
|
"pages",
|
|
1660
1667
|
"fixtures",
|
|
1661
|
-
"
|
|
1668
|
+
"watchtower__landing_ocpp_cp_simulator.json",
|
|
1669
|
+
),
|
|
1670
|
+
Path(
|
|
1671
|
+
settings.BASE_DIR,
|
|
1672
|
+
"pages",
|
|
1673
|
+
"fixtures",
|
|
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",
|
|
1662
1687
|
),
|
|
1663
1688
|
]
|
|
1664
1689
|
call_command("loaddata", *map(str, fixtures))
|
|
@@ -1671,13 +1696,13 @@ class ConstellationNavTests(TestCase):
|
|
|
1671
1696
|
self.assertNotIn("RFID", nav_labels)
|
|
1672
1697
|
self.assertTrue(
|
|
1673
1698
|
Module.objects.filter(
|
|
1674
|
-
path="/ocpp/", node_role__name="
|
|
1699
|
+
path="/ocpp/", node_role__name="Watchtower"
|
|
1675
1700
|
).exists()
|
|
1676
1701
|
)
|
|
1677
1702
|
self.assertFalse(
|
|
1678
1703
|
Module.objects.filter(
|
|
1679
1704
|
path="/ocpp/rfid/",
|
|
1680
|
-
node_role__name="
|
|
1705
|
+
node_role__name="Watchtower",
|
|
1681
1706
|
is_deleted=False,
|
|
1682
1707
|
).exists()
|
|
1683
1708
|
)
|
|
@@ -1689,9 +1714,16 @@ class ConstellationNavTests(TestCase):
|
|
|
1689
1714
|
landing_labels = [landing.label for landing in ocpp_module.enabled_landings]
|
|
1690
1715
|
self.assertIn("RFID Tag Validator", landing_labels)
|
|
1691
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
|
+
|
|
1692
1724
|
def test_ocpp_dashboard_visible(self):
|
|
1693
1725
|
resp = self.client.get(reverse("pages:index"))
|
|
1694
|
-
self.assertContains(resp, 'href="/ocpp/"')
|
|
1726
|
+
self.assertContains(resp, 'href="/ocpp/cpms/dashboard/"')
|
|
1695
1727
|
|
|
1696
1728
|
|
|
1697
1729
|
class ReleaseModuleNavTests(TestCase):
|
|
@@ -1833,7 +1865,7 @@ class ControlNavTests(TestCase):
|
|
|
1833
1865
|
self.client.force_login(user)
|
|
1834
1866
|
resp = self.client.get(reverse("pages:index"))
|
|
1835
1867
|
self.assertEqual(resp.status_code, 200)
|
|
1836
|
-
self.assertContains(resp, 'href="/ocpp/"')
|
|
1868
|
+
self.assertContains(resp, 'href="/ocpp/cpms/dashboard/"')
|
|
1837
1869
|
self.assertContains(
|
|
1838
1870
|
resp, 'badge rounded-pill text-bg-secondary">CHARGERS'
|
|
1839
1871
|
)
|
|
@@ -1865,10 +1897,27 @@ class ControlNavTests(TestCase):
|
|
|
1865
1897
|
self.assertFalse(resp.context["header_references"])
|
|
1866
1898
|
self.assertNotContains(resp, "https://example.com/hidden")
|
|
1867
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
|
+
|
|
1868
1917
|
def test_readme_pill_visible(self):
|
|
1869
1918
|
resp = self.client.get(reverse("pages:readme"))
|
|
1870
1919
|
self.assertContains(resp, 'href="/read/"')
|
|
1871
|
-
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">
|
|
1920
|
+
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOKS')
|
|
1872
1921
|
|
|
1873
1922
|
def test_cookbook_pill_has_no_dropdown(self):
|
|
1874
1923
|
module = Module.objects.get(node_role__name="Control", path="/read/")
|
|
@@ -1883,7 +1932,7 @@ class ControlNavTests(TestCase):
|
|
|
1883
1932
|
|
|
1884
1933
|
self.assertContains(
|
|
1885
1934
|
resp,
|
|
1886
|
-
'<a class="nav-link" href="/read/"><span class="badge rounded-pill text-bg-secondary">
|
|
1935
|
+
'<a class="nav-link" href="/read/"><span class="badge rounded-pill text-bg-secondary">COOKBOOKS</span></a>',
|
|
1887
1936
|
html=True,
|
|
1888
1937
|
)
|
|
1889
1938
|
self.assertNotContains(resp, 'dropdown-item" href="/man/"')
|
|
@@ -1990,7 +2039,7 @@ class SatelliteNavTests(TestCase):
|
|
|
1990
2039
|
def test_readme_pill_visible(self):
|
|
1991
2040
|
resp = self.client.get(reverse("pages:readme"))
|
|
1992
2041
|
self.assertContains(resp, 'href="/read/"')
|
|
1993
|
-
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">
|
|
2042
|
+
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOKS')
|
|
1994
2043
|
|
|
1995
2044
|
|
|
1996
2045
|
class PowerNavTests(TestCase):
|
|
@@ -2025,9 +2074,9 @@ class PowerNavTests(TestCase):
|
|
|
2025
2074
|
power_module = module
|
|
2026
2075
|
break
|
|
2027
2076
|
self.assertIsNotNone(power_module)
|
|
2028
|
-
self.assertEqual(power_module.menu_label.upper(), "
|
|
2077
|
+
self.assertEqual(power_module.menu_label.upper(), "CALCULATORS")
|
|
2029
2078
|
landing_labels = {landing.label for landing in power_module.enabled_landings}
|
|
2030
|
-
self.assertIn("AWG Calculator", landing_labels)
|
|
2079
|
+
self.assertIn("AWG Cable Calculator", landing_labels)
|
|
2031
2080
|
|
|
2032
2081
|
def test_manual_pill_label(self):
|
|
2033
2082
|
resp = self.client.get(reverse("pages:index"))
|
|
@@ -2051,9 +2100,26 @@ class PowerNavTests(TestCase):
|
|
|
2051
2100
|
break
|
|
2052
2101
|
self.assertIsNotNone(power_module)
|
|
2053
2102
|
landing_labels = {landing.label for landing in power_module.enabled_landings}
|
|
2054
|
-
self.assertIn("AWG Calculator", landing_labels)
|
|
2103
|
+
self.assertIn("AWG Cable Calculator", landing_labels)
|
|
2055
2104
|
self.assertIn("Energy Tariff Calculator", landing_labels)
|
|
2056
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
|
+
|
|
2057
2123
|
|
|
2058
2124
|
class StaffNavVisibilityTests(TestCase):
|
|
2059
2125
|
def setUp(self):
|
|
@@ -2075,12 +2141,12 @@ class StaffNavVisibilityTests(TestCase):
|
|
|
2075
2141
|
def test_nonstaff_pill_hidden(self):
|
|
2076
2142
|
self.client.login(username="user", password="pw")
|
|
2077
2143
|
resp = self.client.get(reverse("pages:index"))
|
|
2078
|
-
self.assertContains(resp, 'href="/ocpp/"')
|
|
2144
|
+
self.assertContains(resp, 'href="/ocpp/cpms/dashboard/"')
|
|
2079
2145
|
|
|
2080
2146
|
def test_staff_sees_pill(self):
|
|
2081
2147
|
self.client.login(username="staff", password="pw")
|
|
2082
2148
|
resp = self.client.get(reverse("pages:index"))
|
|
2083
|
-
self.assertContains(resp, 'href="/ocpp/"')
|
|
2149
|
+
self.assertContains(resp, 'href="/ocpp/cpms/dashboard/"')
|
|
2084
2150
|
|
|
2085
2151
|
|
|
2086
2152
|
class ModuleAdminReloadActionTests(TestCase):
|
|
@@ -2093,7 +2159,7 @@ class ModuleAdminReloadActionTests(TestCase):
|
|
|
2093
2159
|
password="pw",
|
|
2094
2160
|
)
|
|
2095
2161
|
self.client.force_login(self.superuser)
|
|
2096
|
-
self.role, _ = NodeRole.objects.get_or_create(name="
|
|
2162
|
+
self.role, _ = NodeRole.objects.get_or_create(name="Watchtower")
|
|
2097
2163
|
Application.objects.get_or_create(name="ocpp")
|
|
2098
2164
|
Application.objects.get_or_create(name="awg")
|
|
2099
2165
|
Site.objects.update_or_create(
|
|
@@ -2131,7 +2197,11 @@ class ModuleAdminReloadActionTests(TestCase):
|
|
|
2131
2197
|
)
|
|
2132
2198
|
self.assertSetEqual(
|
|
2133
2199
|
charger_landings,
|
|
2134
|
-
{
|
|
2200
|
+
{
|
|
2201
|
+
"/ocpp/cpms/dashboard/",
|
|
2202
|
+
"/ocpp/evcs/simulator/",
|
|
2203
|
+
"/ocpp/rfid/validator/",
|
|
2204
|
+
},
|
|
2135
2205
|
)
|
|
2136
2206
|
|
|
2137
2207
|
calculator_landings = set(
|
|
@@ -2331,12 +2401,12 @@ class LandingCreationTests(TestCase):
|
|
|
2331
2401
|
|
|
2332
2402
|
|
|
2333
2403
|
class LandingFixtureTests(TestCase):
|
|
2334
|
-
def
|
|
2404
|
+
def test_watchtower_fixture_loads_without_duplicates(self):
|
|
2335
2405
|
from glob import glob
|
|
2336
2406
|
|
|
2337
|
-
NodeRole.objects.get_or_create(name="
|
|
2407
|
+
NodeRole.objects.get_or_create(name="Watchtower")
|
|
2338
2408
|
fixtures = glob(
|
|
2339
|
-
str(Path(settings.BASE_DIR, "pages", "fixtures", "
|
|
2409
|
+
str(Path(settings.BASE_DIR, "pages", "fixtures", "watchtower__*.json"))
|
|
2340
2410
|
)
|
|
2341
2411
|
fixtures = sorted(
|
|
2342
2412
|
fixtures,
|
|
@@ -2346,9 +2416,11 @@ class LandingFixtureTests(TestCase):
|
|
|
2346
2416
|
)
|
|
2347
2417
|
call_command("loaddata", *fixtures)
|
|
2348
2418
|
call_command("loaddata", *fixtures)
|
|
2349
|
-
module = Module.objects.get(path="/ocpp/", node_role__name="
|
|
2419
|
+
module = Module.objects.get(path="/ocpp/", node_role__name="Watchtower")
|
|
2350
2420
|
module.create_landings()
|
|
2351
|
-
self.assertEqual(
|
|
2421
|
+
self.assertEqual(
|
|
2422
|
+
module.landings.filter(path="/ocpp/rfid/validator/").count(), 1
|
|
2423
|
+
)
|
|
2352
2424
|
|
|
2353
2425
|
|
|
2354
2426
|
class AllowedHostSubnetTests(TestCase):
|
|
@@ -2501,9 +2573,9 @@ class FaviconTests(TestCase):
|
|
|
2501
2573
|
)
|
|
2502
2574
|
self.assertContains(resp, b64)
|
|
2503
2575
|
|
|
2504
|
-
def
|
|
2576
|
+
def test_watchtower_nodes_use_goldenrod_favicon(self):
|
|
2505
2577
|
with override_settings(MEDIA_ROOT=self.tmpdir):
|
|
2506
|
-
role, _ = NodeRole.objects.get_or_create(name="
|
|
2578
|
+
role, _ = NodeRole.objects.get_or_create(name="Watchtower")
|
|
2507
2579
|
Node.objects.update_or_create(
|
|
2508
2580
|
mac_address=Node.get_current_mac(),
|
|
2509
2581
|
defaults={
|
|
@@ -2518,7 +2590,7 @@ class FaviconTests(TestCase):
|
|
|
2518
2590
|
resp = self.client.get(reverse("pages:index"))
|
|
2519
2591
|
b64 = (
|
|
2520
2592
|
Path(settings.BASE_DIR)
|
|
2521
|
-
.joinpath("pages", "fixtures", "data", "
|
|
2593
|
+
.joinpath("pages", "fixtures", "data", "favicon_watchtower.txt")
|
|
2522
2594
|
.read_text()
|
|
2523
2595
|
.strip()
|
|
2524
2596
|
)
|
pages/urls.py
CHANGED
|
@@ -6,6 +6,7 @@ app_name = "pages"
|
|
|
6
6
|
|
|
7
7
|
urlpatterns = [
|
|
8
8
|
path("", views.index, name="index"),
|
|
9
|
+
path("read/<path:doc>/edit/", views.readme_edit, name="readme-edit"),
|
|
9
10
|
path("read/", views.readme, name="readme"),
|
|
10
11
|
path("read/<path:doc>", views.readme, name="readme-document"),
|
|
11
12
|
path("sitemap.xml", views.sitemap, name="pages-sitemap"),
|
pages/views.py
CHANGED
|
@@ -439,7 +439,7 @@ def admin_model_graph(request, app_label: str):
|
|
|
439
439
|
return response
|
|
440
440
|
|
|
441
441
|
|
|
442
|
-
def
|
|
442
|
+
def _locate_readme_document(role, doc: str | None, lang: str) -> SimpleNamespace:
|
|
443
443
|
app = (
|
|
444
444
|
Module.objects.filter(node_role=role, is_default=True)
|
|
445
445
|
.select_related("application")
|
|
@@ -448,9 +448,8 @@ def _render_readme(request, role, doc: str | None = None):
|
|
|
448
448
|
app_slug = app.path.strip("/") if app else ""
|
|
449
449
|
root_base = Path(settings.BASE_DIR).resolve()
|
|
450
450
|
readme_base = (root_base / app_slug).resolve() if app_slug else root_base
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
candidates = []
|
|
451
|
+
candidates: list[Path] = []
|
|
452
|
+
|
|
454
453
|
if doc:
|
|
455
454
|
normalized = doc.strip().replace("\\", "/")
|
|
456
455
|
while normalized.startswith("./"):
|
|
@@ -501,23 +500,72 @@ def _render_readme(request, role, doc: str | None = None):
|
|
|
501
500
|
if short != lang:
|
|
502
501
|
candidates.append(root_base / f"README.{short}.md")
|
|
503
502
|
candidates.append(root_base / "README.md")
|
|
503
|
+
|
|
504
504
|
readme_file = next((p for p in candidates if p.exists()), None)
|
|
505
505
|
if readme_file is None:
|
|
506
506
|
raise Http404("Document not found")
|
|
507
|
-
|
|
508
|
-
html, toc_html = _render_markdown_with_toc(text)
|
|
507
|
+
|
|
509
508
|
title = "README" if readme_file.name.startswith("README") else readme_file.stem
|
|
509
|
+
return SimpleNamespace(
|
|
510
|
+
file=readme_file,
|
|
511
|
+
title=title,
|
|
512
|
+
root_base=root_base,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _relative_readme_path(readme_file: Path, root_base: Path) -> str | None:
|
|
517
|
+
try:
|
|
518
|
+
return readme_file.relative_to(root_base).as_posix()
|
|
519
|
+
except ValueError:
|
|
520
|
+
return None
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _render_readme(request, role, doc: str | None = None):
|
|
524
|
+
lang = getattr(request, "LANGUAGE_CODE", "")
|
|
525
|
+
lang = lang.replace("_", "-").lower()
|
|
526
|
+
document = _locate_readme_document(role, doc, lang)
|
|
527
|
+
text = document.file.read_text(encoding="utf-8")
|
|
528
|
+
html, toc_html = _render_markdown_with_toc(text)
|
|
529
|
+
relative_path = _relative_readme_path(document.file, document.root_base)
|
|
530
|
+
user = getattr(request, "user", None)
|
|
531
|
+
can_edit = bool(
|
|
532
|
+
relative_path
|
|
533
|
+
and user
|
|
534
|
+
and user.is_authenticated
|
|
535
|
+
and user.is_superuser
|
|
536
|
+
)
|
|
537
|
+
edit_url = None
|
|
538
|
+
if can_edit:
|
|
539
|
+
try:
|
|
540
|
+
edit_url = reverse("pages:readme-edit", kwargs={"doc": relative_path})
|
|
541
|
+
except NoReverseMatch:
|
|
542
|
+
edit_url = None
|
|
510
543
|
context = {
|
|
511
544
|
"content": html,
|
|
512
|
-
"title": title,
|
|
545
|
+
"title": document.title,
|
|
513
546
|
"toc": toc_html,
|
|
514
547
|
"page_url": request.build_absolute_uri(),
|
|
548
|
+
"edit_url": edit_url,
|
|
515
549
|
}
|
|
516
550
|
response = render(request, "pages/readme.html", context)
|
|
517
551
|
patch_vary_headers(response, ["Accept-Language", "Cookie"])
|
|
518
552
|
return response
|
|
519
553
|
|
|
520
554
|
|
|
555
|
+
class MarkdownDocumentForm(forms.Form):
|
|
556
|
+
content = forms.CharField(
|
|
557
|
+
widget=forms.Textarea(
|
|
558
|
+
attrs={
|
|
559
|
+
"class": "form-control",
|
|
560
|
+
"rows": 24,
|
|
561
|
+
"spellcheck": "false",
|
|
562
|
+
}
|
|
563
|
+
),
|
|
564
|
+
required=False,
|
|
565
|
+
strip=False,
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
|
|
521
569
|
@landing("Home")
|
|
522
570
|
@never_cache
|
|
523
571
|
def index(request):
|
|
@@ -573,6 +621,57 @@ def readme(request, doc=None):
|
|
|
573
621
|
return _render_readme(request, role, doc)
|
|
574
622
|
|
|
575
623
|
|
|
624
|
+
def readme_edit(request, doc):
|
|
625
|
+
user = getattr(request, "user", None)
|
|
626
|
+
if not (user and user.is_authenticated and user.is_superuser):
|
|
627
|
+
raise PermissionDenied
|
|
628
|
+
|
|
629
|
+
node = Node.get_local()
|
|
630
|
+
role = node.role if node else None
|
|
631
|
+
lang = getattr(request, "LANGUAGE_CODE", "")
|
|
632
|
+
lang = lang.replace("_", "-").lower()
|
|
633
|
+
document = _locate_readme_document(role, doc, lang)
|
|
634
|
+
relative_path = _relative_readme_path(document.file, document.root_base)
|
|
635
|
+
if relative_path:
|
|
636
|
+
read_url = reverse("pages:readme-document", kwargs={"doc": relative_path})
|
|
637
|
+
else:
|
|
638
|
+
read_url = reverse("pages:readme")
|
|
639
|
+
|
|
640
|
+
if request.method == "POST":
|
|
641
|
+
form = MarkdownDocumentForm(request.POST)
|
|
642
|
+
if form.is_valid():
|
|
643
|
+
content = form.cleaned_data["content"]
|
|
644
|
+
try:
|
|
645
|
+
document.file.write_text(content, encoding="utf-8")
|
|
646
|
+
except OSError:
|
|
647
|
+
logger.exception("Failed to update markdown document %s", document.file)
|
|
648
|
+
messages.error(
|
|
649
|
+
request,
|
|
650
|
+
_("Unable to save changes. Please try again."),
|
|
651
|
+
)
|
|
652
|
+
else:
|
|
653
|
+
messages.success(request, _("Document saved successfully."))
|
|
654
|
+
if relative_path:
|
|
655
|
+
return redirect("pages:readme-edit", doc=relative_path)
|
|
656
|
+
return redirect("pages:readme")
|
|
657
|
+
else:
|
|
658
|
+
try:
|
|
659
|
+
initial_text = document.file.read_text(encoding="utf-8")
|
|
660
|
+
except OSError:
|
|
661
|
+
logger.exception("Failed to read markdown document %s", document.file)
|
|
662
|
+
messages.error(request, _("Unable to load the document for editing."))
|
|
663
|
+
return redirect("pages:readme")
|
|
664
|
+
form = MarkdownDocumentForm(initial={"content": initial_text})
|
|
665
|
+
|
|
666
|
+
context = {
|
|
667
|
+
"form": form,
|
|
668
|
+
"title": document.title,
|
|
669
|
+
"relative_path": relative_path,
|
|
670
|
+
"read_url": read_url,
|
|
671
|
+
}
|
|
672
|
+
return render(request, "pages/readme_edit.html", context)
|
|
673
|
+
|
|
674
|
+
|
|
576
675
|
def sitemap(request):
|
|
577
676
|
site = get_site(request)
|
|
578
677
|
node = Node.get_local()
|
|
@@ -1005,13 +1104,14 @@ class ClientReportForm(forms.Form):
|
|
|
1005
1104
|
label=_("Month"),
|
|
1006
1105
|
required=False,
|
|
1007
1106
|
widget=forms.DateInput(attrs={"type": "month"}),
|
|
1107
|
+
input_formats=["%Y-%m"],
|
|
1008
1108
|
help_text=_("Generates the report for the calendar month that you select."),
|
|
1009
1109
|
)
|
|
1010
1110
|
owner = forms.ModelChoiceField(
|
|
1011
1111
|
queryset=get_user_model().objects.all(),
|
|
1012
1112
|
required=False,
|
|
1013
1113
|
help_text=_(
|
|
1014
|
-
"Sets who owns the report schedule and is listed as the
|
|
1114
|
+
"Sets who owns the report schedule and is listed as the requester."
|
|
1015
1115
|
),
|
|
1016
1116
|
)
|
|
1017
1117
|
destinations = forms.CharField(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|