diff --git a/.travis.yml b/.travis.yml index a1eb130..52afbaa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,10 +2,11 @@ language: python python: - 2.7 + - 3.2 + - 3.3 + - 3.4 env: - - DJANGO=1.4 - - DJANGO=1.5 - DJANGO=1.6 - DJANGO=1.7 - DJANGO=1.8 @@ -14,10 +15,11 @@ env: matrix: include: - python: 2.6 - env: DJANGO=1.4 - - python: 2.6 - env: DJANGO=1.5 - - python: 2.6 + env: DJANGO=1.6 + exclude: + - python: 3.2 + env: DJANGO=master + - python: 3.4 env: DJANGO=1.6 allow_failures: - env: DJANGO=1.8 diff --git a/dynamic_choices/admin.py b/dynamic_choices/admin.py index 7c0ec5e..9743b4d 100644 --- a/dynamic_choices/admin.py +++ b/dynamic_choices/admin.py @@ -1,40 +1,33 @@ +from __future__ import unicode_literals + import json from functools import update_wrapper -import django +from django.conf.urls import url from django.contrib import admin from django.core.exceptions import ImproperlyConfigured, ValidationError from django.db import models +from django.db.models.constants import LOOKUP_SEP from django.forms.models import ModelForm, _get_foreign_key, model_to_dict from django.forms.widgets import Select, SelectMultiple from django.http import Http404, HttpResponse, HttpResponseBadRequest -from django.template.base import FilterExpression from django.template.defaultfilters import escape from django.template.loader import get_template from django.template.loader_tags import ExtendsNode -from django.utils.encoding import force_unicode +from django.utils.encoding import force_text from django.utils.functional import Promise -from django.utils.safestring import SafeUnicode +from django.utils.safestring import SafeText +from django.utils.six import with_metaclass +from django.utils.six.moves import range -from .compat import get_model_name from .forms import DynamicModelForm, dynamic_model_form_factory from .forms.fields import DynamicModelChoiceField -try: - from django.conf.urls import url -except ImportError: - from django.conf.urls.defaults import url - -try: - from django.db.models.constants import LOOKUP_SEP -except ImportError: - from django.db.models.sql.constants import LOOKUP_SEP - class LazyEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, Promise): - return force_unicode(obj) + return force_text(obj) return super(LazyEncoder, self).default(obj) lazy_encoder = LazyEncoder() @@ -46,7 +39,7 @@ def get_dynamic_choices_from_form(form): prefix = "%s-%s" % (form.prefix, '%s') else: prefix = '%s' - for name, field in form.fields.iteritems(): + for name, field in form.fields.items(): if isinstance(field, DynamicModelChoiceField): widget_cls = field.widget.widget.__class__ if widget_cls in (Select, SelectMultiple): @@ -63,33 +56,18 @@ def get_dynamic_choices_from_form(form): def dynamic_formset_factory(fieldset_cls, initial): class cls(fieldset_cls): - if django.VERSION >= (1, 6): - def __init__(self, *args, **kwargs): - super(cls, self).__init__(*args, **kwargs) - store = getattr(self, 'initial', None) - if store is None: - store = [] - setattr(self, 'initial', store) - for i in xrange(self.total_form_count()): - try: - actual = store[i] - actual.update(initial) - except (ValueError, IndexError): - store.insert(i, initial) - else: - def _construct_forms(self): - "Append initial data for every single form" - store = getattr(self, 'initial', None) - if store is None: - store = [] - setattr(self, 'initial', store) - for i in xrange(self.total_form_count()): - try: - actual = store[i] - actual.update(initial) - except (ValueError, IndexError): - store.insert(i, initial) - return super(cls, self)._construct_forms() + def __init__(self, *args, **kwargs): + super(cls, self).__init__(*args, **kwargs) + store = getattr(self, 'initial', None) + if store is None: + store = [] + setattr(self, 'initial', store) + for i in range(self.total_form_count()): + try: + actual = store[i] + actual.update(initial) + except (ValueError, IndexError): + store.insert(i, initial) @property def empty_form(self): @@ -102,7 +80,7 @@ def empty_form(self): self.add_fields(form, None) return form - cls.__name__ = "Dynamic%s" % fieldset_cls.__name__ + cls.__name__ = str("Dynamic%s" % fieldset_cls.__name__) return cls @@ -125,7 +103,7 @@ def get_formset(self, request, obj=None, **kwargs): raise Exception('DynamicAdmin inlines\'s formset\'s form must be an instance of DynamicModelForm') return formset - cls.__name__ = "Dynamic%s" % inline_cls.__name__ + cls.__name__ = str("Dynamic%s" % inline_cls.__name__) return cls @@ -134,10 +112,7 @@ def template_extends(template_name, expected_parent_name): if (len(template.nodelist) and isinstance(template.nodelist[0], ExtendsNode)): node = template.nodelist[0] - parent_name = node.parent_name - # As of django 1.4, parent_name is a FilterExpression - if isinstance(parent_name, FilterExpression): - parent_name = parent_name.resolve({}) + parent_name = node.parent_name.resolve({}) if parent_name == expected_parent_name: return True else: @@ -162,8 +137,8 @@ def __new__(cls, name, bases, attrs): # Make sure the specified add|change_form_template # extends "admin/dynamic_choices/change_form.html" - for t, default in {'add_form_template': None, - 'change_form_template': change_form_template}.iteritems(): + for t, default in [('add_form_template', None), + ('change_form_template', change_form_template)]: if t in attrs: if not template_extends(attrs[t], change_form_template): raise ImproperlyConfigured("Make sure specified %s.%s='%s' template extends '%s' " @@ -179,10 +154,7 @@ def __new__(cls, name, bases, attrs): return super(meta_cls, cls).__new__(cls, name, bases, attrs) - class cls(admin_cls): - - __metaclass__ = meta_cls - + class cls(with_metaclass(meta_cls, admin_cls)): def _media(self): media = super(cls, self).media media.add_js(('js/dynamic-choices.js', @@ -196,7 +168,7 @@ def wrapper(*args, **kwargs): return self.admin_site.admin_view(view)(*args, **kwargs) return update_wrapper(wrapper, view) - info = self.model._meta.app_label, get_model_name(self.model._meta) + info = self.model._meta.app_label, self.model._meta.model_name urlpatterns = [ url(r'(?:add|(?P\w+))/choices/$', @@ -221,7 +193,7 @@ def add_fields(to_fields, to_field, bind_fields): to_fields[to_field] = set() to_fields[to_field].update(bind_fields) - model_name = get_model_name(self.model._meta) + model_name = self.model._meta.model_name # Use get_form in order to allow formfield override # We should create a fake request from referer but all this @@ -256,15 +228,14 @@ def add_fields(to_fields, to_field, bind_fields): inlines[prefix] = inline # Replace sets in order to allow JSON serialization - for field, bindeds in fields.iteritems(): - fields[field] = list(bindeds) + for field, bound_fields in fields.items(): + fields[field] = list(bound_fields) - for fieldset, inline_fields in inlines.iteritems(): - for field, bindeds in inline_fields.iteritems(): - inlines[fieldset][field] = list(bindeds) + for fieldset, inline_fields in inlines.items(): + for field, bound_fields in inline_fields.items(): + inlines[fieldset][field] = list(bound_fields) - return SafeUnicode(u"django.dynamicAdmin(%s, %s);" % (json.dumps(fields), - json.dumps(inlines))) + return SafeText("django.dynamicAdmin(%s, %s);" % (json.dumps(fields), json.dumps(inlines))) def dynamic_choices(self, request, object_id=None): @@ -273,7 +244,7 @@ def dynamic_choices(self, request, object_id=None): # Make sure the specified object exists if object_id is not None and obj is None: raise Http404('%(name)s object with primary key %(key)r does not exist.' % { - 'name': force_unicode(opts.verbose_name), 'key': escape(object_id)}) + 'name': force_text(opts.verbose_name), 'key': escape(object_id)}) form = self.get_form(request)(request.GET, instance=obj) data = get_dynamic_choices_from_form(form) @@ -290,8 +261,8 @@ def dynamic_choices(self, request, object_id=None): if 'DYNAMIC_CHOICES_FIELDS' in request.GET: fields = request.GET.get('DYNAMIC_CHOICES_FIELDS').split(',') - for field in data.keys(): - if not (field in fields): + for field in list(data): + if field not in fields: del data[field] return HttpResponse(lazy_encoder.encode(data), content_type='application/json') @@ -321,16 +292,10 @@ def get_formsets(self, request, obj=None): else: initial[k] = v - try: - # Django >= 1.4 - inline_instances = self.get_inline_instances(request) - except AttributeError: - # Django < 1.4 - inline_instances = self.inline_instances - + inline_instances = self.get_inline_instances(request) for formset, inline in zip(super(cls, self).get_formsets(request, obj), inline_instances): fk = _get_foreign_key(self.model, inline.model, fk_name=inline.fk_name).name - fk_initial = dict(('%s__%s' % (fk, k), v) for k, v in initial.iteritems()) + fk_initial = dict(('%s__%s' % (fk, k), v) for k, v in initial.items()) # If we must provide additional data # we must wrap the formset in a subclass # because passing 'initial' key argument is intercepted diff --git a/dynamic_choices/compat.py b/dynamic_choices/compat.py deleted file mode 100644 index dd87a6b..0000000 --- a/dynamic_choices/compat.py +++ /dev/null @@ -1,10 +0,0 @@ -from __future__ import unicode_literals - -import django - -if django.VERSION >= (1, 6): - def get_model_name(opts): - return opts.model_name -else: - def get_model_name(opts): - return opts.module_name diff --git a/dynamic_choices/db/models.py b/dynamic_choices/db/models.py index 5283a6e..b637939 100644 --- a/dynamic_choices/db/models.py +++ b/dynamic_choices/db/models.py @@ -1,32 +1,31 @@ +from __future__ import unicode_literals + import inspect from django.core import exceptions from django.core.exceptions import FieldError from django.db.models import ForeignKey, ManyToManyField, OneToOneField from django.db.models.base import Model +from django.db.models.constants import LOOKUP_SEP from django.db.models.fields import Field, FieldDoesNotExist from django.db.models.fields.related import add_lazy_relation from django.db.models.query import QuerySet from django.db.models.signals import class_prepared from django.forms.models import model_to_dict +from django.utils import six from ..forms.fields import ( DynamicModelChoiceField, DynamicModelMultipleChoiceField, ) from .query import CompositeQuerySet, dynamic_queryset_factory -try: - from django.db.models.constants import LOOKUP_SEP -except ImportError: - from django.db.models.sql.constants import LOOKUP_SEP - class DynamicChoicesField(object): def __init__(self, *args, **kwargs): super(DynamicChoicesField, self).__init__(*args, **kwargs) # Hack to bypass non iterable choices validation - if isinstance(self._choices, basestring) or callable(self._choices): + if isinstance(self._choices, six.string_types) or callable(self._choices): self._choices_callback = self._choices self._choices = [] else: @@ -37,8 +36,7 @@ def contribute_to_class(self, cls, name): super(DynamicChoicesField, self).contribute_to_class(cls, name) if self._choices_callback is not None: - class_prepared.connect(self.__validate_definition, - sender=cls) + class_prepared.connect(self.__validate_definition, sender=cls) def __validate_definition(self, *args, **kwargs): def error(message): @@ -48,14 +46,16 @@ def error(message): # The choices we're defined by a string # therefore it should be a cls method - if isinstance(self._choices_callback, basestring): + if isinstance(self._choices_callback, six.string_types): callback = getattr(self.related.model, self._choices_callback, None) if not callable(callback): error('Cannot find method specified by choices.') args_length = 2 # Since the callback is a method we must emulate the 'self' self._choices_callback = callback + self._choices_callback_requires_instance = True else: args_length = 1 # It's a callable, it needs no reference to model instance + self._choices_callback_requires_instance = False spec = inspect.getargspec(self._choices_callback) @@ -90,7 +90,7 @@ def error(message): # The model hasn't been loaded yet # so we must stop here and start over # when it is loaded. - if isinstance(field.rel.to, basestring): + if isinstance(field.rel.to, six.string_types): self._choices_callback = original_choices_callback return add_lazy_relation(field.model, field, field.rel.to, @@ -125,7 +125,7 @@ def choices_relationships(self): def _invoke_choices_callback(self, model_instance, qs, data): args = [qs] # Make sure we pass the instance if the callback is a class method - if inspect.ismethod(self._choices_callback): + if self._choices_callback_requires_instance: args.insert(0, model_instance) values = {} diff --git a/dynamic_choices/db/query.py b/dynamic_choices/db/query.py index ccae1f5..2dae1d2 100644 --- a/dynamic_choices/db/query.py +++ b/dynamic_choices/db/query.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from itertools import chain import django diff --git a/dynamic_choices/forms/__init__.py b/dynamic_choices/forms/__init__.py index e861439..8667490 100644 --- a/dynamic_choices/forms/__init__.py +++ b/dynamic_choices/forms/__init__.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.forms.models import ModelForm from .fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField # NOQA @@ -15,8 +17,8 @@ def __init__(self, *args, **kwargs): # Fetch initial data for initial data = self.initial.copy() - # Update data if it's avaible - for field in self.fields.iterkeys(): + # Update data if it's available + for field in self.fields: raw_value = self._raw_value(field) if raw_value is not None: if raw_value: @@ -25,14 +27,14 @@ def __init__(self, *args, **kwargs): del data[field] # Bind instances to dynamic fields - for field in self.fields.itervalues(): + for field in self.fields.values(): if isinstance(field, DynamicModelChoiceField): field.set_choice_data(self.instance, data) def get_dynamic_relationships(self): rels = {} opts = self.instance._meta - for name, field in self.fields.iteritems(): + for name, field in self.fields.items(): # TODO: check for excludes? if isinstance(field, DynamicModelChoiceField): for choice in opts.get_field(name).choices_relationships: @@ -41,7 +43,7 @@ def get_dynamic_relationships(self): rels[choice].add(name) return rels - cls.__name__ = "Dynamic%s" % model_form_cls.__name__ + cls.__name__ = str("Dynamic%s" % model_form_cls.__name__) return cls DynamicModelForm = original_dynamic_model_form_factory(ModelForm) diff --git a/dynamic_choices/forms/fields.py b/dynamic_choices/forms/fields.py index 5315d7a..e95e00a 100644 --- a/dynamic_choices/forms/fields.py +++ b/dynamic_choices/forms/fields.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.db.models.query import QuerySet from django.forms.fields import ChoiceField from django.forms.models import ( @@ -15,7 +17,7 @@ def __init__(self, field): def __iter__(self): if self.field.empty_label is not None: - yield (u"", self.field.empty_label) + yield ("", self.field.empty_label) for label, queryset in self.groups: yield (label, [self.choice(obj) for obj in queryset]) diff --git a/dynamic_choices/models.py b/dynamic_choices/models.py index cc75fe3..b8b0c0d 100644 --- a/dynamic_choices/models.py +++ b/dynamic_choices/models.py @@ -1 +1,3 @@ +from __future__ import unicode_literals + # Empty module to make sure this app can be found by django diff --git a/setup.py b/setup.py index 2f29bb5..5ca60af 100755 --- a/setup.py +++ b/setup.py @@ -29,8 +29,11 @@ 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Internet :: WWW/HTTP :: WSGI', @@ -39,6 +42,6 @@ ], keywords=['django admin choices dynamic'], packages=find_packages(exclude=['tests', 'tests.*']), - install_requires=['Django>=1.4,<1.8'], + install_requires=['Django>=1.6,<1.9'], include_package_data=True, ) diff --git a/tests/admin.py b/tests/admin.py index 595b736..20bc30b 100644 --- a/tests/admin.py +++ b/tests/admin.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.contrib import admin from dynamic_choices.admin import DynamicAdmin diff --git a/tests/forms.py b/tests/forms.py index 0adca6f..27bab09 100644 --- a/tests/forms.py +++ b/tests/forms.py @@ -1,4 +1,6 @@ +from __future__ import unicode_literals + from django import forms diff --git a/tests/models.py b/tests/models.py index 3a02bcc..c63c0e2 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,6 +1,8 @@ +from __future__ import unicode_literals from django.db import models -from django.utils.encoding import force_unicode +from django.utils.encoding import force_text +from django.utils.six import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ from dynamic_choices.db.models import ( @@ -25,19 +27,21 @@ def same_alignment(queryset, alignment=None): def alignment_display(alignment): field = Puppet._meta.get_field('alignment') - return force_unicode(dict(field.flatchoices).get(int(alignment), alignment), strings_only=True) + return force_text(dict(field.flatchoices).get(int(alignment), alignment), strings_only=True) +@python_2_unicode_compatible class Master(models.Model): alignment = models.SmallIntegerField(choices=ALIGNMENTS) class Meta: app_label = 'dynamic_choices' - def __unicode__(self): - return u"%s master (%d)" % (self.get_alignment_display(), self.pk) + def __str__(self): + return "%s master (%s)" % (self.get_alignment_display(), self.pk) +@python_2_unicode_compatible class Puppet(models.Model): alignment = models.SmallIntegerField(choices=ALIGNMENTS) master = DynamicChoicesForeignKey(Master, choices=same_alignment) @@ -50,8 +54,8 @@ class Puppet(models.Model): class Meta: app_label = 'dynamic_choices' - def __unicode__(self): - return u"%s puppet (%d)" % (self.get_alignment_display(), self.id) + def __str__(self): + return "%s puppet (%s)" % (self.get_alignment_display(), self.pk) def choices_for_friends(self, queryset, id=None, alignment=None): """ diff --git a/tests/settings.py b/tests/settings.py index b0d3ea7..447c51f 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,7 +1,5 @@ from __future__ import unicode_literals -from django.conf.global_settings import TEST_RUNNER - DEBUG = True SECRET_KEY = 'not-anymore' @@ -34,6 +32,3 @@ ] ROOT_URLCONF = 'tests.urls' - -if not TEST_RUNNER.endswith('DiscoverRunner'): - TEST_RUNNER = str('discover_runner.DiscoverRunner') diff --git a/tests/tests.py b/tests/tests.py index dbef0a3..c490ff0 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + import json import os @@ -5,8 +7,10 @@ FieldError, ImproperlyConfigured, ValidationError, ) from django.db.models import Model -from django.test import TestCase +from django.test import SimpleTestCase, TestCase from django.test.client import Client +from django.test.utils import override_settings +from django.utils.encoding import force_text from dynamic_choices.admin import DynamicAdmin from dynamic_choices.db.models import DynamicChoicesForeignKey @@ -63,40 +67,35 @@ def test_invalid_value(self): ) -class ImproperlyConfiguredAmin(TestCase): - def test_change_form_template_override(self): - """ - Make sure ImproperlyConfigured exceptions are raised when a - `DynamicAdmin` subclass defines a `change_form_template` which do not - extends `admin/dynamic_choices/change_form.html`. - """ - TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', - ) - TEMPLATE_DIRS = ( - os.path.join(MODULE_PATH, 'templates'), +@override_settings( + TEMPLATE_LOADERS=[ + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + ], + TEMPLATE_DIRS=[os.path.join(MODULE_PATH, 'templates')], +) +class AdminConfigurationTests(SimpleTestCase): + def test_doesnt_extend_change_form(self): + expected_message = ( + "Make sure specified " + "ChangeFormDoNotExtends.change_form_template='dynamic_choices_tests/do_not_extends_change_form.html' " + "template extends 'admin/dynamic_choices/change_form.html' in order to enable DynamicAdmin" ) - with self.settings(TEMPLATE_LOADERS=TEMPLATE_LOADERS, - TEMPLATE_DIRS=TEMPLATE_DIRS): - with self.assertRaises(ImproperlyConfigured): - type('ChangeFormDoNotExtends', (DynamicAdmin,), - {'change_form_template': 'dynamic_choices_tests/do_not_extends_change_form.html'}) - try: - type('ChangeFormExtends', (DynamicAdmin,), - {'change_form_template': 'dynamic_choices_tests/extends_change_form.html'}) - except ImproperlyConfigured: - self.fail('Overriding the `change_form_template` on a ' - '`DynamicAdmin` subclass should work when the ' - 'specified template extends "admin/dynamic_choices/change_form.html"') - try: - type('ChangeFormExtendsChild', (DynamicAdmin,), - {'change_form_template': 'dynamic_choices_tests/extends_change_form_twice.html'}) - except ImproperlyConfigured: - self.fail('Overriding the `change_form_template` on a ' - '`DynamicAdmin` subclass should work when the ' - 'specified template extends "admin/dynamic_choices/change_form.html" ' - 'indirectly.') + with self.assertRaisesMessage(ImproperlyConfigured, expected_message): + class ChangeFormDoNotExtends(DynamicAdmin): + change_form_template = 'dynamic_choices_tests/do_not_extends_change_form.html' + + def test_change_form_extends(self): + """Overriding the `change_form_template` on a `DynamicAdmin` subclass should work when the specified + template extends the dynamic choices one.""" + class ChangeFormExtends(DynamicAdmin): + change_form_template = 'dynamic_choices_tests/extends_change_form.html' + + def test_change_form_extends_child(self): + """Overriding the `change_form_template` on a `DynamicAdmin` subclass should work when the specified + template extends the dynamic choices one.""" + class ChangeFormExtends(DynamicAdmin): + change_form_template = 'dynamic_choices_tests/extends_change_form_twice.html' class AdminTest(TestCase): @@ -221,21 +220,19 @@ def _get_choices(self, data=None): } if data: default.update(data) - return self.client.get('/admin/dynamic_choices/puppet/1/choices/', - default) + return self.client.get('/admin/dynamic_choices/puppet/1/choices/', default) def test_medias_presence(self): """Make sure extra js files are present in the response""" response = self.client.get('/admin/dynamic_choices/puppet/1/') - self.assertIn('js/dynamic-choices.js', response.content) - self.assertIn('js/dynamic-choices-admin.js', response.content) + self.assertContains(response, 'js/dynamic-choices.js') + self.assertContains(response, 'js/dynamic-choices-admin.js') def test_fk_as_empty_string(self): """Make sure fk specified as empty string are parsed correctly""" data = {'alignment': ''} response = self._get_choices(data) - self.assertEquals(200, response.status_code, - 'Empty string fk shouldn\'t be cast as int') + self.assertEqual(200, response.status_code, "Empty string fk shouldn't be cast as int") def test_empty_string_value_overrides_default(self): """Make sure specified empty string overrides instance field""" @@ -244,25 +241,23 @@ def test_empty_string_value_overrides_default(self): 'enemy_set-0-id': 1, 'enemy_set-0-enemy': '', 'enemy_set-TOTAL_FORMS': 3, - 'enemy_set-INITIAL_FORMS': 1 + 'enemy_set-INITIAL_FORMS': 1, } response = self._get_choices(data) self.assertEqual(response.status_code, 200) - data = json.loads(response.content) - self.assertEqual(data['enemy_set-0-because_of']['value'], - [['', '---------']]) + data = json.loads(force_text(response.content)) + self.assertEqual(data['enemy_set-0-because_of']['value'], [['', '---------']]) def test_empty_form(self): """Make sure data is provided for an empty form""" data = { 'DYNAMIC_CHOICES_FIELDS': 'enemy_set-__prefix__-enemy', - 'alignment': 1 + 'alignment': 1, } response = self._get_choices(data) self.assertEqual(response.status_code, 200) - data = json.loads(response.content) - self.assertEqual(data['enemy_set-__prefix__-enemy']['value'], - [ + data = json.loads(force_text(response.content)) + self.assertEqual(data['enemy_set-__prefix__-enemy']['value'], [ ['', '---------'], ['Evil', [[2, 'Evil puppet (2)'], ]], ['Neutral', []], diff --git a/tests/urls.py b/tests/urls.py index ed6db10..162cfb7 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,11 +1,8 @@ -from __future__ import absolute_import +from __future__ import absolute_import, unicode_literals -from . import admin +from django.conf.urls import include, url -try: - from django.conf.urls import include, url -except ImportError: - from django.conf.urls.defaults import include, url +from . import admin urlpatterns = [ url(r'^admin/', include(admin.site.urls)), diff --git a/tox.ini b/tox.ini index 0b0f743..8f5d5b0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,19 @@ [tox] args_are_paths = false envlist = - py26-{1.4,1.5,1.6}, - py27-{1.4,1.5,1.6,1.7,1.8,master} + py26-1.6, + py27-{1.6,1.7,1.8,master}, + py32-{1.6,1.7,1.8}, + py33-{1.6,1.7,1.8,master}, + py34-{1.7,1.8,master}, [testenv] basepython = py26: python2.6 py27: python2.7 + py32: python3.2 + py33: python3.3 + py34: python3.4 usedevelop = true commands = python -R -Wonce {envbindir}/coverage run {envbindir}/django-admin.py test -v2 --settings=tests.settings {posargs} @@ -15,10 +21,6 @@ commands = deps = coverage py26: argparse - 1.4: Django>=1.4,<1.5 - 1.4: django-discover-runner - 1.5: Django>=1.5,<1.6 - 1.5: django-discover-runner 1.6: Django>=1.6,<1.7 1.7: Django>=1.7,<1.8 1.8: Django==1.8b1