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