wagtail 7.1.1__py3-none-any.whl → 7.1.3__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.
Files changed (33) hide show
  1. wagtail/__init__.py +1 -1
  2. wagtail/admin/static/wagtailadmin/js/telepath/blocks.js +1 -1
  3. wagtail/admin/static/wagtailadmin/js/userbar.js +1 -1
  4. wagtail/admin/templates/wagtailadmin/generic/form.html +3 -1
  5. wagtail/admin/templates/wagtailadmin/userbar/base.html +6 -3
  6. wagtail/admin/templates/wagtailadmin/userbar/item_admin.html +2 -2
  7. wagtail/admin/templates/wagtailadmin/userbar/item_page_add.html +2 -2
  8. wagtail/admin/templates/wagtailadmin/userbar/item_page_edit.html +2 -2
  9. wagtail/admin/templates/wagtailadmin/userbar/item_page_explore.html +2 -2
  10. wagtail/admin/templatetags/wagtailadmin_tags.py +27 -0
  11. wagtail/admin/tests/pages/test_preview.py +111 -0
  12. wagtail/admin/tests/test_userbar.py +75 -35
  13. wagtail/admin/tests/test_views_generic.py +34 -0
  14. wagtail/admin/userbar.py +6 -1
  15. wagtail/admin/views/generic/preview.py +8 -1
  16. wagtail/admin/views/pages/preview.py +10 -1
  17. wagtail/blocks/struct_block.py +2 -1
  18. wagtail/contrib/settings/tests/shared/test_preview.py +29 -0
  19. wagtail/contrib/settings/views.py +8 -2
  20. wagtail/documents/tests/test_admin_views.py +25 -0
  21. wagtail/documents/views/documents.py +3 -1
  22. wagtail/images/tests/test_admin_views.py +26 -0
  23. wagtail/images/views/images.py +10 -2
  24. wagtail/snippets/tests/test_preview.py +77 -11
  25. wagtail/test/testapp/urls.py +1 -0
  26. wagtail/test/testapp/views.py +15 -1
  27. wagtail/tests/test_blocks.py +15 -0
  28. {wagtail-7.1.1.dist-info → wagtail-7.1.3.dist-info}/METADATA +1 -1
  29. {wagtail-7.1.1.dist-info → wagtail-7.1.3.dist-info}/RECORD +33 -33
  30. {wagtail-7.1.1.dist-info → wagtail-7.1.3.dist-info}/WHEEL +1 -1
  31. {wagtail-7.1.1.dist-info → wagtail-7.1.3.dist-info}/entry_points.txt +0 -0
  32. {wagtail-7.1.1.dist-info → wagtail-7.1.3.dist-info}/licenses/LICENSE +0 -0
  33. {wagtail-7.1.1.dist-info → wagtail-7.1.3.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,9 @@
6
6
  {% if breadcrumbs_items %}
7
7
  <div class="nice-padding w-mt-8">
8
8
  <h2 class="w-relative w-h1" id="header-title">
9
- {% icon classname="w-absolute w-top-1 -w-left-11 w-max-w-[1em] w-max-h-[1em]" name=header_icon %}
9
+ {% if header_icon %}
10
+ {% icon classname="w-absolute w-top-1 -w-left-11 w-max-w-[1em] w-max-h-[1em]" name=header_icon %}
11
+ {% endif %}
10
12
  {{ page_subtitle|default:page_title }}
11
13
  </h2>
12
14
  </div>
@@ -5,7 +5,8 @@
5
5
  <aside {% if request.in_preview_panel %}hidden{% endif %}>
6
6
  <div class="w-userbar w-userbar--{{ position|default:'bottom-right' }} {% admin_theme_classname %}" data-wagtail-userbar data-wagtail-userbar-origin="{{ origin|default:"" }}" part="userbar">
7
7
  {% block css %}
8
- <link rel="stylesheet" href="{% absolute_static 'wagtailadmin/css/core.css' %}">
8
+ {% versioned_static 'wagtailadmin/css/core.css' as core_css_url %}
9
+ <link rel="stylesheet" href="{% build_absolute_url core_css_url %}">
9
10
  {# For headless userbar: the contents of the hook also need absolute URLs #}
10
11
  {% hook_output 'insert_global_admin_css' %}
11
12
  {% endblock %}
@@ -44,7 +45,9 @@
44
45
  </template>
45
46
  <wagtail-userbar></wagtail-userbar>
46
47
  {% block js %}
47
- <script src="{% absolute_static 'wagtailadmin/js/vendor.js' %}"></script>
48
- <script src="{% absolute_static 'wagtailadmin/js/userbar.js' %}"></script>
48
+ {% versioned_static 'wagtailadmin/js/vendor.js' as vendor_js_url %}
49
+ {% versioned_static 'wagtailadmin/js/userbar.js' as userbar_js_url %}
50
+ <script src="{% build_absolute_url vendor_js_url %}"></script>
51
+ <script src="{% build_absolute_url userbar_js_url %}"></script>
49
52
  {% endblock %}
50
53
  <!-- end Wagtail user bar embed code -->
@@ -2,8 +2,8 @@
2
2
  {% load i18n wagtailadmin_tags %}
3
3
 
4
4
  {% block item_content %}
5
- {% base_url_setting default="" as base_url %}
6
- <a href="{{ base_url }}{% url 'wagtailadmin_home' %}" target="_parent" role="menuitem">
5
+ {% url 'wagtailadmin_home' as admin_home_url %}
6
+ <a href="{% build_absolute_url admin_home_url %}" target="_parent" role="menuitem">
7
7
  {% icon name="key" classname="w-action-icon" %}
8
8
  {% trans 'Go to Wagtail admin' %}
9
9
  </a>
@@ -2,8 +2,8 @@
2
2
  {% load i18n wagtailadmin_tags %}
3
3
 
4
4
  {% block item_content %}
5
- {% base_url_setting default="" as base_url %}
6
- <a href="{{ base_url }}{% url 'wagtailadmin_pages:add_subpage' self.page.id %}" target="_parent" role="menuitem">
5
+ {% url 'wagtailadmin_pages:add_subpage' self.page.id as add_url %}
6
+ <a href="{% build_absolute_url add_url %}" target="_parent" role="menuitem">
7
7
  {% icon name="plus" classname="w-action-icon" %}
8
8
  {% trans 'Add a child page' %}
9
9
  </a>
@@ -2,8 +2,8 @@
2
2
  {% load i18n wagtailadmin_tags %}
3
3
 
4
4
  {% block item_content %}
5
- {% base_url_setting default="" as base_url %}
6
- <a href="{{ base_url }}{% url 'wagtailadmin_pages:edit' self.page.id %}" target="_parent" role="menuitem">
5
+ {% url 'wagtailadmin_pages:edit' self.page.id as edit_url %}
6
+ <a href="{% build_absolute_url edit_url %}" target="_parent" role="menuitem">
7
7
  {% icon name="edit" classname="w-action-icon" %}
8
8
  {% trans 'Edit this page' %}
9
9
  </a>
@@ -2,8 +2,8 @@
2
2
  {% load i18n wagtailadmin_tags %}
3
3
 
4
4
  {% block item_content %}
5
- {% base_url_setting default="" as base_url %}
6
- <a href="{{ base_url }}{% url 'wagtailadmin_explore' self.parent_page.id %}" target="_parent" role="menuitem">
5
+ {% url 'wagtailadmin_explore' self.parent_page.id as explore_url %}
6
+ <a href="{% build_absolute_url explore_url %}" target="_parent" role="menuitem">
7
7
  {% icon name="folder-open-inverse" classname="w-action-icon" %}
8
8
  {% trans 'Show in Explorer' %}
9
9
  </a>
@@ -206,6 +206,33 @@ def admin_url_name(obj, action):
206
206
  return obj.snippet_viewset.get_url_name(action)
207
207
 
208
208
 
209
+ @register.simple_tag(takes_context=True)
210
+ def build_absolute_url(context, url):
211
+ """
212
+ Usage: {% build_absolute_url url %}
213
+ Returns the absolute URL of the given URL based on the request's host.
214
+ If the request doesn't exist in the context, falls back to
215
+ WAGTAILADMIN_BASE_URL as the base URL.
216
+ If the given URL is already absolute, or the request is a dummy preview
217
+ request, returns it unchanged.
218
+ """
219
+ request = context.get("request")
220
+ # If request is not found in the context, e.g. in notification emails,
221
+ # fall back to WAGTAILADMIN_BASE_URL.
222
+ if not request:
223
+ return urljoin(get_admin_base_url(), url)
224
+ # With a dummy preview request, the host has been overwritten to be the page
225
+ # Site's host, which may not have been configured correctly. We don't want
226
+ # to fall back to WAGTAILADMIN_BASE_URL either, as that may be different
227
+ # from the original request's host in multi-site instances and may result in
228
+ # users having to log in again. So we just return the URL unchanged.
229
+ if getattr(request, "is_dummy", False):
230
+ return url
231
+ # Rewrite relative URLs to absolute URLs based on the request's host, but
232
+ # leave absolute URLs unchanged.
233
+ return request.build_absolute_uri(url)
234
+
235
+
209
236
  @register.simple_tag
210
237
  def latest_str(obj):
211
238
  """
@@ -1,6 +1,7 @@
1
1
  import datetime
2
2
  from functools import wraps
3
3
 
4
+ from django.contrib.auth.models import Permission
4
5
  from django.test import TestCase, override_settings
5
6
  from django.urls import reverse
6
7
  from django.utils import timezone
@@ -358,6 +359,47 @@ class TestPreview(WagtailTestUtils, TestCase):
358
359
  self.assertTemplateUsed(response, "tests/event_page.html")
359
360
  self.assertContains(response, "Placeholder title")
360
361
 
362
+ def test_preview_on_create_without_permissions(self):
363
+ # Remove privileges from user
364
+ self.user.is_superuser = False
365
+ self.user.user_permissions.add(
366
+ Permission.objects.get(
367
+ content_type__app_label="wagtailadmin", codename="access_admin"
368
+ )
369
+ )
370
+ self.user.save()
371
+
372
+ preview_url = reverse(
373
+ "wagtailadmin_pages:preview_on_add",
374
+ args=("tests", "eventpage", self.home_page.id),
375
+ )
376
+
377
+ response = self.client.post(
378
+ preview_url,
379
+ self.post_data,
380
+ )
381
+ self.assertEqual(response.status_code, 302)
382
+ self.assertRedirects(response, reverse("wagtailadmin_home"))
383
+
384
+ def test_preview_on_create_get_without_permissions(self):
385
+ # Remove privileges from user
386
+ self.user.is_superuser = False
387
+ self.user.user_permissions.add(
388
+ Permission.objects.get(
389
+ content_type__app_label="wagtailadmin", codename="access_admin"
390
+ )
391
+ )
392
+ self.user.save()
393
+
394
+ preview_url = reverse(
395
+ "wagtailadmin_pages:preview_on_add",
396
+ args=("tests", "eventpage", self.home_page.id),
397
+ )
398
+
399
+ response = self.client.get(preview_url)
400
+ self.assertEqual(response.status_code, 302)
401
+ self.assertRedirects(response, reverse("wagtailadmin_home"))
402
+
361
403
  def test_preview_on_edit_with_m2m_field(self):
362
404
  preview_url = reverse(
363
405
  "wagtailadmin_pages:preview_on_edit", args=(self.event_page.id,)
@@ -414,6 +456,36 @@ class TestPreview(WagtailTestUtils, TestCase):
414
456
  self.assertContains(response, "<li>Parties</li>")
415
457
  self.assertContains(response, "<li>Holidays</li>")
416
458
 
459
+ soup = self.get_soup(response.content)
460
+ userbar = soup.select_one("wagtail-userbar")
461
+ self.assertIsNotNone(userbar)
462
+ template = soup.select_one("#wagtail-userbar-template")
463
+ self.assertIsNotNone(template)
464
+
465
+ # Previews use a dummy request object, so the userbar should use
466
+ # relative URLs to avoid using the wrong host.
467
+ admin_url = reverse("wagtailadmin_home")
468
+ admin_link = template.select_one(f'a[href$="{admin_url}"]')
469
+ self.assertIsNotNone(admin_link)
470
+ self.assertEqual(admin_link["href"], admin_url)
471
+
472
+ css_links = soup.select("link[rel='stylesheet']")
473
+ self.assertEqual(
474
+ [link.get("href") for link in css_links],
475
+ [
476
+ versioned_static("wagtailadmin/css/core.css"),
477
+ "/path/to/my/custom.css",
478
+ ],
479
+ )
480
+ scripts = soup.select("script[src]")
481
+ self.assertEqual(
482
+ [script.get("src") for script in scripts],
483
+ [
484
+ versioned_static("wagtailadmin/js/vendor.js"),
485
+ versioned_static("wagtailadmin/js/userbar.js"),
486
+ ],
487
+ )
488
+
417
489
  def test_preview_on_edit_with_valid_then_invalid_data(self):
418
490
  preview_url = reverse(
419
491
  "wagtailadmin_pages:preview_on_edit", args=(self.event_page.id,)
@@ -557,6 +629,45 @@ class TestPreview(WagtailTestUtils, TestCase):
557
629
  response = self.client.get(preview_url)
558
630
  self.assertEqual(response.status_code, 200)
559
631
 
632
+ def test_preview_on_edit_without_permissions(self):
633
+ # Remove privileges from user
634
+ self.user.is_superuser = False
635
+ self.user.user_permissions.add(
636
+ Permission.objects.get(
637
+ content_type__app_label="wagtailadmin", codename="access_admin"
638
+ )
639
+ )
640
+ self.user.save()
641
+
642
+ preview_url = reverse(
643
+ "wagtailadmin_pages:preview_on_edit", args=(self.event_page.id,)
644
+ )
645
+
646
+ response = self.client.post(
647
+ preview_url,
648
+ self.post_data,
649
+ )
650
+ self.assertEqual(response.status_code, 302)
651
+ self.assertRedirects(response, reverse("wagtailadmin_home"))
652
+
653
+ def test_preview_on_edit_get_without_permissions(self):
654
+ # Remove privileges from user
655
+ self.user.is_superuser = False
656
+ self.user.user_permissions.add(
657
+ Permission.objects.get(
658
+ content_type__app_label="wagtailadmin", codename="access_admin"
659
+ )
660
+ )
661
+ self.user.save()
662
+
663
+ preview_url = reverse(
664
+ "wagtailadmin_pages:preview_on_edit", args=(self.event_page.id,)
665
+ )
666
+
667
+ response = self.client.get(preview_url)
668
+ self.assertEqual(response.status_code, 302)
669
+ self.assertRedirects(response, reverse("wagtailadmin_home"))
670
+
560
671
  def test_preview_on_create_clear_preview_data(self):
561
672
  preview_session_key = "wagtail-preview-tests-eventpage-{}".format(
562
673
  self.home_page.id
@@ -1,6 +1,5 @@
1
1
  import json
2
2
 
3
- from django.conf import settings
4
3
  from django.contrib.auth.models import AnonymousUser, Permission
5
4
  from django.template import Context, Template
6
5
  from django.test import TestCase, override_settings
@@ -9,7 +8,7 @@ from django.utils import translation
9
8
  from django.utils.translation import gettext
10
9
 
11
10
  from wagtail import hooks
12
- from wagtail.admin.templatetags.wagtailadmin_tags import absolute_static
11
+ from wagtail.admin.staticfiles import versioned_static
13
12
  from wagtail.admin.userbar import AccessibilityItem, Userbar
14
13
  from wagtail.coreutils import get_dummy_request
15
14
  from wagtail.models import PAGE_TEMPLATE_VAR, Locale, Page, Site
@@ -67,7 +66,7 @@ class TestUserbarTag(WagtailTestUtils, TestCase):
67
66
  # Wagtail admin core CSS should be linked with absolute URL to
68
67
  # ensure it works when loaded from a different domain
69
68
  # (e.g. headless frontend)
70
- absolute_static("wagtailadmin/css/core.css"),
69
+ f"http://localhost{versioned_static('wagtailadmin/css/core.css')}",
71
70
  # Custom CSS must be changed appropriately if necessary
72
71
  "/path/to/my/custom.css",
73
72
  ],
@@ -80,8 +79,8 @@ class TestUserbarTag(WagtailTestUtils, TestCase):
80
79
  # Wagtail vendor and userbar JS should be linked with absolute
81
80
  # URL to ensure it works when loaded from a different domain
82
81
  # (e.g. headless frontend)
83
- absolute_static("wagtailadmin/js/vendor.js"),
84
- absolute_static("wagtailadmin/js/userbar.js"),
82
+ f"http://localhost{versioned_static('wagtailadmin/js/vendor.js')}",
83
+ f"http://localhost{versioned_static('wagtailadmin/js/userbar.js')}",
85
84
  ],
86
85
  )
87
86
 
@@ -136,9 +135,7 @@ class TestUserbarTag(WagtailTestUtils, TestCase):
136
135
  # Should render the "Go to Wagtail admin" link using an absolute URL
137
136
  soup = self.get_soup(content)
138
137
  admin_url = reverse("wagtailadmin_home")
139
- admin_link = soup.select_one(
140
- f"a[href='{settings.WAGTAILADMIN_BASE_URL}{admin_url}']"
141
- )
138
+ admin_link = soup.select_one(f"a[href='http://localhost{admin_url}']")
142
139
  self.assertIsNotNone(admin_link)
143
140
  self.assertEqual(admin_link.text.strip(), "Go to Wagtail admin")
144
141
 
@@ -156,16 +153,12 @@ class TestUserbarTag(WagtailTestUtils, TestCase):
156
153
  soup = self.get_soup(content)
157
154
 
158
155
  edit_url = reverse("wagtailadmin_pages:edit", args=(self.homepage.id,))
159
- edit_link = soup.select_one(
160
- f"a[href='{settings.WAGTAILADMIN_BASE_URL}{edit_url}']"
161
- )
156
+ edit_link = soup.select_one(f"a[href='http://localhost{edit_url}']")
162
157
  self.assertIsNotNone(edit_link)
163
158
  self.assertEqual(edit_link.text.strip(), "Edit this page")
164
159
 
165
160
  explore_url = reverse("wagtailadmin_explore", args=(self.parent_page.id,))
166
- explore_link = soup.select_one(
167
- f"a[href='{settings.WAGTAILADMIN_BASE_URL}{explore_url}']"
168
- )
161
+ explore_link = soup.select_one(f"a[href='http://localhost{explore_url}']")
169
162
  self.assertIsNotNone(explore_link)
170
163
  self.assertEqual(explore_link.text.strip(), "Show in Explorer")
171
164
 
@@ -185,16 +178,12 @@ class TestUserbarTag(WagtailTestUtils, TestCase):
185
178
  soup = self.get_soup(content)
186
179
 
187
180
  edit_url = reverse("wagtailadmin_pages:edit", args=(self.homepage.id,))
188
- edit_link = soup.select_one(
189
- f"a[href='{settings.WAGTAILADMIN_BASE_URL}{edit_url}']"
190
- )
181
+ edit_link = soup.select_one(f"a[href='http://localhost{edit_url}']")
191
182
  self.assertIsNotNone(edit_link)
192
183
  self.assertEqual(edit_link.text.strip(), "Edit this page")
193
184
 
194
185
  explore_url = reverse("wagtailadmin_explore", args=(self.parent_page.id,))
195
- explore_link = soup.select_one(
196
- f"a[href='{settings.WAGTAILADMIN_BASE_URL}{explore_url}']"
197
- )
186
+ explore_link = soup.select_one(f"a[href='http://localhost{explore_url}']")
198
187
  self.assertIsNotNone(explore_link)
199
188
  self.assertEqual(explore_link.text.strip(), "Show in Explorer")
200
189
 
@@ -221,9 +210,7 @@ class TestUserbarTag(WagtailTestUtils, TestCase):
221
210
  # The explore link should still be visible
222
211
  soup = self.get_soup(content)
223
212
  explore_url = reverse("wagtailadmin_explore", args=(self.parent_page.id,))
224
- explore_link = soup.select_one(
225
- f"a[href='{settings.WAGTAILADMIN_BASE_URL}{explore_url}']"
226
- )
213
+ explore_link = soup.select_one(f"a[href='http://localhost{explore_url}']")
227
214
  self.assertIsNotNone(explore_link)
228
215
  self.assertEqual(explore_link.text.strip(), "Show in Explorer")
229
216
 
@@ -718,7 +705,7 @@ class TestUserbarAddLink(WagtailTestUtils, TestCase):
718
705
  self.assertEqual(response.status_code, 200)
719
706
 
720
707
  # page allows subpages, so the 'add page' button should show
721
- expected_url = settings.WAGTAILADMIN_BASE_URL + (
708
+ expected_url = self.request.build_absolute_uri(
722
709
  reverse("wagtailadmin_pages:add_subpage", args=(self.event_index.id,))
723
710
  )
724
711
  needle = f"""
@@ -762,7 +749,7 @@ class TestUserbarComponent(WagtailTestUtils, TestCase):
762
749
  items = soup.select("li")
763
750
  self.assertEqual(len(items), 2)
764
751
 
765
- admin_url = f"{settings.WAGTAILADMIN_BASE_URL}{reverse('wagtailadmin_home')}"
752
+ admin_url = f"http://localhost{reverse('wagtailadmin_home')}"
766
753
  admin_item = items[0]
767
754
  admin_link = admin_item.select_one("a")
768
755
  self.assertIsNotNone(admin_link)
@@ -780,11 +767,18 @@ class TestUserbarComponent(WagtailTestUtils, TestCase):
780
767
  def test_render_no_request(self):
781
768
  rendered = Userbar().render_html({})
782
769
  soup = self.get_soup(rendered)
770
+ # Without a request provided, URLs should fall back to the
771
+ # WAGTAILADMIN_BASE_URL setting
772
+ base_url = "http://testserver"
773
+
774
+ userbar = soup.select_one("[data-wagtail-userbar]")
775
+ self.assertIsNotNone(userbar)
776
+ self.assertEqual(userbar.get("data-wagtail-userbar-origin"), base_url)
783
777
 
784
778
  items = soup.select("li")
785
779
  self.assertEqual(len(items), 2)
786
780
 
787
- admin_url = f"{settings.WAGTAILADMIN_BASE_URL}{reverse('wagtailadmin_home')}"
781
+ admin_url = f"{base_url}{reverse('wagtailadmin_home')}"
788
782
  admin_item = items[0]
789
783
  admin_link = admin_item.select_one("a")
790
784
  self.assertIsNotNone(admin_link)
@@ -799,21 +793,44 @@ class TestUserbarComponent(WagtailTestUtils, TestCase):
799
793
  "Issues found | Accessibility",
800
794
  )
801
795
 
796
+ css_links = soup.select("link[rel='stylesheet']")
797
+ self.assertEqual(
798
+ [link.get("href") for link in css_links],
799
+ [
800
+ # Wagtail admin core CSS should be linked with absolute URL to
801
+ # ensure it works when loaded from a different domain
802
+ # (e.g. headless frontend)
803
+ f"{base_url}{versioned_static('wagtailadmin/css/core.css')}",
804
+ # Custom CSS must be changed appropriately if necessary
805
+ "/path/to/my/custom.css",
806
+ ],
807
+ )
808
+
809
+ scripts = soup.select("script[src]")
810
+ self.assertEqual(
811
+ [script.get("src") for script in scripts],
812
+ [
813
+ # Wagtail vendor and userbar JS should be linked with absolute
814
+ # URL to ensure it works when loaded from a different domain
815
+ # (e.g. headless frontend)
816
+ f"{base_url}{versioned_static('wagtailadmin/js/vendor.js')}",
817
+ f"{base_url}{versioned_static('wagtailadmin/js/userbar.js')}",
818
+ ],
819
+ )
820
+
802
821
  def test_render_minimal(self):
803
822
  rendered = Userbar().render_html({"request": self.request})
804
823
  soup = self.get_soup(rendered)
805
824
 
806
825
  userbar = soup.select_one("[data-wagtail-userbar]")
807
826
  self.assertIsNotNone(userbar)
808
- self.assertEqual(
809
- userbar.get("data-wagtail-userbar-origin"),
810
- settings.WAGTAILADMIN_BASE_URL,
811
- )
827
+ # The origin should be based on the request's scheme and host information
828
+ self.assertEqual(userbar.get("data-wagtail-userbar-origin"), "http://localhost")
812
829
 
813
830
  items = soup.select("li")
814
831
  self.assertEqual(len(items), 2)
815
832
 
816
- admin_url = f"{settings.WAGTAILADMIN_BASE_URL}{reverse('wagtailadmin_home')}"
833
+ admin_url = f"http://localhost{reverse('wagtailadmin_home')}"
817
834
  admin_item = items[0]
818
835
  admin_link = admin_item.select_one("a")
819
836
  self.assertIsNotNone(admin_link)
@@ -844,7 +861,7 @@ class TestUserbarComponent(WagtailTestUtils, TestCase):
844
861
  self.assertEqual(len(links), 4)
845
862
  self.assertEqual(
846
863
  [link.get("href") for link in links],
847
- [f"{settings.WAGTAILADMIN_BASE_URL}{url}" for url in expected_urls],
864
+ [f"http://localhost{url}" for url in expected_urls],
848
865
  )
849
866
 
850
867
  accessibility_button = soup.select_one("li button")
@@ -855,8 +872,8 @@ class TestUserbarComponent(WagtailTestUtils, TestCase):
855
872
  )
