arthexis 0.1.14__py3-none-any.whl → 0.1.16__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.14.dist-info → arthexis-0.1.16.dist-info}/METADATA +3 -2
- {arthexis-0.1.14.dist-info → arthexis-0.1.16.dist-info}/RECORD +41 -39
- config/urls.py +5 -0
- core/admin.py +200 -9
- core/admindocs.py +44 -3
- core/apps.py +1 -1
- core/backends.py +44 -8
- core/entity.py +17 -1
- core/github_issues.py +12 -7
- core/log_paths.py +24 -10
- core/mailer.py +9 -5
- core/models.py +92 -23
- core/release.py +173 -2
- core/system.py +411 -4
- core/tasks.py +5 -1
- core/test_system_info.py +16 -0
- core/tests.py +280 -0
- core/views.py +252 -38
- nodes/admin.py +25 -1
- nodes/models.py +99 -6
- nodes/rfid_sync.py +15 -0
- nodes/tests.py +142 -3
- nodes/utils.py +3 -0
- ocpp/consumers.py +38 -0
- ocpp/models.py +19 -4
- ocpp/tasks.py +156 -2
- ocpp/test_rfid.py +44 -2
- ocpp/tests.py +111 -1
- pages/admin.py +188 -5
- pages/context_processors.py +20 -1
- pages/middleware.py +4 -0
- pages/models.py +39 -1
- pages/module_defaults.py +156 -0
- pages/tasks.py +74 -0
- pages/tests.py +629 -8
- pages/urls.py +2 -0
- pages/utils.py +11 -0
- pages/views.py +106 -38
- {arthexis-0.1.14.dist-info → arthexis-0.1.16.dist-info}/WHEEL +0 -0
- {arthexis-0.1.14.dist-info → arthexis-0.1.16.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.14.dist-info → arthexis-0.1.16.dist-info}/top_level.txt +0 -0
pages/tests.py
CHANGED
|
@@ -7,6 +7,7 @@ import django
|
|
|
7
7
|
django.setup()
|
|
8
8
|
|
|
9
9
|
from django.test import Client, RequestFactory, TestCase, SimpleTestCase, override_settings
|
|
10
|
+
from django.test.utils import CaptureQueriesContext
|
|
10
11
|
from django.urls import reverse
|
|
11
12
|
from django.templatetags.static import static
|
|
12
13
|
from urllib.parse import quote
|
|
@@ -15,7 +16,10 @@ from django.contrib.sites.models import Site
|
|
|
15
16
|
from django.contrib import admin
|
|
16
17
|
from django.contrib.messages.storage.fallback import FallbackStorage
|
|
17
18
|
from django.core.exceptions import DisallowedHost
|
|
19
|
+
from django.core.cache import cache
|
|
20
|
+
from django.db import connection
|
|
18
21
|
import socket
|
|
22
|
+
from django.db import connection
|
|
19
23
|
from pages.models import (
|
|
20
24
|
Application,
|
|
21
25
|
Landing,
|
|
@@ -41,6 +45,7 @@ from pages.screenshot_specs import (
|
|
|
41
45
|
ScreenshotUnavailable,
|
|
42
46
|
registry,
|
|
43
47
|
)
|
|
48
|
+
from pages.context_processors import nav_links
|
|
44
49
|
from django.apps import apps as django_apps
|
|
45
50
|
from core import mailer
|
|
46
51
|
from core.admin import ProfileAdminMixin
|
|
@@ -62,7 +67,7 @@ import shutil
|
|
|
62
67
|
from io import StringIO
|
|
63
68
|
from django.conf import settings
|
|
64
69
|
from pathlib import Path
|
|
65
|
-
from unittest.mock import MagicMock, Mock, patch
|
|
70
|
+
from unittest.mock import MagicMock, Mock, call, patch
|
|
66
71
|
from types import SimpleNamespace
|
|
67
72
|
from django.core.management import call_command
|
|
68
73
|
import re
|
|
@@ -92,7 +97,7 @@ from nodes.models import (
|
|
|
92
97
|
NodeFeature,
|
|
93
98
|
NodeFeatureAssignment,
|
|
94
99
|
)
|
|
95
|
-
|
|
100
|
+
from django.contrib.auth.models import AnonymousUser
|
|
96
101
|
|
|
97
102
|
class LoginViewTests(TestCase):
|
|
98
103
|
def setUp(self):
|
|
@@ -706,6 +711,32 @@ class ViewHistoryLoggingTests(TestCase):
|
|
|
706
711
|
def setUp(self):
|
|
707
712
|
self.client = Client()
|
|
708
713
|
Site.objects.update_or_create(id=1, defaults={"name": "Terminal"})
|
|
714
|
+
self.addCleanup(self._reset_purge_task)
|
|
715
|
+
|
|
716
|
+
def _reset_purge_task(self):
|
|
717
|
+
from django_celery_beat.models import PeriodicTask
|
|
718
|
+
|
|
719
|
+
PeriodicTask.objects.filter(name="pages_purge_landing_leads").delete()
|
|
720
|
+
|
|
721
|
+
def _create_local_node(self):
|
|
722
|
+
node, _ = Node.objects.update_or_create(
|
|
723
|
+
mac_address=Node.get_current_mac(),
|
|
724
|
+
defaults={
|
|
725
|
+
"hostname": socket.gethostname(),
|
|
726
|
+
"address": "127.0.0.1",
|
|
727
|
+
"base_path": settings.BASE_DIR,
|
|
728
|
+
"port": 8000,
|
|
729
|
+
},
|
|
730
|
+
)
|
|
731
|
+
return node
|
|
732
|
+
|
|
733
|
+
def _enable_celery_feature(self):
|
|
734
|
+
node = self._create_local_node()
|
|
735
|
+
feature, _ = NodeFeature.objects.get_or_create(
|
|
736
|
+
slug="celery-queue", defaults={"display": "Celery Queue"}
|
|
737
|
+
)
|
|
738
|
+
NodeFeatureAssignment.objects.get_or_create(node=node, feature=feature)
|
|
739
|
+
return node
|
|
709
740
|
|
|
710
741
|
def test_successful_visit_creates_entry(self):
|
|
711
742
|
resp = self.client.get(reverse("pages:index"))
|
|
@@ -750,6 +781,8 @@ class ViewHistoryLoggingTests(TestCase):
|
|
|
750
781
|
self.assertEqual(user.last_visit_ip_address, "203.0.113.5")
|
|
751
782
|
|
|
752
783
|
def test_landing_visit_records_lead(self):
|
|
784
|
+
self._enable_celery_feature()
|
|
785
|
+
|
|
753
786
|
role = NodeRole.objects.create(name="landing-role")
|
|
754
787
|
application = Application.objects.create(
|
|
755
788
|
name="landing-tests-app", description=""
|
|
@@ -774,6 +807,26 @@ class ViewHistoryLoggingTests(TestCase):
|
|
|
774
807
|
self.assertEqual(lead.path, "/")
|
|
775
808
|
self.assertEqual(lead.referer, "https://example.com/ref")
|
|
776
809
|
|
|
810
|
+
def test_landing_visit_does_not_record_lead_without_celery(self):
|
|
811
|
+
role = NodeRole.objects.create(name="no-celery-role")
|
|
812
|
+
application = Application.objects.create(
|
|
813
|
+
name="no-celery-app", description=""
|
|
814
|
+
)
|
|
815
|
+
module = Module.objects.create(
|
|
816
|
+
node_role=role,
|
|
817
|
+
application=application,
|
|
818
|
+
path="/",
|
|
819
|
+
menu="Landing",
|
|
820
|
+
)
|
|
821
|
+
landing = module.landings.get(path="/")
|
|
822
|
+
landing.label = "No Celery"
|
|
823
|
+
landing.save(update_fields=["label"])
|
|
824
|
+
|
|
825
|
+
resp = self.client.get(reverse("pages:index"))
|
|
826
|
+
|
|
827
|
+
self.assertEqual(resp.status_code, 200)
|
|
828
|
+
self.assertFalse(LandingLead.objects.exists())
|
|
829
|
+
|
|
777
830
|
def test_disabled_landing_does_not_record_lead(self):
|
|
778
831
|
role = NodeRole.objects.create(name="landing-role-disabled")
|
|
779
832
|
application = Application.objects.create(
|
|
@@ -894,6 +947,93 @@ class ViewHistoryAdminTests(TestCase):
|
|
|
894
947
|
self.assertContains(resp, static("core/vendor/chart.umd.min.js"))
|
|
895
948
|
|
|
896
949
|
|
|
950
|
+
class LandingLeadAdminTests(TestCase):
|
|
951
|
+
def setUp(self):
|
|
952
|
+
self.client = Client()
|
|
953
|
+
User = get_user_model()
|
|
954
|
+
self.admin = User.objects.create_superuser(
|
|
955
|
+
username="lead_admin", password="pwd", email="lead@example.com"
|
|
956
|
+
)
|
|
957
|
+
self.client.force_login(self.admin)
|
|
958
|
+
Site.objects.update_or_create(
|
|
959
|
+
id=1, defaults={"name": "test", "domain": "testserver"}
|
|
960
|
+
)
|
|
961
|
+
self.node, _ = Node.objects.update_or_create(
|
|
962
|
+
mac_address=Node.get_current_mac(),
|
|
963
|
+
defaults={
|
|
964
|
+
"hostname": socket.gethostname(),
|
|
965
|
+
"address": "127.0.0.1",
|
|
966
|
+
"base_path": settings.BASE_DIR,
|
|
967
|
+
"port": 8000,
|
|
968
|
+
},
|
|
969
|
+
)
|
|
970
|
+
self.node.features.clear()
|
|
971
|
+
self.addCleanup(self._reset_purge_task)
|
|
972
|
+
|
|
973
|
+
def _reset_purge_task(self):
|
|
974
|
+
from django_celery_beat.models import PeriodicTask
|
|
975
|
+
|
|
976
|
+
PeriodicTask.objects.filter(name="pages_purge_landing_leads").delete()
|
|
977
|
+
|
|
978
|
+
def test_changelist_warns_without_celery(self):
|
|
979
|
+
url = reverse("admin:pages_landinglead_changelist")
|
|
980
|
+
response = self.client.get(url)
|
|
981
|
+
self.assertContains(
|
|
982
|
+
response,
|
|
983
|
+
"Landing leads are not being recorded because Celery is not running on this node.",
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
def test_changelist_no_warning_with_celery(self):
|
|
987
|
+
feature, _ = NodeFeature.objects.get_or_create(
|
|
988
|
+
slug="celery-queue", defaults={"display": "Celery Queue"}
|
|
989
|
+
)
|
|
990
|
+
NodeFeatureAssignment.objects.get_or_create(node=self.node, feature=feature)
|
|
991
|
+
url = reverse("admin:pages_landinglead_changelist")
|
|
992
|
+
response = self.client.get(url)
|
|
993
|
+
self.assertNotContains(
|
|
994
|
+
response,
|
|
995
|
+
"Landing leads are not being recorded because Celery is not running on this node.",
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
class LandingLeadTaskTests(TestCase):
|
|
1000
|
+
def setUp(self):
|
|
1001
|
+
self.role = NodeRole.objects.create(name="lead-task-role")
|
|
1002
|
+
self.application = Application.objects.create(
|
|
1003
|
+
name="lead-task-app", description=""
|
|
1004
|
+
)
|
|
1005
|
+
self.module = Module.objects.create(
|
|
1006
|
+
node_role=self.role,
|
|
1007
|
+
application=self.application,
|
|
1008
|
+
path="/tasks",
|
|
1009
|
+
menu="Landing",
|
|
1010
|
+
)
|
|
1011
|
+
self.landing = Landing.objects.create(
|
|
1012
|
+
module=self.module,
|
|
1013
|
+
path="/tasks/",
|
|
1014
|
+
label="Tasks Landing",
|
|
1015
|
+
enabled=True,
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
def test_purge_expired_landing_leads_removes_old_records(self):
|
|
1019
|
+
from pages.tasks import purge_expired_landing_leads
|
|
1020
|
+
|
|
1021
|
+
stale = LandingLead.objects.create(landing=self.landing, path="/tasks/")
|
|
1022
|
+
recent = LandingLead.objects.create(landing=self.landing, path="/tasks/")
|
|
1023
|
+
LandingLead.objects.filter(pk=stale.pk).update(
|
|
1024
|
+
created_on=timezone.now() - timedelta(days=31)
|
|
1025
|
+
)
|
|
1026
|
+
LandingLead.objects.filter(pk=recent.pk).update(
|
|
1027
|
+
created_on=timezone.now() - timedelta(days=5)
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
deleted = purge_expired_landing_leads()
|
|
1031
|
+
|
|
1032
|
+
self.assertEqual(deleted, 1)
|
|
1033
|
+
self.assertFalse(LandingLead.objects.filter(pk=stale.pk).exists())
|
|
1034
|
+
self.assertTrue(LandingLead.objects.filter(pk=recent.pk).exists())
|
|
1035
|
+
|
|
1036
|
+
|
|
897
1037
|
class LogViewerAdminTests(SimpleTestCase):
|
|
898
1038
|
def setUp(self):
|
|
899
1039
|
self.factory = RequestFactory()
|
|
@@ -1092,6 +1232,45 @@ class SiteAdminScreenshotTests(TestCase):
|
|
|
1092
1232
|
mock_capture.assert_called_once_with("http://testserver/")
|
|
1093
1233
|
|
|
1094
1234
|
|
|
1235
|
+
class SiteAdminReloadFixturesTests(TestCase):
|
|
1236
|
+
def setUp(self):
|
|
1237
|
+
self.client = Client()
|
|
1238
|
+
User = get_user_model()
|
|
1239
|
+
self.admin = User.objects.create_superuser(
|
|
1240
|
+
username="fixture-admin", password="pwd", email="admin@example.com"
|
|
1241
|
+
)
|
|
1242
|
+
self.client.force_login(self.admin)
|
|
1243
|
+
Site.objects.update_or_create(
|
|
1244
|
+
id=1, defaults={"name": "Terminal", "domain": "testserver"}
|
|
1245
|
+
)
|
|
1246
|
+
|
|
1247
|
+
@patch("pages.admin.call_command")
|
|
1248
|
+
def test_reload_site_fixtures_action(self, mock_call_command):
|
|
1249
|
+
response = self.client.post(
|
|
1250
|
+
reverse("admin:pages_siteproxy_changelist"),
|
|
1251
|
+
{"action": "reload_site_fixtures", "_selected_action": [1]},
|
|
1252
|
+
follow=True,
|
|
1253
|
+
)
|
|
1254
|
+
self.assertEqual(response.status_code, 200)
|
|
1255
|
+
|
|
1256
|
+
fixtures_dir = Path(settings.BASE_DIR) / "core" / "fixtures"
|
|
1257
|
+
expected = sorted(fixtures_dir.glob("references__00_site_*.json"))
|
|
1258
|
+
sigil_fixture = fixtures_dir / "sigil_roots__site.json"
|
|
1259
|
+
if sigil_fixture.exists():
|
|
1260
|
+
expected.append(sigil_fixture)
|
|
1261
|
+
|
|
1262
|
+
expected_calls = [
|
|
1263
|
+
call("loaddata", str(path), verbosity=0) for path in expected
|
|
1264
|
+
]
|
|
1265
|
+
self.assertEqual(mock_call_command.call_args_list, expected_calls)
|
|
1266
|
+
|
|
1267
|
+
if expected_calls:
|
|
1268
|
+
self.assertContains(
|
|
1269
|
+
response,
|
|
1270
|
+
f"Reloaded {len(expected_calls)} site fixtures.",
|
|
1271
|
+
)
|
|
1272
|
+
|
|
1273
|
+
|
|
1095
1274
|
class AdminBadgesWebsiteTests(TestCase):
|
|
1096
1275
|
def setUp(self):
|
|
1097
1276
|
self.client = Client()
|
|
@@ -1342,6 +1521,74 @@ class ConstellationNavTests(TestCase):
|
|
|
1342
1521
|
resp = self.client.get(reverse("pages:index"))
|
|
1343
1522
|
self.assertContains(resp, 'href="/ocpp/"')
|
|
1344
1523
|
|
|
1524
|
+
|
|
1525
|
+
class ReleaseModuleNavTests(TestCase):
|
|
1526
|
+
def setUp(self):
|
|
1527
|
+
self.client = Client()
|
|
1528
|
+
self.user_model = get_user_model()
|
|
1529
|
+
role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
1530
|
+
Node.objects.update_or_create(
|
|
1531
|
+
mac_address=Node.get_current_mac(),
|
|
1532
|
+
defaults={
|
|
1533
|
+
"hostname": "localhost",
|
|
1534
|
+
"address": "127.0.0.1",
|
|
1535
|
+
"role": role,
|
|
1536
|
+
},
|
|
1537
|
+
)
|
|
1538
|
+
Site.objects.update_or_create(
|
|
1539
|
+
id=1, defaults={"domain": "testserver", "name": "Terminal"}
|
|
1540
|
+
)
|
|
1541
|
+
application, _ = Application.objects.get_or_create(name="core")
|
|
1542
|
+
module, _ = Module.objects.get_or_create(
|
|
1543
|
+
node_role=role,
|
|
1544
|
+
application=application,
|
|
1545
|
+
path="/release/",
|
|
1546
|
+
defaults={"menu": "Release", "is_default": False},
|
|
1547
|
+
)
|
|
1548
|
+
module_updates = []
|
|
1549
|
+
if module.menu != "Release":
|
|
1550
|
+
module.menu = "Release"
|
|
1551
|
+
module_updates.append("menu")
|
|
1552
|
+
if getattr(module, "is_deleted", False):
|
|
1553
|
+
module.is_deleted = False
|
|
1554
|
+
module_updates.append("is_deleted")
|
|
1555
|
+
if module_updates:
|
|
1556
|
+
module.save(update_fields=module_updates)
|
|
1557
|
+
Landing.objects.update_or_create(
|
|
1558
|
+
module=module,
|
|
1559
|
+
path="/release/",
|
|
1560
|
+
defaults={
|
|
1561
|
+
"label": "Package Releases",
|
|
1562
|
+
"enabled": True,
|
|
1563
|
+
"description": "",
|
|
1564
|
+
},
|
|
1565
|
+
)
|
|
1566
|
+
self.release_group, _ = SecurityGroup.objects.get_or_create(
|
|
1567
|
+
name="Release Managers"
|
|
1568
|
+
)
|
|
1569
|
+
|
|
1570
|
+
def test_release_module_hidden_for_anonymous(self):
|
|
1571
|
+
response = self.client.get(reverse("pages:index"))
|
|
1572
|
+
self.assertNotContains(response, 'badge rounded-pill text-bg-secondary">RELEASE')
|
|
1573
|
+
|
|
1574
|
+
def test_release_module_visible_to_release_manager(self):
|
|
1575
|
+
user = self.user_model.objects.create_user(
|
|
1576
|
+
"release-admin", password="test", is_staff=True
|
|
1577
|
+
)
|
|
1578
|
+
user.groups.add(self.release_group)
|
|
1579
|
+
self.client.force_login(user)
|
|
1580
|
+
response = self.client.get(reverse("pages:index"))
|
|
1581
|
+
self.assertContains(response, 'badge rounded-pill text-bg-secondary">RELEASE')
|
|
1582
|
+
|
|
1583
|
+
def test_release_module_hidden_for_non_member_staff(self):
|
|
1584
|
+
user = self.user_model.objects.create_user(
|
|
1585
|
+
"staff-user", password="test", is_staff=True
|
|
1586
|
+
)
|
|
1587
|
+
self.client.force_login(user)
|
|
1588
|
+
response = self.client.get(reverse("pages:index"))
|
|
1589
|
+
self.assertNotContains(response, 'badge rounded-pill text-bg-secondary">RELEASE')
|
|
1590
|
+
|
|
1591
|
+
|
|
1345
1592
|
class ControlNavTests(TestCase):
|
|
1346
1593
|
def setUp(self):
|
|
1347
1594
|
self.client = Client()
|
|
@@ -1358,6 +1605,12 @@ class ControlNavTests(TestCase):
|
|
|
1358
1605
|
id=1, defaults={"domain": "testserver", "name": ""}
|
|
1359
1606
|
)
|
|
1360
1607
|
fixtures = [
|
|
1608
|
+
Path(
|
|
1609
|
+
settings.BASE_DIR,
|
|
1610
|
+
"pages",
|
|
1611
|
+
"fixtures",
|
|
1612
|
+
"default__application_pages.json",
|
|
1613
|
+
),
|
|
1361
1614
|
Path(
|
|
1362
1615
|
settings.BASE_DIR,
|
|
1363
1616
|
"pages",
|
|
@@ -1388,6 +1641,18 @@ class ControlNavTests(TestCase):
|
|
|
1388
1641
|
"fixtures",
|
|
1389
1642
|
"control__landing_ocpp_rfid.json",
|
|
1390
1643
|
),
|
|
1644
|
+
Path(
|
|
1645
|
+
settings.BASE_DIR,
|
|
1646
|
+
"pages",
|
|
1647
|
+
"fixtures",
|
|
1648
|
+
"control__module_readme.json",
|
|
1649
|
+
),
|
|
1650
|
+
Path(
|
|
1651
|
+
settings.BASE_DIR,
|
|
1652
|
+
"pages",
|
|
1653
|
+
"fixtures",
|
|
1654
|
+
"control__landing_readme.json",
|
|
1655
|
+
),
|
|
1391
1656
|
]
|
|
1392
1657
|
call_command("loaddata", *map(str, fixtures))
|
|
1393
1658
|
|
|
@@ -1428,6 +1693,84 @@ class ControlNavTests(TestCase):
|
|
|
1428
1693
|
self.assertFalse(resp.context["header_references"])
|
|
1429
1694
|
self.assertNotContains(resp, "https://example.com/hidden")
|
|
1430
1695
|
|
|
1696
|
+
def test_readme_pill_visible(self):
|
|
1697
|
+
resp = self.client.get(reverse("pages:readme"))
|
|
1698
|
+
self.assertContains(resp, 'href="/readme/"')
|
|
1699
|
+
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">README')
|
|
1700
|
+
|
|
1701
|
+
|
|
1702
|
+
class SatelliteNavTests(TestCase):
|
|
1703
|
+
def setUp(self):
|
|
1704
|
+
self.client = Client()
|
|
1705
|
+
role, _ = NodeRole.objects.get_or_create(name="Satellite")
|
|
1706
|
+
Node.objects.update_or_create(
|
|
1707
|
+
mac_address=Node.get_current_mac(),
|
|
1708
|
+
defaults={
|
|
1709
|
+
"hostname": "localhost",
|
|
1710
|
+
"address": "127.0.0.1",
|
|
1711
|
+
"role": role,
|
|
1712
|
+
},
|
|
1713
|
+
)
|
|
1714
|
+
Site.objects.update_or_create(
|
|
1715
|
+
id=1, defaults={"domain": "testserver", "name": ""}
|
|
1716
|
+
)
|
|
1717
|
+
fixtures = [
|
|
1718
|
+
Path(
|
|
1719
|
+
settings.BASE_DIR,
|
|
1720
|
+
"pages",
|
|
1721
|
+
"fixtures",
|
|
1722
|
+
"default__application_pages.json",
|
|
1723
|
+
),
|
|
1724
|
+
Path(
|
|
1725
|
+
settings.BASE_DIR,
|
|
1726
|
+
"pages",
|
|
1727
|
+
"fixtures",
|
|
1728
|
+
"satellite_box__application_ocpp.json",
|
|
1729
|
+
),
|
|
1730
|
+
Path(
|
|
1731
|
+
settings.BASE_DIR,
|
|
1732
|
+
"pages",
|
|
1733
|
+
"fixtures",
|
|
1734
|
+
"satellite_box__module_ocpp.json",
|
|
1735
|
+
),
|
|
1736
|
+
Path(
|
|
1737
|
+
settings.BASE_DIR,
|
|
1738
|
+
"pages",
|
|
1739
|
+
"fixtures",
|
|
1740
|
+
"satellite_box__landing_ocpp_dashboard.json",
|
|
1741
|
+
),
|
|
1742
|
+
Path(
|
|
1743
|
+
settings.BASE_DIR,
|
|
1744
|
+
"pages",
|
|
1745
|
+
"fixtures",
|
|
1746
|
+
"satellite_box__landing_ocpp_cp_simulator.json",
|
|
1747
|
+
),
|
|
1748
|
+
Path(
|
|
1749
|
+
settings.BASE_DIR,
|
|
1750
|
+
"pages",
|
|
1751
|
+
"fixtures",
|
|
1752
|
+
"satellite_box__landing_ocpp_rfid.json",
|
|
1753
|
+
),
|
|
1754
|
+
Path(
|
|
1755
|
+
settings.BASE_DIR,
|
|
1756
|
+
"pages",
|
|
1757
|
+
"fixtures",
|
|
1758
|
+
"satellite_box__module_readme.json",
|
|
1759
|
+
),
|
|
1760
|
+
Path(
|
|
1761
|
+
settings.BASE_DIR,
|
|
1762
|
+
"pages",
|
|
1763
|
+
"fixtures",
|
|
1764
|
+
"satellite_box__landing_readme.json",
|
|
1765
|
+
),
|
|
1766
|
+
]
|
|
1767
|
+
call_command("loaddata", *map(str, fixtures))
|
|
1768
|
+
|
|
1769
|
+
def test_readme_pill_visible(self):
|
|
1770
|
+
resp = self.client.get(reverse("pages:readme"))
|
|
1771
|
+
self.assertContains(resp, 'href="/readme/"')
|
|
1772
|
+
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">README')
|
|
1773
|
+
|
|
1431
1774
|
|
|
1432
1775
|
class PowerNavTests(TestCase):
|
|
1433
1776
|
def setUp(self):
|
|
@@ -1519,6 +1862,83 @@ class StaffNavVisibilityTests(TestCase):
|
|
|
1519
1862
|
self.assertContains(resp, 'href="/ocpp/"')
|
|
1520
1863
|
|
|
1521
1864
|
|
|
1865
|
+
class ModuleAdminReloadActionTests(TestCase):
|
|
1866
|
+
def setUp(self):
|
|
1867
|
+
self.client = Client()
|
|
1868
|
+
User = get_user_model()
|
|
1869
|
+
self.superuser = User.objects.create_superuser(
|
|
1870
|
+
username="admin",
|
|
1871
|
+
email="admin@example.com",
|
|
1872
|
+
password="pw",
|
|
1873
|
+
)
|
|
1874
|
+
self.client.force_login(self.superuser)
|
|
1875
|
+
self.role, _ = NodeRole.objects.get_or_create(name="Constellation")
|
|
1876
|
+
Application.objects.get_or_create(name="ocpp")
|
|
1877
|
+
Application.objects.get_or_create(name="awg")
|
|
1878
|
+
Site.objects.update_or_create(
|
|
1879
|
+
id=1, defaults={"domain": "testserver", "name": ""}
|
|
1880
|
+
)
|
|
1881
|
+
|
|
1882
|
+
def _post_reload(self):
|
|
1883
|
+
changelist_url = reverse("admin:pages_module_changelist")
|
|
1884
|
+
self.client.get(changelist_url)
|
|
1885
|
+
csrf_cookie = self.client.cookies.get("csrftoken")
|
|
1886
|
+
token = csrf_cookie.value if csrf_cookie else ""
|
|
1887
|
+
return self.client.post(
|
|
1888
|
+
reverse("admin:pages_module_reload_default_modules"),
|
|
1889
|
+
{"csrfmiddlewaretoken": token},
|
|
1890
|
+
follow=True,
|
|
1891
|
+
)
|
|
1892
|
+
|
|
1893
|
+
def test_reload_restores_missing_modules_and_landings(self):
|
|
1894
|
+
Module.objects.filter(node_role=self.role).delete()
|
|
1895
|
+
Landing.objects.filter(module__node_role=self.role).delete()
|
|
1896
|
+
|
|
1897
|
+
response = self._post_reload()
|
|
1898
|
+
self.assertEqual(response.status_code, 200)
|
|
1899
|
+
|
|
1900
|
+
chargers = Module.objects.get(node_role=self.role, path="/ocpp/")
|
|
1901
|
+
calculators = Module.objects.get(node_role=self.role, path="/awg/")
|
|
1902
|
+
|
|
1903
|
+
self.assertEqual(chargers.menu, "Chargers")
|
|
1904
|
+
self.assertEqual(calculators.menu, "")
|
|
1905
|
+
self.assertFalse(getattr(chargers, "is_deleted", False))
|
|
1906
|
+
self.assertFalse(getattr(calculators, "is_deleted", False))
|
|
1907
|
+
|
|
1908
|
+
charger_landings = set(
|
|
1909
|
+
Landing.objects.filter(module=chargers).values_list("path", flat=True)
|
|
1910
|
+
)
|
|
1911
|
+
self.assertSetEqual(
|
|
1912
|
+
charger_landings,
|
|
1913
|
+
{"/ocpp/", "/ocpp/simulator/", "/ocpp/rfid/"},
|
|
1914
|
+
)
|
|
1915
|
+
|
|
1916
|
+
calculator_landings = set(
|
|
1917
|
+
Landing.objects.filter(module=calculators).values_list(
|
|
1918
|
+
"path", flat=True
|
|
1919
|
+
)
|
|
1920
|
+
)
|
|
1921
|
+
self.assertSetEqual(
|
|
1922
|
+
calculator_landings,
|
|
1923
|
+
{"/awg/", "/awg/energy-tariff/"},
|
|
1924
|
+
)
|
|
1925
|
+
|
|
1926
|
+
def test_reload_is_idempotent(self):
|
|
1927
|
+
self._post_reload()
|
|
1928
|
+
module_count = Module.objects.filter(node_role=self.role).count()
|
|
1929
|
+
landing_count = Landing.objects.filter(module__node_role=self.role).count()
|
|
1930
|
+
|
|
1931
|
+
self._post_reload()
|
|
1932
|
+
|
|
1933
|
+
self.assertEqual(
|
|
1934
|
+
Module.objects.filter(node_role=self.role).count(), module_count
|
|
1935
|
+
)
|
|
1936
|
+
self.assertEqual(
|
|
1937
|
+
Landing.objects.filter(module__node_role=self.role).count(),
|
|
1938
|
+
landing_count,
|
|
1939
|
+
)
|
|
1940
|
+
|
|
1941
|
+
|
|
1522
1942
|
class ApplicationModelTests(TestCase):
|
|
1523
1943
|
def test_path_defaults_to_slugified_name(self):
|
|
1524
1944
|
role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
@@ -2069,6 +2489,40 @@ class FavoriteTests(TestCase):
|
|
|
2069
2489
|
self.assertNotIn("Packages", labels)
|
|
2070
2490
|
ContentType.objects.clear_cache()
|
|
2071
2491
|
|
|
2492
|
+
def test_future_action_items_limits_user_data_queries(self):
|
|
2493
|
+
from pages.templatetags import admin_extras
|
|
2494
|
+
|
|
2495
|
+
cache.delete(admin_extras.USER_DATA_MODELS_CACHE_KEY)
|
|
2496
|
+
self.addCleanup(cache.delete, admin_extras.USER_DATA_MODELS_CACHE_KEY)
|
|
2497
|
+
|
|
2498
|
+
for index in range(3):
|
|
2499
|
+
NodeRole.objects.create(name=f"CachedRole{index}", is_user_data=True)
|
|
2500
|
+
for index in range(2):
|
|
2501
|
+
NodeFeature.objects.create(
|
|
2502
|
+
slug=f"cached-feature-{index}",
|
|
2503
|
+
display=f"Feature {index}",
|
|
2504
|
+
is_user_data=True,
|
|
2505
|
+
)
|
|
2506
|
+
|
|
2507
|
+
Node.objects.create(
|
|
2508
|
+
hostname="cached-node",
|
|
2509
|
+
address="127.0.0.1",
|
|
2510
|
+
mac_address="AA:BB:CC:DD:EE:FF",
|
|
2511
|
+
port=8000,
|
|
2512
|
+
is_user_data=True,
|
|
2513
|
+
)
|
|
2514
|
+
|
|
2515
|
+
response = self.client.get(reverse("admin:index"))
|
|
2516
|
+
request = response.wsgi_request
|
|
2517
|
+
|
|
2518
|
+
admin_extras.future_action_items({"request": request})
|
|
2519
|
+
with CaptureQueriesContext(connection) as ctx:
|
|
2520
|
+
admin_extras.future_action_items({"request": request})
|
|
2521
|
+
|
|
2522
|
+
# History and favorites queries should remain bounded regardless of the
|
|
2523
|
+
# number of Entity subclasses with user data.
|
|
2524
|
+
self.assertLessEqual(len(ctx.captured_queries), 4)
|
|
2525
|
+
|
|
2072
2526
|
def test_favorite_ct_id_recreates_missing_content_type(self):
|
|
2073
2527
|
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
2074
2528
|
ct.delete()
|
|
@@ -2114,6 +2568,24 @@ class FavoriteTests(TestCase):
|
|
|
2114
2568
|
resp, '<div class="todo-details">More info</div>', html=True
|
|
2115
2569
|
)
|
|
2116
2570
|
|
|
2571
|
+
def test_dashboard_shows_todos_when_node_unknown(self):
|
|
2572
|
+
Todo.objects.create(request="Check fallback")
|
|
2573
|
+
from nodes.models import Node
|
|
2574
|
+
|
|
2575
|
+
Node.objects.all().delete()
|
|
2576
|
+
|
|
2577
|
+
resp = self.client.get(reverse("admin:index"))
|
|
2578
|
+
self.assertContains(resp, "Release manager tasks")
|
|
2579
|
+
self.assertContains(resp, "Check fallback")
|
|
2580
|
+
|
|
2581
|
+
def test_dashboard_shows_todos_without_release_manager_profile(self):
|
|
2582
|
+
Todo.objects.create(request="Unrestricted task")
|
|
2583
|
+
ReleaseManager.objects.filter(user=self.user).delete()
|
|
2584
|
+
|
|
2585
|
+
resp = self.client.get(reverse("admin:index"))
|
|
2586
|
+
self.assertContains(resp, "Release manager tasks")
|
|
2587
|
+
self.assertContains(resp, "Unrestricted task")
|
|
2588
|
+
|
|
2117
2589
|
def test_dashboard_excludes_todo_changelist_link(self):
|
|
2118
2590
|
ct = ContentType.objects.get_for_model(Todo)
|
|
2119
2591
|
Favorite.objects.create(user=self.user, content_type=ct)
|
|
@@ -2127,7 +2599,7 @@ class FavoriteTests(TestCase):
|
|
|
2127
2599
|
changelist = reverse("admin:core_todo_changelist")
|
|
2128
2600
|
self.assertNotContains(resp, f'href="{changelist}"')
|
|
2129
2601
|
|
|
2130
|
-
def
|
|
2602
|
+
def test_dashboard_shows_todos_for_admin_without_release_manager(self):
|
|
2131
2603
|
todo = Todo.objects.create(request="Only Release Manager")
|
|
2132
2604
|
User = get_user_model()
|
|
2133
2605
|
other_user = User.objects.create_superuser(
|
|
@@ -2135,10 +2607,10 @@ class FavoriteTests(TestCase):
|
|
|
2135
2607
|
)
|
|
2136
2608
|
self.client.force_login(other_user)
|
|
2137
2609
|
resp = self.client.get(reverse("admin:index"))
|
|
2138
|
-
self.
|
|
2139
|
-
self.
|
|
2610
|
+
self.assertContains(resp, "Release manager tasks")
|
|
2611
|
+
self.assertContains(resp, todo.request)
|
|
2140
2612
|
|
|
2141
|
-
def
|
|
2613
|
+
def test_dashboard_shows_todos_for_non_terminal_node(self):
|
|
2142
2614
|
todo = Todo.objects.create(request="Terminal Tasks")
|
|
2143
2615
|
from nodes.models import NodeRole
|
|
2144
2616
|
|
|
@@ -2146,8 +2618,8 @@ class FavoriteTests(TestCase):
|
|
|
2146
2618
|
self.node.role = control_role
|
|
2147
2619
|
self.node.save(update_fields=["role"])
|
|
2148
2620
|
resp = self.client.get(reverse("admin:index"))
|
|
2149
|
-
self.
|
|
2150
|
-
self.
|
|
2621
|
+
self.assertContains(resp, "Release manager tasks")
|
|
2622
|
+
self.assertContains(resp, todo.request)
|
|
2151
2623
|
|
|
2152
2624
|
def test_dashboard_shows_todos_for_delegate_release_manager(self):
|
|
2153
2625
|
todo = Todo.objects.create(request="Delegate Task")
|
|
@@ -2172,6 +2644,62 @@ class FavoriteTests(TestCase):
|
|
|
2172
2644
|
self.assertContains(resp, todo.request)
|
|
2173
2645
|
|
|
2174
2646
|
|
|
2647
|
+
class AdminIndexQueryRegressionTests(TestCase):
|
|
2648
|
+
def setUp(self):
|
|
2649
|
+
User = get_user_model()
|
|
2650
|
+
self.client = Client()
|
|
2651
|
+
self.user = User.objects.create_superuser(
|
|
2652
|
+
username="queryadmin", password="pwd", email="query@example.com"
|
|
2653
|
+
)
|
|
2654
|
+
self.client.force_login(self.user)
|
|
2655
|
+
Site.objects.update_or_create(
|
|
2656
|
+
id=1, defaults={"name": "test", "domain": "testserver"}
|
|
2657
|
+
)
|
|
2658
|
+
favorite_cts = [
|
|
2659
|
+
ContentType.objects.get_for_model(Application),
|
|
2660
|
+
ContentType.objects.get_for_model(Landing),
|
|
2661
|
+
]
|
|
2662
|
+
for ct in favorite_cts:
|
|
2663
|
+
Favorite.objects.create(user=self.user, content_type=ct)
|
|
2664
|
+
|
|
2665
|
+
def _render_admin_and_count_queries(self):
|
|
2666
|
+
url = reverse("admin:index")
|
|
2667
|
+
with CaptureQueriesContext(connection) as ctx:
|
|
2668
|
+
response = self.client.get(url)
|
|
2669
|
+
self.assertEqual(response.status_code, 200)
|
|
2670
|
+
sql_statements = [query["sql"].lower() for query in ctx.captured_queries]
|
|
2671
|
+
ct_queries = [sql for sql in sql_statements if '"django_content_type"' in sql]
|
|
2672
|
+
favorite_queries = [sql for sql in sql_statements if '"pages_favorite"' in sql]
|
|
2673
|
+
return len(ct_queries), len(favorite_queries)
|
|
2674
|
+
|
|
2675
|
+
def test_admin_index_queries_constant_with_more_models(self):
|
|
2676
|
+
site = admin.site
|
|
2677
|
+
original_registry = site._registry.copy()
|
|
2678
|
+
registry_items = list(original_registry.items())
|
|
2679
|
+
if len(registry_items) < 2:
|
|
2680
|
+
self.skipTest("Not enough registered admin models for regression test")
|
|
2681
|
+
|
|
2682
|
+
for model in original_registry.keys():
|
|
2683
|
+
ContentType.objects.get_for_model(model)
|
|
2684
|
+
|
|
2685
|
+
try:
|
|
2686
|
+
site._registry = dict(registry_items[:1])
|
|
2687
|
+
baseline_ct_queries, baseline_favorite_queries = (
|
|
2688
|
+
self._render_admin_and_count_queries()
|
|
2689
|
+
)
|
|
2690
|
+
|
|
2691
|
+
expanded_limit = min(len(registry_items), 5)
|
|
2692
|
+
site._registry = dict(registry_items[:expanded_limit])
|
|
2693
|
+
expanded_ct_queries, expanded_favorite_queries = (
|
|
2694
|
+
self._render_admin_and_count_queries()
|
|
2695
|
+
)
|
|
2696
|
+
finally:
|
|
2697
|
+
site._registry = original_registry
|
|
2698
|
+
|
|
2699
|
+
self.assertEqual(expanded_ct_queries, baseline_ct_queries)
|
|
2700
|
+
self.assertEqual(expanded_favorite_queries, baseline_favorite_queries)
|
|
2701
|
+
|
|
2702
|
+
|
|
2175
2703
|
class AdminActionListTests(TestCase):
|
|
2176
2704
|
def setUp(self):
|
|
2177
2705
|
User = get_user_model()
|
|
@@ -2352,6 +2880,7 @@ class DatasetteTests(TestCase):
|
|
|
2352
2880
|
|
|
2353
2881
|
class UserStorySubmissionTests(TestCase):
|
|
2354
2882
|
def setUp(self):
|
|
2883
|
+
cache.clear()
|
|
2355
2884
|
self.client = Client()
|
|
2356
2885
|
self.url = reverse("pages:user-story-submit")
|
|
2357
2886
|
User = get_user_model()
|
|
@@ -2367,6 +2896,8 @@ class UserStorySubmissionTests(TestCase):
|
|
|
2367
2896
|
"path": "/wizard/step-1/",
|
|
2368
2897
|
"take_screenshot": "1",
|
|
2369
2898
|
},
|
|
2899
|
+
HTTP_REFERER="https://example.test/wizard/step-1/",
|
|
2900
|
+
HTTP_USER_AGENT="FeedbackBot/1.0",
|
|
2370
2901
|
)
|
|
2371
2902
|
self.assertEqual(response.status_code, 200)
|
|
2372
2903
|
self.assertEqual(response.json(), {"success": True})
|
|
@@ -2378,6 +2909,10 @@ class UserStorySubmissionTests(TestCase):
|
|
|
2378
2909
|
self.assertEqual(story.owner, self.user)
|
|
2379
2910
|
self.assertTrue(story.is_user_data)
|
|
2380
2911
|
self.assertTrue(story.take_screenshot)
|
|
2912
|
+
self.assertEqual(story.status, UserStory.Status.OPEN)
|
|
2913
|
+
self.assertEqual(story.referer, "https://example.test/wizard/step-1/")
|
|
2914
|
+
self.assertEqual(story.user_agent, "FeedbackBot/1.0")
|
|
2915
|
+
self.assertEqual(story.ip_address, "127.0.0.1")
|
|
2381
2916
|
|
|
2382
2917
|
def test_anonymous_submission_uses_provided_name(self):
|
|
2383
2918
|
response = self.client.post(
|
|
@@ -2398,6 +2933,7 @@ class UserStorySubmissionTests(TestCase):
|
|
|
2398
2933
|
self.assertIsNone(story.owner)
|
|
2399
2934
|
self.assertEqual(story.comments, "It was fine.")
|
|
2400
2935
|
self.assertTrue(story.take_screenshot)
|
|
2936
|
+
self.assertEqual(story.status, UserStory.Status.OPEN)
|
|
2401
2937
|
|
|
2402
2938
|
def test_invalid_rating_returns_errors(self):
|
|
2403
2939
|
response = self.client.post(
|
|
@@ -2430,6 +2966,7 @@ class UserStorySubmissionTests(TestCase):
|
|
|
2430
2966
|
self.assertIsNone(story.user)
|
|
2431
2967
|
self.assertIsNone(story.owner)
|
|
2432
2968
|
self.assertTrue(story.take_screenshot)
|
|
2969
|
+
self.assertEqual(story.status, UserStory.Status.OPEN)
|
|
2433
2970
|
|
|
2434
2971
|
def test_submission_without_screenshot_request(self):
|
|
2435
2972
|
response = self.client.post(
|
|
@@ -2445,6 +2982,90 @@ class UserStorySubmissionTests(TestCase):
|
|
|
2445
2982
|
self.assertFalse(story.take_screenshot)
|
|
2446
2983
|
self.assertIsNone(story.owner)
|
|
2447
2984
|
|
|
2985
|
+
def test_rate_limit_blocks_repeated_submissions(self):
|
|
2986
|
+
payload = {
|
|
2987
|
+
"rating": 4,
|
|
2988
|
+
"comments": "Pretty good",
|
|
2989
|
+
"path": "/feedback/",
|
|
2990
|
+
"take_screenshot": "1",
|
|
2991
|
+
}
|
|
2992
|
+
first = self.client.post(self.url, payload)
|
|
2993
|
+
self.assertEqual(first.status_code, 200)
|
|
2994
|
+
second = self.client.post(self.url, payload)
|
|
2995
|
+
self.assertEqual(second.status_code, 429)
|
|
2996
|
+
data = second.json()
|
|
2997
|
+
self.assertFalse(data["success"])
|
|
2998
|
+
self.assertIn("__all__", data.get("errors", {}))
|
|
2999
|
+
self.assertIn("5", data["errors"]["__all__"][0])
|
|
3000
|
+
|
|
3001
|
+
|
|
3002
|
+
class UserStoryIssueAutomationTests(TestCase):
|
|
3003
|
+
def setUp(self):
|
|
3004
|
+
self.lock_dir = Path(settings.BASE_DIR) / "locks"
|
|
3005
|
+
self.lock_dir.mkdir(parents=True, exist_ok=True)
|
|
3006
|
+
self.lock_file = self.lock_dir / "celery.lck"
|
|
3007
|
+
User = get_user_model()
|
|
3008
|
+
self.user = User.objects.create_user(
|
|
3009
|
+
username="feedback_user", password="pwd"
|
|
3010
|
+
)
|
|
3011
|
+
|
|
3012
|
+
def tearDown(self):
|
|
3013
|
+
self.lock_file.unlink(missing_ok=True)
|
|
3014
|
+
|
|
3015
|
+
def test_low_rating_story_enqueues_issue_creation_when_celery_enabled(self):
|
|
3016
|
+
self.lock_file.write_text("")
|
|
3017
|
+
|
|
3018
|
+
with patch("pages.models.create_user_story_github_issue.delay") as mock_delay:
|
|
3019
|
+
story = UserStory.objects.create(
|
|
3020
|
+
path="/feedback/",
|
|
3021
|
+
rating=2,
|
|
3022
|
+
comments="Needs work",
|
|
3023
|
+
take_screenshot=False,
|
|
3024
|
+
user=self.user,
|
|
3025
|
+
)
|
|
3026
|
+
|
|
3027
|
+
mock_delay.assert_called_once_with(story.pk)
|
|
3028
|
+
|
|
3029
|
+
def test_low_rating_story_without_user_does_not_enqueue_issue(self):
|
|
3030
|
+
self.lock_file.write_text("")
|
|
3031
|
+
|
|
3032
|
+
with patch("pages.models.create_user_story_github_issue.delay") as mock_delay:
|
|
3033
|
+
UserStory.objects.create(
|
|
3034
|
+
path="/feedback/",
|
|
3035
|
+
rating=2,
|
|
3036
|
+
comments="Needs work",
|
|
3037
|
+
take_screenshot=False,
|
|
3038
|
+
)
|
|
3039
|
+
|
|
3040
|
+
mock_delay.assert_not_called()
|
|
3041
|
+
|
|
3042
|
+
def test_five_star_story_does_not_enqueue_issue(self):
|
|
3043
|
+
self.lock_file.write_text("")
|
|
3044
|
+
|
|
3045
|
+
with patch("pages.models.create_user_story_github_issue.delay") as mock_delay:
|
|
3046
|
+
UserStory.objects.create(
|
|
3047
|
+
path="/feedback/",
|
|
3048
|
+
rating=5,
|
|
3049
|
+
comments="Great!",
|
|
3050
|
+
take_screenshot=True,
|
|
3051
|
+
)
|
|
3052
|
+
|
|
3053
|
+
mock_delay.assert_not_called()
|
|
3054
|
+
|
|
3055
|
+
def test_low_rating_story_skips_when_celery_disabled(self):
|
|
3056
|
+
self.lock_file.unlink(missing_ok=True)
|
|
3057
|
+
|
|
3058
|
+
with patch("pages.models.create_user_story_github_issue.delay") as mock_delay:
|
|
3059
|
+
UserStory.objects.create(
|
|
3060
|
+
path="/feedback/",
|
|
3061
|
+
rating=1,
|
|
3062
|
+
comments="Not good",
|
|
3063
|
+
take_screenshot=False,
|
|
3064
|
+
user=self.user,
|
|
3065
|
+
)
|
|
3066
|
+
|
|
3067
|
+
mock_delay.assert_not_called()
|
|
3068
|
+
|
|
2448
3069
|
|
|
2449
3070
|
class UserStoryAdminActionTests(TestCase):
|
|
2450
3071
|
def setUp(self):
|