web/lib/django/forms/formsets.py
changeset 38 77b6da96e6f1
equal deleted inserted replaced
37:8d941af65caf 38:77b6da96e6f1
       
     1 from forms import Form
       
     2 from django.core.exceptions import ValidationError
       
     3 from django.utils.encoding import StrAndUnicode
       
     4 from django.utils.safestring import mark_safe
       
     5 from django.utils.translation import ugettext as _
       
     6 from fields import IntegerField, BooleanField
       
     7 from widgets import Media, HiddenInput
       
     8 from util import ErrorList
       
     9 
       
    10 __all__ = ('BaseFormSet', 'all_valid')
       
    11 
       
    12 # special field names
       
    13 TOTAL_FORM_COUNT = 'TOTAL_FORMS'
       
    14 INITIAL_FORM_COUNT = 'INITIAL_FORMS'
       
    15 MAX_NUM_FORM_COUNT = 'MAX_NUM_FORMS'
       
    16 ORDERING_FIELD_NAME = 'ORDER'
       
    17 DELETION_FIELD_NAME = 'DELETE'
       
    18 
       
    19 class ManagementForm(Form):
       
    20     """
       
    21     ``ManagementForm`` is used to keep track of how many form instances
       
    22     are displayed on the page. If adding new forms via javascript, you should
       
    23     increment the count field of this form as well.
       
    24     """
       
    25     def __init__(self, *args, **kwargs):
       
    26         self.base_fields[TOTAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
       
    27         self.base_fields[INITIAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
       
    28         self.base_fields[MAX_NUM_FORM_COUNT] = IntegerField(required=False, widget=HiddenInput)
       
    29         super(ManagementForm, self).__init__(*args, **kwargs)
       
    30 
       
    31 class BaseFormSet(StrAndUnicode):
       
    32     """
       
    33     A collection of instances of the same Form class.
       
    34     """
       
    35     def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
       
    36                  initial=None, error_class=ErrorList):
       
    37         self.is_bound = data is not None or files is not None
       
    38         self.prefix = prefix or self.get_default_prefix()
       
    39         self.auto_id = auto_id
       
    40         self.data = data
       
    41         self.files = files
       
    42         self.initial = initial
       
    43         self.error_class = error_class
       
    44         self._errors = None
       
    45         self._non_form_errors = None
       
    46         # construct the forms in the formset
       
    47         self._construct_forms()
       
    48 
       
    49     def __unicode__(self):
       
    50         return self.as_table()
       
    51 
       
    52     def _management_form(self):
       
    53         """Returns the ManagementForm instance for this FormSet."""
       
    54         if self.data or self.files:
       
    55             form = ManagementForm(self.data, auto_id=self.auto_id, prefix=self.prefix)
       
    56             if not form.is_valid():
       
    57                 raise ValidationError('ManagementForm data is missing or has been tampered with')
       
    58         else:
       
    59             form = ManagementForm(auto_id=self.auto_id, prefix=self.prefix, initial={
       
    60                 TOTAL_FORM_COUNT: self.total_form_count(),
       
    61                 INITIAL_FORM_COUNT: self.initial_form_count(),
       
    62                 MAX_NUM_FORM_COUNT: self.max_num
       
    63             })
       
    64         return form
       
    65     management_form = property(_management_form)
       
    66 
       
    67     def total_form_count(self):
       
    68         """Returns the total number of forms in this FormSet."""
       
    69         if self.data or self.files:
       
    70             return self.management_form.cleaned_data[TOTAL_FORM_COUNT]
       
    71         else:
       
    72             initial_forms = self.initial_form_count()
       
    73             total_forms = initial_forms + self.extra
       
    74             # Allow all existing related objects/inlines to be displayed,
       
    75             # but don't allow extra beyond max_num.
       
    76             if initial_forms > self.max_num >= 0:
       
    77                 total_forms = initial_forms
       
    78             elif total_forms > self.max_num >= 0:
       
    79                 total_forms = self.max_num
       
    80         return total_forms
       
    81 
       
    82     def initial_form_count(self):
       
    83         """Returns the number of forms that are required in this FormSet."""
       
    84         if self.data or self.files:
       
    85             return self.management_form.cleaned_data[INITIAL_FORM_COUNT]
       
    86         else:
       
    87             # Use the length of the inital data if it's there, 0 otherwise.
       
    88             initial_forms = self.initial and len(self.initial) or 0
       
    89             if initial_forms > self.max_num >= 0:
       
    90                 initial_forms = self.max_num
       
    91         return initial_forms
       
    92 
       
    93     def _construct_forms(self):
       
    94         # instantiate all the forms and put them in self.forms
       
    95         self.forms = []
       
    96         for i in xrange(self.total_form_count()):
       
    97             self.forms.append(self._construct_form(i))
       
    98 
       
    99     def _construct_form(self, i, **kwargs):
       
   100         """
       
   101         Instantiates and returns the i-th form instance in a formset.
       
   102         """
       
   103         defaults = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
       
   104         if self.data or self.files:
       
   105             defaults['data'] = self.data
       
   106             defaults['files'] = self.files
       
   107         if self.initial:
       
   108             try:
       
   109                 defaults['initial'] = self.initial[i]
       
   110             except IndexError:
       
   111                 pass
       
   112         # Allow extra forms to be empty.
       
   113         if i >= self.initial_form_count():
       
   114             defaults['empty_permitted'] = True
       
   115         defaults.update(kwargs)
       
   116         form = self.form(**defaults)
       
   117         self.add_fields(form, i)
       
   118         return form
       
   119 
       
   120     def _get_initial_forms(self):
       
   121         """Return a list of all the initial forms in this formset."""
       
   122         return self.forms[:self.initial_form_count()]
       
   123     initial_forms = property(_get_initial_forms)
       
   124 
       
   125     def _get_extra_forms(self):
       
   126         """Return a list of all the extra forms in this formset."""
       
   127         return self.forms[self.initial_form_count():]
       
   128     extra_forms = property(_get_extra_forms)
       
   129 
       
   130     def _get_empty_form(self, **kwargs):
       
   131         defaults = {
       
   132             'auto_id': self.auto_id,
       
   133             'prefix': self.add_prefix('__prefix__'),
       
   134             'empty_permitted': True,
       
   135         }
       
   136         if self.data or self.files:
       
   137             defaults['data'] = self.data
       
   138             defaults['files'] = self.files
       
   139         defaults.update(kwargs)
       
   140         form = self.form(**defaults)
       
   141         self.add_fields(form, None)
       
   142         return form
       
   143     empty_form = property(_get_empty_form)
       
   144 
       
   145     # Maybe this should just go away?
       
   146     def _get_cleaned_data(self):
       
   147         """
       
   148         Returns a list of form.cleaned_data dicts for every form in self.forms.
       
   149         """
       
   150         if not self.is_valid():
       
   151             raise AttributeError("'%s' object has no attribute 'cleaned_data'" % self.__class__.__name__)
       
   152         return [form.cleaned_data for form in self.forms]
       
   153     cleaned_data = property(_get_cleaned_data)
       
   154 
       
   155     def _get_deleted_forms(self):
       
   156         """
       
   157         Returns a list of forms that have been marked for deletion. Raises an
       
   158         AttributeError if deletion is not allowed.
       
   159         """
       
   160         if not self.is_valid() or not self.can_delete:
       
   161             raise AttributeError("'%s' object has no attribute 'deleted_forms'" % self.__class__.__name__)
       
   162         # construct _deleted_form_indexes which is just a list of form indexes
       
   163         # that have had their deletion widget set to True
       
   164         if not hasattr(self, '_deleted_form_indexes'):
       
   165             self._deleted_form_indexes = []
       
   166             for i in range(0, self.total_form_count()):
       
   167                 form = self.forms[i]
       
   168                 # if this is an extra form and hasn't changed, don't consider it
       
   169                 if i >= self.initial_form_count() and not form.has_changed():
       
   170                     continue
       
   171                 if self._should_delete_form(form):
       
   172                     self._deleted_form_indexes.append(i)
       
   173         return [self.forms[i] for i in self._deleted_form_indexes]
       
   174     deleted_forms = property(_get_deleted_forms)
       
   175 
       
   176     def _get_ordered_forms(self):
       
   177         """
       
   178         Returns a list of form in the order specified by the incoming data.
       
   179         Raises an AttributeError if ordering is not allowed.
       
   180         """
       
   181         if not self.is_valid() or not self.can_order:
       
   182             raise AttributeError("'%s' object has no attribute 'ordered_forms'" % self.__class__.__name__)
       
   183         # Construct _ordering, which is a list of (form_index, order_field_value)
       
   184         # tuples. After constructing this list, we'll sort it by order_field_value
       
   185         # so we have a way to get to the form indexes in the order specified
       
   186         # by the form data.
       
   187         if not hasattr(self, '_ordering'):
       
   188             self._ordering = []
       
   189             for i in range(0, self.total_form_count()):
       
   190                 form = self.forms[i]
       
   191                 # if this is an extra form and hasn't changed, don't consider it
       
   192                 if i >= self.initial_form_count() and not form.has_changed():
       
   193                     continue
       
   194                 # don't add data marked for deletion to self.ordered_data
       
   195                 if self.can_delete and self._should_delete_form(form):
       
   196                     continue
       
   197                 self._ordering.append((i, form.cleaned_data[ORDERING_FIELD_NAME]))
       
   198             # After we're done populating self._ordering, sort it.
       
   199             # A sort function to order things numerically ascending, but
       
   200             # None should be sorted below anything else. Allowing None as
       
   201             # a comparison value makes it so we can leave ordering fields
       
   202             # blamk.
       
   203             def compare_ordering_values(x, y):
       
   204                 if x[1] is None:
       
   205                     return 1
       
   206                 if y[1] is None:
       
   207                     return -1
       
   208                 return x[1] - y[1]
       
   209             self._ordering.sort(compare_ordering_values)
       
   210         # Return a list of form.cleaned_data dicts in the order spcified by
       
   211         # the form data.
       
   212         return [self.forms[i[0]] for i in self._ordering]
       
   213     ordered_forms = property(_get_ordered_forms)
       
   214 
       
   215     #@classmethod
       
   216     def get_default_prefix(cls):
       
   217         return 'form'
       
   218     get_default_prefix = classmethod(get_default_prefix)
       
   219 
       
   220     def non_form_errors(self):
       
   221         """
       
   222         Returns an ErrorList of errors that aren't associated with a particular
       
   223         form -- i.e., from formset.clean(). Returns an empty ErrorList if there
       
   224         are none.
       
   225         """
       
   226         if self._non_form_errors is not None:
       
   227             return self._non_form_errors
       
   228         return self.error_class()
       
   229 
       
   230     def _get_errors(self):
       
   231         """
       
   232         Returns a list of form.errors for every form in self.forms.
       
   233         """
       
   234         if self._errors is None:
       
   235             self.full_clean()
       
   236         return self._errors
       
   237     errors = property(_get_errors)
       
   238 
       
   239     def _should_delete_form(self, form):
       
   240         # The way we lookup the value of the deletion field here takes
       
   241         # more code than we'd like, but the form's cleaned_data will
       
   242         # not exist if the form is invalid.
       
   243         field = form.fields[DELETION_FIELD_NAME]
       
   244         raw_value = form._raw_value(DELETION_FIELD_NAME)
       
   245         should_delete = field.clean(raw_value)
       
   246         return should_delete
       
   247 
       
   248     def is_valid(self):
       
   249         """
       
   250         Returns True if form.errors is empty for every form in self.forms.
       
   251         """
       
   252         if not self.is_bound:
       
   253             return False
       
   254         # We loop over every form.errors here rather than short circuiting on the
       
   255         # first failure to make sure validation gets triggered for every form.
       
   256         forms_valid = True
       
   257         for i in range(0, self.total_form_count()):
       
   258             form = self.forms[i]
       
   259             if self.can_delete:
       
   260                 if self._should_delete_form(form):
       
   261                     # This form is going to be deleted so any of its errors
       
   262                     # should not cause the entire formset to be invalid.
       
   263                     continue
       
   264             if bool(self.errors[i]):
       
   265                 forms_valid = False
       
   266         return forms_valid and not bool(self.non_form_errors())
       
   267 
       
   268     def full_clean(self):
       
   269         """
       
   270         Cleans all of self.data and populates self._errors.
       
   271         """
       
   272         self._errors = []
       
   273         if not self.is_bound: # Stop further processing.
       
   274             return
       
   275         for i in range(0, self.total_form_count()):
       
   276             form = self.forms[i]
       
   277             self._errors.append(form.errors)
       
   278         # Give self.clean() a chance to do cross-form validation.
       
   279         try:
       
   280             self.clean()
       
   281         except ValidationError, e:
       
   282             self._non_form_errors = self.error_class(e.messages)
       
   283 
       
   284     def clean(self):
       
   285         """
       
   286         Hook for doing any extra formset-wide cleaning after Form.clean() has
       
   287         been called on every form. Any ValidationError raised by this method
       
   288         will not be associated with a particular form; it will be accesible
       
   289         via formset.non_form_errors()
       
   290         """
       
   291         pass
       
   292 
       
   293     def add_fields(self, form, index):
       
   294         """A hook for adding extra fields on to each form instance."""
       
   295         if self.can_order:
       
   296             # Only pre-fill the ordering field for initial forms.
       
   297             if index is not None and index < self.initial_form_count():
       
   298                 form.fields[ORDERING_FIELD_NAME] = IntegerField(label=_(u'Order'), initial=index+1, required=False)
       
   299             else:
       
   300                 form.fields[ORDERING_FIELD_NAME] = IntegerField(label=_(u'Order'), required=False)
       
   301         if self.can_delete:
       
   302             form.fields[DELETION_FIELD_NAME] = BooleanField(label=_(u'Delete'), required=False)
       
   303 
       
   304     def add_prefix(self, index):
       
   305         return '%s-%s' % (self.prefix, index)
       
   306 
       
   307     def is_multipart(self):
       
   308         """
       
   309         Returns True if the formset needs to be multipart-encrypted, i.e. it
       
   310         has FileInput. Otherwise, False.
       
   311         """
       
   312         return self.forms and self.forms[0].is_multipart()
       
   313 
       
   314     def _get_media(self):
       
   315         # All the forms on a FormSet are the same, so you only need to
       
   316         # interrogate the first form for media.
       
   317         if self.forms:
       
   318             return self.forms[0].media
       
   319         else:
       
   320             return Media()
       
   321     media = property(_get_media)
       
   322 
       
   323     def as_table(self):
       
   324         "Returns this formset rendered as HTML <tr>s -- excluding the <table></table>."
       
   325         # XXX: there is no semantic division between forms here, there
       
   326         # probably should be. It might make sense to render each form as a
       
   327         # table row with each field as a td.
       
   328         forms = u' '.join([form.as_table() for form in self.forms])
       
   329         return mark_safe(u'\n'.join([unicode(self.management_form), forms]))
       
   330 
       
   331 def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
       
   332                     can_delete=False, max_num=None):
       
   333     """Return a FormSet for the given form class."""
       
   334     attrs = {'form': form, 'extra': extra,
       
   335              'can_order': can_order, 'can_delete': can_delete,
       
   336              'max_num': max_num}
       
   337     return type(form.__name__ + 'FormSet', (formset,), attrs)
       
   338 
       
   339 def all_valid(formsets):
       
   340     """Returns true if every formset in formsets is valid."""
       
   341     valid = True
       
   342     for formset in formsets:
       
   343         if not formset.is_valid():
       
   344             valid = False
       
   345     return valid