856
873
 
857
874
  @override_settings(WAGTAILADMIN_BASE_URL=None)
858
- def test_render_without_admin_base_url_setting(self):
859
- rendered = Userbar().render_html({"request": self.request})
875
+ def test_render_without_request_and_admin_base_url_setting(self):
876
+ rendered = Userbar().render_html({})
860
877
  soup = self.get_soup(rendered)
861
878
 
862
879
  userbar = soup.select_one("[data-wagtail-userbar]")
@@ -868,7 +885,8 @@ class TestUserbarComponent(WagtailTestUtils, TestCase):
868
885
  items = soup.select("li")
869
886
  self.assertEqual(len(items), 2)
870
887
 
871
- # Becomes the root-relative URL since WAGTAILADMIN_BASE_URL is None
888
+ # Becomes the root-relative URL since no request is provided
889
+ # and WAGTAILADMIN_BASE_URL is None
872
890
  admin_url = reverse("wagtailadmin_home")
873
891
  admin_item = items[0]
874
892
  admin_link = admin_item.select_one("a")
@@ -883,3 +901,25 @@ class TestUserbarComponent(WagtailTestUtils, TestCase):
883
901
  button.get_text(separator=" | ", strip=True).strip(),
884
902
  "Issues found | Accessibility",
885
903
  )
