arthexis 0.1.15__py3-none-any.whl → 0.1.17__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.15.dist-info → arthexis-0.1.17.dist-info}/METADATA +1 -2
- {arthexis-0.1.15.dist-info → arthexis-0.1.17.dist-info}/RECORD +40 -39
- config/settings.py +3 -0
- config/urls.py +5 -0
- core/admin.py +242 -15
- core/admindocs.py +44 -3
- core/apps.py +1 -1
- core/backends.py +46 -8
- core/changelog.py +66 -5
- core/github_issues.py +12 -7
- core/mailer.py +9 -5
- core/models.py +121 -29
- core/release.py +107 -2
- core/system.py +209 -2
- core/tasks.py +5 -7
- core/test_system_info.py +16 -0
- core/tests.py +329 -0
- core/views.py +279 -40
- nodes/admin.py +25 -1
- nodes/models.py +70 -4
- nodes/rfid_sync.py +15 -0
- nodes/tests.py +119 -0
- nodes/utils.py +3 -0
- ocpp/admin.py +92 -10
- ocpp/consumers.py +38 -0
- ocpp/models.py +19 -4
- ocpp/tasks.py +156 -2
- ocpp/test_rfid.py +92 -5
- ocpp/tests.py +243 -1
- ocpp/views.py +23 -5
- pages/admin.py +126 -4
- pages/context_processors.py +20 -1
- pages/models.py +3 -1
- pages/module_defaults.py +156 -0
- pages/tests.py +241 -8
- pages/urls.py +1 -0
- pages/views.py +61 -4
- {arthexis-0.1.15.dist-info → arthexis-0.1.17.dist-info}/WHEEL +0 -0
- {arthexis-0.1.15.dist-info → arthexis-0.1.17.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.15.dist-info → arthexis-0.1.17.dist-info}/top_level.txt +0 -0
pages/tests.py
CHANGED
|
@@ -8,7 +8,7 @@ django.setup()
|
|
|
8
8
|
|
|
9
9
|
from django.test import Client, RequestFactory, TestCase, SimpleTestCase, override_settings
|
|
10
10
|
from django.test.utils import CaptureQueriesContext
|
|
11
|
-
from django.urls import reverse
|
|
11
|
+
from django.urls import NoReverseMatch, reverse
|
|
12
12
|
from django.templatetags.static import static
|
|
13
13
|
from urllib.parse import quote
|
|
14
14
|
from django.contrib.auth import get_user_model
|
|
@@ -45,6 +45,7 @@ from pages.screenshot_specs import (
|
|
|
45
45
|
ScreenshotUnavailable,
|
|
46
46
|
registry,
|
|
47
47
|
)
|
|
48
|
+
from pages.context_processors import nav_links
|
|
48
49
|
from django.apps import apps as django_apps
|
|
49
50
|
from core import mailer
|
|
50
51
|
from core.admin import ProfileAdminMixin
|
|
@@ -96,7 +97,7 @@ from nodes.models import (
|
|
|
96
97
|
NodeFeature,
|
|
97
98
|
NodeFeatureAssignment,
|
|
98
99
|
)
|
|
99
|
-
|
|
100
|
+
from django.contrib.auth.models import AnonymousUser
|
|
100
101
|
|
|
101
102
|
class LoginViewTests(TestCase):
|
|
102
103
|
def setUp(self):
|
|
@@ -684,6 +685,22 @@ class AdminDashboardAppListTests(TestCase):
|
|
|
684
685
|
resp = self.client.get(reverse("admin:index"))
|
|
685
686
|
self.assertContains(resp, "5. Horologia MODELS")
|
|
686
687
|
|
|
688
|
+
def test_dashboard_handles_missing_last_net_message_url(self):
|
|
689
|
+
from pages.templatetags import admin_extras
|
|
690
|
+
|
|
691
|
+
real_reverse = admin_extras.reverse
|
|
692
|
+
|
|
693
|
+
def fake_reverse(name, *args, **kwargs):
|
|
694
|
+
if name == "last-net-message":
|
|
695
|
+
raise NoReverseMatch("missing")
|
|
696
|
+
return real_reverse(name, *args, **kwargs)
|
|
697
|
+
|
|
698
|
+
with patch("pages.templatetags.admin_extras.reverse", side_effect=fake_reverse):
|
|
699
|
+
resp = self.client.get(reverse("admin:index"))
|
|
700
|
+
|
|
701
|
+
self.assertEqual(resp.status_code, 200)
|
|
702
|
+
self.assertNotIn(b"last-net-message", resp.content)
|
|
703
|
+
|
|
687
704
|
|
|
688
705
|
class AdminSidebarTests(TestCase):
|
|
689
706
|
def setUp(self):
|
|
@@ -1520,6 +1537,74 @@ class ConstellationNavTests(TestCase):
|
|
|
1520
1537
|
resp = self.client.get(reverse("pages:index"))
|
|
1521
1538
|
self.assertContains(resp, 'href="/ocpp/"')
|
|
1522
1539
|
|
|
1540
|
+
|
|
1541
|
+
class ReleaseModuleNavTests(TestCase):
|
|
1542
|
+
def setUp(self):
|
|
1543
|
+
self.client = Client()
|
|
1544
|
+
self.user_model = get_user_model()
|
|
1545
|
+
role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
1546
|
+
Node.objects.update_or_create(
|
|
1547
|
+
mac_address=Node.get_current_mac(),
|
|
1548
|
+
defaults={
|
|
1549
|
+
"hostname": "localhost",
|
|
1550
|
+
"address": "127.0.0.1",
|
|
1551
|
+
"role": role,
|
|
1552
|
+
},
|
|
1553
|
+
)
|
|
1554
|
+
Site.objects.update_or_create(
|
|
1555
|
+
id=1, defaults={"domain": "testserver", "name": "Terminal"}
|
|
1556
|
+
)
|
|
1557
|
+
application, _ = Application.objects.get_or_create(name="core")
|
|
1558
|
+
module, _ = Module.objects.get_or_create(
|
|
1559
|
+
node_role=role,
|
|
1560
|
+
application=application,
|
|
1561
|
+
path="/release/",
|
|
1562
|
+
defaults={"menu": "Release", "is_default": False},
|
|
1563
|
+
)
|
|
1564
|
+
module_updates = []
|
|
1565
|
+
if module.menu != "Release":
|
|
1566
|
+
module.menu = "Release"
|
|
1567
|
+
module_updates.append("menu")
|
|
1568
|
+
if getattr(module, "is_deleted", False):
|
|
1569
|
+
module.is_deleted = False
|
|
1570
|
+
module_updates.append("is_deleted")
|
|
1571
|
+
if module_updates:
|
|
1572
|
+
module.save(update_fields=module_updates)
|
|
1573
|
+
Landing.objects.update_or_create(
|
|
1574
|
+
module=module,
|
|
1575
|
+
path="/release/",
|
|
1576
|
+
defaults={
|
|
1577
|
+
"label": "Package Releases",
|
|
1578
|
+
"enabled": True,
|
|
1579
|
+
"description": "",
|
|
1580
|
+
},
|
|
1581
|
+
)
|
|
1582
|
+
self.release_group, _ = SecurityGroup.objects.get_or_create(
|
|
1583
|
+
name="Release Managers"
|
|
1584
|
+
)
|
|
1585
|
+
|
|
1586
|
+
def test_release_module_hidden_for_anonymous(self):
|
|
1587
|
+
response = self.client.get(reverse("pages:index"))
|
|
1588
|
+
self.assertNotContains(response, 'badge rounded-pill text-bg-secondary">RELEASE')
|
|
1589
|
+
|
|
1590
|
+
def test_release_module_visible_to_release_manager(self):
|
|
1591
|
+
user = self.user_model.objects.create_user(
|
|
1592
|
+
"release-admin", password="test", is_staff=True
|
|
1593
|
+
)
|
|
1594
|
+
user.groups.add(self.release_group)
|
|
1595
|
+
self.client.force_login(user)
|
|
1596
|
+
response = self.client.get(reverse("pages:index"))
|
|
1597
|
+
self.assertContains(response, 'badge rounded-pill text-bg-secondary">RELEASE')
|
|
1598
|
+
|
|
1599
|
+
def test_release_module_hidden_for_non_member_staff(self):
|
|
1600
|
+
user = self.user_model.objects.create_user(
|
|
1601
|
+
"staff-user", password="test", is_staff=True
|
|
1602
|
+
)
|
|
1603
|
+
self.client.force_login(user)
|
|
1604
|
+
response = self.client.get(reverse("pages:index"))
|
|
1605
|
+
self.assertNotContains(response, 'badge rounded-pill text-bg-secondary">RELEASE')
|
|
1606
|
+
|
|
1607
|
+
|
|
1523
1608
|
class ControlNavTests(TestCase):
|
|
1524
1609
|
def setUp(self):
|
|
1525
1610
|
self.client = Client()
|
|
@@ -1793,6 +1878,83 @@ class StaffNavVisibilityTests(TestCase):
|
|
|
1793
1878
|
self.assertContains(resp, 'href="/ocpp/"')
|
|
1794
1879
|
|
|
1795
1880
|
|
|
1881
|
+
class ModuleAdminReloadActionTests(TestCase):
|
|
1882
|
+
def setUp(self):
|
|
1883
|
+
self.client = Client()
|
|
1884
|
+
User = get_user_model()
|
|
1885
|
+
self.superuser = User.objects.create_superuser(
|
|
1886
|
+
username="admin",
|
|
1887
|
+
email="admin@example.com",
|
|
1888
|
+
password="pw",
|
|
1889
|
+
)
|
|
1890
|
+
self.client.force_login(self.superuser)
|
|
1891
|
+
self.role, _ = NodeRole.objects.get_or_create(name="Constellation")
|
|
1892
|
+
Application.objects.get_or_create(name="ocpp")
|
|
1893
|
+
Application.objects.get_or_create(name="awg")
|
|
1894
|
+
Site.objects.update_or_create(
|
|
1895
|
+
id=1, defaults={"domain": "testserver", "name": ""}
|
|
1896
|
+
)
|
|
1897
|
+
|
|
1898
|
+
def _post_reload(self):
|
|
1899
|
+
changelist_url = reverse("admin:pages_module_changelist")
|
|
1900
|
+
self.client.get(changelist_url)
|
|
1901
|
+
csrf_cookie = self.client.cookies.get("csrftoken")
|
|
1902
|
+
token = csrf_cookie.value if csrf_cookie else ""
|
|
1903
|
+
return self.client.post(
|
|
1904
|
+
reverse("admin:pages_module_reload_default_modules"),
|
|
1905
|
+
{"csrfmiddlewaretoken": token},
|
|
1906
|
+
follow=True,
|
|
1907
|
+
)
|
|
1908
|
+
|
|
1909
|
+
def test_reload_restores_missing_modules_and_landings(self):
|
|
1910
|
+
Module.objects.filter(node_role=self.role).delete()
|
|
1911
|
+
Landing.objects.filter(module__node_role=self.role).delete()
|
|
1912
|
+
|
|
1913
|
+
response = self._post_reload()
|
|
1914
|
+
self.assertEqual(response.status_code, 200)
|
|
1915
|
+
|
|
1916
|
+
chargers = Module.objects.get(node_role=self.role, path="/ocpp/")
|
|
1917
|
+
calculators = Module.objects.get(node_role=self.role, path="/awg/")
|
|
1918
|
+
|
|
1919
|
+
self.assertEqual(chargers.menu, "Chargers")
|
|
1920
|
+
self.assertEqual(calculators.menu, "")
|
|
1921
|
+
self.assertFalse(getattr(chargers, "is_deleted", False))
|
|
1922
|
+
self.assertFalse(getattr(calculators, "is_deleted", False))
|
|
1923
|
+
|
|
1924
|
+
charger_landings = set(
|
|
1925
|
+
Landing.objects.filter(module=chargers).values_list("path", flat=True)
|
|
1926
|
+
)
|
|
1927
|
+
self.assertSetEqual(
|
|
1928
|
+
charger_landings,
|
|
1929
|
+
{"/ocpp/", "/ocpp/simulator/", "/ocpp/rfid/"},
|
|
1930
|
+
)
|
|
1931
|
+
|
|
1932
|
+
calculator_landings = set(
|
|
1933
|
+
Landing.objects.filter(module=calculators).values_list(
|
|
1934
|
+
"path", flat=True
|
|
1935
|
+
)
|
|
1936
|
+
)
|
|
1937
|
+
self.assertSetEqual(
|
|
1938
|
+
calculator_landings,
|
|
1939
|
+
{"/awg/", "/awg/energy-tariff/"},
|
|
1940
|
+
)
|
|
1941
|
+
|
|
1942
|
+
def test_reload_is_idempotent(self):
|
|
1943
|
+
self._post_reload()
|
|
1944
|
+
module_count = Module.objects.filter(node_role=self.role).count()
|
|
1945
|
+
landing_count = Landing.objects.filter(module__node_role=self.role).count()
|
|
1946
|
+
|
|
1947
|
+
self._post_reload()
|
|
1948
|
+
|
|
1949
|
+
self.assertEqual(
|
|
1950
|
+
Module.objects.filter(node_role=self.role).count(), module_count
|
|
1951
|
+
)
|
|
1952
|
+
self.assertEqual(
|
|
1953
|
+
Landing.objects.filter(module__node_role=self.role).count(),
|
|
1954
|
+
landing_count,
|
|
1955
|
+
)
|
|
1956
|
+
|
|
1957
|
+
|
|
1796
1958
|
class ApplicationModelTests(TestCase):
|
|
1797
1959
|
def test_path_defaults_to_slugified_name(self):
|
|
1798
1960
|
role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
@@ -2422,6 +2584,33 @@ class FavoriteTests(TestCase):
|
|
|
2422
2584
|
resp, '<div class="todo-details">More info</div>', html=True
|
|
2423
2585
|
)
|
|
2424
2586
|
|
|
2587
|
+
def test_dashboard_hides_completed_todos(self):
|
|
2588
|
+
todo = Todo.objects.create(request="Completed task")
|
|
2589
|
+
Todo.objects.filter(pk=todo.pk).update(done_on=timezone.now())
|
|
2590
|
+
|
|
2591
|
+
resp = self.client.get(reverse("admin:index"))
|
|
2592
|
+
|
|
2593
|
+
self.assertNotContains(resp, todo.request)
|
|
2594
|
+
self.assertNotContains(resp, "Completed")
|
|
2595
|
+
|
|
2596
|
+
def test_dashboard_shows_todos_when_node_unknown(self):
|
|
2597
|
+
Todo.objects.create(request="Check fallback")
|
|
2598
|
+
from nodes.models import Node
|
|
2599
|
+
|
|
2600
|
+
Node.objects.all().delete()
|
|
2601
|
+
|
|
2602
|
+
resp = self.client.get(reverse("admin:index"))
|
|
2603
|
+
self.assertContains(resp, "Release manager tasks")
|
|
2604
|
+
self.assertContains(resp, "Check fallback")
|
|
2605
|
+
|
|
2606
|
+
def test_dashboard_shows_todos_without_release_manager_profile(self):
|
|
2607
|
+
Todo.objects.create(request="Unrestricted task")
|
|
2608
|
+
ReleaseManager.objects.filter(user=self.user).delete()
|
|
2609
|
+
|
|
2610
|
+
resp = self.client.get(reverse("admin:index"))
|
|
2611
|
+
self.assertContains(resp, "Release manager tasks")
|
|
2612
|
+
self.assertContains(resp, "Unrestricted task")
|
|
2613
|
+
|
|
2425
2614
|
def test_dashboard_excludes_todo_changelist_link(self):
|
|
2426
2615
|
ct = ContentType.objects.get_for_model(Todo)
|
|
2427
2616
|
Favorite.objects.create(user=self.user, content_type=ct)
|
|
@@ -2435,7 +2624,7 @@ class FavoriteTests(TestCase):
|
|
|
2435
2624
|
changelist = reverse("admin:core_todo_changelist")
|
|
2436
2625
|
self.assertNotContains(resp, f'href="{changelist}"')
|
|
2437
2626
|
|
|
2438
|
-
def
|
|
2627
|
+
def test_dashboard_shows_todos_for_admin_without_release_manager(self):
|
|
2439
2628
|
todo = Todo.objects.create(request="Only Release Manager")
|
|
2440
2629
|
User = get_user_model()
|
|
2441
2630
|
other_user = User.objects.create_superuser(
|
|
@@ -2443,10 +2632,10 @@ class FavoriteTests(TestCase):
|
|
|
2443
2632
|
)
|
|
2444
2633
|
self.client.force_login(other_user)
|
|
2445
2634
|
resp = self.client.get(reverse("admin:index"))
|
|
2446
|
-
self.
|
|
2447
|
-
self.
|
|
2635
|
+
self.assertContains(resp, "Release manager tasks")
|
|
2636
|
+
self.assertContains(resp, todo.request)
|
|
2448
2637
|
|
|
2449
|
-
def
|
|
2638
|
+
def test_dashboard_shows_todos_for_non_terminal_node(self):
|
|
2450
2639
|
todo = Todo.objects.create(request="Terminal Tasks")
|
|
2451
2640
|
from nodes.models import NodeRole
|
|
2452
2641
|
|
|
@@ -2454,8 +2643,8 @@ class FavoriteTests(TestCase):
|
|
|
2454
2643
|
self.node.role = control_role
|
|
2455
2644
|
self.node.save(update_fields=["role"])
|
|
2456
2645
|
resp = self.client.get(reverse("admin:index"))
|
|
2457
|
-
self.
|
|
2458
|
-
self.
|
|
2646
|
+
self.assertContains(resp, "Release manager tasks")
|
|
2647
|
+
self.assertContains(resp, todo.request)
|
|
2459
2648
|
|
|
2460
2649
|
def test_dashboard_shows_todos_for_delegate_release_manager(self):
|
|
2461
2650
|
todo = Todo.objects.create(request="Delegate Task")
|
|
@@ -2716,6 +2905,7 @@ class DatasetteTests(TestCase):
|
|
|
2716
2905
|
|
|
2717
2906
|
class UserStorySubmissionTests(TestCase):
|
|
2718
2907
|
def setUp(self):
|
|
2908
|
+
cache.clear()
|
|
2719
2909
|
self.client = Client()
|
|
2720
2910
|
self.url = reverse("pages:user-story-submit")
|
|
2721
2911
|
User = get_user_model()
|
|
@@ -2731,6 +2921,8 @@ class UserStorySubmissionTests(TestCase):
|
|
|
2731
2921
|
"path": "/wizard/step-1/",
|
|
2732
2922
|
"take_screenshot": "1",
|
|
2733
2923
|
},
|
|
2924
|
+
HTTP_REFERER="https://example.test/wizard/step-1/",
|
|
2925
|
+
HTTP_USER_AGENT="FeedbackBot/1.0",
|
|
2734
2926
|
)
|
|
2735
2927
|
self.assertEqual(response.status_code, 200)
|
|
2736
2928
|
self.assertEqual(response.json(), {"success": True})
|
|
@@ -2742,6 +2934,10 @@ class UserStorySubmissionTests(TestCase):
|
|
|
2742
2934
|
self.assertEqual(story.owner, self.user)
|
|
2743
2935
|
self.assertTrue(story.is_user_data)
|
|
2744
2936
|
self.assertTrue(story.take_screenshot)
|
|
2937
|
+
self.assertEqual(story.status, UserStory.Status.OPEN)
|
|
2938
|
+
self.assertEqual(story.referer, "https://example.test/wizard/step-1/")
|
|
2939
|
+
self.assertEqual(story.user_agent, "FeedbackBot/1.0")
|
|
2940
|
+
self.assertEqual(story.ip_address, "127.0.0.1")
|
|
2745
2941
|
|
|
2746
2942
|
def test_anonymous_submission_uses_provided_name(self):
|
|
2747
2943
|
response = self.client.post(
|
|
@@ -2762,6 +2958,7 @@ class UserStorySubmissionTests(TestCase):
|
|
|
2762
2958
|
self.assertIsNone(story.owner)
|
|
2763
2959
|
self.assertEqual(story.comments, "It was fine.")
|
|
2764
2960
|
self.assertTrue(story.take_screenshot)
|
|
2961
|
+
self.assertEqual(story.status, UserStory.Status.OPEN)
|
|
2765
2962
|
|
|
2766
2963
|
def test_invalid_rating_returns_errors(self):
|
|
2767
2964
|
response = self.client.post(
|
|
@@ -2794,6 +2991,7 @@ class UserStorySubmissionTests(TestCase):
|
|
|
2794
2991
|
self.assertIsNone(story.user)
|
|
2795
2992
|
self.assertIsNone(story.owner)
|
|
2796
2993
|
self.assertTrue(story.take_screenshot)
|
|
2994
|
+
self.assertEqual(story.status, UserStory.Status.OPEN)
|
|
2797
2995
|
|
|
2798
2996
|
def test_submission_without_screenshot_request(self):
|
|
2799
2997
|
response = self.client.post(
|
|
@@ -2809,12 +3007,32 @@ class UserStorySubmissionTests(TestCase):
|
|
|
2809
3007
|
self.assertFalse(story.take_screenshot)
|
|
2810
3008
|
self.assertIsNone(story.owner)
|
|
2811
3009
|
|
|
3010
|
+
def test_rate_limit_blocks_repeated_submissions(self):
|
|
3011
|
+
payload = {
|
|
3012
|
+
"rating": 4,
|
|
3013
|
+
"comments": "Pretty good",
|
|
3014
|
+
"path": "/feedback/",
|
|
3015
|
+
"take_screenshot": "1",
|
|
3016
|
+
}
|
|
3017
|
+
first = self.client.post(self.url, payload)
|
|
3018
|
+
self.assertEqual(first.status_code, 200)
|
|
3019
|
+
second = self.client.post(self.url, payload)
|
|
3020
|
+
self.assertEqual(second.status_code, 429)
|
|
3021
|
+
data = second.json()
|
|
3022
|
+
self.assertFalse(data["success"])
|
|
3023
|
+
self.assertIn("__all__", data.get("errors", {}))
|
|
3024
|
+
self.assertIn("5", data["errors"]["__all__"][0])
|
|
3025
|
+
|
|
2812
3026
|
|
|
2813
3027
|
class UserStoryIssueAutomationTests(TestCase):
|
|
2814
3028
|
def setUp(self):
|
|
2815
3029
|
self.lock_dir = Path(settings.BASE_DIR) / "locks"
|
|
2816
3030
|
self.lock_dir.mkdir(parents=True, exist_ok=True)
|
|
2817
3031
|
self.lock_file = self.lock_dir / "celery.lck"
|
|
3032
|
+
User = get_user_model()
|
|
3033
|
+
self.user = User.objects.create_user(
|
|
3034
|
+
username="feedback_user", password="pwd"
|
|
3035
|
+
)
|
|
2818
3036
|
|
|
2819
3037
|
def tearDown(self):
|
|
2820
3038
|
self.lock_file.unlink(missing_ok=True)
|
|
@@ -2828,10 +3046,24 @@ class UserStoryIssueAutomationTests(TestCase):
|
|
|
2828
3046
|
rating=2,
|
|
2829
3047
|
comments="Needs work",
|
|
2830
3048
|
take_screenshot=False,
|
|
3049
|
+
user=self.user,
|
|
2831
3050
|
)
|
|
2832
3051
|
|
|
2833
3052
|
mock_delay.assert_called_once_with(story.pk)
|
|
2834
3053
|
|
|
3054
|
+
def test_low_rating_story_without_user_does_not_enqueue_issue(self):
|
|
3055
|
+
self.lock_file.write_text("")
|
|
3056
|
+
|
|
3057
|
+
with patch("pages.models.create_user_story_github_issue.delay") as mock_delay:
|
|
3058
|
+
UserStory.objects.create(
|
|
3059
|
+
path="/feedback/",
|
|
3060
|
+
rating=2,
|
|
3061
|
+
comments="Needs work",
|
|
3062
|
+
take_screenshot=False,
|
|
3063
|
+
)
|
|
3064
|
+
|
|
3065
|
+
mock_delay.assert_not_called()
|
|
3066
|
+
|
|
2835
3067
|
def test_five_star_story_does_not_enqueue_issue(self):
|
|
2836
3068
|
self.lock_file.write_text("")
|
|
2837
3069
|
|
|
@@ -2854,6 +3086,7 @@ class UserStoryIssueAutomationTests(TestCase):
|
|
|
2854
3086
|
rating=1,
|
|
2855
3087
|
comments="Not good",
|
|
2856
3088
|
take_screenshot=False,
|
|
3089
|
+
user=self.user,
|
|
2857
3090
|
)
|
|
2858
3091
|
|
|
2859
3092
|
mock_delay.assert_not_called()
|
pages/urls.py
CHANGED
|
@@ -8,6 +8,7 @@ urlpatterns = [
|
|
|
8
8
|
path("", views.index, name="index"),
|
|
9
9
|
path("readme/", views.readme, name="readme"),
|
|
10
10
|
path("sitemap.xml", views.sitemap, name="pages-sitemap"),
|
|
11
|
+
path("release/", views.release_admin_redirect, name="release-admin"),
|
|
11
12
|
path("client-report/", views.client_report, name="client-report"),
|
|
12
13
|
path("release-checklist", views.release_checklist, name="release-checklist"),
|
|
13
14
|
path("login/", views.login_view, name="login"),
|
pages/views.py
CHANGED
|
@@ -19,12 +19,14 @@ from django.contrib.auth.tokens import default_token_generator
|
|
|
19
19
|
from django.contrib.auth.views import LoginView
|
|
20
20
|
from django import forms
|
|
21
21
|
from django.apps import apps as django_apps
|
|
22
|
+
from utils.decorators import security_group_required
|
|
22
23
|
from utils.sites import get_site
|
|
23
24
|
from django.http import Http404, HttpResponse, JsonResponse
|
|
24
25
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
25
26
|
from nodes.models import Node
|
|
27
|
+
from django.template import loader
|
|
26
28
|
from django.template.response import TemplateResponse
|
|
27
|
-
from django.test import RequestFactory
|
|
29
|
+
from django.test import RequestFactory, signals as test_signals
|
|
28
30
|
from django.urls import NoReverseMatch, reverse
|
|
29
31
|
from django.utils import timezone
|
|
30
32
|
from django.utils.encoding import force_bytes, force_str
|
|
@@ -105,6 +107,18 @@ def _get_registered_models(app_label: str):
|
|
|
105
107
|
return sorted(registered, key=lambda model: str(model._meta.verbose_name))
|
|
106
108
|
|
|
107
109
|
|
|
110
|
+
def _get_client_ip(request) -> str:
|
|
111
|
+
"""Return the client IP from the request headers."""
|
|
112
|
+
|
|
113
|
+
forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR", "")
|
|
114
|
+
if forwarded_for:
|
|
115
|
+
for value in forwarded_for.split(","):
|
|
116
|
+
candidate = value.strip()
|
|
117
|
+
if candidate:
|
|
118
|
+
return candidate
|
|
119
|
+
return request.META.get("REMOTE_ADDR", "")
|
|
120
|
+
|
|
121
|
+
|
|
108
122
|
def _filter_models_for_request(models, request):
|
|
109
123
|
"""Filter ``models`` to only those viewable by ``request.user``."""
|
|
110
124
|
|
|
@@ -408,7 +422,21 @@ def admin_model_graph(request, app_label: str):
|
|
|
408
422
|
}
|
|
409
423
|
)
|
|
410
424
|
|
|
411
|
-
|
|
425
|
+
template_name = "admin/model_graph.html"
|
|
426
|
+
response = render(request, template_name, context)
|
|
427
|
+
if getattr(response, "context", None) is None:
|
|
428
|
+
response.context = context
|
|
429
|
+
if test_signals.template_rendered.receivers:
|
|
430
|
+
template = loader.get_template(template_name)
|
|
431
|
+
signal_context = context
|
|
432
|
+
if request is not None and "request" not in signal_context:
|
|
433
|
+
signal_context = {**context, "request": request}
|
|
434
|
+
test_signals.template_rendered.send(
|
|
435
|
+
sender=template.__class__,
|
|
436
|
+
template=template,
|
|
437
|
+
context=signal_context,
|
|
438
|
+
)
|
|
439
|
+
return response
|
|
412
440
|
|
|
413
441
|
|
|
414
442
|
def _render_readme(request, role):
|
|
@@ -523,6 +551,12 @@ def sitemap(request):
|
|
|
523
551
|
return HttpResponse("\n".join(lines), content_type="application/xml")
|
|
524
552
|
|
|
525
553
|
|
|
554
|
+
@landing("Package Releases")
|
|
555
|
+
@security_group_required("Release Managers")
|
|
556
|
+
def release_admin_redirect(request):
|
|
557
|
+
return redirect("admin:core_packagerelease_changelist")
|
|
558
|
+
|
|
559
|
+
|
|
526
560
|
def release_checklist(request):
|
|
527
561
|
file_path = Path(settings.BASE_DIR) / "releases" / "release-checklist.md"
|
|
528
562
|
if not file_path.exists():
|
|
@@ -554,7 +588,7 @@ class CustomLoginView(LoginView):
|
|
|
554
588
|
return super().dispatch(request, *args, **kwargs)
|
|
555
589
|
|
|
556
590
|
def get_context_data(self, **kwargs):
|
|
557
|
-
context = super(
|
|
591
|
+
context = super().get_context_data(**kwargs)
|
|
558
592
|
current_site = get_site(self.request)
|
|
559
593
|
redirect_target = self.request.GET.get(self.redirect_field_name)
|
|
560
594
|
restricted_notice = None
|
|
@@ -569,11 +603,13 @@ class CustomLoginView(LoginView):
|
|
|
569
603
|
restricted_notice = _(
|
|
570
604
|
"This page is reserved for members only. Please log in to continue."
|
|
571
605
|
)
|
|
606
|
+
redirect_value = context.get(self.redirect_field_name) or self.get_success_url()
|
|
607
|
+
context[self.redirect_field_name] = redirect_value
|
|
608
|
+
context["next"] = redirect_value
|
|
572
609
|
context.update(
|
|
573
610
|
{
|
|
574
611
|
"site": current_site,
|
|
575
612
|
"site_name": getattr(current_site, "name", ""),
|
|
576
|
-
"next": self.get_success_url(),
|
|
577
613
|
"can_request_invite": mailer.can_send_email(),
|
|
578
614
|
"restricted_notice": restricted_notice,
|
|
579
615
|
}
|
|
@@ -1093,6 +1129,24 @@ def client_report(request):
|
|
|
1093
1129
|
|
|
1094
1130
|
@require_POST
|
|
1095
1131
|
def submit_user_story(request):
|
|
1132
|
+
throttle_seconds = getattr(settings, "USER_STORY_THROTTLE_SECONDS", 300)
|
|
1133
|
+
client_ip = _get_client_ip(request)
|
|
1134
|
+
cache_key = None
|
|
1135
|
+
|
|
1136
|
+
if throttle_seconds:
|
|
1137
|
+
cache_key = f"user-story:ip:{client_ip or 'unknown'}"
|
|
1138
|
+
if not cache.add(cache_key, timezone.now(), throttle_seconds):
|
|
1139
|
+
minutes = throttle_seconds // 60
|
|
1140
|
+
if throttle_seconds % 60:
|
|
1141
|
+
minutes += 1
|
|
1142
|
+
error_message = _(
|
|
1143
|
+
"You can only submit feedback once every %(minutes)s minutes."
|
|
1144
|
+
) % {"minutes": minutes or 1}
|
|
1145
|
+
return JsonResponse(
|
|
1146
|
+
{"success": False, "errors": {"__all__": [error_message]}},
|
|
1147
|
+
status=429,
|
|
1148
|
+
)
|
|
1149
|
+
|
|
1096
1150
|
data = request.POST.copy()
|
|
1097
1151
|
if request.user.is_authenticated and not data.get("name"):
|
|
1098
1152
|
data["name"] = request.user.get_username()[:40]
|
|
@@ -1113,6 +1167,9 @@ def submit_user_story(request):
|
|
|
1113
1167
|
if not story.name:
|
|
1114
1168
|
story.name = str(_("Anonymous"))[:40]
|
|
1115
1169
|
story.path = (story.path or request.get_full_path())[:500]
|
|
1170
|
+
story.referer = request.META.get("HTTP_REFERER", "")
|
|
1171
|
+
story.user_agent = request.META.get("HTTP_USER_AGENT", "")
|
|
1172
|
+
story.ip_address = client_ip or None
|
|
1116
1173
|
story.is_user_data = True
|
|
1117
1174
|
story.save()
|
|
1118
1175
|
return JsonResponse({"success": True})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|