From 3778c476bb8b8f9972e0857d0c6bd0ea4ca2c235 Mon Sep 17 00:00:00 2001 From: Tom Talbot Date: Wed, 18 Jun 2014 17:16:04 +0100 Subject: [PATCH] Add unit tests for edit_handlers.py. Lint edit_handlers.py. --- .gitignore | 1 + wagtail/wagtailadmin/edit_handlers.py | 77 ++- .../wagtailadmin/tests/test_edit_handlers.py | 523 ++++++++++++++++++ 3 files changed, 572 insertions(+), 29 deletions(-) create mode 100644 wagtail/wagtailadmin/tests/test_edit_handlers.py diff --git a/.gitignore b/.gitignore index bd904efc9..57f9d0eab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.pyc .DS_Store +/.ropeproject/ /.coverage /dist/ /MANIFEST diff --git a/wagtail/wagtailadmin/edit_handlers.py b/wagtail/wagtailadmin/edit_handlers.py index 245f7ad60..39f38d499 100644 --- a/wagtail/wagtailadmin/edit_handlers.py +++ b/wagtail/wagtailadmin/edit_handlers.py @@ -12,7 +12,11 @@ from django import forms from django.db import models from django.forms.models import fields_for_model from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured, ValidationError +from django.core.exceptions import ( + ObjectDoesNotExist, + ImproperlyConfigured, + ValidationError +) from django.core.urlresolvers import reverse from django.conf import settings from django.utils.translation import ugettext as _ @@ -33,7 +37,8 @@ class FriendlyDateInput(forms.DateInput): if attrs: default_attrs.update(attrs) - super(FriendlyDateInput, self).__init__(attrs=default_attrs, format='%d %b %Y') + super(FriendlyDateInput, self).__init__(attrs=default_attrs, + format='%d %b %Y') class FriendlyTimeInput(forms.TimeInput): @@ -46,7 +51,8 @@ class FriendlyTimeInput(forms.TimeInput): if attrs: default_attrs.update(attrs) - super(FriendlyTimeInput, self).__init__(attrs=default_attrs, format='%I.%M%p') + super(FriendlyTimeInput, self).__init__(attrs=default_attrs, + format='%I.%M%p') class FriendlyTimeField(forms.CharField): @@ -87,7 +93,7 @@ class LocalizedDateInput(forms.DateInput): and adds class="friendly_date" to be picked up by jquery datepicker. """ def __init__(self, attrs=None): - default_attrs = {'class': 'localized_date', 'localize':True} + default_attrs = {'class': 'localized_date', 'localize': True} if attrs: default_attrs.update(attrs) @@ -104,7 +110,8 @@ class LocalizedTimeInput(forms.TimeInput): if attrs: default_attrs.update(attrs) # Just use 24-hour format - super(LocalizedTimeInput, self).__init__(attrs=default_attrs, format='%H:%M') + super(LocalizedTimeInput, self).__init__(attrs=default_attrs, + format='%H:%M') class LocalizedTimeField(forms.CharField): @@ -118,7 +125,7 @@ class LocalizedTimeField(forms.CharField): match = expr.match(time_string.lower()) if match: # Pull out values from string - hour_string, minute_string= match.groups() + hour_string, minute_string = match.groups() # Convert hours and minutes to integers hour = int(hour_string) @@ -126,31 +133,38 @@ class LocalizedTimeField(forms.CharField): minute = int(minute_string) else: minute = 0 - if hour>=24 or hour < 0 or minute >=60 or minute < 0: + if hour >= 24 or hour < 0 or minute >= 60 or minute < 0: raise ValidationError(_("Please type a valid time")) return datetime.time(hour=hour, minute=minute) else: - raise ValidationError(_("Please type a valid time") ) + raise ValidationError(_("Please type a valid time")) -if hasattr(settings, 'USE_L10N') and settings.USE_L10N==True: +if hasattr(settings, 'USE_L10N') and settings.USE_L10N is True: FORM_FIELD_OVERRIDES = { models.DateField: {'widget': LocalizedDateInput}, - models.TimeField: {'widget': LocalizedTimeInput, 'form_class': LocalizedTimeField}, + models.TimeField: {'widget': LocalizedTimeInput, + 'form_class': LocalizedTimeField}, } -else: # Fall back to friendly date/time +else: # Fall back to friendly date/time FORM_FIELD_OVERRIDES = { models.DateField: {'widget': FriendlyDateInput}, - models.TimeField: {'widget': FriendlyTimeInput, 'form_class': FriendlyTimeField}, + models.TimeField: {'widget': FriendlyTimeInput, + 'form_class': FriendlyTimeField}, } WIDGET_JS = { - FriendlyDateInput: (lambda id: "initFriendlyDateChooser(fixPrefix('%s'));" % id), - FriendlyTimeInput: (lambda id: "initFriendlyTimeChooser(fixPrefix('%s'));" % id), - LocalizedDateInput: (lambda id: "initLocalizedDateChooser(fixPrefix('%s'));" % id), - LocalizedTimeInput: (lambda id: "initLocalizedTimeChooser(fixPrefix('%s'));" % id), - RichTextArea: (lambda id: "makeRichTextEditable(fixPrefix('%s'));" % id), + FriendlyDateInput: (lambda id: "initFriendlyDateChooser(fixPrefix('%s'));" + % id), + FriendlyTimeInput: (lambda id: "initFriendlyTimeChooser(fixPrefix('%s'));" + % id), + LocalizedDateInput: (lambda id: "initLocalizedDateChooser(fixPrefix('%s'));" + % id), + LocalizedTimeInput: (lambda id: "initLocalizedTimeChooser(fixPrefix('%s'));" + % id), + RichTextArea: (lambda id: "makeRichTextEditable(fixPrefix('%s'));" + % id), TagWidget: ( lambda id: "initTagField(fixPrefix('%s'), '%s');" % ( id, addslashes(reverse('wagtailadmin_tag_autocomplete')) @@ -159,7 +173,8 @@ WIDGET_JS = { } -# Callback to allow us to override the default form fields provided for each model field. +# Callback to allow us to override the default form fields provided +# for each model field. def formfield_for_dbfield(db_field, **kwargs): # snarfed from django/contrib/admin/options.py @@ -177,13 +192,13 @@ def formfield_for_dbfield(db_field, **kwargs): class WagtailAdminModelFormMetaclass(ClusterFormMetaclass): # Override the behaviour of the regular ModelForm metaclass - # which handles the translation of model fields to form fields - - # to use our own formfield_for_dbfield function to do that translation. - # This is done by sneaking a formfield_callback property into the class - # being defined (unless the class already provides a formfield_callback - # of its own). + # to use our own formfield_for_dbfield function to do that + # translation. This is done by sneaking a formfield_callback + # property into the class being defined (unless the class already + # provides a formfield_callback of its own). - # while we're at it, we'll also set extra_form_count to 0, as we're creating - # extra forms in JS + # while we're at it, we'll also set extra_form_count to 0, as + # we're creating extra forms in JS extra_form_count = 0 def __new__(cls, name, bases, attrs): @@ -200,8 +215,8 @@ WagtailAdminModelForm = WagtailAdminModelFormMetaclass('WagtailAdminModelForm', def get_form_for_model( - model, - fields=None, exclude=None, formsets=None, exclude_formsets=None, widgets=None + model, fields=None, exclude=None, formsets=None, + exclude_formsets=None, widgets=None ): # django's modelform_factory with a bit of custom behaviour @@ -215,7 +230,8 @@ def get_form_for_model( if exclude is not None: attrs['exclude'] = exclude if issubclass(model, Page): - attrs['exclude'] = attrs.get('exclude', []) + ['content_type', 'path', 'depth', 'numchild'] + attrs['exclude'] = attrs.get('exclude', []) + ['content_type', 'path', + 'depth', 'numchild'] if widgets is not None: attrs['widgets'] = widgets @@ -232,7 +248,9 @@ def get_form_for_model( 'Meta': type('Meta', (object,), attrs) } - return WagtailAdminModelFormMetaclass(class_name, (WagtailAdminModelForm,), form_class_attrs) + return WagtailAdminModelFormMetaclass(class_name, + (WagtailAdminModelForm,), + form_class_attrs) def extract_panel_definitions_from_model_class(model, exclude=None): @@ -247,7 +265,8 @@ def extract_panel_definitions_from_model_class(model, exclude=None): if issubclass(model, Page): _exclude = ['content_type', 'path', 'depth', 'numchild'] - fields = fields_for_model(model, exclude=_exclude, formfield_callback=formfield_for_dbfield) + fields = fields_for_model(model, exclude=_exclude, + formfield_callback=formfield_for_dbfield) for field_name, field in fields.items(): try: diff --git a/wagtail/wagtailadmin/tests/test_edit_handlers.py b/wagtail/wagtailadmin/tests/test_edit_handlers.py new file mode 100644 index 000000000..45db35c5b --- /dev/null +++ b/wagtail/wagtailadmin/tests/test_edit_handlers.py @@ -0,0 +1,523 @@ +from mock import MagicMock + +from django.test import TestCase +from django.core.exceptions import ValidationError + +from wagtail.wagtailadmin.edit_handlers import ( + FriendlyDateInput, + FriendlyTimeInput, + FriendlyTimeField, + LocalizedTimeInput, + LocalizedDateInput, + LocalizedTimeField, + get_form_for_model, + extract_panel_definitions_from_model_class, + BaseFieldPanel, + FieldPanel, + RichTextFieldPanel, + EditHandler, + WagtailAdminModelForm, + BaseCompositeEditHandler, + BaseTabbedInterface, + TabbedInterface, + BaseObjectList, + ObjectList, + PageChooserPanel +) +from wagtail.wagtailcore.models import Page, Site + + +class TestFriendlyDateInput(TestCase): + def test_attrs(self): + """ + When the attrs argument is passed to FriendlyDateInput's + constructor, they should be set on the FriendlyDateInput + object along with the default attrs + """ + friendly = FriendlyDateInput(attrs={'awesome': 'sauce'}) + self.assertEqual(friendly.attrs, {'class': 'friendly_date', + 'awesome': 'sauce'}) + + +class TestFriendlyTimeInput(TestCase): + def test_attrs(self): + """ + When the attrs argument is passed to FriendlyDateInput's + constructor, they should be set on the FriendlyDateInput + object along with the default attrs + """ + friendly = FriendlyTimeInput(attrs={'awesome': 'sauce'}) + self.assertEqual(friendly.attrs, {'class': 'friendly_time', + 'awesome': 'sauce'}) + + +class TestFriendlyTimeField(TestCase): + def setUp(self): + self.friendly = FriendlyTimeField() + + def test_no_time_string(self): + """ + to_python() should return None if it is passed an empty + string + """ + result = self.friendly.to_python('') + self.assertEqual(result, None) + + def test_invalid_time_string(self): + """ + to_python() should raise a ValidationError if it is passed + an invalid time string + """ + self.assertRaises(ValidationError, self.friendly.to_python, 'bacon') + + def test_afternoon_time_string(self): + """ + to_python() should convert a time string that ends with 'pm' + to a 24-hour time in the afternoon + """ + python_time = self.friendly.to_python('3:49pm') + self.assertEqual(str(python_time), '15:49:00') + + def test_morning_time_string(self): + """ + to_python() should convert a time string that ends with 'am' + to a 24-hour time in the morning + """ + python_time = self.friendly.to_python('3:49am') + self.assertEqual(str(python_time), '03:49:00') + + def test_no_minutes_time_string(self): + """ + If minutes are not specified in the time string, they should + default to zero + """ + python_time = self.friendly.to_python('3am') + self.assertEqual(str(python_time), '03:00:00') + + +class TestLocalizedDateInput(TestCase): + def test_attrs(self): + """ + When the attrs argument is passed to LocalizedDateInput's + constructor, they should be set on the LocalizedDateInput + object along with the default attrs + """ + localized = LocalizedDateInput(attrs={'awesome': 'sauce'}) + self.assertEqual(localized.attrs, {'class': 'localized_date', + 'localize': True, + 'awesome': 'sauce'}) + + +class TestLocalizedTimeInput(TestCase): + def test_attrs(self): + """ + When the attrs argument is passed to LocalizedTimeInput's + constructor, they should be set on the LocalizedTimeInput + object along with the default attrs + """ + localized = LocalizedTimeInput(attrs={'awesome': 'sauce'}) + self.assertEqual(localized.attrs, {'class': 'localized_time', + 'awesome': 'sauce'}) + + +class TestLocalizedTimeField(TestCase): + def setUp(self): + self.localized = LocalizedTimeField() + + def test_no_time_string(self): + """ + to_python() should return None if it is passed an empty + string + """ + result = self.localized.to_python('') + self.assertEqual(result, None) + + def test_non_time_string(self): + """ + to_python() should raise a ValidationError if it is passed + a string that does not represent a time + """ + self.assertRaises(ValidationError, self.localized.to_python, 'bacon') + + def test_invalid_time_string(self): + """ + to_python() should raise a ValidationError if it is passed + an invalid time string + """ + self.assertRaises(ValidationError, self.localized.to_python, '99:99') + + def test_afternoon_time_string(self): + """ + to_python() should understand 24-hour time + """ + python_time = self.localized.to_python('15:49') + self.assertEqual(str(python_time), '15:49:00') + + def test_morning_time_string(self): + """ + to_python() should understand 24-hour time + """ + python_time = self.localized.to_python('3:49') + self.assertEqual(str(python_time), '03:49:00') + + def test_no_minutes_time_string(self): + """ + If minutes are not specified in the time string, they should + default to zero + """ + python_time = self.localized.to_python('3') + self.assertEqual(str(python_time), '03:00:00') + + +class TestGetFormForModel(TestCase): + class FakeClass(object): + _meta = MagicMock() + + def setUp(self): + self.mock_exclude = MagicMock() + + def test_get_form_for_model(self): + form = get_form_for_model(self.FakeClass, + fields=[], + exclude=[self.mock_exclude], + formsets=['baz'], + exclude_formsets=['quux'], + widgets=['bacon']) + self.assertEqual(form.Meta.exclude, [self.mock_exclude]) + self.assertEqual(form.Meta.formsets, ['baz']) + self.assertEqual(form.Meta.exclude_formsets, ['quux']) + self.assertEqual(form.Meta.widgets, ['bacon']) + + +class TestExtractPanelDefinitionsFromModelClass(TestCase): + class FakePage(Page): + pass + + def test_can_extract_panels(self): + mock = MagicMock() + mock.panels = 'foo' + result = extract_panel_definitions_from_model_class(mock) + self.assertEqual(result, 'foo') + + def test_exclude(self): + panels = extract_panel_definitions_from_model_class(Site, exclude=['hostname']) + for panel in panels: + self.assertNotEqual(panel.field_name, 'hostname') + + def test_extracted_objects_are_panels(self): + panels = extract_panel_definitions_from_model_class(self.FakePage) + for panel in panels: + self.assertTrue(issubclass(panel, BaseFieldPanel)) + + +class TestEditHandler(TestCase): + class FakeForm(dict): + def __init__(self, *args, **kwargs): + self.fields = self.fields_iterator() + + def fields_iterator(self): + for i in self: + yield i + + def setUp(self): + self.edit_handler = EditHandler(form=True, instance=True) + self.edit_handler.render = lambda: "foo" + + def test_widget_overrides(self): + result = EditHandler.widget_overrides() + self.assertEqual(result, {}) + + def test_required_formsets(self): + result = EditHandler.required_formsets() + self.assertEqual(result, []) + + def test_get_form_class(self): + result = EditHandler.get_form_class(Page) + self.assertTrue(issubclass(result, WagtailAdminModelForm)) + + def test_edit_handler_init_no_instance(self): + self.assertRaises(ValueError, EditHandler, form=True) + + def test_edit_handler_init_no_form(self): + self.assertRaises(ValueError, EditHandler, instance=True) + + def test_object_classnames(self): + result = self.edit_handler.object_classnames() + self.assertEqual(result, "") + + def test_field_classnames(self): + result = self.edit_handler.field_classnames() + self.assertEqual(result, "") + + def test_field_type(self): + result = self.edit_handler.field_type() + self.assertEqual(result, "") + + def test_render_as_object(self): + result = self.edit_handler.render_as_object() + self.assertEqual(result, "foo") + + def test_render_as_field(self): + result = self.edit_handler.render_as_field() + self.assertEqual(result, "foo") + + def test_render_js(self): + result = self.edit_handler.render_js() + self.assertEqual(result, "") + + def test_rendered_fields(self): + result = self.edit_handler.rendered_fields() + self.assertEqual(result, []) + + def test_render_missing_fields(self): + fake_form = self.FakeForm() + fake_form["foo"] = "bar" + self.edit_handler.form = fake_form + self.assertEqual(self.edit_handler.render_missing_fields(), "bar") + + def test_render_form_content(self): + fake_form = self.FakeForm() + fake_form["foo"] = "bar" + self.edit_handler.form = fake_form + self.assertEqual(self.edit_handler.render_form_content(), "foobar") + + +class TestBaseCompositeEditHandler(TestCase): + def setUp(self): + mock = MagicMock() + mock.widget_overrides.return_value = {'foo': 'bar'} + mock.required_formsets.return_value = {'baz': 'quux'} + BaseCompositeEditHandler.children = [mock] + self.base_composite_edit_handler = BaseCompositeEditHandler( + instance=True, + form=True) + + def tearDown(self): + BaseCompositeEditHandler.children = None + + def test_object_classnames_no_classname(self): + result = self.base_composite_edit_handler.object_classnames() + self.assertEqual(result, "multi-field") + + def test_object_classnames(self): + self.base_composite_edit_handler.classname = "foo" + result = self.base_composite_edit_handler.object_classnames() + self.assertEqual(result, "multi-field foo") + + def test_widget_overrides(self): + result = self.base_composite_edit_handler.widget_overrides() + self.assertEqual(result, {'foo': 'bar'}) + + def test_required_formsets(self): + result = self.base_composite_edit_handler.required_formsets() + self.assertEqual(result, ['baz']) + + +class TestBaseTabbedInterface(TestCase): + class FakeChild(object): + class FakeGrandchild(object): + def render_js(self): + return "foo" + + def rendered_fields(self): + return ["bar"] + + def __call__(self, *args, **kwargs): + fake_grandchild = self.FakeGrandchild() + return fake_grandchild + + def test_render(self): + mock = MagicMock() + BaseTabbedInterface.children = [mock] + self.base_tabbed_interface = BaseTabbedInterface( + instance=True, + form=True) + result = self.base_tabbed_interface.render() + self.assertRegexpMatches(result, 'label') + self.assertRegexpMatches(result, + '
  • ') + self.assertRegexpMatches(result, + '

    ') + + def test_render_js(self): + field = self.FakeField() + bound_field = self.FakeField() + widget = FriendlyDateInput() + field.widget = widget + bound_field.field = field + self.field_panel.bound_field = bound_field + result = self.field_panel.render_js() + self.assertEqual(result, + "initFriendlyDateChooser(fixPrefix('id for label'));") + + def test_render_js_unknown_widget(self): + field = self.FakeField() + bound_field = self.FakeField() + widget = self.FakeField() + field.widget = widget + bound_field.field = field + self.field_panel.bound_field = bound_field + result = self.field_panel.render_js() + self.assertEqual(result, + '') + + def test_render_as_field(self): + field = self.FakeField() + bound_field = self.FakeField() + bound_field.field = field + self.field_panel.bound_field = bound_field + result = self.field_panel.render_as_field() + self.assertRegexpMatches(result, + '

    help text

    ') + self.assertRegexpMatches(result, + 'errors') + + def test_rendered_fields(self): + result = self.field_panel.rendered_fields() + self.assertEqual(result, ['barbecue']) + + +class TestRichTextFieldPanel(TestCase): + class FakeField(object): + label = 'label' + help_text = 'help text' + errors = ['errors'] + id_for_label = 'id for label' + + def test_render_js(self): + fake_field = self.FakeField() + rich_text_field_panel = RichTextFieldPanel('barbecue')( + instance=True, + form={'barbecue': fake_field}) + result = rich_text_field_panel.render_js() + self.assertEqual(result, + "makeRichTextEditable(fixPrefix('id for label'));") + + +class TestPageChooserPanel(TestCase): + class FakeField(object): + label = 'label' + help_text = 'help text' + errors = ['errors'] + id_for_label = 'id for label' + + class FakeInstance(object): + class FakePage(object): + class FakeParent(object): + id = 1 + + def get_parent(self): + return self.FakeParent() + + def __init__(self): + fake_page = self.FakePage() + self.barbecue = fake_page + + + def setUp(self): + fake_field = self.FakeField() + fake_instance = self.FakeInstance() + self.page_chooser_panel = PageChooserPanel('barbecue')( + instance=fake_instance, + form={'barbecue': fake_field}) + + def test_render_js(self): + result = self.page_chooser_panel.render_js() + self.assertEqual(result, "createPageChooser(fixPrefix('id for label'), 'wagtailcore.page', 1);")