Merge pull request #52 from goinnn/min-choices

Min choices field option
This commit is contained in:
blag 2017-01-01 21:15:38 -07:00 committed by GitHub
commit 5d02ceb6ba
16 changed files with 124 additions and 74 deletions

View file

@ -7,52 +7,60 @@ python:
- "3.4" - "3.4"
- "3.5" - "3.5"
env: env:
- DJANGO=1.4.22 - DJANGO_VERSION='Django>=1.4,<1.5'
- DJANGO=1.5.12 - DJANGO_VERSION='Django>=1.5,<1.6'
- DJANGO=1.6.11 - DJANGO_VERSION='Django>=1.6,<1.7'
- DJANGO=1.7.11 - DJANGO_VERSION='Django>=1.7,<1.8'
- DJANGO=1.8.14 - DJANGO_VERSION='Django>=1.8,<1.9'
- DJANGO=1.9.9 - DJANGO_VERSION='Django>=1.9,<1.10'
- DJANGO=1.10.1 - DJANGO_VERSION='Django>=1.10,<1.11'
- DJANGO_VERSION='https://github.com/django/django/archive/master.tar.gz'
matrix: matrix:
exclude: exclude:
- python: "2.6" - python: "2.6"
env: DJANGO=1.7.11 env: DJANGO_VERSION='Django>=1.7,<1.8'
- python: "2.6" - python: "2.6"
env: DJANGO=1.8.14 env: DJANGO_VERSION='Django>=1.8,<1.9'
- python: "2.6" - python: "2.6"
env: DJANGO=1.9.9 env: DJANGO_VERSION='Django>=1.9,<1.10'
- python: "2.6" - python: "2.6"
env: DJANGO=1.10.1 env: DJANGO_VERSION='Django>=1.10,<1.11'
- python: "2.6"
env: DJANGO_VERSION='https://github.com/django/django/archive/master.tar.gz'
- python: "3.3" - python: "3.3"
env: DJANGO=1.4.22 env: DJANGO_VERSION='Django>=1.4,<1.5'
- python: "3.3"
env: DJANGO_VERSION='Django>=1.5,<1.6'
- python: "3.3"
env: DJANGO_VERSION='Django>=1.9,<1.10'
- python: "3.3"
env: DJANGO_VERSION='Django>=1.10,<1.11'
- python: "3.3"
env: DJANGO_VERSION='https://github.com/django/django/archive/master.tar.gz'
- python: "3.4" - python: "3.4"
env: DJANGO=1.4.22 env: DJANGO_VERSION='Django>=1.4,<1.5'
- python: "3.5"
env: DJANGO=1.4.22
- python: "3.3"
env: DJANGO=1.5.12
- python: "3.4" - python: "3.4"
env: DJANGO=1.5.12 env: DJANGO_VERSION='Django>=1.5,<1.6'
- python: "3.5"
env: DJANGO=1.5.12
- python: "3.4" - python: "3.4"
env: DJANGO=1.6.11 env: DJANGO_VERSION='Django>=1.6,<1.7'
- python: "3.5" - python: "3.5"
env: DJANGO=1.6.11 env: DJANGO_VERSION='Django>=1.4,<1.5'
- python: "3.5" - python: "3.5"
env: DJANGO=1.7.11 env: DJANGO_VERSION='Django>=1.5,<1.6'
- python: "3.3" - python: "3.5"
env: DJANGO=1.9.9 env: DJANGO_VERSION='Django>=1.6,<1.7'
- python: "3.3" - python: "3.5"
env: DJANGO=1.10.1 env: DJANGO_VERSION='Django>=1.7,<1.8'
allow_failures:
- env: DJANGO_VERSION='https://github.com/django/django/archive/master.tar.gz'
install: install:
- pip install -q Django==$DJANGO - pip install -q $DJANGO_VERSION
- pip install tox coveralls - pip install tox coveralls flake8
script: script:
- coverage erase - coverage erase
- if [[ $(python -c 'import sys; print("0" if sys.version_info < (2, 7) else "1")') == "1" ]]; then flake8 --ignore=E501; fi
- PYTHONPATH=. coverage run -p example/run_tests.py - PYTHONPATH=. coverage run -p example/run_tests.py
- PYTHONPATH=. coverage run -p example/run_tests.py example.settings_no_debug - PYTHONPATH=. coverage run -p example/run_tests.py example.settings_no_debug
after_success: after_success:

