mirror of
https://github.com/wagtail/wagtail.git
synced 2024-12-01 11:41:20 +01:00
Merge branch 'takeflight-merge-settings'
This commit is contained in:
commit
dc02375cc8
@ -4,6 +4,7 @@ Changelog
|
||||
1.2 (xx.xx.xxxx)
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
* Added `wagtail.contrib.settings`, a module to allow administrators to edit site-specific settings
|
||||
* Core templatetags (pageurl, image, wagtailuserbar, etc) are now compatible with Jinja2
|
||||
* Image and document models now provide a `search` method on their QuerySets
|
||||
* Search methods now accept an `operator` argument to determine whether multiple terms are ORed or ANDed together
|
||||
|
@ -1,3 +1,5 @@
|
||||
.. _styleguide:
|
||||
|
||||
UI Styleguide
|
||||
=============
|
||||
|
||||
@ -15,4 +17,4 @@ To install the styleguide module on your site, add it to the list of ``INSTALLED
|
||||
|
||||
At present the styleguide is static: new UI components must be added to it manually, and there are no hooks into it for other modules to use. We hope to support hooks in the future.
|
||||
|
||||
The styleguide doesn't currently provide examples of all the core interface components; notably the Page, Document, Image and Snippet chooser interfaces are not currently represented.
|
||||
The styleguide doesn't currently provide examples of all the core interface components; notably the Page, Document, Image and Snippet chooser interfaces are not currently represented.
|
||||
|
@ -7,6 +7,7 @@ Wagtail ships with a variety of extra optional modules.
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
settings
|
||||
forms
|
||||
staticsitegen
|
||||
sitemaps
|
||||
@ -16,6 +17,12 @@ Wagtail ships with a variety of extra optional modules.
|
||||
searchpromotions
|
||||
|
||||
|
||||
:doc:`settings`
|
||||
---------------
|
||||
|
||||
Site-wide settings that are editable by administrators in the Wagtail admin.
|
||||
|
||||
|
||||
:doc:`forms`
|
||||
------------
|
||||
|
||||
|
128
docs/reference/contrib/settings.rst
Normal file
128
docs/reference/contrib/settings.rst
Normal file
@ -0,0 +1,128 @@
|
||||
.. _settings:
|
||||
|
||||
=============
|
||||
Site settings
|
||||
=============
|
||||
|
||||
You can define settings for your site that are editable by administrators in the Wagtail admin. These settings can be accessed in code, as well as in templates.
|
||||
|
||||
To use these settings, you must add ``wagtail.contrib.settings`` to your ``INSTALLED_APPS``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
INSTALLED_APPS += [
|
||||
'wagtail.contrib.settings',
|
||||
]
|
||||
|
||||
|
||||
Defining settings
|
||||
=================
|
||||
|
||||
Create a model that inherits from ``BaseSetting``, and register it using the ``register_setting`` decorator:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from wagtail.contrib.settings.models import BaseSetting, register_setting
|
||||
|
||||
@register_setting
|
||||
class SocialMediaSettings(BaseSetting):
|
||||
facebook = models.URLField(
|
||||
help_text='Your Facebook page URL')
|
||||
instagram = models.CharField(
|
||||
max_length=255, help_text='Your Instagram username, without the @')
|
||||
trip_advisor = models.URLField(
|
||||
help_text='Your Trip Advisor page URL')
|
||||
youtube = models.URLField(
|
||||
help_text='Your YouTube channel or user account URL')
|
||||
|
||||
|
||||
A 'Social media settings' link will appear in the Wagtail admin 'Settings' menu.
|
||||
|
||||
Edit handlers
|
||||
-------------
|
||||
|
||||
Settings use edit handlers much like the rest of Wagtail. Add a ``panels`` setting to your model defining all the edit handlers required:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@register_setting
|
||||
class ImportantPages(BaseSetting):
|
||||
donate_page = models.ForeignKey(
|
||||
'wagtailcore.Page', null=True, on_delete=models.SET_NULL)
|
||||
sign_up_page = models.ForeignKey(
|
||||
'wagtailcore.Page', null=True, on_delete=models.SET_NULL)
|
||||
|
||||
panels = [
|
||||
PageChooserPanel('donate_page'),
|
||||
PageChooserPanel('sign_up_page'),
|
||||
]
|
||||
|
||||
Appearance
|
||||
----------
|
||||
|
||||
You can change the label used in the menu by changing the `verbose_name <https://docs.djangoproject.com/en/dev/ref/models/options/#verbose-name>`_ of your model.
|
||||
|
||||
You can add an icon to the menu by passing an 'icon' argument to the ``register_setting`` decorator:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@register_setting(icon='icon-placeholder')
|
||||
class SocialMediaSettings(BaseSetting):
|
||||
class Meta:
|
||||
verbose_name = 'Social media accounts'
|
||||
...
|
||||
|
||||
For a list of all available icons, please see the :ref:`styleguide`.
|
||||
|
||||
Using the settings
|
||||
==================
|
||||
|
||||
Settings are designed to be used both in Python code, and in templates.
|
||||
|
||||
Using in Python
|
||||
---------------
|
||||
|
||||
If access to a setting is required in the code, the :func:`~wagtail.contrib.settings.models.BaseSetting.for_site` method will retrieve the setting for the supplied site:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def view(request):
|
||||
social_media_settings = SocialMediaSettings.for_site(request.site)
|
||||
...
|
||||
|
||||
Using in templates
|
||||
------------------
|
||||
|
||||
Add the ``request`` and ``settings`` context processors to your settings:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.conf import global_settings
|
||||
TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + [
|
||||
'django.core.context_processors.request',
|
||||
'wagtail.contrib.settings.context_processors.settings',
|
||||
]
|
||||
|
||||
Then access the settings through ``{{ settings }}``:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{{ settings.app_label.SocialMediaSettings.instagram }}
|
||||
|
||||
If you are not in a ``RequestContext``, then context processors will not have run, and the ``settings`` variable will not be availble. To get the ``settings``, use the provided ``{% get_settings %}`` template tag. If a ``request`` is in the template context, but for some reason it is not a ``RequestContext``, just use ``{% get_settings %}``:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% load wagtailsettings_tags %}
|
||||
{% get_settings %}
|
||||
{{ settings.app_label.SocialMediaSettings.instagram }}
|
||||
|
||||
If there is no ``request`` available in the template at all, you can use the settings for the default Wagtail site instead:
|
||||
|
||||
.. code-block:: html+django
|
||||
|
||||
{% load wagtailsettings_tags %}
|
||||
{% get_settings use_default_site=True %}
|
||||
{{ settings.app_label.SocialMediaSettings.instagram }}
|
||||
|
||||
.. note:: You can not reliably get the correct settings instance for the current site from this template tag if the request object is not available. This is only relevant for multisite instances of Wagtail.
|
@ -10,6 +10,14 @@ Wagtail 1.2 release notes - IN DEVELOPMENT
|
||||
What's new
|
||||
==========
|
||||
|
||||
Site settings module
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Wagtail now includes a contrib module (previously available as the `wagtailsettings <https://pypi.python.org/pypi/wagtailsettings/>`_ package) to allow administrators to edit site-specific settings.
|
||||
|
||||
See: :doc:`/reference/contrib/settings`
|
||||
|
||||
|
||||
Jinja2 support
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -54,6 +54,9 @@ var apps = [
|
||||
'wagtailstyleguide/scss/styleguide.scss'
|
||||
],
|
||||
}),
|
||||
new App('wagtail/contrib/settings', {
|
||||
'appName': 'wagtailsettings',
|
||||
}),
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
|
1
wagtail/contrib/settings/.gitignore
vendored
Normal file
1
wagtail/contrib/settings/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
static/
|
0
wagtail/contrib/settings/__init__.py
Normal file
0
wagtail/contrib/settings/__init__.py
Normal file
7
wagtail/contrib/settings/apps.py
Normal file
7
wagtail/contrib/settings/apps.py
Normal file
@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class WagtailSettingsAppConfig(AppConfig):
|
||||
name = 'wagtail.contrib.settings'
|
||||
label = 'wagtailsettings'
|
||||
verbose_name = "Wagtail site settings"
|
56
wagtail/contrib/settings/context_processors.py
Normal file
56
wagtail/contrib/settings/context_processors.py
Normal file
@ -0,0 +1,56 @@
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
|
||||
from .registry import registry
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class SettingsProxy(dict):
|
||||
"""
|
||||
Get a SettingModuleProxy for an app using proxy['app_label']
|
||||
"""
|
||||
def __init__(self, site):
|
||||
self.site = site
|
||||
|
||||
def __missing__(self, app_label):
|
||||
self[app_label] = value = SettingModuleProxy(self.site, app_label)
|
||||
return value
|
||||
|
||||
def __str__(self):
|
||||
return 'SettingsProxy'
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class SettingModuleProxy(dict):
|
||||
"""
|
||||
Get a setting instance using proxy['modelname']
|
||||
"""
|
||||
def __init__(self, site, app_label):
|
||||
self.site = site
|
||||
self.app_label = app_label
|
||||
|
||||
def __getitem__(self, model_name):
|
||||
""" Get a setting instance for a model """
|
||||
# Model names are treated as case-insensitive
|
||||
return super(SettingModuleProxy, self).__getitem__(model_name.lower())
|
||||
|
||||
def __missing__(self, model_name):
|
||||
""" Get and cache settings that have not been looked up yet """
|
||||
self[model_name] = value = self.get_setting(model_name)
|
||||
return value
|
||||
|
||||
def get_setting(self, model_name):
|
||||
"""
|
||||
Get a setting instance
|
||||
"""
|
||||
Model = registry.get_by_natural_key(self.app_label, model_name)
|
||||
if Model is None:
|
||||
return None
|
||||
|
||||
return Model.for_site(self.site)
|
||||
|
||||
def __str__(self):
|
||||
return 'SettingsModuleProxy({0})'.format(self.app_label)
|
||||
|
||||
|
||||
def settings(request):
|
||||
return {'settings': SettingsProxy(request.site)}
|
27
wagtail/contrib/settings/forms.py
Normal file
27
wagtail/contrib/settings/forms.py
Normal file
@ -0,0 +1,27 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from wagtail.wagtailcore.models import Site
|
||||
|
||||
|
||||
class SiteSwitchForm(forms.Form):
|
||||
site = forms.ChoiceField(choices=[])
|
||||
|
||||
class Media:
|
||||
js = [
|
||||
'wagtailsettings/js/site-switcher.js',
|
||||
]
|
||||
|
||||
def __init__(self, current_site, model, **kwargs):
|
||||
initial_data = {'site': self.get_change_url(current_site, model)}
|
||||
super(SiteSwitchForm, self).__init__(initial=initial_data, **kwargs)
|
||||
sites = [(self.get_change_url(site, model), site)
|
||||
for site in Site.objects.all()]
|
||||
self.fields['site'].choices = sites
|
||||
|
||||
@classmethod
|
||||
def get_change_url(cls, site, model):
|
||||
return reverse('wagtailsettings:edit', args=[
|
||||
site.pk, model._meta.app_label, model._meta.model_name])
|
25
wagtail/contrib/settings/models.py
Normal file
25
wagtail/contrib/settings/models.py
Normal file
@ -0,0 +1,25 @@
|
||||
from django.db import models
|
||||
from .registry import register_setting
|
||||
|
||||
__all__ = ['BaseSetting', 'register_setting']
|
||||
|
||||
|
||||
class BaseSetting(models.Model):
|
||||
"""
|
||||
The abstract base model for settings. Subclasses must be registered using
|
||||
:func:`~wagtail.contrib.settings.registry.register_setting`
|
||||
"""
|
||||
|
||||
site = models.OneToOneField(
|
||||
'wagtailcore.Site', unique=True, db_index=True, editable=False)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@classmethod
|
||||
def for_site(cls, site):
|
||||
"""
|
||||
Get an instance of this setting for the site.
|
||||
"""
|
||||
instance, created = cls.objects.get_or_create(site=site)
|
||||
return instance
|
4
wagtail/contrib/settings/permissions.py
Normal file
4
wagtail/contrib/settings/permissions.py
Normal file
@ -0,0 +1,4 @@
|
||||
def user_can_edit_setting_type(user, model):
|
||||
""" Check if a user has permission to edit this setting type """
|
||||
return user.has_perm("{}.change_{}".format(
|
||||
model._meta.app_label, model._meta.model_name))
|
81
wagtail/contrib/settings/registry.py
Normal file
81
wagtail/contrib/settings/registry.py
Normal file
@ -0,0 +1,81 @@
|
||||
from django.apps import apps
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.text import capfirst
|
||||
|
||||
from wagtail.wagtailadmin.menu import MenuItem
|
||||
from wagtail.wagtailcore import hooks
|
||||
from .permissions import user_can_edit_setting_type
|
||||
|
||||
|
||||
class SettingMenuItem(MenuItem):
|
||||
def __init__(self, model, icon='cog', classnames='', **kwargs):
|
||||
|
||||
icon_classes = 'icon icon-' + icon
|
||||
if classnames:
|
||||
classnames += ' ' + icon_classes
|
||||
else:
|
||||
classnames = icon_classes
|
||||
|
||||
self.model = model
|
||||
super(SettingMenuItem, self).__init__(
|
||||
label=capfirst(model._meta.verbose_name),
|
||||
url=reverse('wagtailsettings:edit', args=[
|
||||
model._meta.app_label, model._meta.model_name]),
|
||||
classnames=classnames,
|
||||
**kwargs)
|
||||
|
||||
def is_shown(self, request):
|
||||
return user_can_edit_setting_type(request.user, self.model)
|
||||
|
||||
|
||||
class Registry(list):
|
||||
|
||||
def register(self, model, **kwargs):
|
||||
"""
|
||||
Register a model as a setting, adding it to the wagtail admin menu
|
||||
"""
|
||||
|
||||
# Don't bother registering this if it is already registered
|
||||
if model in self:
|
||||
return model
|
||||
self.append(model)
|
||||
|
||||
# Register a new menu item in the settings menu
|
||||
@hooks.register('register_settings_menu_item')
|
||||
def menu_hook():
|
||||
return SettingMenuItem(model, **kwargs)
|
||||
|
||||
@hooks.register('register_permissions')
|
||||
def permissions_hook():
|
||||
return Permission.objects.filter(
|
||||
content_type__app_label=model._meta.app_label,
|
||||
codename='change_{}'.format(model._meta.model_name))
|
||||
|
||||
return model
|
||||
|
||||
def register_decorator(self, model=None, **kwargs):
|
||||
"""
|
||||
Register a model as a setting in the Wagtail admin
|
||||
"""
|
||||
if model is None:
|
||||
return lambda model: self.register(model, **kwargs)
|
||||
return self.register(model, **kwargs)
|
||||
|
||||
def get_by_natural_key(self, app_label, model_name):
|
||||
"""
|
||||
Get a setting model using its app_label and model_name.
|
||||
|
||||
If the app_label.model_name combination is not a valid model, or the
|
||||
model is not registered as a setting, returns None.
|
||||
"""
|
||||
try:
|
||||
Model = apps.get_model(app_label, model_name)
|
||||
except LookupError:
|
||||
return None
|
||||
if Model not in registry:
|
||||
return None
|
||||
return Model
|
||||
|
||||
registry = Registry()
|
||||
register_setting = registry.register_decorator
|
@ -0,0 +1,12 @@
|
||||
$(function() {
|
||||
var $switcher = $('form#settings-site-switch select');
|
||||
if (!$switcher.length) return;
|
||||
|
||||
var initial = $switcher.val();
|
||||
$switcher.on('change', function() {
|
||||
var url = $switcher.val();
|
||||
if (url != initial) {
|
||||
window.location = url;
|
||||
}
|
||||
});
|
||||
});
|
53
wagtail/contrib/settings/templates/wagtailsettings/edit.html
Normal file
53
wagtail/contrib/settings/templates/wagtailsettings/edit.html
Normal file
@ -0,0 +1,53 @@
|
||||
{% extends "wagtailadmin/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block titletag %}{% blocktrans %}Editing {{ setting_type_name}} - {{ instance }}{% endblocktrans %}{% endblock %}
|
||||
{% block bodyclass %}menu-settings{% endblock %}
|
||||
{% block content %}
|
||||
<header class="nice-padding">
|
||||
<div class="row">
|
||||
<div class="left">
|
||||
<div class="col">
|
||||
<h1 class="icon icon-cogs">
|
||||
{% trans "Editing" %}
|
||||
<span>{{ setting_type_name|capfirst }}</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
{% if site_switcher %}
|
||||
<form method="get" id="settings-site-switch">
|
||||
<label for="{{ site_switcher.site.id_for_label }}">
|
||||
Site:
|
||||
</label>
|
||||
{{ site_switcher.site }}
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<form action="{% url 'wagtailsettings:edit' site.pk opts.app_label opts.model_name %}" method="POST">
|
||||
{% csrf_token %}
|
||||
{{ edit_handler.render_form_content }}
|
||||
|
||||
<footer>
|
||||
<ul>
|
||||
<li class="actions">
|
||||
<input type="submit" value="{% trans 'Save' %}" class="button" />
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
{% include "wagtailadmin/pages/_editor_css.html" %}
|
||||
{{ form.media.css }}
|
||||
{{ site_switcher.media.css }}
|
||||
{% endblock %}
|
||||
{% block extra_js %}
|
||||
{% include "wagtailadmin/pages/_editor_js.html" %}
|
||||
{{ form.media.js }}
|
||||
{{ site_switcher.media.js }}
|
||||
{% endblock %}
|
@ -0,0 +1,25 @@
|
||||
{% extends "wagtailadmin/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block titletag %}{% trans "Settings" %}{% endblock %}
|
||||
{% block bodyclass %}menu-settings{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{% include "wagtailadmin/shared/header.html" with title="Settings" icon="cogs" %}
|
||||
|
||||
<div class="nice-padding">
|
||||
<ul class="listing">
|
||||
{% for name, description, content_type in setting_types %}
|
||||
<li>
|
||||
<div class="row row-flush title">
|
||||
<h2>
|
||||
<a href="{% url 'wagtailsettings_edit' content_type.app_label content_type.model %}" class="col6">
|
||||
{{ name|capfirst }}
|
||||
</a>
|
||||
</h2>
|
||||
<small class="col6">{{ description }}</small>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
0
wagtail/contrib/settings/templatetags/__init__.py
Normal file
0
wagtail/contrib/settings/templatetags/__init__.py
Normal file
@ -0,0 +1,21 @@
|
||||
from django.template import Library
|
||||
|
||||
from wagtail.wagtailcore.models import Site
|
||||
|
||||
from ..context_processors import SettingsProxy
|
||||
|
||||
register = Library()
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def get_settings(context, use_default_site=False):
|
||||
if use_default_site:
|
||||
site = Site.objects.get(is_default_site=True)
|
||||
elif 'request' in context:
|
||||
site = context['request'].site
|
||||
else:
|
||||
raise RuntimeError('No request found in context, and use_default_site '
|
||||
'flag not set')
|
||||
|
||||
context['settings'] = SettingsProxy(site)
|
||||
return ''
|
0
wagtail/contrib/settings/tests/__init__.py
Normal file
0
wagtail/contrib/settings/tests/__init__.py
Normal file
206
wagtail/contrib/settings/tests/test_admin.py
Normal file
206
wagtail/contrib/settings/tests/test_admin.py
Normal file
@ -0,0 +1,206 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from django.utils.text import capfirst
|
||||
|
||||
from wagtail.contrib.settings.registry import SettingMenuItem
|
||||
from wagtail.tests.testapp.models import IconSetting, TestSetting
|
||||
from wagtail.tests.utils import WagtailTestUtils
|
||||
from wagtail.wagtailcore import hooks
|
||||
from wagtail.wagtailcore.models import Page, Site
|
||||
|
||||
|
||||
class TestSettingMenu(TestCase, WagtailTestUtils):
|
||||
|
||||
def login_only_admin(self):
|
||||
""" Log in with a user that only has permission to access the admin """
|
||||
user = get_user_model().objects.create_user(
|
||||
username='test', email='test@email.com', password='password')
|
||||
user.user_permissions.add(Permission.objects.get_by_natural_key(
|
||||
codename='access_admin', app_label='wagtailadmin', model='admin'))
|
||||
self.client.login(username='test', password='password')
|
||||
return user
|
||||
|
||||
def test_menu_item_in_admin(self):
|
||||
self.login()
|
||||
response = self.client.get(reverse('wagtailadmin_home'))
|
||||
|
||||
self.assertContains(response, capfirst(TestSetting._meta.verbose_name))
|
||||
self.assertContains(response, reverse('wagtailsettings:edit', args=('tests', 'testsetting')))
|
||||
|
||||
def test_menu_item_no_permissions(self):
|
||||
self.login_only_admin()
|
||||
response = self.client.get(reverse('wagtailadmin_home'))
|
||||
|
||||
self.assertNotContains(response, TestSetting._meta.verbose_name)
|
||||
self.assertNotContains(response, reverse('wagtailsettings:edit', args=('tests', 'testsetting')))
|
||||
|
||||
def test_menu_item_icon(self):
|
||||
menu_item = SettingMenuItem(IconSetting, icon='tag', classnames='test-class')
|
||||
classnames = set(menu_item.classnames.split(' '))
|
||||
self.assertEqual(classnames, {'icon', 'icon-tag', 'test-class'})
|
||||
|
||||
|
||||
class BaseTestSettingView(TestCase, WagtailTestUtils):
|
||||
def get(self, site_pk=1, params={}):
|
||||
url = self.edit_url('tests', 'testsetting', site_pk=site_pk)
|
||||
return self.client.get(url, params)
|
||||
|
||||
def post(self, site_pk=1, post_data={}):
|
||||
url = self.edit_url('tests', 'testsetting', site_pk=site_pk)
|
||||
return self.client.post(url, post_data)
|
||||
|
||||
def edit_url(self, app, model, site_pk=1):
|
||||
return reverse('wagtailsettings:edit', args=[site_pk, app, model])
|
||||
|
||||
|
||||
class TestSettingCreateView(BaseTestSettingView):
|
||||
def setUp(self):
|
||||
self.login()
|
||||
|
||||
def test_status_code(self):
|
||||
self.assertEqual(self.get().status_code, 200)
|
||||
|
||||
def test_edit_invalid(self):
|
||||
response = self.post(post_data={'foo': 'bar'})
|
||||
self.assertContains(response, "The setting could not be saved due to errors.")
|
||||
self.assertContains(response, "This field is required.")
|
||||
|
||||
def test_edit(self):
|
||||
response = self.post(post_data={'title': 'Edited site title',
|
||||
'email': 'test@example.com'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
default_site = Site.objects.get(is_default_site=True)
|
||||
setting = TestSetting.objects.get(site=default_site)
|
||||
self.assertEqual(setting.title, 'Edited site title')
|
||||
self.assertEqual(setting.email, 'test@example.com')
|
||||
|
||||
|
||||
class TestSettingEditView(BaseTestSettingView):
|
||||
def setUp(self):
|
||||
default_site = Site.objects.get(is_default_site=True)
|
||||
|
||||
self.test_setting = TestSetting()
|
||||
self.test_setting.title = 'Site title'
|
||||
self.test_setting.email = 'initial@example.com'
|
||||
self.test_setting.site = default_site
|
||||
self.test_setting.save()
|
||||
|
||||
self.login()
|
||||
|
||||
def test_status_code(self):
|
||||
self.assertEqual(self.get().status_code, 200)
|
||||
|
||||
def test_non_existant_model(self):
|
||||
response = self.client.get(self.edit_url('test', 'foo'))
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_edit_invalid(self):
|
||||
response = self.post(post_data={'foo': 'bar'})
|
||||
self.assertContains(response, "The setting could not be saved due to errors.")
|
||||
self.assertContains(response, "This field is required.")
|
||||
|
||||
def test_edit(self):
|
||||
response = self.post(post_data={'title': 'Edited site title',
|
||||
'email': 'test@example.com'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
default_site = Site.objects.get(is_default_site=True)
|
||||
setting = TestSetting.objects.get(site=default_site)
|
||||
self.assertEqual(setting.title, 'Edited site title')
|
||||
self.assertEqual(setting.email, 'test@example.com')
|
||||
|
||||
|
||||
class TestMultiSite(BaseTestSettingView):
|
||||
def setUp(self):
|
||||
self.default_site = Site.objects.get(is_default_site=True)
|
||||
self.other_site = Site.objects.create(hostname='example.com', root_page=Page.objects.get(pk=2))
|
||||
self.login()
|
||||
|
||||
def test_redirect_to_default(self):
|
||||
"""
|
||||
Should redirect to the setting for the default site.
|
||||
"""
|
||||
start_url = reverse('wagtailsettings:edit', args=[
|
||||
'tests', 'testsetting'])
|
||||
dest_url = 'http://testserver' + reverse('wagtailsettings:edit', args=[
|
||||
self.default_site.pk, 'tests', 'testsetting'])
|
||||
response = self.client.get(start_url, follow=True)
|
||||
self.assertEqual([(dest_url, 302)], response.redirect_chain)
|
||||
|
||||
def test_redirect_to_current(self):
|
||||
"""
|
||||
Should redirect to the setting for the current site taken from the URL,
|
||||
by default
|
||||
"""
|
||||
start_url = reverse('wagtailsettings:edit', args=[
|
||||
'tests', 'testsetting'])
|
||||
dest_url = 'http://example.com' + reverse('wagtailsettings:edit', args=[
|
||||
self.other_site.pk, 'tests', 'testsetting'])
|
||||
response = self.client.get(start_url, follow=True, HTTP_HOST=self.other_site.hostname)
|
||||
self.assertEqual([(dest_url, 302)], response.redirect_chain)
|
||||
|
||||
def test_with_no_current_site(self):
|
||||
"""
|
||||
Redirection should not break if the current request does not correspond to a site
|
||||
"""
|
||||
self.default_site.is_default_site = False
|
||||
self.default_site.save()
|
||||
|
||||
start_url = reverse('wagtailsettings:edit', args=[
|
||||
'tests', 'testsetting'])
|
||||
response = self.client.get(start_url, follow=True, HTTP_HOST="noneoftheabove.example.com")
|
||||
self.assertEqual(302, response.redirect_chain[0][1])
|
||||
|
||||
def test_switcher(self):
|
||||
""" Check that the switcher form exists in the page """
|
||||
response = self.get()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'id="settings-site-switch"')
|
||||
|
||||
def test_unknown_site(self):
|
||||
""" Check that unknown sites throw a 404 """
|
||||
response = self.get(site_pk=3)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_edit(self):
|
||||
"""
|
||||
Check that editing settings in multi-site mode edits the correct
|
||||
setting, and leaves the other ones alone
|
||||
"""
|
||||
TestSetting.objects.create(
|
||||
title='default',
|
||||
email='default@example.com',
|
||||
site=self.default_site)
|
||||
TestSetting.objects.create(
|
||||
title='other',
|
||||
email='other@example.com',
|
||||
site=self.other_site)
|
||||
response = self.post(site_pk=self.other_site.pk, post_data={
|
||||
'title': 'other-new', 'email': 'other-other@example.com'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
# Check that the correct setting was updated
|
||||
other_setting = TestSetting.for_site(self.other_site)
|
||||
self.assertEqual(other_setting.title, 'other-new')
|
||||
self.assertEqual(other_setting.email, 'other-other@example.com')
|
||||
|
||||
# Check that the other setting was not updated
|
||||
default_setting = TestSetting.for_site(self.default_site)
|
||||
self.assertEqual(default_setting.title, 'default')
|
||||
self.assertEqual(default_setting.email, 'default@example.com')
|
||||
|
||||
|
||||
class TestAdminPermission(TestCase, WagtailTestUtils):
|
||||
def test_registered_permission(self):
|
||||
permission = Permission.objects.get_by_natural_key(
|
||||
app_label='tests', model='testsetting', codename='change_testsetting')
|
||||
for fn in hooks.get_hooks('register_permissions'):
|
||||
if permission in fn():
|
||||
break
|
||||
else:
|
||||
self.fail('Change permission for tests.TestSetting not registered')
|
22
wagtail/contrib/settings/tests/test_register.py
Normal file
22
wagtail/contrib/settings/tests/test_register.py
Normal file
@ -0,0 +1,22 @@
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
|
||||
from wagtail.contrib.settings.registry import Registry
|
||||
from wagtail.tests.testapp.models import NotYetRegisteredSetting
|
||||
from wagtail.tests.utils import WagtailTestUtils
|
||||
|
||||
|
||||
class TestRegister(TestCase, WagtailTestUtils):
|
||||
def setUp(self):
|
||||
self.registry = Registry()
|
||||
self.login()
|
||||
|
||||
def test_register(self):
|
||||
self.assertNotIn(NotYetRegisteredSetting, self.registry)
|
||||
NowRegisteredSetting = self.registry.register_decorator(NotYetRegisteredSetting)
|
||||
self.assertIn(NotYetRegisteredSetting, self.registry)
|
||||
self.assertIs(NowRegisteredSetting, NotYetRegisteredSetting)
|
||||
|
||||
def test_icon(self):
|
||||
admin = self.client.get(reverse('wagtailadmin_home'))
|
||||
self.assertContains(admin, 'icon icon-tag')
|
153
wagtail/contrib/settings/tests/test_templates.py
Normal file
153
wagtail/contrib/settings/tests/test_templates.py
Normal file
@ -0,0 +1,153 @@
|
||||
from django.template import Context, RequestContext, Template
|
||||
from django.test import TestCase
|
||||
|
||||
from wagtail.tests.testapp.models import TestSetting
|
||||
from wagtail.tests.utils import WagtailTestUtils
|
||||
from wagtail.wagtailcore.models import Page, Site
|
||||
|
||||
|
||||
class TemplateTestCase(TestCase, WagtailTestUtils):
|
||||
def setUp(self):
|
||||
root = Page.objects.first()
|
||||
other_home = Page(title='Other Root', slug='other')
|
||||
root.add_child(instance=other_home)
|
||||
|
||||
self.default_site = Site.objects.get(is_default_site=True)
|
||||
self.other_site = Site.objects.create(hostname='other', root_page=other_home)
|
||||
|
||||
self.test_setting = TestSetting.objects.create(
|
||||
title='Site title',
|
||||
email='initial@example.com',
|
||||
site=self.default_site)
|
||||
|
||||
self.other_setting = TestSetting.objects.create(
|
||||
title='Other title',
|
||||
email='other@example.com',
|
||||
site=self.other_site)
|
||||
|
||||
def get_request(self, site=None):
|
||||
if site is None:
|
||||
site = self.default_site
|
||||
request = self.client.get('/test/', HTTP_HOST=site.hostname)
|
||||
request.site = site
|
||||
return request
|
||||
|
||||
def render(self, request, string, context=None, site=None):
|
||||
template = Template(string)
|
||||
context = RequestContext(request, context)
|
||||
return template.render(context)
|
||||
|
||||
|
||||
class TestContextProcessor(TemplateTestCase):
|
||||
|
||||
def test_accessing_setting(self):
|
||||
""" Check that the context processor works """
|
||||
request = self.get_request()
|
||||
self.assertEqual(
|
||||
self.render(request, '{{ settings.tests.TestSetting.title }}'),
|
||||
self.test_setting.title)
|
||||
|
||||
def test_multisite(self):
|
||||
""" Check that the correct setting for the current site is returned """
|
||||
request = self.get_request(site=self.default_site)
|
||||
self.assertEqual(
|
||||
self.render(request, '{{ settings.tests.TestSetting.title }}'),
|
||||
self.test_setting.title)
|
||||
|
||||
request = self.get_request(site=self.other_site)
|
||||
self.assertEqual(
|
||||
self.render(request, '{{ settings.tests.TestSetting.title }}'),
|
||||
self.other_setting.title)
|
||||
|
||||
def test_model_case_insensitive(self):
|
||||
""" Model names should be case insensitive """
|
||||
request = self.get_request()
|
||||
self.assertEqual(
|
||||
self.render(request, '{{ settings.tests.testsetting.title }}'),
|
||||
self.test_setting.title)
|
||||
self.assertEqual(
|
||||
self.render(request, '{{ settings.tests.TESTSETTING.title }}'),
|
||||
self.test_setting.title)
|
||||
self.assertEqual(
|
||||
self.render(request, '{{ settings.tests.TestSetting.title }}'),
|
||||
self.test_setting.title)
|
||||
self.assertEqual(
|
||||
self.render(request, '{{ settings.tests.tEstsEttIng.title }}'),
|
||||
self.test_setting.title)
|
||||
|
||||
def test_models_cached(self):
|
||||
""" Accessing a setting should only hit the DB once per render """
|
||||
request = self.get_request()
|
||||
get_title = '{{ settings.tests.testsetting.title }}'
|
||||
|
||||
for i in range(1, 4):
|
||||
with self.assertNumQueries(1):
|
||||
self.assertEqual(
|
||||
self.render(request, get_title * i),
|
||||
self.test_setting.title * i)
|
||||
|
||||
|
||||
class TestTemplateTag(TemplateTestCase):
|
||||
def test_no_context_processor(self):
|
||||
"""
|
||||
Assert that not running the context processor means settings are not in
|
||||
the context, as expected.
|
||||
"""
|
||||
template = Template('{{ settings.tests.TestSetting.title }}')
|
||||
context = Context()
|
||||
self.assertEqual(template.render(context), '')
|
||||
|
||||
def test_get_settings_request_context(self):
|
||||
""" Check that the {% get_settings %} tag works """
|
||||
request = self.get_request(site=self.other_site)
|
||||
context = Context({'request': request})
|
||||
|
||||
# This should use the site in the request
|
||||
template = Template('{% load wagtailsettings_tags %}'
|
||||
'{% get_settings %}'
|
||||
'{{ settings.tests.testsetting.title}}')
|
||||
|
||||
self.assertEqual(template.render(context), self.other_setting.title)
|
||||
|
||||
def test_get_settings_request_context_use_default(self):
|
||||
"""
|
||||
Check that the {% get_settings use_default_site=True %} option
|
||||
overrides a request in the context.
|
||||
"""
|
||||
request = self.get_request(site=self.other_site)
|
||||
context = Context({'request': request})
|
||||
|
||||
# This should use the default site, ignoring the site in the request
|
||||
template = Template('{% load wagtailsettings_tags %}'
|
||||
'{% get_settings use_default_site=True %}'
|
||||
'{{ settings.tests.testsetting.title}}')
|
||||
|
||||
self.assertEqual(template.render(context), self.test_setting.title)
|
||||
|
||||
def test_get_settings_use_default(self):
|
||||
"""
|
||||
Check that the {% get_settings use_default_site=True %} option works
|
||||
"""
|
||||
context = Context()
|
||||
|
||||
# This should use the default site
|
||||
template = Template('{% load wagtailsettings_tags %}'
|
||||
'{% get_settings use_default_site=True %}'
|
||||
'{{ settings.tests.testsetting.title}}')
|
||||
|
||||
self.assertEqual(template.render(context), self.test_setting.title)
|
||||
|
||||
def test_get_settings_no_request_no_default(self):
|
||||
"""
|
||||
Check that the {% get_settings %} throws an error if it can not find a
|
||||
site to work with
|
||||
"""
|
||||
context = Context()
|
||||
|
||||
# Without a request in the context, and without use_default_site, this
|
||||
# should bail with an error
|
||||
template = Template('{% load wagtailsettings_tags %}'
|
||||
'{% get_settings %}'
|
||||
'{{ settings.tests.testsetting.title}}')
|
||||
with self.assertRaises(RuntimeError):
|
||||
template.render(context)
|
8
wagtail/contrib/settings/urls.py
Normal file
8
wagtail/contrib/settings/urls.py
Normal file
@ -0,0 +1,8 @@
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^(\w+)/(\w+)/$', views.edit_current_site, name='edit'),
|
||||
url(r'^(\d+)/(\w+)/(\w+)/$', views.edit, name='edit'),
|
||||
]
|
87
wagtail/contrib/settings/views.py
Normal file
87
wagtail/contrib/settings/views.py
Normal file
@ -0,0 +1,87 @@
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404
|
||||
from django.shortcuts import redirect, render, get_object_or_404
|
||||
from django.utils.lru_cache import lru_cache
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from wagtail.wagtailadmin import messages
|
||||
from wagtail.wagtailadmin.edit_handlers import (
|
||||
ObjectList, extract_panel_definitions_from_model_class)
|
||||
from wagtail.wagtailcore.models import Site
|
||||
|
||||
from .forms import SiteSwitchForm
|
||||
from .permissions import user_can_edit_setting_type
|
||||
from .registry import registry
|
||||
|
||||
|
||||
def get_model_from_url_params(app_name, model_name):
|
||||
"""
|
||||
retrieve a content type from an app_name / model_name combo.
|
||||
Throw Http404 if not a valid setting type
|
||||
"""
|
||||
model = registry.get_by_natural_key(app_name, model_name)
|
||||
if model is None:
|
||||
raise Http404
|
||||
return model
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def get_setting_edit_handler(model):
|
||||
panels = extract_panel_definitions_from_model_class(model, ['site'])
|
||||
return ObjectList(panels).bind_to_model(model)
|
||||
|
||||
|
||||
def edit_current_site(request, app_name, model_name):
|
||||
# Redirect the user to the edit page for the current site
|
||||
# (or the current request does not correspond to a site, the first site in the list)
|
||||
site = request.site or Site.objects.first()
|
||||
return redirect('wagtailsettings:edit', site.pk, app_name, model_name)
|
||||
|
||||
|
||||
def edit(request, site_pk, app_name, model_name):
|
||||
model = get_model_from_url_params(app_name, model_name)
|
||||
if not user_can_edit_setting_type(request.user, model):
|
||||
raise PermissionDenied
|
||||
site = get_object_or_404(Site, pk=site_pk)
|
||||
|
||||
setting_type_name = model._meta.verbose_name
|
||||
|
||||
instance = model.for_site(site)
|
||||
edit_handler_class = get_setting_edit_handler(model)
|
||||
form_class = edit_handler_class.get_form_class(model)
|
||||
|
||||
if request.POST:
|
||||
form = form_class(request.POST, request.FILES, instance=instance)
|
||||
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
_("{setting_type} updated.").format(
|
||||
setting_type=capfirst(setting_type_name),
|
||||
instance=instance
|
||||
)
|
||||
)
|
||||
return redirect('wagtailsettings:edit', site.pk, app_name, model_name)
|
||||
else:
|
||||
messages.error(request, _("The setting could not be saved due to errors."))
|
||||
edit_handler = edit_handler_class(instance=instance, form=form)
|
||||
else:
|
||||
form = form_class(instance=instance)
|
||||
edit_handler = edit_handler_class(instance=instance, form=form)
|
||||
|
||||
# Show a site switcher form if there are multiple sites
|
||||
site_switcher = None
|
||||
if Site.objects.count() > 1:
|
||||
site_switcher = SiteSwitchForm(site, model)
|
||||
|
||||
return render(request, 'wagtailsettings/edit.html', {
|
||||
'opts': model._meta,
|
||||
'setting_type_name': setting_type_name,
|
||||
'instance': instance,
|
||||
'edit_handler': edit_handler,
|
||||
'site': site,
|
||||
'site_switcher': site_switcher,
|
||||
})
|
12
wagtail/contrib/settings/wagtail_hooks.py
Normal file
12
wagtail/contrib/settings/wagtail_hooks.py
Normal file
@ -0,0 +1,12 @@
|
||||
from django.conf.urls import include, url
|
||||
|
||||
from wagtail.wagtailcore import hooks
|
||||
|
||||
from . import urls
|
||||
|
||||
|
||||
@hooks.register('register_admin_urls')
|
||||
def register_admin_urls():
|
||||
return [
|
||||
url(r'^settings/', include(urls, namespace='wagtailsettings')),
|
||||
]
|
@ -53,6 +53,7 @@ if django.VERSION >= (1, 8):
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'django.template.context_processors.request',
|
||||
'wagtail.tests.context_processors.do_not_use_static_url',
|
||||
'wagtail.contrib.settings.context_processors.settings',
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -72,6 +73,7 @@ else:
|
||||
TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + (
|
||||
'django.core.context_processors.request',
|
||||
'wagtail.tests.context_processors.do_not_use_static_url',
|
||||
'wagtail.contrib.settings.context_processors.settings',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
@ -106,6 +108,7 @@ INSTALLED_APPS = (
|
||||
'wagtail.contrib.wagtailfrontendcache',
|
||||
'wagtail.contrib.wagtailapi',
|
||||
'wagtail.contrib.wagtailsearchpromotions',
|
||||
'wagtail.contrib.settings',
|
||||
'wagtail.wagtailforms',
|
||||
'wagtail.wagtailsearch',
|
||||
'wagtail.wagtailembeds',
|
||||
|
@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0019_verbose_names_cleanup'),
|
||||
('tests', '0012_filepage'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='IconSetting',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('site', models.OneToOneField(editable=False, to='wagtailcore.Site')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='NotYetRegisteredSetting',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('site', models.OneToOneField(editable=False, to='wagtailcore.Site')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TestSetting',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('title', models.CharField(max_length=100)),
|
||||
('email', models.EmailField(max_length=50)),
|
||||
('site', models.OneToOneField(editable=False, to='wagtailcore.Site')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
@ -11,6 +11,7 @@ from modelcluster.fields import ParentalKey
|
||||
from modelcluster.models import ClusterableModel
|
||||
from modelcluster.contrib.taggit import ClusterTaggableManager
|
||||
|
||||
from wagtail.contrib.settings.models import BaseSetting, register_setting
|
||||
from wagtail.wagtailcore.models import Page, Orderable
|
||||
from wagtail.wagtailcore.fields import RichTextField, StreamField
|
||||
from wagtail.wagtailcore.blocks import CharBlock, RichTextBlock
|
||||
@ -319,7 +320,6 @@ FormPage.content_panels = [
|
||||
]
|
||||
|
||||
|
||||
|
||||
# Snippets
|
||||
class AdvertPlacement(models.Model):
|
||||
page = ParentalKey('wagtailcore.Page', related_name='advert_placements')
|
||||
@ -378,6 +378,7 @@ StandardChild.edit_handler = TabbedInterface([
|
||||
ObjectList([], heading='Dinosaurs'),
|
||||
])
|
||||
|
||||
|
||||
class BusinessIndex(Page):
|
||||
""" Can be placed anywhere, can only have Business children """
|
||||
subpage_types = ['tests.BusinessChild', 'tests.BusinessSubIndex']
|
||||
@ -411,9 +412,11 @@ TaggedPage.content_panels = [
|
||||
class PageChooserModel(models.Model):
|
||||
page = models.ForeignKey('wagtailcore.Page', help_text='help text')
|
||||
|
||||
|
||||
class EventPageChooserModel(models.Model):
|
||||
page = models.ForeignKey('tests.EventPage', help_text='more help text')
|
||||
|
||||
|
||||
class SnippetChooserModel(models.Model):
|
||||
advert = models.ForeignKey(Advert, help_text='help text')
|
||||
|
||||
@ -461,3 +464,18 @@ class MTIChildPage(MTIBasePage):
|
||||
class AbstractPage(Page):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
@register_setting
|
||||
class TestSetting(BaseSetting):
|
||||
title = models.CharField(max_length=100)
|
||||
email = models.EmailField(max_length=50)
|
||||
|
||||
|
||||
@register_setting(icon="tag")
|
||||
class IconSetting(BaseSetting):
|
||||
pass
|
||||
|
||||
|
||||
class NotYetRegisteredSetting(BaseSetting):
|
||||
pass
|
||||
|
Loading…
Reference in New Issue
Block a user