Add get_page_url() method to BaseSetting

* adds a convenience `page_url` shortcut to improve how page URLs can be accessed from site settings in Django templates
pull/4922/head
Andy Babic 2020-04-03 22:14:56 +01:00 zatwierdzone przez LB
rodzic a7f58821a7
commit 4846a3e801
6 zmienionych plików z 230 dodań i 3 usunięć

Wyświetl plik

@ -15,6 +15,7 @@ Changelog
* Add ability to sort search promotions on listing page (Chris Ranjana, LB (Ben Johnston))
* Upgrade internal JS tooling to Gulp v4 & Node v10 (Jim Jazwiecki, Kim LaRocca)
* Add `after_publish_page`, `before_publish_page`, `after_unpublish_page` & `before_unpublish_page` hooks (Jonatas Baldin, Coen van der Kamp)
* Add convenience `page_url` shortcut to improve how page URLs can be accessed from site settings in Django templates (Andy Babic)
* Fix: Support IPv6 domain (Alex Gleason, Coen van der Kamp)
* Fix: Ensure link to add a new user works when no users are visible in the users list (LB (Ben Johnston))
* Fix: `AbstractEmailForm` saved submission fields are now aligned with the email content fields, `form.cleaned_data` will be used instead of `form.fields` (Haydn Greatnews)

Wyświetl plik

@ -227,6 +227,8 @@ Or, alternately, using the ``set`` tag:
Utilising ``select_related`` to improve efficiency
--------------------------------------------------
.. versionadded:: 2.9
For models with foreign key relationships to other objects (e.g. pages),
which are very often needed to output values in templates, you can set
the ``select_related`` attribute on your model to have Wagtail utilise
@ -264,5 +266,55 @@ and two more to fetch each page):
.. code-block:: html
{{ settings.app_label.ImportantPages.donate_page.url }}
{{ settings.app_label.ImportantPages.sign_up_page.url }}
{% load wagtailcore_tags %}
{% pageurl settings.app_label.ImportantPages.donate_page %}
{% pageurl settings.app_label.ImportantPages.sign_up_page %}
Utilising the ``page_url`` setting shortcut
-------------------------------------------
.. versionadded:: 2.10
If, like in the previous section, your settings model references pages,
and you regularly need to output the URLs of those pages in your project,
you can likely use the setting model's ``page_url`` shortcut to do that more
cleanly. For example, instead of doing the following:
.. code-block:: html
{% load wagtailcore_tags %}
{% pageurl settings.app_label.ImportantPages.donate_page %}
{% pageurl settings.app_label.ImportantPages.sign_up_page %}
You could write:
.. code-block:: html
{{ settings.app_label.ImportantPages.page_url.donate_page }}
{{ settings.app_label.ImportantPages.page_url.sign_up_page }}
Using the ``page_url`` shortcut has a few of advantages over using the tag:
1. The 'specific' page is automatically fetched to generate the URL,
so you don't have to worry about doing this (or forgetting to do this)
yourself.
2. The results are cached, so if you need to access the same page URL
in more than one place (e.g. in a form and in footer navigation), using
the ``page_url`` shortcut will be more efficient.
3. It's more concise, and the syntax is the same whether using it in templates
or views (or other Python code), allowing you to write more more consistent
code.
When using the ``page_url`` shortcut, there are a couple of points worth noting:
1. The same limitations that apply to the `{% pageurl %}` tag apply to the
shortcut: If the settings are accessed from a template context where the
current request is not available, all URLs returned will include the
site's scheme/domain, and URL generation will not be quite as efficient.
2. If using the shortcut in views or other Python code, the method will
raise an ``AttributeError`` if the attribute you request from ``page_url``
is not an attribute on the settings object.
3. If the settings object DOES have the attribute, but the attribute returns
a value of ``None`` (or something that is not a ``Page``), the shortcut
will return an empty string.

Wyświetl plik

@ -24,6 +24,7 @@ Other features
* Add ability to sort search promotions on listing page (Chris Ranjana, LB (Ben Johnston))
* Upgrade internal JS tooling to Gulp v4 & Node v10 (Jim Jazwiecki, Kim LaRocca)
* Add ``after_publish_page``, ``before_publish_page``, ``after_unpublish_page`` & ``before_unpublish_page`` hooks (Jonatas Baldin, Coen van der Kamp)
* Add convenience ``page_url`` shortcut to improve how page URLs can be accessed from site settings in Django templates (Andy Babic)
Bug fixes

Wyświetl plik

