Ticket #16256: ticket16256-with-admin-InlineAdmin-refactor-with-EnhancedInlineFormSet.patch
File ticket16256-with-admin-InlineAdmin-refactor-with-EnhancedInlineFormSet.patch, 45.8 KB (added by , 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: 412 412 Luciano Ramalho 413 413 Amit Ramon <amit.ramon@gmail.com> 414 414 Philippe Raoult <philippe.raoult@n2nsoft.com> 415 Iván Raskovsky <raskovsky@gmail.com> 415 416 Massimiliano Ravelli <massimiliano.ravelli@gmail.com> 416 417 Brian Ray <http://brianray.chipy.org/> 417 418 Ł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 25 25 from django.utils.translation import ugettext as _ 26 26 from django.utils.translation import ungettext 27 27 from django.utils.encoding import force_unicode 28 from django.views.generic import EnhancedInlineFormSet 28 29 29 30 HORIZONTAL, VERTICAL = 1, 2 30 31 # returns the <ul> class for a given radio_admin field … … class ModelAdmin(BaseModelAdmin): 1293 1294 "admin/object_history.html" 1294 1295 ], context, current_app=self.admin_site.name) 1295 1296 1296 class InlineModelAdmin(BaseModelAdmin ):1297 class InlineModelAdmin(BaseModelAdmin, EnhancedInlineFormSet): 1297 1298 """ 1298 1299 Options for inline editing of ``model`` instances. 1299 1300 … … class InlineModelAdmin(BaseModelAdmin): 1301 1302 ``model`` to its parent. This is required if ``model`` has more than one 1302 1303 ``ForeignKey`` to its parent. 1303 1304 """ 1304 model = None1305 fk_name = None1306 1305 formset = BaseInlineFormSet 1307 extra = 31308 max_num = None1309 1306 template = None 1310 1307 verbose_name = None 1311 1308 verbose_name_plural = None 1312 can_delete = True 1309 can_delete = True # True in EnhancedInlineFormSet 1313 1310 1314 1311 def __init__(self, parent_model, admin_site): 1315 1312 self.admin_site = admin_site … … class InlineModelAdmin(BaseModelAdmin): 1334 1331 1335 1332 def get_formset(self, request, obj=None, **kwargs): 1336 1333 """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 [] 1346 1336 exclude.extend(self.get_readonly_fields(request, obj)) 1347 # if exclude is an empty list we use None, since that's the actual1348 # default1349 1337 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 = { 1355 1340 "exclude": exclude, 1356 1341 "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, 1360 1343 } 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) 1363 1347 1364 1348 def get_fieldsets(self, request, obj=None): 1365 1349 if self.declared_fieldsets: … … class InlineModelAdmin(BaseModelAdmin): 1368 1352 fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj)) 1369 1353 return [(None, {'fields': fields})] 1370 1354 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 1371 1368 class StackedInline(InlineModelAdmin): 1372 1369 template = 'admin/edit_inline/stacked.html' 1373 1370 -
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 6 6 from django.views.generic.edit import FormView, CreateView, UpdateView, DeleteView 7 7 from django.views.generic.list import ListView 8 8 9 from django.views.generic.formsets import (FormSetView, ModelFormSetView, 10 InlineFormSetView, EnhancedFormSet, 11 EnhancedModelFormSet, EnhancedInlineFormSet, ) 12 9 13 10 14 class GenericViewError(Exception): 11 15 """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_forms2 from django.core.exceptions import ImproperlyConfigured3 from django.http import HttpResponseRedirect4 from django.views.generic.base import TemplateResponseMixin, View5 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 = None16 success_url = None17 18 def get_initial(self):19 """20 Returns the initial data to use for forms on this view.21 """22 return self.initial23 24 def get_form_class(self):25 """26 Returns the form class to use in this view27 """28 return self.form_class29 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 kwargs47 48 def get_context_data(self, **kwargs):49 return kwargs50 51 def get_success_url(self):52 if self.success_url:53 url = self.success_url54 else:55 raise ImproperlyConfigured(56 "No URL to redirect to. Provide a success_url.")57 return url58 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 view74 """75 if self.form_class:76 return self.form_class77 else:78 if self.model is not None:79 # If a model has been explicitly provided, use it80 model = self.model81 elif hasattr(self, 'object') and self.object is not None:82 # If this view is operating on a single object, use83 # the class of that object84 model = self.object.__class__85 else:86 # Try to get a queryset and extract the model class87 # from that88 model = self.get_queryset().model89 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 kwargs98 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 url110 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 = kwargs117 if self.object:118 context['object'] = self.object119 context_object_name = self.get_context_object_name(self.object)120 if context_object_name:121 context[context_object_name] = self.object122 return context123 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 an143 # 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 form151 """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 = None168 return super(BaseCreateView, self).get(request, *args, **kwargs)169 170 def post(self, request, *args, **kwargs):171 self.object = None172 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 objects209 """210 success_url = None211 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_url224 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
- + 1 from django.views.generic.detail import SingleObjectTemplateResponseMixin 2 from django.views.generic.base import TemplateResponseMixin 3 from django.views.generic.edit.base import (BaseFormView, BaseCreateView, 4 BaseUpdateView, BaseDeleteView, BaseFormSetView, 5 BaseModelFormSetView, BaseInlineFormSetView, ) 6 from django.views.generic.edit.forms import (FormMixin, ModelFormMixin, 7 ProcessFormView, DeletionMixin, ) 8 from django.views.generic.edit.formset import (EnhancedFormSet, 9 EnhancedModelFormSet, EnhancedInlineFormSet, 10 FormSetMixin, ModelFormSetMixin, 11 InlineFormSetMixin, ProcessFormSetView, 12 ProcessInlineFormSetView, ) 13 14 15 class FormView(TemplateResponseMixin, BaseFormView): 16 """ 17 A view for displaying a form, and rendering a template response. 18 """ 19 20 21 class 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 29 class 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 38 class 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 46 class FormSetView(TemplateResponseMixin, BaseFormSetView): 47 """ 48 A view for displaying formsets, and rendering a template response 49 """ 50 51 52 class ModelFormSetView(TemplateResponseMixin, BaseModelFormSetView): 53 """ 54 A view for displaying model formsets, and rendering a template response 55 """ 56 57 58 class 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
- + 1 from django.views.generic.detail import BaseDetailView 2 from django.views.generic.base import View 3 from django.views.generic.edit.forms import (FormMixin, ModelFormMixin, 4 ProcessFormView, DeletionMixin, ) 5 from django.views.generic.edit.formset import (FormSetMixin, ModelFormSetMixin, 6 InlineFormSetMixin, ProcessFormSetView, 7 ProcessInlineFormSetView, ) 8 9 10 class BaseFormView(FormMixin, ProcessFormView): 11 """ 12 A base view for displaying a form 13 """ 14 15 16 class 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 31 class 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 46 class 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 54 class BaseFormSetView(FormSetMixin, ProcessFormSetView): 55 """ 56 A base view for displaying formsets 57 """ 58 59 60 class BaseModelFormSetView(ModelFormSetMixin, ProcessFormSetView): 61 """ 62 A base view for displaying model formsets 63 """ 64 65 66 class 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
- + 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 View 5 from django.views.generic.detail import SingleObjectMixin 6 7 8 class 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 65 class 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 124 class 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 147 class 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
- + 1 from django.http import HttpResponseRedirect 2 from django.core.exceptions import ImproperlyConfigured 3 from django.forms.formsets import formset_factory, BaseFormSet, all_valid 4 from django.forms.models import (modelformset_factory, inlineformset_factory, 5 BaseModelFormSet, BaseInlineFormSet, ModelForm) 6 from django.views.generic.edit.forms import ModelFormMixin 7 from django.views.generic.base import View 8 9 10 class 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 53 class 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 98 class 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 120 class 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 206 class 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 229 class 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 273 class 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 292 class 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 1 1 from django import forms 2 from django.forms.formsets import formset_factory 2 3 3 from regressiontests.generic_views.models import Author 4 from django.views.generic import (EnhancedFormSet, EnhancedModelFormSet, 5 EnhancedInlineFormSet, ) 6 from regressiontests.generic_views.models import Author, Article 4 7 5 8 6 9 class AuthorForm(forms.ModelForm): … … class AuthorForm(forms.ModelForm): 9 12 10 13 class Meta: 11 14 model = Author 15 16 17 class ArticleForm(forms.ModelForm): 18 class Meta: 19 model = Article 20 exclude = ('author', ) 21 22 23 class ArticleEnhancedFormSet(EnhancedFormSet): 24 form_class = ArticleForm 25 26 27 class AuthorEnhancedFormSet(EnhancedFormSet): 28 form_class = AuthorForm 29 30 31 class ArticleEnhancedModelFormSet(EnhancedModelFormSet): 32 model = Article 33 34 35 class AuthorEnhancedModelFormSet(EnhancedModelFormSet): 36 model = Author 37 38 39 class 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
- + 1 from django.test import TestCase 2 from django.core.exceptions import ImproperlyConfigured 3 from django.views.generic.formsets import (FormSetMixin, ModelFormSetMixin, 4 EnhancedModelFormSet, ) 5 from regressiontests.generic_views.models import Author, Article 6 7 8 class 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 64 class ModelFormSetTests(TestCase): 65 def test_no_model_no_form_class(self): 66 formset = EnhancedModelFormSet() 67 self.assertRaises(ImproperlyConfigured, formset.get_model) 68 69 70 class 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 127 class 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 1 1 from django.db import models 2 2 3 3 4 class Artist(models.Model): 4 5 name = models.CharField(max_length=100) 5 6 … … class Artist(models.Model): 15 16 def get_absolute_url(self): 16 17 return ('artist_detail', (), {'pk': self.id}) 17 18 19 18 20 class Author(models.Model): 19 21 name = models.CharField(max_length=100) 20 22 slug = models.SlugField() … … class Author(models.Model): 25 27 def __unicode__(self): 26 28 return self.name 27 29 30 28 31 class Book(models.Model): 29 32 name = models.CharField(max_length=300) 30 33 slug = models.SlugField() … … class Book(models.Model): 38 41 def __unicode__(self): 39 42 return self.name 40 43 44 41 45 class Page(models.Model): 42 46 content = models.TextField() 43 47 template = models.CharField(max_length=300) 48 49 50 class 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 3 3 from regressiontests.generic_views.detail import DetailViewTest 4 4 from regressiontests.generic_views.edit import ModelFormMixinTests, CreateViewTests, UpdateViewTests, DeleteViewTests 5 5 from regressiontests.generic_views.list import ListViewTests 6 from 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('', 208 208 views.BookDetail.as_view(allow_future=True)), 209 209 (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/nopk/$', 210 210 views.BookDetail.as_view()), 211 212 211 (r'^dates/books/(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\d{1,2})/byslug/(?P<slug>[\w-]+)/$', 213 212 views.BookDetail.as_view()), 214 213 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 215 222 # Useful for testing redirects 216 223 (r'^accounts/login/$', 'django.contrib.auth.views.login') 217 224 ) -
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 5 5 from django.views import generic 6 6 7 7 from regressiontests.generic_views.models import Artist, Author, Book, Page 8 from regressiontests.generic_views.forms import AuthorForm 9 8 from regressiontests.generic_views.forms import (AuthorForm, 9 ArticleEnhancedFormSet, AuthorEnhancedFormSet, 10 ArticleEnhancedModelFormSet, AuthorEnhancedModelFormSet, 11 ArticleEnhancedInlineFormSet, ) 10 12 11 13 class CustomTemplateView(generic.TemplateView): 12 14 template_name = 'generic_views/about.html' … … class BookDetail(BookConfig, generic.DateDetailView): 177 179 class AuthorGetQuerySetFormView(generic.edit.ModelFormMixin): 178 180 def get_queryset(self): 179 181 return Author.objects.all() 182 183 184 class AuthorsArticlesView(generic.FormSetView): 185 formsets = [ArticleEnhancedFormSet, AuthorEnhancedFormSet, ] 186 template_name = 'authors_articles.html' 187 success_url = '/list/authors/' 188 189 190 class AuthorsArticlesModelsView(generic.ModelFormSetView): 191 formsets = [ArticleEnhancedModelFormSet, AuthorEnhancedModelFormSet, ] 192 template_name = 'authors_articles.html' 193 success_url = '/list/authors/' 194 195 196 class AuthorsInlinesView(generic.InlineFormSetView): 197 formsets = [ArticleEnhancedInlineFormSet, ] 198 template_name = 'authors_articles.html' 199 success_url = '/list/authors/' 200 model = Author