diff --git a/.gitignore b/.gitignore index a7f7aeb..1833915 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ dist .coverage* .env example/example.db +.tox + diff --git a/.travis.yml b/.travis.yml index 873d9f2..5acb9b9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,25 +5,79 @@ python: - "3.4" - "3.5" - "3.6" + - "3.7" env: + - DJANGO_VERSION='Django>=1.4,<1.5' + - DJANGO_VERSION='Django>=1.5,<1.6' + - DJANGO_VERSION='Django>=1.6,<1.7' + - DJANGO_VERSION='Django>=1.7,<1.8' + - DJANGO_VERSION='Django>=1.8,<1.9' + - DJANGO_VERSION='Django>=1.9,<1.10' + - DJANGO_VERSION='Django>=1.10,<1.11' - DJANGO_VERSION='Django>=1.11,<2.0' - DJANGO_VERSION='Django>=2.0,<2.1' - DJANGO_VERSION='Django>=2.1,<2.2' - DJANGO_VERSION='https://github.com/django/django/archive/master.tar.gz' matrix: exclude: + - python: "2.7" + env: DJANGO_VERSION='https://github.com/django/django/archive/master.tar.gz' + - python: "2.7" + env: DJANGO_VERSION='Django>=2.1,<2.2' - python: "2.7" env: DJANGO_VERSION='Django>=2.0,<2.1' - python: "2.7" - env: DJANGO_VERSION='Django>=2.1,<2.2' - - python: "2.7" - env: DJANGO_VERSION='https://github.com/django/django/archive/master.tar.gz' + env: DJANGO_VERSION='Django>=1.6,<1.7' + - python: "3.4" + env: DJANGO_VERSION='Django>=1.4,<1.5' + - python: "3.4" + env: DJANGO_VERSION='Django>=1.5,<1.6' + - python: "3.4" + env: DJANGO_VERSION='Django>=1.6,<1.7' - python: "3.4" env: DJANGO_VERSION='Django>=2.1,<2.2' - - python: "2.7" + - python: "3.4" env: DJANGO_VERSION='https://github.com/django/django/archive/master.tar.gz' - allow_failures: - - env: DJANGO_VERSION='https://github.com/django/django/archive/master.tar.gz' + - python: "3.5" + env: DJANGO_VERSION='Django>=1.4,<1.5' + - python: "3.5" + env: DJANGO_VERSION='Django>=1.5,<1.6' + - python: "3.5" + env: DJANGO_VERSION='Django>=1.6,<1.7' + - python: "3.5" + env: DJANGO_VERSION='Django>=1.7,<1.8' + - python: "3.5" + env: DJANGO_VERSION='Django>=2.1,<2.2' + - python: "3.5" + env: DJANGO_VERSION='https://github.com/django/django/archive/master.tar.gz' + - python: "3.6" + env: DJANGO_VERSION='Django>=1.4,<1.5' + - python: "3.6" + env: DJANGO_VERSION='Django>=1.5,<1.6' + - python: "3.6" + env: DJANGO_VERSION='Django>=1.6,<1.7' + - python: "3.6" + env: DJANGO_VERSION='Django>=1.7,<1.8' + - python: "3.6" + env: DJANGO_VERSION='Django>=1.8,<1.9' + - python: "3.6" + env: DJANGO_VERSION='Django>=1.9,<1.10' + - python: "3.6" + env: DJANGO_VERSION='Django>=1.10,<1.11' + - python: "3.7" + env: DJANGO_VERSION='Django>=1.4,<1.5' + - python: "3.7" + env: DJANGO_VERSION='Django>=1.5,<1.6' + - python: "3.7" + env: DJANGO_VERSION='Django>=1.6,<1.7' + - python: "3.7" + env: DJANGO_VERSION='Django>=1.7,<1.8' + - python: "3.7" + env: DJANGO_VERSION='Django>=1.8,<1.9' + - python: "3.7" + env: DJANGO_VERSION='Django>=1.9,<1.10' + - python: "3.7" + env: DJANGO_VERSION='Django>=1.10,<1.11' install: - pip install -q $DJANGO_VERSION diff --git a/CHANGES.rst b/CHANGES.rst index 197d7b3..12e1fc0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +0.1.9 (2019-10-02) +------------------ + +* Added support for Django 2 +* Added support for Python 3.6 +* Drop support for Python (2.6, 3.3) +* Thanks to: + * `hirokinko `_ + 0.1.6 (2017-05-10) ------------------ @@ -64,7 +73,7 @@ * Test/example project * Now works if the first composant of the list of tuple is an integer -* Now max_length is not required, the Multiselect field calculate it automatically. +* Now max_length is not required, the Multiselect field calculate it automatically. * The max_choices attr can be a attr in the model field * Refactor the code * Spanish translations @@ -88,4 +97,4 @@ 0.0.1 (2012-09-27) ------------------ -* Initial version from the next `snippet `_ \ No newline at end of file +* Initial version from the next `snippet `_ diff --git a/README.rst b/README.rst index 628dbad..5eaa059 100644 --- a/README.rst +++ b/README.rst @@ -16,7 +16,7 @@ This egg is inspired by this `snippet Supported Python versions: 2.7, 3.4+ -Supported Django versions: 1.11-2.0+ +Supported Django versions: 1.4-2.0+ Installation ============ @@ -35,25 +35,25 @@ Configure 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, @@ -103,7 +103,7 @@ Django REST Framework comes with a ``MultipleChoiceField`` that works perfectly .. code-block:: python from rest_framework import fields, serializers - + from myapp.models import MY_CHOICES, MY_CHOICES2 class MyModelSerializer(serializers.HyperlinkedModelSerializer): diff --git a/example/app/test_msf.py b/example/app/test_msf.py index dc057ee..ca121e7 100644 --- a/example/app/test_msf.py +++ b/example/app/test_msf.py @@ -32,8 +32,12 @@ else: u = str -def get_field(model, name): - return model._meta.get_field(name) +if VERSION < (1, 9): + def get_field(model, name): + return model._meta.get_field_by_name(name)[0] +else: + def get_field(model, name): + return model._meta.get_field(name) class MultiSelectTestCase(TestCase): @@ -73,8 +77,12 @@ class MultiSelectTestCase(TestCase): # 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+. - self.assertListEqual(tag_list_list, [['sex', 'work', 'happy']]) - self.assertListEqual(categories_list_list, [['1', '3', '5']]) + 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=('title', 'tags', 'categories')) @@ -131,17 +139,28 @@ class MultiSelectTestCase(TestCase): self.assertEqual(len(form_class.base_fields), 1) form = form_class(initial={'published_in': ['BC', 'AK']}) - expected_html = u("""

  • Canada - Provinces
    • \n""" + expected_html = u("""

      • Canada - Provinces
        • \n""" """
      • \n""" """
      • USA - States
        • \n""" """
        • \n""" """

      """) actual_html = form.as_p() + if (1, 11) <= VERSION: + # Django 1.11+ does not assign 'for' attributes on labels if they + # are group labels + expected_html = expected_html.replace('label for="id_published_in_0"', 'label') + + if VERSION < (1, 6): + # Django 1.6 renders the Python repr() for each group (eg: tuples + # with HTML entities), so we skip the test for that version + self.assertEqual(expected_html.replace('\n', ''), actual_html.replace('\n', '')) if VERSION >= (2, 0): expected_html = expected_html.replace('input checked="checked"', 'input checked') + if VERSION >= (1, 7): + self.assertHTMLEqual(expected_html, actual_html) self.assertHTMLEqual(expected_html, actual_html) diff --git a/example/app/views.py b/example/app/views.py index 6142ef1..587443e 100644 --- a/example/app/views.py +++ b/example/app/views.py @@ -14,14 +14,17 @@ # You should have received a copy of the GNU Lesser General Public License # along with this software. If not, see . - +from django import VERSION from django.conf import settings from django.contrib.auth import login from django.contrib.auth import get_user_model - -from django.urls import reverse from django.http import HttpResponseRedirect +if VERSION >= (2, 0): + from django.urls import reverse +else: + from django.core.urlresolvers import reverse + def app_index(request): user = get_user_model().objects.get(username='admin') diff --git a/example/example/settings.py b/example/example/settings.py index cf50cf4..ff2e5d0 100644 --- a/example/example/settings.py +++ b/example/example/settings.py @@ -181,7 +181,7 @@ INSTALLED_APPS = ( 'django.contrib.messages', 'django.contrib.staticfiles', # Uncomment the next line to enable the admin: - 'django.contrib.admin', + # 'django.contrib.admin', # Uncomment the next line to enable admin documentation: # 'django.contrib.admindocs', diff --git a/example/example/urls.py b/example/example/urls.py index 0e60278..77c7f6a 100644 --- a/example/example/urls.py +++ b/example/example/urls.py @@ -14,33 +14,54 @@ # You should have received a copy of the GNU Lesser General Public License # along with this software. If not, see . +from django import VERSION from django.conf import settings -try: - from django.conf.urls import include, url -except ImportError: - from django.urls import include, url -from django.contrib import admin from django.views.static import serve -admin.autodiscover() +try: + from django.conf.urls import include, url + + # Compatibility for Django > 1.8 + def patterns(prefix, *args): + if VERSION < (1, 9): + from django.conf.urls import patterns as django_patterns + return django_patterns(prefix, *args) + elif prefix != '': + raise NotImplementedError("You need to update your URLConf for " + "Django 1.10, or tweak it to remove the " + "prefix parameter") + else: + return list(args) +except ImportError: # Django < 1.4 + if VERSION < (1, 4): + from django.conf.urls.defaults import include, patterns, url + else: + from django.urls import include, url + js_info_dict = { 'packages': ('django.conf',), } -urlpatterns = [ - url(r'^', include('app.urls')), - url(r'^admin/', admin.site.urls), -] - -urlpatterns += [ - url( - r'^%s(?P.*)$' % settings.MEDIA_URL[1:], - serve, - { - 'document_root': settings.MEDIA_ROOT, - 'show_indexes': True, - }, - ), -] +if VERSION < (1, 11): + urlpatterns = patterns( + '', + url(r'^', include('app.urls')), + ) + urlpatterns += patterns( + '', + url(r'^%s(?P.*)$' % settings.MEDIA_URL[1:]), + ) +else: + urlpatterns = [ + url(r'^', include('app.urls')), + url( + r'^%s(?P.*)$' % settings.MEDIA_URL[1:], + serve, + { + 'document_root': settings.MEDIA_ROOT, + 'show_indexes': True, + }, + ), + ] diff --git a/example/run_tests.py b/example/run_tests.py index ec3f39a..42bf316 100644 --- a/example/run_tests.py +++ b/example/run_tests.py @@ -19,6 +19,7 @@ import os import sys +import django from django.conf import ENVIRONMENT_VARIABLE from django.core import management from django.core.wsgi import get_wsgi_application @@ -29,6 +30,10 @@ if len(sys.argv) == 1: else: os.environ[ENVIRONMENT_VARIABLE] = sys.argv[1] -application = get_wsgi_application() +if django.VERSION[0] == 1 and django.VERSION[1] >= 7: + from django.core.wsgi import get_wsgi_application as get_wsgi_application_v1 + application = get_wsgi_application_v1() +else: + application = get_wsgi_application() management.call_command('test', 'app') diff --git a/multiselectfield/db/fields.py b/multiselectfield/db/fields.py index 0fd8966..cb5e68f 100644 --- a/multiselectfield/db/fields.py +++ b/multiselectfield/db/fields.py @@ -20,7 +20,6 @@ from django import VERSION from django.db import models from django.utils.text import capfirst -from django.utils.encoding import python_2_unicode_compatible from django.core import exceptions from ..forms.fields import MultiSelectFormField, MinChoicesValidator, MaxChoicesValidator @@ -47,7 +46,6 @@ def add_metaclass(metaclass): return wrapper -@python_2_unicode_compatible class MSFList(list): def __init__(self, choices, *args, **kwargs): @@ -58,6 +56,10 @@ class MSFList(list): msg_list = [msgl.choices.get(int(i)) if i.isdigit() else msgl.choices.get(i) for i in msgl] return u', '.join([string_type(s) for s in msg_list]) + if sys.version_info < (3,): + def __unicode__(self, msgl): + return self.__str__(msgl) + class MultiSelectField(models.CharField): """ Choice values can not contain commas. """ @@ -102,7 +104,10 @@ class MultiSelectField(models.CharField): return choices_selected def value_to_string(self, obj): - value = super(MultiSelectField, self).value_from_object(obj) + try: + value = self._get_val_from_obj(obj) + except AttributeError: + value = super(MultiSelectField, self).value_from_object(obj) return self.get_prep_value(value) def validate(self, value, model_instance): diff --git a/setup.py b/setup.py index acf9632..98185c4 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ def read(*rnames): setup( name="django-multiselectfield", - version="0.1.8", + version="0.1.9", author="Pablo Martin", author_email="goinnn@gmail.com", description="Django multiple select field", @@ -41,8 +41,10 @@ setup( 'Framework :: Django', 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)', 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', @@ -54,13 +56,13 @@ setup( packages=find_packages(), include_package_data=True, tests_require=[ - 'django>=1.11', + 'django>=1.4', 'tox', 'coverage', 'flake8', ], install_requires=[ - 'django>=1.11', + 'django>=1.4', ], zip_safe=False, ) diff --git a/tox.ini b/tox.ini index df70693..9d221da 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{27,34,35,36}-dj111,py{34,35,36,37}-dj20,py{35,36,37}-dj21 +envlist = py{27,34,35,36}-dj{14,15,16,16,17,18,19,110,111},py{34,35,36,37}-dj{14,15,16,16,17,18,19,110,111,20},py{35,36,37}-dj{21,22}, [testenv] usedevelop = True @@ -12,6 +12,69 @@ commands = install_command = pip install {opts} {packages} +[testenv:py27-dj14] +basepython = python2.7 +deps = + django==1.4.22 + pillow==1.7.8 + PyYAML==3.10 + coveralls==0.3 + flake8 + +[testenv:py27-dj15] +basepython = python2.7 +deps = + django==1.5.12 + pillow==1.7.8 + PyYAML==3.10 + coveralls==0.3 + flake8 + +[testenv:py27-dj16] +basepython = python2.7 +deps = + django==1.6.11 + pillow==1.7.8 + PyYAML==3.10 + coveralls==0.3 + flake8 + +[testenv:py27-dj17] +basepython = python2.7 +deps = + django==1.7.11 + pillow==1.7.8 + PyYAML==3.10 + coveralls==0.3 + flake8 + +[testenv:py27-dj18] +basepython = python2.7 +deps = + django==1.8.17 + pillow==1.7.8 + PyYAML==3.10 + coveralls==0.3 + flake8 + +[testenv:py27-dj19] +basepython = python2.7 +deps = + django==1.9.12 + pillow==1.7.8 + PyYAML==3.10 + coveralls==0.3 + flake8 + +[testenv:py27-dj110] +basepython = python2.7 +deps = + django==1.10.4 + pillow==1.7.8 + PyYAML==3.10 + coveralls==0.3 + flake8 + [testenv:py27-dj111] basepython = python2.7 deps = @@ -21,7 +84,6 @@ deps = coveralls==0.3 flake8 - [testenv:py34-dj111] basepython = python3.4 deps = @@ -40,7 +102,6 @@ deps = coveralls==0.3 flake8 - [testenv:py35-dj111] basepython = python3.5 deps = @@ -68,6 +129,15 @@ deps = coveralls==0.3 flake8 +[testenv:py35-dj22] +basepython = python3.5 +deps = + django==2.2.1 + pillow==2.1.0 + PyYAML==3.13 + coveralls==0.3 + flake8 + [testenv:py36-dj111] basepython = python3.6 deps = @@ -95,6 +165,14 @@ deps = coveralls==0.3 flake8 +[testenv:py36-dj22] +basepython = python3.6 +deps = + django==2.2.1 + pillow==2.1.0 + PyYAML==3.13 + coveralls==0.3 + flake8 [testenv:py37-dj20] basepython = python3.7 @@ -112,4 +190,13 @@ deps = pillow==2.1.0 PyYAML==3.13 coveralls==0.3 - flake8 \ No newline at end of file + flake8 + +[testenv:py37-dj22] +basepython = python3.7 +deps = + django==2.2.1 + pillow==2.1.0 + PyYAML==3.13 + coveralls==0.3 + flake8