Merge branch 'jossingram-snippet_chooser_panel'

This commit is contained in:
Matt Westcott 2015-08-28 10:59:39 +01:00
commit 8c8606b5c7
7 changed files with 86 additions and 18 deletions

View file

@ -26,6 +26,7 @@ Changelog
* Added database indexes on PageRevision and Image to improve performance on large sites
* Search in page chooser now uses Wagtail's search framework, to order results by relevance
* `PageChooserPanel` now supports passing a list (or tuple) of accepted page types
* The snippet type parameter of `SnippetChooserPanel` can now be omitted, or passed as a model name string rather than a model class
* Fix: Text areas in the non-default tab of the page editor now resize to the correct height
* Fix: Tabs in "insert link" modal in the rich text editor no longer disappear (Tim Heap)
* Fix: H2 elements in rich text fields were accidentally given a click() binding when put insite a collapsible multi field panel

View file

@ -23,7 +23,7 @@ As standard, Wagtail organises panels into three tabs: 'Content', 'Promote' and
FieldPanel('body', classname="full"),
]
sidebar_content_panels = [
SnippetChooserPanel('advert', Advert),
SnippetChooserPanel('advert'),
InlinePanel('related_links', label="Related links"),
]

View file

@ -233,9 +233,13 @@ DocumentChooserPanel
SnippetChooserPanel
-------------------
.. class:: wagtail.wagtailsnippets.edit_handlers.SnippetChooserPanel(field_name, model)
.. versionchanged:: 1.1
Snippets are vanilla Django models you create yourself without a Wagtail-provided base class. So using them as a field in a page requires specifying your own ``appname.modelname``. A chooser, ``SnippetChooserPanel``, is provided which takes the field name and snippet class.
Before Wagtail 1.1, it was necessary to pass the snippet model class as a second parameter to ``SnippetChooserPanel``. This is now automatically picked up from the field.
.. class:: wagtail.wagtailsnippets.edit_handlers.SnippetChooserPanel(field_name, snippet_type=None)
Snippets are vanilla Django models you create yourself without a Wagtail-provided base class. A chooser, ``SnippetChooserPanel``, is provided which takes the field name as an argument.
.. code-block:: python
@ -251,7 +255,7 @@ SnippetChooserPanel
)
content_panels = Page.content_panels + [
SnippetChooserPanel('advert', Advert),
SnippetChooserPanel('advert'),
]
See :ref:`snippets` for more information.

View file

@ -63,6 +63,7 @@ Minor features
* Added database indexes on PageRevision and Image to improve performance on large sites
* Search in page chooser now uses Wagtail's search framework, to order results by relevance
* ``PageChooserPanel`` now supports passing a list (or tuple) of accepted page types
* The snippet type parameter of ``SnippetChooserPanel`` can now be omitted, or passed as a model name string rather than a model class
Bug fixes
~~~~~~~~~

View file

@ -112,7 +112,7 @@ In the above example, the list of adverts is a fixed list, displayed as part of
BookPage.content_panels = [
SnippetChooserPanel('advert', Advert),
SnippetChooserPanel('advert'),
# ...
]
@ -142,7 +142,7 @@ To attach multiple adverts to a page, the ``SnippetChooserPanel`` can be placed
verbose_name_plural = "Advert Placements"
panels = [
SnippetChooserPanel('advert', Advert),
SnippetChooserPanel('advert'),
]
def __str__(self): # __unicode__ on Python 2

View file

