summaryrefslogtreecommitdiff
path: root/django/newforms
diff options
context:
space:
mode:
Diffstat (limited to 'django/newforms')
-rw-r--r--django/newforms/__init__.py1
-rw-r--r--django/newforms/forms.py51
-rw-r--r--django/newforms/formsets.py292
-rw-r--r--django/newforms/models.py222
-rw-r--r--django/newforms/widgets.py170
5 files changed, 724 insertions, 12 deletions
diff --git a/django/newforms/__init__.py b/django/newforms/__init__.py
index 0d9c68f9e0..99631e4e8f 100644
--- a/django/newforms/__init__.py
+++ b/django/newforms/__init__.py
@@ -15,3 +15,4 @@ from widgets import *
from fields import *
from forms import *
from models import *
+from formsets import * \ No newline at end of file
diff --git a/django/newforms/forms.py b/django/newforms/forms.py
index fc203f36b5..753ee254bc 100644
--- a/django/newforms/forms.py
+++ b/django/newforms/forms.py
@@ -10,7 +10,7 @@ from django.utils.encoding import StrAndUnicode, smart_unicode, force_unicode
from django.utils.safestring import mark_safe
from fields import Field, FileField
-from widgets import TextInput, Textarea
+from widgets import Media, media_property, TextInput, Textarea
from util import flatatt, ErrorDict, ErrorList, ValidationError
__all__ = ('BaseForm', 'Form')
@@ -31,6 +31,7 @@ def get_declared_fields(bases, attrs, with_base_fields=True):
If 'with_base_fields' is True, all fields from the bases are used.
Otherwise, only fields in the 'declared_fields' attribute on the bases are
used. The distinction is useful in ModelForm subclassing.
+ Also integrates any additional media definitions
"""
fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)]
fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter))
@@ -56,8 +57,11 @@ class DeclarativeFieldsMetaclass(type):
"""
def __new__(cls, name, bases, attrs):
attrs['base_fields'] = get_declared_fields(bases, attrs)
- return super(DeclarativeFieldsMetaclass,
+ new_class = super(DeclarativeFieldsMetaclass,
cls).__new__(cls, name, bases, attrs)
+ if 'media' not in attrs:
+ new_class.media = media_property(new_class)
+ return new_class
class BaseForm(StrAndUnicode):
# This is the main implementation of all the Form logic. Note that this
@@ -65,7 +69,8 @@ class BaseForm(StrAndUnicode):
# information. Any improvements to the form API should be made to *this*
# class, not to the Form class.
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
- initial=None, error_class=ErrorList, label_suffix=':'):
+ initial=None, error_class=ErrorList, label_suffix=':',
+ empty_permitted=False):
self.is_bound = data is not None or files is not None
self.data = data or {}
self.files = files or {}
@@ -74,7 +79,9 @@ class BaseForm(StrAndUnicode):
self.initial = initial or {}
self.error_class = error_class
self.label_suffix = label_suffix
+ self.empty_permitted = empty_permitted
self._errors = None # Stores the errors after clean() has been called.
+ self._changed_data = None
# The base_fields class attribute is the *class-wide* definition of
# fields. Because a particular *instance* of the class might want to
@@ -194,6 +201,10 @@ class BaseForm(StrAndUnicode):
if not self.is_bound: # Stop further processing.
return
self.cleaned_data = {}
+ # If the form is permitted to be empty, and none of the form data has
+ # changed from the initial data, short circuit any validation.
+ if self.empty_permitted and not self.has_changed():
+ return
for name, field in self.fields.items():
# value_from_datadict() gets the data from the data dictionaries.
# Each widget type knows how to retrieve its own data, because some
@@ -229,6 +240,40 @@ class BaseForm(StrAndUnicode):
"""
return self.cleaned_data
+ def has_changed(self):
+ """
+ Returns True if data differs from initial.
+ """
+ return bool(self.changed_data)
+
+ def _get_changed_data(self):
+ if self._changed_data is None:
+ self._changed_data = []
+ # XXX: For now we're asking the individual widgets whether or not the
+ # data has changed. It would probably be more efficient to hash the
+ # initial data, store it in a hidden field, and compare a hash of the
+ # submitted data, but we'd need a way to easily get the string value
+ # for a given field. Right now, that logic is embedded in the render
+ # method of each widget.
+ for name, field in self.fields.items():
+ prefixed_name = self.add_prefix(name)
+ data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name)
+ initial_value = self.initial.get(name, field.initial)
+ if field.widget._has_changed(initial_value, data_value):
+ self._changed_data.append(name)
+ return self._changed_data
+ changed_data = property(_get_changed_data)
+
+ def _get_media(self):
+ """
+ Provide a description of all media required to render the widgets on this form
+ """
+ media = Media()
+ for field in self.fields.values():
+ media = media + field.widget.media
+ return media
+ media = property(_get_media)
+
def is_multipart(self):
"""
Returns True if the form needs to be multipart-encrypted, i.e. it has
diff --git a/django/newforms/formsets.py b/django/newforms/formsets.py
new file mode 100644
index 0000000000..1ae27bf58c
--- /dev/null
+++ b/django/newforms/formsets.py
@@ -0,0 +1,292 @@
+from forms import Form
+from django.utils.encoding import StrAndUnicode
+from django.utils.safestring import mark_safe
+from fields import IntegerField, BooleanField
+from widgets import Media, HiddenInput, TextInput
+from util import ErrorList, ValidationError
+
+__all__ = ('BaseFormSet', 'all_valid')
+
+# special field names
+TOTAL_FORM_COUNT = 'TOTAL_FORMS'
+INITIAL_FORM_COUNT = 'INITIAL_FORMS'
+MAX_FORM_COUNT = 'MAX_FORMS'
+ORDERING_FIELD_NAME = 'ORDER'
+DELETION_FIELD_NAME = 'DELETE'
+
+class ManagementForm(Form):
+ """
+ ``ManagementForm`` is used to keep track of how many form instances
+ are displayed on the page. If adding new forms via javascript, you should
+ increment the count field of this form as well.
+ """
+ def __init__(self, *args, **kwargs):
+ self.base_fields[TOTAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
+ self.base_fields[INITIAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
+ self.base_fields[MAX_FORM_COUNT] = IntegerField(widget=HiddenInput)
+ super(ManagementForm, self).__init__(*args, **kwargs)
+
+class BaseFormSet(StrAndUnicode):
+ """
+ A collection of instances of the same Form class.
+ """
+ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
+ initial=None, error_class=ErrorList):
+ self.is_bound = data is not None or files is not None
+ self.prefix = prefix or 'form'
+ self.auto_id = auto_id
+ self.data = data
+ self.files = files
+ self.initial = initial
+ self.error_class = error_class
+ self._errors = None
+ self._non_form_errors = None
+ # initialization is different depending on whether we recieved data, initial, or nothing
+ if data or files:
+ self.management_form = ManagementForm(data, auto_id=self.auto_id, prefix=self.prefix)
+ if self.management_form.is_valid():
+ self._total_form_count = self.management_form.cleaned_data[TOTAL_FORM_COUNT]
+ self._initial_form_count = self.management_form.cleaned_data[INITIAL_FORM_COUNT]
+ self._max_form_count = self.management_form.cleaned_data[MAX_FORM_COUNT]
+ else:
+ raise ValidationError('ManagementForm data is missing or has been tampered with')
+ else:
+ if initial:
+ self._initial_form_count = len(initial)
+ if self._initial_form_count > self._max_form_count and self._max_form_count > 0:
+ self._initial_form_count = self._max_form_count
+ self._total_form_count = self._initial_form_count + self.extra
+ else:
+ self._initial_form_count = 0
+ self._total_form_count = self.extra
+ if self._total_form_count > self._max_form_count and self._max_form_count > 0:
+ self._total_form_count = self._max_form_count
+ initial = {TOTAL_FORM_COUNT: self._total_form_count,
+ INITIAL_FORM_COUNT: self._initial_form_count,
+ MAX_FORM_COUNT: self._max_form_count}
+ self.management_form = ManagementForm(initial=initial, auto_id=self.auto_id, prefix=self.prefix)
+
+ # construct the forms in the formset
+ self._construct_forms()
+
+ def __unicode__(self):
+ return self.as_table()
+
+ def _construct_forms(self):
+ # instantiate all the forms and put them in self.forms
+ self.forms = []
+ for i in xrange(self._total_form_count):
+ self.forms.append(self._construct_form(i))
+
+ def _construct_form(self, i, **kwargs):
+ """
+ Instantiates and returns the i-th form instance in a formset.
+ """
+ defaults = {'auto_id': self.auto_id, 'prefix': self.add_prefix(i)}
+ if self.data or self.files:
+ defaults['data'] = self.data
+ defaults['files'] = self.files
+ if self.initial:
+ try:
+ defaults['initial'] = self.initial[i]
+ except IndexError:
+ pass
+ # Allow extra forms to be empty.
+ if i >= self._initial_form_count:
+ defaults['empty_permitted'] = True
+ defaults.update(kwargs)
+ form = self.form(**defaults)
+ self.add_fields(form, i)
+ return form
+
+ def _get_initial_forms(self):
+ """Return a list of all the intial forms in this formset."""
+ return self.forms[:self._initial_form_count]
+ initial_forms = property(_get_initial_forms)
+
+ def _get_extra_forms(self):
+ """Return a list of all the extra forms in this formset."""
+ return self.forms[self._initial_form_count:]
+ extra_forms = property(_get_extra_forms)
+
+ # Maybe this should just go away?
+ def _get_cleaned_data(self):
+ """
+ Returns a list of form.cleaned_data dicts for every form in self.forms.
+ """
+ if not self.is_valid():
+ raise AttributeError("'%s' object has no attribute 'cleaned_data'" % self.__class__.__name__)
+ return [form.cleaned_data for form in self.forms]
+ cleaned_data = property(_get_cleaned_data)
+
+ def _get_deleted_forms(self):
+ """
+ Returns a list of forms that have been marked for deletion. Raises an
+ AttributeError is deletion is not allowed.
+ """
+ if not self.is_valid() or not self.can_delete:
+ raise AttributeError("'%s' object has no attribute 'deleted_forms'" % self.__class__.__name__)
+ # construct _deleted_form_indexes which is just a list of form indexes
+ # that have had their deletion widget set to True
+ if not hasattr(self, '_deleted_form_indexes'):
+ self._deleted_form_indexes = []
+ for i in range(0, self._total_form_count):
+ form = self.forms[i]
+ # if this is an extra form and hasn't changed, don't consider it
+ if i >= self._initial_form_count and not form.has_changed():
+ continue
+ if form.cleaned_data[DELETION_FIELD_NAME]:
+ self._deleted_form_indexes.append(i)
+ return [self.forms[i] for i in self._deleted_form_indexes]
+ deleted_forms = property(_get_deleted_forms)
+
+ def _get_ordered_forms(self):
+ """
+ Returns a list of form in the order specified by the incoming data.
+ Raises an AttributeError is deletion is not allowed.
+ """
+ if not self.is_valid() or not self.can_order:
+ raise AttributeError("'%s' object has no attribute 'ordered_forms'" % self.__class__.__name__)
+ # Construct _ordering, which is a list of (form_index, order_field_value)
+ # tuples. After constructing this list, we'll sort it by order_field_value
+ # so we have a way to get to the form indexes in the order specified
+ # by the form data.
+ if not hasattr(self, '_ordering'):
+ self._ordering = []
+ for i in range(0, self._total_form_count):
+ form = self.forms[i]
+ # if this is an extra form and hasn't changed, don't consider it
+ if i >= self._initial_form_count and not form.has_changed():
+ continue
+ # don't add data marked for deletion to self.ordered_data
+ if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
+ continue
+ # A sort function to order things numerically ascending, but
+ # None should be sorted below anything else. Allowing None as
+ # a comparison value makes it so we can leave ordering fields
+ # blamk.
+ def compare_ordering_values(x, y):
+ if x[1] is None:
+ return 1
+ if y[1] is None:
+ return -1
+ return x[1] - y[1]
+ self._ordering.append((i, form.cleaned_data[ORDERING_FIELD_NAME]))
+ # After we're done populating self._ordering, sort it.
+ self._ordering.sort(compare_ordering_values)
+ # Return a list of form.cleaned_data dicts in the order spcified by
+ # the form data.
+ return [self.forms[i[0]] for i in self._ordering]
+ ordered_forms = property(_get_ordered_forms)
+
+ def non_form_errors(self):
+ """
+ Returns an ErrorList of errors that aren't associated with a particular
+ form -- i.e., from formset.clean(). Returns an empty ErrorList if there
+ are none.
+ """
+ if self._non_form_errors is not None:
+ return self._non_form_errors
+ return self.error_class()
+
+ def _get_errors(self):
+ """
+ Returns a list of form.errors for every form in self.forms.
+ """
+ if self._errors is None:
+ self.full_clean()
+ return self._errors
+ errors = property(_get_errors)
+
+ def is_valid(self):
+ """
+ Returns True if form.errors is empty for every form in self.forms.
+ """
+ if not self.is_bound:
+ return False
+ # We loop over every form.errors here rather than short circuiting on the
+ # first failure to make sure validation gets triggered for every form.
+ forms_valid = True
+ for errors in self.errors:
+ if bool(errors):
+ forms_valid = False
+ return forms_valid and not bool(self.non_form_errors())
+
+ def full_clean(self):
+ """
+ Cleans all of self.data and populates self._errors.
+ """
+ self._errors = []
+ if not self.is_bound: # Stop further processing.
+ return
+ for i in range(0, self._total_form_count):
+ form = self.forms[i]
+ self._errors.append(form.errors)
+ # Give self.clean() a chance to do cross-form validation.
+ try:
+ self.clean()
+ except ValidationError, e:
+ self._non_form_errors = e.messages
+
+ def clean(self):
+ """
+ Hook for doing any extra formset-wide cleaning after Form.clean() has
+ been called on every form. Any ValidationError raised by this method
+ will not be associated with a particular form; it will be accesible
+ via formset.non_form_errors()
+ """
+ pass
+
+ def add_fields(self, form, index):
+ """A hook for adding extra fields on to each form instance."""
+ if self.can_order:
+ # Only pre-fill the ordering field for initial forms.
+ if index < self._initial_form_count:
+ form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', initial=index+1, required=False)
+ else:
+ form.fields[ORDERING_FIELD_NAME] = IntegerField(label='Order', required=False)
+ if self.can_delete:
+ form.fields[DELETION_FIELD_NAME] = BooleanField(label='Delete', required=False)
+
+ def add_prefix(self, index):
+ return '%s-%s' % (self.prefix, index)
+
+ def is_multipart(self):
+ """
+ Returns True if the formset needs to be multipart-encrypted, i.e. it
+ has FileInput. Otherwise, False.
+ """
+ return self.forms[0].is_multipart()
+
+ def _get_media(self):
+ # All the forms on a FormSet are the same, so you only need to
+ # interrogate the first form for media.
+ if self.forms:
+ return self.forms[0].media
+ else:
+ return Media()
+ media = property(_get_media)
+
+ def as_table(self):
+ "Returns this formset rendered as HTML <tr>s -- excluding the <table></table>."
+ # XXX: there is no semantic division between forms here, there
+ # probably should be. It might make sense to render each form as a
+ # table row with each field as a td.
+ forms = u' '.join([form.as_table() for form in self.forms])
+ return mark_safe(u'\n'.join([unicode(self.management_form), forms]))
+
+def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False,
+ can_delete=False, max_num=0):
+ """Return a FormSet for the given form class."""
+ attrs = {'form': form, 'extra': extra,
+ 'can_order': can_order, 'can_delete': can_delete,
+ '_max_form_count': max_num}
+ return type(form.__name__ + 'FormSet', (formset,), attrs)
+
+def all_valid(formsets):
+ """Returns true if every formset in formsets is valid."""
+ valid = True
+ for formset in formsets:
+ if not formset.is_valid():
+ valid = False
+ return valid
diff --git a/django/newforms/models.py b/django/newforms/models.py
index c3938d9ae7..43e2978ba8 100644
--- a/django/newforms/models.py
+++ b/django/newforms/models.py
@@ -12,13 +12,15 @@ from django.core.exceptions import ImproperlyConfigured
from util import ValidationError, ErrorList
from forms import BaseForm, get_declared_fields
-from fields import Field, ChoiceField, EMPTY_VALUES
-from widgets import Select, SelectMultiple, MultipleHiddenInput
+from fields import Field, ChoiceField, IntegerField, EMPTY_VALUES
+from widgets import Select, SelectMultiple, HiddenInput, MultipleHiddenInput
+from widgets import media_property
+from formsets import BaseFormSet, formset_factory, DELETION_FIELD_NAME
__all__ = (
'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
- 'ModelChoiceField', 'ModelMultipleChoiceField'
+ 'ModelChoiceField', 'ModelMultipleChoiceField',
)
def save_instance(form, instance, fields=None, fail_message='saved',
@@ -30,7 +32,7 @@ def save_instance(form, instance, fields=None, fail_message='saved',
database. Returns ``instance``.
"""
from django.db import models
- opts = instance.__class__._meta
+ opts = instance._meta
if form.errors:
raise ValueError("The %s could not be %s because the data didn't"
" validate." % (opts.object_name, fail_message))
@@ -44,7 +46,7 @@ def save_instance(form, instance, fields=None, fail_message='saved',
f.save_form_data(instance, cleaned_data[f.name])
# Wrap up the saving of m2m data as a function.
def save_m2m():
- opts = instance.__class__._meta
+ opts = instance._meta
cleaned_data = form.cleaned_data
for f in opts.many_to_many:
if fields and f.name not in fields:
@@ -226,6 +228,8 @@ class ModelFormMetaclass(type):
if not parents:
return new_class
+ if 'media' not in attrs:
+ new_class.media = media_property(new_class)
declared_fields = get_declared_fields(bases, attrs, False)
opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None))
if opts.model:
@@ -244,7 +248,7 @@ class ModelFormMetaclass(type):
class BaseModelForm(BaseForm):
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList, label_suffix=':',
- instance=None):
+ empty_permitted=False, instance=None):
opts = self._meta
if instance is None:
# if we didn't get an instance, instantiate a new one
@@ -256,7 +260,8 @@ class BaseModelForm(BaseForm):
# if initial was provided, it should override the values from instance
if initial is not None:
object_data.update(initial)
- BaseForm.__init__(self, data, files, auto_id, prefix, object_data, error_class, label_suffix)
+ BaseForm.__init__(self, data, files, auto_id, prefix, object_data,
+ error_class, label_suffix, empty_permitted)
def save(self, commit=True):
"""
@@ -275,6 +280,209 @@ class BaseModelForm(BaseForm):
class ModelForm(BaseModelForm):
__metaclass__ = ModelFormMetaclass
+def modelform_factory(model, form=ModelForm, fields=None, exclude=None,
+ formfield_callback=lambda f: f.formfield()):
+ # HACK: we should be able to construct a ModelForm without creating
+ # and passing in a temporary inner class
+ class Meta:
+ pass
+ setattr(Meta, 'model', model)
+ setattr(Meta, 'fields', fields)
+ setattr(Meta, 'exclude', exclude)
+ class_name = model.__name__ + 'Form'
+ return ModelFormMetaclass(class_name, (form,), {'Meta': Meta,
+ 'formfield_callback': formfield_callback})
+
+
+# ModelFormSets ##############################################################
+
+class BaseModelFormSet(BaseFormSet):
+ """
+ A ``FormSet`` for editing a queryset and/or adding new objects to it.
+ """
+ model = None
+
+ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
+ queryset=None, **kwargs):
+ self.queryset = queryset
+ defaults = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
+ if self._max_form_count > 0:
+ qs = self.get_queryset()[:self._max_form_count]
+ else:
+ qs = self.get_queryset()
+ defaults['initial'] = [model_to_dict(obj) for obj in qs]
+ defaults.update(kwargs)
+ super(BaseModelFormSet, self).__init__(**defaults)
+
+ def get_queryset(self):
+ if self.queryset is not None:
+ return self.queryset
+ return self.model._default_manager.get_query_set()
+
+ def save_new(self, form, commit=True):
+ """Saves and returns a new model instance for the given form."""
+ return save_instance(form, self.model(), commit=commit)
+
+ def save_existing(self, form, instance, commit=True):
+ """Saves and returns an existing model instance for the given form."""
+ return save_instance(form, instance, commit=commit)
+
+ def save(self, commit=True):
+ """Saves model instances for every form, adding and changing instances
+ as necessary, and returns the list of instances.
+ """
+ if not commit:
+ self.saved_forms = []
+ def save_m2m():
+ for form in self.saved_forms:
+ form.save_m2m()
+ self.save_m2m = save_m2m
+ return self.save_existing_objects(commit) + self.save_new_objects(commit)
+
+ def save_existing_objects(self, commit=True):
+ self.changed_objects = []
+ self.deleted_objects = []
+ if not self.get_queryset():
+ return []
+
+ # Put the objects from self.get_queryset into a dict so they are easy to lookup by pk
+ existing_objects = {}
+ for obj in self.get_queryset():
+ existing_objects[obj.pk] = obj
+ saved_instances = []
+ for form in self.initial_forms:
+ obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]]
+ if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
+ self.deleted_objects.append(obj)
+ obj.delete()
+ else:
+ if form.changed_data:
+ self.changed_objects.append((obj, form.changed_data))
+ saved_instances.append(self.save_existing(form, obj, commit=commit))
+ if not commit:
+ self.saved_forms.append(form)
+ return saved_instances
+
+ def save_new_objects(self, commit=True):
+ self.new_objects = []
+ for form in self.extra_forms:
+ if not form.has_changed():
+ continue
+ # If someone has marked an add form for deletion, don't save the
+ # object.
+ if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
+ continue
+ self.new_objects.append(self.save_new(form, commit=commit))
+ if not commit:
+ self.saved_forms.append(form)
+ return self.new_objects
+
+ def add_fields(self, form, index):
+ """Add a hidden field for the object's primary key."""
+ self._pk_field_name = self.model._meta.pk.attname
+ form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput)
+ super(BaseModelFormSet, self).add_fields(form, index)
+
+def modelformset_factory(model, form=ModelForm, formfield_callback=lambda f: f.formfield(),
+ formset=BaseModelFormSet,
+ extra=1, can_delete=False, can_order=False,
+ max_num=0, fields=None, exclude=None):
+ """
+ Returns a FormSet class for the given Django model class.
+ """
+ form = modelform_factory(model, form=form, fields=fields, exclude=exclude,
+ formfield_callback=formfield_callback)
+ FormSet = formset_factory(form, formset, extra=extra, max_num=max_num,
+ can_order=can_order, can_delete=can_delete)
+ FormSet.model = model
+ return FormSet
+
+
+# InlineFormSets #############################################################
+
+class BaseInlineFormset(BaseModelFormSet):
+ """A formset for child objects related to a parent."""
+ def __init__(self, data=None, files=None, instance=None, save_as_new=False):
+ from django.db.models.fields.related import RelatedObject
+ self.instance = instance
+ self.save_as_new = save_as_new
+ # is there a better way to get the object descriptor?
+ self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
+ super(BaseInlineFormset, self).__init__(data, files, prefix=self.rel_name)
+
+ def _construct_forms(self):
+ if self.save_as_new:
+ self._total_form_count = self._initial_form_count
+ self._initial_form_count = 0
+ super(BaseInlineFormset, self)._construct_forms()
+
+ def get_queryset(self):
+ """
+ Returns this FormSet's queryset, but restricted to children of
+ self.instance
+ """
+ kwargs = {self.fk.name: self.instance}
+ return self.model._default_manager.filter(**kwargs)
+
+ def save_new(self, form, commit=True):
+ kwargs = {self.fk.get_attname(): self.instance.pk}
+ new_obj = self.model(**kwargs)
+ return save_instance(form, new_obj, commit=commit)
+
+def _get_foreign_key(parent_model, model, fk_name=None):
+ """
+ Finds and returns the ForeignKey from model to parent if there is one.
+ If fk_name is provided, assume it is the name of the ForeignKey field.
+ """
+ # avoid circular import
+ from django.db.models import ForeignKey
+ opts = model._meta
+ if fk_name:
+ fks_to_parent = [f for f in opts.fields if f.name == fk_name]
+ if len(fks_to_parent) == 1:
+ fk = fks_to_parent[0]
+ if not isinstance(fk, ForeignKey) or fk.rel.to != parent_model:
+ raise Exception("fk_name '%s' is not a ForeignKey to %s" % (fk_name, parent_model))
+ elif len(fks_to_parent) == 0:
+ raise Exception("%s has no field named '%s'" % (model, fk_name))
+ else:
+ # Try to discover what the ForeignKey from model to parent_model is
+ fks_to_parent = [f for f in opts.fields if isinstance(f, ForeignKey) and f.rel.to == parent_model]
+ if len(fks_to_parent) == 1:
+ fk = fks_to_parent[0]
+ elif len(fks_to_parent) == 0:
+ raise Exception("%s has no ForeignKey to %s" % (model, parent_model))
+ else:
+ raise Exception("%s has more than 1 ForeignKey to %s" % (model, parent_model))
+ return fk
+
+
+def inlineformset_factory(parent_model, model, form=ModelForm,
+ formset=BaseInlineFormset, fk_name=None,
+ fields=None, exclude=None,
+ extra=3, can_order=False, can_delete=True, max_num=0,
+ formfield_callback=lambda f: f.formfield()):
+ """
+ Returns an ``InlineFormset`` for the given kwargs.
+
+ You must provide ``fk_name`` if ``model`` has more than one ``ForeignKey``
+ to ``parent_model``.
+ """
+ fk = _get_foreign_key(parent_model, model, fk_name=fk_name)
+ # let the formset handle object deletion by default
+
+ if exclude is not None:
+ exclude.append(fk.name)
+ else:
+ exclude = [fk.name]
+ FormSet = modelformset_factory(model, form=form,
+ formfield_callback=formfield_callback,
+ formset=formset,
+ extra=extra, can_delete=can_delete, can_order=can_order,
+ fields=fields, exclude=exclude, max_num=max_num)
+ FormSet.fk = fk
+ return FormSet
+
# Fields #####################################################################
diff --git a/django/newforms/widgets.py b/django/newforms/widgets.py
index dc36530b93..2c9f3c2eba 100644
--- a/django/newforms/widgets.py
+++ b/django/newforms/widgets.py
@@ -9,7 +9,7 @@ except NameError:
import copy
from itertools import chain
-
+from django.conf import settings
from django.utils.datastructures import MultiValueDict
from django.utils.html import escape, conditional_escape
from django.utils.translation import ugettext
@@ -17,16 +17,118 @@ from django.utils.encoding import StrAndUnicode, force_unicode
from django.utils.safestring import mark_safe
from django.utils import datetime_safe
from util import flatatt
+from urlparse import urljoin
__all__ = (
- 'Widget', 'TextInput', 'PasswordInput',
+ 'Media', 'MediaDefiningClass', 'Widget', 'TextInput', 'PasswordInput',
'HiddenInput', 'MultipleHiddenInput',
'FileInput', 'DateTimeInput', 'Textarea', 'CheckboxInput',
'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
'CheckboxSelectMultiple', 'MultiWidget', 'SplitDateTimeWidget',
)
+MEDIA_TYPES = ('css','js')
+
+class Media(StrAndUnicode):
+ def __init__(self, media=None, **kwargs):
+ if media:
+ media_attrs = media.__dict__
+ else:
+ media_attrs = kwargs
+
+ self._css = {}
+ self._js = []
+
+ for name in MEDIA_TYPES:
+ getattr(self, 'add_' + name)(media_attrs.get(name, None))
+
+ # Any leftover attributes must be invalid.
+ # if media_attrs != {}:
+ # raise TypeError, "'class Media' has invalid attribute(s): %s" % ','.join(media_attrs.keys())
+
+ def __unicode__(self):
+ return self.render()
+
+ def render(self):
+ return u'\n'.join(chain(*[getattr(self, 'render_' + name)() for name in MEDIA_TYPES]))
+
+ def render_js(self):
+ return [u'<script type="text/javascript" src="%s"></script>' % self.absolute_path(path) for path in self._js]
+
+ def render_css(self):
+ # To keep rendering order consistent, we can't just iterate over items().
+ # We need to sort the keys, and iterate over the sorted list.
+ media = self._css.keys()
+ media.sort()
+ return chain(*[
+ [u'<link href="%s" type="text/css" media="%s" rel="stylesheet" />' % (self.absolute_path(path), medium)
+ for path in self._css[medium]]
+ for medium in media])
+
+ def absolute_path(self, path):
+ if path.startswith(u'http://') or path.startswith(u'https://') or path.startswith(u'/'):
+ return path
+ return urljoin(settings.MEDIA_URL,path)
+
+ def __getitem__(self, name):
+ "Returns a Media object that only contains media of the given type"
+ if name in MEDIA_TYPES:
+ return Media(**{name: getattr(self, '_' + name)})
+ raise KeyError('Unknown media type "%s"' % name)
+
+ def add_js(self, data):
+ if data:
+ self._js.extend([path for path in data if path not in self._js])
+
+ def add_css(self, data):
+ if data:
+ for medium, paths in data.items():
+ self._css.setdefault(medium, []).extend([path for path in paths if path not in self._css[medium]])
+
+ def __add__(self, other):
+ combined = Media()
+ for name in MEDIA_TYPES:
+ getattr(combined, 'add_' + name)(getattr(self, '_' + name, None))
+ getattr(combined, 'add_' + name)(getattr(other, '_' + name, None))
+ return combined
+
+def media_property(cls):
+ def _media(self):
+ # Get the media property of the superclass, if it exists
+ if hasattr(super(cls, self), 'media'):
+ base = super(cls, self).media
+ else:
+ base = Media()
+
+ # Get the media definition for this class
+ definition = getattr(cls, 'Media', None)
+ if definition:
+ extend = getattr(definition, 'extend', True)
+ if extend:
+ if extend == True:
+ m = base
+ else:
+ m = Media()
+ for medium in extend:
+ m = m + base[medium]
+ return m + Media(definition)
+ else:
+ return Media(definition)
+ else:
+ return base
+ return property(_media)
+
+class MediaDefiningClass(type):
+ "Metaclass for classes that can have media definitions"
+ def __new__(cls, name, bases, attrs):
+ new_class = super(MediaDefiningClass, cls).__new__(cls, name, bases,
+ attrs)
+ if 'media' not in attrs:
+ new_class.media = media_property(new_class)
+ return new_class
+
class Widget(object):
+ __metaclass__ = MediaDefiningClass
is_hidden = False # Determines whether this corresponds to an <input type="hidden">.
needs_multipart_form = False # Determines does this widget need multipart-encrypted form
@@ -65,6 +167,25 @@ class Widget(object):
"""
return data.get(name, None)
+ def _has_changed(self, initial, data):
+ """
+ Return True if data differs from initial.
+ """
+ # For purposes of seeing whether something has changed, None is
+ # the same as an empty string, if the data or inital value we get
+ # is None, replace it w/ u''.
+ if data is None:
+ data_value = u''
+ else:
+ data_value = data
+ if initial is None:
+ initial_value = u''
+ else:
+ initial_value = initial
+ if force_unicode(initial_value) != force_unicode(data_value):
+ return True
+ return False
+
def id_for_label(self, id_):
"""
Returns the HTML ID attribute of this Widget for use by a <label>,
@@ -143,6 +264,11 @@ class FileInput(Input):
def value_from_datadict(self, data, files, name):
"File widgets take data from FILES, not POST"
return files.get(name, None)
+
+ def _has_changed(self, initial, data):
+ if data is None:
+ return False
+ return True
class Textarea(Widget):
def __init__(self, attrs=None):
@@ -202,6 +328,11 @@ class CheckboxInput(Widget):
return False
return super(CheckboxInput, self).value_from_datadict(data, files, name)
+ def _has_changed(self, initial, data):
+ # Sometimes data or initial could be None or u'' which should be the
+ # same thing as False.
+ return bool(initial) != bool(data)
+
class Select(Widget):
def __init__(self, attrs=None, choices=()):
super(Select, self).__init__(attrs)
@@ -244,6 +375,11 @@ class NullBooleanSelect(Select):
value = data.get(name, None)
return {u'2': True, u'3': False, True: True, False: False}.get(value, None)
+ def _has_changed(self, initial, data):
+ # Sometimes data or initial could be None or u'' which should be the
+ # same thing as False.
+ return bool(initial) != bool(data)
+
class SelectMultiple(Widget):
def __init__(self, attrs=None, choices=()):
super(SelectMultiple, self).__init__(attrs)
@@ -268,6 +404,18 @@ class SelectMultiple(Widget):
if isinstance(data, MultiValueDict):
return data.getlist(name)
return data.get(name, None)
+
+ def _has_changed(self, initial, data):
+ if initial is None:
+ initial = []
+ if data is None:
+ data = []
+ if len(initial) != len(data):
+ return True
+ for value1, value2 in zip(initial, data):
+ if force_unicode(value1) != force_unicode(value2):
+ return True
+ return False
class RadioInput(StrAndUnicode):
"""
@@ -447,6 +595,16 @@ class MultiWidget(Widget):
def value_from_datadict(self, data, files, name):
return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
+
+ def _has_changed(self, initial, data):
+ if initial is None:
+ initial = [u'' for x in range(0, len(data))]
+ else:
+ initial = self.decompress(initial)
+ for widget, initial, data in zip(self.widgets, initial, data):
+ if widget._has_changed(initial, data):
+ return True
+ return False
def format_output(self, rendered_widgets):
"""
@@ -466,6 +624,14 @@ class MultiWidget(Widget):
"""
raise NotImplementedError('Subclasses must implement this method.')
+ def _get_media(self):
+ "Media for a multiwidget is the combination of all media of the subwidgets"
+ media = Media()
+ for w in self.widgets:
+ media = media + w.media
+ return media
+ media = property(_get_media)
+
class SplitDateTimeWidget(MultiWidget):
"""
A Widget that splits datetime input into two <input type="text"> boxes.