diff --git a/.travis.yml b/.travis.yml index aac68505..30059654 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,9 @@ language: python python: - 2.7 - - 3.3 - 3.4 - 3.5 env: - - DJANGO=django==1.7.11 DRF=3.3.1 DATABASE_URL=mysql://root@localhost/test - - DJANGO=django==1.7.11 DRF=3.3.1 DATABASE_URL=postgres://postgres@localhost/test - DJANGO=django==1.8.17 DRF=3.3.1 DATABASE_URL=mysql://root@localhost/test - DJANGO=django==1.8.17 DRF=3.3.1 DATABASE_URL=postgres://postgres@localhost/test - DJANGO=django==1.9.12 DRF=3.3.1 DATABASE_URL=mysql://root@localhost/test @@ -27,17 +24,3 @@ script: coverage run --source=hvad --omit='hvad/test*' runtests.py after_success: if [[ $COVERALLS_REPO_TOKEN ]]; then coveralls; fi -matrix: - exclude: - - python: 3.3 - env: DJANGO=django==1.9.12 DRF=3.3.1 DATABASE_URL=mysql://root@localhost/test - - python: 3.3 - env: DJANGO=django==1.9.12 DRF=3.3.1 DATABASE_URL=postgres://postgres@localhost/test - - python: 3.3 - env: DJANGO=django==1.10.5 DRF=3.5.3 DATABASE_URL=mysql://root@localhost/test - - python: 3.3 - env: DJANGO=django==1.10.5 DRF=3.5.3 DATABASE_URL=postgres://postgres@localhost/test - - python: 3.5 - env: DJANGO=django==1.7.11 DRF=3.3.1 DATABASE_URL=mysql://root@localhost/test - - python: 3.5 - env: DJANGO=django==1.7.11 DRF=3.3.1 DATABASE_URL=postgres://postgres@localhost/test diff --git a/README.rst b/README.rst index 2095eb7b..2fbdae48 100644 --- a/README.rst +++ b/README.rst @@ -26,7 +26,7 @@ Features * **Complete** - relationships, custom managers and querysets, proxy models, and abstract models. * **Batteries included** - translation-enabled forms and admin are provided. * **Reliable** - more than 300 test cases and counting. |coverage| |build| -* **Compatible** with Django 1.7 to 1.10, running Python 2.7, 3.3, 3.4 or 3.5. +* **Compatible** with Django 1.8 to 1.10, running Python 2.7, 3.4 or 3.5. Django-hvad also features support for `Django REST framework`_ 3.1 or newer, including translation-aware serializers. @@ -113,9 +113,9 @@ Releases Django-hvad uses the same release pattern as Django. The following versions are thus available: -* Stable branch 1.5, available through `PyPI`_ and git branch ``releases/1.5.x``. * Stable branch 1.6, available through `PyPI`_ and git branch ``releases/1.6.x``. -* Development branch 1.7, available through git branch ``master``. +* Stable branch 1.7, available through `PyPI`_ and git branch ``releases/1.7.x``. +* Development branch 1.8, available through git branch ``master``. Stable branches have minor bugfix releases as needed, with guaranteed compatibility. See the `installation guide`_ for details, or have a look at the `release notes`_. diff --git a/docs/conf.py b/docs/conf.py index 89ed6aa6..4c27f995 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,8 +44,8 @@ project = u'django-hvad' copyright = u'2011-2015, Kristian Øllegaard, Jonas Obrist & contributors' -version = '1.7' -release = '1.7.0' +version = '1.8' +release = '1.8.0' # The name of the Pygments (syntax highlighting) style to use. diff --git a/docs/public/installation.rst b/docs/public/installation.rst index 4670c24b..c38c83d1 100644 --- a/docs/public/installation.rst +++ b/docs/public/installation.rst @@ -7,8 +7,8 @@ Installation Requirements ************ -* `Django`_ 1.7 or higher. -* Python 2.7 or PyPy 1.5 or higher, Python 3.3 or higher. +* `Django`_ 1.8 or higher. +* Python 2.7 or PyPy 1.5 or higher, Python 3.4 or higher. ************ Installation diff --git a/docs/public/release_notes.rst b/docs/public/release_notes.rst index cc562aab..5d962d33 100644 --- a/docs/public/release_notes.rst +++ b/docs/public/release_notes.rst @@ -2,6 +2,34 @@ Release Notes ############# +***************************** +1.8.0 - upcoming release +***************************** + +Python and Django versions supported: + +- Django 1.7 is no longer supported. +- So, as a reminder, supported Django versions for this release are: + 1.8 LTS, 1.9, 1.10.x (for x ≥ 1). + +Compatibility warnings: + +- Deprecated class :class:`~ hvad.manager.FallbackQueryset` has been removed. + Using it along with + :meth:`FallbackQueryset.use_fallbacks() ` + did not work on Django 1.9 and newer and was deprecated on older versions. Using + it without that method made it behave like a regular queryset. So as a summary, + + * Code using `.untranslated().use_fallbacks()` must be replaced + with :ref:`.language().fallbacks() `. + * All other uses of `FallbackQueryset` can be safely replaced with a regular + :class:`~django.db.models.query.QuerySet`. + +- Translated admin no longer shows objects lacking a translation. This was + already the case on Django 1.9 and newer, and this behavior now extends + to all Django versions. + Such objects should not happen anyway, and throw a warning when encountered. + ***************************** 1.7.0 - current release ***************************** diff --git a/hvad/.tx/config b/hvad/.tx/config deleted file mode 100644 index 48154125..00000000 --- a/hvad/.tx/config +++ /dev/null @@ -1,8 +0,0 @@ -[main] -host = https://www.transifex.com - -[django-hvad.core] -file_filter = hvad/locale//LC_MESSAGES/django.po -source_lang = en - - diff --git a/hvad/__init__.py b/hvad/__init__.py index 4fb05e97..560f7b70 100644 --- a/hvad/__init__.py +++ b/hvad/__init__.py @@ -1,2 +1,2 @@ -__version__ = '1.7.0' -VERSION = (1, 7, 0) +__version__ = '1.8.0' +VERSION = (1, 8, 0) diff --git a/hvad/admin.py b/hvad/admin.py index 2a230af8..9f6fcf0f 100644 --- a/hvad/admin.py +++ b/hvad/admin.py @@ -5,7 +5,7 @@ from django.contrib.admin.options import ModelAdmin, csrf_protect_m, InlineModelAdmin from django.contrib.admin.utils import flatten_fieldsets, unquote, get_deleted_objects from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import PermissionDenied, ValidationError +from django.core.exceptions import FieldDoesNotExist, PermissionDenied, ValidationError from django.core.urlresolvers import reverse from django.db import router, transaction from django.forms.models import model_to_dict @@ -13,7 +13,6 @@ from django.http import Http404, HttpResponseRedirect, QueryDict from django.shortcuts import render from django.template import TemplateDoesNotExist -from django.template.context import RequestContext from django.template.loader import select_template from django.utils.encoding import iri_to_uri, force_text from django.utils.functional import curry @@ -227,12 +226,8 @@ def delete_translation(self, request, object_id, language_code): # will also be deleted. protected = False - if django.VERSION >= (1, 8): - deleted_objects, model_count, perms_needed, protected = get_deleted_objects( - [obj], translations_model._meta, request.user, self.admin_site, using) - else: - deleted_objects, perms_needed, protected = get_deleted_objects( - [obj], translations_model._meta, request.user, self.admin_site, using) + deleted_objects, model_count, perms_needed, protected = get_deleted_objects( + [obj], translations_model._meta, request.user, self.admin_site, using) lang = get_language_name(language_code) @@ -333,21 +328,13 @@ def get_object(self, request, object_id, from_field=None): def get_queryset(self, request): language = self._language(request) - if django.VERSION >= (1, 9): - qs = self.model._default_manager.language(language).fallbacks(*FALLBACK_LANGUAGES) - else: - languages = [language,] - for lang in FALLBACK_LANGUAGES: - if not lang in languages: - languages.append(lang) - qs = self.model._default_manager.untranslated().use_fallbacks(*languages) + qs = self.model._default_manager.language(language).fallbacks(*FALLBACK_LANGUAGES) + # TODO: this should be handled by some parameter to the ChangeList. ordering = getattr(self, 'ordering', None) or () if ordering: qs = qs.order_by(*ordering) return qs - if django.VERSION < (1, 8): - queryset = get_queryset def get_change_form_base_template(self): opts = self.model._meta @@ -443,106 +430,6 @@ def response_change(self, request, obj): self.query_language_key, request.GET[self.query_language_key]) return redirect - """ -# Should be added - @csrf_protect_m - @transaction.atomic - def delete_translation(self, request, object_id, language_code): - "The 'delete translation' admin view for this model." - opts = self.model._meta - app_label = opts.app_label - translations_model = opts.translations_model - - try: - obj = translations_model.objects.select_related('maser').get( - master__pk=unquote(object_id), - language_code=language_code) - except translations_model.DoesNotExist: - raise Http404 - - if not self.has_delete_permission(request, obj): - raise PermissionDenied - - if len(obj.master.get_available_languages()) <= 1: - return self.deletion_not_allowed(request, obj, language_code) - - using = router.db_for_write(translations_model) - - # Populate deleted_objects, a data structure of all related objects that - # will also be deleted. - - protected = False - if NEW_GET_DELETE_OBJECTS: - (deleted_objects, perms_needed, protected) = get_deleted_objects( - [obj], translations_model._meta, request.user, self.admin_site, using) - else: # pragma: no cover - (deleted_objects, perms_needed) = get_deleted_objects( - [obj], translations_model._meta, request.user, self.admin_site) - - - lang = get_language_name(language_code) - - - if request.POST: # The user has already confirmed the deletion. - if perms_needed: - raise PermissionDenied - obj_display = '%s translation of %s' % (lang, force_text(obj.master)) - self.log_deletion(request, obj, obj_display) - self.delete_model_translation(request, obj) - - self.message_user(request, - _('The %(name)s "%(obj)s" was deleted successfully.') % { - 'name': force_text(opts.verbose_name), - 'obj': force_text(obj_display) - } - ) - - if not self.has_change_permission(request, None): - return HttpResponseRedirect(reverse('admin:index')) - return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % (opts.app_label, opts.model_name))) - - object_name = '%s Translation' % force_text(opts.verbose_name) - - if perms_needed or protected: - title = _("Cannot delete %(name)s") % {"name": object_name} - else: - title = _("Are you sure?") - - context = { - "title": title, - "object_name": object_name, - "object": obj, - "deleted_objects": deleted_objects, - "perms_lacking": perms_needed, - "protected": protected, - "opts": opts, - "root_path": self.admin_site.root_path, - "app_label": app_label, - } - - return render(request, self.delete_confirmation_template or [ - "admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()), - "admin/%s/delete_confirmation.html" % app_label, - "admin/delete_confirmation.html" - ], context) - - def deletion_not_allowed(self, request, obj, language_code): - opts = self.model._meta - return render( - request, self.deletion_not_allowed_template, - { - 'object': obj.master, - 'language_code': language_code, - 'opts': opts, - 'app_label': opts.app_label, - 'language_name': get_language_name(language_code), - 'object_name': force_text(opts.verbose_name), - }, - ) - - def delete_model_translation(self, request, obj): - obj.delete() - """ def get_queryset(self, request): qs = self.model._default_manager.all()#.language(language) # TODO: this should be handled by some parameter to the ChangeList. @@ -550,8 +437,6 @@ def get_queryset(self, request): if ordering: qs = qs.order_by(*ordering) return qs - if django.VERSION < (1, 8): - queryset = get_queryset class TranslatableStackedInline(TranslatableInlineModelAdmin): template = 'admin/hvad/edit_inline/stacked.html' diff --git a/hvad/compat.py b/hvad/compat.py index 73ea7402..d9844ebf 100644 --- a/hvad/compat.py +++ b/hvad/compat.py @@ -1,7 +1,7 @@ import sys PYTHON_MAJOR, PYTHON_MINOR = sys.version_info[0:2] -__all__ = ('with_metaclass', 'MethodType', 'StringIO', 'string_types', +__all__ = ('with_metaclass', 'MethodType', 'string_types', 'urlencode', 'urlparse', 'unquote') #============================================================================= @@ -27,14 +27,7 @@ def MethodType(function, instance): from urlparse import urlparse from urllib import unquote - if PYTHON_MINOR >= 6: - from io import StringIO - else: #pragma: no cover - from StringIO import StringIO - else: from types import MethodType string_types = (str,) from urllib.parse import urlencode, urlparse, unquote - from io import StringIO - diff --git a/hvad/contrib/restframework/__init__.py b/hvad/contrib/restframework/__init__.py index 03a3c3d1..b3c05f82 100644 --- a/hvad/contrib/restframework/__init__.py +++ b/hvad/contrib/restframework/__init__.py @@ -2,3 +2,9 @@ TranslationsMixin, TranslatableModelSerializer, HyperlinkedTranslatableModelSerializer, NestedTranslationSerializer, ) +__all__ = ( + 'TranslationsMixin', + 'TranslatableModelSerializer', + 'HyperlinkedTranslatableModelSerializer', + 'NestedTranslationSerializer', +) diff --git a/hvad/contrib/restframework/serializers.py b/hvad/contrib/restframework/serializers.py index a74557c7..ed2dea57 100644 --- a/hvad/contrib/restframework/serializers.py +++ b/hvad/contrib/restframework/serializers.py @@ -1,8 +1,8 @@ -import django from django.db.models.fields import FieldDoesNotExist from django.utils.translation import get_language, ugettext_lazy as _l from rest_framework import serializers from rest_framework.exceptions import ValidationError +from rest_framework.fields import SkipField from hvad.exceptions import WrongManager from hvad.utils import get_cached_translation, set_cached_translation, load_translation from hvad.contrib.restframework.utils import TranslationListSerializer @@ -133,15 +133,11 @@ def update(self, instance, data): return instance def update_translation(self, instance, data): - if django.VERSION >= (1, 8): - fields = set(field.name - for field in self.Meta.model._meta.translations_model._meta.get_fields() - if not field.is_relation or # regular fields are ok - field.one_to_one or # one to one is ok - field.many_to_one and field.related_model) # many_to_one only if not generic - else: - fields = set(name - for name in self.Meta.model._meta.translations_model._meta.get_all_field_names()) + fields = set(field.name + for field in self.Meta.model._meta.translations_model._meta.get_fields() + if not field.is_relation or # regular fields are ok + field.one_to_one or # one to one is ok + field.many_to_one and field.related_model) # many_to_one only if not generic fields.intersection_update(data) vetoed = fields.intersection('id', 'master', 'master_id', 'language_code') if vetoed: diff --git a/hvad/descriptors.py b/hvad/descriptors.py index deaf7ea3..47779053 100644 --- a/hvad/descriptors.py +++ b/hvad/descriptors.py @@ -1,9 +1,8 @@ -import django -from django.db.models.fields import FieldDoesNotExist from django.utils.translation import get_language from hvad.utils import get_translation, set_cached_translation, get_cached_translation from django.apps import registry + class BaseDescriptor(object): """ Base descriptor class with a helper to get the translations instance. @@ -39,10 +38,7 @@ def __get__(self, instance, instance_type=None): if not instance: if not registry.apps.ready: #pragma: no cover raise AttributeError('Attribute not available until registry is ready.') - if django.VERSION >= (1, 8): - return self.opts.translations_model._meta.get_field(self.name).default - else: - return self.opts.translations_model._meta.get_field_by_name(self.name)[0].default + return self.opts.translations_model._meta.get_field(self.name).default return getattr(self.translation(instance), self.name) def __set__(self, instance, value): @@ -62,8 +58,7 @@ def __init__(self, opts): super(LanguageCodeAttribute, self).__init__(opts, 'language_code') def __set__(self, instance, value): - raise AttributeError("The 'language_code' attribute cannot be " - "changed directly! Use the translate() method instead.") + raise AttributeError("The 'language_code' attribute cannot be changed directly.") def __delete__(self, instance): - raise AttributeError("The 'language_code' attribute cannot be deleted!") + raise AttributeError("The 'language_code' attribute cannot be deleted.") diff --git a/hvad/forms.py b/hvad/forms.py index 7fea1516..e749786b 100644 --- a/hvad/forms.py +++ b/hvad/forms.py @@ -1,9 +1,8 @@ -import django from django.conf import settings from django.core.exceptions import FieldError, ValidationError from django.forms.fields import CharField from django.forms.formsets import formset_factory -from django.forms.models import (ModelForm, BaseModelForm, ModelFormMetaclass, +from django.forms.models import (BaseModelForm, ModelFormMetaclass, fields_for_model, model_to_dict, construct_instance, BaseInlineFormSet, BaseModelFormSet, modelform_factory, inlineformset_factory, ALL_FIELDS) from django.forms.utils import ErrorList @@ -12,11 +11,7 @@ from hvad.compat import with_metaclass from hvad.models import TranslatableModel, BaseTranslationModel from hvad.utils import (set_cached_translation, get_cached_translation, load_translation) -try: - from collections import OrderedDict -except ImportError: #pragma: no cover (python < 2.7) - from django.utils.datastructures import SortedDict as OrderedDict -import warnings +from collections import OrderedDict veto_fields = {'id', 'master', 'master_id', 'language_code'} @@ -201,7 +196,7 @@ def save(self, commit=True): class TranslatableModelForm(with_metaclass(TranslatableModelFormMetaclass, - BaseTranslatableModelForm)): + BaseTranslatableModelForm)): pass #============================================================================= diff --git a/hvad/manager.py b/hvad/manager.py index f562ffa7..217b75e4 100644 --- a/hvad/manager.py +++ b/hvad/manager.py @@ -1,27 +1,22 @@ -from collections import defaultdict import django from django.conf import settings from django.core.exceptions import FieldError from django.db import connections, models, transaction, IntegrityError if django.VERSION >= (1, 9): from django.db.models.query import QuerySet -elif django.VERSION >= (1, 8): - from django.db.models.query import QuerySet, ValuesQuerySet else: - from django.db.models.query import QuerySet, ValuesQuerySet, DateQuerySet, DateTimeQuerySet -if django.VERSION >= (1, 8): - from django.db.models.sql.datastructures import Join, LOUTER -from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE as CHUNK_SIZE + from django.db.models.query import QuerySet, ValuesQuerySet +from django.db.models.sql.datastructures import Join, LOUTER from django.db.models import F, Q +from django.utils.functional import cached_property from django.utils.translation import get_language from hvad.compat import string_types -from hvad.query import (query_terms, q_children, expression_nodes, where_node_children, +from hvad.query import (query_terms, q_children, expression_nodes, add_alias_constraints) -from hvad.utils import combine, minimumDjangoVersion, settings_updater +from hvad.utils import combine, settings_updater from copy import deepcopy import logging import sys -import warnings #=============================================================================== @@ -31,9 +26,8 @@ # Global settings, wrapped so they react to SettingsOverride @settings_updater def update_settings(*args, **kwargs): - global FALLBACK_LANGUAGES, LEGACY_FALLBACKS + global FALLBACK_LANGUAGES FALLBACK_LANGUAGES = tuple(code for code, name in settings.LANGUAGES) - LEGACY_FALLBACKS = bool(getattr(settings, 'HVAD_LEGACY_FALLBACKS', False)) #=============================================================================== @@ -44,16 +38,13 @@ class FieldTranslator(object): """ def __init__(self, manager): self._manager = manager - if django.VERSION >= (1, 8): - fields = set() - for field in manager.shared_model._meta.get_fields(): - fields.add(field.name) - if hasattr(field, 'attname'): - fields.add(field.attname) - fields.add('pk') - self._shared_fields = tuple(fields) - else: - self._shared_fields = tuple(manager.shared_model._meta.get_all_field_names()) + ('pk',) + fields = set() + for field in manager.shared_model._meta.get_fields(): + fields.add(field.name) + if hasattr(field, 'attname'): + fields.add(field.attname) + fields.add('pk') + self._shared_fields = tuple(fields) self._cache = dict() super(FieldTranslator, self).__init__() @@ -167,7 +158,6 @@ def get_extra_restriction(self, where_class, alias, related_alias): alias, related_alias) ) - @minimumDjangoVersion(1, 8) def get_joining_columns(self): return ((self._master, self._master), ) @@ -188,9 +178,6 @@ class TranslationQueryset(QuerySet): override_classes = {} if django.VERSION < (1, 9): override_classes[ValuesQuerySet] = ValuesMixin - if django.VERSION < (1, 8): - override_classes[DateQuerySet] = SkipMasterSelectMixin - override_classes[DateTimeQuerySet] = SkipMasterSelectMixin _skip_master_select = False def __init__(self, *args, **kwargs): @@ -252,15 +239,12 @@ def field_translator(self): @property def shared_local_field_names(self): if self._local_field_names is None: - if django.VERSION >= (1, 8): - fields = set() - for field in self.shared_model._meta.get_fields(): - fields.add(field.name) - if hasattr(field, 'attname'): - fields.add(field.attname) - self._local_field_names = tuple(fields) - else: - self._local_field_names = self.shared_model._meta.get_all_field_names() + fields = set() + for field in self.shared_model._meta.get_fields(): + fields.add(field.name) + if hasattr(field, 'attname'): + fields.add(field.attname) + self._local_field_names = tuple(fields) return self._local_field_names def _translate_args_kwargs(self, *args, **kwargs): @@ -287,9 +271,7 @@ def _translate_expression(self, expr): return expr def _translate_fieldnames(self, fieldnames): - annotations = (self.query.annotations if django.VERSION >= (1, 8) - else self.query.aggregates) - return [name if name in annotations + return [name if name in self.query.annotations else self.field_translator(name) for name in fieldnames] @@ -414,24 +396,15 @@ def _add_language_filter(self): 'fallbacks() is not supported') languages = tuple(get_language() if lang is None else lang for lang in (self._language_code,) + self._language_fallbacks) - - if django.VERSION >= (1, 8): - masteratt = self.model._meta.get_field('master').attname - alias = self.query.join(Join( - self.model._meta.db_table, - self.query.get_initial_alias(), - None, - LOUTER, - BetterTranslationsField(languages, master=masteratt), - True - )) - else: - masteratt = self.model._meta.get_field('master').attname - nullable = {'nullable': True} - alias = self.query.join((self.query.get_initial_alias(), self.model._meta.db_table, - ((masteratt, masteratt),)), - join_field=BetterTranslationsField(languages, master=masteratt), - **nullable) + masteratt = self.model._meta.get_field('master').attname + alias = self.query.join(Join( + self.model._meta.db_table, + self.query.get_initial_alias(), + None, + LOUTER, + BetterTranslationsField(languages, master=masteratt), + True + )) add_alias_constraints(self, (self.model, alias), id__isnull=True) self.query.add_filter(('%s__isnull' % masteratt, False)) @@ -730,16 +703,6 @@ def values_list(self, *fields, **kwargs): fields = self._translate_fieldnames(fields) return super(TranslationQueryset, self).values_list(*fields, **kwargs) - def dates(self, field_name, kind=None, order='ASC'): - if django.VERSION < (1, 8): - field_name = self.field_translator(field_name) - return super(TranslationQueryset, self).dates(field_name, kind=kind, order=order) - - def datetimes(self, field, *args, **kwargs): - if django.VERSION < (1, 8): - field = self.field_translator(field) - return super(TranslationQueryset, self).datetimes(field, *args, **kwargs) - def select_related(self, *fields): if not fields: raise NotImplementedError('To use select_related on a translated model, ' @@ -782,9 +745,6 @@ def annotate(self, *args, **kwargs): return qs def order_by(self, *field_names): - """ - Returns a new QuerySet instance with the ordering changed. - """ fieldnames = self._translate_fieldnames(field_names) return super(TranslationQueryset, self).order_by(*fieldnames) @@ -803,216 +763,10 @@ def only(self, *field_names): fieldnames += ('master__%s' % self.shared_model._meta.pk.name, 'master_id', 'language_code') return super(TranslationQueryset, self).only(*fieldnames) -#=============================================================================== -# Fallbacks -#=============================================================================== - -class _SharedFallbackQueryset(QuerySet): - translation_fallbacks = None - - def use_fallbacks(self, *fallbacks): - if django.VERSION >= (1, 9): - raise AssertionError( - 'TranslationManager.untranslated().use_fallbacks() is not supported on Django 1.9, ' - 'and is deprecated on previous versions as well. Please use the fallbacks() method ' - 'on queryset instead, eg: MyModel.objects.language().fallbacks()') - warnings.warn('TranslationManager.untranslated().use_fallbacks() is deprecated. ' - 'Please use the fallbacks() method on queryset instead, eg: ' - 'MyModel.objects.language().fallbacks()', DeprecationWarning, stacklevel=2) - self.translation_fallbacks = fallbacks or (None,)+FALLBACK_LANGUAGES - return self - - def _clone(self, klass=None, setup=False, **kwargs): - kwargs.update({ - 'translation_fallbacks': self.translation_fallbacks, - }) - if django.VERSION < (1, 9): - kwargs.update({'klass': klass, 'setup': setup}) - return super(_SharedFallbackQueryset, self)._clone(**kwargs) - - def aggregate(self, *args, **kwargs): - raise NotImplementedError() - - def annotate(self, *args, **kwargs): - raise NotImplementedError() - - def defer(self, *args, **kwargs): - raise NotImplementedError() - - def only(self, *args, **kwargs): - raise NotImplementedError() - - -class LegacyFallbackQueryset(_SharedFallbackQueryset): #pragma: no cover - ''' - Queryset that tries to load a translated version using fallbacks on a per - instance basis. - BEWARE: creates a lot of queries! - ''' - def _get_real_instances(self, base_results): - """ - The logic for this method was taken from django-polymorphic by Bert - Constantin (https://github.com/bconstantin/django_polymorphic) and was - slightly altered to fit the needs of django-hvad. - """ - # get the primary keys of the shared model results - base_ids = [obj.pk for obj in base_results] - fallbacks = [get_language() if lang is None else lang - for lang in self.translation_fallbacks] - # get all translations for the fallbacks chosen for those shared models, - # note that this query is *BIG* and might return a lot of data, but it's - # arguably faster than running one query for each result or even worse - # one query per result per language until we find something - translations_manager = self.model._meta.translations_model.objects - baseqs = translations_manager.select_related('master') - translations = baseqs.filter(language_code__in=fallbacks, - master__pk__in=base_ids) - fallback_objects = defaultdict(dict) - # turn the results into a dict of dicts with shared model primary key as - # keys for the first dict and language codes for the second dict - for obj in translations: - fallback_objects[obj.master.pk][obj.language_code] = obj - # iterate over the share dmodel results - for instance in base_results: - translation = None - # find the translation - for fallback in fallbacks: - translation = fallback_objects[instance.pk].get(fallback, None) - if translation is not None: - break - # if we found a translation, yield the combined result - if translation: - yield combine(translation, self.model) - else: - # otherwise yield the shared instance only - _logger.error("no translation for %s.%s (pk=%s)" % - (instance._meta.app_label, - instance.__class__.__name__, - str(instance.pk))) - yield instance - - def iterator(self): - """ - The logic for this method was taken from django-polymorphic by Bert - Constantin (https://github.com/bconstantin/django_polymorphic) and was - slightly altered to fit the needs of django-hvad. - """ - base_iter = super(LegacyFallbackQueryset, self).iterator() - - # only do special stuff when we actually want fallbacks - if self.translation_fallbacks: - while True: - base_result_objects = [] - reached_end = False - - # get the next "chunk" of results - for i in range(CHUNK_SIZE): - try: - instance = next(base_iter) - base_result_objects.append(instance) - except StopIteration: - reached_end = True - break - - # "combine" the results with their fallbacks - real_results = self._get_real_instances(base_result_objects) - - # yield em! - for instance in real_results: - yield instance - - # get out of the while loop if we're at the end, since this is - # an iterator, we need to raise StopIteration, not "return". - if reached_end: - raise StopIteration - else: - # just iterate over it - for instance in base_iter: - yield instance - -class SelfJoinFallbackQueryset(_SharedFallbackQueryset): - def iterator(self): - # only do special stuff when we actually want fallbacks - if self.translation_fallbacks: - fallbacks = [get_language() if lang is None else lang - for lang in self.translation_fallbacks] - tmodel = self.model._meta.translations_model - taccessor = self.model._meta.translations_accessor - taccessorcache = ( - getattr(self.model, taccessor).rel.get_cache_name() if django.VERSION >= (1, 9) else - getattr(self.model, taccessor).related.get_cache_name() - ) - tcache = self.model._meta.translations_cache - masteratt = tmodel._meta.get_field('master').attname - field = BetterTranslationsField(fallbacks, master=masteratt) - - qs = self._clone() - - qs.query.add_select_related((taccessor,)) - # This join will be reused by the select_related. We must provide it - # anyway because the order matters and add_select_related does not - # populate joins right away. - if django.VERSION >= (1, 8): - alias1 = qs.query.join(Join( - tmodel._meta.db_table, - qs.query.get_initial_alias(), - None, - LOUTER, - getattr(qs.model, taccessor).field.rel if django.VERSION >= (1, 9) else - getattr(qs.model, taccessor).related.field.rel, - True - )) - alias2 = qs.query.join(Join( - tmodel._meta.db_table, - alias1, - None, - LOUTER, - field, - True - )) - else: - nullable = {'nullable': True} - alias1 = qs.query.join((qs.query.get_initial_alias(), tmodel._meta.db_table, - ((qs.model._meta.pk.attname, masteratt),)), - join_field=getattr(qs.model, taccessor).related.field.rel, - **nullable) - alias2 = qs.query.join((tmodel._meta.db_table, tmodel._meta.db_table, - ((masteratt, masteratt),)), - join_field=field, **nullable) - - add_alias_constraints(qs, (tmodel, alias2), id__isnull=True) - - # We must force the _unique field so get_cached_row populates the cache - # Unfortunately, this means we must load everything in one go - related_field = (getattr(qs.model, taccessor).field if django.VERSION >= (1, 9) else - getattr(qs.model, taccessor).related.field) - with ForcedUniqueFields([related_field]): - objects = [] - for instance in super(SelfJoinFallbackQueryset, qs).iterator(): - try: - translation = getattr(instance, taccessorcache) - except AttributeError: #pragma: no cover - _logger.error("no translation for %s.%s (pk=%s)", - instance._meta.app_label, - instance.__class__.__name__, - str(instance.pk)) - else: - setattr(instance, tcache, translation) - delattr(instance, taccessorcache) - objects.append(instance) - return iter(objects) - else: - return super(SelfJoinFallbackQueryset, self).iterator() - - -FallbackQueryset = LegacyFallbackQueryset if LEGACY_FALLBACKS else SelfJoinFallbackQueryset - - #=============================================================================== # TranslationManager #=============================================================================== - class TranslationManager(models.Manager): """ Manager class for models with translated fields @@ -1024,7 +778,7 @@ class TranslationManager(models.Manager): silence_use_for_related_fields_deprecation = True # Django 1.10 queryset_class = TranslationQueryset - fallback_class = FallbackQueryset + fallback_class = QuerySet default_class = QuerySet def __init__(self, *args, **kwargs): @@ -1057,7 +811,7 @@ def get_queryset(self): # Internals #=========================================================================== - @property + @cached_property def translations_model(self): return self.model._meta.translations_model @@ -1066,7 +820,6 @@ def translations_model(self): # TranslationAware #=============================================================================== - class TranslationAwareQueryset(QuerySet): def __init__(self, *args, **kwargs): super(TranslationAwareQueryset, self).__init__(*args, **kwargs) diff --git a/hvad/models.py b/hvad/models.py index f6f3be98..f2d9bb8a 100644 --- a/hvad/models.py +++ b/hvad/models.py @@ -6,7 +6,7 @@ from django.db.models.base import ModelBase from django.db.models.fields import FieldDoesNotExist from django.db.models.manager import Manager -from django.db.models.signals import post_save, class_prepared +from django.db.models.signals import class_prepared from django.utils.translation import get_language from hvad.descriptors import LanguageCodeAttribute, TranslatedAttribute from hvad.manager import TranslationManager, TranslationsModelManager @@ -15,7 +15,6 @@ from hvad.compat import MethodType from itertools import chain import sys -import warnings #=============================================================================== @@ -37,13 +36,8 @@ def __init__(self, meta=None, base_class=None, **fields): self.fields = fields @staticmethod - def _split_together(constraints, fields, meta, name): + def _split_together(constraints, fields, name): sconst, tconst = [], [] - if name in meta: - # remove in 1.9 - raise ValueError('Passing \'%s\' to TranslatedFields is no longer valid. ' - 'Please use Meta.%s instead.' % (name, name), DeprecationWarning) - for constraint in constraints: if all(item in fields for item in constraint): tconst.append(constraint) @@ -57,7 +51,7 @@ def _split_together(constraints, fields, meta, name): def contribute_to_class(self, model, name): if model._meta.order_with_respect_to in self.fields: - raise ValueError( + raise ImproperlyConfigured( 'Using a translated fields in %s.Meta.order_with_respect_to is ambiguous ' 'and hvad does not support it.' % model._meta.model_name @@ -158,7 +152,7 @@ def _build_meta_class(self, model, tfields): # Split fields in Meta.unique_together sconst, tconst = self._split_together( - model._meta.unique_together, tfields, meta, 'unique_together' + model._meta.unique_together, tfields, 'unique_together' ) model._meta.unique_together = tuple(sconst) model._meta.original_attrs['unique_together'] = tuple(sconst) @@ -168,7 +162,7 @@ def _build_meta_class(self, model, tfields): # Split fields in Meta.index_together sconst, tconst = self._split_together( - model._meta.index_together, tfields, meta, 'index_together' + model._meta.index_together, tfields, 'index_together' ) model._meta.index_together = tuple(sconst) model._meta.original_attrs['index_together'] = tuple(sconst) @@ -199,10 +193,6 @@ def contribute_translations(self, model, translations_model, related_name): #=============================================================================== class BaseTranslationModel(models.Model): - """ - Needed for detection of translation models. Due to the way dynamic classes - are created, we cannot put the 'language_code' field on here. - """ def _get_unique_checks(self, exclude=None): # Due to the way translations are handled, checking for unicity of # the ('language_code', 'master') constraint is useless. We filter it out @@ -224,7 +214,6 @@ class TranslatableModel(models.Model): """ Base model for all models supporting translated fields (via TranslatedFields). """ - # change the default manager to the translation manager objects = TranslationManager() _plain_manager = models.Manager() @@ -237,12 +226,17 @@ def __init__(self, *args, **kwargs): # Split arguments into shared/translatd veto_names = ('pk', 'master', 'master_id', self._meta.translations_model._meta.pk.name) skwargs, tkwargs = {}, {} + translations_opts = self._meta.translations_model._meta for key, value in kwargs.items(): - if key in self._translated_field_names and not key in veto_names: - tkwargs[key] = value - else: + if key in veto_names: skwargs[key] = value - + else: + try: + translations_opts.get_field(key) + except FieldDoesNotExist: + skwargs[key] = value + else: + tkwargs[key] = value super(TranslatableModel, self).__init__(*args, **skwargs) # Create a translation if there are translated fields @@ -251,7 +245,8 @@ def __init__(self, *args, **kwargs): set_cached_translation(self, self._meta.translations_model(**tkwargs)) def save(self, *args, **skwargs): - translation_model = self._meta.translations_model + veto_names = ('pk', 'master', 'master_id', self._meta.translations_model._meta.pk.name) + translations_opts = self._meta.translations_model._meta translation = get_cached_translation(self) tkwargs = skwargs.copy() @@ -260,10 +255,15 @@ def save(self, *args, **skwargs): if update_fields is not None: supdate, tupdate = [], [] for name in update_fields: - if name in self._translated_field_names and not name in ('id', 'master_id', 'master'): - tupdate.append(name) - else: + if name in veto_names: supdate.append(name) + else: + try: + translations_opts.get_field(name) + except FieldDoesNotExist: + supdate.append(name) + else: + tupdate.append(name) skwargs['update_fields'], tkwargs['update_fields'] = supdate, tupdate # save share and translated model in a single transaction @@ -350,13 +350,14 @@ def validate_unique(self, exclude=None): translation.validate_unique(exclude=exclude) #=========================================================================== - # Checks - require Django 1.7 or newer + # Checks #=========================================================================== @classmethod def check(cls, **kwargs): errors = super(TranslatableModel, cls).check(**kwargs) errors.extend(cls._check_shared_translated_clash()) + errors.extend(cls._check_default_manager_translation_aware()) return errors @classmethod @@ -374,6 +375,18 @@ def _check_shared_translated_clash(cls): hint=None, obj=cls, id='hvad.models.E01') for field in tfields.intersection(fields)] + @classmethod + def _check_default_manager_translation_aware(cls): + errors = [] + if not isinstance(cls._default_manager, TranslationManager): + errors.append(checks.Error( + "The default manager on a TranslatableModel must be a " + "TranslationManager instance or an instance of a subclass of " + "TranslationManager, the default manager of %r is not." % cls, + hint=None, obj=cls, id='hvad.models.E02' + )) + return errors + @classmethod def _check_local_fields(cls, fields, option): """ Remove fields we recognize as translated fields from tests """ @@ -412,45 +425,12 @@ def _check_ordering(cls): hint=None, obj=cls, id='models.E015') for field in fields - valid_fields - valid_tfields] - #=========================================================================== - # Internals - #=========================================================================== - - @property - def _translated_field_names(self): - if getattr(self, '_translated_field_names_cache', None) is None: - opts = self._meta.translations_model._meta - result = set() - - if django.VERSION >= (1, 8): - for field in opts.get_fields(): - result.add(field.name) - if hasattr(field, 'attname'): - result.add(field.attname) - else: - result = set(opts.get_all_field_names()) - for name in tuple(result): - try: - attname = opts.get_field(name).get_attname() - except (FieldDoesNotExist, AttributeError): - continue - if attname: - result.add(attname) - - self._translated_field_names_cache = tuple(result) - return self._translated_field_names_cache - #============================================================================= def prepare_translatable_model(sender, **kwargs): model = sender if not issubclass(model, TranslatableModel) or model._meta.abstract: return - if not isinstance(model._default_manager, TranslationManager): - raise ImproperlyConfigured( - "The default manager on a TranslatableModel must be a " - "TranslationManager instance or an instance of a subclass of " - "TranslationManager, the default manager of %r is not." % model) if model._meta.proxy: model._meta.translations_accessor = model._meta.concrete_model._meta.translations_accessor @@ -458,10 +438,8 @@ def prepare_translatable_model(sender, **kwargs): model._meta.translations_cache = model._meta.concrete_model._meta.translations_cache if not hasattr(model._meta, 'translations_model'): - raise ImproperlyConfigured( - "No TranslatedFields found on %r, subclasses of " - "TranslatableModel must define TranslatedFields." % model - ) + raise ImproperlyConfigured("No TranslatedFields found on %r, subclasses of " + "TranslatableModel must define TranslatedFields." % model) #### Now we have to work #### diff --git a/hvad/query.py b/hvad/query.py index 5229b307..40a212da 100644 --- a/hvad/query.py +++ b/hvad/query.py @@ -1,12 +1,6 @@ -import django from django.db.models import Q, FieldDoesNotExist -if django.VERSION >= (1, 8): - from django.db.models.expressions import Expression, Col -else: - from django.db.models.expressions import ExpressionNode as Expression +from django.db.models.expressions import Expression, Col from django.db.models.sql.where import WhereNode, AND -if django.VERSION < (1, 9): - from django.db.models.sql.where import Constraint from collections import namedtuple #=============================================================================== @@ -26,32 +20,20 @@ def query_terms(model, path): bit = model._meta.pk.name try: - if django.VERSION >= (1, 8): - try: # is field on the shared model? - field = model._meta.get_field.real(bit) - translated = False - except FieldDoesNotExist: # nope, get field from translations model - field = model._meta.translations_model._meta.get_field(bit) - translated = True - except AttributeError: # current model is a standard model - field = model._meta.get_field(bit) - translated = False - direct = ( - not field.auto_created or - getattr(field, 'db_column', None) or - getattr(field, 'attname', None) - ) - else: - # older versions do not retrieve reverse/m2m with get_field, we must use the obsolete api - try: - field, _, direct, _ = model._meta.get_field_by_name.real(bit) - translated = False - except FieldDoesNotExist: - field, _, direct, _ = model._meta.translations_model._meta.get_field_by_name(bit) - translated = True - except AttributeError: - field, _, direct, _ = model._meta.get_field_by_name(bit) - translated = False + try: # is field on the shared model? + field = model._meta.get_field.real(bit) + translated = False + except FieldDoesNotExist: # nope, get field from translations model + field = model._meta.translations_model._meta.get_field(bit) + translated = True + except AttributeError: # current model is a standard model + field = model._meta.get_field(bit) + translated = False + direct = ( + not field.auto_created or + getattr(field, 'db_column', None) or + getattr(field, 'attname', None) + ) except FieldDoesNotExist: break @@ -63,8 +45,7 @@ def query_terms(model, path): else: # field is a regular field target = None else: # field is a m2m or reverse fk, follow it - target = (field.related_model._meta.concrete_model if django.VERSION >= (1, 8) else - field.model._meta.concrete_model) + target = field.related_model._meta.concrete_model yield QueryTerm( depth=depth, @@ -113,32 +94,17 @@ def q_children(q): else: yield child, q.children, index -if django.VERSION >= (1, 8): - def expression_nodes(expression): - ''' Recursively visit an expression object, yielding each node in turn. - - expression: the expression object to visit - ''' - todo = [expression] - while todo: - expression = todo.pop() - if expression is not None: - yield expression - if isinstance(expression, Expression): - todo.extend(expression.get_source_expressions()) - -else: - def expression_nodes(expression): - ''' Recursively visit an expression object, yielding each node in turn. - - expression: the expression object to visit - ''' - todo = [expression] - while todo: - expression = todo.pop() - if expression is not None: - yield expression - if isinstance(expression, Expression): - todo.extend(expression.children or ()) - +def expression_nodes(expression): + ''' Recursively visit an expression object, yielding each node in turn. + - expression: the expression object to visit + ''' + todo = [expression] + while todo: + expression = todo.pop() + if expression is not None: + yield expression + if isinstance(expression, Expression): + todo.extend(expression.get_source_expressions()) def where_node_children(node): ''' Recursively visit all children of a where node, yielding each field in turn. @@ -160,24 +126,14 @@ def where_node_children(node): #=============================================================================== # Query manipulations -if django.VERSION >= (1, 8): - def add_alias_constraints(queryset, alias, **kwargs): - model, alias = alias - clause = queryset.query.where_class() - for lookup, value in kwargs.items(): - field_name, lookup = lookup.split('__') - clause.add(queryset.query.build_lookup( - [lookup], - Col(alias, model._meta.get_field(field_name)), - value - ), AND) - queryset.query.where.add(clause, AND) -else: - def add_alias_constraints(queryset, alias, **kwargs): - model, alias = alias - clause = queryset.query.where_class() - for lookup, value in kwargs.items(): - field_name, lookup = lookup.split('__') - field = model._meta.get_field(field_name) - clause.add((Constraint(alias, field.column, field), lookup, value), AND) - queryset.query.where.add(clause, AND) +def add_alias_constraints(queryset, alias, **kwargs): + model, alias = alias + clause = queryset.query.where_class() + for lookup, value in kwargs.items(): + field_name, lookup = lookup.split('__') + clause.add(queryset.query.build_lookup( + [lookup], + Col(alias, model._meta.get_field(field_name)), + value + ), AND) + queryset.query.where.add(clause, AND) diff --git a/hvad/test_utils/cli.py b/hvad/test_utils/cli.py index acfda763..b96069cb 100644 --- a/hvad/test_utils/cli.py +++ b/hvad/test_utils/cli.py @@ -73,20 +73,6 @@ def configure(**extra): 'django.contrib.auth.hashers.MD5PasswordHasher', ) ) - if django.VERSION < (1, 8): - defaults.update(dict( - TEMPLATE_DEBUG = True, - TEMPLATE_CONTEXT_PROCESSORS = ( # Remove when dropping support for Django 1.7 - 'django.contrib.auth.context_processors.auth', - ), - TEMPLATE_LOADERS = ( # Remove when dropping support for Django 1.7 - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', - ), - TEMPLATE_DIRS = [ # Remove when dropping support for Django 1.7 - os.path.abspath(os.path.join(os.path.dirname(__file__), 'project', 'templates')) - ], - )) defaults.update(extra) settings.configure(**defaults) diff --git a/hvad/test_utils/runners.py b/hvad/test_utils/runners.py index e2571381..71d08030 100644 --- a/hvad/test_utils/runners.py +++ b/hvad/test_utils/runners.py @@ -1,4 +1,3 @@ -import django from django.conf import settings from django.test.runner import DiscoverRunner as DjangoRunner import operator diff --git a/hvad/tests/abstract.py b/hvad/tests/abstract.py index 4eca5a65..b6e4c089 100644 --- a/hvad/tests/abstract.py +++ b/hvad/tests/abstract.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from hvad.test_utils.testcase import HvadTestCase from hvad.test_utils.project.app.models import ConcreteAB, ConcreteABProxy from hvad.test_utils.fixtures import ConcreteABFixture diff --git a/hvad/tests/admin.py b/hvad/tests/admin.py index 37dc0679..edebaa49 100644 --- a/hvad/tests/admin.py +++ b/hvad/tests/admin.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- import django -from django.conf import settings from django.contrib import admin from django.contrib.contenttypes.models import ContentType from django.core.urlresolvers import reverse @@ -12,7 +11,7 @@ from hvad.forms import TranslatableModelForm from hvad.test_utils.fixtures import NormalFixture, UsersFixture from hvad.test_utils.data import NORMAL -from hvad.test_utils.testcase import HvadTestCase, minimumDjangoVersion +from hvad.test_utils.testcase import HvadTestCase from hvad.test_utils.project.app.models import Normal, Unique, SimpleRelated, AutoPopulated @@ -179,13 +178,7 @@ def test_get_object(self): # Check what happens if there is no translations at all obj = Normal.objects.untranslated().create(shared_field="shared") with translation.override('en'): - if django.VERSION >= (1, 9): - self.assertIs(myadmin.get_object(get_request, obj.pk), None) - else: - self.assertEqual(myadmin.get_object(get_request, obj.pk).pk, obj.pk) - self.assertEqual(myadmin.get_object(get_request, obj.pk).shared_field, obj.shared_field) - self.assertEqual(myadmin.get_object(get_request, obj.pk).language_code, 'en') - self.assertEqual(myadmin.get_object(get_request, obj.pk).translated_field, '') + self.assertIs(myadmin.get_object(get_request, obj.pk), None) def test_get_object_nonexisting(self): # In case the object doesnt exist, it should return None diff --git a/hvad/tests/basic.py b/hvad/tests/basic.py index 3e00563f..bf0f9b1b 100644 --- a/hvad/tests/basic.py +++ b/hvad/tests/basic.py @@ -6,13 +6,12 @@ from django.db.models.manager import Manager from django.db.models.query_utils import Q from django.utils import translation -from hvad.compat import with_metaclass -from hvad.manager import TranslationQueryset, TranslationManager +from hvad.manager import TranslationQueryset from hvad.models import TranslatableModel, TranslatedFields from hvad.utils import get_cached_translation from hvad.test_utils.data import NORMAL from hvad.test_utils.fixtures import NormalFixture -from hvad.test_utils.testcase import HvadTestCase, minimumDjangoVersion +from hvad.test_utils.testcase import HvadTestCase from hvad.test_utils.project.app.models import Normal, Unique, Related, MultipleFields, Boolean, Standard from hvad.test_utils.project.alternate_models_app.models import NormalAlternate from copy import deepcopy @@ -20,23 +19,19 @@ class DefinitionTests(HvadTestCase): def test_invalid_manager(self): - attrs = { - 'objects': Manager(), - '__module__': 'hvad.test_utils.project.app', - } - self.assertRaises(ImproperlyConfigured, type, - 'InvalidModel', (TranslatableModel,), attrs) + class InvalidModel(TranslatableModel): + translations = TranslatedFields( + translated=models.CharField(max_length=250) + ) + object = Manager() + errors = InvalidModel.check() + self.assertEqual(len(errors), 1) + self.assertEqual(errors[0].id, 'hvad.models.E02') def test_no_translated_fields(self): - class InvalidModel2(object): - objects = TranslationManager() - - attrs = dict(InvalidModel2.__dict__) - del attrs['__dict__'] - del attrs['__weakref__'] - bases = (TranslatableModel,InvalidModel2,) - self.assertRaises(ImproperlyConfigured, type, - 'InvalidModel2', bases, attrs) + with self.assertRaises(ImproperlyConfigured): + class InvalidModel2(TranslatableModel): + pass def test_field_name_clash_check(self): class ClashingFieldsModel(TranslatableModel): @@ -56,7 +51,7 @@ class InvalidModel3(Normal): ) def test_order_with_respect_to_raises(self): - with self.assertRaises(ValueError): + with self.assertRaises(ImproperlyConfigured): class InvalidModel4(TranslatableModel): translations = TranslatedFields( translated_field = models.CharField(max_length=250) @@ -87,14 +82,6 @@ class Meta: self.assertIn(('tfield_a', 'tfield_b'), UniqueTogetherModel._meta.translations_model._meta.unique_together) - with self.assertRaises(ValueError): - class DeprecatedUniqueTogetherModel(TranslatableModel): - translations = TranslatedFields( - tfield_a = models.CharField(max_length=250), - tfield_b = models.CharField(max_length=250), - meta = { 'unique_together': [('tfield_a', 'tfield_b')] } - ) - def test_unique_together_invalid(self): with self.assertRaises(ImproperlyConfigured): class InvalidUniqueTogetherModel(TranslatableModel): @@ -159,14 +146,6 @@ class Meta: self.assertIn(('tfield_a', 'tfield_b'), IndexTogetherModel._meta.translations_model._meta.index_together) - with self.assertRaises(ValueError): - class DeprecatedIndexTogetherModel(TranslatableModel): - translations = TranslatedFields( - tfield_a = models.CharField(max_length=250), - tfield_b = models.CharField(max_length=250), - meta = { 'index_together': [('tfield_a', 'tfield_b')] } - ) - with self.assertRaises(ImproperlyConfigured): class InvalidIndexTogetherModel(TranslatableModel): sfield = models.CharField(max_length=250) @@ -219,10 +198,6 @@ class CustomBaseModel(TranslatableModel): self.assertTrue(issubclass(CustomBaseModel._meta.translations_model, CustomTranslation)) self.assertEqual(get_cached_translation(obj).test(), 'foo') - def test_internal_properties(self): - self.assertCountEqual(Normal()._translated_field_names, - ['id', 'master', 'master_id', 'language_code', 'translated_field']) - def test_manager_properties(self): manager = Normal.objects self.assertEqual(manager.translations_model, Normal._meta.translations_model) @@ -232,10 +207,7 @@ def test_options(self): opts = Normal._meta self.assertTrue(hasattr(opts, 'translations_model')) self.assertTrue(hasattr(opts, 'translations_accessor')) - if django.VERSION >= (1, 8): - relmodel = Normal._meta.get_field(opts.translations_accessor).field.model - else: - relmodel = Normal._meta.get_field_by_name(opts.translations_accessor)[0].model + relmodel = Normal._meta.get_field(opts.translations_accessor).field.model self.assertEqual(relmodel, opts.translations_model) diff --git a/hvad/tests/dates.py b/hvad/tests/dates.py index fc545a1b..4b27cc6c 100644 --- a/hvad/tests/dates.py +++ b/hvad/tests/dates.py @@ -1,6 +1,6 @@ from hvad.test_utils.data import DATE_REVERSED, DATE_VALUES from hvad.test_utils.fixtures import DateFixture -from hvad.test_utils.testcase import HvadTestCase, minimumDjangoVersion +from hvad.test_utils.testcase import HvadTestCase from hvad.test_utils.project.app.models import Date import datetime diff --git a/hvad/tests/fallbacks.py b/hvad/tests/fallbacks.py deleted file mode 100644 index 1aad7c94..00000000 --- a/hvad/tests/fallbacks.py +++ /dev/null @@ -1,274 +0,0 @@ -# -*- coding: utf-8 -*- -from django.utils import translation -from hvad.test_utils.data import NORMAL -from hvad.test_utils.testcase import HvadTestCase, minimumDjangoVersion, maximumDjangoVersion -from hvad.test_utils.project.app.models import Normal -from hvad.test_utils.fixtures import NormalFixture -from hvad.exceptions import WrongManager -from hvad.manager import LEGACY_FALLBACKS - -class FallbackDeprecationTests(HvadTestCase): - @maximumDjangoVersion(1, 9) - def test_untranslated_fallbacks_deprecation(self): - with self.assertThrowsWarning(DeprecationWarning): - Normal.objects.untranslated().use_fallbacks('en') - - @minimumDjangoVersion(1, 9) - def test_untranslated_fallbacks_removal(self): - with self.assertRaises(AssertionError): - Normal.objects.untranslated().use_fallbacks('en') - -@maximumDjangoVersion(1, 9) -class FallbackTests(HvadTestCase, NormalFixture): - normal_count = 2 - - def test_single_instance_fallback(self): - # fetch an object in a language that does not exist - with translation.override('de'): - with self.assertNumQueries(2 if LEGACY_FALLBACKS else 1): - obj = Normal.objects.untranslated().use_fallbacks('en', 'ja').get(pk=self.normal_id[1]) - self.assertEqual(obj.language_code, 'en') - self.assertEqual(obj.translated_field, NORMAL[1].translated_field['en']) - - def test_deferred_fallbacks(self): - with translation.override('de'): - qs = Normal.objects.untranslated().use_fallbacks('ru', None, 'en') - with translation.override('ja'): - with self.assertNumQueries(2 if LEGACY_FALLBACKS else 1): - obj = qs.get(pk=self.normal_id[1]) - self.assertEqual(obj.language_code, 'ja') - self.assertEqual(obj.translated_field, NORMAL[1].translated_field['ja']) - with translation.override('en'): - with self.assertNumQueries(2 if LEGACY_FALLBACKS else 1): - obj = qs.get(pk=self.normal_id[1]) - self.assertEqual(obj.language_code, 'en') - self.assertEqual(obj.translated_field, NORMAL[1].translated_field['en']) - - def test_shared_only(self): - with translation.override('de'): - with self.assertNumQueries(1): - obj = Normal.objects.untranslated().get(pk=self.normal_id[1]) - self.assertEqual(obj.shared_field, NORMAL[1].shared_field) - with self.assertNumQueries(1): - self.assertRaises(AttributeError, getattr, obj, 'translated_field') - - def test_mixed_fallback(self): - with translation.override('de'): - pk = Normal.objects.language('ja').create( - shared_field='shared_field', - translated_field=u'日本語三', - ).pk - with self.assertNumQueries(2 if LEGACY_FALLBACKS else 1): - objs = list(Normal.objects.untranslated().use_fallbacks('en', 'ja')) - self.assertEqual(len(objs), 3) - obj = dict([(obj.pk, obj) for obj in objs])[pk] - self.assertEqual(obj.language_code, 'ja') - with self.assertNumQueries(2 if LEGACY_FALLBACKS else 1): - objs = list(Normal.objects.untranslated().use_fallbacks('en')) - self.assertEqual(len(objs), 3) - - -@maximumDjangoVersion(1, 9) -class FallbackFilterTests(HvadTestCase, NormalFixture): - normal_count = 2 - - def test_simple_filter_untranslated(self): - with translation.override('en'): - qs = Normal.objects.untranslated() .filter(shared_field__contains='2') - with self.assertNumQueries(1): - self.assertEqual(qs.count(), 1) - with self.assertNumQueries(1): - obj = qs[0] - self.assertEqual(obj.shared_field, NORMAL[2].shared_field) - with self.assertNumQueries(1): - self.assertEqual(obj.translated_field, NORMAL[2].translated_field['en']) - - def test_simple_filter_fallbacks(self): - qs = (Normal.objects.untranslated() - .use_fallbacks('en', 'ja') - .filter(shared_field__contains='2')) - with self.assertNumQueries(1): - self.assertEqual(qs.count(), 1) - with self.assertNumQueries(2 if LEGACY_FALLBACKS else 1): - obj = qs[0] - with self.assertNumQueries(0): - self.assertEqual(obj.shared_field, NORMAL[2].shared_field) - self.assertEqual(obj.translated_field, NORMAL[2].translated_field['en']) - - qs = (Normal.objects.untranslated() - .use_fallbacks('ja', 'en') - .filter(shared_field__contains='1')) - with self.assertNumQueries(1): - self.assertEqual(qs.count(), 1) - with self.assertNumQueries(2 if LEGACY_FALLBACKS else 1): - obj = qs[0] - with self.assertNumQueries(0): - self.assertEqual(obj.shared_field, NORMAL[1].shared_field) - self.assertEqual(obj.translated_field, NORMAL[1].translated_field['ja']) - - def test_translated_filter(self): - with self.assertRaises(WrongManager): - Normal.objects.untranslated().filter(translated_field__contains='English') - - -@maximumDjangoVersion(1, 9) -class FallbackCachingTests(HvadTestCase, NormalFixture): - normal_count = 2 - - def _try_all_cache_using_methods(self, qs, length): - with self.assertNumQueries(0): - x = 0 - for obj in qs: x += 1 - self.assertEqual(x, length) - with self.assertNumQueries(0): - qs[0] - with self.assertNumQueries(0): - self.assertEqual(qs.exists(), length != 0) - with self.assertNumQueries(0): - self.assertEqual(qs.count(), length) - with self.assertNumQueries(0): - self.assertEqual(len(qs), length) - with self.assertNumQueries(0): - self.assertEqual(bool(qs), length != 0) - - def test_iter_caches(self): - index = 0 - qs = Normal.objects.untranslated().use_fallbacks().filter(pk=self.normal_id[1]) - for obj in qs: - index += 1 - self.assertEqual(index, 1) - self._try_all_cache_using_methods(qs, 1) - - def test_pickling_caches(self): - import pickle - qs = Normal.objects.untranslated().use_fallbacks().filter(pk=self.normal_id[1]) - pickle.dumps(qs) - self._try_all_cache_using_methods(qs, 1) - - def test_len_caches(self): - qs = Normal.objects.untranslated().use_fallbacks().filter(pk=self.normal_id[1]) - self.assertEqual(len(qs), 1) - self._try_all_cache_using_methods(qs, 1) - - def test_bool_caches(self): - qs = Normal.objects.untranslated().use_fallbacks().filter(pk=self.normal_id[1]) - self.assertTrue(qs) - self._try_all_cache_using_methods(qs, 1) - - -@maximumDjangoVersion(1, 9) -class FallbackIterTests(HvadTestCase, NormalFixture): - normal_count = 2 - - def test_simple_iter_no_fallbacks(self): - with translation.override('en'): - with self.assertNumQueries(1): - objs = list(Normal.objects.untranslated().order_by('pk')) - for index, obj in enumerate(objs, 1): - self.assertEqual(obj.shared_field, NORMAL[index].shared_field) - with self.assertNumQueries(1): - self.assertEqual(obj.translated_field, NORMAL[index].translated_field['en']) - - def test_simple_iter_fallbacks(self): - with self.assertNumQueries(2 if LEGACY_FALLBACKS else 1): - for index, obj in enumerate(Normal.objects.untranslated().use_fallbacks('en', 'ja').order_by('pk'), 1): - self.assertEqual(obj.shared_field, NORMAL[index].shared_field) - self.assertEqual(obj.translated_field, NORMAL[index].translated_field['en']) - - with self.assertNumQueries(2 if LEGACY_FALLBACKS else 1): - for index, obj in enumerate(Normal.objects.untranslated().use_fallbacks('ja', 'en').order_by('pk'), 1): - self.assertEqual(obj.shared_field, NORMAL[index].shared_field) - self.assertEqual(obj.translated_field, NORMAL[index].translated_field['ja']) - - def test_iter_unique_reply(self): - # Make sure .all() only returns unique rows - self.assertEqual(len(Normal.objects.untranslated().use_fallbacks('en', 'ja').all()), - len(Normal.objects.untranslated())) - - - -@maximumDjangoVersion(1, 9) -class FallbackValuesListTests(HvadTestCase, NormalFixture): - normal_count = 2 - - def test_values_list_shared(self): - values = (Normal.objects.untranslated() - .use_fallbacks('en', 'ja') - .values_list('shared_field', flat=True)) - with self.assertNumQueries(1): - values_list = list(values) - self.assertCountEqual(values_list, [NORMAL[1].shared_field, NORMAL[2].shared_field]) - - def test_values_list_translated(self): - with self.assertRaises(WrongManager): - Normal.objects.untranslated().values_list('translated_field', flat=True) - - -@maximumDjangoVersion(1, 9) -class FallbackValuesTests(HvadTestCase, NormalFixture): - normal_count = 2 - - def test_values_shared(self): - values = (Normal.objects.untranslated() - .use_fallbacks('en', 'ja') - .values('shared_field')) - with self.assertNumQueries(1): - values_list = list(values) - check = [ - {'shared_field': NORMAL[1].shared_field}, - {'shared_field': NORMAL[2].shared_field}, - ] - self.assertCountEqual(values_list, check) - - def test_values_translated(self): - with self.assertRaises(WrongManager): - Normal.objects.untranslated().values('translated_field') - - -@maximumDjangoVersion(1, 9) -class FallbackInBulkTests(HvadTestCase, NormalFixture): - normal_count = 2 - - def test_in_bulk_untranslated(self): - pk1, pk2 = self.normal_id[1], self.normal_id[2] - with translation.override('en'): - with self.assertNumQueries(1): - result = Normal.objects.untranslated().in_bulk([pk1, pk2]) - with self.assertNumQueries(0): - self.assertCountEqual((pk1, pk2), result) - self.assertEqual(result[pk1].shared_field, NORMAL[1].shared_field) - with self.assertNumQueries(1): - self.assertEqual(result[pk1].translated_field, NORMAL[1].translated_field['en']) - with self.assertNumQueries(0): - self.assertEqual(result[pk1].language_code, 'en') - with translation.override('ja'): - with self.assertNumQueries(0): - self.assertEqual(result[pk2].shared_field, NORMAL[2].shared_field) - with self.assertNumQueries(1): - self.assertEqual(result[pk2].translated_field, NORMAL[2].translated_field['ja']) - with self.assertNumQueries(0): - self.assertEqual(result[pk2].language_code, 'ja') - - def test_in_bulk_fallbacks(self): - pk1, pk2 = self.normal_id[1], self.normal_id[2] - with self.assertNumQueries(2 if LEGACY_FALLBACKS else 1): - result = (Normal.objects.untranslated() - .use_fallbacks('en', 'ja') - .in_bulk([pk1, pk2])) - with self.assertNumQueries(0): - self.assertCountEqual((pk1, pk2), result) - self.assertEqual(result[pk1].shared_field, NORMAL[1].shared_field) - self.assertEqual(result[pk1].translated_field, NORMAL[1].translated_field['en']) - self.assertEqual(result[pk1].language_code, 'en') - self.assertEqual(result[pk2].shared_field, NORMAL[2].shared_field) - self.assertEqual(result[pk2].translated_field, NORMAL[2].translated_field['en']) - self.assertEqual(result[pk2].language_code, 'en') - - -class FallbackNotImplementedTests(HvadTestCase): - def test_defer(self): - baseqs = Normal.objects.untranslated() - self.assertRaises(NotImplementedError, baseqs.defer, 'shared_field') - self.assertRaises(NotImplementedError, baseqs.only) - self.assertRaises(NotImplementedError, baseqs.aggregate) - self.assertRaises(NotImplementedError, baseqs.annotate) diff --git a/hvad/tests/forms.py b/hvad/tests/forms.py index ceaaf0ed..a249c02a 100644 --- a/hvad/tests/forms.py +++ b/hvad/tests/forms.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -import django from django.core.exceptions import FieldError from django.utils import translation from hvad.forms import (TranslatableModelForm, diff --git a/hvad/tests/limit_choices_to.py b/hvad/tests/limit_choices_to.py index 0ab3998c..750372d5 100644 --- a/hvad/tests/limit_choices_to.py +++ b/hvad/tests/limit_choices_to.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.contrib import admin from django.contrib.auth.models import User diff --git a/hvad/tests/ordering.py b/hvad/tests/ordering.py index bca072f9..13fd34d7 100644 --- a/hvad/tests/ordering.py +++ b/hvad/tests/ordering.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -import django -from hvad.test_utils.testcase import HvadTestCase, minimumDjangoVersion +from hvad.test_utils.testcase import HvadTestCase from hvad.test_utils.project.app.models import Normal from hvad.test_utils.fixtures import NormalFixture from hvad.exceptions import WrongManager diff --git a/hvad/tests/proxy.py b/hvad/tests/proxy.py index 934c5d30..da55e1f6 100644 --- a/hvad/tests/proxy.py +++ b/hvad/tests/proxy.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from hvad.test_utils.testcase import HvadTestCase, minimumDjangoVersion, maximumDjangoVersion +from hvad.test_utils.testcase import HvadTestCase from hvad.test_utils.project.app.models import Normal, NormalProxy, NormalProxyProxy, RelatedProxy, SimpleRelatedProxy @@ -67,12 +67,3 @@ def test_translation_queryset(self): self.assertTrue(isinstance(NormalProxy.objects.language('en').get(), NormalProxy)) self.assertFalse(isinstance(NormalProxy.objects.language('en').get(), NormalProxyProxy)) self.assertTrue(isinstance(NormalProxyProxy.objects.language('en').get(), NormalProxyProxy)) - - @maximumDjangoVersion(1, 9) - def test_fallback_queryset(self): - NormalProxyProxy.objects.language('en').create(shared_field='SHARED2', translated_field='English2') - self.assertTrue(isinstance(Normal.objects.untranslated().use_fallbacks().get(), Normal)) - self.assertFalse(isinstance(Normal.objects.untranslated().use_fallbacks().get(), NormalProxy)) - self.assertTrue(isinstance(NormalProxy.objects.untranslated().use_fallbacks().get(), NormalProxy)) - self.assertFalse(isinstance(NormalProxy.objects.untranslated().use_fallbacks().get(), NormalProxyProxy)) - self.assertTrue(isinstance(NormalProxyProxy.objects.untranslated().use_fallbacks().get(), NormalProxyProxy)) diff --git a/hvad/tests/query.py b/hvad/tests/query.py index 14ebeef0..4ea03f4c 100644 --- a/hvad/tests/query.py +++ b/hvad/tests/query.py @@ -1,12 +1,10 @@ -# -*- coding: utf-8 -*- -import django from django.db import connection from django.db.models import Count from django.db.models.query_utils import Q from django.utils import translation from hvad.utils import get_cached_translation from hvad.test_utils.data import NORMAL, STANDARD -from hvad.test_utils.testcase import HvadTestCase, minimumDjangoVersion +from hvad.test_utils.testcase import HvadTestCase from hvad.test_utils.project.app.models import Normal, AggregateModel, Standard, SimpleRelated from hvad.test_utils.fixtures import NormalFixture, StandardFixture diff --git a/hvad/tests/related.py b/hvad/tests/related.py index 806ddd4e..b90ab195 100644 --- a/hvad/tests/related.py +++ b/hvad/tests/related.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import django from django.core.exceptions import FieldError from django.db import connection, models, IntegrityError, transaction @@ -9,7 +8,7 @@ from hvad.models import (TranslatedFields, TranslatableModel) from hvad.test_utils.data import NORMAL, STANDARD from hvad.test_utils.fixtures import NormalFixture, StandardFixture -from hvad.test_utils.testcase import HvadTestCase, minimumDjangoVersion +from hvad.test_utils.testcase import HvadTestCase from hvad.utils import get_translation_aware_manager from hvad.test_utils.project.app.models import (Normal, Related, SimpleRelated, RelatedRelated, Standard, StandardRelated, diff --git a/hvad/tests/views.py b/hvad/tests/views.py index 14ad5eb4..9dec1454 100644 --- a/hvad/tests/views.py +++ b/hvad/tests/views.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -import django from django import forms from django.http import Http404 from django.utils import translation @@ -16,22 +15,17 @@ class TestCreateView(TranslatableCreateView): model = Normal - success_url = '{id}' if django.VERSION >= (1, 8) else '%(id)s' + success_url = '{id}' class TestUpdateView(TranslatableUpdateView): model = Normal slug_field = 'shared_field' - success_url = '{id}' if django.VERSION >= (1, 8) else '%(id)s' + success_url = '{id}' class TestDeleteView(TranslatableDeleteView): model = Normal slug_field = 'shared_field' -class DeprecatedFilterUpdateView(TranslatableUpdateView): - model = Normal - def filter_kwargs(self): - return {'shared_field': self.kwargs['custom']} - #============================================================================= class CreateViewTests(HvadTestCase): diff --git a/hvad/utils.py b/hvad/utils.py index c55e6b47..21c68d8c 100644 --- a/hvad/utils.py +++ b/hvad/utils.py @@ -3,7 +3,6 @@ from django.test.signals import setting_changed from django.utils.translation import get_language from hvad.exceptions import WrongManager -import warnings #============================================================================= # Translation manipulators diff --git a/hvad/views.py b/hvad/views.py index 6623fccd..d9a7fcbb 100644 --- a/hvad/views.py +++ b/hvad/views.py @@ -1,11 +1,10 @@ -from django.views.generic.detail import SingleObjectMixin, SingleObjectTemplateResponseMixin +from django.views.generic.detail import SingleObjectTemplateResponseMixin from django.views.generic.edit import ModelFormMixin, ProcessFormView, BaseDeleteView from django.utils.translation import get_language from hvad.forms import translatable_modelform_factory -import warnings -class TranslatableModelFormMixin(ModelFormMixin, SingleObjectMixin): +class TranslatableModelFormMixin(ModelFormMixin): ''' ModelFormMixin that works with an TranslatableModelForm in **enforce** mode ''' query_language_key = 'language' @@ -56,7 +55,7 @@ class TranslatableUpdateView(SingleObjectTemplateResponseMixin, TranslatableBase #------------------------------------------------------------------------- -class TranslatableBaseDeleteView(BaseDeleteView, SingleObjectMixin): +class TranslatableBaseDeleteView(BaseDeleteView): pass class TranslatableDeleteView(SingleObjectTemplateResponseMixin, TranslatableBaseDeleteView): diff --git a/runtests.py b/runtests.py index 4db23bf5..a085db32 100755 --- a/runtests.py +++ b/runtests.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -from __future__ import with_statement from django.utils.encoding import force_str from hvad.test_utils.cli import configure from hvad.test_utils.context_managers import TemporaryDirectory diff --git a/setup.py b/setup.py index b830829f..aad409fe 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,12 @@ from setuptools import setup, find_packages -from hvad import __version__ as version +import hvad with open('README.rst', 'rb') as f: long_description = f.read().decode('utf-8') setup( name = 'django-hvad', - version = version, + version = hvad.__version__, description = 'A content translation framework for django integrated automatically in the normal ORM. Removes the pain of having to think about translations in a django project.', long_description = long_description, author = 'Kristian Ollegaard', @@ -22,7 +22,7 @@ zip_safe=False, include_package_data = True, install_requires=[ - 'Django>=1.7', + 'Django>=1.8', ], classifiers = [ "Development Status :: 5 - Production/Stable", @@ -31,7 +31,6 @@ "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Topic :: Database",