Code

Ticket #8209: django-8209.3.diff

File django-8209.3.diff, 11.9 KB (added by Alex, 6 years ago)

Removed usage of any

Line 
1diff --git a/django/forms/models.py b/django/forms/models.py
2index 4563ace..c0062f5 100644
3--- a/django/forms/models.py
4+++ b/django/forms/models.py
5@@ -3,9 +3,10 @@ Helper functions for creating Form classes from Django models
6 and database field objects.
7 """
8 
9-from django.utils.translation import ugettext_lazy as _
10 from django.utils.encoding import smart_unicode
11 from django.utils.datastructures import SortedDict
12+from django.utils.text import get_text_list
13+from django.utils.translation import ugettext_lazy as _
14 
15 from util import ValidationError, ErrorList
16 from forms import BaseForm, get_declared_fields
17@@ -20,6 +21,7 @@ __all__ = (
18     'ModelMultipleChoiceField',
19 )
20 
21+
22 def save_instance(form, instance, fields=None, fail_message='saved',
23                   commit=True, exclude=None):
24     """
25@@ -202,6 +204,58 @@ class BaseModelForm(BaseForm):
26             object_data.update(initial)
27         super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
28                                             error_class, label_suffix, empty_permitted)
29+    def clean(self):
30+        self.validate_unique()
31+        return self.cleaned_data
32+   
33+    def validate_unique(self):
34+        from django.db.models.fields import FieldDoesNotExist
35+        unique_checks = list(self.instance._meta.unique_together[:])
36+        form_errors = []
37+        for name, field in self.fields.items():
38+            try:
39+                f = self.instance._meta.get_field_by_name(name)[0]
40+            except FieldDoesNotExist:
41+                # This is an extra field that's not on the model, ignore it
42+                continue
43+            if name in self.cleaned_data and f.unique:
44+                unique_checks.append((name,))
45+        # we ignore fields that already have an error
46+        for unique_check in [check for check in unique_checks if not [x in self._errors for x in check if x in self._errors]]:
47+            # generate are parameters for the query
48+            kwargs = dict([(field_name, self.cleaned_data[field_name]) for field_name in unique_check])
49+            qs = self.instance.__class__._default_manager.filter(**kwargs)
50+            if self.instance.pk is not None:
51+                # exclude the current object from the query if we are editing an
52+                # instance
53+                qs = qs.exclude(pk=self.instance.pk)
54+            if qs.extra(select={'a': 1}).values('a').order_by():
55+                model_name = self.instance._meta.verbose_name.title()
56+                # a unique field
57+                if len(unique_check) == 1:
58+                    field_name = unique_check[0]
59+                    field_label = self.fields[field_name].label
60+                    # insert the error into the error dict, very sneaky
61+                    self._errors[field_name] = ErrorList([
62+                        _("%(model_name)s with this %(field_label)s already exists.") % \
63+                        {'model_name': model_name, 'field_label': field_label}
64+                    ])
65+                # a unique together
66+                else:
67+                    field_labels = [self.fields[field_name].label for field_name in unique_check]
68+                    field_labels = get_text_list(field_labels, _('and'))
69+                    form_errors.append(
70+                        _("%(model_name)s with this %(field_label)s already exists.") % \
71+                        {'model_name': model_name, 'field_label': field_labels}
72+                    )
73+                for field_name in unique_check:
74+                    # remove the data from the cleaned_data dict since it was
75+                    # invalid
76+                    del self.cleaned_data[field_name]
77+        if form_errors:
78+            # raise the unique together errors since they are considered
79+            # form wide.
80+            raise ValidationError(form_errors)
81 
82     def save(self, commit=True):
83         """
84@@ -246,19 +300,27 @@ class BaseModelFormSet(BaseFormSet):
85                  queryset=None, **kwargs):
86         self.queryset = queryset
87         defaults = {'data': data, 'files': files, 'auto_id': auto_id, 'prefix': prefix}
88-        if self.max_num > 0:
89-            qs = self.get_queryset()[:self.max_num]
90-        else:
91-            qs = self.get_queryset()
92-        defaults['initial'] = [model_to_dict(obj) for obj in qs]
93+        defaults['initial'] = [model_to_dict(obj) for obj in self.get_queryset()]
94         defaults.update(kwargs)
95         super(BaseModelFormSet, self).__init__(**defaults)
96-
97+   
98+    def _construct_form(self, i, **kwargs):
99+        if i < self._initial_form_count:
100+            kwargs['instance'] = self.get_queryset()[i]
101+        return super(BaseModelFormSet, self)._construct_form(i, **kwargs)
102+       
103     def get_queryset(self):
104-        if self.queryset is not None:
105-            return self.queryset
106-        return self.model._default_manager.get_query_set()
107-
108+        if not hasattr(self, '_queryset'):
109+            if self.queryset is not None:
110+                qs = self.queryset
111+            else:
112+                qs = self.model._default_manager.get_query_set()
113+            if self.max_num > 0:
114+                self._queryset = qs[:self.max_num]
115+            else:
116+                self._queryset = qs
117+        return self._queryset
118+   
119     def save_new(self, form, commit=True):
120         """Saves and returns a new model instance for the given form."""
121         return save_instance(form, self.model(), exclude=[self._pk_field.name], commit=commit)
122@@ -358,6 +420,14 @@ class BaseInlineFormSet(BaseModelFormSet):
123             self._total_form_count = self._initial_form_count
124             self._initial_form_count = 0
125         super(BaseInlineFormSet, self)._construct_forms()
126+   
127+    def _construct_form(self, i, **kwargs):
128+        form = super(BaseInlineFormSet, self)._construct_form(i, **kwargs)
129+        if self.save_as_new:
130+            # Remove the primary key from the form's data, we are only
131+            # creating new instances
132+            form.data[form.add_prefix(self._pk_field.name)] = None
133+        return form
134 
135     def get_queryset(self):
136         """
137diff --git a/django/utils/itercompat.py b/django/utils/itercompat.py
138index c166da3..4a28f81 100644
139--- a/django/utils/itercompat.py
140+++ b/django/utils/itercompat.py
141@@ -72,3 +72,4 @@ def sorted(in_value):
142     out_value = in_value[:]
143     out_value.sort()
144     return out_value
145+
146diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt
147index d161b3f..163c428 100644
148--- a/docs/topics/forms/modelforms.txt
149+++ b/docs/topics/forms/modelforms.txt
150@@ -338,6 +338,16 @@ parameter when declaring the form field::
151    ...     class Meta:
152    ...         model = Article
153 
154+Overriding the clean() method
155+-----------------------------
156+
157+You can overide the ``clean()`` method on a model form to provide additional
158+validation in the same way you can on a normal form.  However, by default the
159+``clean()`` method validates the uniqueness of fields that are marked as unique
160+on the model, and those marked as unque_together, if you would like to overide
161+the ``clean()`` method and maintain the default validation you must call the
162+parent class's ``clean()`` method.
163+
164 Form inheritance
165 ----------------
166 
167@@ -500,4 +510,4 @@ books of a specific author. Here is how you could accomplish this::
168     >>> from django.forms.models import inlineformset_factory
169     >>> BookFormSet = inlineformset_factory(Author, Book)
170     >>> author = Author.objects.get(name=u'Orson Scott Card')
171-    >>> formset = BookFormSet(instance=author)
172\ No newline at end of file
173+    >>> formset = BookFormSet(instance=author)
174diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py
175index 5f714fb..9a306aa 100644
176--- a/tests/modeltests/model_forms/models.py
177+++ b/tests/modeltests/model_forms/models.py
178@@ -117,9 +117,26 @@ class CommaSeparatedInteger(models.Model):
179     def __unicode__(self):
180         return self.field
181 
182+class Product(models.Model):
183+    slug = models.SlugField(unique=True)
184+   
185+    def __unicode__(self):
186+        return self.slug
187+
188+class Price(models.Model):
189+    price = models.DecimalField(max_digits=10, decimal_places=2)
190+    quantity = models.PositiveIntegerField()
191+   
192+    def __unicode__(self):
193+        return u"%s for %s" % (self.quantity, self.price)
194+   
195+    class Meta:
196+        unique_together = (('price', 'quantity'),)
197+
198 class ArticleStatus(models.Model):
199     status = models.CharField(max_length=2, choices=ARTICLE_STATUS_CHAR, blank=True, null=True)
200 
201+
202 __test__ = {'API_TESTS': """
203 >>> from django import forms
204 >>> from django.forms.models import ModelForm, model_to_dict
205@@ -1132,8 +1149,42 @@ u'1,,2'
206 >>> f.clean('1')
207 u'1'
208 
209-# Choices on CharField and IntegerField
210+# unique/unique_together validation
211 
212+>>> class ProductForm(ModelForm):
213+...     class Meta:
214+...         model = Product
215+>>> form = ProductForm({'slug': 'teddy-bear-blue'})
216+>>> form.is_valid()
217+True
218+>>> obj = form.save()
219+>>> obj
220+<Product: teddy-bear-blue>
221+>>> form = ProductForm({'slug': 'teddy-bear-blue'})
222+>>> form.is_valid()
223+False
224+>>> form._errors
225+{'slug': [u'Product with this Slug already exists.']}
226+>>> form = ProductForm({'slug': 'teddy-bear-blue'}, instance=obj)
227+>>> form.is_valid()
228+True
229+
230+# ModelForm test of unique_together constraint
231+>>> class PriceForm(ModelForm):
232+...     class Meta:
233+...         model = Price
234+>>> form = PriceForm({'price': '6.00', 'quantity': '1'})
235+>>> form.is_valid()
236+True
237+>>> form.save()
238+<Price: 1 for 6.00>
239+>>> form = PriceForm({'price': '6.00', 'quantity': '1'})
240+>>> form.is_valid()
241+False
242+>>> form._errors
243+{'__all__': [u'Price with this Price and Quantity already exists.']}
244+
245+# Choices on CharField and IntegerField
246 >>> class ArticleForm(ModelForm):
247 ...     class Meta:
248 ...         model = Article
249diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py
250index 332c5a7..1e25baa 100644
251--- a/tests/modeltests/model_formsets/models.py
252+++ b/tests/modeltests/model_formsets/models.py
253@@ -73,6 +73,22 @@ class Restaurant(Place):
254     def __unicode__(self):
255         return self.name
256 
257+class Product(models.Model):
258+    slug = models.SlugField(unique=True)
259+
260+    def __unicode__(self):
261+        return self.slug
262+
263+class Price(models.Model):
264+    price = models.DecimalField(max_digits=10, decimal_places=2)
265+    quantity = models.PositiveIntegerField()
266+
267+    def __unicode__(self):
268+        return u"%s for %s" % (self.quantity, self.price)
269+
270+    class Meta:
271+        unique_together = (('price', 'quantity'),)
272+
273 class MexicanRestaurant(Restaurant):
274     serves_tacos = models.BooleanField()
275 
276@@ -553,4 +569,56 @@ True
277 >>> type(_get_foreign_key(MexicanRestaurant, Owner))
278 <class 'django.db.models.fields.related.ForeignKey'>
279 
280+# unique/unique_together validation ###########################################
281+
282+>>> FormSet = modelformset_factory(Product, extra=1)
283+>>> data = {
284+...     'form-TOTAL_FORMS': '1',
285+...     'form-INITIAL_FORMS': '0',
286+...     'form-0-slug': 'car-red',
287+... }
288+>>> formset = FormSet(data)
289+>>> formset.is_valid()
290+True
291+>>> formset.save()
292+[<Product: car-red>]
293+
294+>>> data = {
295+...     'form-TOTAL_FORMS': '1',
296+...     'form-INITIAL_FORMS': '0',
297+...     'form-0-slug': 'car-red',
298+... }
299+>>> formset = FormSet(data)
300+>>> formset.is_valid()
301+False
302+>>> formset.errors
303+[{'slug': [u'Product with this Slug already exists.']}]
304+
305+# unique_together
306+
307+>>> FormSet = modelformset_factory(Price, extra=1)
308+>>> data = {
309+...     'form-TOTAL_FORMS': '1',
310+...     'form-INITIAL_FORMS': '0',
311+...     'form-0-price': u'12.00',
312+...     'form-0-quantity': '1',
313+... }
314+>>> formset = FormSet(data)
315+>>> formset.is_valid()
316+True
317+>>> formset.save()
318+[<Price: 1 for 12.00>]
319+
320+>>> data = {
321+...     'form-TOTAL_FORMS': '1',
322+...     'form-INITIAL_FORMS': '0',
323+...     'form-0-price': u'12.00',
324+...     'form-0-quantity': '1',
325+... }
326+>>> formset = FormSet(data)
327+>>> formset.is_valid()
328+False
329+>>> formset.errors
330+[{'__all__': [u'Price with this Price and Quantity already exists.']}]
331+
332 """}