904
+
905
+ css_links = soup.select("link[rel='stylesheet']")
906
+ self.assertEqual(
907
+ [link.get("href") for link in css_links],
908
+ [
909
+ # Cannot build absolute URL without a request and
910
+ # WAGTAILADMIN_BASE_URL, so should be root-relative
911
+ versioned_static("wagtailadmin/css/core.css"),
912
+ "/path/to/my/custom.css",
913
+ ],
914
+ )
915
+
916
+ scripts = soup.select("script[src]")
917
+ self.assertEqual(
918
+ [script.get("src") for script in scripts],
919
+ [
920
+ # Cannot build absolute URL without a request and
921
+ # WAGTAILADMIN_BASE_URL, so should be root-relative
922
+ versioned_static("wagtailadmin/js/vendor.js"),
923
+ versioned_static("wagtailadmin/js/userbar.js"),
924
+ ],
925
+ )
@@ -2,6 +2,7 @@ from django.contrib.admin.utils import quote
2
2
  from django.test import TestCase
3
3
  from django.urls import reverse
4
4
 
5
+ from wagtail.test.testapp.models import ModelWithStringTypePrimaryKey
5
6
  from wagtail.test.utils import WagtailTestUtils
6
7
 
7
8
 
@@ -37,6 +38,39 @@ class TestGenericIndexViewWithoutModel(WagtailTestUtils, TestCase):
37
38
  self.assertEqual(response_object_count, 4)
