Add test for .values_list()
This commit is contained in:
parent
67e47d24a8
commit
d997a7a422
28
README.rst
28
README.rst
|
@ -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,11 +24,11 @@ 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'),
|
||||||
|
@ -44,7 +44,7 @@ In your models.py
|
||||||
|
|
||||||
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,
|
||||||
|
@ -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,11 +72,23 @@ 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:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
git clone https://github.com/goinnn/django-multiselectfield
|
git clone https://github.com/goinnn/django-multiselectfield
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
6
tox.ini
6
tox.ini
|
@ -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}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue