Ticket #16256: ticket16256-with-admin-InlineAdmin-refactor-with-EnhancedInlineFormSet.patch

File ticket16256-with-admin-InlineAdmin-refactor-with-EnhancedInlineFormSet.patch, 45.8 KB (added by rasca, 13 years ago)
  • AUTHORS

    diff --git a/AUTHORS b/AUTHORS
    index 8cb71c1..fae6f81 100644
    a b answer newbie questions, and generally made Django that much better:  
    412412    Luciano Ramalho
    413413    Amit Ramon <amit.ramon@gmail.com>
    414414    Philippe Raoult <philippe.raoult@n2nsoft.com>
     415    Iván Raskovsky <raskovsky@gmail.com>
    415416    Massimiliano Ravelli <massimiliano.ravelli@gmail.com>
    416417    Brian Ray <http://brianray.chipy.org/>
    417418    Łukasz Rekucki <lrekucki@gmail.com>
  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    index f052fe1..9a25ace 100644
    a b from django.utils.text import capfirst, get_text_list  
    2525from django.utils.translation import ugettext as _
    2626from django.utils.translation import ungettext
    2727from django.utils.encoding import force_unicode
     28from django.views.generic import EnhancedInlineFormSet
    2829
    2930HORIZONTAL, VERTICAL = 1, 2
    3031# returns the <ul> class for a given radio_admin field
    class ModelAdmin(BaseModelAdmin):  
    12931294            "admin/object_history.html"
    12941295        ], context, current_app=self.admin_site.name)
    12951296
    1296 class InlineModelAdmin(BaseModelAdmin):
     1297class InlineModelAdmin(BaseModelAdmin, EnhancedInlineFormSet):
    12971298    """
    12981299    Options for inline editing of ``model`` instances.
    12991300
    class InlineModelAdmin(BaseModelAdmin):  
    13011302    ``model`` to its parent. This is required if ``model`` has more than one
    13021303    ``ForeignKey`` to its parent.
    13031304    """
    1304     model = None
    1305     fk_name = None
    13061305    formset = BaseInlineFormSet
    1307     extra = 3
    1308     max_num = None
    13091306    template = None
    13101307    verbose_name = None
    13111308    verbose_name_plural = None
    1312     can_delete = True
     1309    can_delete = True # True in EnhancedInlineFormSet
    13131310
    13141311    def __init__(self, parent_model, admin_site):
    13151312        self.admin_site = admin_site
    class InlineModelAdmin(BaseModelAdmin):  
    13341331
    13351332    def get_formset(self, request, obj=None, **kwargs):
    13361333        """Returns a BaseInlineFormSet class for use in admin add/change views."""
    1337         if self.declared_fieldsets:
    1338             fields = flatten_fieldsets(self.declared_fieldsets)
    1339         else:
    1340             fields = None
    1341         if self.exclude is None:
    1342             exclude = []
    1343         else:
    1344             exclude = list(self.exclude)
    1345         exclude.extend(kwargs.get("exclude", []))
     1334
     1335        exclude = self.get_exclude() or []
    13461336        exclude.extend(self.get_readonly_fields(request, obj))
    1347         # if exclude is an empty list we use None, since that's the actual
    1348         # default
    13491337        exclude = exclude or None
    1350         defaults = {
    1351             "form": self.form,
    1352             "formset": self.formset,
    1353             "fk_name": self.fk_name,
    1354             "fields": fields,
     1338
     1339        new_kwargs = {
    13551340            "exclude": exclude,
    13561341            "formfield_callback": partial(self.formfield_for_dbfield, request=request),
    1357             "extra": self.extra,
    1358             "max_num": self.max_num,
    1359             "can_delete": self.can_delete,
     1342            "parent_model": self.parent_model,
    13601343        }
    1361         defaults.update(kwargs)
    1362         return inlineformset_factory(self.parent_model, self.model, **defaults)
     1344        new_kwargs.update(kwargs)
     1345       
     1346        return self.get_base_formset(**new_kwargs)
    13631347
    13641348    def get_fieldsets(self, request, obj=None):
    13651349        if self.declared_fieldsets:
    class InlineModelAdmin(BaseModelAdmin):  
    13681352        fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
    13691353        return [(None, {'fields': fields})]
    13701354
     1355    def get_fields(self):
     1356        if self.declared_fieldsets:
     1357            return flatten_fieldsets(self.declared_fieldsets)
     1358        else:
     1359            return self.fields
     1360
     1361    # Stuff due to different naming
     1362    def get_formset_class(self):
     1363        return self.formset
     1364
     1365    def get_form_class(self):
     1366        return self.form
     1367
    13711368class StackedInline(InlineModelAdmin):
    13721369    template = 'admin/edit_inline/stacked.html'
    13731370
  • django/views/generic/__init__.py

    diff --git a/django/views/generic/__init__.py b/django/views/generic/__init__.py
    index 1a98067..c16ba52 100644
    a b from django.views.generic.detail import DetailView  
    66from django.views.generic.edit import FormView, CreateView, UpdateView, DeleteView
    77from django.views.generic.list import ListView
    88
     9from django.views.generic.formsets import (FormSetView, ModelFormSetView,
     10                               InlineFormSetView, EnhancedFormSet,
     11                               EnhancedModelFormSet, EnhancedInlineFormSet, )
     12
    913
    1014class GenericViewError(Exception):
    1115    """A problem in a generic view."""
  • deleted file django/views/generic/edit.py

    diff --git a/django/views/generic/edit.py b/django/views/generic/edit.py
    deleted file mode 100644
    index 3cade52..0000000
    + -  
    1 from django.forms import models as model_forms
    2 from django.core.exceptions import ImproperlyConfigured
    3 from django.http import HttpResponseRedirect
    4 from django.views.generic.base import TemplateResponseMixin, View
    5 from django.views.generic.detail import (SingleObjectMixin,
    6                         SingleObjectTemplateResponseMixin, BaseDetailView)
    7 
    8 
    9 class FormMixin(object):
    10     """
    11     A mixin that provides a way to show and handle a form in a request.
    12     """
    13 
    14     initial = {}
    15     form_class = None
    16     success_url = None
    17 
    18     def get_initial(self):
    19         """
    20         Returns the initial data to use for forms on this view.
    21         """
    22         return self.initial
    23 
    24     def get_form_class(self):
    25         """
    26         Returns the form class to use in this view
    27         """
    28         return self.form_class
    29 
    30     def get_form(self, form_class):
    31         """
    32         Returns an instance of the form to be used in this view.
    33         """
    34         return form_class(**self.get_form_kwargs())
    35 
    36     def get_form_kwargs(self):
    37         """
    38         Returns the keyword arguments for instanciating the form.
    39         """
    40         kwargs = {'initial': self.get_initial()}
    41         if self.request.method in ('POST', 'PUT'):
    42             kwargs.update({
    43                 'data': self.request.POST,
    44                 'files': self.request.FILES,
    45             })
    46         return kwargs
    47 
    48     def get_context_data(self, **kwargs):
    49         return kwargs
    50 
    51     def get_success_url(self):
    52         if self.success_url:
    53             url = self.success_url
    54         else:
    55             raise ImproperlyConfigured(
    56                 "No URL to redirect to. Provide a success_url.")
    57         return url
    58 
    59     def form_valid(self, form):
    60         return HttpResponseRedirect(self.get_success_url())
    61 
    62     def form_invalid(self, form):
    63         return self.render_to_response(self.get_context_data(form=form))
    64 
    65 
    66 class ModelFormMixin(FormMixin, SingleObjectMixin):
    67     """
    68     A mixin that provides a way to show and handle a modelform in a request.
    69     """
    70 
    71     def get_form_class(self):
    72         """
    73         Returns the form class to use in this view
    74         """
    75         if self.form_class:
    76             return self.form_class
    77         else:
    78             if self.model is not None:
    79                 # If a model has been explicitly provided, use it
    80                 model = self.model
    81             elif hasattr(self, 'object') and self.object is not None:
    82                 # If this view is operating on a single object, use
    83                 # the class of that object
    84                 model = self.object.__class__
    85             else:
    86                 # Try to get a queryset and extract the model class
    87                 # from that
    88                 model = self.get_queryset().model
    89             return model_forms.modelform_factory(model)
    90 
    91     def get_form_kwargs(self):
    92         """
    93         Returns the keyword arguments for instanciating the form.
    94         """
    95         kwargs = super(ModelFormMixin, self).get_form_kwargs()
    96         kwargs.update({'instance': self.object})
    97         return kwargs
    98 
    99     def get_success_url(self):
    100         if self.success_url:
    101             url = self.success_url % self.object.__dict__
    102         else:
    103             try:
    104                 url = self.object.get_absolute_url()
    105             except AttributeError:
    106                 raise ImproperlyConfigured(
    107                     "No URL to redirect to.  Either provide a url or define"
    108                     " a get_absolute_url method on the Model.")
    109         return url
    110 
    111     def form_valid(self, form):
    112         self.object = form.save()
    113         return super(ModelFormMixin, self).form_valid(form)
    114 
    115     def get_context_data(self, **kwargs):
    116         context = kwargs
    117         if self.object:
    118             context['object'] = self.object
    119             context_object_name = self.get_context_object_name(self.object)
    120             if context_object_name:
    121                 context[context_object_name] = self.object
    122         return context
    123 
    124 
    125 class ProcessFormView(View):
    126     """
    127     A mixin that processes a form on POST.
    128     """
    129     def get(self, request, *args, **kwargs):
    130         form_class = self.get_form_class()
    131         form = self.get_form(form_class)
    132         return self.render_to_response(self.get_context_data(form=form))
    133 
    134     def post(self, request, *args, **kwargs):
    135         form_class = self.get_form_class()
    136         form = self.get_form(form_class)
    137         if form.is_valid():
    138             return self.form_valid(form)
    139         else:
    140             return self.form_invalid(form)
    141 
    142     # PUT is a valid HTTP verb for creating (with a known URL) or editing an
    143     # object, note that browsers only support POST for now.
    144     def put(self, *args, **kwargs):
    145         return self.post(*args, **kwargs)
    146 
    147 
    148 class BaseFormView(FormMixin, ProcessFormView):
    149     """
    150     A base view for displaying a form
    151     """
    152 
    153 
    154 class FormView(TemplateResponseMixin, BaseFormView):
    155     """
    156     A view for displaying a form, and rendering a template response.
    157     """
    158 
    159 
    160 class BaseCreateView(ModelFormMixin, ProcessFormView):
    161     """
    162     Base view for creating an new object instance.
    163 
    164     Using this base class requires subclassing to provide a response mixin.
    165     """
    166     def get(self, request, *args, **kwargs):
    167         self.object = None
    168         return super(BaseCreateView, self).get(request, *args, **kwargs)
    169 
    170     def post(self, request, *args, **kwargs):
    171         self.object = None
    172         return super(BaseCreateView, self).post(request, *args, **kwargs)
    173 
    174 
    175 class CreateView(SingleObjectTemplateResponseMixin, BaseCreateView):
    176     """
    177     View for creating an new object instance,
    178     with a response rendered by template.
    179     """
    180     template_name_suffix = '_form'
    181 
    182 
    183 class BaseUpdateView(ModelFormMixin, ProcessFormView):
    184     """
    185     Base view for updating an existing object.
    186 
    187     Using this base class requires subclassing to provide a response mixin.
    188     """
    189     def get(self, request, *args, **kwargs):
    190         self.object = self.get_object()
    191         return super(BaseUpdateView, self).get(request, *args, **kwargs)
    192 
    193     def post(self, request, *args, **kwargs):
    194         self.object = self.get_object()
    195         return super(BaseUpdateView, self).post(request, *args, **kwargs)
    196 
    197 
    198 class UpdateView(SingleObjectTemplateResponseMixin, BaseUpdateView):
    199     """
    200     View for updating an object,
    201     with a response rendered by template..
    202     """
    203     template_name_suffix = '_form'
    204 
    205 
    206 class DeletionMixin(object):
    207     """
    208     A mixin providing the ability to delete objects
    209     """
    210     success_url = None
    211 
    212     def delete(self, request, *args, **kwargs):
    213         self.object = self.get_object()
    214         self.object.delete()
    215         return HttpResponseRedirect(self.get_success_url())
    216 
    217     # Add support for browsers which only accept GET and POST for now.
    218     def post(self, *args, **kwargs):
    219         return self.delete(*args, **kwargs)
    220 
    221     def get_success_url(self):
    222         if self.success_url:
    223             return self.success_url
    224         else:
    225             raise ImproperlyConfigured(
    226                 "No URL to redirect to. Provide a success_url.")
    227 
    228 
    229 class BaseDeleteView(DeletionMixin, BaseDetailView):
    230     """
    231     Base view for deleting an object.
    232 
    233     Using this base class requires subclassing to provide a response mixin.
    234     """
    235 
    236 
    237 class DeleteView(SingleObjectTemplateResponseMixin, BaseDeleteView):
    238     """
    239     View for deleting an object retrieved with `self.get_object()`,
    240     with a response rendered by template.
    241     """
    242     template_name_suffix = '_confirm_delete'
  • new file django/views/generic/edit/__init__.py

    diff --git a/django/views/generic/edit/__init__.py b/django/views/generic/edit/__init__.py
    new file mode 100644
    index 0000000..3567224
    - +  
     1from django.views.generic.detail import SingleObjectTemplateResponseMixin
     2from django.views.generic.base import TemplateResponseMixin
     3from django.views.generic.edit.base import (BaseFormView, BaseCreateView,
     4                                BaseUpdateView, BaseDeleteView, BaseFormSetView,
     5                                BaseModelFormSetView, BaseInlineFormSetView, )
     6from django.views.generic.edit.forms import (FormMixin, ModelFormMixin,
     7                                    ProcessFormView, DeletionMixin, )
     8from django.views.generic.edit.formset import (EnhancedFormSet,
     9                                   EnhancedModelFormSet, EnhancedInlineFormSet,
     10                                   FormSetMixin, ModelFormSetMixin,
     11                                   InlineFormSetMixin, ProcessFormSetView,
     12                                   ProcessInlineFormSetView, )
     13
     14
     15class FormView(TemplateResponseMixin, BaseFormView):
     16    """
     17    A view for displaying a form, and rendering a template response.
     18    """
     19
     20
     21class CreateView(SingleObjectTemplateResponseMixin, BaseCreateView):
     22    """
     23    View for creating an new object instance,
     24    with a response rendered by template.
     25    """
     26    template_name_suffix = '_form'
     27
     28
     29class UpdateView(SingleObjectTemplateResponseMixin, BaseUpdateView):
     30    """
     31    View for updating an object,
     32    with a response rendered by template..
     33    """
     34    template_name_suffix = '_form'
     35
     36
     37
     38class DeleteView(SingleObjectTemplateResponseMixin, BaseDeleteView):
     39    """
     40    View for deleting an object retrieved with `self.get_object()`,
     41    with a response rendered by template.
     42    """
     43    template_name_suffix = '_confirm_delete'
     44
     45
     46class FormSetView(TemplateResponseMixin, BaseFormSetView):
     47    """
     48    A view for displaying formsets, and rendering a template response
     49    """
     50
     51
     52class ModelFormSetView(TemplateResponseMixin, BaseModelFormSetView):
     53    """
     54    A view for displaying model formsets, and rendering a template response
     55    """
     56
     57
     58class InlineFormSetView(SingleObjectTemplateResponseMixin,
     59                         BaseInlineFormSetView):
     60    """
     61    A view for displaying a model instance with it's inline formsets, and
     62    rendering a template response
     63    """
     64    template_name_suffix = '_form'
     65
  • new file django/views/generic/edit/base.py

    diff --git a/django/views/generic/edit/base.py b/django/views/generic/edit/base.py
    new file mode 100644
    index 0000000..43fc2eb
    - +  
     1from django.views.generic.detail import BaseDetailView
     2from django.views.generic.base import View
     3from django.views.generic.edit.forms import (FormMixin, ModelFormMixin,
     4                                             ProcessFormView, DeletionMixin, )
     5from django.views.generic.edit.formset import (FormSetMixin, ModelFormSetMixin,
     6                                        InlineFormSetMixin, ProcessFormSetView,
     7                                        ProcessInlineFormSetView, )
     8
     9
     10class BaseFormView(FormMixin, ProcessFormView):
     11    """
     12    A base view for displaying a form
     13    """
     14
     15
     16class BaseCreateView(ModelFormMixin, ProcessFormView):
     17    """
     18    Base view for creating an new object instance.
     19
     20    Using this base class requires subclassing to provide a response mixin.
     21    """
     22    def get(self, request, *args, **kwargs):
     23        self.object = None
     24        return super(BaseCreateView, self).get(request, *args, **kwargs)
     25
     26    def post(self, request, *args, **kwargs):
     27        self.object = None
     28        return super(BaseCreateView, self).post(request, *args, **kwargs)
     29
     30
     31class BaseUpdateView(ModelFormMixin, ProcessFormView):
     32    """
     33    Base view for updating an existing object.
     34
     35    Using this base class requires subclassing to provide a response mixin.
     36    """
     37    def get(self, request, *args, **kwargs):
     38        self.object = self.get_object()
     39        return super(BaseUpdateView, self).get(request, *args, **kwargs)
     40
     41    def post(self, request, *args, **kwargs):
     42        self.object = self.get_object()
     43        return super(BaseUpdateView, self).post(request, *args, **kwargs)
     44
     45
     46class BaseDeleteView(DeletionMixin, BaseDetailView):
     47    """
     48    Base view for deleting an object.
     49
     50    Using this base class requires subclassing to provide a response mixin.
     51    """
     52
     53
     54class BaseFormSetView(FormSetMixin, ProcessFormSetView):
     55    """
     56    A base view for displaying formsets
     57    """
     58
     59
     60class BaseModelFormSetView(ModelFormSetMixin, ProcessFormSetView):
     61    """
     62    A base view for displaying model formsets
     63    """
     64
     65
     66class BaseInlineFormSetView(InlineFormSetMixin, ProcessInlineFormSetView):
     67    """
     68    A base view for displaying a model instance with it's inline formsets
     69    """
  • new file django/views/generic/edit/forms.py

    diff --git a/django/views/generic/edit/forms.py b/django/views/generic/edit/forms.py
    new file mode 100644
    index 0000000..50ad970
    - +  
     1from django.forms import models as model_forms
     2from django.core.exceptions import ImproperlyConfigured
     3from django.http import HttpResponseRedirect
     4from django.views.generic.base import View
     5from django.views.generic.detail import SingleObjectMixin
     6
     7
     8class FormMixin(object):
     9    """
     10    A mixin that provides a way to show and handle a form in a request.
     11    """
     12
     13    initial = {}
     14    form_class = None
     15    success_url = None
     16
     17    def get_initial(self):
     18        """
     19        Returns the initial data to use for forms on this view.
     20        """
     21        return self.initial
     22
     23    def get_form_class(self):
     24        """
     25        Returns the form class to use in this view
     26        """
     27        return self.form_class
     28
     29    def get_form(self, form_class):
     30        """
     31        Returns an instance of the form to be used in this view.
     32        """
     33        return form_class(**self.get_form_kwargs())
     34
     35    def get_form_kwargs(self):
     36        """
     37        Returns the keyword arguments for instanciating the form.
     38        """
     39        kwargs = {'initial': self.get_initial()}
     40        if self.request.method in ('POST', 'PUT'):
     41            kwargs.update({
     42                'data': self.request.POST,
     43                'files': self.request.FILES,
     44            })
     45        return kwargs
     46
     47    def get_context_data(self, **kwargs):
     48        return kwargs
     49
     50    def get_success_url(self):
     51        if self.success_url:
     52            url = self.success_url
     53        else:
     54            raise ImproperlyConfigured(
     55                "No URL to redirect to. Provide a success_url.")
     56        return url
     57
     58    def form_valid(self, form):
     59        return HttpResponseRedirect(self.get_success_url())
     60
     61    def form_invalid(self, form):
     62        return self.render_to_response(self.get_context_data(form=form))
     63
     64
     65class ModelFormMixin(FormMixin, SingleObjectMixin):
     66    """
     67    A mixin that provides a way to show and handle a modelform in a request.
     68    """
     69
     70    def get_form_class(self):
     71        """
     72        Returns the form class to use in this view
     73        """
     74        if self.form_class:
     75            return self.form_class
     76        else:
     77            if self.model is not None:
     78                # If a model has been explicitly provided, use it
     79                model = self.model
     80            elif hasattr(self, 'object') and self.object is not None:
     81                # If this view is operating on a single object, use
     82                # the class of that object
     83                model = self.object.__class__
     84            else:
     85                # Try to get a queryset and extract the model class
     86                # from that
     87                model = self.get_queryset().model
     88            return model_forms.modelform_factory(model)
     89
     90    def get_form_kwargs(self):
     91        """
     92        Returns the keyword arguments for instanciating the form.
     93        """
     94        kwargs = super(ModelFormMixin, self).get_form_kwargs()
     95        kwargs.update({'instance': self.object})
     96        return kwargs
     97
     98    def get_success_url(self):
     99        if self.success_url:
     100            url = self.success_url % self.object.__dict__
     101        else:
     102            try:
     103                url = self.object.get_absolute_url()
     104            except AttributeError:
     105                raise ImproperlyConfigured(
     106                    "No URL to redirect to.  Either provide a url or define"
     107                    " a get_absolute_url method on the Model.")
     108        return url
     109
     110    def form_valid(self, form):
     111        self.object = form.save()
     112        return super(ModelFormMixin, self).form_valid(form)
     113
     114    def get_context_data(self, **kwargs):
     115        context = kwargs
     116        if self.object:
     117            context['object'] = self.object
     118            context_object_name = self.get_context_object_name(self.object)
     119            if context_object_name:
     120                context[context_object_name] = self.object
     121        return context
     122
     123
     124class ProcessFormView(View):
     125    """
     126    A mixin that processes a form on POST.
     127    """
     128    def get(self, request, *args, **kwargs):
     129        form_class = self.get_form_class()
     130        form = self.get_form(form_class)
     131        return self.render_to_response(self.get_context_data(form=form))
     132
     133    def post(self, request, *args, **kwargs):
     134        form_class = self.get_form_class()
     135        form = self.get_form(form_class)
     136        if form.is_valid():
     137            return self.form_valid(form)
     138        else:
     139            return self.form_invalid(form)
     140
     141    # PUT is a valid HTTP verb for creating (with a known URL) or editing an
     142    # object, note that browsers only support POST for now.
     143    def put(self, *args, **kwargs):
     144        return self.post(*args, **kwargs)
     145
     146
     147class DeletionMixin(object):
     148    """
     149    A mixin providing the ability to delete objects
     150    """
     151    success_url = None
     152
     153    def delete(self, request, *args, **kwargs):
     154        self.object = self.get_object()
     155        self.object.delete()
     156        return HttpResponseRedirect(self.get_success_url())
     157
     158    # Add support for browsers which only accept GET and POST for now.
     159    def post(self, *args, **kwargs):
     160        return self.delete(*args, **kwargs)
     161
     162    def get_success_url(self):
     163        if self.success_url:
     164            return self.success_url
     165        else:
     166            raise ImproperlyConfigured(
     167                "No URL to redirect to. Provide a success_url.")
  • new file django/views/generic/edit/formset.py

    diff --git a/django/views/generic/edit/formset.py b/django/views/generic/edit/formset.py
    new file mode 100644
    index 0000000..4d8b2f8
    - +  
     1from django.http import HttpResponseRedirect
     2from django.core.exceptions import ImproperlyConfigured
     3from django.forms.formsets import formset_factory, BaseFormSet, all_valid
     4from django.forms.models import (modelformset_factory, inlineformset_factory,
     5                                 BaseModelFormSet, BaseInlineFormSet, ModelForm)
     6from django.views.generic.edit.forms import ModelFormMixin
     7from django.views.generic.base import View
     8
     9
     10class EnhancedFormSet(object):
     11    """
     12    A base class for generic formsets
     13    """
     14
     15    form_class = None
     16    formset_class = BaseFormSet
     17
     18    # formset_factory kwargs
     19    extra = 3
     20    can_order = False
     21    can_delete = False
     22    max_num = None
     23
     24    def get_base_formset(self, **kwargs):
     25        """
     26        Returns the base formset
     27        """
     28        new_kwargs = self.get_kwargs()
     29        new_kwargs.update(**kwargs)
     30        return self.get_factory()(**new_kwargs)
     31
     32    def get_factory(self):
     33        """
     34        Returns the factory used to construct the formsets
     35        """
     36        return formset_factory
     37
     38    def get_form_class(self):
     39        return self.form_class
     40
     41    def get_formset_class(self):
     42        return self.formset_class
     43
     44    def get_kwargs(self):
     45        return {'form': self.get_form_class(),
     46                'formset': self.get_formset_class(),
     47                'extra': self.extra,
     48                'can_order': self.can_order,
     49                'can_delete': self.can_delete,
     50                'max_num': self.max_num, }
     51
     52
     53class EnhancedModelFormSet(EnhancedFormSet):
     54    """
     55    A base class for generic model formsets
     56    """
     57    # TODO: provide a hook for formfield_callback
     58
     59    form_class = ModelForm
     60    formset_class = BaseModelFormSet
     61    model = None
     62    queryset = None
     63    fields = None
     64    exclude = None
     65
     66    def get_factory(self):
     67        return modelformset_factory
     68
     69    def get_model(self):
     70        if self.model:
     71            return self.model
     72        else:
     73            try:
     74                return self.get_form_class().Meta.model
     75            except AttributeError:
     76                raise ImproperlyConfigured(
     77                "No model to create the modelformset. Provide one.")
     78
     79    def get_queryset(self):
     80        return self.queryset
     81
     82    def get_fields(self):
     83        return self.fields
     84
     85    def get_exclude(self):
     86        return self.exclude
     87
     88    def get_kwargs(self):
     89        kwargs = super(EnhancedModelFormSet, self).get_kwargs()
     90        kwargs.update({
     91            'model': self.get_model(),
     92            'fields': self.get_fields(),
     93            'exclude': self.get_exclude(),
     94        })
     95        return kwargs
     96
     97
     98class EnhancedInlineFormSet(EnhancedModelFormSet):
     99    """
     100    A base class for generic inline formsets
     101    """
     102
     103    fk_name = None
     104    formset_class = BaseInlineFormSet
     105
     106    def get_factory(self):
     107        return inlineformset_factory
     108
     109    def get_fk_name(self):
     110        return self.fk_name
     111
     112    def get_kwargs(self):
     113        kwargs = super(EnhancedInlineFormSet, self).get_kwargs()
     114        kwargs.update({
     115            'fk_name': self.get_fk_name(),
     116        })
     117        return kwargs
     118
     119
     120class FormSetMixin(object):
     121    """
     122    A mixin that provides a way to show and handle formsets
     123    """
     124
     125    formsets = []  # must be a list of BaseGenericFormSet
     126    success_url = None
     127
     128    def __init__(self, *args, **kwargs):
     129        self.instantiate_enhanced_formsets()
     130
     131    def instantiate_enhanced_formsets(self):
     132        """
     133        Instantiates the enhanced formsets
     134        """
     135        self.enhanced_formsets_instances = []
     136        for formset in self.formsets:
     137            enhanced_formset_instance = formset()
     138            self.enhanced_formsets_instances.append(enhanced_formset_instance)
     139
     140    def construct_formsets(self):
     141        """
     142        Constructs the formsets
     143        """
     144        self.formsets_instances = []
     145
     146        prefixes = {}
     147        for enhanced_formset in self.enhanced_formsets_instances:
     148            base_formset = enhanced_formset.get_base_formset(
     149                **self.get_factory_kwargs())
     150
     151            # calculate prefix
     152            prefix = base_formset.get_default_prefix()
     153            prefixes[prefix] = prefixes.get(prefix, 0) + 1
     154            if prefixes[prefix] != 1:
     155                prefix = "%s-%s" % (prefix, prefixes[prefix])
     156
     157            self.formsets_instances.append(
     158                base_formset(prefix=prefix, **self.get_formsets_kwargs(
     159                    enhanced_formset))
     160            )
     161
     162    def get_factory_kwargs(self):
     163        """
     164        Returns the keyword arguments for the formsets factory
     165        """
     166        return {}
     167
     168    def get_formsets_kwargs(self, enhanced_formset):
     169        """"
     170        Returns the keyword arguments for instanciating the formsets
     171        """
     172
     173        # default kwargs
     174        kwargs = {}
     175
     176        if self.request.method in ('POST', 'PUT'):
     177            kwargs.update({
     178                'data': self.request.POST,
     179                'files': self.request.FILES,
     180            })
     181        return kwargs
     182
     183    def get_context_data(self, **kwargs):
     184        context_data = {
     185            'formsets': [formset for formset in self.formsets_instances],
     186        }
     187
     188        context_data.update(kwargs)
     189        return context_data
     190
     191    def get_success_url(self):
     192        if self.success_url:
     193            url = self.success_url
     194        else:
     195            raise ImproperlyConfigured(
     196                "No URL to redirect to. Provide a success_url")
     197        return url
     198
     199    def formsets_valid(self):
     200        return HttpResponseRedirect(self.get_success_url())
     201
     202    def formsets_invalid(self):
     203        return self.render_to_response(self.get_context_data())
     204
     205
     206class ModelFormSetMixin(FormSetMixin):
     207    """
     208    A mixin that provides a way to show and handle model formsets
     209    """
     210
     211    def get_formsets_kwargs(self, enhanced_formset):
     212        """"
     213        Returns the keyword arguments for instanciating the model formsets
     214        """
     215        kwargs = super(ModelFormSetMixin, self).get_formsets_kwargs(
     216                                                    enhanced_formset)
     217        kwargs.update({
     218            'queryset': enhanced_formset.get_queryset()
     219        })
     220        return kwargs
     221
     222    def formsets_valid(self):
     223        # FIXME: beware of m2m
     224        for formset in self.formsets_instances:
     225            formset.save()
     226        return super(ModelFormSetMixin, self).formsets_valid()
     227
     228
     229class InlineFormSetMixin(ModelFormSetMixin, ModelFormMixin):
     230    """
     231    A mixin that provides a way to show and handle a model with it's inline
     232    formsets
     233    """
     234    def get_formsets_kwargs(self, enhanced_formset):
     235        """"
     236        Returns the keyword arguments for instanciating the inline formsets
     237        """
     238        kwargs = super(InlineFormSetMixin, self).get_formsets_kwargs(
     239                                                    enhanced_formset)
     240        kwargs.update({
     241            'instance': self.object
     242        })
     243        return kwargs
     244
     245    def get_context_data(self, **kwargs):
     246        """
     247        Adds the context data from both parents
     248        """
     249        context_data = ModelFormSetMixin.get_context_data(self)
     250        context_data.update(ModelFormMixin.get_context_data(self, **kwargs))
     251        return context_data
     252
     253    def get_factory_kwargs(self):
     254        """
     255        Returns the keyword arguments for the formsets factory
     256        """
     257        return {
     258            'parent_model': self.object.__class__,
     259        }
     260
     261    def form_valid(self, form):
     262        self.object.save()
     263        form.save_m2m()
     264        for formset in self.formsets_instances:
     265            formset.save()
     266
     267        return HttpResponseRedirect(self.get_success_url())
     268
     269    def form_invalid(self, form):
     270        return self.render_to_response(self.get_context_data(form=form))
     271
     272
     273class ProcessFormSetView(View):
     274    """
     275    A mixin that processes formsets on POST
     276    """
     277    def get(self, request, *args, **kwargs):
     278        self.construct_formsets()
     279        return self.render_to_response(self.get_context_data())
     280
     281    def post(self, request, *args, **kwargs):
     282        self.construct_formsets()
     283        if all_valid(self.formsets_instances):
     284            return self.formsets_valid()
     285        else:
     286            return self.formsets_invalid()
     287
     288    def put(self, request, *args, **kwargs):
     289        return self.post(*args, **kwargs)
     290
     291
     292class ProcessInlineFormSetView(View):
     293    """
     294    A mixin that processes a model instance and it's inline formsets on POST
     295    """
     296
     297    def get(self, request, *args, **kwargs):
     298        # Create or Update
     299        try:
     300            self.object = self.get_object()
     301        except AttributeError:
     302            self.object = self.model()
     303
     304        # ProcessFormView
     305        form_class = self.get_form_class()
     306        form = self.get_form(form_class)
     307
     308        # ProcessFormSetView
     309        self.construct_formsets()
     310
     311        return self.render_to_response(self.get_context_data(form=form))
     312
     313    def post(self, request, *args, **kwargs):
     314        # Create or Update
     315        try:
     316            self.object = self.get_object()
     317        except AttributeError:
     318            self.object = self.model()
     319
     320        # ProcessFormView
     321        form_class = self.get_form_class()
     322        form = self.get_form(form_class)
     323
     324        if form.is_valid():
     325            self.object = form.save(commit=False)
     326
     327            # ProcessFormSetViewV
     328            self.construct_formsets()
     329
     330            if all_valid(self.formsets_instances):
     331                return self.form_valid(form)
     332        else:
     333            # ProcessFormSetViewV
     334            self.construct_formsets()
     335        return self.form_invalid(form)
     336
     337
     338    def put(self, request, *args, **kwargs):
     339        return self.post(*args, **kwargs)
     340
  • tests/regressiontests/generic_views/forms.py

    diff --git a/tests/regressiontests/generic_views/forms.py b/tests/regressiontests/generic_views/forms.py
    index 7200947..b6ee715 100644
    a b  
    11from django import forms
     2from django.forms.formsets import formset_factory
    23
    3 from regressiontests.generic_views.models import Author
     4from django.views.generic import (EnhancedFormSet, EnhancedModelFormSet,
     5                                     EnhancedInlineFormSet, )
     6from regressiontests.generic_views.models import Author, Article
    47
    58
    69class AuthorForm(forms.ModelForm):
    class AuthorForm(forms.ModelForm):  
    912
    1013    class Meta:
    1114        model = Author
     15
     16
     17class ArticleForm(forms.ModelForm):
     18    class Meta:
     19        model = Article
     20        exclude = ('author', )
     21
     22
     23class ArticleEnhancedFormSet(EnhancedFormSet):
     24    form_class = ArticleForm
     25
     26
     27class AuthorEnhancedFormSet(EnhancedFormSet):
     28    form_class = AuthorForm
     29
     30
     31class ArticleEnhancedModelFormSet(EnhancedModelFormSet):
     32    model = Article
     33
     34
     35class AuthorEnhancedModelFormSet(EnhancedModelFormSet):
     36    model = Author
     37
     38
     39class ArticleEnhancedInlineFormSet(EnhancedInlineFormSet):
     40    model = Article
  • new file tests/regressiontests/generic_views/formsets.py

    diff --git a/tests/regressiontests/generic_views/formsets.py b/tests/regressiontests/generic_views/formsets.py
    new file mode 100644
    index 0000000..be39734
    - +  
     1from django.test import TestCase
     2from django.core.exceptions import ImproperlyConfigured
     3from django.views.generic.formsets import (FormSetMixin, ModelFormSetMixin,
     4        EnhancedModelFormSet, )
     5from regressiontests.generic_views.models import Author, Article
     6
     7
     8class FormSetViewTests(TestCase):
     9    urls = 'regressiontests.generic_views.urls'
     10
     11    def setUp(self):
     12        self.data = {
     13            'form-TOTAL_FORMS': u'3',
     14            'form-INITIAL_FORMS': u'0',
     15            'form-MAX_NUM_FORMS': u'',
     16            'form-0-title': u'',
     17            'form-0-pubdate': u'',
     18            'form-1-title': u'',
     19            'form-1-pubdate': u'',
     20            'form-2-title': u'',
     21            'form-2-pubdate': u'',
     22
     23            'form-2-TOTAL_FORMS': u'3',
     24            'form-2-INITIAL_FORMS': u'0',
     25            'form-2-MAX_NUM_FORMS': u'',
     26            'form-2-0-name': u'',
     27            'form-2-0-slug': u'',
     28            'form-2-1-name': u'',
     29            'form-2-1-slug': u'',
     30            'form-2-2-name': u'',
     31            'form-2-2-slug': u'',
     32        }
     33
     34    def test_get(self):
     35        response = self.client.get('/edit/formsets/')
     36        self.assertEqual(response.status_code, 200)
     37
     38    def test_empty_post(self):
     39        response = self.client.post('/edit/formsets/', self.data)
     40        self.assertEqual(response.status_code, 302)
     41
     42    def test_valid(self):
     43        self.data.update({
     44            'form-0-title': u'first title',
     45            'form-0-pubdate': u'2011-01-13',
     46            'form-1-title': u'second title',
     47            'form-1-pubdate': u'2011-01-13',
     48            'form-2-0-name': u'this is my name',
     49            'form-2-0-slug': u'this-is-my-name',
     50        })
     51        response = self.client.post('/edit/formsets/', self.data)
     52        self.assertEqual(response.status_code, 302)
     53
     54    def test_invalid(self):
     55        self.data.update({
     56            'form-0-title': u'first title',
     57            'form-0-pubdate': u'',
     58        })
     59        response = self.client.post('/edit/formsets/', self.data)
     60        self.assertEqual(response.status_code, 200)
     61        self.assertContains(response, 'ERROR')
     62
     63
     64class ModelFormSetTests(TestCase):
     65    def test_no_model_no_form_class(self):
     66        formset = EnhancedModelFormSet()
     67        self.assertRaises(ImproperlyConfigured, formset.get_model)
     68
     69
     70class ModelFormSetViewTests(TestCase):
     71    urls = 'regressiontests.generic_views.urls'
     72
     73    def setUp(self):
     74        self.data = {
     75            'form-TOTAL_FORMS': u'3',
     76            'form-INITIAL_FORMS': u'0',
     77            'form-MAX_NUM_FORMS': u'',
     78            'form-0-title': u'',
     79            'form-0-pubdate': u'',
     80            'form-1-title': u'',
     81            'form-1-pubdate': u'',
     82            'form-2-title': u'',
     83            'form-2-pubdate': u'',
     84
     85            'form-2-TOTAL_FORMS': u'3',
     86            'form-2-INITIAL_FORMS': u'0',
     87            'form-2-MAX_NUM_FORMS': u'',
     88            'form-2-0-name': u'',
     89            'form-2-1-name': u'',
     90            'form-2-2-name': u'',
     91        }
     92
     93    def test_get(self):
     94        response = self.client.get('/edit/modelformsets/')
     95        self.assertEqual(response.status_code, 200)
     96
     97    def test_empty_post(self):
     98        response = self.client.post('/edit/modelformsets/', self.data)
     99        self.assertEqual(response.status_code, 302)
     100
     101    def test_valid(self):
     102        self.data.update({
     103            'form-0-title': u'first title',
     104            'form-0-pubdate': u'2011-01-13',
     105            'form-1-title': u'second title',
     106            'form-1-pubdate': u'2011-01-13',
     107            'form-2-0-name': u'this is my name',
     108            'form-2-0-slug': u'this-is-my-name',
     109        })
     110        response = self.client.post('/edit/modelformsets/', self.data)
     111        self.assertEqual(Article.objects.count(), 2)
     112        self.assertEqual(Author.objects.count(), 1)
     113        self.assertEqual(response.status_code, 302)
     114
     115    def test_invalid(self):
     116        self.data.update({
     117            'form-0-title': u'first title',
     118            'form-0-pubdate': u'',
     119        })
     120        response = self.client.post('/edit/modelformsets/', self.data)
     121        self.assertEqual(Article.objects.count(), 0)
     122        self.assertEqual(Author.objects.count(), 0)
     123        self.assertEqual(response.status_code, 200)
     124        self.assertContains(response, 'ERROR')
     125
     126
     127class InlineFormSetViewTests(TestCase):
     128    urls = 'regressiontests.generic_views.urls'
     129
     130    def setUp(self):
     131        self.formsetmgmt = {
     132            'article_set-TOTAL_FORMS': u'3',
     133            'article_set-INITIAL_FORMS': u'0',
     134            'article_set-MAX_NUM_FORMS': u'',
     135        }
     136        self.formsetdata = {
     137            'article_set-0-title': u'title1',
     138            'article_set-0-pubdate': u'2011-01-26',
     139            'article_set-1-title': u'title2',
     140            'article_set-1-pubdate': u'2011-01-26',
     141            'article_set-2-title': u'title3',
     142            'article_set-2-pubdate': u'2011-01-26',
     143        }
     144        self.formdata = {
     145            'name': u'this is my name',
     146            'slug': u'this-is-my-name',
     147        }
     148
     149    def test_get(self):
     150        response = self.client.get('/edit/inlineformsets/')
     151        self.assertEqual(response.status_code, 200)
     152
     153    def test_empty_post(self):
     154        data = {
     155            'article_set-TOTAL_FORMS': u'3',
     156            'article_set-INITIAL_FORMS': u'0',
     157            'article_set-MAX_NUM_FORMS': u'',
     158            'article_set-0-title': u'',
     159            'article_set-0-pubdate': u'',
     160            'article_set-1-title': u'',
     161            'article_set-1-pubdate': u'',
     162            'article_set-2-title': u'',
     163            'article_set-2-pubdate': u'',
     164            'name': u'',
     165        }
     166        response = self.client.post('/edit/inlineformsets/', data)
     167        self.assertEqual(response.status_code, 200)
     168
     169    def test_valid(self):
     170        data = self.formdata
     171        data.update(self.formsetdata)
     172        data.update(self.formsetmgmt)
     173        response = self.client.post('/edit/inlineformsets/', data)
     174        self.assertEqual(Article.objects.count(), 3)
     175        self.assertEqual(Author.objects.count(), 1)
     176        self.assertEqual(response.status_code, 302)
     177
     178    def test_no_form(self):
     179        data = self.formsetdata
     180        data.update(self.formsetmgmt)
     181        response = self.client.post('/edit/inlineformsets/', data)
     182        self.assertEqual(Article.objects.count(), 0)
     183        self.assertEqual(Author.objects.count(), 0)
     184        self.assertEqual(response.status_code, 200)
     185        self.assertContains(response, 'ERROR')
     186
     187    def test_no_formset(self):
     188        data = self.formdata
     189        data.update(self.formsetmgmt)
     190        response = self.client.post('/edit/inlineformsets/', data)
     191        self.assertEqual(Article.objects.count(), 0)
     192        self.assertEqual(Author.objects.count(), 1)
     193        self.assertEqual(response.status_code, 302)
  • tests/regressiontests/generic_views/models.py

    diff --git a/tests/regressiontests/generic_views/models.py b/tests/regressiontests/generic_views/models.py
    index 5445e24..01433fe 100644
    a b  
    11from django.db import models
    22
     3
    34class Artist(models.Model):
    45    name = models.CharField(max_length=100)
    56
    class Artist(models.Model):  
    1516    def get_absolute_url(self):
    1617        return ('artist_detail', (), {'pk': self.id})
    1718
     19
    1820class Author(models.Model):
    1921    name = models.CharField(max_length=100)
    2022    slug = models.SlugField()
    class Author(models.Model):  
    2527    def __unicode__(self):
    2628        return self.name
    2729
     30
    2831class Book(models.Model):
    2932    name = models.CharField(max_length=300)
    3033    slug = models.SlugField()
    class Book(models.Model):  
    3841    def __unicode__(self):
    3942        return self.name
    4043
     44
    4145class Page(models.Model):
    4246    content = models.TextField()
    4347    template = models.CharField(max_length=300)
     48
     49
     50class Article(models.Model):
     51    title = models.CharField(max_length=100)
     52    pubdate = models.DateField()
     53    author = models.ForeignKey(Author, blank=True, null=True)
  • new file tests/regressiontests/generic_views/templates/authors_articles.html

    diff --git a/tests/regressiontests/generic_views/templates/authors_articles.html b/tests/regressiontests/generic_views/templates/authors_articles.html
    new file mode 100644
    index 0000000..7c9eca3
    - +  
     1{% for formset in formsets %}
     2    {% if formset.errors %}
     3    ERROR
     4    {{formset.errors}}
     5    {% endif %}
     6    {% for form in formset %}
     7    {{form.as_p}}
     8    {% endfor %}
     9    {{formset.management_form}}
     10{% endfor %}
  • tests/regressiontests/generic_views/tests.py

    diff --git a/tests/regressiontests/generic_views/tests.py b/tests/regressiontests/generic_views/tests.py
    index a4010aa..7f4b79f 100644
    a b from regressiontests.generic_views.dates import ArchiveIndexViewTests, YearArchi  
    33from regressiontests.generic_views.detail import DetailViewTest
    44from regressiontests.generic_views.edit import ModelFormMixinTests, CreateViewTests, UpdateViewTests, DeleteViewTests
    55from regressiontests.generic_views.list import ListViewTests
     6from regressiontests.generic_views.formsets import FormSetViewTests, ModelFormSetTests, ModelFormSetViewTests, InlineFormSetViewTests
  • tests/regressiontests/generic_views/urls.py

    diff --git a/tests/regressiontests/generic_views/urls.py b/tests/regressiontests/generic_views/urls.py
    index 067c1f6..e14a252 100644
    a b urlpatterns = patterns('',  
    208208        views.BookDetail.as_view(allow_future=True)),
    209209    (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/nopk/$',
    210210        views.BookDetail.as_view()),
    211 
    212211    (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/byslug/(?P<slug>[\w-]+)/$',
    213212        views.BookDetail.as_view()),
    214213
     214    # FormSet
     215    (r'^edit/formsets/$',
     216        views.AuthorsArticlesView.as_view()),
     217    (r'^edit/modelformsets/$',
     218        views.AuthorsArticlesModelsView.as_view()),
     219    (r'^edit/inlineformsets/$',
     220        views.AuthorsInlinesView.as_view()),
     221
    215222    # Useful for testing redirects
    216223    (r'^accounts/login/$',  'django.contrib.auth.views.login')
    217224)
  • tests/regressiontests/generic_views/views.py

    diff --git a/tests/regressiontests/generic_views/views.py b/tests/regressiontests/generic_views/views.py
    index 0c8fd49..58fd9d5 100644
    a b from django.utils.decorators import method_decorator  
    55from django.views import generic
    66
    77from regressiontests.generic_views.models import Artist, Author, Book, Page
    8 from regressiontests.generic_views.forms import AuthorForm
    9 
     8from regressiontests.generic_views.forms import (AuthorForm,
     9        ArticleEnhancedFormSet, AuthorEnhancedFormSet,
     10        ArticleEnhancedModelFormSet, AuthorEnhancedModelFormSet,
     11        ArticleEnhancedInlineFormSet, )
    1012
    1113class CustomTemplateView(generic.TemplateView):
    1214    template_name = 'generic_views/about.html'
    class BookDetail(BookConfig, generic.DateDetailView):  
    177179class AuthorGetQuerySetFormView(generic.edit.ModelFormMixin):
    178180    def get_queryset(self):
    179181        return Author.objects.all()
     182
     183
     184class AuthorsArticlesView(generic.FormSetView):
     185    formsets = [ArticleEnhancedFormSet, AuthorEnhancedFormSet, ]
     186    template_name = 'authors_articles.html'
     187    success_url = '/list/authors/'
     188   
     189
     190class AuthorsArticlesModelsView(generic.ModelFormSetView):
     191    formsets = [ArticleEnhancedModelFormSet, AuthorEnhancedModelFormSet, ]
     192    template_name = 'authors_articles.html'
     193    success_url = '/list/authors/'
     194
     195
     196class AuthorsInlinesView(generic.InlineFormSetView):
     197    formsets = [ArticleEnhancedInlineFormSet, ]
     198    template_name = 'authors_articles.html'
     199    success_url = '/list/authors/'
     200    model = Author
Back to Top