kopia lustrzana https://github.com/wagtail/wagtail
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 templatespull/4922/head
rodzic
a7f58821a7
commit
4846a3e801
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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, '')
|
||||
|
|
|
@ -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)
|
||||
|
|
Ładowanie…
Reference in New Issue