arthexis 0.1.15__py3-none-any.whl → 0.1.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of arthexis might be problematic. Click here for more details.
- {arthexis-0.1.15.dist-info → arthexis-0.1.16.dist-info}/METADATA +1 -2
- {arthexis-0.1.15.dist-info → arthexis-0.1.16.dist-info}/RECORD +36 -35
- config/urls.py +5 -0
- core/admin.py +174 -7
- core/admindocs.py +44 -3
- core/apps.py +1 -1
- core/backends.py +44 -8
- core/github_issues.py +12 -7
- core/mailer.py +9 -5
- core/models.py +64 -23
- core/release.py +52 -0
- core/system.py +208 -1
- core/tasks.py +5 -1
- core/test_system_info.py +16 -0
- core/tests.py +207 -0
- core/views.py +221 -33
- 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/consumers.py +38 -0
- ocpp/models.py +19 -4
- ocpp/tasks.py +156 -2
- ocpp/test_rfid.py +44 -2
- ocpp/tests.py +111 -1
- pages/admin.py +126 -4
- pages/context_processors.py +20 -1
- pages/models.py +3 -1
- pages/module_defaults.py +156 -0
- pages/tests.py +215 -7
- pages/urls.py +1 -0
- pages/views.py +61 -4
- {arthexis-0.1.15.dist-info → arthexis-0.1.16.dist-info}/WHEEL +0 -0
- {arthexis-0.1.15.dist-info → arthexis-0.1.16.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.15.dist-info → arthexis-0.1.16.dist-info}/top_level.txt +0 -0
pages/tests.py
CHANGED
|
@@ -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):
|
|
@@ -1520,6 +1521,74 @@ class ConstellationNavTests(TestCase):
|
|
|
1520
1521
|
resp = self.client.get(reverse("pages:index"))
|
|
1521
1522
|
self.assertContains(resp, 'href="/ocpp/"')
|
|
1522
1523
|
|
|
1524
|
+
|
|
1525
|
+
class ReleaseModuleNavTests(TestCase):
|
|
1526
|
+
def setUp(self):
|
|
1527
|
+
self.client = Client()
|
|
1528
|
+
self.user_model = get_user_model()
|
|
1529
|
+
role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
1530
|
+
Node.objects.update_or_create(
|
|
1531
|
+
mac_address=Node.get_current_mac(),
|
|
1532
|
+
defaults={
|
|
1533
|
+
"hostname": "localhost",
|
|
1534
|
+
"address": "127.0.0.1",
|
|
1535
|
+
"role": role,
|
|
1536
|
+
},
|
|
1537
|
+
)
|
|
1538
|
+
Site.objects.update_or_create(
|
|
1539
|
+
id=1, defaults={"domain": "testserver", "name": "Terminal"}
|
|
1540
|
+
)
|
|
1541
|
+
application, _ = Application.objects.get_or_create(name="core")
|
|
1542
|
+
module, _ = Module.objects.get_or_create(
|
|
1543
|
+
node_role=role,
|
|
1544
|
+
application=application,
|
|
1545
|
+
path="/release/",
|
|
1546
|
+
defaults={"menu": "Release", "is_default": False},
|
|
1547
|
+
)
|
|
1548
|
+
module_updates = []
|
|
1549
|
+
if module.menu != "Release":
|
|
1550
|
+
module.menu = "Release"
|
|
1551
|
+
module_updates.append("menu")
|
|
1552
|
+
if getattr(module, "is_deleted", False):
|
|
1553
|
+
module.is_deleted = False
|
|
1554
|
+
module_updates.append("is_deleted")
|
|
1555
|
+
if module_updates:
|
|
1556
|
+
module.save(update_fields=module_updates)
|
|
1557
|
+
Landing.objects.update_or_create(
|
|
1558
|
+
module=module,
|
|
1559
|
+
path="/release/",
|
|
1560
|
+
defaults={
|
|
1561
|
+
"label": "Package Releases",
|
|
1562
|
+
"enabled": True,
|
|
1563
|
+
"description": "",
|
|
1564
|
+
},
|
|
1565
|
+
)
|
|
1566
|
+
self.release_group, _ = SecurityGroup.objects.get_or_create(
|
|
1567
|
+
name="Release Managers"
|
|
1568
|
+
)
|
|
1569
|
+
|
|
1570
|
+
def test_release_module_hidden_for_anonymous(self):
|
|
1571
|
+
response = self.client.get(reverse("pages:index"))
|
|
1572
|
+
self.assertNotContains(response, 'badge rounded-pill text-bg-secondary">RELEASE')
|
|
1573
|
+
|
|
1574
|
+
def test_release_module_visible_to_release_manager(self):
|
|
1575
|
+
user = self.user_model.objects.create_user(
|
|
1576
|
+
"release-admin", password="test", is_staff=True
|
|
1577
|
+
)
|
|
1578
|
+
user.groups.add(self.release_group)
|
|
1579
|
+
self.client.force_login(user)
|
|
1580
|
+
response = self.client.get(reverse("pages:index"))
|
|
1581
|
+
self.assertContains(response, 'badge rounded-pill text-bg-secondary">RELEASE')
|
|
1582
|
+
|
|
1583
|
+
def test_release_module_hidden_for_non_member_staff(self):
|
|
1584
|
+
user = self.user_model.objects.create_user(
|
|
1585
|
+
"staff-user", password="test", is_staff=True
|
|
1586
|
+
)
|
|
1587
|
+
self.client.force_login(user)
|
|
1588
|
+
response = self.client.get(reverse("pages:index"))
|
|
1589
|
+
self.assertNotContains(response, 'badge rounded-pill text-bg-secondary">RELEASE')
|
|
1590
|
+
|
|
1591
|
+
|
|
1523
1592
|
class ControlNavTests(TestCase):
|
|
1524
1593
|
def setUp(self):
|
|
1525
1594
|
self.client = Client()
|
|
@@ -1793,6 +1862,83 @@ class StaffNavVisibilityTests(TestCase):
|
|
|
1793
1862
|
self.assertContains(resp, 'href="/ocpp/"')
|
|
1794
1863
|
|
|
1795
1864
|
|
|
1865
|
+
class ModuleAdminReloadActionTests(TestCase):
|
|
1866
|
+
def setUp(self):
|
|
1867
|
+
self.client = Client()
|
|
1868
|
+
User = get_user_model()
|
|
1869
|
+
self.superuser = User.objects.create_superuser(
|
|
1870
|
+
username="admin",
|
|
1871
|
+
email="admin@example.com",
|
|
1872
|
+
password="pw",
|
|
1873
|
+
)
|
|
1874
|
+
self.client.force_login(self.superuser)
|
|
1875
|
+
self.role, _ = NodeRole.objects.get_or_create(name="Constellation")
|
|
1876
|
+
Application.objects.get_or_create(name="ocpp")
|
|
1877
|
+
Application.objects.get_or_create(name="awg")
|
|
1878
|
+
Site.objects.update_or_create(
|
|
1879
|
+
id=1, defaults={"domain": "testserver", "name": ""}
|
|
1880
|
+
)
|
|
1881
|
+
|
|
1882
|
+
def _post_reload(self):
|
|
1883
|
+
changelist_url = reverse("admin:pages_module_changelist")
|
|
1884
|
+
self.client.get(changelist_url)
|
|
1885
|
+
csrf_cookie = self.client.cookies.get("csrftoken")
|
|
1886
|
+
token = csrf_cookie.value if csrf_cookie else ""
|
|
1887
|
+
return self.client.post(
|
|
1888
|
+
reverse("admin:pages_module_reload_default_modules"),
|
|
1889
|
+
{"csrfmiddlewaretoken": token},
|
|
1890
|
+
follow=True,
|
|
1891
|
+
)
|
|
1892
|
+
|
|
1893
|
+
def test_reload_restores_missing_modules_and_landings(self):
|
|
1894
|
+
Module.objects.filter(node_role=self.role).delete()
|
|
1895
|
+
Landing.objects.filter(module__node_role=self.role).delete()
|
|
1896
|
+
|
|
1897
|
+
response = self._post_reload()
|
|
1898
|
+
self.assertEqual(response.status_code, 200)
|
|
1899
|
+
|
|
1900
|
+
chargers = Module.objects.get(node_role=self.role, path="/ocpp/")
|
|
1901
|
+
calculators = Module.objects.get(node_role=self.role, path="/awg/")
|
|
1902
|
+
|
|
1903
|
+
self.assertEqual(chargers.menu, "Chargers")
|
|
1904
|
+
self.assertEqual(calculators.menu, "")
|
|
1905
|
+
self.assertFalse(getattr(chargers, "is_deleted", False))
|
|
1906
|
+
self.assertFalse(getattr(calculators, "is_deleted", False))
|
|
1907
|
+
|
|
1908
|
+
charger_landings = set(
|
|
1909
|
+
Landing.objects.filter(module=chargers).values_list("path", flat=True)
|
|
1910
|
+
)
|
|
1911
|
+
self.assertSetEqual(
|
|
1912
|
+
charger_landings,
|
|
1913
|
+
{"/ocpp/", "/ocpp/simulator/", "/ocpp/rfid/"},
|
|
1914
|
+
)
|
|
1915
|
+
|
|
1916
|
+
calculator_landings = set(
|
|
1917
|
+
Landing.objects.filter(module=calculators).values_list(
|
|
1918
|
+
"path", flat=True
|
|
1919
|
+
)
|
|
1920
|
+
)
|
|
1921
|
+
self.assertSetEqual(
|
|
1922
|
+
calculator_landings,
|
|
1923
|
+
{"/awg/", "/awg/energy-tariff/"},
|
|
1924
|
+
)
|
|
1925
|
+
|
|
1926
|
+
def test_reload_is_idempotent(self):
|
|
1927
|
+
self._post_reload()
|
|
1928
|
+
module_count = Module.objects.filter(node_role=self.role).count()
|
|
1929
|
+
landing_count = Landing.objects.filter(module__node_role=self.role).count()
|
|
1930
|
+
|
|
1931
|
+
self._post_reload()
|
|
1932
|
+
|
|
1933
|
+
self.assertEqual(
|
|
1934
|
+
Module.objects.filter(node_role=self.role).count(), module_count
|
|
1935
|
+
)
|
|
1936
|
+
self.assertEqual(
|
|
1937
|
+
Landing.objects.filter(module__node_role=self.role).count(),
|
|
1938
|
+
landing_count,
|
|
1939
|
+
)
|
|
1940
|
+
|
|
1941
|
+
|
|
1796
1942
|
class ApplicationModelTests(TestCase):
|
|
1797
1943
|
def test_path_defaults_to_slugified_name(self):
|
|
1798
1944
|
role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
@@ -2422,6 +2568,24 @@ class FavoriteTests(TestCase):
|
|
|
2422
2568
|
resp, '<div class="todo-details">More info</div>', html=True
|
|
2423
2569
|
)
|
|
2424
2570
|
|
|
2571
|
+
def test_dashboard_shows_todos_when_node_unknown(self):
|
|
2572
|
+
Todo.objects.create(request="Check fallback")
|
|
2573
|
+
from nodes.models import Node
|
|
2574
|
+
|
|
2575
|
+
Node.objects.all().delete()
|
|
2576
|
+
|
|
2577
|
+
resp = self.client.get(reverse("admin:index"))
|
|
2578
|
+
self.assertContains(resp, "Release manager tasks")
|
|
2579
|
+
self.assertContains(resp, "Check fallback")
|
|
2580
|
+
|
|
2581
|
+
def test_dashboard_shows_todos_without_release_manager_profile(self):
|
|
2582
|
+
Todo.objects.create(request="Unrestricted task")
|
|
2583
|
+
ReleaseManager.objects.filter(user=self.user).delete()
|
|
2584
|
+
|
|
2585
|
+
resp = self.client.get(reverse("admin:index"))
|
|
2586
|
+
self.assertContains(resp, "Release manager tasks")
|
|
2587
|
+
self.assertContains(resp, "Unrestricted task")
|
|
2588
|
+
|
|
2425
2589
|
def test_dashboard_excludes_todo_changelist_link(self):
|
|
2426
2590
|
ct = ContentType.objects.get_for_model(Todo)
|
|
2427
2591
|
Favorite.objects.create(user=self.user, content_type=ct)
|
|
@@ -2435,7 +2599,7 @@ class FavoriteTests(TestCase):
|
|
|
2435
2599
|
changelist = reverse("admin:core_todo_changelist")
|
|
2436
2600
|
self.assertNotContains(resp, f'href="{changelist}"')
|
|
2437
2601
|
|
|
2438
|
-
def
|
|
2602
|
+
def test_dashboard_shows_todos_for_admin_without_release_manager(self):
|
|
2439
2603
|
todo = Todo.objects.create(request="Only Release Manager")
|
|
2440
2604
|
User = get_user_model()
|
|
2441
2605
|
other_user = User.objects.create_superuser(
|
|
@@ -2443,10 +2607,10 @@ class FavoriteTests(TestCase):
|
|
|
2443
2607
|
)
|
|
2444
2608
|
self.client.force_login(other_user)
|
|
2445
2609
|
resp = self.client.get(reverse("admin:index"))
|
|
2446
|
-
self.
|
|
2447
|
-
self.
|
|
2610
|
+
self.assertContains(resp, "Release manager tasks")
|
|
2611
|
+
self.assertContains(resp, todo.request)
|
|
2448
2612
|
|
|
2449
|
-
def
|
|
2613
|
+
def test_dashboard_shows_todos_for_non_terminal_node(self):
|
|
2450
2614
|
todo = Todo.objects.create(request="Terminal Tasks")
|
|
2451
2615
|
from nodes.models import NodeRole
|
|
2452
2616
|
|
|
@@ -2454,8 +2618,8 @@ class FavoriteTests(TestCase):
|
|
|
2454
2618
|
self.node.role = control_role
|
|
2455
2619
|
self.node.save(update_fields=["role"])
|
|
2456
2620
|
resp = self.client.get(reverse("admin:index"))
|
|
2457
|
-
self.
|
|
2458
|
-
self.
|
|
2621
|
+
self.assertContains(resp, "Release manager tasks")
|
|
2622
|
+
self.assertContains(resp, todo.request)
|
|
2459
2623
|
|
|
2460
2624
|
def test_dashboard_shows_todos_for_delegate_release_manager(self):
|
|
2461
2625
|
todo = Todo.objects.create(request="Delegate Task")
|
|
@@ -2716,6 +2880,7 @@ class DatasetteTests(TestCase):
|
|
|
2716
2880
|
|
|
2717
2881
|
class UserStorySubmissionTests(TestCase):
|
|
2718
2882
|
def setUp(self):
|
|
2883
|
+
cache.clear()
|
|
2719
2884
|
self.client = Client()
|
|
2720
2885
|
self.url = reverse("pages:user-story-submit")
|
|
2721
2886
|
User = get_user_model()
|
|
@@ -2731,6 +2896,8 @@ class UserStorySubmissionTests(TestCase):
|
|
|
2731
2896
|
"path": "/wizard/step-1/",
|
|
2732
2897
|
"take_screenshot": "1",
|
|
2733
2898
|
},
|
|
2899
|
+
HTTP_REFERER="https://example.test/wizard/step-1/",
|
|
2900
|
+
HTTP_USER_AGENT="FeedbackBot/1.0",
|
|
2734
2901
|
)
|
|
2735
2902
|
self.assertEqual(response.status_code, 200)
|
|
2736
2903
|
self.assertEqual(response.json(), {"success": True})
|
|
@@ -2742,6 +2909,10 @@ class UserStorySubmissionTests(TestCase):
|
|
|
2742
2909
|
self.assertEqual(story.owner, self.user)
|
|
2743
2910
|
self.assertTrue(story.is_user_data)
|
|
2744
2911
|
self.assertTrue(story.take_screenshot)
|
|
2912
|
+
self.assertEqual(story.status, UserStory.Status.OPEN)
|
|
2913
|
+
self.assertEqual(story.referer, "https://example.test/wizard/step-1/")
|
|
2914
|
+
self.assertEqual(story.user_agent, "FeedbackBot/1.0")
|
|
2915
|
+
self.assertEqual(story.ip_address, "127.0.0.1")
|
|
2745
2916
|
|
|
2746
2917
|
def test_anonymous_submission_uses_provided_name(self):
|
|
2747
2918
|
response = self.client.post(
|
|
@@ -2762,6 +2933,7 @@ class UserStorySubmissionTests(TestCase):
|
|
|
2762
2933
|
self.assertIsNone(story.owner)
|
|
2763
2934
|
self.assertEqual(story.comments, "It was fine.")
|
|
2764
2935
|
self.assertTrue(story.take_screenshot)
|
|
2936
|
+
self.assertEqual(story.status, UserStory.Status.OPEN)
|
|
2765
2937
|
|
|
2766
2938
|
def test_invalid_rating_returns_errors(self):
|
|
2767
2939
|
response = self.client.post(
|
|
@@ -2794,6 +2966,7 @@ class UserStorySubmissionTests(TestCase):
|
|
|
2794
2966
|
self.assertIsNone(story.user)
|
|
2795
2967
|
self.assertIsNone(story.owner)
|
|
2796
2968
|
self.assertTrue(story.take_screenshot)
|
|
2969
|
+
self.assertEqual(story.status, UserStory.Status.OPEN)
|
|
2797
2970
|
|
|
2798
2971
|
def test_submission_without_screenshot_request(self):
|
|
2799
2972
|
response = self.client.post(
|
|
@@ -2809,12 +2982,32 @@ class UserStorySubmissionTests(TestCase):
|
|
|
2809
2982
|
self.assertFalse(story.take_screenshot)
|
|
2810
2983
|
self.assertIsNone(story.owner)
|
|
2811
2984
|
|
|
2985
|
+
def test_rate_limit_blocks_repeated_submissions(self):
|
|
2986
|
+
payload = {
|
|
2987
|
+
"rating": 4,
|
|
2988
|
+
"comments": "Pretty good",
|
|
2989
|
+
"path": "/feedback/",
|
|
2990
|
+
"take_screenshot": "1",
|
|
2991
|
+
}
|
|
2992
|
+
first = self.client.post(self.url, payload)
|
|
2993
|
+
self.assertEqual(first.status_code, 200)
|
|
2994
|
+
second = self.client.post(self.url, payload)
|
|
2995
|
+
self.assertEqual(second.status_code, 429)
|
|
2996
|
+
data = second.json()
|
|
2997
|
+
self.assertFalse(data["success"])
|
|
2998
|
+
self.assertIn("__all__", data.get("errors", {}))
|
|
2999
|
+
self.assertIn("5", data["errors"]["__all__"][0])
|
|
3000
|
+
|
|
2812
3001
|
|
|
2813
3002
|
class UserStoryIssueAutomationTests(TestCase):
|
|
2814
3003
|
def setUp(self):
|
|
2815
3004
|
self.lock_dir = Path(settings.BASE_DIR) / "locks"
|
|
2816
3005
|
self.lock_dir.mkdir(parents=True, exist_ok=True)
|
|
2817
3006
|
self.lock_file = self.lock_dir / "celery.lck"
|
|
3007
|
+
User = get_user_model()
|
|
3008
|
+
self.user = User.objects.create_user(
|
|
3009
|
+
username="feedback_user", password="pwd"
|
|
3010
|
+
)
|
|
2818
3011
|
|
|
2819
3012
|
def tearDown(self):
|
|
2820
3013
|
self.lock_file.unlink(missing_ok=True)
|
|
@@ -2828,10 +3021,24 @@ class UserStoryIssueAutomationTests(TestCase):
|
|
|
2828
3021
|
rating=2,
|
|
2829
3022
|
comments="Needs work",
|
|
2830
3023
|
take_screenshot=False,
|
|
3024
|
+
user=self.user,
|
|
2831
3025
|
)
|
|
2832
3026
|
|
|
2833
3027
|
mock_delay.assert_called_once_with(story.pk)
|
|
2834
3028
|
|
|
3029
|
+
def test_low_rating_story_without_user_does_not_enqueue_issue(self):
|
|
3030
|
+
self.lock_file.write_text("")
|
|
3031
|
+
|
|
3032
|
+
with patch("pages.models.create_user_story_github_issue.delay") as mock_delay:
|
|
3033
|
+
UserStory.objects.create(
|
|
3034
|
+
path="/feedback/",
|
|
3035
|
+
rating=2,
|
|
3036
|
+
comments="Needs work",
|
|
3037
|
+
take_screenshot=False,
|
|
3038
|
+
)
|
|
3039
|
+
|
|
3040
|
+
mock_delay.assert_not_called()
|
|
3041
|
+
|
|
2835
3042
|
def test_five_star_story_does_not_enqueue_issue(self):
|
|
2836
3043
|
self.lock_file.write_text("")
|
|
2837
3044
|
|
|
@@ -2854,6 +3061,7 @@ class UserStoryIssueAutomationTests(TestCase):
|
|
|
2854
3061
|
rating=1,
|
|
2855
3062
|
comments="Not good",
|
|
2856
3063
|
take_screenshot=False,
|
|
3064
|
+
user=self.user,
|
|
2857
3065
|
)
|
|
2858
3066
|
|
|
2859
3067
|
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
|