@ -1,6 +1,7 @@
from django.db import models
from wagtail.core.models import Site
from wagtail.core.utils import InvokeViaAttributeShortcut
from .registry import register_setting
@ -62,6 +63,8 @@ class BaseSetting(models.Model):
return getattr(request, attr_name)
site = Site.find_for_request(request)
site_settings = cls.for_site(site)
# to allow more efficient page url generation
site_settings._request = request
setattr(request, attr_name, site_settings)
return site_settings
@ -74,3 +77,42 @@ class BaseSetting(models.Model):
return "_{}.{}".format(
cls._meta.app_label, cls._meta.model_name
).lower()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Allows get_page_url() to be invoked using
# `obj.page_url.foreign_key_name` syntax
self.page_url = InvokeViaAttributeShortcut(self, 'get_page_url')
# Per-instance page URL cache
self._page_url_cache = {}
def get_page_url(self, attribute_name, request=None):
"""
Returns the URL of a page referenced by a foreign key
(or other attribute) matching the name ``attribute_name``.
If the field value is null, or links to something other
than a ``Page`` object, an empty string is returned.
The result is also cached per-object to facilitate
fast repeat access.
Raises an ``AttributeError`` if the object has no such
field or attribute.
"""
if attribute_name in self._page_url_cache:
return self._page_url_cache[attribute_name]
if not hasattr(self, attribute_name):
raise AttributeError(
"'{}' object has no attribute '{}'"
.format(self.__class__.__name__, attribute_name)
)
page = getattr(self, attribute_name)
if hasattr(page, 'specific'):
url = page.specific.get_url(getattr(self, '_request', None))
else:
url = ""
self._page_url_cache[attribute_name] = url
return url

Wyświetl plik

@ -51,7 +51,7 @@ class SettingModelTestCase(SettingsTestMixin, TestCase):
def _create_importantpages_object(self):
site = self.default_site
ImportantPages.objects.create(
return ImportantPages.objects.create(
site=site,
sign_up_page=site.root_page,
general_terms_page=site.root_page,
@ -83,3 +83,105 @@ class SettingModelTestCase(SettingsTestMixin, TestCase):
finally:
# undo temporary change
ImportantPages.select_related = None
def test_get_page_url_when_settings_fetched_via_for_request(self):
""" Using ImportantPages.for_request() makes the setting
object request-aware, improving efficiency and allowing
site-relative URLs to be returned """
self._create_importantpages_object()
request = self.get_request()
settings = ImportantPages.for_request(request)
# Force site root paths query beforehand
self.default_site.root_page._get_site_root_paths(request)
for page_fk_field, expected_result in (
('sign_up_page', '/'),
('general_terms_page', '/'),
('privacy_policy_page', 'http://other/'),
):
with self.subTest(page_fk_field=page_fk_field):
with self.assertNumQueries(1):
# because results are cached, only the first
# request for a URL will trigger a query to
# fetch the page
self.assertEqual(
settings.get_page_url(page_fk_field),
expected_result)
# when called directly
self.assertEqual(
settings.get_page_url(page_fk_field),
expected_result
)
# when called indirectly via shortcut
self.assertEqual(
getattr(settings.page_url, page_fk_field),
expected_result
)
def test_get_page_url_when_for_settings_fetched_via_for_site(self):
""" ImportantPages.for_site() cannot make the settings object
request-aware, so things are a little less efficient, and the
URLs returned will not be site-relative """
self._create_importantpages_object()
settings = ImportantPages.for_site(self.default_site)
# Force site root paths query beforehand
self.default_site.root_page._get_site_root_paths()
for page_fk_field, expected_result in (
('sign_up_page', 'http://localhost/'),
('general_terms_page', 'http://localhost/'),
('privacy_policy_page', 'http://other/'),
):
with self.subTest(page_fk_field=page_fk_field):
# only the first request for each URL will trigger queries.
# 2 are triggered instead of 1 here, because tests use the
# database cache backed, and the cache is queried each time
# to fetch site root paths (because there's no 'request' to
# store them on)
with self.assertNumQueries(2):
self.assertEqual(
settings.get_page_url(page_fk_field),
expected_result
)
# when called directly
self.assertEqual(
settings.get_page_url(page_fk_field),
expected_result
)
# when called indirectly via shortcut
self.assertEqual(
getattr(settings.page_url, page_fk_field),
expected_result
)
def test_get_page_url_raises_attributeerror_if_attribute_name_invalid(self):
settings = self._create_importantpages_object()
# when called directly
with self.assertRaises(AttributeError):
settings.get_page_url('not_an_attribute')
# when called indirectly via shortcut
with self.assertRaises(AttributeError):
settings.page_url.not_an_attribute
def test_get_page_url_returns_empty_string_if_attribute_value_not_a_page(self):
settings = self._create_importantpages_object()
for value in (None, self.default_site):
with self.subTest(attribute_value=value):
settings.test_attribute = value
# when called directly
self.assertEqual(settings.get_page_url('test_attribute'), '')
# when called indirectly via shortcut
self.assertEqual(settings.page_url.test_attribute, '')

Wyświetl plik

@ -103,3 +103,32 @@ def accepts_kwarg(func, kwarg):
return True
except TypeError:
return False
class InvokeViaAttributeShortcut:
"""
Used to create a shortcut that allows an object's named
single-argument method to be invoked using a simple
attribute reference syntax. For example, adding the
following to an object:
obj.page_url = InvokeViaAttributeShortcut(obj, 'get_page_url')
Would allow you to invoke get_page_url() like so:
obj.page_url.terms_and_conditions
As well as the usual:
obj.get_page_url('terms_and_conditions')
"""
__slots__ = 'obj', 'method_name'
def __init__(self, obj, method_name):
self.obj = obj
self.method_name = method_name
def __getattr__(self, name):
method = getattr(self.obj, self.method_name)
return method(name)