Add test for .values_list()

This commit is contained in:
Drew Hubl 2016-09-22 14:12:56 -06:00
parent 67e47d24a8
commit d997a7a422
7 changed files with 114 additions and 37 deletions

View file

@ -13,9 +13,9 @@ django-multiselectfield
.. image:: https://pypip.in/d/django-multiselectfield/badge.png .. image:: https://pypip.in/d/django-multiselectfield/badge.png
:target: https://pypi.python.org/pypi/django-multiselectfield :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 <http://djangosnippets.org/snippets/1200/>`_ This egg is inspired by this `snippet <http://djangosnippets.org/snippets/1200/>`_.
Installation Installation
============ ============
@ -24,28 +24,28 @@ Installation
In your models.py In your models.py
----------------- -----------------
:: .. code-block:: python
from multiselectfield import MultiSelectField from multiselectfield import MultiSelectField
... # ...
MY_CHOICES = (('item_key1', 'Item title 1.1'), MY_CHOICES = (('item_key1', 'Item title 1.1'),
('item_key2', 'Item title 1.2'), ('item_key2', 'Item title 1.2'),
('item_key3', 'Item title 1.3'), ('item_key3', 'Item title 1.3'),
('item_key4', 'Item title 1.4'), ('item_key4', 'Item title 1.4'),
('item_key5', 'Item title 1.5')) ('item_key5', 'Item title 1.5'))
MY_CHOICES2 = ((1, 'Item title 2.1'), MY_CHOICES2 = ((1, 'Item title 2.1'),
(2, 'Item title 2.2'), (2, 'Item title 2.2'),
(3, 'Item title 2.3'), (3, 'Item title 2.3'),
(4, 'Item title 2.4'), (4, 'Item title 2.4'),
(5, 'Item title 2.5')) (5, 'Item title 2.5'))
class MyModel(models.Model): class MyModel(models.Model):
..... # .....
my_field = MultiSelectField(choices=MY_CHOICES) my_field = MultiSelectField(choices=MY_CHOICES)
my_field2 = MultiSelectField(choices=MY_CHOICES2, my_field2 = MultiSelectField(choices=MY_CHOICES2,
max_choices=3, max_choices=3,
@ -57,7 +57,7 @@ In your settings.py
Only you need it, if you want the translation of django-multiselectfield Only you need it, if you want the translation of django-multiselectfield
:: .. code-block:: python
INSTALLED_APPS = ( INSTALLED_APPS = (
'django.contrib.auth', '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 <https://code.djangoproject.com/ticket/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 <https://github.com/goinnn/django-multiselectfield/issues/40>`_ for discussion about this bug.
Development Development
=========== ===========
You can get the last bleeding edge version of django-multiselectfield by doing a clone 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 Example project
@ -87,7 +99,7 @@ Example project
In the source tree, you will find a directory called `example <https://github.com/goinnn/django-multiselectfield/tree/master/example/>`_. It contains In the source tree, you will find a directory called `example <https://github.com/goinnn/django-multiselectfield/tree/master/example/>`_. It contains
a readily setup project that uses django-multiselectfield. You can run it as usual: 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 syncdb --noinput
python manage.py loaddata app_data python manage.py loaddata app_data

View file

@ -5,7 +5,8 @@
"fields": { "fields": {
"title": "My book 1", "title": "My book 1",
"tags": "sex,work,happy", "tags": "sex,work,happy",
"categories": "1,3,5" "categories": "1,3,5",
"published_in": "BC,AL,AK"
} }
}, },
{ {

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- 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 __future__ import unicode_literals
from django.db import migrations, models 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')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=200)), ('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)), ('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')),
], ],
), ),
] ]

View file

