From a19e9d80ffa10f8da43addcaa4ddd440beee8a4d Mon Sep 17 00:00:00 2001 From: Donald Stufft Date: Fri, 17 May 2013 15:31:41 -0400 Subject: [PATCH] Fixed #20430 - Enable iterable of iterables for model choices Allows for any iterable, not just lists or tuples, to be used as the inner item for a list of choices in a model. --- django/core/management/validation.py | 4 +-- docs/ref/models/fields.txt | 7 ++--- docs/releases/1.6.txt | 3 +++ tests/invalid_models/invalid_models/models.py | 4 +-- tests/model_validation/__init__.py | 0 tests/model_validation/models.py | 27 +++++++++++++++++++ tests/model_validation/tests.py | 14 ++++++++++ 7 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 tests/model_validation/__init__.py create mode 100644 tests/model_validation/models.py create mode 100644 tests/model_validation/tests.py diff --git a/django/core/management/validation.py b/django/core/management/validation.py index 0f0eade569..2040a14582 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -118,8 +118,8 @@ def get_validation_errors(outfile, app=None): e.add(opts, '"%s": "choices" should be iterable (e.g., a tuple or list).' % f.name) else: for c in f.choices: - if not isinstance(c, (list, tuple)) or len(c) != 2: - e.add(opts, '"%s": "choices" should be a sequence of two-tuples.' % f.name) + if isinstance(c, six.string_types) or not is_iterable(c) or len(c) != 2: + e.add(opts, '"%s": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples).' % f.name) if f.db_index not in (None, True, False): e.add(opts, '"%s": "db_index" should be either None, True or False.' % f.name) diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 99ba78cb09..38b6459909 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -80,9 +80,10 @@ If a field has ``blank=False``, the field will be required. .. attribute:: Field.choices -An iterable (e.g., a list or tuple) of 2-tuples to use as choices for this -field. If this is given, the default form widget will be a select box with -these choices instead of the standard text field. +An iterable (e.g., a list or tuple) consisting itself of iterables of exactly +two items (e.g. ``[(A, B), (A, B) ...]``) to use as choices for this field. If +this is given, the default form widget will be a select box with these choices +instead of the standard text field. The first element in each tuple is the actual value to be stored, and the second element is the human-readable name. For example:: diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 60b3381dd6..b10a9ef81e 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -238,6 +238,9 @@ Minor features Meta option: ``localized_fields``. Fields included in this list will be localized (by setting ``localize`` on the form field). +* The ``choices`` argument to model fields now accepts an iterable of iterables + instead of requiring an iterable of lists or tuples. + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/invalid_models/invalid_models/models.py b/tests/invalid_models/invalid_models/models.py index 3c21e1ddb8..6ba3f04e5e 100644 --- a/tests/invalid_models/invalid_models/models.py +++ b/tests/invalid_models/invalid_models/models.py @@ -375,8 +375,8 @@ invalid_models.fielderrors: "decimalfield3": DecimalFields require a "max_digits invalid_models.fielderrors: "decimalfield4": DecimalFields require a "max_digits" attribute value that is greater than or equal to the value of the "decimal_places" attribute. invalid_models.fielderrors: "filefield": FileFields require an "upload_to" attribute. invalid_models.fielderrors: "choices": "choices" should be iterable (e.g., a tuple or list). -invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-tuples. -invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-tuples. +invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples). +invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples). invalid_models.fielderrors: "index": "db_index" should be either None, True or False. invalid_models.fielderrors: "field_": Field names cannot end with underscores, because this would lead to ambiguous queryset filters. invalid_models.fielderrors: "nullbool": BooleanFields do not accept null values. Use a NullBooleanField instead. diff --git a/tests/model_validation/__init__.py b/tests/model_validation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/model_validation/models.py b/tests/model_validation/models.py new file mode 100644 index 0000000000..9a2a5f7cd0 --- /dev/null +++ b/tests/model_validation/models.py @@ -0,0 +1,27 @@ +from django.db import models + + +class ThingItem(object): + + def __init__(self, value, display): + self.value = value + self.display = display + + def __iter__(self): + return (x for x in [self.value, self.display]) + + def __len__(self): + return 2 + + +class Things(object): + + def __iter__(self): + return (x for x in [ThingItem(1, 2), ThingItem(3, 4)]) + + +class ThingWithIterableChoices(models.Model): + + # Testing choices= Iterable of Iterables + # See: https://code.djangoproject.com/ticket/20430 + thing = models.CharField(max_length=100, blank=True, choices=Things()) diff --git a/tests/model_validation/tests.py b/tests/model_validation/tests.py new file mode 100644 index 0000000000..96baf640eb --- /dev/null +++ b/tests/model_validation/tests.py @@ -0,0 +1,14 @@ +import io + +from django.core import management +from django.test import TestCase + + +class ModelValidationTest(TestCase): + + def test_models_validate(self): + # All our models should validate properly + # Validation Tests: + # * choices= Iterable of Iterables + # See: https://code.djangoproject.com/ticket/20430 + management.call_command("validate", stdout=io.BytesIO())