From 4166a6022e08a85393ac86708781973abf0e6890 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Mon, 27 Aug 2018 17:19:40 +0200 Subject: [PATCH] Remove Django 1.7, 1.8, 1.9, 1.10 compatibility code --- .travis.yml | 14 ---- docs/advanced/migrating.rst | 10 +-- docs/advanced/mptt.rst | 20 +---- docs/conf.py | 3 +- example/article/models.py | 7 +- example/article/tests.py | 15 +--- example/example/settings.py | 8 +- example/example/urls.py | 17 +--- parler/admin.py | 72 ++++------------- parler/cache.py | 10 +-- parler/fields.py | 22 +----- parler/forms.py | 45 +++-------- parler/managers.py | 68 ++-------------- parler/models.py | 77 +++++-------------- parler/static/parler/admin/parler_admin.css | 17 ++++ .../static/parler/admin/parler_admin_flat.css | 19 ----- parler/templatetags/parler_tags.py | 16 +--- parler/tests/test_admin.py | 6 +- parler/tests/test_forms.py | 13 +--- parler/tests/test_model_construction.py | 56 +------------- parler/tests/test_urls.py | 28 +------ parler/tests/test_utils.py | 7 +- parler/tests/testapp/models.py | 7 +- parler/tests/testapp/urls.py | 14 +--- parler/utils/compat.py | 10 +-- parler/utils/template.py | 5 +- parler/views.py | 8 +- runtests.py | 77 +++++++------------ tox.ini | 12 +-- 29 files changed, 148 insertions(+), 535 deletions(-) delete mode 100644 parler/static/parler/admin/parler_admin_flat.css diff --git a/.travis.yml b/.travis.yml index c9ae0537..bdfa92ce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,28 +7,14 @@ python: - '3.4' - '3.6' env: -- DJANGO="django>=1.7.0,<1.8.0" -- DJANGO="django>=1.8.0,<1.9.0" -- DJANGO="django>=1.9.0,<1.10.0" -- DJANGO="django>=1.10.1,<1.11.0" - DJANGO="django>=1.11.0,<1.12.0" - DJANGO="django>=2.0,<2.1" matrix: exclude: - - python: '3.3' - env: DJANGO="django>=1.9.0,<1.10.0" - - python: '3.3' - env: DJANGO="django>=1.10.1,<1.11.0" - python: '3.3' env: DJANGO="django>=1.11.0,<1.12.0" - python: '3.3' env: DJANGO="django>=2.0,<2.1" - - python: '3.6' - env: DJANGO="django>=1.7.0,<1.8.0" - - python: '3.6' - env: DJANGO="django>=1.8.0,<1.9.0" - - python: '3.6' - env: DJANGO="django>=1.9.0,<1.10.0" - python: '2.7' env: DJANGO="django>=2.0,<2.1" before_install: diff --git a/docs/advanced/migrating.rst b/docs/advanced/migrating.rst index 0403c476..fc49414e 100644 --- a/docs/advanced/migrating.rst +++ b/docs/advanced/migrating.rst @@ -32,10 +32,9 @@ First create the translatable fields:: name=models.CharField(max_length=123), ) -Now create the migration: +Now create the migration:: -* For Django 1.7, use: ``manage.py makemigrations myapp "add_translation_model"`` -* For Django 1.8 and above, use: ``manage.py makemigrations myapp --name "add_translation_model"`` + manage.py makemigrations myapp --name "add_translation_model" Step 2: Copy the data @@ -115,10 +114,9 @@ The example model now looks like:: name=models.CharField(max_length=123), ) -Create the database migration, it will simply remove the original field. +Create the database migration, it will simply remove the original field:: -* For Django 1.7, use: ``manage.py makemigrations myapp "remove_untranslated_fields"`` -* For Django 1.8 and above, use: ``manage.py makemigrations myapp --name "remove_untranslated_fields"`` + manage.py makemigrations myapp --name "remove_untranslated_fields" Updating code diff --git a/docs/advanced/mptt.rst b/docs/advanced/mptt.rst index 8a6ea71b..6bab7249 100644 --- a/docs/advanced/mptt.rst +++ b/docs/advanced/mptt.rst @@ -49,23 +49,19 @@ Say we have a base ``Category`` model that needs to be translatable: Combining managers ------------------ -The managers can be combined by inheriting them. -Unfortunately, django-mptt_ 0.7 overrides the ``get_queryset()`` method, -so it needs to be redefined: +The managers can be combined by inheriting them: .. code-block:: python - import django from parler.managers import TranslatableManager, TranslatableQuerySet from mptt.managers import TreeManager - from mptt.querysets import TreeQuerySet # new as of mptt 0.7 + from mptt.querysets import TreeQuerySet class CategoryQuerySet(TranslatableQuerySet, TreeQuerySet): - pass - # Optional: make sure the Django 1.7 way of creating managers works. def as_manager(cls): + # make sure creating managers from querysets works. manager = CategoryManager.from_queryset(cls)() manager._built_with_as_manager = True return manager @@ -74,15 +70,7 @@ so it needs to be redefined: class CategoryManager(TreeManager, TranslatableManager): - queryset_class = CategoryQuerySet - - def get_queryset(self): - # This is the safest way to combine both get_queryset() calls - # supporting all Django versions and MPTT 0.7.x versions - return self.queryset_class(self.model, using=self._db).order_by(self.tree_id_attr, self.left_attr) - - if django.VERSION < (1,6): - get_query_set = get_queryset + _queryset_class = CategoryQuerySet Assign the manager to the model ``objects`` attribute. diff --git a/docs/conf.py b/docs/conf.py index 4f52e433..51efa0d0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,8 +22,7 @@ sys.path.insert(0, os.path.abspath('_ext')) sys.path.insert(0, os.path.abspath('..')) os.environ['DJANGO_SETTINGS_MODULE'] = 'djangodummy.settings' -if django.VERSION >= (1, 7): - django.setup() +django.setup() # -- General configuration ------------------------------------------------ diff --git a/example/article/models.py b/example/article/models.py index 59074e52..ef7998bc 100644 --- a/example/article/models.py +++ b/example/article/models.py @@ -1,15 +1,10 @@ from __future__ import unicode_literals from django.db import models +from django.urls import reverse from django.utils.encoding import python_2_unicode_compatible from parler.models import TranslatableModel, TranslatedFields from parler.utils.context import switch_language -try: - from django.urls import reverse -except ImportError: - # Django <= 1.10 - from django.core.urlresolvers import reverse - @python_2_unicode_compatible class Article(TranslatableModel): diff --git a/example/article/tests.py b/example/article/tests.py index f89e963e..fa0d9187 100644 --- a/example/article/tests.py +++ b/example/article/tests.py @@ -1,23 +1,16 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals from collections import deque -from unittest import skipIf, expectedFailure -import django from django.test.html import parse_html, Element from django.test.utils import override_settings +from django.urls import reverse from django.utils import encoding, translation from django.test import TestCase from django.contrib import auth from .models import Article, Category from parler.appsettings import PARLER_LANGUAGES -try: - from django.urls import reverse -except ImportError: - # Support for Django <= 1.10 - from django.core.urlresolvers import reverse - class TestMixin(object): @@ -79,8 +72,6 @@ def _is_dict_subset(d1, d2): class ArticleTestCase(TestMixin, TestCase): - if django.VERSION < (1, 8): - urls = 'example.urls' @override_settings(ROOT_URLCONF='example.urls') def test_home(self): @@ -128,7 +119,6 @@ def test_admin_list(self): self.assertEqual(200, resp.status_code) self.assertTemplateUsed(resp, 'admin/change_list.html') - @skipIf(django.VERSION < (1, 5), "bug with declared_fieldsets in ArticleAdmin") def test_admin_add(self): self.client.login(**self.credentials) @@ -153,7 +143,6 @@ def test_admin_add(self): self.assertEqual(200, resp.status_code) self.assertInContent('

