models.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. """
  2. Helper functions for creating Form classes from Django models
  3. and database field objects.
  4. """
  5. from django.utils.translation import gettext
  6. from util import ValidationError
  7. from forms import BaseForm, DeclarativeFieldsMetaclass, SortedDictFromList
  8. from fields import Field, ChoiceField
  9. from widgets import Select, SelectMultiple, MultipleHiddenInput
  10. __all__ = ('save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields',
  11. 'ModelChoiceField', 'ModelMultipleChoiceField')
  12. def save_instance(form, instance, fields=None, fail_message='saved', commit=True):
  13. """
  14. Saves bound Form ``form``'s cleaned_data into model instance ``instance``.
  15. Assumes ``form`` has a field for every non-AutoField database field in
  16. ``instance``. If commit=True, then the changes to ``instance`` will be
  17. saved to the database. Returns ``instance``.
  18. """
  19. from django.db import models
  20. opts = instance.__class__._meta
  21. if form.errors:
  22. raise ValueError("The %s could not be %s because the data didn't validate." % (opts.object_name, fail_message))
  23. cleaned_data = form.cleaned_data
  24. for f in opts.fields:
  25. if not f.editable or isinstance(f, models.AutoField) or not f.name in cleaned_data:
  26. continue
  27. if fields and f.name not in fields:
  28. continue
  29. setattr(instance, f.name, cleaned_data[f.name])
  30. if commit:
  31. instance.save()
  32. for f in opts.many_to_many:
  33. if fields and f.name not in fields:
  34. continue
  35. if f.name in cleaned_data:
  36. setattr(instance, f.attname, cleaned_data[f.name])
  37. # GOTCHA: If many-to-many data is given and commit=False, the many-to-many
  38. # data will be lost. This happens because a many-to-many options cannot be
  39. # set on an object until after it's saved. Maybe we should raise an
  40. # exception in that case.
  41. return instance
  42. def make_model_save(model, fields, fail_message):
  43. "Returns the save() method for a Form."
  44. def save(self, commit=True):
  45. return save_instance(self, model(), fields, fail_message, commit)
  46. return save
  47. def make_instance_save(instance, fields, fail_message):
  48. "Returns the save() method for a Form."
  49. def save(self, commit=True):
  50. return save_instance(self, instance, fields, fail_message, commit)
  51. return save
  52. def form_for_model(model, form=BaseForm, fields=None, formfield_callback=lambda f: f.formfield()):
  53. """
  54. Returns a Form class for the given Django model class.
  55. Provide ``form`` if you want to use a custom BaseForm subclass.
  56. Provide ``formfield_callback`` if you want to define different logic for
  57. determining the formfield for a given database field. It's a callable that
  58. takes a database Field instance and returns a form Field instance.
  59. """
  60. opts = model._meta
  61. field_list = []
  62. for f in opts.fields + opts.many_to_many:
  63. if not f.editable:
  64. continue
  65. if fields and not f.name in fields:
  66. continue
  67. formfield = formfield_callback(f)
  68. if formfield:
  69. field_list.append((f.name, formfield))
  70. base_fields = SortedDictFromList(field_list)
  71. return type(opts.object_name + 'Form', (form,),
  72. {'base_fields': base_fields, '_model': model, 'save': make_model_save(model, fields, 'created')})
  73. def form_for_instance(instance, form=BaseForm, fields=None, formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)):
  74. """
  75. Returns a Form class for the given Django model instance.
  76. Provide ``form`` if you want to use a custom BaseForm subclass.
  77. Provide ``formfield_callback`` if you want to define different logic for
  78. determining the formfield for a given database field. It's a callable that
  79. takes a database Field instance, plus **kwargs, and returns a form Field
  80. instance with the given kwargs (i.e. 'initial').
  81. """
  82. model = instance.__class__
  83. opts = model._meta
  84. field_list = []
  85. for f in opts.fields + opts.many_to_many:
  86. if not f.editable:
  87. continue
  88. if fields and not f.name in fields:
  89. continue
  90. current_value = f.value_from_object(instance)
  91. formfield = formfield_callback(f, initial=current_value)
  92. if formfield:
  93. field_list.append((f.name, formfield))
  94. base_fields = SortedDictFromList(field_list)
  95. return type(opts.object_name + 'InstanceForm', (form,),
  96. {'base_fields': base_fields, '_model': model, 'save': make_instance_save(instance, fields, 'changed')})
  97. def form_for_fields(field_list):
  98. "Returns a Form class for the given list of Django database field instances."
  99. fields = SortedDictFromList([(f.name, f.formfield()) for f in field_list if f.editable])
  100. return type('FormForFields', (BaseForm,), {'base_fields': fields})
  101. class QuerySetIterator(object):
  102. def __init__(self, queryset, empty_label, cache_choices):
  103. self.queryset, self.empty_label, self.cache_choices = queryset, empty_label, cache_choices
  104. def __iter__(self):
  105. if self.empty_label is not None:
  106. yield (u"", self.empty_label)
  107. for obj in self.queryset:
  108. yield (obj._get_pk_val(), str(obj))
  109. # Clear the QuerySet cache if required.
  110. if not self.cache_choices:
  111. self.queryset._result_cache = None
  112. class ModelChoiceField(ChoiceField):
  113. "A ChoiceField whose choices are a model QuerySet."
  114. # This class is a subclass of ChoiceField for purity, but it doesn't
  115. # actually use any of ChoiceField's implementation.
  116. def __init__(self, queryset, empty_label=u"---------", cache_choices=False,
  117. required=True, widget=Select, label=None, initial=None, help_text=None):
  118. self.queryset = queryset
  119. self.empty_label = empty_label
  120. self.cache_choices = cache_choices
  121. # Call Field instead of ChoiceField __init__() because we don't need
  122. # ChoiceField.__init__().
  123. Field.__init__(self, required, widget, label, initial, help_text)
  124. self.widget.choices = self.choices
  125. def _get_choices(self):
  126. # If self._choices is set, then somebody must have manually set
  127. # the property self.choices. In this case, just return self._choices.
  128. if hasattr(self, '_choices'):
  129. return self._choices
  130. # Otherwise, execute the QuerySet in self.queryset to determine the
  131. # choices dynamically. Return a fresh QuerySetIterator that has not
  132. # been consumed. Note that we're instantiating a new QuerySetIterator
  133. # *each* time _get_choices() is called (and, thus, each time
  134. # self.choices is accessed) so that we can ensure the QuerySet has not
  135. # been consumed.
  136. return QuerySetIterator(self.queryset, self.empty_label, self.cache_choices)
  137. def _set_choices(self, value):
  138. # This method is copied from ChoiceField._set_choices(). It's necessary
  139. # because property() doesn't allow a subclass to overwrite only
  140. # _get_choices without implementing _set_choices.
  141. self._choices = self.widget.choices = list(value)
  142. choices = property(_get_choices, _set_choices)
  143. def clean(self, value):
  144. Field.clean(self, value)
  145. if value in ('', None):
  146. return None
  147. try:
  148. value = self.queryset.model._default_manager.get(pk=value)
  149. except self.queryset.model.DoesNotExist:
  150. raise ValidationError(gettext(u'Select a valid choice. That choice is not one of the available choices.'))
  151. return value
  152. class ModelMultipleChoiceField(ModelChoiceField):
  153. "A MultipleChoiceField whose choices are a model QuerySet."
  154. hidden_widget = MultipleHiddenInput
  155. def __init__(self, queryset, cache_choices=False, required=True,
  156. widget=SelectMultiple, label=None, initial=None, help_text=None):
  157. super(ModelMultipleChoiceField, self).__init__(queryset, None, cache_choices,
  158. required, widget, label, initial, help_text)
  159. def clean(self, value):
  160. if self.required and not value:
  161. raise ValidationError(gettext(u'This field is required.'))
  162. elif not self.required and not value:
  163. return []
  164. if not isinstance(value, (list, tuple)):
  165. raise ValidationError(gettext(u'Enter a list of values.'))
  166. final_values = []
  167. for val in value:
  168. try:
  169. obj = self.queryset.model._default_manager.get(pk=val)
  170. except self.queryset.model.DoesNotExist:
  171. raise ValidationError(gettext(u'Select a valid choice. %s is not one of the available choices.') % val)
  172. else:
  173. final_values.append(obj)
  174. return final_values