38
39
 
39
40
 
41
+ class TestGenericCreateView(WagtailTestUtils, TestCase):
42
+ fixtures = ["test.json"]
43
+
44
+ def get(self, params={}):
45
+ return self.client.get(reverse("testapp_generic_create"), params)
46
+
47
+ def test_get_create_view(self):
48
+ response = self.get()
49
+ self.assertEqual(response.status_code, 200)
50
+ soup = self.get_soup(response.content)
51
+ h2 = soup.select_one("main h2")
52
+ self.assertIsNotNone(h2)
53
+ self.assertEqual(h2.text.strip(), "Model with string type primary key")
54
+ form = soup.select_one("main form")
55
+ self.assertIsNotNone(form)
56
+ id_input = form.select_one("input[name='custom_id']")
57
+ self.assertIsNotNone(id_input)
58
+ self.assertEqual(id_input.get("type"), "text")
59
+ content_input = form.select_one("input[name='content']")
60
+ self.assertIsNotNone(content_input)
61
+
62
+ def test_post_create_view(self):
63
+ post_data = {
64
+ "custom_id": "string-pk-3",
65
+ "content": "third modelwithstringtypeprimarykey model",
66
+ }
67
+ response = self.client.post(reverse("testapp_generic_create"), post_data)
68
+ self.assertEqual(response.status_code, 302) # Redirect to index view
69
+ self.assertTrue(
70
+ ModelWithStringTypePrimaryKey.objects.filter(pk="string-pk-3").exists()
71
+ )
72
+
73
+
40
74
  class TestGenericEditView(WagtailTestUtils, TestCase):
