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