arthexis 0.1.20__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.20.dist-info → arthexis-0.1.22.dist-info}/METADATA +10 -11
- {arthexis-0.1.20.dist-info → arthexis-0.1.22.dist-info}/RECORD +34 -36
- config/asgi.py +1 -15
- config/settings.py +4 -26
- config/urls.py +5 -1
- core/admin.py +140 -252
- core/apps.py +0 -6
- core/environment.py +2 -220
- core/models.py +425 -77
- core/system.py +76 -0
- core/tests.py +153 -15
- core/views.py +35 -97
- nodes/admin.py +165 -32
- nodes/apps.py +11 -0
- nodes/models.py +26 -6
- nodes/tests.py +263 -1
- nodes/views.py +61 -1
- ocpp/admin.py +68 -7
- ocpp/consumers.py +1 -0
- ocpp/models.py +71 -1
- ocpp/tasks.py +99 -1
- ocpp/tests.py +310 -2
- ocpp/views.py +365 -5
- pages/admin.py +112 -15
- pages/apps.py +32 -0
- pages/context_processors.py +0 -12
- pages/forms.py +31 -8
- pages/models.py +42 -2
- pages/tests.py +361 -63
- pages/urls.py +5 -1
- pages/views.py +264 -16
- core/workgroup_urls.py +0 -17
- core/workgroup_views.py +0 -94
- {arthexis-0.1.20.dist-info → arthexis-0.1.22.dist-info}/WHEEL +0 -0
- {arthexis-0.1.20.dist-info → arthexis-0.1.22.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.20.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):
|
|
@@ -3221,47 +3443,6 @@ class AdminModelGraphViewTests(TestCase):
|
|
|
3221
3443
|
self.assertEqual(kwargs.get("format"), "pdf")
|
|
3222
3444
|
|
|
3223
3445
|
|
|
3224
|
-
class DatasetteTests(TestCase):
|
|
3225
|
-
def setUp(self):
|
|
3226
|
-
self.client = Client()
|
|
3227
|
-
User = get_user_model()
|
|
3228
|
-
self.user = User.objects.create_user(username="ds", password="pwd")
|
|
3229
|
-
Site.objects.update_or_create(id=1, defaults={"name": "Terminal"})
|
|
3230
|
-
|
|
3231
|
-
def test_datasette_auth_endpoint(self):
|
|
3232
|
-
resp = self.client.get(reverse("pages:datasette-auth"))
|
|
3233
|
-
self.assertEqual(resp.status_code, 401)
|
|
3234
|
-
self.client.force_login(self.user)
|
|
3235
|
-
resp = self.client.get(reverse("pages:datasette-auth"))
|
|
3236
|
-
self.assertEqual(resp.status_code, 200)
|
|
3237
|
-
|
|
3238
|
-
def test_navbar_includes_datasette_when_enabled(self):
|
|
3239
|
-
lock_dir = Path(settings.BASE_DIR) / "locks"
|
|
3240
|
-
lock_dir.mkdir(exist_ok=True)
|
|
3241
|
-
lock_file = lock_dir / "datasette.lck"
|
|
3242
|
-
try:
|
|
3243
|
-
lock_file.touch()
|
|
3244
|
-
resp = self.client.get(reverse("pages:index"))
|
|
3245
|
-
self.assertContains(resp, 'href="/data/"')
|
|
3246
|
-
finally:
|
|
3247
|
-
lock_file.unlink(missing_ok=True)
|
|
3248
|
-
|
|
3249
|
-
def test_admin_home_includes_datasette_button_when_enabled(self):
|
|
3250
|
-
lock_dir = Path(settings.BASE_DIR) / "locks"
|
|
3251
|
-
lock_dir.mkdir(exist_ok=True)
|
|
3252
|
-
lock_file = lock_dir / "datasette.lck"
|
|
3253
|
-
try:
|
|
3254
|
-
lock_file.touch()
|
|
3255
|
-
self.user.is_staff = True
|
|
3256
|
-
self.user.is_superuser = True
|
|
3257
|
-
self.user.save()
|
|
3258
|
-
self.client.force_login(self.user)
|
|
3259
|
-
resp = self.client.get(reverse("admin:index"))
|
|
3260
|
-
self.assertContains(resp, 'href="/data/"')
|
|
3261
|
-
self.assertContains(resp, ">Datasette<")
|
|
3262
|
-
finally:
|
|
3263
|
-
lock_file.unlink(missing_ok=True)
|
|
3264
|
-
|
|
3265
3446
|
|
|
3266
3447
|
class UserStorySubmissionTests(TestCase):
|
|
3267
3448
|
def setUp(self):
|
|
@@ -3270,6 +3451,14 @@ class UserStorySubmissionTests(TestCase):
|
|
|
3270
3451
|
self.url = reverse("pages:user-story-submit")
|
|
3271
3452
|
User = get_user_model()
|
|
3272
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)
|
|
3273
3462
|
|
|
3274
3463
|
def test_authenticated_submission_defaults_to_username(self):
|
|
3275
3464
|
self.client.force_login(self.user)
|
|
@@ -3298,12 +3487,98 @@ class UserStorySubmissionTests(TestCase):
|
|
|
3298
3487
|
self.assertEqual(story.referer, "https://example.test/wizard/step-1/")
|
|
3299
3488
|
self.assertEqual(story.user_agent, "FeedbackBot/1.0")
|
|
3300
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
|
+
)
|
|
3301
3510
|
|
|
3302
|
-
|
|
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"
|
|
3303
3531
|
response = self.client.post(
|
|
3304
3532
|
self.url,
|
|
3305
3533
|
{
|
|
3306
|
-
"
|
|
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",
|
|
3307
3582
|
"rating": 3,
|
|
3308
3583
|
"comments": "It was fine.",
|
|
3309
3584
|
"path": "/status/",
|
|
@@ -3313,7 +3588,7 @@ class UserStorySubmissionTests(TestCase):
|
|
|
3313
3588
|
self.assertEqual(response.status_code, 200)
|
|
3314
3589
|
self.assertEqual(UserStory.objects.count(), 1)
|
|
3315
3590
|
story = UserStory.objects.get()
|
|
3316
|
-
self.assertEqual(story.name, "
|
|
3591
|
+
self.assertEqual(story.name, "guest@example.com")
|
|
3317
3592
|
self.assertIsNone(story.user)
|
|
3318
3593
|
self.assertIsNone(story.owner)
|
|
3319
3594
|
self.assertEqual(story.comments, "It was fine.")
|
|
@@ -3335,7 +3610,7 @@ class UserStorySubmissionTests(TestCase):
|
|
|
3335
3610
|
self.assertFalse(UserStory.objects.exists())
|
|
3336
3611
|
self.assertIn("rating", data.get("errors", {}))
|
|
3337
3612
|
|
|
3338
|
-
def
|
|
3613
|
+
def test_anonymous_submission_without_email_returns_errors(self):
|
|
3339
3614
|
response = self.client.post(
|
|
3340
3615
|
self.url,
|
|
3341
3616
|
{
|
|
@@ -3345,18 +3620,32 @@ class UserStorySubmissionTests(TestCase):
|
|
|
3345
3620
|
"take_screenshot": "1",
|
|
3346
3621
|
},
|
|
3347
3622
|
)
|
|
3348
|
-
self.assertEqual(response.status_code,
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
self.
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
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", {}))
|
|
3355
3643
|
|
|
3356
3644
|
def test_submission_without_screenshot_request(self):
|
|
3357
3645
|
response = self.client.post(
|
|
3358
3646
|
self.url,
|
|
3359
3647
|
{
|
|
3648
|
+
"name": "guest@example.com",
|
|
3360
3649
|
"rating": 4,
|
|
3361
3650
|
"comments": "Skip the screenshot, please.",
|
|
3362
3651
|
"path": "/feedback/",
|
|
@@ -3366,9 +3655,14 @@ class UserStorySubmissionTests(TestCase):
|
|
|
3366
3655
|
story = UserStory.objects.get()
|
|
3367
3656
|
self.assertFalse(story.take_screenshot)
|
|
3368
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()
|
|
3369
3662
|
|
|
3370
3663
|
def test_rate_limit_blocks_repeated_submissions(self):
|
|
3371
3664
|
payload = {
|
|
3665
|
+
"name": "guest@example.com",
|
|
3372
3666
|
"rating": 4,
|
|
3373
3667
|
"comments": "Pretty good",
|
|
3374
3668
|
"path": "/feedback/",
|
|
@@ -3469,6 +3763,8 @@ class UserStoryAdminActionTests(TestCase):
|
|
|
3469
3763
|
comments="Helpful notes",
|
|
3470
3764
|
take_screenshot=True,
|
|
3471
3765
|
)
|
|
3766
|
+
self.story.language_code = "es"
|
|
3767
|
+
self.story.save(update_fields=["language_code"])
|
|
3472
3768
|
self.admin = UserStoryAdmin(UserStory, admin.site)
|
|
3473
3769
|
|
|
3474
3770
|
def _build_request(self):
|
|
@@ -3502,6 +3798,8 @@ class UserStoryAdminActionTests(TestCase):
|
|
|
3502
3798
|
args, kwargs = mock_create_issue.call_args
|
|
3503
3799
|
self.assertIn("Feedback for", args[0])
|
|
3504
3800
|
self.assertIn("**Rating:**", args[1])
|
|
3801
|
+
self.assertIn("**Language:**", args[1])
|
|
3802
|
+
self.assertIn("(es)", args[1])
|
|
3505
3803
|
self.assertEqual(kwargs.get("labels"), ["feedback"])
|
|
3506
3804
|
self.assertEqual(
|
|
3507
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"),
|
|
@@ -21,7 +26,6 @@ urlpatterns = [
|
|
|
21
26
|
views.invitation_login,
|
|
22
27
|
name="invitation-login",
|
|
23
28
|
),
|
|
24
|
-
path("datasette-auth/", views.datasette_auth, name="datasette-auth"),
|
|
25
29
|
path("man/", views.manual_list, name="manual-list"),
|
|
26
30
|
path("man/<slug:slug>/", views.manual_detail, name="manual-detail"),
|
|
27
31
|
path("man/<slug:slug>/pdf/", views.manual_pdf, name="manual-pdf"),
|