app/django/forms/formsets.py
changeset 323 ff1a9aa48cfd
equal deleted inserted replaced
322:6641e941ef1e 323:ff1a9aa48cfd
       
     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, 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 'form'
       
    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         # initialization is different depending on whether we recieved data, initial, or nothing
       
    44         if data or files:
       
    45             self.management_form = ManagementForm(data, auto_id=self.auto_id, prefix=self.prefix)
       
    46             if self.management_form.is_valid():
       
    47                 self._total_form_count = self.management_form.cleaned_data[TOTAL_FORM_COUNT]
       
    48                 self._initial_form_count = self.management_form.cleaned_data[INITIAL_FORM_COUNT]
       
    49             else:
       
    50                 raise ValidationError('ManagementForm data is missing or has been tampered with')
       
    51         else:
       
    52             if initial:
       
    53                 self._initial_form_count = len(initial)
       
    54                 if self._initial_form_count > self.max_num and self.max_num > 0:
       
    55                     self._initial_form_count = self.max_num
       
    56                 self._total_form_count = self._initial_form_count + self.extra
       
    57             else:
       
    58                 self._initial_form_count = 0
       
    59                 self._total_form_count = self.extra
       
    60             if self._total_form_count > self.max_num and self.max_num > 0:
       
    61                 self._total_form_count = self.max_num
       
    62             initial = {TOTAL_FORM_COUNT: self._total_form_count,
       
    63                        INITIAL_FORM_COUNT: self._initial_form_count}
       
    64             self.management_form = ManagementForm(initial=initial, auto_id=self.auto_id, prefix=self.prefix)
       
    65         
       
    66         # construct the forms in the formset
       
    67         self._construct_forms()
       
    68 
       
    69     def __unicode__(self):
       
    70         return self.as_table()
       
    71 
       
    72     def _construct_forms(self):
       
    73         # instantiate all the forms and put them in self.forms
       
    74         self.forms = []
       
    75         for i in xrange(self._total_form_count):
       
    76             self.forms.append(self._construct_form(i))
       
    77     
       
    78     def _construct_form(self, i, **kwargs):
       
    79         """
       
    80         Instantiates and returns the i-th form instance in a formset.
       
    81         """
       
    82         defaults = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
       
    83         if self.data or self.files:
       
    84             defaults['data'] = self.data
       
    85             defaults['files'] = self.files
       
    86         if self.initial:
       
    87             try:
       
    88                 defaults['initial'] = self.initial[i]
       
    89             except IndexError:
       
    90                 pass
       
    91         # Allow extra forms to be empty.
       
    92         if i >= self._initial_form_count:
       
    93             defaults['empty_permitted'] = True
       
    94         defaults.update(kwargs)
       
    95         form = self.form(**defaults)
       
    96         self.add_fields(form, i)
       
    97         return form
       
    98 
       
    99     def _get_initial_forms(self):
       
   100         """Return a list of all the intial forms in this formset."""
       
   101         return self.forms[:self._initial_form_count]
       
   102     initial_forms = property(_get_initial_forms)
       
   103 
       
   104     def _get_extra_forms(self):
       
   105         """Return a list of all the extra forms in this formset."""
       
   106         return self.forms[self._initial_form_count:]
       
   107     extra_forms = property(_get_extra_forms)
       
   108 
       
   109     # Maybe this should just go away?
       
   110     def _get_cleaned_data(self):
       
   111         """
       
   112         Returns a list of form.cleaned_data dicts for every form in self.forms.
       
   113         """
       
   114         if not self.is_valid():
       
   115             raise AttributeError("'%s' object has no attribute 'cleaned_data'" % self.__class__.__name__)
       
   116         return [form.cleaned_data for form in self.forms]
       
   117     cleaned_data = property(_get_cleaned_data)
       
   118 
       
   119     def _get_deleted_forms(self):
       
   120         """
       
   121         Returns a list of forms that have been marked for deletion. Raises an 
       
   122         AttributeError if deletion is not allowed.
       
   123         """
       
   124         if not self.is_valid() or not self.can_delete:
       
   125             raise AttributeError("'%s' object has no attribute 'deleted_forms'" % self.__class__.__name__)
       
   126         # construct _deleted_form_indexes which is just a list of form indexes
       
   127         # that have had their deletion widget set to True
       
   128         if not hasattr(self, '_deleted_form_indexes'):
       
   129             self._deleted_form_indexes = []
       
   130             for i in range(0, self._total_form_count):
       
   131                 form = self.forms[i]
       
   132                 # if this is an extra form and hasn't changed, don't consider it
       
   133                 if i >= self._initial_form_count and not form.has_changed():
       
   134                     continue
       
   135                 if form.cleaned_data[DELETION_FIELD_NAME]:
       
   136                     self._deleted_form_indexes.append(i)
       
   137         return [self.forms[i] for i in self._deleted_form_indexes]
       
   138     deleted_forms = property(_get_deleted_forms)
       
   139 
       
   140     def _get_ordered_forms(self):
       
   141         """
       
   142         Returns a list of form in the order specified by the incoming data.
       
   143         Raises an AttributeError if deletion is not allowed.
       
   144         """
       
   145         if not self.is_valid() or not self.can_order:
       
   146             raise AttributeError("'%s' object has no attribute 'ordered_forms'" % self.__class__.__name__)
       
   147         # Construct _ordering, which is a list of (form_index, order_field_value)
       
   148         # tuples. After constructing this list, we'll sort it by order_field_value
       
   149         # so we have a way to get to the form indexes in the order specified
       
   150         # by the form data.
       
   151         if not hasattr(self, '_ordering'):
       
   152             self._ordering = []
       
   153             for i in range(0, self._total_form_count):
       
   154                 form = self.forms[i]
       
   155                 # if this is an extra form and hasn't changed, don't consider it
       
   156                 if i >= self._initial_form_count and not form.has_changed():
       
   157                     continue
       
   158                 # don't add data marked for deletion to self.ordered_data
       
   159                 if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
       
   160                     continue
       
   161                 # A sort function to order things numerically ascending, but
       
   162                 # None should be sorted below anything else. Allowing None as
       
   163                 # a comparison value makes it so we can leave ordering fields
       
   164                 # blamk.
       
   165                 def compare_ordering_values(x, y):
       
   166                     if x[1] is None:
       
   167                         return 1
       
   168                     if y[1] is None:
       
   169                         return -1
       
   170                     return x[1] - y[1]
       
   171                 self._ordering.append((i, form.cleaned_data[ORDERING_FIELD_NAME]))
       
   172             # After we're done populating self._ordering, sort it.
       
   173             self._ordering.sort(compare_ordering_values)
       
   174         # Return a list of form.cleaned_data dicts in the order spcified by
       
   175         # the form data.
       
   176         return [self.forms[i[0]] for i in self._ordering]
       
   177     ordered_forms = property(_get_ordered_forms)
       
   178 
       
   179     def non_form_errors(self):
       
   180         """
       
   181         Returns an ErrorList of errors that aren't associated with a particular
       
   182         form -- i.e., from formset.clean(). Returns an empty ErrorList if there
       
   183         are none.
       
   184         """
       
   185         if self._non_form_errors is not None:
       
   186             return self._non_form_errors
       
   187         return self.error_class()
       
   188 
       
   189     def _get_errors(self):
       
   190         """
       
   191         Returns a list of form.errors for every form in self.forms.
       
   192         """
       
   193         if self._errors is None:
       
   194             self.full_clean()
       
   195         return self._errors
       
   196     errors = property(_get_errors)
       
   197 
       
   198     def is_valid(self):
       
   199         """
       
   200         Returns True if form.errors is empty for every form in self.forms.
       
   201         """
       
   202         if not self.is_bound:
       
   203             return False
       
   204         # We loop over every form.errors here rather than short circuiting on the
       
   205         # first failure to make sure validation gets triggered for every form.
       
   206         forms_valid = True
       
   207         for errors in self.errors:
       
   208             if bool(errors):
       
   209                 forms_valid = False
       
   210         return forms_valid and not bool(self.non_form_errors())
       
   211 
       
   212     def full_clean(self):
       
   213         """
       
   214         Cleans all of self.data and populates self._errors.
       
   215         """
       
   216         self._errors = []
       
   217         if not self.is_bound: # Stop further processing.
       
   218             return
       
   219         for i in range(0, self._total_form_count):
       
   220             form = self.forms[i]
       
   221             self._errors.append(form.errors)
       
   222         # Give self.clean() a chance to do cross-form validation.
       
   223         try:
       
   224             self.clean()
       
   225         except ValidationError, e:
       
   226             self._non_form_errors = e.messages
       
   227 
       
   228     def clean(self):
       
   229         """
       
   230         Hook for doing any extra formset-wide cleaning after Form.clean() has
       
   231         been called on every form. Any ValidationError raised by this method
       
   232         will not be associated with a particular form; it will be accesible
       
   233         via formset.non_form_errors()
       
   234         """
       
   235         pass
       
   236 
       
   237     def add_fields(self, form, index):
       
   238         """A hook for adding extra fields on to each form instance."""
       
   239         if self.can_order:
       
   240             # Only pre-fill the ordering field for initial forms.
       
   241             if index < self._initial_form_count:
       
   242                 form.fields[ORDERING_FIELD_NAME] = IntegerField(label=_(u'Order'), initial=index+1, required=False)
       
   243             else:
       
   244                 form.fields[ORDERING_FIELD_NAME] = IntegerField(label=_(u'Order'), required=False)
       
   245         if self.can_delete:
       
   246             form.fields[DELETION_FIELD_NAME] = BooleanField(label=_(u'Delete'), required=False)
       
   247 
       
   248     def add_prefix(self, index):
       
   249         return '%s-%s' % (self.prefix, index)
       
   250 
       
   251     def is_multipart(self):
       
   252         """
       
   253         Returns True if the formset needs to be multipart-encrypted, i.e. it
       
   254         has FileInput. Otherwise, False.
       
   255         """
       
   256         return self.forms[0].is_multipart()
       
   257 
       
   258     def _get_media(self):
       
   259         # All the forms on a FormSet are the same, so you only need to
       
   260         # interrogate the first form for media.
       
   261         if self.forms:
       
   262             return self.forms[0].media
       
   263         else:
       
   264             return Media()
       
   265     media = property(_get_media)
       
   266 
       
   267     def as_table(self):
       
   268         "Returns this formset rendered as HTML <tr>s -- excluding the <table></table>."
       
   269         # XXX: there is no semantic division between forms here, there
       
   270         # probably should be. It might make sense to render each form as a
       
   271         # table row with each field as a td.
       
   272         forms = u' '.join([form.as_table() for form in self.forms])
       
   273         return mark_safe(u'\n'.join([unicode(self.management_form), forms]))
       
   274 
       
   275 def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
       
   276                     can_delete=False, max_num=0):
       
   277     """Return a FormSet for the given form class."""
       
   278     attrs = {'form': form, 'extra': extra,
       
   279              'can_order': can_order, 'can_delete': can_delete,
       
   280              'max_num': max_num}
       
   281     return type(form.__name__ + 'FormSet', (formset,), attrs)
       
   282 
       
   283 def all_valid(formsets):
       
   284     """Returns true if every formset in formsets is valid."""
       
   285     valid = True
       
   286     for formset in formsets:
       
   287         if not formset.is_valid():
       
   288             valid = False
       
   289     return valid