From e0e00a134d9168d190e89d204898164c15d322de Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Wed, 28 Dec 2016 16:04:28 -0800 Subject: [PATCH 01/21] Increase verbosity of py.test to make it more user friendly on failures. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 00f7a58..09c6ad2 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ deps = 18: Django >= 1.8, < 1.9 19: Django >= 1.9, < 1.10 -r{toxinidir}/tests/requirements.txt -commands = py.test --cov django_superform {posargs:tests} +commands = py.test -vv --cov django_superform {posargs:tests} [testenv:docs] changedir = docs From d8580b97cb31f4f1cf1dc7a30459ab10eb1c5f39 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Wed, 28 Dec 2016 16:05:43 -0800 Subject: [PATCH 02/21] Update to inherit directly from field. --- django_superform/fields.py | 55 +++++++------------------------------ django_superform/forms.py | 1 - django_superform/widgets.py | 5 ++++ tests/test_superform.py | 2 +- 4 files changed, 16 insertions(+), 47 deletions(-) diff --git a/django_superform/fields.py b/django_superform/fields.py index 8f5297e..e82a206 100644 --- a/django_superform/fields.py +++ b/django_superform/fields.py @@ -1,52 +1,11 @@ from django.forms.models import inlineformset_factory +from django.forms import Field from .boundfield import CompositeBoundField from .widgets import FormWidget, FormSetWidget -class BaseCompositeField(object): - """ - The ``BaseCompositeField`` takes care of keeping some kind of compatibility - with the ``django.forms.Field`` class. - """ - - widget = None - show_hidden_initial = False - - # Tracks each time a FormSetField instance is created. Used to retain - # order. - creation_counter = 0 - - def __init__(self, required=True, widget=None, label=None, help_text='', - localize=False, disabled=False): - self.required = required - self.label = label - self.help_text = help_text - self.disabled = disabled - - widget = widget or self.widget - if isinstance(widget, type): - widget = widget() - - # Trigger the localization machinery if needed. - self.localize = localize - if self.localize: - widget.is_localized = True - - # Let the widget know whether it should display as required. - widget.is_required = self.required - - # We do not call self.widget_attrs() here as the original field is - # doing it. - - self.widget = widget - - # Increase the creation counter, and save our local copy. - self.creation_counter = BaseCompositeField.creation_counter - BaseCompositeField.creation_counter += 1 - - -class CompositeField(BaseCompositeField): +class CompositeField(Field): """ Implements the base structure that is relevant for all composite fields. This field cannot be used directly, use a subclass of it. @@ -152,9 +111,9 @@ class RegistrationForm(SuperForm): widget = FormWidget def __init__(self, form_class, kwargs=None, **field_kwargs): + self.form_class = form_class super(FormField, self).__init__(**field_kwargs) - self.form_class = form_class if kwargs is None: kwargs = {} self.default_kwargs = kwargs @@ -179,6 +138,9 @@ def get_form(self, form, name): **kwargs) return composite_form + def widget_attrs(self, widget): + return {'form_class': self.form_class} + class ModelFormField(FormField): """ @@ -349,9 +311,9 @@ class FormSetField(CompositeField): widget = FormSetWidget def __init__(self, formset_class, kwargs=None, **field_kwargs): + self.formset_class = formset_class super(FormSetField, self).__init__(**field_kwargs) - self.formset_class = formset_class if kwargs is None: kwargs = {} self.default_kwargs = kwargs @@ -376,6 +338,9 @@ def get_formset(self, form, name): **kwargs) return formset + def widget_attrs(self, widget): + return {'formset_class': self.formset_class} + class ModelFormSetField(FormSetField): def shall_save(self, form, name, formset): diff --git a/django_superform/forms.py b/django_superform/forms.py index 13fe99e..f6e3f09 100644 --- a/django_superform/forms.py +++ b/django_superform/forms.py @@ -103,7 +103,6 @@ def __new__(mcs, name, bases, attrs): for key, value in list(attrs.items()): if isinstance(value, CompositeField): current_fields.append((key, value)) - attrs.pop(key) current_fields.sort(key=lambda x: x[1].creation_counter) attrs['declared_composite_fields'] = OrderedDict(current_fields) diff --git a/django_superform/widgets.py b/django_superform/widgets.py index f5173ce..803379e 100644 --- a/django_superform/widgets.py +++ b/django_superform/widgets.py @@ -57,7 +57,12 @@ class FormWidget(TemplateWidget): template_name = 'superform/formfield.html' value_context_name = 'form' + def value_from_datadict(self, data, files, name): + return self.attrs['form_class'](data, files, prefix=name) class FormSetWidget(TemplateWidget): template_name = 'superform/formsetfield.html' value_context_name = 'formset' + + def value_from_datadict(self, data, files, name): + return self.attrs['formset_class'](data, files, prefix=name) diff --git a/tests/test_superform.py b/tests/test_superform.py index 9deb282..46593c7 100644 --- a/tests/test_superform.py +++ b/tests/test_superform.py @@ -31,7 +31,7 @@ class SubclassedAccountForm(AccountForm): class SuperFormTests(TestCase): def test_base_composite_fields(self): - self.assertEqual(list(AccountForm.base_fields.keys()), ['username']) + self.assertEqual(list(AccountForm.base_fields.keys()), ['username', 'emails', 'nested_form']) self.assertTrue(hasattr(AccountForm, 'base_composite_fields')) self.assertEqual(list(AccountForm.base_composite_fields.keys()), ['emails', 'nested_form']) From 8ccae821aaed053525918a0ca2ae669794281155 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Wed, 28 Dec 2016 16:41:29 -0800 Subject: [PATCH 03/21] Eliminate need for SuperForm.composite_fields. --- django_superform/forms.py | 67 +++++++++++++----------------------- django_superform/widgets.py | 8 +++++ tests/test_formfield.py | 4 +-- tests/test_modelformfield.py | 4 +-- tests/test_superform.py | 1 - 5 files changed, 35 insertions(+), 49 deletions(-) diff --git a/django_superform/forms.py b/django_superform/forms.py index f6e3f09..2009827 100644 --- a/django_superform/forms.py +++ b/django_superform/forms.py @@ -59,15 +59,15 @@ def post_form(request): Now to how you can access the instantiated formsets:: >>> form = PostForm() - >>> form.composite_fields['comments'] + >>> form.fields['comments'] Or in the template:: {{ form.as_p }} - {{ form.composite_fields.comments.management_form }} - {% for fieldset_form in form.composite_fields.comments %} + {{ form.fields.comments.management_form }} + {% for fieldset_form in form.fields.comments %} {{ fieldset_form.as_p }} {% endfor %} @@ -75,12 +75,14 @@ def post_form(request): """ +import copy from functools import reduce + +import django from django import forms from django.forms.forms import DeclarativeFieldsMetaclass, ErrorDict, ErrorList from django.forms.models import ModelFormMetaclass from django.utils import six -import copy from .fields import CompositeField @@ -174,30 +176,25 @@ def __init__(self, *args, **kwargs): super(SuperFormMixin, self).__init__(*args, **kwargs) self._init_composite_fields() - def __getitem__(self, name): - """ - Returns a ``django.forms.BoundField`` for the given field name. It also - returns :class:`~django_superform.boundfield.CompositeBoundField` - instances for composite fields. - """ - if name not in self.fields and name in self.composite_fields: - field = self.composite_fields[name] - return field.get_bound_field(self, name) - return super(SuperFormMixin, self).__getitem__(name) + if django.VERSION < (1, 9): + # This behavior is not needed after django 1.9 introduced get_bound_field. - def add_composite_field(self, name, field): - """ - Add a dynamic composite field to the already existing ones and - initialize it appropriatly. - """ - self.composite_fields[name] = field - self._init_composite_field(name, field) + def __getitem__(self, name): + """ + Returns a ``django.forms.BoundField`` for the given field name. It also + returns :class:`~django_superform.boundfield.CompositeBoundField` + instances for composite fields. + """ + field = self.fields[name] + if hasattr(field, 'get_bound_field'): + return field.get_bound_field(self, name) + return super(SuperFormMixin, self).__getitem__(name) def get_composite_field_value(self, name): """ Return the form/formset instance for the given field name. """ - field = self.composite_fields[name] + field = self.fields[name] if hasattr(field, 'get_form'): return self.forms[name] if hasattr(field, 'get_formset'): @@ -215,16 +212,10 @@ def _init_composite_fields(self): """ Setup the forms and formsets. """ - # The base_composite_fields class attribute is the *class-wide* - # definition of fields. Because a particular *instance* of the class - # might want to alter self.composite_fields, we create - # self.composite_fields here by copying base_composite_fields. - # Instances should always modify self.composite_fields; they should not - # modify base_composite_fields. - self.composite_fields = copy.deepcopy(self.base_composite_fields) self.forms = OrderedDict() self.formsets = OrderedDict() - for name, field in self.composite_fields.items(): + for name in self.base_composite_fields: + field = self.fields[name] self._init_composite_field(name, field) def full_clean(self): @@ -243,18 +234,6 @@ def full_clean(self): if not composite.is_valid() and composite._errors: self._errors[field_name] = ErrorList(composite._errors) - @property - def media(self): - """ - Incooperate composite field's media. - """ - media_list = [] - media_list.append(super(SuperFormMixin, self).media) - for composite_name in self.composite_fields.keys(): - form = self.get_composite_field_value(composite_name) - media_list.append(form.media) - return reduce(lambda a, b: a + b, media_list) - class SuperModelFormMixin(SuperFormMixin): """ @@ -348,7 +327,7 @@ def save_form(self, commit=True): def save_forms(self, commit=True): saved_composites = [] for name, composite in self.forms.items(): - field = self.composite_fields[name] + field = self.fields[name] if hasattr(field, 'save'): field.save(self, name, composite, commit=commit) saved_composites.append(composite) @@ -363,7 +342,7 @@ def save_formsets(self, commit=True): """ saved_composites = [] for name, composite in self.formsets.items(): - field = self.composite_fields[name] + field = self.fields[name] if hasattr(field, 'save'): field.save(self, name, composite, commit=commit) saved_composites.append(composite) diff --git a/django_superform/widgets.py b/django_superform/widgets.py index 803379e..d9f50f6 100644 --- a/django_superform/widgets.py +++ b/django_superform/widgets.py @@ -57,6 +57,10 @@ class FormWidget(TemplateWidget): template_name = 'superform/formfield.html' value_context_name = 'form' + @property + def media(self): + return self.field.form_class().media + def value_from_datadict(self, data, files, name): return self.attrs['form_class'](data, files, prefix=name) @@ -64,5 +68,9 @@ class FormSetWidget(TemplateWidget): template_name = 'superform/formsetfield.html' value_context_name = 'formset' + @property + def media(self): + return self.field.formset_class().media + def value_from_datadict(self, data, files, name): return self.attrs['formset_class'](data, files, prefix=name) diff --git a/tests/test_formfield.py b/tests/test_formfield.py index bf14171..225c1a9 100644 --- a/tests/test_formfield.py +++ b/tests/test_formfield.py @@ -33,9 +33,9 @@ class RegistrationForm(SuperForm): class FormFieldTests(TestCase): - def test_is_in_composite_fields(self): + def test_is_in_fields(self): superform = RegistrationForm() - self.assertTrue('address' in superform.composite_fields) + self.assertTrue('address' in superform.fields) def test_is_in_forms_attr(self): superform = RegistrationForm() diff --git a/tests/test_modelformfield.py b/tests/test_modelformfield.py index 4ee592a..28365c4 100644 --- a/tests/test_modelformfield.py +++ b/tests/test_modelformfield.py @@ -45,9 +45,9 @@ class Meta: class FormFieldTests(TestCase): - def test_is_in_composite_fields(self): + def test_is_in_fields(self): superform = PostForm() - self.assertTrue('series' in superform.composite_fields) + self.assertTrue('series' in superform.fields) def test_is_in_forms_attr(self): superform = PostForm() diff --git a/tests/test_superform.py b/tests/test_superform.py index 46593c7..e87d804 100644 --- a/tests/test_superform.py +++ b/tests/test_superform.py @@ -50,7 +50,6 @@ def test_base_composite_fields(self): def test_fields_in_instantiated_forms(self): form = AccountForm() - self.assertTrue(hasattr(form, 'composite_fields')) self.assertTrue(hasattr(form, 'forms')) self.assertTrue(hasattr(form, 'formsets')) From 7c9beaf18553596990f26039edbe5b780ec9e5bd Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Wed, 28 Dec 2016 16:53:22 -0800 Subject: [PATCH 04/21] Remove base_composite_fields and need for custom metaclasses. --- django_superform/forms.py | 83 +++++---------------------------------- docs/howitworks.rst | 22 ----------- tests/test_superform.py | 17 -------- 3 files changed, 9 insertions(+), 113 deletions(-) diff --git a/django_superform/forms.py b/django_superform/forms.py index 2009827..7b14ee3 100644 --- a/django_superform/forms.py +++ b/django_superform/forms.py @@ -92,75 +92,16 @@ def post_form(request): from django.utils.datastructures import SortedDict as OrderedDict -class DeclerativeCompositeFieldsMetaclass(type): - """ - Metaclass that converts FormField and FormSetField attributes to a - dictionary called `composite_fields`. It will also include all composite - fields from parent classes. - """ - - def __new__(mcs, name, bases, attrs): - # Collect composite fields from current class. - current_fields = [] - for key, value in list(attrs.items()): - if isinstance(value, CompositeField): - current_fields.append((key, value)) - current_fields.sort(key=lambda x: x[1].creation_counter) - attrs['declared_composite_fields'] = OrderedDict(current_fields) - - new_class = super(DeclerativeCompositeFieldsMetaclass, mcs).__new__( - mcs, name, bases, attrs) - - # Walk through the MRO. - declared_fields = OrderedDict() - for base in reversed(new_class.__mro__): - # Collect fields from base class. - if hasattr(base, 'declared_composite_fields'): - declared_fields.update(base.declared_composite_fields) - - # Field shadowing. - for attr, value in base.__dict__.items(): - if value is None and attr in declared_fields: - declared_fields.pop(attr) - - new_class.base_composite_fields = declared_fields - new_class.declared_composite_fields = declared_fields - - return new_class - - -class SuperFormMetaclass( - DeclerativeCompositeFieldsMetaclass, - DeclarativeFieldsMetaclass): - """ - Metaclass for :class:`~django_superform.forms.SuperForm`. - """ - - -class SuperModelFormMetaclass( - DeclerativeCompositeFieldsMetaclass, - ModelFormMetaclass): - """ - Metaclass for :class:`~django_superform.forms.SuperModelForm`. - """ - - class SuperFormMixin(object): """ The base class for all super forms. It does not inherit from any other - classes, so you are free to mix it into any custom form class you have. You - need to use it together with ``SuperFormMetaclass``, like this: + classes, so you are free to mix it into any custom form class you have. .. code:: python from django_superform import SuperFormMixin - from django_superform import SuperFormMetaclass - import six - class MySuperForm(six.with_metaclass( - SuperFormMetaclass, - SuperFormMixin, - MyCustomForm)): + class MySuperForm(MyCustomForm): pass The goal of a superform is to behave just like a normal django form but is @@ -214,9 +155,10 @@ def _init_composite_fields(self): """ self.forms = OrderedDict() self.formsets = OrderedDict() - for name in self.base_composite_fields: - field = self.fields[name] - self._init_composite_field(name, field) + composite_fields = [field for field in self.fields if isinstance(field, CompositeField)] + for name, field in self.fields.items(): + if isinstance(field, CompositeField): + self._init_composite_field(name, field) def full_clean(self): """ @@ -242,13 +184,8 @@ class SuperModelFormMixin(SuperFormMixin): .. code:: python from django_superform import SuperModelFormMixin - from django_superform import SuperModelFormMetaclass - import six - class MySuperForm(six.with_metaclass( - SuperModelFormMetaclass, - SuperModelFormMixin, - MyCustomModelForm)): + class MySuperForm(SuperModelFormMixin, MyCustomModelForm)): pass """ @@ -350,8 +287,7 @@ def save_formsets(self, commit=True): self._extend_save_m2m('save_formsets_m2m', saved_composites) -class SuperModelForm(six.with_metaclass(SuperModelFormMetaclass, - SuperModelFormMixin, forms.ModelForm)): +class SuperModelForm(SuperModelFormMixin, forms.ModelForm): """ The ``SuperModelForm`` works like a Django ``ModelForm`` but has the capabilities of nesting like :class:`~django_superform.forms.SuperForm`. @@ -360,8 +296,7 @@ class SuperModelForm(six.with_metaclass(SuperModelFormMetaclass, """ -class SuperForm(six.with_metaclass(SuperFormMetaclass, - SuperFormMixin, forms.Form)): +class SuperForm(SuperFormMixin, forms.Form): """ The base class for all super forms. The goal of a superform is to behave just like a normal django form but is able to take composite fields, like diff --git a/docs/howitworks.rst b/docs/howitworks.rst index 4a00b47..c78a112 100644 --- a/docs/howitworks.rst +++ b/docs/howitworks.rst @@ -12,28 +12,6 @@ you as expected. So here is a description of the life cycle of a super form and its composite fields. -The ``SuperFormMetaclass`` --------------------------- - -A super form is using a metaclass to discover all the fields used the form, -just like Django does it with normal form fields. That's what the -:class:`~django_superform.forms.SuperFormMetaclass` is for. - -That will discover all the used :class:`~django_superform.fields.FormField` and -:class:`~django_superform.fields.FormSetField` on your form and puts the -them in a attribute called ``base_composite_fields`` on the form class. - -So you can inspect all the composite fields with it: - -.. code:: python - - class PersonForm(SuperForm): - name = forms.CharField() - social_accounts = FormSet(SocialAccountsForm) - addresses = FormSetField(AddressForm) - - print(PersonForm.base_composite_fields) - On form instantiation --------------------- diff --git a/tests/test_superform.py b/tests/test_superform.py index e87d804..5852f8e 100644 --- a/tests/test_superform.py +++ b/tests/test_superform.py @@ -30,23 +30,6 @@ class SubclassedAccountForm(AccountForm): class SuperFormTests(TestCase): - def test_base_composite_fields(self): - self.assertEqual(list(AccountForm.base_fields.keys()), ['username', 'emails', 'nested_form']) - - self.assertTrue(hasattr(AccountForm, 'base_composite_fields')) - self.assertEqual(list(AccountForm.base_composite_fields.keys()), ['emails', 'nested_form']) - self.assertTrue(hasattr(SubclassedAccountForm, 'base_composite_fields')) - self.assertEqual(list(SubclassedAccountForm.base_composite_fields.keys()), ['emails', 'nested_form', 'nested_form_2']) - - field = AccountForm.base_composite_fields['emails'] - self.assertIsInstance(field, FormSetField) - - field = AccountForm.base_composite_fields['nested_form'] - self.assertIsInstance(field, FormField) - - self.assertFalse(hasattr(AccountForm, 'forms')) - self.assertFalse(hasattr(AccountForm, 'formsets')) - def test_fields_in_instantiated_forms(self): form = AccountForm() From 30deb038c45ee7c1c08766fbbad1f74ddc760c35 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Wed, 28 Dec 2016 17:00:12 -0800 Subject: [PATCH 05/21] Remove support for old end-of-lifed versions due to incompatibility with django 1.4. --- tox.ini | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tox.ini b/tox.ini index 09c6ad2..54a585c 100644 --- a/tox.ini +++ b/tox.ini @@ -3,12 +3,11 @@ minversion = 1.8 envlist = docs, flake8, - py26-{14,16}, - py27-{14,16,17,18,19}, - py33-{16,17,18}, - py34-{16,17,18,19}, + py27-{18,19}, + py33-{18}, + py34-{18,19}, py35-{18,19}, - pypy-{14,16,17,18,19} + pypy-{18,19} [testenv] deps = From c124126a641c6473ffcf9f0fd3216bfdd55d0b2a Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Wed, 28 Dec 2016 17:14:59 -0800 Subject: [PATCH 06/21] Add Django 1.10 to the build matrix. --- tox.ini | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tox.ini b/tox.ini index 54a585c..6310935 100644 --- a/tox.ini +++ b/tox.ini @@ -3,19 +3,17 @@ minversion = 1.8 envlist = docs, flake8, - py27-{18,19}, + py27-{18,19,110}, py33-{18}, - py34-{18,19}, - py35-{18,19}, - pypy-{18,19} + py34-{18,19,110}, + py35-{18,19,110}, + pypy-{18,19,110} [testenv] deps = - 14: Django >= 1.4, < 1.5 - 16: Django >= 1.6, < 1.7 - 17: Django >= 1.7, < 1.8 18: Django >= 1.8, < 1.9 19: Django >= 1.9, < 1.10 + 110: Django >= 1.10, < 1.11 -r{toxinidir}/tests/requirements.txt commands = py.test -vv --cov django_superform {posargs:tests} From aef4da941430eeebdfc0ebb3f1c2afaa197c1c6a Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Wed, 28 Dec 2016 17:15:23 -0800 Subject: [PATCH 07/21] Upgrade django test dependencies. --- tests/requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/requirements.txt b/tests/requirements.txt index 498da52..4c555c1 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,7 +1,7 @@ coverage==3.7.1 tox==2.0.1 flake8==2.5.4 -pytest==2.8.7 -pytest-cov==2.2.1 -pytest-django==2.9.1 +pytest==3.0.5 +pytest-cov==2.4.0 +pytest-django==3.1.2 pytest-pythonpath==0.7 From 2171536ad3a7418aa1c0067fcafb7d25126e89ee Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Wed, 28 Dec 2016 17:15:47 -0800 Subject: [PATCH 08/21] Add new required TEMPLATES setup. --- tests/settings.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/settings.py b/tests/settings.py index f61e178..e16e03f 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -21,3 +21,22 @@ STATIC_URL = '/static/' SECRET_KEY = '0' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + 'loaders': [ + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + ], + }, + }, +] From e8dacbe18c49318ebfd566f177941b2682998da2 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Wed, 28 Dec 2016 17:16:07 -0800 Subject: [PATCH 09/21] Remove usage of deprecated kwargs --- django_superform/widgets.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/django_superform/widgets.py b/django_superform/widgets.py index d9f50f6..a119963 100644 --- a/django_superform/widgets.py +++ b/django_superform/widgets.py @@ -18,7 +18,6 @@ def __init__(self, *args, **kwargs): if template_name is not None: self.template_name = template_name super(TemplateWidget, self).__init__(*args, **kwargs) - self.context_instance = None def get_context_data(self): return {} @@ -47,10 +46,7 @@ def render(self, name, value, attrs=None, **kwargs): if template_name is None: template_name = self.template_name context = self.get_context(name, value, attrs=attrs or {}, **kwargs) - return loader.render_to_string( - template_name, - dictionary=context, - context_instance=self.context_instance) + return loader.render_to_string(template_name, context=context) class FormWidget(TemplateWidget): From 052fd69c87e25b0928800aca097535d5d0d3cd1b Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Wed, 28 Dec 2016 17:42:43 -0800 Subject: [PATCH 10/21] update travis environments. --- .travis.yml | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 22c45a8..db2a0eb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,25 +1,18 @@ language: python python: 2.7 env: -- TOX_ENV=py26-14 -- TOX_ENV=py26-16 -- TOX_ENV=py27-14 -- TOX_ENV=py27-16 -- TOX_ENV=py27-17 - TOX_ENV=py27-18 - TOX_ENV=py27-19 -- TOX_ENV=py33-16 -- TOX_ENV=py33-17 -- TOX_ENV=py34-16 -- TOX_ENV=py34-17 +- TOX_ENV=py27-110 - TOX_ENV=py34-18 +- TOX_ENV=py34-19 +- TOX_ENV=py34-110 - TOX_ENV=py35-18 - TOX_ENV=py35-19 -- TOX_ENV=pypy-14 -- TOX_ENV=pypy-16 -- TOX_ENV=pypy-17 +- TOX_ENV=py35-110 - TOX_ENV=pypy-18 - TOX_ENV=pypy-19 +- TOX_ENV=pypy-110 - TOX_ENV=docs - TOX_ENV=flake8 install: From 81caaa97cc2a39778211f78ccaa1bfcdefaafd74 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Wed, 28 Dec 2016 18:39:09 -0800 Subject: [PATCH 11/21] Fix flake8 failures. --- django_superform/forms.py | 16 ++++++---------- django_superform/widgets.py | 1 + 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/django_superform/forms.py b/django_superform/forms.py index 7b14ee3..66d083e 100644 --- a/django_superform/forms.py +++ b/django_superform/forms.py @@ -75,14 +75,9 @@ def post_form(request): """ -import copy -from functools import reduce - import django from django import forms -from django.forms.forms import DeclarativeFieldsMetaclass, ErrorDict, ErrorList -from django.forms.models import ModelFormMetaclass -from django.utils import six +from django.forms.forms import ErrorDict, ErrorList from .fields import CompositeField @@ -118,12 +113,14 @@ def __init__(self, *args, **kwargs): self._init_composite_fields() if django.VERSION < (1, 9): - # This behavior is not needed after django 1.9 introduced get_bound_field. + # This behavior is not needed after django 1.9 introduced + # get_bound_field. def __getitem__(self, name): """ - Returns a ``django.forms.BoundField`` for the given field name. It also - returns :class:`~django_superform.boundfield.CompositeBoundField` + Returns a ``django.forms.BoundField`` for the given field name. + It also returns + :class:`~django_superform.boundfield.CompositeBoundField` instances for composite fields. """ field = self.fields[name] @@ -155,7 +152,6 @@ def _init_composite_fields(self): """ self.forms = OrderedDict() self.formsets = OrderedDict() - composite_fields = [field for field in self.fields if isinstance(field, CompositeField)] for name, field in self.fields.items(): if isinstance(field, CompositeField): self._init_composite_field(name, field) diff --git a/django_superform/widgets.py b/django_superform/widgets.py index a119963..a7390bf 100644 --- a/django_superform/widgets.py +++ b/django_superform/widgets.py @@ -60,6 +60,7 @@ def media(self): def value_from_datadict(self, data, files, name): return self.attrs['form_class'](data, files, prefix=name) + class FormSetWidget(TemplateWidget): template_name = 'superform/formsetfield.html' value_context_name = 'formset' From 1beb80890178d4a0193a943f318290fa8f66e7bb Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Thu, 29 Dec 2016 15:48:08 -0800 Subject: [PATCH 12/21] Partially revert `media` changes. I'd hoped to allow using the widget's `media` property to eventually allow use of FormSetField and FormField on classes that do not inherit from `SuperFormMixin`. Unfortunately, since `media` is a _property_ of the `Form`/`FormSet`, we need an instantiated instance of those. Due to the way field/widget scoping takes place, there's no way to access the "already instantiated" form/formset without hacky stack introspection. Instantiating a formset or form isn't too bad, but can lead to unexpected behavior (like duplicate database queries), so probably best avoided. --- django_superform/forms.py | 13 +++++++++++++ django_superform/widgets.py | 8 -------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/django_superform/forms.py b/django_superform/forms.py index 66d083e..5a268e8 100644 --- a/django_superform/forms.py +++ b/django_superform/forms.py @@ -172,6 +172,19 @@ def full_clean(self): if not composite.is_valid() and composite._errors: self._errors[field_name] = ErrorList(composite._errors) + @property + def media(self): + """ + Incorporate composite field's media. + """ + media = forms.Media() + for name, field in self.fields.items(): + if isinstance(field, CompositeField): + media = media + self.get_composite_field_value(name).media + else: + media = media + field.widget.media + return media + class SuperModelFormMixin(SuperFormMixin): """ diff --git a/django_superform/widgets.py b/django_superform/widgets.py index a7390bf..9668c41 100644 --- a/django_superform/widgets.py +++ b/django_superform/widgets.py @@ -53,10 +53,6 @@ class FormWidget(TemplateWidget): template_name = 'superform/formfield.html' value_context_name = 'form' - @property - def media(self): - return self.field.form_class().media - def value_from_datadict(self, data, files, name): return self.attrs['form_class'](data, files, prefix=name) @@ -65,9 +61,5 @@ class FormSetWidget(TemplateWidget): template_name = 'superform/formsetfield.html' value_context_name = 'formset' - @property - def media(self): - return self.field.formset_class().media - def value_from_datadict(self, data, files, name): return self.attrs['formset_class'](data, files, prefix=name) From 1cedcc16db7dc84523e3b361e113a6b0da37e90f Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Tue, 3 Jan 2017 15:58:06 -0800 Subject: [PATCH 13/21] Override add_prefix. --- django_superform/forms.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/django_superform/forms.py b/django_superform/forms.py index 5a268e8..58e87d5 100644 --- a/django_superform/forms.py +++ b/django_superform/forms.py @@ -128,6 +128,18 @@ def __getitem__(self, name): return field.get_bound_field(self, name) return super(SuperFormMixin, self).__getitem__(name) + def add_prefix(self, name): + """ + Returns the field name with a prefix appended, if this Form has a + prefix set. + + Subclasses may wish to override. + """ + field = self.fields.get(name) + if isinstance(field, CompositeField): + return field.get_prefix(self, name) + return super(SuperFormMixin, self).add_prefix(name) + def get_composite_field_value(self, name): """ Return the form/formset instance for the given field name. From 4663c07317feccc34361b4d5e0835e9608c5a975 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Tue, 3 Jan 2017 17:00:02 -0800 Subject: [PATCH 14/21] Remove need for get_form_class. --- django_superform/fields.py | 14 +------------- docs/fields.rst | 2 +- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/django_superform/fields.py b/django_superform/fields.py index e82a206..269cef1 100644 --- a/django_superform/fields.py +++ b/django_superform/fields.py @@ -118,20 +118,12 @@ def __init__(self, form_class, kwargs=None, **field_kwargs): kwargs = {} self.default_kwargs = kwargs - def get_form_class(self, form, name): - """ - Return the form class that will be used for instantiation in - ``get_form``. You can override this method in subclasses to change - the behaviour of the given form class. - """ - return self.form_class - def get_form(self, form, name): """ Get an instance of the form. """ kwargs = self.get_kwargs(form, name) - form_class = self.get_form_class(form, name) + form_class = self.form_class composite_form = form_class( data=form.data if form.is_bound else None, files=form.files if form.is_bound else None, @@ -271,10 +263,6 @@ def allow_blank(self, form, name): field = model._meta.get_field(self.get_field_name(form, name)) return field.blank - def get_form_class(self, form, name): - form_class = self.form_class - return form_class - def get_instance(self, form, name): field_name = self.get_field_name(form, name) return getattr(form.instance, field_name) diff --git a/docs/fields.rst b/docs/fields.rst index d854431..388a212 100644 --- a/docs/fields.rst +++ b/docs/fields.rst @@ -28,7 +28,7 @@ a ``SuperForm``:: ------------- .. autoclass:: django_superform.fields.FormField - :members: get_form_class, get_form + :members: get_form ``ModelFormField`` ------------------ From 2dbd0f2116152229694712ca8dfd680cb682ee77 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Tue, 3 Jan 2017 17:03:15 -0800 Subject: [PATCH 15/21] Remove unused `form` parameter from `get_field_name`. --- django_superform/fields.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/django_superform/fields.py b/django_superform/fields.py index 269cef1..8869b17 100644 --- a/django_superform/fields.py +++ b/django_superform/fields.py @@ -249,7 +249,7 @@ def get_kwargs(self, form, name): kwargs['empty_permitted'] = True return kwargs - def get_field_name(self, form, name): + def get_field_name(self, name): return self.field_name or name def allow_blank(self, form, name): @@ -260,11 +260,11 @@ def allow_blank(self, form, name): if self.blank is not None: return self.blank model = form._meta.model - field = model._meta.get_field(self.get_field_name(form, name)) + field = model._meta.get_field(self.get_field_name(name)) return field.blank def get_instance(self, form, name): - field_name = self.get_field_name(form, name) + field_name = self.get_field_name(name) return getattr(form.instance, field_name) def save(self, form, name, composite_form, commit): @@ -276,7 +276,7 @@ def save(self, form, name, composite_form, commit): saved_obj = super(ForeignKeyFormField, self).save(form, name, composite_form, commit) - setattr(form.instance, self.get_field_name(form, name), saved_obj) + setattr(form.instance, self.get_field_name(name), saved_obj) if commit: form.instance.save() else: From 6d7814e74f3426a3679a0bbcf1fcb2f643b25ea3 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Tue, 3 Jan 2017 17:24:08 -0800 Subject: [PATCH 16/21] Ensure that the values returned by the widget is the same as form.forms and form.formsets. --- django_superform/forms.py | 2 ++ django_superform/widgets.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/django_superform/forms.py b/django_superform/forms.py index 58e87d5..2cbbe70 100644 --- a/django_superform/forms.py +++ b/django_superform/forms.py @@ -153,9 +153,11 @@ def get_composite_field_value(self, name): def _init_composite_field(self, name, field): if hasattr(field, 'get_form'): form = field.get_form(self, name) + field.widget.form = form self.forms[name] = form if hasattr(field, 'get_formset'): formset = field.get_formset(self, name) + field.widget.formset = formset self.formsets[name] = formset def _init_composite_fields(self): diff --git a/django_superform/widgets.py b/django_superform/widgets.py index 9668c41..050d1a4 100644 --- a/django_superform/widgets.py +++ b/django_superform/widgets.py @@ -54,7 +54,7 @@ class FormWidget(TemplateWidget): value_context_name = 'form' def value_from_datadict(self, data, files, name): - return self.attrs['form_class'](data, files, prefix=name) + return self.form class FormSetWidget(TemplateWidget): @@ -62,4 +62,4 @@ class FormSetWidget(TemplateWidget): value_context_name = 'formset' def value_from_datadict(self, data, files, name): - return self.attrs['formset_class'](data, files, prefix=name) + return self.formset From 5b0f9df4058739c4927b120dfad307a699c06753 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Wed, 11 Jan 2017 12:03:14 -0800 Subject: [PATCH 17/21] Update changelog. --- CHANGES.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 51c4d3e..3df0a6b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,9 +5,13 @@ Changelog ----- * Fix formset rendering in Django 1.9. `#17`_ * Add support for Django 1.9's ``get_bound_field``. `#18`_ +* Remove need for a custom metaclass. Formsets and forms now + show up in ``form.cleaned_data`` after validation in addition to the + ``SuperForm.forms`` and ``SuperForm.formsets`` attributes. `#19`_ .. _#17: https://github.com/gregmuellegger/django-superform/pull/17 .. _#18: https://github.com/gregmuellegger/django-superform/pull/18 +.. _#19: https://github.com/gregmuellegger/django-superform/pull/19 0.3.1 ----- From fedaf89ad2e68c80d307009d0e62372e84dd559e Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Thu, 1 Mar 2018 15:47:24 -0800 Subject: [PATCH 18/21] Update test matrix to include 1.11 and 2.0, as well as python 3.6. --- tox.ini | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 6310935..f397b8f 100644 --- a/tox.ini +++ b/tox.ini @@ -3,17 +3,20 @@ minversion = 1.8 envlist = docs, flake8, - py27-{18,19,110}, + py27-{18,19,110,111}, py33-{18}, - py34-{18,19,110}, - py35-{18,19,110}, - pypy-{18,19,110} + py34-{18,19,110,111,20}, + py35-{18,19,110,111,20}, + py36-{18,19,110,111,20}, + pypy-{18,19,110,111} [testenv] deps = 18: Django >= 1.8, < 1.9 19: Django >= 1.9, < 1.10 110: Django >= 1.10, < 1.11 + 111: Django >= 1.11, < 2 + 20: Django >= 2.0, < 2.1 -r{toxinidir}/tests/requirements.txt commands = py.test -vv --cov django_superform {posargs:tests} From 53f729a0d36e87056d25256f9e1c5ac4656b28d3 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Thu, 1 Mar 2018 15:57:34 -0800 Subject: [PATCH 19/21] Handle extra renderer argument. --- django_superform/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_superform/widgets.py b/django_superform/widgets.py index 050d1a4..787f736 100644 --- a/django_superform/widgets.py +++ b/django_superform/widgets.py @@ -22,7 +22,7 @@ def __init__(self, *args, **kwargs): def get_context_data(self): return {} - def get_context(self, name, value, attrs=None): + def get_context(self, name, value, attrs=None, **kwargs): context = { 'name': name, 'hidden': self.is_hidden, From 97664118d76b0844e9bac8956f7510f827301233 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Thu, 1 Mar 2018 16:22:54 -0800 Subject: [PATCH 20/21] Update for Django 1.11 compatibility. --- django_superform/boundfield.py | 3 ++- django_superform/widgets.py | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/django_superform/boundfield.py b/django_superform/boundfield.py index a9774b6..b1077df 100644 --- a/django_superform/boundfield.py +++ b/django_superform/boundfield.py @@ -24,7 +24,8 @@ def __iter__(self): for item in self.form.get_composite_field_value(self.name): yield item - # __len__, no changes required + def __len__(self): + return len(list(self.__iter__())) def __getitem__(self, item): """ diff --git a/django_superform/widgets.py b/django_superform/widgets.py index 787f736..4aa181c 100644 --- a/django_superform/widgets.py +++ b/django_superform/widgets.py @@ -1,3 +1,4 @@ +import django from django import forms from django.template import loader @@ -23,6 +24,7 @@ def get_context_data(self): return {} def get_context(self, name, value, attrs=None, **kwargs): + attrs = {} if attrs is None else attrs context = { 'name': name, 'hidden': self.is_hidden, @@ -38,6 +40,12 @@ def get_context(self, name, value, attrs=None, **kwargs): context.update(self.get_context_data()) context['attrs'] = self.build_attrs(attrs) + if django.VERSION >= (1, 11): + # Once support for older versions is dropped, this class + # should be replaced with template widgets now that django + # supports this. + # See https://docs.djangoproject.com/en/1.11/ref/forms/renderers/ + context['widget'] = context return context From c118e9df5e35ef833215affb27749fb0e874229b Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Thu, 1 Mar 2018 16:31:36 -0800 Subject: [PATCH 21/21] Updates for django 2.0. --- tests/models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/models.py b/tests/models.py index 45f61db..bb207e0 100644 --- a/tests/models.py +++ b/tests/models.py @@ -15,11 +15,13 @@ class Post(models.Model): """ title = models.CharField(max_length=50) - series = models.ForeignKey('Series', null=True, blank=True) + series = models.ForeignKey('Series', null=True, blank=True, + on_delete=models.CASCADE) class Image(models.Model): - post = models.ForeignKey('Post', related_name='images') + post = models.ForeignKey('Post', related_name='images', + on_delete=models.CASCADE) name = models.CharField(max_length=50) position = models.PositiveIntegerField(default=0)