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}