Source code for django_superform.fields

from django.forms.models import inlineformset_factory

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):
        self.required = required
        self.label = label
        self.help_text = help_text

        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


[docs]class CompositeField(BaseCompositeField): """ Implements the base structure that is relevant for all composite fields. This field cannot be used directly, use a subclass of it. """ prefix_name = 'composite' def __init__(self, *args, **kwargs): super(CompositeField, self).__init__(*args, **kwargs) # Let the widget know about the field for easier complex renderings in # the template. self.widget.field = self
[docs] def get_prefix(self, form, name): """ Return the prefix that is used for the formset. """ return '{form_prefix}{prefix_name}-{field_name}'.format( form_prefix=form.prefix + '-' if form.prefix else '', prefix_name=self.prefix_name, field_name=name)
[docs] def get_initial(self, form, name): """ Get the initial data that got passed into the superform for this composite field. It should return ``None`` if no initial values where given. """ if hasattr(form, 'initial'): return form.initial.get(name, None) return None
[docs] def get_kwargs(self, form, name): """ Return the keyword arguments that are used to instantiate the formset. """ kwargs = { 'prefix': self.get_prefix(form, name), 'initial': self.get_initial(form, name), } kwargs.update(self.default_kwargs) return kwargs
[docs]class FormField(CompositeField): """ A field that can be used to nest a form inside another form:: from django import forms from django_superform import SuperForm class AddressForm(forms.Form): street = forms.CharField() city = forms.CharField() class RegistrationForm(SuperForm): first_name = forms.CharField() last_name = forms.CharField() address = FormField(AddressForm) You can then display the fields in the template with (given that ``registration_form`` is an instance of ``RegistrationForm``):: {{ registration_form.address.street }} {{ registration_form.address.street.errors }} {{ registration_form.address.city }} {{ registration_form.address.city.errors }} The fields will all have a prefix in their name so that the naming does not clash with other fields on the page. The name attribute of the input tag for the ``street`` field in this example will be: ``form-address-street``. The name will change if you set a prefix on the superform:: form = RegistrationForm(prefix='registration') Then the field name will be ``registration-form-address-street``. You can pass the ``kwargs`` argument to the ``__init__`` method in order to give keyword arguments that you want to pass through to the form when it is instaniated. So you could use this to pass in initial values:: class RegistrationForm(SuperForm): address = FormField(AddressForm, kwargs={ 'initial': {'street': 'Stairway to Heaven 1'} }) But you can also use nested initial values which you pass into the superform:: RegistrationForm(initial={ 'address': {'street': 'Highway to Hell 666'} }) The first method (using ``kwargs``) will take precedence. """ prefix_name = 'form' widget = FormWidget def __init__(self, form_class, kwargs=None, **field_kwargs): super(FormField, self).__init__(**field_kwargs) self.form_class = form_class if kwargs is None: kwargs = {} self.default_kwargs = kwargs
[docs] 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
[docs] 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) composite_form = form_class( data=form.data if form.is_bound else None, files=form.files if form.is_bound else None, **kwargs) return composite_form
[docs]class ModelFormField(FormField): """ This class is the to :class:`~django_superform.fields.FormField` what Django's :class:`ModelForm` is to :class:`Form`. It has the same behaviour as :class:`~django_superform.fields.FormField` but will also save the nested form if the super form is saved. Here is an example:: from django_superform import ModelFormField class EmailForm(forms.ModelForm): class Meta: model = EmailAddress fields = ('email',) class UserForm(SuperModelForm): email = ModelFormField(EmailForm) class Meta: model = User fields = ('username',) user_form = UserForm( {'username': 'john', 'form-email-email': 'john@example.com'}) if user_form.is_valid(): user_form.save() This will save the ``user_form`` and create a new instance of ``User`` model and it will also save the ``EmailForm`` and therefore create an instance of ``EmailAddress``! However you usually want to use one of the exsting subclasses, like :class:`~django_superform.fields.ForeignKeyFormField` or extend from ``ModelFormField`` class and override the :meth:`~django_superform.fields.ModelFormField.get_instance` method. .. note:: Usually the :class:`~django_superform.fields.ModelFormField` is used inside a :class:`~django_superform.forms.SuperModelForm`. You actually can use it within a :class:`~django_superform.forms.SuperForm`, but since this form type does not have a ``save()`` method, you will need to take care of saving the nested model form yourself. """
[docs] def get_instance(self, form, name): """ Provide an instance that shall be used when instantiating the modelform. The ``form`` argument is the super-form instance that this ``ModelFormField`` is used in. ``name`` is the name of this field on the super-form. This returns ``None`` by default. So you usually want to override this method in a subclass. """ return None
[docs] def get_kwargs(self, form, name): """ Return the keyword arguments that are used to instantiate the formset. The ``instance`` kwarg will be set to the value returned by :meth:`~django_superform.fields.ModelFormField.get_instance`. The ``empty_permitted`` kwarg will be set to the inverse of the ``required`` argument passed into the constructor of this field. """ kwargs = super(ModelFormField, self).get_kwargs(form, name) instance = self.get_instance(form, name) kwargs.setdefault('instance', instance) kwargs.setdefault('empty_permitted', not self.required) return kwargs
[docs] def shall_save(self, form, name, composite_form): """ Return ``True`` if the given ``composite_form`` (the nested form of this field) shall be saved. Return ``False`` if the form shall not be saved together with the super-form. By default it will return ``False`` if the form was not changed and the ``empty_permitted`` argument for the form was set to ``True``. That way you can allow empty forms. """ if composite_form.empty_permitted and not composite_form.has_changed(): return False return True
[docs] def save(self, form, name, composite_form, commit): """ This method is called by :meth:`django_superform.forms.SuperModelForm.save` in order to save the modelform that this field takes care of and calls on the nested form's ``save()`` method. But only if :meth:`~django_superform.fields.ModelFormField.shall_save` returns ``True``. """ if self.shall_save(form, name, composite_form): return composite_form.save(commit=commit) return None
[docs]class ForeignKeyFormField(ModelFormField): def __init__(self, form_class, kwargs=None, field_name=None, blank=None, **field_kwargs): super(ForeignKeyFormField, self).__init__(form_class, kwargs, **field_kwargs) self.field_name = field_name self.blank = blank def get_kwargs(self, form, name): kwargs = super(ForeignKeyFormField, self).get_kwargs(form, name) if 'instance' not in kwargs: kwargs.setdefault('instance', self.get_instance(form, name)) if 'empty_permitted' not in kwargs: if self.allow_blank(form, name): kwargs['empty_permitted'] = True return kwargs def get_field_name(self, form, name): return self.field_name or name def allow_blank(self, form, name): """ Allow blank determines if the form might be completely empty. If it's empty it will result in a None as the saved value for the ForeignKey. """ if self.blank is not None: return self.blank model = form._meta.model 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) def save(self, form, name, composite_form, commit): # Support the ``empty_permitted`` attribute. This is set if the field # is ``blank=True`` . if composite_form.empty_permitted and not composite_form.has_changed(): saved_obj = composite_form.instance else: saved_obj = super(ForeignKeyFormField, self).save(form, name, composite_form, commit) setattr(form.instance, self.get_field_name(form, name), saved_obj) if commit: form.instance.save() else: raise NotImplementedError( 'ForeignKeyFormField cannot yet be used with non-commiting ' 'form saves.') return saved_obj
[docs]class FormSetField(CompositeField): """ First argument is a formset class that is instantiated by this FormSetField. You can pass the ``kwargs`` argument to specify kwargs values that are used when the ``formset_class`` is instantiated. """ prefix_name = 'formset' widget = FormSetWidget def __init__(self, formset_class, kwargs=None, **field_kwargs): super(FormSetField, self).__init__(**field_kwargs) self.formset_class = formset_class if kwargs is None: kwargs = {} self.default_kwargs = kwargs def get_formset_class(self, form, name): """ Return the formset class that will be used for instantiation in ``get_formset``. You can override this method in subclasses to change the behaviour of the given formset class. """ return self.formset_class def get_formset(self, form, name): """ Get an instance of the formset. """ kwargs = self.get_kwargs(form, name) formset_class = self.get_formset_class(form, name) formset = formset_class( form.data if form.is_bound else None, form.files if form.is_bound else None, **kwargs) return formset
[docs]class ModelFormSetField(FormSetField): def shall_save(self, form, name, formset): return True def save(self, form, name, formset, commit): if self.shall_save(form, name, formset): return formset.save(commit=commit) return None
[docs]class InlineFormSetField(ModelFormSetField): """ The ``InlineFormSetField`` helps when you want to use a inline formset. You can pass in either the keyword argument ``formset_class`` which is a ready to use formset that inherits from ``BaseInlineFormSet`` or was created by the ``inlineformset_factory``. The other option is to provide the arguments that you would usually pass into the ``inlineformset_factory``. The required arguments for that are: ``model`` The model class which should be represented by the forms in the formset. ``parent_model`` The parent model is the one that is referenced by the model in a foreignkey. ``form`` (optional) The model form that is used as a baseclass for the forms in the inline formset. You can use the ``kwargs`` keyword argument to pass extra arguments for the formset that are passed through when the formset is instantiated. All other not mentioned keyword arguments, like ``extra``, ``max_num`` etc. will be passed directly to the ``inlineformset_factory``. Example: class Gallery(models.Model): name = models.CharField(max_length=50) class Image(models.Model): gallery = models.ForeignKey(Gallery) image = models.ImageField(...) class GalleryForm(ModelFormWithFormSets): class Meta: model = Gallery fields = ('name',) images = InlineFormSetField( parent_model=Gallery, model=Image, extra=1) """ def __init__(self, parent_model=None, model=None, formset_class=None, kwargs=None, **factory_kwargs): """ You need to either provide the ``formset_class`` or the ``model`` argument. If the ``formset_class`` argument is not given, the ``model`` argument is used to create the formset_class on the fly when needed by using the ``inlineformset_factory``. """ # Make sure that all standard arguments will get passed through to the # parent's __init__ method. field_kwargs = {} for arg in ['required', 'widget', 'label', 'help_text', 'localize']: if arg in factory_kwargs: field_kwargs[arg] = factory_kwargs.pop(arg) self.parent_model = parent_model self.model = model self.formset_factory_kwargs = factory_kwargs super(InlineFormSetField, self).__init__(formset_class, kwargs=kwargs, **field_kwargs) if ( self.formset_class is None and 'form' not in self.formset_factory_kwargs and 'fields' not in self.formset_factory_kwargs and 'exclude' not in self.formset_factory_kwargs): raise ValueError( 'You need to either specify the `formset_class` argument or ' 'one of `form`/`fields`/`exclude` arguments ' 'when creating a {0}.' .format(self.__class__.__name__)) def get_model(self, form, name): return self.model def get_parent_model(self, form, name): if self.parent_model is not None: return self.parent_model return form._meta.model def get_formset_class(self, form, name): """ Either return the formset class that was provided as argument to the __init__ method, or build one based on the ``parent_model`` and ``model`` attributes. """ if self.formset_class is not None: return self.formset_class formset_class = inlineformset_factory( self.get_parent_model(form, name), self.get_model(form, name), **self.formset_factory_kwargs) return formset_class def get_kwargs(self, form, name): kwargs = super(InlineFormSetField, self).get_kwargs(form, name) kwargs.setdefault('instance', form.instance) return kwargs