Add Article (Dutch)

', resp) - @skipIf(django.VERSION < (1, 5), "bug with declared_fieldsets in ArticleAdmin") def test_admin_add_post(self): self.client.login(**self.credentials) resp = self.client.post( @@ -169,7 +158,6 @@ def test_admin_add_post(self): self.assertRedirects(resp, reverse('admin:article_article_changelist')) self.assertEqual(1, Article.objects.filter(translations__slug='my-article').count()) - @skipIf(django.VERSION < (1, 5), "bug with declared_fieldsets in ArticleAdmin") def test_admin_change(self): self.client.login(**self.credentials) @@ -255,7 +243,6 @@ def test_admin_delete_translation(self): self.assertEqual(200, resp.status_code) self.assertTemplateUsed(resp, 'admin/parler/deletion_not_allowed.html') - @expectedFailure def test_admin_delete_translation_unavailable(self): """ To be fixed : when trying to delete the last language when a translation diff --git a/example/example/settings.py b/example/example/settings.py index b02b087d..307becb0 100644 --- a/example/example/settings.py +++ b/example/example/settings.py @@ -1,5 +1,4 @@ # Django settings for example project. -import django from os.path import join, dirname, realpath SRC_DIR = dirname(dirname(realpath(__file__))) @@ -63,7 +62,7 @@ ] -MIDDLEWARE_CLASSES = ( +MIDDLEWARE = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', # Inserted language switcher, easy way to have multiple frontend languages. @@ -71,8 +70,6 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', ) -# Support Django >= 2.0 -MIDDLEWARE = MIDDLEWARE_CLASSES ROOT_URLCONF = 'example.urls' @@ -117,8 +114,7 @@ } } -if django.VERSION >= (1, 7): - TEST_RUNNER = 'django.test.runner.DiscoverRunner' # silence system checks +TEST_RUNNER = 'django.test.runner.DiscoverRunner' # silence system checks PARLER_DEFAULT_LANGUAGE = 'en' diff --git a/example/example/urls.py b/example/example/urls.py index 3f59f555..d1ff0bee 100644 --- a/example/example/urls.py +++ b/example/example/urls.py @@ -1,4 +1,3 @@ -import django from django.conf.urls import include, url from django.conf.urls.i18n import i18n_patterns from django.contrib import admin @@ -8,15 +7,7 @@ # Patterns are prefixed with the language code. # This is not mandatory, can also use a `django_language` cookie, # or custom middleware that calls `django.utils.translation.activate()`. -if django.VERSION >= (1, 8): - # New style without prefix argument - urlpatterns = i18n_patterns( - url(r'^admin/', admin.site.urls), - url(r'', include('article.urls')), - ) -else: - # Old style - urlpatterns = i18n_patterns('', - url(r'^admin/', include(admin.site.urls)), - url(r'', include('article.urls')), - ) +urlpatterns = i18n_patterns( + url(r'^admin/', admin.site.urls), + url(r'', include('article.urls')), +) diff --git a/parler/admin.py b/parler/admin.py index 4f1239f8..3e6e51a5 100644 --- a/parler/admin.py +++ b/parler/admin.py @@ -49,6 +49,7 @@ class ProjectAdmin(TranslatableAdmin): from django.forms import Media from django.http import HttpResponseRedirect, Http404, HttpRequest from django.shortcuts import render +from django.urls import reverse from django.utils.encoding import iri_to_uri, force_text from django.utils.functional import cached_property from django.utils.html import conditional_escape, escape @@ -63,12 +64,6 @@ class ProjectAdmin(TranslatableAdmin): from parler.utils.views import get_language_parameter, get_language_tabs from parler.utils.template import select_template_name -try: - from django.urls import reverse -except ImportError: - # Support for Django <= 1.10 - from django.core.urlresolvers import reverse - # Code partially taken from django-hvad # which is (c) 2011, Jonas Obrist, BSD licensed @@ -81,17 +76,11 @@ class ProjectAdmin(TranslatableAdmin): 'SortedRelatedFieldListFilter', ) -if django.VERSION >= (1, 9) or 'flat' in settings.INSTALLED_APPS: - _language_media = Media(css={ - 'all': ( - 'parler/admin/parler_admin.css', - 'parler/admin/parler_admin_flat.css', - ) - }) -else: - _language_media = Media(css={ - 'all': ('parler/admin/parler_admin.css',) - }) +_language_media = Media(css={ + 'all': ( + 'parler/admin/parler_admin.css', + ) +}) _language_prepopulated_media = _language_media + Media(js=( 'admin/js/urlify.js', @@ -158,10 +147,7 @@ def get_queryset(self, request): """ Make sure the current language is selected. """ - if django.VERSION >= (1, 6): - qs = super(BaseTranslatableAdmin, self).get_queryset(request) - else: - qs = super(BaseTranslatableAdmin, self).queryset(request) + qs = super(BaseTranslatableAdmin, self).get_queryset(request) if self._has_translatable_model(): if not isinstance(qs, TranslatableQuerySet): @@ -219,7 +205,6 @@ def language_column(self, object): return mark_safe( self._languages_column(object, span_classes='available-languages') ) # span class for backwards compatibility - language_column.allow_tags = True # Django < 1.9 language_column.short_description = _("Languages") def all_languages_column(self, object): @@ -233,7 +218,6 @@ def all_languages_column(self, object): object, all_languages, span_classes='all-languages' ) ) - all_languages_column.allow_tags = True # Django < 1.9 all_languages_column.short_description = _("Languages") def _languages_column(self, object, all_languages=None, span_classes=''): @@ -253,7 +237,7 @@ def _languages_column(self, object, all_languages=None, span_classes=''): if code == current_language: classes.append('current') - info = _get_model_meta(opts) + info = opts.app_label, opts.model_name admin_url = reverse('admin:{0}_{1}_change'.format(*info), args=(quote(object.pk),), current_app=self.admin_site.name) buttons.append('{title}'.format( language_code=code, @@ -301,7 +285,6 @@ def get_object(self, request, object_id, *args, **kwargs): """ Make sure the object is fetched in the correct language. """ - # The args/kwargs are to support Django 1.8, which adds a from_field parameter obj = super(TranslatableAdmin, self).get_object(request, object_id, *args, **kwargs) if obj is not None and self._has_translatable_model(): # Allow fallback to regular models. @@ -328,22 +311,12 @@ def get_urls(self): return urlpatterns else: opts = self.model._meta - info = _get_model_meta(opts) - - if django.VERSION < (1, 9): - delete_path = url( - r'^(.+)/delete-translation/(.+)/$', - self.admin_site.admin_view(self.delete_translation), - name='{0}_{1}_delete_translation'.format(*info) - ) - else: - delete_path = url( - r'^(.+)/change/delete-translation/(.+)/$', - self.admin_site.admin_view(self.delete_translation), - name='{0}_{1}_delete_translation'.format(*info) - ) - - return [delete_path] + urlpatterns + info = opts.app_label, opts.model_name + return [url( + r'^(.+)/change/delete-translation/(.+)/$', + self.admin_site.admin_view(self.delete_translation), + name='{0}_{1}_delete_translation'.format(*info) + )] + urlpatterns def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None): """ @@ -396,7 +369,7 @@ def _patch_redirect(self, request, obj, redirect): uri = iri_to_uri(request.path) opts = self.model._meta - info = _get_model_meta(opts) + info = opts.app_label, opts.model_name # Pass ?language=.. to next page. language = request.GET.get(self.query_language_key) @@ -460,10 +433,7 @@ def delete_translation(self, request, object_id, language_code): qs_opts = qs.model._meta deleted_result = get_deleted_objects(qs, qs_opts, request.user, self.admin_site, using) - if django.VERSION >= (1, 8): - (del2, model_counts, perms2, protected2) = deleted_result - else: - (del2, perms2, protected2) = deleted_result + (del2, model_counts, perms2, protected2) = deleted_result deleted_objects += del2 perms_needed = perms_needed or perms2 @@ -481,7 +451,7 @@ def delete_translation(self, request, object_id, language_code): )) if self.has_change_permission(request, None): - info = _get_model_meta(opts) + info = opts.app_label, opts.model_name return HttpResponseRedirect(reverse('admin:{0}_{1}_change'.format(*info), args=(object_id,), current_app=self.admin_site.name)) else: return HttpResponseRedirect(reverse('admin:index', current_app=self.admin_site.name)) @@ -728,11 +698,3 @@ class MyAdmin(admin.ModelAdmin): def __init__(self, *args, **kwargs): super(SortedRelatedFieldListFilter, self).__init__(*args, **kwargs) self.lookup_choices = sorted(self.lookup_choices, key=lambda a: a[1].lower()) - - -if django.VERSION >= (1, 7): - def _get_model_meta(opts): - return opts.app_label, opts.model_name -else: - def _get_model_meta(opts): - return opts.app_label, opts.module_name diff --git a/parler/cache.py b/parler/cache.py index 1932c0f6..78a44842 100644 --- a/parler/cache.py +++ b/parler/cache.py @@ -5,7 +5,6 @@ Since all calls to the translation table are routed through our model descriptor fields, cache access and expiry is rather simple to implement. """ -import django from django.core.cache import cache from django.utils import six from parler import appsettings @@ -14,11 +13,6 @@ if six.PY3: long = int -if django.VERSION >= (1, 6): - DEFAULT_TIMEOUT = cache.default_timeout -else: - DEFAULT_TIMEOUT = 0 - class IsMissing(object): # Allow _get_any_translated_model() to evaluate this as False. @@ -150,7 +144,7 @@ def _get_cached_values(instance, translated_model, language_code, use_fallback=F return values -def _cache_translation(translation, timeout=DEFAULT_TIMEOUT): +def _cache_translation(translation, timeout=cache.default_timeout): """ Store a new translation in the cache. """ @@ -171,7 +165,7 @@ def _cache_translation(translation, timeout=DEFAULT_TIMEOUT): cache.set(key, values, timeout=timeout) -def _cache_translation_needs_fallback(instance, language_code, related_name, timeout=DEFAULT_TIMEOUT): +def _cache_translation_needs_fallback(instance, language_code, related_name, timeout=cache.default_timeout): """ Store the fact that a translation doesn't exist, and the fallback should be used. """ diff --git a/parler/fields.py b/parler/fields.py index ed201529..9a93412f 100644 --- a/parler/fields.py +++ b/parler/fields.py @@ -11,16 +11,10 @@ """ from __future__ import unicode_literals -import django from django.core.exceptions import ImproperlyConfigured from django.db import models +from django.db.models.fields.related_descriptors import ForwardManyToOneDescriptor from django.forms.forms import pretty_name -from parler.utils import compat - -if django.VERSION >= (1, 9): - from django.db.models.fields.related_descriptors import ForwardManyToOneDescriptor -else: - from django.db.models.fields.related import ReverseSingleRelatedObjectDescriptor as ForwardManyToOneDescriptor def _validate_master(new_class): @@ -30,12 +24,8 @@ def _validate_master(new_class): if not new_class.master or not isinstance(new_class.master, ForwardManyToOneDescriptor): raise ImproperlyConfigured("{0}.master should be a ForeignKey to the shared table.".format(new_class.__name__)) - remote_field = compat.get_remote_field(new_class) - try: - shared_model = remote_field.model - except AttributeError: - # Django <= 1.8 compatibility - shared_model = remote_field.to + remote_field = new_class.master.field.remote_field + shared_model = remote_field.model meta = shared_model._parler_meta if meta is not None: @@ -183,11 +173,7 @@ def short_description(self): # Fallback to what the admin label_for_field() would have done otherwise. return pretty_name(self.field.name) - if django.VERSION >= (1, 8): - field = translations_model._meta.get_field(self.field.name) - else: - field = translations_model._meta.get_field_by_name(self.field.name)[0] - + field = translations_model._meta.get_field(self.field.name) return field.verbose_name diff --git a/parler/forms.py b/parler/forms.py index 7e5dc9d1..46788922 100644 --- a/parler/forms.py +++ b/parler/forms.py @@ -1,5 +1,4 @@ from django import forms -import django from django.core.exceptions import NON_FIELD_ERRORS, ObjectDoesNotExist, ValidationError from django.forms.forms import BoundField from django.forms.models import ModelFormMetaclass, BaseInlineFormSet @@ -156,45 +155,21 @@ def save_translated_fields(self): continue setattr(self.instance, field, getattr(translation, field)) - if django.VERSION >= (1, 6): + def _post_clean_translation(self, translation): + exclude = self._get_translation_validation_exclusions(translation) + try: + translation.full_clean( + exclude=exclude, validate_unique=False) + except ValidationError as e: + self._update_errors(e) - def _post_clean_translation(self, translation): - exclude = self._get_translation_validation_exclusions(translation) + # Validate uniqueness if needed. + if self._validate_unique: try: - translation.full_clean( - exclude=exclude, validate_unique=False) + translation.validate_unique() except ValidationError as e: self._update_errors(e) - # Validate uniqueness if needed. - if self._validate_unique: - try: - translation.validate_unique() - except ValidationError as e: - self._update_errors(e) - else: - - def _post_clean_translation(self, translation): - exclude = self._get_translation_validation_exclusions(translation) - # Clean the model instance's fields. - try: - translation.clean_fields(exclude=exclude) - except ValidationError as e: - self._update_errors(e.message_dict) - - # Call the model instance's clean method. - try: - translation.clean() - except ValidationError as e: - self._update_errors({NON_FIELD_ERRORS: e.messages}) - - # Validate uniqueness if needed. - if self._validate_unique: - try: - translation.validate_unique() - except ValidationError as e: - self._update_errors(e.message_dict) - @cached_property def _translated_fields(self): field_names = self._meta.model._parler_meta.get_all_fields() diff --git a/parler/managers.py b/parler/managers.py index 66050dd7..fb075ec0 100644 --- a/parler/managers.py +++ b/parler/managers.py @@ -1,7 +1,6 @@ """ Custom generic managers """ -import django from django.core.exceptions import ImproperlyConfigured from django.db import models from django.db.models.query import QuerySet @@ -23,11 +22,8 @@ def __init__(self, *args, **kwargs): super(TranslatableQuerySet, self).__init__(*args, **kwargs) self._language = None - def _clone(self, klass=None, setup=False, **kw): - if django.VERSION < (1, 9): - kw['klass'] = klass - kw['setup'] = setup - c = super(TranslatableQuerySet, self)._clone(**kw) + def _clone(self, *args, **kwargs): + c = super(TranslatableQuerySet, self)._clone(*args, **kwargs) c._language = self._language return c @@ -41,9 +37,7 @@ def create(self, **kwargs): def _fetch_all(self): # Make sure the current language is assigned when Django fetches the data. # This low-level method is overwritten as that works better across Django versions. - # Alternatives include: - # - overwriting iterator() for Django <= 1.10 - # - hacking _iterable_class, which breaks django-polymorphic + # Alternatives includes hacking the _iterable_class, which breaks django-polymorphic super(TranslatableQuerySet, self)._fetch_all() if self._language is not None and self._result_cache and isinstance(self._result_cache[0], models.Model): for obj in self._result_cache: @@ -62,11 +56,6 @@ def _extract_model_params(self, defaults, **kwargs): lookup, params = super(TranslatableQuerySet, self)._extract_model_params(defaults, **kwargs) params.update(translated_defaults) - - if (1, 7) <= django.VERSION < (1, 8): - if self._language: - params['_current_language'] = self._language - return lookup, params def language(self, language_code=None): @@ -130,57 +119,16 @@ def active_translations(self, language_code=None, **translated_fields): return self.translated(*language_codes, **translated_fields) -class TranslatableManager(models.Manager): +class TranslatableManager(models.Manager.from_queryset(TranslatableQuerySet)): """ The manager class which ensures the enhanced TranslatableQuerySet object is used. """ - queryset_class = TranslatableQuerySet def get_queryset(self): - if not issubclass(self.queryset_class, TranslatableQuerySet): - raise ImproperlyConfigured("{0}.queryset_class does not inherit from TranslatableQuerySet".format(self.__class__.__name__)) - return self.queryset_class(self.model, using=self._db) - - # Leave for Django 1.6/1.7, so backwards compatibility can be fixed. - # It will be removed in Django 1.8, so remove it here too to avoid false promises. - if django.VERSION < (1, 8): - get_query_set = get_queryset - - # NOTE: Fetching the queryset is done by calling self.all() here on purpose. - # By using .all(), the proper get_query_set()/get_queryset() will be used for each Django version. - - def language(self, language_code=None): - """ - Set the language code to assign to objects retrieved using this Manager. - """ - return self.all().language(language_code) - - def translated(self, *language_codes, **translated_fields): - """ - Only return objects which are translated in the given languages. - - NOTE: due to Django `ORM limitations `_, - this method can't be combined with other filters that access the translated fields. As such, query the fields in one filter: - - .. code-block:: python - - qs.translated('en', name="Cheese Omelette") - - This will query the translated model for the ``name`` field. - """ - return self.all().translated(*language_codes, **translated_fields) - - def active_translations(self, language_code=None, **translated_fields): - """ - Only return objects which are translated, or have a fallback that should be displayed. - - Typically that's the currently active language and fallback language. - This should be combined with ``.distinct()``. - - When ``hide_untranslated = True``, only the currently active language will be returned. - """ - return self.all().active_translations(language_code, **translated_fields) - + qs = super(TranslatableManager, self).get_queryset() + if not isinstance(qs, TranslatableQuerySet): + raise ImproperlyConfigured("{0}._queryset_class does not inherit from TranslatableQuerySet".format(self.__class__.__name__)) + return qs # Export the names in django-hvad style too: TranslationQueryset = TranslatableQuerySet diff --git a/parler/models.py b/parler/models.py index 322c7b42..08ffc03a 100644 --- a/parler/models.py +++ b/parler/models.py @@ -57,13 +57,13 @@ class Meta: The manager and queryset objects of django-parler can work together with django-mptt and django-polymorphic. """ from __future__ import unicode_literals -from collections import defaultdict -import django +from collections import defaultdict, OrderedDict from django.conf import settings from django.core.exceptions import ImproperlyConfigured, ValidationError, FieldError, ObjectDoesNotExist from django.db import models, router from django.db.models.base import ModelBase -from django.utils.encoding import python_2_unicode_compatible +from django.db.models.fields.related_descriptors import ForwardManyToOneDescriptor +from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.functional import lazy from django.utils.translation import ugettext, ugettext_lazy as _ from django.utils import six @@ -77,16 +77,6 @@ class Meta: import sys import warnings -try: - from collections import OrderedDict -except ImportError: - from django.utils.datastructures import SortedDict as OrderedDict - -if django.VERSION >= (1, 9): - from django.db.models.fields.related_descriptors import ForwardManyToOneDescriptor -else: - from django.db.models.fields.related import ReverseSingleRelatedObjectDescriptor as ForwardManyToOneDescriptor - __all__ = ( 'TranslatableModelMixin', 'TranslatableModel', @@ -153,8 +143,7 @@ def create_translations_model(shared_model, related_name, meta, **fields): # Avoid creating permissions for the translated model, these are not used at all. # This also avoids creating lengthy permission names above 50 chars. - if django.VERSION >= (1, 7): - meta.setdefault('default_permissions', ()) + meta.setdefault('default_permissions', ()) # Define attributes for translation table name = str('{0}Translation'.format(shared_model.__name__)) # makes it bytes, for type() @@ -527,7 +516,7 @@ def _get_translated_model(self, language_code=None, use_fallback=False, auto_cre 'language_code': language_code, } if self.pk: - # ID might be None at this point, and Django 1.8 does not allow that. + # ID might be None at this point, and Django does not allow that. kwargs['master'] = self object = meta.model(**kwargs) @@ -613,14 +602,8 @@ def _get_translated_queryset(self, meta=None): if meta is None: meta = self._parler_meta.root - accessor = getattr(self, meta.rel_name) - if django.VERSION >= (1, 6): - # Call latest version - return accessor.get_queryset() - else: - # Must call RelatedManager.get_query_set() and avoid calling a custom get_queryset() - # method for packages with Django 1.6/1.7 compatibility. - return accessor.get_query_set() + accessor = getattr(self, meta.rel_name) # RelatedManager + return accessor.get_queryset() def _get_prefetched_translations(self, meta=None): """ @@ -677,7 +660,7 @@ def validate_unique(self, exclude=None): try: super(TranslatableModelMixin, self).validate_unique(exclude=exclude) except ValidationError as e: - errors = e.message_dict # Django 1.5 + 1.6 compatible + errors = e.message_dict for local_cache in six.itervalues(self._translations_cache): for translation in six.itervalues(local_cache): @@ -875,21 +858,17 @@ def shared_model(self): """ Returns the shared model this model is linked to. """ - remote_field = compat.get_remote_field(self.__class__) - try: - return remote_field.model - except AttributeError: - return remote_field.to + return self.__class__.master.field.remote_field.model @property def related_name(self): """ Returns the related name that this model is known at in the shared model. """ - return compat.get_remote_field(self.__class__).related_name + return self.__class__.master.field.remote_field.related_name def save_base(self, raw=False, using=None, **kwargs): - # As of Django 1.8, not calling translations.activate() or disabling the translation + # Not calling translations.activate() or disabling the translation # causes get_language() to explicitly return None instead of LANGUAGE_CODE. # This helps developers find solutions by bailing out properly. # @@ -933,24 +912,13 @@ def delete(self, using=None): if not self._meta.auto_created: signals.post_translation_delete.send(sender=self.shared_model, instance=self, using=using) - if django.VERSION >= (1, 8): - - def _get_field_names(self): - # Use the new Model._meta API. - return [field.get_attname() for field in self._meta.get_fields() if not field.is_relation or field.many_to_one] - - def _get_field_values(self): - # Use the new Model._meta API. - return [getattr(self, field.get_attname()) for field in self._meta.get_fields() if not field.is_relation or field.many_to_one] - else: - - def _get_field_names(self): - # Return all field names in a consistent (sorted) manner. - return [field.get_attname() for field, _ in self._meta.get_fields_with_model()] + def _get_field_names(self): + # Use the new Model._meta API. + return [field.get_attname() for field in self._meta.get_fields() if not field.is_relation or field.many_to_one] - def _get_field_values(self): - # Return all field values in a consistent (sorted) manner. - return [getattr(self, field.get_attname()) for field, _ in self._meta.get_fields_with_model()] + def _get_field_values(self): + # Use the new Model._meta API. + return [getattr(self, field.get_attname()) for field in self._meta.get_fields() if not field.is_relation or field.many_to_one] @classmethod def get_translated_fields(cls): @@ -968,7 +936,7 @@ def contribute_translations(cls, shared_model): base.add_meta(ParlerMeta( shared_model=shared_model, translations_model=cls, - related_name=compat.get_remote_field(cls).related_name + related_name=cls.master.field.remote_field.related_name )) else: # Place a new _parler_meta at the current inheritance level. @@ -977,7 +945,7 @@ def contribute_translations(cls, shared_model): base, shared_model=shared_model, translations_model=cls, - related_name=compat.get_remote_field(cls).related_name + related_name=cls.master.field.remote_field.related_name ) # Assign the proxy fields @@ -1009,9 +977,7 @@ def contribute_translations(cls, shared_model): cls.DoesNotExist = type(str('DoesNotExist'), (TranslationDoesNotExist, shared_model.DoesNotExist, cls.DoesNotExist,), {}) def __str__(self): - # use format to avoid weird error in django 1.4 - # TypeError: coercing to Unicode: need string or buffer, __proxy__ found - return "{0}".format(get_language_title(self.language_code)) + return force_text(get_language_title(self.language_code)) def __repr__(self): return "<{0}: #{1}, {2}, master: #{3}>".format( @@ -1024,8 +990,7 @@ class TranslatedFieldsModel(six.with_metaclass(TranslatedFieldsModelBase, Transl class Meta: abstract = True - if django.VERSION >= (1, 7): - default_permissions = () + default_permissions = () class ParlerMeta(object): diff --git a/parler/static/parler/admin/parler_admin.css b/parler/static/parler/admin/parler_admin.css index b47d864e..c6e84401 100644 --- a/parler/static/parler/admin/parler_admin.css +++ b/parler/static/parler/admin/parler_admin.css @@ -1,3 +1,7 @@ +.parler-language-tabs { + border-bottom: 2px solid #79AEC8; +} + .parler-language-tabs span { display: inline-block; padding: 5px 15px; @@ -8,8 +12,21 @@ top: 1px; font-weight: bold; } + +.parler-language-tabs span.available, +.parler-language-tabs span.current, +.parler-language-tabs span.empty { + border-radius: 6px 6px 0 0; +} + +.parler-language-tabs span.available { + border-color: #C4DCE8; +} .parler-language-tabs span.current { border-bottom: 1px solid #fff; + background-color: #79AEC8; + border-color: #79AEC8; + color: #fff; } .parler-language-tabs span.empty { opacity: 0.7; diff --git a/parler/static/parler/admin/parler_admin_flat.css b/parler/static/parler/admin/parler_admin_flat.css deleted file mode 100644 index f1404420..00000000 --- a/parler/static/parler/admin/parler_admin_flat.css +++ /dev/null @@ -1,19 +0,0 @@ -.parler-language-tabs { - border-bottom: 2px solid #79AEC8; -} - -.parler-language-tabs span.available, -.parler-language-tabs span.current, -.parler-language-tabs span.empty { - border-radius: 6px 6px 0 0; -} - -.parler-language-tabs span.available { - border-color: #C4DCE8; -} - -.parler-language-tabs span.current { - background-color: #79AEC8; - border-color: #79AEC8; - color: #fff; -} diff --git a/parler/templatetags/parler_tags.py b/parler/templatetags/parler_tags.py index 3f52fb35..6cb0d361 100644 --- a/parler/templatetags/parler_tags.py +++ b/parler/templatetags/parler_tags.py @@ -1,27 +1,15 @@ import inspect -import django from django.template import Node, Library, TemplateSyntaxError +from django.urls import reverse from django.utils.encoding import force_text from django.utils.translation import get_language from django.utils import six from parler.models import TranslatableModel, TranslationDoesNotExist from parler.utils.context import switch_language, smart_override -try: - from django.urls import reverse -except ImportError: - # Support for Django <= 1.10 - from django.core.urlresolvers import reverse - register = Library() -if django.VERSION < (1, 9): - # Support older versions without implicit assignment support in simple_tag. - simple_tag = register.assignment_tag -else: - simple_tag = register.simple_tag - class ObjectLanguageNode(Node): @@ -75,7 +63,7 @@ def objectlanguage(parser, token): return ObjectLanguageNode(nodelist, object_var, language_var) -@simple_tag(takes_context=True) +@register.simple_tag(takes_context=True) def get_translated_url(context, lang_code, object=None): """ Get the proper URL for this page in a different language. diff --git a/parler/tests/test_admin.py b/parler/tests/test_admin.py index 062525dd..f9165356 100644 --- a/parler/tests/test_admin.py +++ b/parler/tests/test_admin.py @@ -1,13 +1,9 @@ from __future__ import unicode_literals from django.contrib.admin import AdminSite +from django.contrib.admin.utils import label_for_field from parler.admin import TranslatableAdmin - -try: - from django.contrib.admin.utils import label_for_field -except ImportError: - from django.contrib.admin.util import label_for_field from .utils import AppTestCase from .testapp.models import SimpleModel, ConcreteModel, AbstractModel diff --git a/parler/tests/test_forms.py b/parler/tests/test_forms.py index 3329e8d7..755cb4c2 100644 --- a/parler/tests/test_forms.py +++ b/parler/tests/test_forms.py @@ -1,4 +1,3 @@ -import django from django.core.exceptions import ValidationError from django.utils import translation from parler.forms import TranslatableModelForm @@ -10,32 +9,28 @@ class SimpleForm(TranslatableModelForm): class Meta: model = SimpleModel - if django.VERSION >= (1, 6): - fields = '__all__' + fields = '__all__' class CleanFieldForm(TranslatableModelForm): class Meta: model = CleanFieldModel - if django.VERSION >= (1, 6): - fields = '__all__' + fields = '__all__' class UniqueTogetherForm(TranslatableModelForm): class Meta: model = UniqueTogetherModel - if django.VERSION >= (1, 6): - fields = '__all__' + fields = '__all__' class ForeignKeyTranslationModelForm(TranslatableModelForm): class Meta: model = ForeignKeyTranslationModel - if django.VERSION >= (1, 6): - fields = '__all__' + fields = '__all__' class FormTests(AppTestCase): diff --git a/parler/tests/test_model_construction.py b/parler/tests/test_model_construction.py index 7233eff4..51242d9a 100644 --- a/parler/tests/test_model_construction.py +++ b/parler/tests/test_model_construction.py @@ -1,18 +1,11 @@ from functools import wraps +import unittest -import django from django.db import models from django.db.models import Manager from django.utils import six from parler.models import TranslatableModel from parler.models import TranslatedFields - -try: - from unittest import expectedFailure, skipIf -except ImportError: - # python<2.7 - from django.utils.unittest import expectedFailure, skipIf - from .utils import AppTestCase from .testapp.models import ManualModel, ManualModelTranslations, SimpleModel, Level1, Level2, ProxyBase, ProxyModel, DoubleModel, RegularModel, CharModel @@ -31,10 +24,7 @@ def _clearing_dec(*args, **kwargs): # TODO: this doens't yet work. apps.clear_cache() - if django.VERSION >= (1, 10): - return _clearing_dec - else: - return func + return _clearing_dec class ModelConstructionTests(AppTestCase): @@ -107,51 +97,9 @@ def test_double_translation_table(self): self.assertEqual(DoubleModel._parler_meta[0].rel_name, "base_translations") self.assertEqual(DoubleModel._parler_meta[1].rel_name, "more_translations") - @skipIf(django.VERSION >= (1, 10), "This breaks the Django 1.10 app registry") - @clear_app_registry - def test_overlapping_proxy_model(self): - """ - Test the simple model syntax. - """ - from parler.tests.testapp.invalid_models import RegularModelProxy - - # Create an object without translations - RegularModel.objects.create(id=98, original_field='untranslated') - self.assertEqual(RegularModelProxy.objects.count(), 1) - - # Refetch from db, should raise an error. - self.assertRaises(RuntimeError, lambda: RegularModelProxy.objects.all()[0]) - def test_model_with_different_pks(self): """ Test that TranslatableModels works with different types of pks """ self.assertIsInstance(SimpleModel.objects.create(tr_title='Test'), SimpleModel) self.assertIsInstance(CharModel.objects.create(pk='test', tr_title='Test'), CharModel) - - @skipIf(django.VERSION >= (1, 10), "This breaks the Django 1.10 app registry") - @clear_app_registry - @expectedFailure - def test_model_metaclass_create_order(self): - """ - For some reason, having a custom ModelBase metaclass breaks - the ``pk`` field detection when ``TranslatableModel`` is the first model in an inheritance chain. - Using ``Book(Product, TranslatableModel)`` does work. - """ - from django.db.models.base import ModelBase - class FooModelBase(ModelBase): - pass - - class FooModel(six.with_metaclass(FooModelBase, models.Model)): - class Meta: - abstract = True - - class Product(FooModel): - pass - - class Book(TranslatableModel, Product): - translations = TranslatedFields( - slug=models.SlugField(blank=False, default='', max_length=128) - ) - - self.assertTrue(Book._meta.pk) diff --git a/parler/tests/test_urls.py b/parler/tests/test_urls.py index d12f72ab..0f987207 100644 --- a/parler/tests/test_urls.py +++ b/parler/tests/test_urls.py @@ -1,33 +1,17 @@ from __future__ import unicode_literals -import django from django.test import RequestFactory +from django.test.utils import override_settings +from django.urls import reverse, resolve, get_urlconf from django.utils import translation from parler.templatetags.parler_tags import get_translated_url from .utils import AppTestCase from .testapp.models import ArticleSlugModel -try: - from django.urls import reverse, resolve, get_urlconf -except ImportError: - # Support for Django <= 1.10 - from django.core.urlresolvers import reverse, resolve, get_urlconf - -try: - from django.test.utils import override_settings # Django 1.7+ -except ImportError: - def override_settings(ROOT_URLCONF=None): - assert get_urlconf() == ROOT_URLCONF - def dummy_dec(func): - return func - return dummy_dec - class UrlTests(AppTestCase): """ Test model construction """ - if django.VERSION < (1, 8): - urls = 'parler.tests.testapp.urls' @classmethod def setUpClass(cls): @@ -147,10 +131,4 @@ def test_translatable_slug_mixin_redirect(self): # Try call on the default slug (which is resolvable), although there is a translated version. with translation.override(self.other_lang2): response = self.client.get('/{0}/article/default/'.format(self.other_lang2)) - if django.VERSION >= (1, 9): - self.assertRedirects(response, '/{0}/article/lang2/'.format(self.other_lang2), status_code=301) - elif django.VERSION >= (1, 7): - self.assertRedirects(response, 'http://testserver/{0}/article/lang2/'.format(self.other_lang2), status_code=301) - else: - self.assertEqual(response.status_code, 301, "Unexpected response, got: {0}, expected 301".format(response.content)) - self.assertEqual(response['Location'], 'http://testserver/{0}/article/lang2/'.format(self.other_lang2)) + self.assertRedirects(response, '/{0}/article/lang2/'.format(self.other_lang2), status_code=301) diff --git a/parler/tests/test_utils.py b/parler/tests/test_utils.py index 29038cc7..6f9b6309 100644 --- a/parler/tests/test_utils.py +++ b/parler/tests/test_utils.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals -import django from django.test import TestCase from django.utils.translation import override @@ -102,8 +101,7 @@ def test_get_language_no_fallback(self): """Test get_language patch function, no fallback""" with override(None): - if django.VERSION >= (1, 8): - self.assertEquals(get_language(), None) + self.assertEquals(get_language(), None) @override_parler_settings(PARLER_DEFAULT_ACTIVATE=True) def test_get_language_with_fallback(self): @@ -111,8 +109,7 @@ def test_get_language_with_fallback(self): from parler import appsettings with override(None): - if django.VERSION >= (1, 8): - self.assertEquals(get_language(), appsettings.PARLER_DEFAULT_LANGUAGE_CODE) + self.assertEquals(get_language(), appsettings.PARLER_DEFAULT_LANGUAGE_CODE) def test_url_qs(self): matches = [ diff --git a/parler/tests/testapp/models.py b/parler/tests/testapp/models.py index 6adf0570..01ae1dcc 100644 --- a/parler/tests/testapp/models.py +++ b/parler/tests/testapp/models.py @@ -1,16 +1,11 @@ from __future__ import unicode_literals from django.db import models +from django.urls import reverse from django.utils.encoding import python_2_unicode_compatible from parler.fields import TranslatedField, TranslationsForeignKey from parler.models import TranslatableModel, TranslatedFields, TranslatedFieldsModel from parler.utils.context import switch_language -try: - from django.urls import reverse -except ImportError: - # Django <= 1.10 - from django.core.urlresolvers import reverse - class ManualModel(TranslatableModel): shared = models.CharField(max_length=200, default='') diff --git a/parler/tests/testapp/urls.py b/parler/tests/testapp/urls.py index dbeb2132..22ec291a 100644 --- a/parler/tests/testapp/urls.py +++ b/parler/tests/testapp/urls.py @@ -1,18 +1,12 @@ -import django from django.conf.urls import url from django.conf.urls.i18n import i18n_patterns +from django.urls import reverse_lazy from .views import ArticleSlugView # To intru from django.contrib.auth import forms as auth_forms from django.contrib.auth import views as auth_views -try: - from django.urls import reverse_lazy -except ImportError: - # Support for Django <= 1.10 - from django.core.urlresolvers import reverse_lazy - class PasswordResetForm(auth_forms.PasswordResetForm): pass @@ -32,8 +26,4 @@ class PasswordResetForm(auth_forms.PasswordResetForm): url(r'^password-reset/done/$', auth_views.password_reset_done, name='password-reset-done'), ] -if django.VERSION >= (1, 8): - # New style without prefix - urlpatterns = i18n_patterns(*urls) -else: - urlpatterns = i18n_patterns('', *urls) +urlpatterns = i18n_patterns(*urls) diff --git a/parler/utils/compat.py b/parler/utils/compat.py index 8f5805ca..0fbc65cc 100644 --- a/parler/utils/compat.py +++ b/parler/utils/compat.py @@ -9,7 +9,7 @@ class HideChoicesCharField(models.CharField): - # For Django 1.7, hide the 'choices' for a field. + # Hide the 'choices' for a field. def deconstruct(self): name, path, args, kwargs = models.CharField.deconstruct(self) @@ -23,11 +23,3 @@ def deconstruct(self): pass return name, path, args, kwargs - - -def get_remote_field(klass): - try: - return klass.master.field.remote_field - except AttributeError: - # Django <= 1.8 compatibility - return klass.master.field.rel diff --git a/parler/utils/template.py b/parler/utils/template.py index 77752d42..1066fa76 100644 --- a/parler/utils/template.py +++ b/parler/utils/template.py @@ -5,7 +5,7 @@ _cached_name_lookups = {} -def select_template_name(template_name_list): +def select_template_name(template_name_list, using=None): """ Given a list of template names, find the first one that exists. """ @@ -18,8 +18,7 @@ def select_template_name(template_name_list): # Find which template of the template_names is selected by the Django loader. for template_name in template_name_list: try: - # TODO: For Django 1.8, add using= parameter so it only selects templates from the same engine. - get_template(template_name) + get_template(template_name, using=using) except TemplateDoesNotExist: continue else: diff --git a/parler/views.py b/parler/views.py index 5fc03621..f15274d0 100644 --- a/parler/views.py +++ b/parler/views.py @@ -14,10 +14,10 @@ * :class:`TranslatableUpdateView` - The :class:`~django.views.generic.edit.UpdateView` with :class:`TranslatableModelFormMixin` support. """ from __future__ import unicode_literals -import django from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.forms.models import modelform_factory from django.http import Http404, HttpResponsePermanentRedirect +from django.urls import reverse from django.utils import translation from django.views import generic from django.views.generic.edit import ModelFormMixin @@ -27,12 +27,6 @@ from parler.utils.context import switch_language from parler.utils.views import get_language_parameter, get_language_tabs -try: - from django.urls import reverse -except ImportError: - # Support for Django <= 1.10 - from django.core.urlresolvers import reverse - __all__ = ( 'ViewUrlMixin', diff --git a/runtests.py b/runtests.py index ceb43b2f..9c29e9ad 100755 --- a/runtests.py +++ b/runtests.py @@ -14,51 +14,8 @@ if not settings.configured: module_root = path.dirname(path.realpath(__file__)) - sys.path.insert(0, path.join(module_root, 'example')) - if django.VERSION >= (1, 8): - template_settings = dict( - TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': (), - 'OPTIONS': { - 'loaders': ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', - ), - 'context_processors': ( - 'django.template.context_processors.debug', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.request', - 'django.template.context_processors.static', - 'django.contrib.auth.context_processors.auth', - ), - }, - }, - ] - ) - else: - template_settings = dict( - TEMPLATE_LOADERS = ( - 'django.template.loaders.app_directories.Loader', - 'django.template.loaders.filesystem.Loader', - ), - TEMPLATE_CONTEXT_PROCESSORS = list(default_settings.TEMPLATE_CONTEXT_PROCESSORS) + [ - 'django.core.context_processors.request', - ], - TEMPLATE_DEBUG = True, - ) - - MIDDLEWARE = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.locale.LocaleMiddleware', # / will be redirected to // - ) - settings.configure( DEBUG = False, # will be False anyway by DjangoTestRunner. DATABASES = { @@ -85,11 +42,34 @@ 'article', 'theme1', ), - # we define MIDDLEWARE_CLASSES explicitly, the default were changed in django 1.7 - MIDDLEWARE_CLASSES=MIDDLEWARE, - MIDDLEWARE=MIDDLEWARE, # support Django >= 2.0 + TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': (), + 'OPTIONS': { + 'loaders': ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + ), + 'context_processors': ( + 'django.template.context_processors.debug', + 'django.template.context_processors.i18n', + 'django.template.context_processors.media', + 'django.template.context_processors.request', + 'django.template.context_processors.static', + 'django.contrib.auth.context_processors.auth', + ), + }, + }, + ], + MIDDLEWARE = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.locale.LocaleMiddleware', # / will be redirected to // + ), ROOT_URLCONF = 'example.urls', - TEST_RUNNER = 'django.test.simple.DjangoTestSuiteRunner' if django.VERSION < (1, 6) else 'django.test.runner.DiscoverRunner', + TEST_RUNNER = 'django.test.runner.DiscoverRunner', SITE_ID = 4, LANGUAGE_CODE = 'en', @@ -108,8 +88,7 @@ 'default': { 'fallbacks': ['en'], }, - }, - **template_settings + } ) diff --git a/tox.ini b/tox.ini index a0183848..f0c73c9b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,20 +1,14 @@ [tox] envlist= - py27-django{17,18,19,110,111}, - py32-django{17,18}, - py33-django{17,18}, - py34-django{17,18,19,110,111,20}, - py36-django{17,18,19,110,111,20}, + py27-django{111}, + py34-django{111,20}, + py36-django{111,20}, # py33-django-dev, coverage, docs, [testenv] deps = django-polymorphic - django17: Django >= 1.7,<1.8 - django18: Django >= 1.8,<1.9 - django19: Django >= 1.9,<1.10 - django110: Django >= 1.10.1,<1.11 django111: Django >= 1.11,<1.12 django20: Django >= 2.0,<2.1 django-dev: https://github.com/django/django/tarball/master