@ -4,40 +4,62 @@ from django.template.loader import render_to_string
from django.contrib.contenttypes.models import ContentType
from django.utils.safestring import mark_safe
from django.utils.encoding import force_text
from django.core.exceptions import ImproperlyConfigured
from wagtail.wagtailadmin.edit_handlers import BaseChooserPanel
from wagtail.wagtailcore.utils import resolve_model_string
from .widgets import AdminSnippetChooser
class BaseSnippetChooserPanel(BaseChooserPanel):
object_type_name = 'item'
_content_type = None
_target_model = None
_target_content_type = None
@classmethod
def widget_overrides(cls):
return {cls.field_name: AdminSnippetChooser(
content_type=cls.content_type(), snippet_type_name=cls.snippet_type_name)}
content_type=cls.target_content_type(), snippet_type_name=cls.get_snippet_type_name())}
@classmethod
def content_type(cls):
if cls._content_type is None:
# TODO: infer the content type by introspection on the foreign key rather than having to pass it explicitly
cls._content_type = ContentType.objects.get_for_model(cls.snippet_type)
def target_model(cls):
if cls._target_model is None:
if cls.snippet_type:
try:
cls._target_model = resolve_model_string(cls.snippet_type)
except LookupError:
raise ImproperlyConfigured("{0}.snippet_type must be of the form 'app_label.model_name', given {1!r}".format(
cls.__name__, cls.snippet_type))
except ValueError:
raise ImproperlyConfigured("{0}.snippet_type refers to model {1!r} that has not been installed".format(
cls.__name__, cls.snippet_type))
else:
cls._target_model = cls.model._meta.get_field(cls.field_name).rel.to
return cls._content_type
return cls._target_model
@classmethod
def target_content_type(cls):
if cls._target_content_type is None:
cls._target_content_type = ContentType.objects.get_for_model(cls.target_model())
return cls._target_content_type
def render_as_field(self):
instance_obj = self.get_chosen_item()
return mark_safe(render_to_string(self.field_template, {
'field': self.bound_field,
self.object_type_name: instance_obj,
'snippet_type_name': self.snippet_type_name,
'snippet_type_name': self.get_snippet_type_name(),
}))
@classmethod
def get_snippet_type_name(cls):
return force_text(cls.target_model()._meta.verbose_name)
class SnippetChooserPanel(object):
def __init__(self, field_name, snippet_type):
def __init__(self, field_name, snippet_type=None):
self.field_name = field_name
self.snippet_type = snippet_type
@ -45,6 +67,5 @@ class SnippetChooserPanel(object):
return type(str('_SnippetChooserPanel'), (BaseSnippetChooserPanel,), {
'model': model,
'field_name': self.field_name,
'snippet_type_name': force_text(self.snippet_type._meta.verbose_name),
'snippet_type': self.snippet_type,
})

View file

@ -3,11 +3,13 @@ from django.core.urlresolvers import reverse
from django.test.utils import override_settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.core.exceptions import ImproperlyConfigured
from wagtail.tests.utils import WagtailTestUtils
from wagtail.tests.testapp.models import Advert, SnippetChooserModel
from wagtail.tests.snippets.models import AlphaSnippet, ZuluSnippet, RegisterDecorator, RegisterFunction, SearchableSnippet
from wagtail.wagtailsnippets.models import register_snippet, SNIPPET_MODELS
from wagtail.wagtailsnippets.edit_handlers import SnippetChooserPanel
from wagtail.wagtailsnippets.views.snippets import (
get_snippet_edit_handler
@ -198,7 +200,7 @@ class TestSnippetDelete(TestCase, WagtailTestUtils):
self.assertEqual(response.status_code, 200)
def test_delete_post(self):
post_data = {'foo': 'bar'} # For some reason, this test doesn't work without a bit of POST data
post_data = {'foo': 'bar'} # For some reason, this test doesn't work without a bit of POST data
response = self.client.post(reverse('wagtailsnippets:delete', args=('tests', 'advert', self.test_snippet.id, )), post_data)
# Should be redirected to explorer page
@ -231,12 +233,51 @@ class TestSnippetChooserPanel(TestCase):
'_SnippetChooserPanel')
def test_render_as_field(self):
self.assertTrue(self.advert_text in self.snippet_chooser_panel.render_as_field())
field_html = self.snippet_chooser_panel.render_as_field()
self.assertIn(self.advert_text, field_html)
self.assertIn("Choose advert", field_html)
self.assertIn("Choose another advert", field_html)
def test_render_js(self):
self.assertIn('createSnippetChooser("id_advert", "tests/advert");',
self.snippet_chooser_panel.render_as_field())
def test_target_content_type_from_string(self):
result = SnippetChooserPanel(
'advert',
'tests.advert'
).bind_to_model(SnippetChooserModel).target_content_type()
self.assertEqual(result.name, 'advert')
def test_target_content_type_from_model(self):
result = SnippetChooserPanel(
'advert',
Advert
).bind_to_model(SnippetChooserModel).target_content_type()
self.assertEqual(result.name, 'advert')
def test_target_content_type_autodetected(self):
result = SnippetChooserPanel(
'advert'
).bind_to_model(SnippetChooserModel).target_content_type()
self.assertEqual(result.name, 'advert')
def test_target_content_type_malformed_type(self):
result = SnippetChooserPanel(
'advert',
'snowman'
).bind_to_model(SnippetChooserModel)
self.assertRaises(ImproperlyConfigured,
result.target_content_type)
def test_target_content_type_nonexistent_type(self):
result = SnippetChooserPanel(
'advert',
'snowman.lorry'
).bind_to_model(SnippetChooserModel)
self.assertRaises(ImproperlyConfigured,
result.target_content_type)
class TestSnippetRegistering(TestCase):
def test_register_function(self):