View file

@ -22,4 +22,5 @@ from .models import Book
class BookAdmin(admin.ModelAdmin): class BookAdmin(admin.ModelAdmin):
pass pass
admin.site.register(Book, BookAdmin) admin.site.register(Book, BookAdmin)

View file

@ -28,15 +28,15 @@ CATEGORY_CHOICES = (
) )
TAGS_CHOICES = ( TAGS_CHOICES = (
('sex', _('Sex')), ('sex', _('Sex')), # noqa: E241
('work', _('Work')), ('work', _('Work')), # noqa: E241
('happy', _('Happy')), ('happy', _('Happy')), # noqa: E241
('food', _('Food')), ('food', _('Food')), # noqa: E241
('field', _('Field')), ('field', _('Field')), # noqa: E241
('boring', _('Boring')), ('boring', _('Boring')), # noqa: E241
('interesting', _('Interesting')), ('interesting', _('Interesting')), # noqa: E241
('huge', _('Huge')), ('huge', _('Huge')), # noqa: E241
('nice', _('Nice')), ('nice', _('Nice')), # noqa: E241
) )
PROVINCES = ( PROVINCES = (
@ -52,7 +52,7 @@ STATES = (
PROVINCES_AND_STATES = ( PROVINCES_AND_STATES = (
(_("Canada - Provinces"), PROVINCES), (_("Canada - Provinces"), PROVINCES),
(_("USA - States"), STATES), (_("USA - States"), STATES), # noqa: E241
) )
@ -60,7 +60,7 @@ class Book(models.Model):
title = models.CharField(max_length=200) title = models.CharField(max_length=200)
categories = MultiSelectField(choices=CATEGORY_CHOICES, categories = MultiSelectField(choices=CATEGORY_CHOICES,
max_choices=3, max_choices=3,
#default='1,5') # default='1,5')
default=1) default=1)
tags = MultiSelectField(choices=TAGS_CHOICES, tags = MultiSelectField(choices=TAGS_CHOICES,
null=True, blank=True) null=True, blank=True)

View file

@ -27,7 +27,7 @@ from .models import Book, PROVINCES, STATES, PROVINCES_AND_STATES
if sys.version_info < (3,): if sys.version_info < (3,):
u = unicode u = unicode # noqa: F821
else: else:
u = str u = str
@ -129,7 +129,7 @@ class MultiSelectTestCase(TestCase):
self.assertEqual(get_field(Book, 'categories').value_to_string(book), '1,3,5') self.assertEqual(get_field(Book, 'categories').value_to_string(book), '1,3,5')
def test_flatchoices(self): def test_flatchoices(self):
self.assertEqual(get_field(Book, 'published_in').flatchoices, list(PROVINCES+STATES)) self.assertEqual(get_field(Book, 'published_in').flatchoices, list(PROVINCES + STATES))
def test_named_groups(self): def test_named_groups(self):
self.assertEqual(get_field(Book, 'published_in').choices, PROVINCES_AND_STATES) self.assertEqual(get_field(Book, 'published_in').choices, PROVINCES_AND_STATES)

View file

@ -35,6 +35,7 @@ except ImportError: # Django < 1.4
from .views import app_index from .views import app_index
urlpatterns = patterns('', urlpatterns = patterns(
'',
url(r'^$', app_index, name='app_index'), url(r'^$', app_index, name='app_index'),
) )

View file

@ -100,7 +100,7 @@ STATICFILES_DIRS = (
STATICFILES_FINDERS = ( STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# 'django.contrib.staticfiles.finders.DefaultStorageFinder', # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
) )
# Make this unique, and don't share it with anybody. # Make this unique, and don't share it with anybody.
@ -191,13 +191,13 @@ INSTALLED_APPS = (
# If formadmin is installed # If formadmin is installed
from django.conf import ENVIRONMENT_VARIABLE from django.conf import ENVIRONMENT_VARIABLE # noqa: E402
# I check it if formadmin is installed of this way because if I execute # I check it if formadmin is installed of this way because if I execute
# python manage.py runserver --settings=settings_no_debug # python manage.py runserver --settings=settings_no_debug
# I get an error # I get an error
if os.environ[ENVIRONMENT_VARIABLE] == 'example.settings': if os.environ[ENVIRONMENT_VARIABLE] == 'example.settings':
try: try:
import formadmin import formadmin # noqa: F401
INSTALLED_APPS += ('formadmin',) INSTALLED_APPS += ('formadmin',)
except ImportError: except ImportError:
pass pass

View file

@ -1,4 +1,4 @@
from example.settings import * from example.settings import * # noqa: F401,F403
DEBUG = False DEBUG = False
TEMPLATE_DEBUG = DEBUG TEMPLATE_DEBUG = DEBUG

View file

@ -42,12 +42,14 @@ js_info_dict = {
'packages': ('django.conf',), 'packages': ('django.conf',),
} }
urlpatterns = patterns('', urlpatterns = patterns(
'',
url(r'^', include('app.urls')), url(r'^', include('app.urls')),
url(r'^admin/', include(admin.site.urls)), url(r'^admin/', include(admin.site.urls)),
) )
urlpatterns += patterns('', urlpatterns += patterns(
'',
url(r'^%s(?P<path>.*)$' % settings.MEDIA_URL[1:], url(r'^%s(?P<path>.*)$' % settings.MEDIA_URL[1:],
serve, serve,
{'document_root': settings.MEDIA_ROOT, 'show_indexes': True}), {'document_root': settings.MEDIA_ROOT, 'show_indexes': True}),

View file

@ -15,16 +15,17 @@ framework.
""" """
import os import os
# This application object is used by any WSGI server configured to use this
# file. This includes Django's development server, if the WSGI_APPLICATION
# setting points here.
from django.core.wsgi import get_wsgi_application
# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
# if running multiple sites in the same mod_wsgi process. To fix this, use # if running multiple sites in the same mod_wsgi process. To fix this, use
# mod_wsgi daemon mode with each site in its own daemon process, or use # mod_wsgi daemon mode with each site in its own daemon process, or use
# os.environ["DJANGO_SETTINGS_MODULE"] = "test_project.settings" # os.environ["DJANGO_SETTINGS_MODULE"] = "test_project.settings"
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings")
# This application object is used by any WSGI server configured to use this
# file. This includes Django's development server, if the WSGI_APPLICATION
# setting points here.
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application() application = get_wsgi_application()
# Apply WSGI middleware here. # Apply WSGI middleware here.

View file

@ -1,2 +1,2 @@
from multiselectfield.db.fields import MultiSelectField from multiselectfield.db.fields import MultiSelectField # noqa: F401
from multiselectfield.forms.fields import MultiSelectFormField from multiselectfield.forms.fields import MultiSelectFormField # noqa: F401

View file

@ -22,12 +22,12 @@ from django.db import models
from django.utils.text import capfirst from django.utils.text import capfirst
from django.core import exceptions from django.core import exceptions
from ..forms.fields import MultiSelectFormField, MaxChoicesValidator from ..forms.fields import MultiSelectFormField, MinChoicesValidator, 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 < (3,): if sys.version_info < (3,):
string_type = unicode string_type = unicode # noqa: F821
else: else:
string_type = str string_type = str
@ -50,10 +50,13 @@ class MultiSelectField(models.CharField):
""" Choice values can not contain commas. """ """ Choice values can not contain commas. """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.min_choices = kwargs.pop('min_choices', None)
self.max_choices = kwargs.pop('max_choices', None) self.max_choices = kwargs.pop('max_choices', None)
super(MultiSelectField, self).__init__(*args, **kwargs) super(MultiSelectField, self).__init__(*args, **kwargs)
self.max_length = get_max_length(self.choices, self.max_length) self.max_length = get_max_length(self.choices, self.max_length)
self.validators[0] = MaxValueMultiFieldValidator(self.max_length) self.validators[0] = MaxValueMultiFieldValidator(self.max_length)
if self.min_choices is not None:
self.validators.append(MinChoicesValidator(self.min_choices))
if self.max_choices is not None: if self.max_choices is not None:
self.validators.append(MaxChoicesValidator(self.max_choices)) self.validators.append(MaxChoicesValidator(self.max_choices))
@ -145,6 +148,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 VERSION < (1, 8): if VERSION < (1, 8):
MultiSelectField = add_metaclass(models.SubfieldBase)(MultiSelectField) MultiSelectField = add_metaclass(models.SubfieldBase)(MultiSelectField)

View file

@ -17,13 +17,14 @@
from django import forms from django import forms
from ..utils import get_max_length from ..utils import get_max_length
from ..validators import MaxValueMultiFieldValidator, MaxChoicesValidator from ..validators import MaxValueMultiFieldValidator, MinChoicesValidator, MaxChoicesValidator
class MultiSelectFormField(forms.MultipleChoiceField): class MultiSelectFormField(forms.MultipleChoiceField):
widget = forms.CheckboxSelectMultiple widget = forms.CheckboxSelectMultiple
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.min_choices = kwargs.pop('min_choices', None)
self.max_choices = kwargs.pop('max_choices', None) self.max_choices = kwargs.pop('max_choices', None)
self.max_length = kwargs.pop('max_length', None) self.max_length = kwargs.pop('max_length', None)
super(MultiSelectFormField, self).__init__(*args, **kwargs) super(MultiSelectFormField, self).__init__(*args, **kwargs)
@ -31,3 +32,5 @@ class MultiSelectFormField(forms.MultipleChoiceField):
self.validators.append(MaxValueMultiFieldValidator(self.max_length)) self.validators.append(MaxValueMultiFieldValidator(self.max_length))
if self.max_choices is not None: if self.max_choices is not None:
self.validators.append(MaxChoicesValidator(self.max_choices)) self.validators.append(MaxChoicesValidator(self.max_choices))
if self.min_choices is not None:
self.validators.append(MinChoicesValidator(self.min_choices))

View file

@ -18,8 +18,8 @@ import sys
if sys.version_info[0] == 2: if sys.version_info[0] == 2:
string = basestring string = basestring # noqa: F821
string_type = unicode string_type = unicode # noqa: F821
else: else:
string = str string = str
string_type = string string_type = string

View file

@ -20,9 +20,16 @@ from django.utils.translation import ugettext_lazy as _
class MaxValueMultiFieldValidator(validators.MaxLengthValidator): class MaxValueMultiFieldValidator(validators.MaxLengthValidator):
clean = lambda self, x: len(','.join(x))
code = 'max_multifield_value' code = 'max_multifield_value'
def clean(self, x):
return len(','.join(x))
class MinChoicesValidator(validators.MinLengthValidator):
message = _(u'You must select a minimum of %(limit_value)d choices.')
code = 'min_choices'
class MaxChoicesValidator(validators.MaxLengthValidator): class MaxChoicesValidator(validators.MaxLengthValidator):
message = _(u'You must select a maximum of %(limit_value)d choices.') message = _(u'You must select a maximum of %(limit_value)d choices.')

View file

@ -23,6 +23,7 @@ from setuptools import setup, find_packages
def read(*rnames): def read(*rnames):
return open(os.path.join(os.path.dirname(__file__), *rnames)).read() return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
setup( setup(
name="django-multiselectfield", name="django-multiselectfield",
version="0.1.4", version="0.1.4",
@ -51,6 +52,7 @@ setup(
'django>=1.4', 'django>=1.4',
'tox', 'tox',
'coverage', 'coverage',
'flake8',
], ],
install_requires=[ install_requires=[
'django>=1.4', 'django>=1.4',

43
tox.ini
View file

@ -1,11 +1,12 @@
[tox] [tox]
envlist = py26-dj14,py27-dj14,py26-dj15,py27-dj15,py26-dj16,py27-dj16,py33-dj16,py27-dj17,py33-dj17,py34-dj17,py27-dj18,py33-dj18,py34-dj18,py35-dj18,py27-dj19,py34-dj19,py35-dj19,py27-dj110,py34-dj110,py35-dj110 envlist = py26-dj14,py26-dj15,py26-dj16,py27-dj14,py27-dj15,py27-dj16,py27-dj17,py27-dj18,py27-dj19,py27-dj110,py33-dj16,py33-dj17,py33-dj18,py34-dj17,py34-dj18,py34-dj19,py34-dj110,py35-dj18,py35-dj19,py35-dj110
[testenv] [testenv]
usedevelop = True usedevelop = True
setenv = setenv =
PYTHONPATH=. PYTHONPATH=.
commands = commands =
python {envbindir}/flake8 --ignore=E501
python {envbindir}/coverage run -p example/run_tests.py python {envbindir}/coverage run -p example/run_tests.py
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 =
@ -18,6 +19,7 @@ deps =
pillow==1.7.8 pillow==1.7.8
PyYAML==3.10 PyYAML==3.10
coveralls==0.3 coveralls==0.3
flake8
[testenv:py26-dj15] [testenv:py26-dj15]
basepython = python2.6 basepython = python2.6
@ -26,6 +28,7 @@ deps =
pillow==1.7.8 pillow==1.7.8
PyYAML==3.10 PyYAML==3.10
coveralls==0.3 coveralls==0.3
flake8
[testenv:py26-dj16] [testenv:py26-dj16]
basepython = python2.6 basepython = python2.6
@ -34,6 +37,7 @@ deps =
pillow==1.7.8 pillow==1.7.8
PyYAML==3.10 PyYAML==3.10
coveralls==0.3 coveralls==0.3
flake8
[testenv:py27-dj14] [testenv:py27-dj14]
@ -43,6 +47,7 @@ deps =
pillow==1.7.8 pillow==1.7.8
PyYAML==3.10 PyYAML==3.10
coveralls==0.3 coveralls==0.3
flake8
[testenv:py27-dj15] [testenv:py27-dj15]
basepython = python2.7 basepython = python2.7
@ -51,6 +56,7 @@ deps =
pillow==1.7.8 pillow==1.7.8
PyYAML==3.10 PyYAML==3.10
coveralls==0.3 coveralls==0.3
flake8
[testenv:py27-dj16] [testenv:py27-dj16]
basepython = python2.7 basepython = python2.7
@ -59,6 +65,7 @@ deps =
pillow==1.7.8 pillow==1.7.8
PyYAML==3.10 PyYAML==3.10
coveralls==0.3 coveralls==0.3
flake8
[testenv:py27-dj17] [testenv:py27-dj17]
basepython = python2.7 basepython = python2.7
@ -67,30 +74,34 @@ deps =
pillow==1.7.8 pillow==1.7.8
PyYAML==3.10 PyYAML==3.10
coveralls==0.3 coveralls==0.3
flake8
[testenv:py27-dj18] [testenv:py27-dj18]
basepython = python2.7 basepython = python2.7
deps = deps =
django==1.8.14 django==1.8.17
pillow==1.7.8 pillow==1.7.8
PyYAML==3.10 PyYAML==3.10
coveralls==0.3 coveralls==0.3
flake8
[testenv:py27-dj19] [testenv:py27-dj19]
basepython = python2.7 basepython = python2.7
deps = deps =
django==1.9.9 django==1.9.12
pillow==1.7.8 pillow==1.7.8
PyYAML==3.10 PyYAML==3.10
coveralls==0.3 coveralls==0.3
flake8
[testenv:py27-dj110] [testenv:py27-dj110]
basepython = python2.7 basepython = python2.7
deps = deps =
django==1.10.1 django==1.10.4
pillow==1.7.8 pillow==1.7.8
PyYAML==3.10 PyYAML==3.10
coveralls==0.3 coveralls==0.3
flake8
[testenv:py33-dj16] [testenv:py33-dj16]
@ -100,6 +111,7 @@ deps =
pillow==2.1.0 pillow==2.1.0
PyYAML==3.10 PyYAML==3.10
coveralls==0.3 coveralls==0.3
flake8
[testenv:py33-dj17] [testenv:py33-dj17]
basepython = python3.3 basepython = python3.3
@ -108,14 +120,16 @@ deps =
pillow==2.1.0 pillow==2.1.0
PyYAML==3.10 PyYAML==3.10
coveralls==0.3 coveralls==0.3
flake8
[testenv:py33-dj18] [testenv:py33-dj18]
basepython = python3.3 basepython = python3.3
deps = deps =
django==1.8.14 django==1.8.17
pillow==2.1.0 pillow==2.1.0
PyYAML==3.10 PyYAML==3.10
coveralls==0.3 coveralls==0.3
flake8
[testenv:py34-dj17] [testenv:py34-dj17]
@ -125,52 +139,59 @@ deps =
pillow==2.1.0 pillow==2.1.0
PyYAML==3.10 PyYAML==3.10
coveralls==0.3 coveralls==0.3
flake8
[testenv:py34-dj18] [testenv:py34-dj18]
basepython = python3.4 basepython = python3.4
deps = deps =
django==1.8.14 django==1.8.17
pillow==2.1.0 pillow==2.1.0
PyYAML==3.10 PyYAML==3.10
coveralls==0.3 coveralls==0.3
flake8
[testenv:py34-dj19] [testenv:py34-dj19]
basepython = python3.4 basepython = python3.4
deps = deps =
django==1.9.9 django==1.9.12
pillow==2.1.0 pillow==2.1.0
PyYAML==3.10 PyYAML==3.10
coveralls==0.3 coveralls==0.3
flake8
[testenv:py34-dj110] [testenv:py34-dj110]
basepython = python3.4 basepython = python3.4
deps = deps =
django==1.10.1 django==1.10.4
pillow==2.1.0 pillow==2.1.0
PyYAML==3.10 PyYAML==3.10
coveralls==0.3 coveralls==0.3
flake8
[testenv:py35-dj18] [testenv:py35-dj18]
basepython = python3.5 basepython = python3.5
deps = deps =
django==1.8.14 django==1.8.17
pillow==2.1.0 pillow==2.1.0
PyYAML==3.10 PyYAML==3.10
coveralls==0.3 coveralls==0.3
flake8
[testenv:py35-dj19] [testenv:py35-dj19]
basepython = python3.5 basepython = python3.5
deps = deps =
django==1.9.9 django==1.9.12
pillow==2.1.0 pillow==2.1.0
PyYAML==3.10 PyYAML==3.10
coveralls==0.3 coveralls==0.3
flake8
[testenv:py35-dj110] [testenv:py35-dj110]
basepython = python3.5 basepython = python3.5
deps = deps =
django==1.10.1 django==1.10.4
pillow==2.1.0 pillow==2.1.0
PyYAML==3.10 PyYAML==3.10
coveralls==0.3 coveralls==0.3
flake8