@ -15,6 +15,7 @@
# along with this software. If not, see <http://www.gnu.org/licenses/>. # along with this software. If not, see <http://www.gnu.org/licenses/>.
from django.db import models from django.db import models
from django.utils.translation import gettext as _
from multiselectfield import MultiSelectField from multiselectfield import MultiSelectField
@ -27,15 +28,31 @@ CATEGORY_CHOICES = (
) )
TAGS_CHOICES = ( TAGS_CHOICES = (
('sex', 'Sex'), ('sex', _('Sex')),
('work', 'Work'), ('work', _('Work')),
('happy', 'Happy'), ('happy', _('Happy')),
('food', 'Food'), ('food', _('Food')),
('field', 'Field'), ('field', _('Field')),
('boring', 'Boring'), ('boring', _('Boring')),
('interesting', 'Interesting'), ('interesting', _('Interesting')),
('huge', 'huge'), ('huge', _('Huge')),
('nice', 'Nice'), ('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) default=1)
tags = MultiSelectField(choices=TAGS_CHOICES, tags = MultiSelectField(choices=TAGS_CHOICES,
null=True, blank=True) null=True, blank=True)
published_in = MultiSelectField(_("Province or State"), max_length=2, choices=PROVINCES_AND_STATES)
def __str__(self): def __str__(self):
return self.title return self.title

View file

@ -14,6 +14,8 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with this software. If not, see <http://www.gnu.org/licenses/>. # along with this software. If not, see <http://www.gnu.org/licenses/>.
import sys
from django import VERSION from django import VERSION
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.forms.models import modelform_factory from django.forms.models import modelform_factory
@ -24,6 +26,12 @@ from multiselectfield.utils import get_max_length
from .models import Book from .models import Book
if sys.version_info < (3,):
u = unicode
else:
u = str
if VERSION < (1, 9): if VERSION < (1, 9):
def get_field(model, name): def get_field(model, name):
return model._meta.get_field_by_name(name)[0] return model._meta.get_field_by_name(name)[0]
@ -36,12 +44,47 @@ class MultiSelectTestCase(TestCase):
fixtures = ['app_data.json'] 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): def test_filter(self):
self.assertEqual(Book.objects.filter(tags__contains='sex').count(), 1) self.assertEqual(Book.objects.filter(tags__contains='sex').count(), 1)
self.assertEqual(Book.objects.filter(tags__contains='boring').count(), 0) 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): 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) self.assertEqual(len(form_class.base_fields), 3)
form = form_class({'title': 'new book', form = form_class({'title': 'new book',
'categories': '1,2'}) 'categories': '1,2'})
@ -87,10 +130,10 @@ class MultiSelectTestCase(TestCase):
class MultiSelectUtilsTestCase(TestCase): class MultiSelectUtilsTestCase(TestCase):
def test_get_max_length_max_length_is_not_none(self): 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): 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): def test_get_max_length_max_length_is_none_and_choices_is_not_empty(self):
choices = [ choices = [
@ -98,4 +141,4 @@ class MultiSelectUtilsTestCase(TestCase):
('key2', 'value2'), ('key2', 'value2'),
('key3', 'value3'), ('key3', 'value3'),
] ]
self.assertEqual(14, get_max_length(choices, None)) self.assertEqual(get_max_length(choices, None), 14)

View file

@ -16,7 +16,7 @@
import sys import sys
import django from django import VERSION
from django.db import models from django.db import models
from django.utils.text import capfirst from django.utils.text import capfirst
@ -26,7 +26,7 @@ from ..forms.fields import MultiSelectFormField, MaxChoicesValidator
from ..utils import get_max_length from ..utils import get_max_length
from ..validators import MaxValueMultiFieldValidator from ..validators import MaxValueMultiFieldValidator
if sys.version_info[0] == 2: if sys.version_info < (3,):
string_type = unicode string_type = unicode
else: else:
string_type = str string_type = str
@ -78,7 +78,7 @@ class MultiSelectField(models.CharField):
arr_choices = self.get_choices_selected(self.get_choices_default()) arr_choices = self.get_choices_selected(self.get_choices_default())
for opt_select in value: for opt_select in value:
if (opt_select not in arr_choices): 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}) raise exceptions.ValidationError(self.error_messages['invalid_choice'] % {"value": value})
else: else:
raise exceptions.ValidationError(self.error_messages['invalid_choice'] % value) 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_list' % self.name, get_list)
setattr(cls, 'get_%s_display' % self.name, get_display) setattr(cls, 'get_%s_display' % self.name, get_display)
if django.VERSION < (1, 8): if VERSION < (1, 8):
MultiSelectField = add_metaclass(models.SubfieldBase)(MultiSelectField) MultiSelectField = add_metaclass(models.SubfieldBase)(MultiSelectField)
try: try:

View file

@ -3,9 +3,11 @@ envlist = py26-dj14,py27-dj14,py26-dj15,py27-dj15,py26-dj16,py27-dj16,py33-dj16,
[testenv] [testenv]
usedevelop = True usedevelop = True
setenv =
PYTHONPATH=.
commands = commands =
PYTHONPATH=. python {envbindir}/coverage run -p example/run_tests.py 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 example.settings_no_debug
install_command = install_command =
pip install {opts} {packages} pip install {opts} {packages}