41
75
  fixtures = ["test.json"]
42
76
 
wagtail/admin/userbar.py CHANGED
@@ -364,10 +364,15 @@ class Userbar(Component):
364
364
  # Remove any unrendered items
365
365
  rendered_items = [item for item in rendered_items if item]
366
366
 
367
+ if request:
368
+ origin = f"{request.scheme}://{request.get_host()}"
369
+ else:
370
+ origin = get_admin_base_url() or ""
371
+
367
372
  # Render the userbar items
368
373
  return {
369
374
  "request": request,
370
- "origin": get_admin_base_url(),
375
+ "origin": origin,
371
376
  "items": rendered_items,
372
377
  "position": self.position,
373
378
  "page": self.object,
@@ -17,13 +17,16 @@ from wagtail.blocks.base import Block
17
17
  from wagtail.models import PreviewableMixin, RevisionMixin
18
18
  from wagtail.utils.decorators import xframe_options_sameorigin_override
19
19
 
20
+ from .permissions import PermissionCheckedMixin
20
21
 
21
- class PreviewOnEdit(View):
22
+
23
+ class PreviewOnEdit(PermissionCheckedMixin, View):
22
24
  model = None
23
25
  form_class = None
24
26
  http_method_names = ("post", "get", "delete")
25
27
  preview_expiration_timeout = 60 * 60 * 24 # seconds
26
28
  session_key_prefix = "wagtail-preview-"
29
+ permission_required = "change"
27
30
 
28
31
  def setup(self, request, *args, **kwargs):
29
32
  super().setup(request, *args, **kwargs)
@@ -54,6 +57,8 @@ class PreviewOnEdit(View):
54
57
 
55
58
  def get_object(self):
56
59
  obj = get_object_or_404(self.model, pk=unquote(str(self.kwargs["pk"])))
60
+ if not self.user_has_permission_for_instance(self.permission_required, obj):
61
+ raise PermissionDenied
57
62
  if isinstance(obj, RevisionMixin):
58
63
  obj = obj.get_latest_revision_as_object()
59
64
  return obj
@@ -136,6 +141,8 @@ class PreviewOnEdit(View):
136
141
 
137
142
 
138
143
  class PreviewOnCreate(PreviewOnEdit):
144
+ permission_required = "add"
145
+
139
146
  @property
140
147
  def session_key(self):
141
148
  app_label = self.model._meta.app_label
@@ -30,9 +30,13 @@ class PreviewOnEdit(GenericPreviewOnEdit):
30
30
  return "{}{}".format(self.session_key_prefix, self.kwargs["page_id"])
31
31
 
32
32
  def get_object(self):
33
- return get_object_or_404(
33
+ page = get_object_or_404(
34
34
  Page, id=self.kwargs["page_id"]
35
35
  ).get_latest_revision_as_object()
36
+ page_perms = page.permissions_for_user(self.request.user)
37
+ if not page_perms.can_edit():
38
+ raise PermissionDenied
39
+ return page
36
40
 
37
41
  def get_form(self, query_dict):
38
42
  form_class = self.object.get_edit_handler().get_form_class()
@@ -87,6 +91,11 @@ class PreviewOnCreate(PreviewOnEdit):
87
91
 
88
92
  page = content_type.model_class()()
89
93
  parent_page = get_object_or_404(Page, id=parent_page_id).specific
94
+
95
+ parent_page_perms = parent_page.permissions_for_user(self.request.user)
96
+ if not parent_page_perms.can_add_subpage():
97
+ raise PermissionDenied
98
+
90
99
  # We need to populate treebeard's path / depth fields in order to
91
100
  # pass validation. We can't make these 100% consistent with the rest
92
101
  # of the tree without making actual database changes (such as
@@ -427,7 +427,8 @@ class StructBlockAdapter(Adapter):
427
427
  if block.meta.form_template:
428
428
  meta["formTemplate"] = block.render_form_template()
429
429
 
430
- if block.meta.label_format:
430
+ # Check specifically for None to allow for empty string
431
+ if block.meta.label_format is not None:
431
432
  meta["labelFormat"] = block.meta.label_format
432
433
 
433
434
  return [