arthexis 0.1.21__py3-none-any.whl → 0.1.22__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.22.dist-info}/METADATA +8 -8
- {arthexis-0.1.21.dist-info → arthexis-0.1.22.dist-info}/RECORD +31 -31
- config/settings.py +4 -0
- config/urls.py +5 -0
- core/admin.py +139 -19
- core/environment.py +2 -239
- core/models.py +419 -2
- core/system.py +76 -0
- core/tests.py +152 -8
- core/views.py +35 -1
- nodes/admin.py +148 -38
- nodes/apps.py +11 -0
- nodes/models.py +26 -6
- nodes/tests.py +214 -1
- nodes/views.py +1 -0
- ocpp/admin.py +20 -1
- ocpp/consumers.py +1 -0
- ocpp/models.py +23 -1
- ocpp/tasks.py +99 -1
- ocpp/tests.py +227 -2
- ocpp/views.py +281 -3
- pages/admin.py +112 -15
- pages/apps.py +32 -0
- pages/forms.py +31 -8
- pages/models.py +42 -2
- pages/tests.py +361 -22
- pages/urls.py +5 -0
- pages/views.py +264 -11
- {arthexis-0.1.21.dist-info → arthexis-0.1.22.dist-info}/WHEEL +0 -0
- {arthexis-0.1.21.dist-info → arthexis-0.1.22.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.21.dist-info → arthexis-0.1.22.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
|
|
@@ -195,6 +200,41 @@ class LoginViewTests(TestCase):
|
|
|
195
200
|
)
|
|
196
201
|
self.assertRedirects(resp, "/nodes/list/")
|
|
197
202
|
|
|
203
|
+
def test_homepage_excludes_version_banner_for_anonymous(self):
|
|
204
|
+
response = self.client.get(reverse("pages:index"))
|
|
205
|
+
|
|
206
|
+
self.assertEqual(response.status_code, 200)
|
|
207
|
+
self.assertNotContains(response, "__versionCheckInitialized")
|
|
208
|
+
|
|
209
|
+
def test_homepage_includes_version_banner_for_staff(self):
|
|
210
|
+
self.client.force_login(self.staff)
|
|
211
|
+
response = self.client.get(reverse("pages:index"))
|
|
212
|
+
|
|
213
|
+
self.assertEqual(response.status_code, 200)
|
|
214
|
+
self.assertContains(response, "__versionCheckInitialized")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class AdminTemplateVersionBannerTests(TestCase):
|
|
218
|
+
def setUp(self):
|
|
219
|
+
self.client = Client()
|
|
220
|
+
User = get_user_model()
|
|
221
|
+
self.staff = User.objects.create_user(
|
|
222
|
+
username="admin-staff", password="pwd", is_staff=True
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def test_admin_login_excludes_version_banner_for_anonymous(self):
|
|
226
|
+
response = self.client.get(reverse("admin:login"))
|
|
227
|
+
|
|
228
|
+
self.assertEqual(response.status_code, 200)
|
|
229
|
+
self.assertNotContains(response, "__versionCheckInitialized")
|
|
230
|
+
|
|
231
|
+
def test_admin_dashboard_includes_version_banner_for_staff(self):
|
|
232
|
+
self.client.force_login(self.staff)
|
|
233
|
+
response = self.client.get(reverse("admin:index"))
|
|
234
|
+
|
|
235
|
+
self.assertEqual(response.status_code, 200)
|
|
236
|
+
self.assertContains(response, "__versionCheckInitialized")
|
|
237
|
+
|
|
198
238
|
def test_staff_redirects_next_when_specified(self):
|
|
199
239
|
resp = self.client.post(
|
|
200
240
|
reverse("pages:login") + "?next=/nodes/list/",
|
|
@@ -715,6 +755,66 @@ class AdminSidebarTests(TestCase):
|
|
|
715
755
|
self.assertContains(resp, 'id="admin-collapsible-apps"')
|
|
716
756
|
|
|
717
757
|
|
|
758
|
+
class AdminGoogleCalendarSidebarTests(TestCase):
|
|
759
|
+
def setUp(self):
|
|
760
|
+
self.client = Client()
|
|
761
|
+
User = get_user_model()
|
|
762
|
+
self.admin = User.objects.create_superuser(
|
|
763
|
+
username="calendar_admin", password="pwd", email="admin@example.com"
|
|
764
|
+
)
|
|
765
|
+
self.client.force_login(self.admin)
|
|
766
|
+
Site.objects.update_or_create(
|
|
767
|
+
id=1, defaults={"name": "test", "domain": "testserver"}
|
|
768
|
+
)
|
|
769
|
+
Node.objects.create(hostname="testserver", address="127.0.0.1")
|
|
770
|
+
|
|
771
|
+
def test_calendar_module_hidden_without_profile(self):
|
|
772
|
+
resp = self.client.get(reverse("admin:index"))
|
|
773
|
+
self.assertNotContains(resp, 'id="google-calendar-module"', html=False)
|
|
774
|
+
|
|
775
|
+
@patch("core.models.GoogleCalendarProfile.fetch_events")
|
|
776
|
+
def test_calendar_module_shows_events_for_user(self, fetch_events):
|
|
777
|
+
fetch_events.return_value = [
|
|
778
|
+
{
|
|
779
|
+
"summary": "Standup",
|
|
780
|
+
"start": timezone.now(),
|
|
781
|
+
"end": None,
|
|
782
|
+
"all_day": False,
|
|
783
|
+
"html_link": "https://calendar.google.com/event",
|
|
784
|
+
"location": "HQ",
|
|
785
|
+
}
|
|
786
|
+
]
|
|
787
|
+
GoogleCalendarProfile.objects.create(
|
|
788
|
+
user=self.admin,
|
|
789
|
+
calendar_id="example@group.calendar.google.com",
|
|
790
|
+
api_key="secret",
|
|
791
|
+
display_name="Team Calendar",
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
resp = self.client.get(reverse("admin:index"))
|
|
795
|
+
|
|
796
|
+
self.assertContains(resp, 'id="google-calendar-module"', html=False)
|
|
797
|
+
self.assertContains(resp, "Standup")
|
|
798
|
+
self.assertContains(resp, "Open full calendar")
|
|
799
|
+
fetch_events.assert_called_once()
|
|
800
|
+
|
|
801
|
+
@patch("core.models.GoogleCalendarProfile.fetch_events")
|
|
802
|
+
def test_calendar_module_uses_group_profile(self, fetch_events):
|
|
803
|
+
fetch_events.return_value = []
|
|
804
|
+
group = SecurityGroup.objects.create(name="Calendar Group")
|
|
805
|
+
self.admin.groups.add(group)
|
|
806
|
+
GoogleCalendarProfile.objects.create(
|
|
807
|
+
group=group,
|
|
808
|
+
calendar_id="group@calendar.google.com",
|
|
809
|
+
api_key="secret",
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
resp = self.client.get(reverse("admin:index"))
|
|
813
|
+
|
|
814
|
+
self.assertContains(resp, 'id="google-calendar-module"', html=False)
|
|
815
|
+
fetch_events.assert_called_once()
|
|
816
|
+
|
|
817
|
+
|
|
718
818
|
class ViewHistoryLoggingTests(TestCase):
|
|
719
819
|
def setUp(self):
|
|
720
820
|
self.client = Client()
|
|
@@ -816,6 +916,35 @@ class ViewHistoryLoggingTests(TestCase):
|
|
|
816
916
|
self.assertEqual(lead.path, "/")
|
|
817
917
|
self.assertEqual(lead.referer, "https://example.com/ref")
|
|
818
918
|
|
|
919
|
+
def test_pages_config_purges_old_view_history(self):
|
|
920
|
+
ViewHistory.objects.all().delete()
|
|
921
|
+
|
|
922
|
+
old_entry = ViewHistory.objects.create(
|
|
923
|
+
path="/old/",
|
|
924
|
+
method="GET",
|
|
925
|
+
status_code=200,
|
|
926
|
+
status_text="OK",
|
|
927
|
+
)
|
|
928
|
+
new_entry = ViewHistory.objects.create(
|
|
929
|
+
path="/recent/",
|
|
930
|
+
method="GET",
|
|
931
|
+
status_code=200,
|
|
932
|
+
status_text="OK",
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
ViewHistory.objects.filter(pk=old_entry.pk).update(
|
|
936
|
+
visited_at=timezone.now() - timedelta(days=20)
|
|
937
|
+
)
|
|
938
|
+
ViewHistory.objects.filter(pk=new_entry.pk).update(
|
|
939
|
+
visited_at=timezone.now() - timedelta(days=10)
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
config = django_apps.get_app_config("pages")
|
|
943
|
+
config._purge_view_history()
|
|
944
|
+
|
|
945
|
+
self.assertFalse(ViewHistory.objects.filter(pk=old_entry.pk).exists())
|
|
946
|
+
self.assertTrue(ViewHistory.objects.filter(pk=new_entry.pk).exists())
|
|
947
|
+
|
|
819
948
|
def test_landing_visit_does_not_record_lead_without_celery(self):
|
|
820
949
|
role = NodeRole.objects.create(name="no-celery-role")
|
|
821
950
|
application = Application.objects.create(
|
|
@@ -913,6 +1042,44 @@ class ViewHistoryAdminTests(TestCase):
|
|
|
913
1042
|
self.assertEqual(totals.get("/"), 2)
|
|
914
1043
|
self.assertEqual(totals.get("/about/"), 1)
|
|
915
1044
|
|
|
1045
|
+
def test_graph_data_endpoint_respects_days_parameter(self):
|
|
1046
|
+
ViewHistory.all_objects.all().delete()
|
|
1047
|
+
reference_date = date(2025, 5, 1)
|
|
1048
|
+
tz = timezone.get_current_timezone()
|
|
1049
|
+
path = "/range/"
|
|
1050
|
+
|
|
1051
|
+
for offset in range(10):
|
|
1052
|
+
entry = ViewHistory.objects.create(
|
|
1053
|
+
path=path,
|
|
1054
|
+
method="GET",
|
|
1055
|
+
status_code=200,
|
|
1056
|
+
status_text="OK",
|
|
1057
|
+
error_message="",
|
|
1058
|
+
view_name="pages:index",
|
|
1059
|
+
)
|
|
1060
|
+
visited_date = reference_date - timedelta(days=offset)
|
|
1061
|
+
visited_at = timezone.make_aware(
|
|
1062
|
+
datetime.combine(visited_date, datetime_time(12, 0)), tz
|
|
1063
|
+
)
|
|
1064
|
+
entry.visited_at = visited_at
|
|
1065
|
+
entry.save(update_fields=["visited_at"])
|
|
1066
|
+
|
|
1067
|
+
url = reverse("admin:pages_viewhistory_traffic_data")
|
|
1068
|
+
with patch("pages.admin.timezone.localdate", return_value=reference_date):
|
|
1069
|
+
resp = self.client.get(url, {"days": 7})
|
|
1070
|
+
|
|
1071
|
+
self.assertEqual(resp.status_code, 200)
|
|
1072
|
+
data = resp.json()
|
|
1073
|
+
|
|
1074
|
+
self.assertEqual(len(data.get("labels", [])), 7)
|
|
1075
|
+
self.assertEqual(data.get("meta", {}).get("start"), (reference_date - timedelta(days=6)).isoformat())
|
|
1076
|
+
self.assertEqual(data.get("meta", {}).get("end"), reference_date.isoformat())
|
|
1077
|
+
|
|
1078
|
+
totals = {
|
|
1079
|
+
dataset["label"]: sum(dataset["data"]) for dataset in data.get("datasets", [])
|
|
1080
|
+
}
|
|
1081
|
+
self.assertEqual(totals.get(path), 7)
|
|
1082
|
+
|
|
916
1083
|
def test_graph_data_includes_late_evening_visits(self):
|
|
917
1084
|
target_date = date(2025, 9, 27)
|
|
918
1085
|
entry = ViewHistory.objects.create(
|
|
@@ -2657,6 +2824,20 @@ class FavoriteTests(TestCase):
|
|
|
2657
2824
|
self.assertEqual(fav.custom_label, "Apps")
|
|
2658
2825
|
self.assertTrue(fav.user_data)
|
|
2659
2826
|
|
|
2827
|
+
def test_add_favorite_defaults_user_data_checked(self):
|
|
2828
|
+
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
2829
|
+
url = reverse("admin:favorite_toggle", args=[ct.id])
|
|
2830
|
+
resp = self.client.get(url)
|
|
2831
|
+
self.assertContains(resp, 'name="user_data" checked')
|
|
2832
|
+
|
|
2833
|
+
def test_add_favorite_with_priority(self):
|
|
2834
|
+
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
2835
|
+
url = reverse("admin:favorite_toggle", args=[ct.id])
|
|
2836
|
+
resp = self.client.post(url, {"priority": "7"})
|
|
2837
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
2838
|
+
fav = Favorite.objects.get(user=self.user, content_type=ct)
|
|
2839
|
+
self.assertEqual(fav.priority, 7)
|
|
2840
|
+
|
|
2660
2841
|
def test_cancel_link_uses_next(self):
|
|
2661
2842
|
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
2662
2843
|
next_url = reverse("admin:pages_application_changelist")
|
|
@@ -2666,14 +2847,31 @@ class FavoriteTests(TestCase):
|
|
|
2666
2847
|
resp = self.client.get(url)
|
|
2667
2848
|
self.assertContains(resp, f'href="{next_url}"')
|
|
2668
2849
|
|
|
2669
|
-
def
|
|
2850
|
+
def test_existing_favorite_shows_update_form(self):
|
|
2670
2851
|
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
2671
|
-
Favorite.objects.create(
|
|
2852
|
+
favorite = Favorite.objects.create(
|
|
2853
|
+
user=self.user, content_type=ct, custom_label="Apps", user_data=True
|
|
2854
|
+
)
|
|
2672
2855
|
url = reverse("admin:favorite_toggle", args=[ct.id])
|
|
2673
2856
|
resp = self.client.get(url)
|
|
2674
|
-
self.
|
|
2675
|
-
|
|
2676
|
-
self.assertContains(resp,
|
|
2857
|
+
self.assertContains(resp, "Update Favorite")
|
|
2858
|
+
self.assertContains(resp, "value=\"Apps\"")
|
|
2859
|
+
self.assertContains(resp, "checked")
|
|
2860
|
+
self.assertContains(resp, "name=\"remove\"")
|
|
2861
|
+
|
|
2862
|
+
resp = self.client.post(url, {"custom_label": "Apps Updated"})
|
|
2863
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
2864
|
+
favorite.refresh_from_db()
|
|
2865
|
+
self.assertEqual(favorite.custom_label, "Apps Updated")
|
|
2866
|
+
self.assertFalse(favorite.user_data)
|
|
2867
|
+
|
|
2868
|
+
def test_remove_existing_favorite_from_toggle(self):
|
|
2869
|
+
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
2870
|
+
Favorite.objects.create(user=self.user, content_type=ct)
|
|
2871
|
+
url = reverse("admin:favorite_toggle", args=[ct.id])
|
|
2872
|
+
resp = self.client.post(url, {"remove": "1"})
|
|
2873
|
+
self.assertRedirects(resp, reverse("admin:index"))
|
|
2874
|
+
self.assertFalse(Favorite.objects.filter(user=self.user, content_type=ct).exists())
|
|
2677
2875
|
|
|
2678
2876
|
def test_update_user_data_from_list(self):
|
|
2679
2877
|
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
@@ -2684,6 +2882,15 @@ class FavoriteTests(TestCase):
|
|
|
2684
2882
|
fav.refresh_from_db()
|
|
2685
2883
|
self.assertTrue(fav.user_data)
|
|
2686
2884
|
|
|
2885
|
+
def test_update_priority_from_list(self):
|
|
2886
|
+
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
2887
|
+
fav = Favorite.objects.create(user=self.user, content_type=ct, priority=3)
|
|
2888
|
+
url = reverse("admin:favorite_list")
|
|
2889
|
+
resp = self.client.post(url, {f"priority_{fav.pk}": "12"})
|
|
2890
|
+
self.assertRedirects(resp, url)
|
|
2891
|
+
fav.refresh_from_db()
|
|
2892
|
+
self.assertEqual(fav.priority, 12)
|
|
2893
|
+
|
|
2687
2894
|
def test_dashboard_includes_favorites_and_user_data(self):
|
|
2688
2895
|
fav_ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
2689
2896
|
Favorite.objects.create(
|
|
@@ -2694,6 +2901,12 @@ class FavoriteTests(TestCase):
|
|
|
2694
2901
|
self.assertContains(resp, reverse("admin:pages_application_changelist"))
|
|
2695
2902
|
self.assertContains(resp, reverse("admin:nodes_noderole_changelist"))
|
|
2696
2903
|
|
|
2904
|
+
def test_dashboard_shows_empty_todo_state(self):
|
|
2905
|
+
Todo.objects.all().delete()
|
|
2906
|
+
resp = self.client.get(reverse("admin:index"))
|
|
2907
|
+
self.assertContains(resp, "Release manager tasks")
|
|
2908
|
+
self.assertContains(resp, "No pending TODOs")
|
|
2909
|
+
|
|
2697
2910
|
def test_dashboard_merges_duplicate_future_actions(self):
|
|
2698
2911
|
ct = ContentType.objects.get_for_model(NodeRole)
|
|
2699
2912
|
Favorite.objects.create(user=self.user, content_type=ct)
|
|
@@ -2933,7 +3146,11 @@ class FavoriteTests(TestCase):
|
|
|
2933
3146
|
todo = Todo.objects.create(request="Do thing")
|
|
2934
3147
|
resp = self.client.get(reverse("admin:index"))
|
|
2935
3148
|
done_url = reverse("todo-done", args=[todo.pk])
|
|
2936
|
-
|
|
3149
|
+
tooltip = escape(todo.request)
|
|
3150
|
+
self.assertContains(resp, f'title="{tooltip}"')
|
|
3151
|
+
self.assertContains(resp, f'aria-label="{tooltip}"')
|
|
3152
|
+
task_label = gettext("Task %(counter)s") % {"counter": 1}
|
|
3153
|
+
self.assertContains(resp, task_label)
|
|
2937
3154
|
self.assertContains(resp, f'action="{done_url}"')
|
|
2938
3155
|
self.assertContains(resp, "DONE")
|
|
2939
3156
|
|
|
@@ -2961,7 +3178,8 @@ class FavoriteTests(TestCase):
|
|
|
2961
3178
|
|
|
2962
3179
|
resp = self.client.get(reverse("admin:index"))
|
|
2963
3180
|
self.assertContains(resp, "Release manager tasks")
|
|
2964
|
-
|
|
3181
|
+
tooltip = escape("Check fallback")
|
|
3182
|
+
self.assertContains(resp, f'title="{tooltip}"')
|
|
2965
3183
|
|
|
2966
3184
|
def test_dashboard_shows_todos_without_release_manager_profile(self):
|
|
2967
3185
|
Todo.objects.create(request="Unrestricted task")
|
|
@@ -2969,7 +3187,8 @@ class FavoriteTests(TestCase):
|
|
|
2969
3187
|
|
|
2970
3188
|
resp = self.client.get(reverse("admin:index"))
|
|
2971
3189
|
self.assertContains(resp, "Release manager tasks")
|
|
2972
|
-
|
|
3190
|
+
tooltip = escape("Unrestricted task")
|
|
3191
|
+
self.assertContains(resp, f'title="{tooltip}"')
|
|
2973
3192
|
|
|
2974
3193
|
def test_dashboard_excludes_todo_changelist_link(self):
|
|
2975
3194
|
ct = ContentType.objects.get_for_model(Todo)
|
|
@@ -2993,7 +3212,8 @@ class FavoriteTests(TestCase):
|
|
|
2993
3212
|
self.client.force_login(other_user)
|
|
2994
3213
|
resp = self.client.get(reverse("admin:index"))
|
|
2995
3214
|
self.assertContains(resp, "Release manager tasks")
|
|
2996
|
-
|
|
3215
|
+
tooltip = escape(todo.request)
|
|
3216
|
+
self.assertContains(resp, f'title="{tooltip}"')
|
|
2997
3217
|
|
|
2998
3218
|
def test_dashboard_shows_todos_for_non_terminal_node(self):
|
|
2999
3219
|
todo = Todo.objects.create(request="Terminal Tasks")
|
|
@@ -3004,7 +3224,8 @@ class FavoriteTests(TestCase):
|
|
|
3004
3224
|
self.node.save(update_fields=["role"])
|
|
3005
3225
|
resp = self.client.get(reverse("admin:index"))
|
|
3006
3226
|
self.assertContains(resp, "Release manager tasks")
|
|
3007
|
-
|
|
3227
|
+
tooltip = escape(todo.request)
|
|
3228
|
+
self.assertContains(resp, f'title="{tooltip}"')
|
|
3008
3229
|
|
|
3009
3230
|
def test_dashboard_shows_todos_for_delegate_release_manager(self):
|
|
3010
3231
|
todo = Todo.objects.create(request="Delegate Task")
|
|
@@ -3026,7 +3247,8 @@ class FavoriteTests(TestCase):
|
|
|
3026
3247
|
self.client.force_login(operator)
|
|
3027
3248
|
resp = self.client.get(reverse("admin:index"))
|
|
3028
3249
|
self.assertContains(resp, "Release manager tasks")
|
|
3029
|
-
|
|
3250
|
+
tooltip = escape(todo.request)
|
|
3251
|
+
self.assertContains(resp, f'title="{tooltip}"')
|
|
3030
3252
|
|
|
3031
3253
|
|
|
3032
3254
|
class AdminIndexQueryRegressionTests(TestCase):
|
|
@@ -3229,6 +3451,14 @@ class UserStorySubmissionTests(TestCase):
|
|
|
3229
3451
|
self.url = reverse("pages:user-story-submit")
|
|
3230
3452
|
User = get_user_model()
|
|
3231
3453
|
self.user = User.objects.create_user(username="feedbacker", password="pwd")
|
|
3454
|
+
self.capture_patcher = patch("pages.views.capture_screenshot", autospec=True)
|
|
3455
|
+
self.save_patcher = patch("pages.views.save_screenshot", autospec=True)
|
|
3456
|
+
self.mock_capture = self.capture_patcher.start()
|
|
3457
|
+
self.mock_save = self.save_patcher.start()
|
|
3458
|
+
self.mock_capture.return_value = Path("/tmp/fake.png")
|
|
3459
|
+
self.mock_save.return_value = None
|
|
3460
|
+
self.addCleanup(self.capture_patcher.stop)
|
|
3461
|
+
self.addCleanup(self.save_patcher.stop)
|
|
3232
3462
|
|
|
3233
3463
|
def test_authenticated_submission_defaults_to_username(self):
|
|
3234
3464
|
self.client.force_login(self.user)
|
|
@@ -3257,12 +3487,98 @@ class UserStorySubmissionTests(TestCase):
|
|
|
3257
3487
|
self.assertEqual(story.referer, "https://example.test/wizard/step-1/")
|
|
3258
3488
|
self.assertEqual(story.user_agent, "FeedbackBot/1.0")
|
|
3259
3489
|
self.assertEqual(story.ip_address, "127.0.0.1")
|
|
3490
|
+
expected_language = (translation.get_language() or "").split("-")[0]
|
|
3491
|
+
self.assertTrue(story.language_code)
|
|
3492
|
+
self.assertTrue(
|
|
3493
|
+
story.language_code.startswith(expected_language),
|
|
3494
|
+
story.language_code,
|
|
3495
|
+
)
|
|
3496
|
+
|
|
3497
|
+
def test_submission_records_request_language(self):
|
|
3498
|
+
self.client.cookies[settings.LANGUAGE_COOKIE_NAME] = "es"
|
|
3499
|
+
with translation.override("es"):
|
|
3500
|
+
response = self.client.post(
|
|
3501
|
+
self.url,
|
|
3502
|
+
{
|
|
3503
|
+
"rating": 4,
|
|
3504
|
+
"comments": "Buena experiencia",
|
|
3505
|
+
"path": "/es/soporte/",
|
|
3506
|
+
"take_screenshot": "1",
|
|
3507
|
+
},
|
|
3508
|
+
HTTP_ACCEPT_LANGUAGE="es",
|
|
3509
|
+
)
|
|
3260
3510
|
|
|
3261
|
-
|
|
3511
|
+
self.assertEqual(response.status_code, 200)
|
|
3512
|
+
story = UserStory.objects.get()
|
|
3513
|
+
self.assertEqual(story.language_code, "es")
|
|
3514
|
+
|
|
3515
|
+
def test_superuser_submission_creates_triage_todo(self):
|
|
3516
|
+
Todo.objects.all().delete()
|
|
3517
|
+
superuser = get_user_model().objects.create_superuser(
|
|
3518
|
+
username="overseer", email="overseer@example.com", password="pwd"
|
|
3519
|
+
)
|
|
3520
|
+
Node.objects.update_or_create(
|
|
3521
|
+
mac_address=Node.get_current_mac(),
|
|
3522
|
+
defaults={
|
|
3523
|
+
"hostname": "local-node",
|
|
3524
|
+
"address": "127.0.0.1",
|
|
3525
|
+
"port": 8000,
|
|
3526
|
+
"public_endpoint": "local-node",
|
|
3527
|
+
},
|
|
3528
|
+
)
|
|
3529
|
+
self.client.force_login(superuser)
|
|
3530
|
+
comments = "Review analytics dashboard flow"
|
|
3262
3531
|
response = self.client.post(
|
|
3263
3532
|
self.url,
|
|
3264
3533
|
{
|
|
3265
|
-
"
|
|
3534
|
+
"rating": 5,
|
|
3535
|
+
"comments": comments,
|
|
3536
|
+
"path": "/reports/analytics/",
|
|
3537
|
+
"take_screenshot": "0",
|
|
3538
|
+
},
|
|
3539
|
+
)
|
|
3540
|
+
self.assertEqual(response.status_code, 200)
|
|
3541
|
+
self.assertEqual(Todo.objects.count(), 1)
|
|
3542
|
+
todo = Todo.objects.get()
|
|
3543
|
+
self.assertEqual(todo.request, f"Triage {comments}")
|
|
3544
|
+
self.assertTrue(todo.is_user_data)
|
|
3545
|
+
self.assertEqual(todo.original_user, superuser)
|
|
3546
|
+
self.assertTrue(todo.original_user_is_authenticated)
|
|
3547
|
+
self.assertEqual(todo.origin_node, Node.get_local())
|
|
3548
|
+
|
|
3549
|
+
def test_screenshot_request_links_saved_sample(self):
|
|
3550
|
+
self.client.force_login(self.user)
|
|
3551
|
+
screenshot_file = Path("/tmp/fake.png")
|
|
3552
|
+
self.mock_capture.return_value = screenshot_file
|
|
3553
|
+
sample = ContentSample.objects.create(kind=ContentSample.IMAGE)
|
|
3554
|
+
self.mock_save.return_value = sample
|
|
3555
|
+
|
|
3556
|
+
response = self.client.post(
|
|
3557
|
+
self.url,
|
|
3558
|
+
{
|
|
3559
|
+
"rating": 5,
|
|
3560
|
+
"comments": "Loved the experience!",
|
|
3561
|
+
"path": "/wizard/step-1/",
|
|
3562
|
+
"take_screenshot": "1",
|
|
3563
|
+
},
|
|
3564
|
+
HTTP_REFERER="https://example.test/wizard/step-1/",
|
|
3565
|
+
)
|
|
3566
|
+
|
|
3567
|
+
self.assertEqual(response.status_code, 200)
|
|
3568
|
+
story = UserStory.objects.get()
|
|
3569
|
+
self.assertEqual(story.screenshot, sample)
|
|
3570
|
+
self.mock_capture.assert_called_once_with("https://example.test/wizard/step-1/")
|
|
3571
|
+
self.mock_save.assert_called_once_with(
|
|
3572
|
+
screenshot_file,
|
|
3573
|
+
method="USER_STORY",
|
|
3574
|
+
user=self.user,
|
|
3575
|
+
)
|
|
3576
|
+
|
|
3577
|
+
def test_anonymous_submission_uses_provided_email(self):
|
|
3578
|
+
response = self.client.post(
|
|
3579
|
+
self.url,
|
|
3580
|
+
{
|
|
3581
|
+
"name": "guest@example.com",
|
|
3266
3582
|
"rating": 3,
|
|
3267
3583
|
"comments": "It was fine.",
|
|
3268
3584
|
"path": "/status/",
|
|
@@ -3272,7 +3588,7 @@ class UserStorySubmissionTests(TestCase):
|
|
|
3272
3588
|
self.assertEqual(response.status_code, 200)
|
|
3273
3589
|
self.assertEqual(UserStory.objects.count(), 1)
|
|
3274
3590
|
story = UserStory.objects.get()
|
|
3275
|
-
self.assertEqual(story.name, "
|
|
3591
|
+
self.assertEqual(story.name, "guest@example.com")
|
|
3276
3592
|
self.assertIsNone(story.user)
|
|
3277
3593
|
self.assertIsNone(story.owner)
|
|
3278
3594
|
self.assertEqual(story.comments, "It was fine.")
|
|
@@ -3294,7 +3610,7 @@ class UserStorySubmissionTests(TestCase):
|
|
|
3294
3610
|
self.assertFalse(UserStory.objects.exists())
|
|
3295
3611
|
self.assertIn("rating", data.get("errors", {}))
|
|
3296
3612
|
|
|
3297
|
-
def
|
|
3613
|
+
def test_anonymous_submission_without_email_returns_errors(self):
|
|
3298
3614
|
response = self.client.post(
|
|
3299
3615
|
self.url,
|
|
3300
3616
|
{
|
|
@@ -3304,18 +3620,32 @@ class UserStorySubmissionTests(TestCase):
|
|
|
3304
3620
|
"take_screenshot": "1",
|
|
3305
3621
|
},
|
|
3306
3622
|
)
|
|
3307
|
-
self.assertEqual(response.status_code,
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
self.
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
self.
|
|
3623
|
+
self.assertEqual(response.status_code, 400)
|
|
3624
|
+
self.assertFalse(UserStory.objects.exists())
|
|
3625
|
+
data = response.json()
|
|
3626
|
+
self.assertIn("name", data.get("errors", {}))
|
|
3627
|
+
|
|
3628
|
+
def test_anonymous_submission_with_invalid_email_returns_errors(self):
|
|
3629
|
+
response = self.client.post(
|
|
3630
|
+
self.url,
|
|
3631
|
+
{
|
|
3632
|
+
"name": "Guest Reviewer",
|
|
3633
|
+
"rating": 3,
|
|
3634
|
+
"comments": "Needs improvement.",
|
|
3635
|
+
"path": "/feedback/",
|
|
3636
|
+
"take_screenshot": "1",
|
|
3637
|
+
},
|
|
3638
|
+
)
|
|
3639
|
+
self.assertEqual(response.status_code, 400)
|
|
3640
|
+
self.assertFalse(UserStory.objects.exists())
|
|
3641
|
+
data = response.json()
|
|
3642
|
+
self.assertIn("name", data.get("errors", {}))
|
|
3314
3643
|
|
|
3315
3644
|
def test_submission_without_screenshot_request(self):
|
|
3316
3645
|
response = self.client.post(
|
|
3317
3646
|
self.url,
|
|
3318
3647
|
{
|
|
3648
|
+
"name": "guest@example.com",
|
|
3319
3649
|
"rating": 4,
|
|
3320
3650
|
"comments": "Skip the screenshot, please.",
|
|
3321
3651
|
"path": "/feedback/",
|
|
@@ -3325,9 +3655,14 @@ class UserStorySubmissionTests(TestCase):
|
|
|
3325
3655
|
story = UserStory.objects.get()
|
|
3326
3656
|
self.assertFalse(story.take_screenshot)
|
|
3327
3657
|
self.assertIsNone(story.owner)
|
|
3658
|
+
self.assertIsNone(story.screenshot)
|
|
3659
|
+
self.assertEqual(story.status, UserStory.Status.OPEN)
|
|
3660
|
+
self.mock_capture.assert_not_called()
|
|
3661
|
+
self.mock_save.assert_not_called()
|
|
3328
3662
|
|
|
3329
3663
|
def test_rate_limit_blocks_repeated_submissions(self):
|
|
3330
3664
|
payload = {
|
|
3665
|
+
"name": "guest@example.com",
|
|
3331
3666
|
"rating": 4,
|
|
3332
3667
|
"comments": "Pretty good",
|
|
3333
3668
|
"path": "/feedback/",
|
|
@@ -3428,6 +3763,8 @@ class UserStoryAdminActionTests(TestCase):
|
|
|
3428
3763
|
comments="Helpful notes",
|
|
3429
3764
|
take_screenshot=True,
|
|
3430
3765
|
)
|
|
3766
|
+
self.story.language_code = "es"
|
|
3767
|
+
self.story.save(update_fields=["language_code"])
|
|
3431
3768
|
self.admin = UserStoryAdmin(UserStory, admin.site)
|
|
3432
3769
|
|
|
3433
3770
|
def _build_request(self):
|
|
@@ -3461,6 +3798,8 @@ class UserStoryAdminActionTests(TestCase):
|
|
|
3461
3798
|
args, kwargs = mock_create_issue.call_args
|
|
3462
3799
|
self.assertIn("Feedback for", args[0])
|
|
3463
3800
|
self.assertIn("**Rating:**", args[1])
|
|
3801
|
+
self.assertIn("**Language:**", args[1])
|
|
3802
|
+
self.assertIn("(es)", args[1])
|
|
3464
3803
|
self.assertEqual(kwargs.get("labels"), ["feedback"])
|
|
3465
3804
|
self.assertEqual(
|
|
3466
3805
|
kwargs.get("fingerprint"), f"user-story:{self.story.pk}"
|
pages/urls.py
CHANGED
|
@@ -6,6 +6,11 @@ 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"),
|