arthexis 0.1.21__py3-none-any.whl → 0.1.23__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.21.dist-info → arthexis-0.1.23.dist-info}/METADATA +9 -8
- {arthexis-0.1.21.dist-info → arthexis-0.1.23.dist-info}/RECORD +33 -33
- config/settings.py +4 -0
- config/urls.py +5 -0
- core/admin.py +224 -32
- core/environment.py +2 -239
- core/models.py +903 -65
- core/release.py +0 -5
- core/system.py +76 -0
- core/tests.py +181 -9
- core/user_data.py +42 -2
- core/views.py +68 -27
- nodes/admin.py +211 -60
- nodes/apps.py +11 -0
- nodes/models.py +35 -7
- nodes/tests.py +288 -1
- nodes/views.py +101 -48
- ocpp/admin.py +32 -2
- ocpp/consumers.py +1 -0
- ocpp/models.py +52 -3
- ocpp/tasks.py +99 -1
- ocpp/tests.py +350 -2
- ocpp/views.py +300 -6
- pages/admin.py +112 -15
- pages/apps.py +32 -0
- pages/forms.py +31 -8
- pages/models.py +42 -2
- pages/tests.py +386 -28
- pages/urls.py +10 -0
- pages/views.py +347 -18
- {arthexis-0.1.21.dist-info → arthexis-0.1.23.dist-info}/WHEEL +0 -0
- {arthexis-0.1.21.dist-info → arthexis-0.1.23.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.21.dist-info → arthexis-0.1.23.dist-info}/top_level.txt +0 -0
pages/tests.py
CHANGED
|
@@ -62,6 +62,7 @@ from core.models import (
|
|
|
62
62
|
RFID,
|
|
63
63
|
ReleaseManager,
|
|
64
64
|
SecurityGroup,
|
|
65
|
+
GoogleCalendarProfile,
|
|
65
66
|
Todo,
|
|
66
67
|
TOTPDeviceSettings,
|
|
67
68
|
)
|
|
@@ -71,8 +72,11 @@ import base64
|
|
|
71
72
|
import json
|
|
72
73
|
import tempfile
|
|
73
74
|
import shutil
|
|
75
|
+
from datetime import timedelta
|
|
74
76
|
from io import StringIO
|
|
75
77
|
from django.conf import settings
|
|
78
|
+
from django.utils import timezone
|
|
79
|
+
from django.utils.html import escape
|
|
76
80
|
from pathlib import Path
|
|
77
81
|
from unittest.mock import MagicMock, Mock, call, patch
|
|
78
82
|
from types import SimpleNamespace
|
|
@@ -90,6 +94,7 @@ from datetime import (
|
|
|
90
94
|
from django.core import mail
|
|
91
95
|
from django.utils import timezone
|
|
92
96
|
from django.utils.text import slugify
|
|
97
|
+
from django.utils import translation
|
|
93
98
|
from django.utils.translation import gettext
|
|
94
99
|
from django_otp import DEVICE_ID_SESSION_KEY
|
|
95
100
|
from django_otp.oath import TOTP
|
|
@@ -104,6 +109,7 @@ from nodes.models import (
|
|
|
104
109
|
NodeRole,
|
|
105
110
|
NodeFeature,
|
|
106
111
|
NodeFeatureAssignment,
|
|
112
|
+
NetMessage,
|
|
107
113
|
)
|
|
108
114
|
from django.contrib.auth.models import AnonymousUser
|
|
109
115
|
|
|
@@ -195,6 +201,41 @@ class LoginViewTests(TestCase):
|
|
|
195
201
|
)
|
|
196
202
|
self.assertRedirects(resp, "/nodes/list/")
|
|
197
203
|
|
|
204
|
+
def test_homepage_excludes_version_banner_for_anonymous(self):
|
|
205
|
+
response = self.client.get(reverse("pages:index"))
|
|
206
|
+
|
|
207
|
+
self.assertEqual(response.status_code, 200)
|
|
208
|
+
self.assertNotContains(response, "__versionCheckInitialized")
|
|
209
|
+
|
|
210
|
+
def test_homepage_includes_version_banner_for_staff(self):
|
|
211
|
+
self.client.force_login(self.staff)
|
|
212
|
+
response = self.client.get(reverse("pages:index"))
|
|
213
|
+
|
|
214
|
+
self.assertEqual(response.status_code, 200)
|
|
215
|
+
self.assertContains(response, "__versionCheckInitialized")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class AdminTemplateVersionBannerTests(TestCase):
|
|
219
|
+
def setUp(self):
|
|
220
|
+
self.client = Client()
|
|
221
|
+
User = get_user_model()
|
|
222
|
+
self.staff = User.objects.create_user(
|
|
223
|
+
username="admin-staff", password="pwd", is_staff=True
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def test_admin_login_excludes_version_banner_for_anonymous(self):
|
|
227
|
+
response = self.client.get(reverse("admin:login"))
|
|
228
|
+
|
|
229
|
+
self.assertEqual(response.status_code, 200)
|
|
230
|
+
self.assertNotContains(response, "__versionCheckInitialized")
|
|
231
|
+
|
|
232
|
+
def test_admin_dashboard_includes_version_banner_for_staff(self):
|
|
233
|
+
self.client.force_login(self.staff)
|
|
234
|
+
response = self.client.get(reverse("admin:index"))
|
|
235
|
+
|
|
236
|
+
self.assertEqual(response.status_code, 200)
|
|
237
|
+
self.assertContains(response, "__versionCheckInitialized")
|
|
238
|
+
|
|
198
239
|
def test_staff_redirects_next_when_specified(self):
|
|
199
240
|
resp = self.client.post(
|
|
200
241
|
reverse("pages:login") + "?next=/nodes/list/",
|
|
@@ -681,18 +722,36 @@ class AdminDashboardAppListTests(TestCase):
|
|
|
681
722
|
|
|
682
723
|
def test_horologia_hidden_without_celery_feature(self):
|
|
683
724
|
resp = self.client.get(reverse("admin:index"))
|
|
684
|
-
self.assertNotContains(resp, "5. Horologia
|
|
725
|
+
self.assertNotContains(resp, "5. Horologia</a>")
|
|
685
726
|
|
|
686
727
|
def test_horologia_visible_with_celery_feature(self):
|
|
687
728
|
feature = NodeFeature.objects.create(slug="celery-queue", display="Celery Queue")
|
|
688
729
|
NodeFeatureAssignment.objects.create(node=self.node, feature=feature)
|
|
689
730
|
resp = self.client.get(reverse("admin:index"))
|
|
690
|
-
self.assertContains(resp, "5. Horologia
|
|
731
|
+
self.assertContains(resp, "5. Horologia</a>")
|
|
691
732
|
|
|
692
733
|
def test_horologia_visible_with_celery_lock(self):
|
|
693
734
|
self.celery_lock.write_text("")
|
|
694
735
|
resp = self.client.get(reverse("admin:index"))
|
|
695
|
-
self.assertContains(resp, "5. Horologia
|
|
736
|
+
self.assertContains(resp, "5. Horologia</a>")
|
|
737
|
+
|
|
738
|
+
def test_dashboard_shows_last_net_message(self):
|
|
739
|
+
NetMessage.objects.all().delete()
|
|
740
|
+
NetMessage.objects.create(subject="Older", body="First body")
|
|
741
|
+
NetMessage.objects.create(subject="Latest", body="Signal ready")
|
|
742
|
+
|
|
743
|
+
resp = self.client.get(reverse("admin:index"))
|
|
744
|
+
|
|
745
|
+
self.assertContains(resp, gettext("Net message"))
|
|
746
|
+
self.assertContains(resp, "Latest — Signal ready")
|
|
747
|
+
self.assertNotContains(resp, gettext("No net messages available"))
|
|
748
|
+
|
|
749
|
+
def test_dashboard_shows_placeholder_without_net_message(self):
|
|
750
|
+
NetMessage.objects.all().delete()
|
|
751
|
+
|
|
752
|
+
resp = self.client.get(reverse("admin:index"))
|
|
753
|
+
|
|
754
|
+
self.assertContains(resp, gettext("No net messages available"))
|
|
696
755
|
|
|
697
756
|
class AdminSidebarTests(TestCase):
|
|
698
757
|
def setUp(self):
|
|
@@ -715,6 +774,66 @@ class AdminSidebarTests(TestCase):
|
|
|
715
774
|
self.assertContains(resp, 'id="admin-collapsible-apps"')
|
|
716
775
|
|
|
717
776
|
|
|
777
|
+
class AdminGoogleCalendarSidebarTests(TestCase):
|
|
778
|
+
def setUp(self):
|
|
779
|
+
self.client = Client()
|
|
780
|
+
User = get_user_model()
|
|
781
|
+
self.admin = User.objects.create_superuser(
|
|
782
|
+
username="calendar_admin", password="pwd", email="admin@example.com"
|
|
783
|
+
)
|
|
784
|
+
self.client.force_login(self.admin)
|
|
785
|
+
Site.objects.update_or_create(
|
|
786
|
+
id=1, defaults={"name": "test", "domain": "testserver"}
|
|
787
|
+
)
|
|
788
|
+
Node.objects.create(hostname="testserver", address="127.0.0.1")
|
|
789
|
+
|
|
790
|
+
def test_calendar_module_hidden_without_profile(self):
|
|
791
|
+
resp = self.client.get(reverse("admin:index"))
|
|
792
|
+
self.assertNotContains(resp, 'id="google-calendar-module"', html=False)
|
|
793
|
+
|
|
794
|
+
@patch("core.models.GoogleCalendarProfile.fetch_events")
|
|
795
|
+
def test_calendar_module_shows_events_for_user(self, fetch_events):
|
|
796
|
+
fetch_events.return_value = [
|
|
797
|
+
{
|
|
798
|
+
"summary": "Standup",
|
|
799
|
+
"start": timezone.now(),
|
|
800
|
+
"end": None,
|
|
801
|
+
"all_day": False,
|
|
802
|
+
"html_link": "https://calendar.google.com/event",
|
|
803
|
+
"location": "HQ",
|
|
804
|
+
}
|
|
805
|
+
]
|
|
806
|
+
GoogleCalendarProfile.objects.create(
|
|
807
|
+
user=self.admin,
|
|
808
|
+
calendar_id="example@group.calendar.google.com",
|
|
809
|
+
api_key="secret",
|
|
810
|
+
display_name="Team Calendar",
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
resp = self.client.get(reverse("admin:index"))
|
|
814
|
+
|
|
815
|
+
self.assertContains(resp, 'id="google-calendar-module"', html=False)
|
|
816
|
+
self.assertContains(resp, "Standup")
|
|
817
|
+
self.assertContains(resp, "Open full calendar")
|
|
818
|
+
fetch_events.assert_called_once()
|
|
819
|
+
|
|
820
|
+
@patch("core.models.GoogleCalendarProfile.fetch_events")
|
|
821
|
+
def test_calendar_module_uses_group_profile(self, fetch_events):
|
|
822
|
+
fetch_events.return_value = []
|
|
823
|
+
group = SecurityGroup.objects.create(name="Calendar Group")
|
|
824
|
+
self.admin.groups.add(group)
|
|
825
|
+
GoogleCalendarProfile.objects.create(
|
|
826
|
+
group=group,
|
|
827
|
+
calendar_id="group@calendar.google.com",
|
|
828
|
+
api_key="secret",
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
resp = self.client.get(reverse("admin:index"))
|
|
832
|
+
|
|
833
|
+
self.assertContains(resp, 'id="google-calendar-module"', html=False)
|
|
834
|
+
fetch_events.assert_called_once()
|
|
835
|
+
|
|
836
|
+
|
|
718
837
|
class ViewHistoryLoggingTests(TestCase):
|
|
719
838
|
def setUp(self):
|
|
720
839
|
self.client = Client()
|
|
@@ -816,6 +935,35 @@ class ViewHistoryLoggingTests(TestCase):
|
|
|
816
935
|
self.assertEqual(lead.path, "/")
|
|
817
936
|
self.assertEqual(lead.referer, "https://example.com/ref")
|
|
818
937
|
|
|
938
|
+
def test_pages_config_purges_old_view_history(self):
|
|
939
|
+
ViewHistory.objects.all().delete()
|
|
940
|
+
|
|
941
|
+
old_entry = ViewHistory.objects.create(
|
|
942
|
+
path="/old/",
|
|
943
|
+
method="GET",
|
|
944
|
+
status_code=200,
|
|
945
|
+
status_text="OK",
|
|
946
|
+
)
|
|
947
|
+
new_entry = ViewHistory.objects.create(
|
|
948
|
+
path="/recent/",
|
|
949
|
+
method="GET",
|
|
950
|
+
status_code=200,
|
|
951
|
+
status_text="OK",
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
ViewHistory.objects.filter(pk=old_entry.pk).update(
|
|
955
|
+
visited_at=timezone.now() - timedelta(days=20)
|
|
956
|
+
)
|
|
957
|
+
ViewHistory.objects.filter(pk=new_entry.pk).update(
|
|
958
|
+
visited_at=timezone.now() - timedelta(days=10)
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
config = django_apps.get_app_config("pages")
|
|
962
|
+
config._purge_view_history()
|
|
963
|
+
|
|
964
|
+
self.assertFalse(ViewHistory.objects.filter(pk=old_entry.pk).exists())
|
|
965
|
+
self.assertTrue(ViewHistory.objects.filter(pk=new_entry.pk).exists())
|
|
966
|
+
|
|
819
967
|
def test_landing_visit_does_not_record_lead_without_celery(self):
|
|
820
968
|
role = NodeRole.objects.create(name="no-celery-role")
|
|
821
969
|
application = Application.objects.create(
|
|
@@ -913,6 +1061,44 @@ class ViewHistoryAdminTests(TestCase):
|
|
|
913
1061
|
self.assertEqual(totals.get("/"), 2)
|
|
914
1062
|
self.assertEqual(totals.get("/about/"), 1)
|
|
915
1063
|
|
|
1064
|
+
def test_graph_data_endpoint_respects_days_parameter(self):
|
|
1065
|
+
ViewHistory.all_objects.all().delete()
|
|
1066
|
+
reference_date = date(2025, 5, 1)
|
|
1067
|
+
tz = timezone.get_current_timezone()
|
|
1068
|
+
path = "/range/"
|
|
1069
|
+
|
|
1070
|
+
for offset in range(10):
|
|
1071
|
+
entry = ViewHistory.objects.create(
|
|
1072
|
+
path=path,
|
|
1073
|
+
method="GET",
|
|
1074
|
+
status_code=200,
|
|
1075
|
+
status_text="OK",
|
|
1076
|
+
error_message="",
|
|
1077
|
+
view_name="pages:index",
|
|
1078
|
+
)
|
|
1079
|
+
visited_date = reference_date - timedelta(days=offset)
|
|
1080
|
+
visited_at = timezone.make_aware(
|
|
1081
|
+
datetime.combine(visited_date, datetime_time(12, 0)), tz
|
|
1082
|
+
)
|
|
1083
|
+
entry.visited_at = visited_at
|
|
1084
|
+
entry.save(update_fields=["visited_at"])
|
|
1085
|
+
|
|
1086
|
+
url = reverse("admin:pages_viewhistory_traffic_data")
|
|
1087
|
+
with patch("pages.admin.timezone.localdate", return_value=reference_date):
|
|
1088
|
+
resp = self.client.get(url, {"days": 7})
|
|
1089
|
+
|
|
1090
|
+
self.assertEqual(resp.status_code, 200)
|
|
1091
|
+
data = resp.json()
|
|
1092
|
+
|
|
1093
|
+
self.assertEqual(len(data.get("labels", [])), 7)
|
|
1094
|
+
self.assertEqual(data.get("meta", {}).get("start"), (reference_date - timedelta(days=6)).isoformat())
|
|
1095
|
+
self.assertEqual(data.get("meta", {}).get("end"), reference_date.isoformat())
|
|
1096
|
+
|
|
1097
|
+
totals = {
|
|
1098
|
+
dataset["label"]: sum(dataset["data"]) for dataset in data.get("datasets", [])
|
|
1099
|
+
}
|
|
1100
|
+
self.assertEqual(totals.get(path), 7)
|
|
1101
|
+
|
|
916
1102
|
def test_graph_data_includes_late_evening_visits(self):
|
|
917
1103
|
target_date = date(2025, 9, 27)
|
|
918
1104
|
entry = ViewHistory.objects.create(
|
|
@@ -1916,7 +2102,7 @@ class ControlNavTests(TestCase):
|
|
|
1916
2102
|
|
|
1917
2103
|
def test_readme_pill_visible(self):
|
|
1918
2104
|
resp = self.client.get(reverse("pages:readme"))
|
|
1919
|
-
self.assertContains(resp, 'href="/read/"')
|
|
2105
|
+
self.assertContains(resp, 'href="/read/docs/cookbooks/install-start-stop-upgrade-uninstall"')
|
|
1920
2106
|
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOKS')
|
|
1921
2107
|
|
|
1922
2108
|
def test_cookbook_pill_has_no_dropdown(self):
|
|
@@ -1932,7 +2118,7 @@ class ControlNavTests(TestCase):
|
|
|
1932
2118
|
|
|
1933
2119
|
self.assertContains(
|
|
1934
2120
|
resp,
|
|
1935
|
-
'<a class="nav-link" href="/read/"><span class="badge rounded-pill text-bg-secondary">COOKBOOKS</span></a>',
|
|
2121
|
+
'<a class="nav-link" href="/read/docs/cookbooks/install-start-stop-upgrade-uninstall"><span class="badge rounded-pill text-bg-secondary">COOKBOOKS</span></a>',
|
|
1936
2122
|
html=True,
|
|
1937
2123
|
)
|
|
1938
2124
|
self.assertNotContains(resp, 'dropdown-item" href="/man/"')
|
|
@@ -2038,7 +2224,7 @@ class SatelliteNavTests(TestCase):
|
|
|
2038
2224
|
|
|
2039
2225
|
def test_readme_pill_visible(self):
|
|
2040
2226
|
resp = self.client.get(reverse("pages:readme"))
|
|
2041
|
-
self.assertContains(resp, 'href="/read/"')
|
|
2227
|
+
self.assertContains(resp, 'href="/read/docs/cookbooks/install-start-stop-upgrade-uninstall"')
|
|
2042
2228
|
self.assertContains(resp, 'badge rounded-pill text-bg-secondary">COOKBOOKS')
|
|
2043
2229
|
|
|
2044
2230
|
|
|
@@ -2657,6 +2843,20 @@ class FavoriteTests(TestCase):
|
|
|
2657
2843
|
self.assertEqual(fav.custom_label, "Apps")
|
|
2658
2844
|
self.assertTrue(fav.user_data)
|
|
2659
2845
|
|
|
2846
|
+
def test_add_favorite_defaults_user_data_checked(self):
|
|
2847
|
+
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
2848
|
+
url = reverse("admin:favorite_toggle", args=[ct.id])
|
|
2849
|
+
resp = self.client.get(url)
|
|
2850
|
+
self.assertContains(resp, 'name="user_data" checked')
|
|
2851
|
+
|
|
2852
|
+
def test_add_favorite_with_priority(self):
|
|
2853
|
+
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
2854
|
+
url = reverse("admin:favorite_toggle", args=[ct.id])
|
|
2855
|
+
resp = self.client.post(url, {"priority": "7"})
|
|
2856
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
2857
|
+
fav = Favorite.objects.get(user=self.user, content_type=ct)
|
|
2858
|
+
self.assertEqual(fav.priority, 7)
|
|
2859
|
+
|
|
2660
2860
|
def test_cancel_link_uses_next(self):
|
|
2661
2861
|
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
2662
2862
|
next_url = reverse("admin:pages_application_changelist")
|
|
@@ -2666,14 +2866,31 @@ class FavoriteTests(TestCase):
|
|
|
2666
2866
|
resp = self.client.get(url)
|
|
2667
2867
|
self.assertContains(resp, f'href="{next_url}"')
|
|
2668
2868
|
|
|
2669
|
-
def
|
|
2869
|
+
def test_existing_favorite_shows_update_form(self):
|
|
2670
2870
|
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
2671
|
-
Favorite.objects.create(
|
|
2871
|
+
favorite = Favorite.objects.create(
|
|
2872
|
+
user=self.user, content_type=ct, custom_label="Apps", user_data=True
|
|
2873
|
+
)
|
|
2672
2874
|
url = reverse("admin:favorite_toggle", args=[ct.id])
|
|
2673
2875
|
resp = self.client.get(url)
|
|
2674
|
-
self.
|
|
2675
|
-
|
|
2676
|
-
self.assertContains(resp,
|
|
2876
|
+
self.assertContains(resp, "Update Favorite")
|
|
2877
|
+
self.assertContains(resp, "value=\"Apps\"")
|
|
2878
|
+
self.assertContains(resp, "checked")
|
|
2879
|
+
self.assertContains(resp, "name=\"remove\"")
|
|
2880
|
+
|
|
2881
|
+
resp = self.client.post(url, {"custom_label": "Apps Updated"})
|
|
2882
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
2883
|
+
favorite.refresh_from_db()
|
|
2884
|
+
self.assertEqual(favorite.custom_label, "Apps Updated")
|
|
2885
|
+
self.assertFalse(favorite.user_data)
|
|
2886
|
+
|
|
2887
|
+
def test_remove_existing_favorite_from_toggle(self):
|
|
2888
|
+
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
2889
|
+
Favorite.objects.create(user=self.user, content_type=ct)
|
|
2890
|
+
url = reverse("admin:favorite_toggle", args=[ct.id])
|
|
2891
|
+
resp = self.client.post(url, {"remove": "1"})
|
|
2892
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
2893
|
+
self.assertFalse(Favorite.objects.filter(user=self.user, content_type=ct).exists())
|
|
2677
2894
|
|
|
2678
2895
|
def test_update_user_data_from_list(self):
|
|
2679
2896
|
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
@@ -2684,6 +2901,15 @@ class FavoriteTests(TestCase):
|
|
|
2684
2901
|
fav.refresh_from_db()
|
|
2685
2902
|
self.assertTrue(fav.user_data)
|
|
2686
2903
|
|
|
2904
|
+
def test_update_priority_from_list(self):
|
|
2905
|
+
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
2906
|
+
fav = Favorite.objects.create(user=self.user, content_type=ct, priority=3)
|
|
2907
|
+
url = reverse("admin:favorite_list")
|
|
2908
|
+
resp = self.client.post(url, {f"priority_{fav.pk}": "12"})
|
|
2909
|
+
self.assertRedirects(resp, url)
|
|
2910
|
+
fav.refresh_from_db()
|
|
2911
|
+
self.assertEqual(fav.priority, 12)
|
|
2912
|
+
|
|
2687
2913
|
def test_dashboard_includes_favorites_and_user_data(self):
|
|
2688
2914
|
fav_ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
2689
2915
|
Favorite.objects.create(
|
|
@@ -2694,6 +2920,12 @@ class FavoriteTests(TestCase):
|
|
|
2694
2920
|
self.assertContains(resp, reverse("admin:pages_application_changelist"))
|
|
2695
2921
|
self.assertContains(resp, reverse("admin:nodes_noderole_changelist"))
|
|
2696
2922
|
|
|
2923
|
+
def test_dashboard_shows_empty_todo_state(self):
|
|
2924
|
+
Todo.objects.all().delete()
|
|
2925
|
+
resp = self.client.get(reverse("admin:index"))
|
|
2926
|
+
self.assertContains(resp, "Release manager tasks")
|
|
2927
|
+
self.assertContains(resp, "No pending TODOs")
|
|
2928
|
+
|
|
2697
2929
|
def test_dashboard_merges_duplicate_future_actions(self):
|
|
2698
2930
|
ct = ContentType.objects.get_for_model(NodeRole)
|
|
2699
2931
|
Favorite.objects.create(user=self.user, content_type=ct)
|
|
@@ -2933,7 +3165,11 @@ class FavoriteTests(TestCase):
|
|
|
2933
3165
|
todo = Todo.objects.create(request="Do thing")
|
|
2934
3166
|
resp = self.client.get(reverse("admin:index"))
|
|
2935
3167
|
done_url = reverse("todo-done", args=[todo.pk])
|
|
2936
|
-
|
|
3168
|
+
tooltip = escape(todo.request)
|
|
3169
|
+
self.assertContains(resp, f'title="{tooltip}"')
|
|
3170
|
+
self.assertContains(resp, f'aria-label="{tooltip}"')
|
|
3171
|
+
task_label = gettext("Task %(counter)s") % {"counter": 1}
|
|
3172
|
+
self.assertContains(resp, task_label)
|
|
2937
3173
|
self.assertContains(resp, f'action="{done_url}"')
|
|
2938
3174
|
self.assertContains(resp, "DONE")
|
|
2939
3175
|
|
|
@@ -2961,7 +3197,8 @@ class FavoriteTests(TestCase):
|
|
|
2961
3197
|
|
|
2962
3198
|
resp = self.client.get(reverse("admin:index"))
|
|
2963
3199
|
self.assertContains(resp, "Release manager tasks")
|
|
2964
|
-
|
|
3200
|
+
tooltip = escape("Check fallback")
|
|
3201
|
+
self.assertContains(resp, f'title="{tooltip}"')
|
|
2965
3202
|
|
|
2966
3203
|
def test_dashboard_shows_todos_without_release_manager_profile(self):
|
|
2967
3204
|
Todo.objects.create(request="Unrestricted task")
|
|
@@ -2969,7 +3206,8 @@ class FavoriteTests(TestCase):
|
|
|
2969
3206
|
|
|
2970
3207
|
resp = self.client.get(reverse("admin:index"))
|
|
2971
3208
|
self.assertContains(resp, "Release manager tasks")
|
|
2972
|
-
|
|
3209
|
+
tooltip = escape("Unrestricted task")
|
|
3210
|
+
self.assertContains(resp, f'title="{tooltip}"')
|
|
2973
3211
|
|
|
2974
3212
|
def test_dashboard_excludes_todo_changelist_link(self):
|
|
2975
3213
|
ct = ContentType.objects.get_for_model(Todo)
|
|
@@ -2993,7 +3231,8 @@ class FavoriteTests(TestCase):
|
|
|
2993
3231
|
self.client.force_login(other_user)
|
|
2994
3232
|
resp = self.client.get(reverse("admin:index"))
|
|
2995
3233
|
self.assertContains(resp, "Release manager tasks")
|
|
2996
|
-
|
|
3234
|
+
tooltip = escape(todo.request)
|
|
3235
|
+
self.assertContains(resp, f'title="{tooltip}"')
|
|
2997
3236
|
|
|
2998
3237
|
def test_dashboard_shows_todos_for_non_terminal_node(self):
|
|
2999
3238
|
todo = Todo.objects.create(request="Terminal Tasks")
|
|
@@ -3004,7 +3243,8 @@ class FavoriteTests(TestCase):
|
|
|
3004
3243
|
self.node.save(update_fields=["role"])
|
|
3005
3244
|
resp = self.client.get(reverse("admin:index"))
|
|
3006
3245
|
self.assertContains(resp, "Release manager tasks")
|
|
3007
|
-
|
|
3246
|
+
tooltip = escape(todo.request)
|
|
3247
|
+
self.assertContains(resp, f'title="{tooltip}"')
|
|
3008
3248
|
|
|
3009
3249
|
def test_dashboard_shows_todos_for_delegate_release_manager(self):
|
|
3010
3250
|
todo = Todo.objects.create(request="Delegate Task")
|
|
@@ -3026,7 +3266,8 @@ class FavoriteTests(TestCase):
|
|
|
3026
3266
|
self.client.force_login(operator)
|
|
3027
3267
|
resp = self.client.get(reverse("admin:index"))
|
|
3028
3268
|
self.assertContains(resp, "Release manager tasks")
|
|
3029
|
-
|
|
3269
|
+
tooltip = escape(todo.request)
|
|
3270
|
+
self.assertContains(resp, f'title="{tooltip}"')
|
|
3030
3271
|
|
|
3031
3272
|
|
|
3032
3273
|
class AdminIndexQueryRegressionTests(TestCase):
|
|
@@ -3229,6 +3470,14 @@ class UserStorySubmissionTests(TestCase):
|
|
|
3229
3470
|
self.url = reverse("pages:user-story-submit")
|
|
3230
3471
|
User = get_user_model()
|
|
3231
3472
|
self.user = User.objects.create_user(username="feedbacker", password="pwd")
|
|
3473
|
+
self.capture_patcher = patch("pages.views.capture_screenshot", autospec=True)
|
|
3474
|
+
self.save_patcher = patch("pages.views.save_screenshot", autospec=True)
|
|
3475
|
+
self.mock_capture = self.capture_patcher.start()
|
|
3476
|
+
self.mock_save = self.save_patcher.start()
|
|
3477
|
+
self.mock_capture.return_value = Path("/tmp/fake.png")
|
|
3478
|
+
self.mock_save.return_value = None
|
|
3479
|
+
self.addCleanup(self.capture_patcher.stop)
|
|
3480
|
+
self.addCleanup(self.save_patcher.stop)
|
|
3232
3481
|
|
|
3233
3482
|
def test_authenticated_submission_defaults_to_username(self):
|
|
3234
3483
|
self.client.force_login(self.user)
|
|
@@ -3257,12 +3506,98 @@ class UserStorySubmissionTests(TestCase):
|
|
|
3257
3506
|
self.assertEqual(story.referer, "https://example.test/wizard/step-1/")
|
|
3258
3507
|
self.assertEqual(story.user_agent, "FeedbackBot/1.0")
|
|
3259
3508
|
self.assertEqual(story.ip_address, "127.0.0.1")
|
|
3509
|
+
expected_language = (translation.get_language() or "").split("-")[0]
|
|
3510
|
+
self.assertTrue(story.language_code)
|
|
3511
|
+
self.assertTrue(
|
|
3512
|
+
story.language_code.startswith(expected_language),
|
|
3513
|
+
story.language_code,
|
|
3514
|
+
)
|
|
3515
|
+
|
|
3516
|
+
def test_submission_records_request_language(self):
|
|
3517
|
+
self.client.cookies[settings.LANGUAGE_COOKIE_NAME] = "es"
|
|
3518
|
+
with translation.override("es"):
|
|
3519
|
+
response = self.client.post(
|
|
3520
|
+
self.url,
|
|
3521
|
+
{
|
|
3522
|
+
"rating": 4,
|
|
3523
|
+
"comments": "Buena experiencia",
|
|
3524
|
+
"path": "/es/soporte/",
|
|
3525
|
+
"take_screenshot": "1",
|
|
3526
|
+
},
|
|
3527
|
+
HTTP_ACCEPT_LANGUAGE="es",
|
|
3528
|
+
)
|
|
3529
|
+
|
|
3530
|
+
self.assertEqual(response.status_code, 200)
|
|
3531
|
+
story = UserStory.objects.get()
|
|
3532
|
+
self.assertEqual(story.language_code, "es")
|
|
3260
3533
|
|
|
3261
|
-
def
|
|
3534
|
+
def test_superuser_submission_creates_triage_todo(self):
|
|
3535
|
+
Todo.objects.all().delete()
|
|
3536
|
+
superuser = get_user_model().objects.create_superuser(
|
|
3537
|
+
username="overseer", email="overseer@example.com", password="pwd"
|
|
3538
|
+
)
|
|
3539
|
+
Node.objects.update_or_create(
|
|
3540
|
+
mac_address=Node.get_current_mac(),
|
|
3541
|
+
defaults={
|
|
3542
|
+
"hostname": "local-node",
|
|
3543
|
+
"address": "127.0.0.1",
|
|
3544
|
+
"port": 8000,
|
|
3545
|
+
"public_endpoint": "local-node",
|
|
3546
|
+
},
|
|
3547
|
+
)
|
|
3548
|
+
self.client.force_login(superuser)
|
|
3549
|
+
comments = "Review analytics dashboard flow"
|
|
3262
3550
|
response = self.client.post(
|
|
3263
3551
|
self.url,
|
|
3264
3552
|
{
|
|
3265
|
-
"
|
|
3553
|
+
"rating": 5,
|
|
3554
|
+
"comments": comments,
|
|
3555
|
+
"path": "/reports/analytics/",
|
|
3556
|
+
"take_screenshot": "0",
|
|
3557
|
+
},
|
|
3558
|
+
)
|
|
3559
|
+
self.assertEqual(response.status_code, 200)
|
|
3560
|
+
self.assertEqual(Todo.objects.count(), 1)
|
|
3561
|
+
todo = Todo.objects.get()
|
|
3562
|
+
self.assertEqual(todo.request, f"Triage {comments}")
|
|
3563
|
+
self.assertTrue(todo.is_user_data)
|
|
3564
|
+
self.assertEqual(todo.original_user, superuser)
|
|
3565
|
+
self.assertTrue(todo.original_user_is_authenticated)
|
|
3566
|
+
self.assertEqual(todo.origin_node, Node.get_local())
|
|
3567
|
+
|
|
3568
|
+
def test_screenshot_request_links_saved_sample(self):
|
|
3569
|
+
self.client.force_login(self.user)
|
|
3570
|
+
screenshot_file = Path("/tmp/fake.png")
|
|
3571
|
+
self.mock_capture.return_value = screenshot_file
|
|
3572
|
+
sample = ContentSample.objects.create(kind=ContentSample.IMAGE)
|
|
3573
|
+
self.mock_save.return_value = sample
|
|
3574
|
+
|
|
3575
|
+
response = self.client.post(
|
|
3576
|
+
self.url,
|
|
3577
|
+
{
|
|
3578
|
+
"rating": 5,
|
|
3579
|
+
"comments": "Loved the experience!",
|
|
3580
|
+
"path": "/wizard/step-1/",
|
|
3581
|
+
"take_screenshot": "1",
|
|
3582
|
+
},
|
|
3583
|
+
HTTP_REFERER="https://example.test/wizard/step-1/",
|
|
3584
|
+
)
|
|
3585
|
+
|
|
3586
|
+
self.assertEqual(response.status_code, 200)
|
|
3587
|
+
story = UserStory.objects.get()
|
|
3588
|
+
self.assertEqual(story.screenshot, sample)
|
|
3589
|
+
self.mock_capture.assert_called_once_with("https://example.test/wizard/step-1/")
|
|
3590
|
+
self.mock_save.assert_called_once_with(
|
|
3591
|
+
screenshot_file,
|
|
3592
|
+
method="USER_STORY",
|
|
3593
|
+
user=self.user,
|
|
3594
|
+
)
|
|
3595
|
+
|
|
3596
|
+
def test_anonymous_submission_uses_provided_email(self):
|
|
3597
|
+
response = self.client.post(
|
|
3598
|
+
self.url,
|
|
3599
|
+
{
|
|
3600
|
+
"name": "guest@example.com",
|
|
3266
3601
|
"rating": 3,
|
|
3267
3602
|
"comments": "It was fine.",
|
|
3268
3603
|
"path": "/status/",
|
|
@@ -3272,7 +3607,7 @@ class UserStorySubmissionTests(TestCase):
|
|
|
3272
3607
|
self.assertEqual(response.status_code, 200)
|
|
3273
3608
|
self.assertEqual(UserStory.objects.count(), 1)
|
|
3274
3609
|
story = UserStory.objects.get()
|
|
3275
|
-
self.assertEqual(story.name, "
|
|
3610
|
+
self.assertEqual(story.name, "guest@example.com")
|
|
3276
3611
|
self.assertIsNone(story.user)
|
|
3277
3612
|
self.assertIsNone(story.owner)
|
|
3278
3613
|
self.assertEqual(story.comments, "It was fine.")
|
|
@@ -3294,7 +3629,7 @@ class UserStorySubmissionTests(TestCase):
|
|
|
3294
3629
|
self.assertFalse(UserStory.objects.exists())
|
|
3295
3630
|
self.assertIn("rating", data.get("errors", {}))
|
|
3296
3631
|
|
|
3297
|
-
def
|
|
3632
|
+
def test_anonymous_submission_without_email_returns_errors(self):
|
|
3298
3633
|
response = self.client.post(
|
|
3299
3634
|
self.url,
|
|
3300
3635
|
{
|
|
@@ -3304,18 +3639,32 @@ class UserStorySubmissionTests(TestCase):
|
|
|
3304
3639
|
"take_screenshot": "1",
|
|
3305
3640
|
},
|
|
3306
3641
|
)
|
|
3307
|
-
self.assertEqual(response.status_code,
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
self.
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
self.
|
|
3642
|
+
self.assertEqual(response.status_code, 400)
|
|
3643
|
+
self.assertFalse(UserStory.objects.exists())
|
|
3644
|
+
data = response.json()
|
|
3645
|
+
self.assertIn("name", data.get("errors", {}))
|
|
3646
|
+
|
|
3647
|
+
def test_anonymous_submission_with_invalid_email_returns_errors(self):
|
|
3648
|
+
response = self.client.post(
|
|
3649
|
+
self.url,
|
|
3650
|
+
{
|
|
3651
|
+
"name": "Guest Reviewer",
|
|
3652
|
+
"rating": 3,
|
|
3653
|
+
"comments": "Needs improvement.",
|
|
3654
|
+
"path": "/feedback/",
|
|
3655
|
+
"take_screenshot": "1",
|
|
3656
|
+
},
|
|
3657
|
+
)
|
|
3658
|
+
self.assertEqual(response.status_code, 400)
|
|
3659
|
+
self.assertFalse(UserStory.objects.exists())
|
|
3660
|
+
data = response.json()
|
|
3661
|
+
self.assertIn("name", data.get("errors", {}))
|
|
3314
3662
|
|
|
3315
3663
|
def test_submission_without_screenshot_request(self):
|
|
3316
3664
|
response = self.client.post(
|
|
3317
3665
|
self.url,
|
|
3318
3666
|
{
|
|
3667
|
+
"name": "guest@example.com",
|
|
3319
3668
|
"rating": 4,
|
|
3320
3669
|
"comments": "Skip the screenshot, please.",
|
|
3321
3670
|
"path": "/feedback/",
|
|
@@ -3325,9 +3674,14 @@ class UserStorySubmissionTests(TestCase):
|
|
|
3325
3674
|
story = UserStory.objects.get()
|
|
3326
3675
|
self.assertFalse(story.take_screenshot)
|
|
3327
3676
|
self.assertIsNone(story.owner)
|
|
3677
|
+
self.assertIsNone(story.screenshot)
|
|
3678
|
+
self.assertEqual(story.status, UserStory.Status.OPEN)
|
|
3679
|
+
self.mock_capture.assert_not_called()
|
|
3680
|
+
self.mock_save.assert_not_called()
|
|
3328
3681
|
|
|
3329
3682
|
def test_rate_limit_blocks_repeated_submissions(self):
|
|
3330
3683
|
payload = {
|
|
3684
|
+
"name": "guest@example.com",
|
|
3331
3685
|
"rating": 4,
|
|
3332
3686
|
"comments": "Pretty good",
|
|
3333
3687
|
"path": "/feedback/",
|
|
@@ -3428,6 +3782,8 @@ class UserStoryAdminActionTests(TestCase):
|
|
|
3428
3782
|
comments="Helpful notes",
|
|
3429
3783
|
take_screenshot=True,
|
|
3430
3784
|
)
|
|
3785
|
+
self.story.language_code = "es"
|
|
3786
|
+
self.story.save(update_fields=["language_code"])
|
|
3431
3787
|
self.admin = UserStoryAdmin(UserStory, admin.site)
|
|
3432
3788
|
|
|
3433
3789
|
def _build_request(self):
|
|
@@ -3461,6 +3817,8 @@ class UserStoryAdminActionTests(TestCase):
|
|
|
3461
3817
|
args, kwargs = mock_create_issue.call_args
|
|
3462
3818
|
self.assertIn("Feedback for", args[0])
|
|
3463
3819
|
self.assertIn("**Rating:**", args[1])
|
|
3820
|
+
self.assertIn("**Language:**", args[1])
|
|
3821
|
+
self.assertIn("(es)", args[1])
|
|
3464
3822
|
self.assertEqual(kwargs.get("labels"), ["feedback"])
|
|
3465
3823
|
self.assertEqual(
|
|
3466
3824
|
kwargs.get("fingerprint"), f"user-story:{self.story.pk}"
|
pages/urls.py
CHANGED
|
@@ -6,12 +6,22 @@ app_name = "pages"
|
|
|
6
6
|
|
|
7
7
|
urlpatterns = [
|
|
8
8
|
path("", views.index, name="index"),
|
|
9
|
+
path(
|
|
10
|
+
"read/assets/<str:source>/<path:asset>",
|
|
11
|
+
views.readme_asset,
|
|
12
|
+
name="readme-asset",
|
|
13
|
+
),
|
|
9
14
|
path("read/<path:doc>/edit/", views.readme_edit, name="readme-edit"),
|
|
10
15
|
path("read/", views.readme, name="readme"),
|
|
11
16
|
path("read/<path:doc>", views.readme, name="readme-document"),
|
|
12
17
|
path("sitemap.xml", views.sitemap, name="pages-sitemap"),
|
|
13
18
|
path("release/", views.release_admin_redirect, name="release-admin"),
|
|
14
19
|
path("client-report/", views.client_report, name="client-report"),
|
|
20
|
+
path(
|
|
21
|
+
"client-report/download/<int:report_id>/",
|
|
22
|
+
views.client_report_download,
|
|
23
|
+
name="client-report-download",
|
|
24
|
+
),
|
|
15
25
|
path("release-checklist", views.release_checklist, name="release-checklist"),
|
|
16
26
|
path("login/", views.login_view, name="login"),
|
|
17
27
|
path("authenticator/setup/", views.authenticator_setup, name="authenticator-setup"),
|