From d997a7a422b82b0c505b81e61e299e51b874c7c0 Mon Sep 17 00:00:00 2001 From: Drew Hubl Date: Thu, 22 Sep 2016 14:12:56 -0600 Subject: [PATCH] Add test for .values_list() --- README.rst | 42 +++++++++++++-------- example/app/fixtures/app_data.json | 3 +- example/app/migrations/0001_initial.py | 5 ++- example/app/models.py | 36 +++++++++++++----- example/app/test_msf.py | 51 ++++++++++++++++++++++++-- multiselectfield/db/fields.py | 8 ++-- tox.ini | 6 ++- 7 files changed, 114 insertions(+), 37 deletions(-) diff --git a/README.rst b/README.rst index b014e7a..a1ca3a5 100644 --- a/README.rst +++ b/README.rst @@ -13,9 +13,9 @@ django-multiselectfield .. image:: https://pypip.in/d/django-multiselectfield/badge.png :target: https://pypi.python.org/pypi/django-multiselectfield -A new model and form field. With this you can get a multiple select from a choices +A new model field and form field. With this you can get a multiple select from a choices. Stores to the database as a CharField of comma-separated values. -This egg is inspired by this `snippet `_ +This egg is inspired by this `snippet `_. Installation ============ @@ -24,28 +24,28 @@ Installation In your models.py ----------------- -:: +.. code-block:: python from multiselectfield import MultiSelectField - - ... - + + # ... + MY_CHOICES = (('item_key1', 'Item title 1.1'), ('item_key2', 'Item title 1.2'), ('item_key3', 'Item title 1.3'), ('item_key4', 'Item title 1.4'), ('item_key5', 'Item title 1.5')) - + MY_CHOICES2 = ((1, 'Item title 2.1'), (2, 'Item title 2.2'), (3, 'Item title 2.3'), (4, 'Item title 2.4'), (5, 'Item title 2.5')) - + class MyModel(models.Model): - - ..... - + + # ..... + my_field = MultiSelectField(choices=MY_CHOICES) my_field2 = MultiSelectField(choices=MY_CHOICES2, max_choices=3, @@ -57,7 +57,7 @@ In your settings.py Only you need it, if you want the translation of django-multiselectfield -:: +.. code-block:: python INSTALLED_APPS = ( 'django.contrib.auth', @@ -72,13 +72,25 @@ Only you need it, if you want the translation of django-multiselectfield ) +Known Bugs and Limitations +========================== + +Only in Django 1.6 and 1.7, due to `Django bug #9619 `_, passing a MultiSelectField to ``values()`` or ``values_list()`` will return the database representation of the field (a string of comma-separated values). The workaround is to manually call ``.split(',')`` on the result. + +The Django bug was introduced in Django 1.6 and is fixed in Django 1.8 and onward, so ``values()`` and ``values_list()`` return a vanilla Python list of values for Django <= 1.5 and Django >= 1.8. + +See `issue #40 `_ for discussion about this bug. + + Development =========== You can get the last bleeding edge version of django-multiselectfield by doing a clone -of its git repository:: +of its git repository: - git clone https://github.com/goinnn/django-multiselectfield +.. code-block:: bash + + git clone https://github.com/goinnn/django-multiselectfield Example project @@ -87,7 +99,7 @@ Example project In the source tree, you will find a directory called `example `_. It contains a readily setup project that uses django-multiselectfield. You can run it as usual: -:: +.. code-block:: bash python manage.py syncdb --noinput python manage.py loaddata app_data diff --git a/example/app/fixtures/app_data.json b/example/app/fixtures/app_data.json index f742ee6..9ae79ff 100644 --- a/example/app/fixtures/app_data.json +++ b/example/app/fixtures/app_data.json @@ -5,7 +5,8 @@ "fields": { "title": "My book 1", "tags": "sex,work,happy", - "categories": "1,3,5" + "categories": "1,3,5", + "published_in": "BC,AL,AK" } }, { diff --git a/example/app/migrations/0001_initial.py b/example/app/migrations/0001_initial.py index 2637a02..164054b 100644 --- a/example/app/migrations/0001_initial.py +++ b/example/app/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.9 on 2016-09-22 16:56 +# Generated by Django 1.9 on 2016-09-22 20:34 from __future__ import unicode_literals from django.db import migrations, models @@ -20,7 +20,8 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('title', models.CharField(max_length=200)), ('categories', multiselectfield.db.fields.MultiSelectField(choices=[(1, 'Handbooks and manuals by discipline'), (2, 'Business books'), (3, 'Books of literary criticism'), (4, 'Books about literary theory'), (5, 'Books about literature')], default=1, max_length=9)), - ('tags', multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('sex', 'Sex'), ('work', 'Work'), ('happy', 'Happy'), ('food', 'Food'), ('field', 'Field'), ('boring', 'Boring'), ('interesting', 'Interesting'), ('huge', 'huge'), ('nice', 'Nice')], max_length=54, null=True)), + ('tags', multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('sex', 'Sex'), ('work', 'Work'), ('happy', 'Happy'), ('food', 'Food'), ('field', 'Field'), ('boring', 'Boring'), ('interesting', 'Interesting'), ('huge', 'Huge'), ('nice', 'Nice')], max_length=54, null=True)), + ('published_in', multiselectfield.db.fields.MultiSelectField(choices=[('Canada - Provinces', (('AB', 'Alberta'), ('BC', 'British Columbia'))), ('USA - States', (('AK', 'Alaska'), ('AL', 'Alabama'), ('AZ', 'Arizona')))], max_length=2, verbose_name='Province or State')), ], ), ] diff --git a/example/app/models.py b/example/app/models.py index 9910245..1cca15e 100644 --- a/example/app/models.py +++ b/example/app/models.py @@ -15,6 +15,7 @@ # along with this software. If not, see . from django.db import models +from django.utils.translation import gettext as _ from multiselectfield import MultiSelectField @@ -27,15 +28,31 @@ CATEGORY_CHOICES = ( ) TAGS_CHOICES = ( - ('sex', 'Sex'), - ('work', 'Work'), - ('happy', 'Happy'), - ('food', 'Food'), - ('field', 'Field'), - ('boring', 'Boring'), - ('interesting', 'Interesting'), - ('huge', 'huge'), - ('nice', 'Nice'), + ('sex', _('Sex')), + ('work', _('Work')), + ('happy', _('Happy')), + ('food', _('Food')), + ('field', _('Field')), + ('boring', _('Boring')), + ('interesting', _('Interesting')), + ('huge', _('Huge')), + ('nice', _('Nice')), +) + +PROVINCES = ( + ('AB', _("Alberta")), + ('BC', _("British Columbia")), +) + +STATES = ( + ('AK', _("Alaska")), + ('AL', _("Alabama")), + ('AZ', _("Arizona")), +) + +PROVINCES_AND_STATES = ( + (_("Canada - Provinces"), PROVINCES), + (_("USA - States"), STATES), ) @@ -47,6 +64,7 @@ class Book(models.Model): default=1) tags = MultiSelectField(choices=TAGS_CHOICES, null=True, blank=True) + published_in = MultiSelectField(_("Province or State"), max_length=2, choices=PROVINCES_AND_STATES) def __str__(self): return self.title diff --git a/example/app/test_msf.py b/example/app/test_msf.py index 926ed54..54e03bb 100644 --- a/example/app/test_msf.py +++ b/example/app/test_msf.py @@ -14,6 +14,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with this software. If not, see . +import sys + from django import VERSION from django.core.exceptions import ValidationError from django.forms.models import modelform_factory @@ -24,6 +26,12 @@ from multiselectfield.utils import get_max_length from .models import Book +if sys.version_info < (3,): + u = unicode +else: + u = str + + if VERSION < (1, 9): def get_field(model, name): return model._meta.get_field_by_name(name)[0] @@ -36,12 +44,47 @@ class MultiSelectTestCase(TestCase): fixtures = ['app_data.json'] + def assertListEqual(self, left, right, msg=None): + if sys.version_info >= (3, 2): + # Added in Python 3.2 + self.assertCountEqual(left, right, msg=msg) + else: + # Manually check list equality + self.assertEqual(len(left), len(right), msg=msg) + for i, tag_list in enumerate(left): + for j, tag in enumerate(tag_list): + self.assertEqual(tag, right[i][j], msg=msg) + + def assertStringEqual(self, left, right, msg=None): + _msg = "Chars in position %%d differ: %%s != %%s. %s" % msg + + # Compare characters individually + for i, chars in enumerate(zip(left, right)): + self.assertEqual(chars[0], chars[1], msg=_msg % (i, chars[0], chars[1])) + def test_filter(self): self.assertEqual(Book.objects.filter(tags__contains='sex').count(), 1) self.assertEqual(Book.objects.filter(tags__contains='boring').count(), 0) + def test_values_list(self): + tag_list_list = Book.objects.all().values_list('tags', flat=True) + categories_list_list = Book.objects.all().values_list('categories', flat=True) + + # Workaround for Django bug #9619 + # https://code.djangoproject.com/ticket/9619 + # For Django 1.6 and 1.7, calling values() or values_list() doesn't + # call Field.from_db_field, it simply returns a Python representation + # of the data in the database (which in our case is a string of + # comma-separated values). The bug was fixed in Django 1.8+. + if VERSION >= (1, 6) and VERSION < (1, 8): + self.assertStringEqual(tag_list_list, [u('sex,work,happy')]) + self.assertStringEqual(categories_list_list, [u('1,3,5')]) + else: + self.assertListEqual(tag_list_list, [['sex', 'work', 'happy']]) + self.assertListEqual(categories_list_list, [['1', '3', '5']]) + def test_form(self): - form_class = modelform_factory(Book, fields='__all__') + form_class = modelform_factory(Book, fields=('title', 'tags', 'categories')) self.assertEqual(len(form_class.base_fields), 3) form = form_class({'title': 'new book', 'categories': '1,2'}) @@ -87,10 +130,10 @@ class MultiSelectTestCase(TestCase): class MultiSelectUtilsTestCase(TestCase): def test_get_max_length_max_length_is_not_none(self): - self.assertEqual(5, get_max_length([], 5)) + self.assertEqual(get_max_length([], 5), 5) def test_get_max_length_max_length_is_none_and_choices_is_empty(self): - self.assertEqual(200, get_max_length([], None)) + self.assertEqual(get_max_length([], None), 200) def test_get_max_length_max_length_is_none_and_choices_is_not_empty(self): choices = [ @@ -98,4 +141,4 @@ class MultiSelectUtilsTestCase(TestCase): ('key2', 'value2'), ('key3', 'value3'), ] - self.assertEqual(14, get_max_length(choices, None)) + self.assertEqual(get_max_length(choices, None), 14) diff --git a/multiselectfield/db/fields.py b/multiselectfield/db/fields.py index 8d9b408..f45948f 100644 --- a/multiselectfield/db/fields.py +++ b/multiselectfield/db/fields.py @@ -16,7 +16,7 @@ import sys -import django +from django import VERSION from django.db import models from django.utils.text import capfirst @@ -26,7 +26,7 @@ from ..forms.fields import MultiSelectFormField, MaxChoicesValidator from ..utils import get_max_length from ..validators import MaxValueMultiFieldValidator -if sys.version_info[0] == 2: +if sys.version_info < (3,): string_type = unicode else: string_type = str @@ -78,7 +78,7 @@ class MultiSelectField(models.CharField): arr_choices = self.get_choices_selected(self.get_choices_default()) for opt_select in value: if (opt_select not in arr_choices): - if django.VERSION[0] == 1 and django.VERSION[1] >= 6: + if VERSION >= (1, 6): raise exceptions.ValidationError(self.error_messages['invalid_choice'] % {"value": value}) else: raise exceptions.ValidationError(self.error_messages['invalid_choice'] % value) @@ -143,7 +143,7 @@ class MultiSelectField(models.CharField): setattr(cls, 'get_%s_list' % self.name, get_list) setattr(cls, 'get_%s_display' % self.name, get_display) -if django.VERSION < (1, 8): +if VERSION < (1, 8): MultiSelectField = add_metaclass(models.SubfieldBase)(MultiSelectField) try: diff --git a/tox.ini b/tox.ini index 4258ea7..bf4297c 100644 --- a/tox.ini +++ b/tox.ini @@ -3,9 +3,11 @@ envlist = py26-dj14,py27-dj14,py26-dj15,py27-dj15,py26-dj16,py27-dj16,py33-dj16, [testenv] usedevelop = True +setenv = + PYTHONPATH=. commands = - PYTHONPATH=. python {envbindir}/coverage run -p example/run_tests.py - PYTHONPATH=. python {envbindir}/coverage run -p example/run_tests.py example.settings_no_debug + python {envbindir}/coverage run -p example/run_tests.py + python {envbindir}/coverage run -p example/run_tests.py example.settings_no_debug install_command = pip install {opts} {packages}