Skip to content

Commit

Permalink
Add premilinary support for Django 1.11.
Browse files Browse the repository at this point in the history
  • Loading branch information
spectras committed Feb 20, 2017
1 parent ccb47a3 commit f3c21eb
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 109 deletions.
18 changes: 18 additions & 0 deletions .travis.yml
Expand Up @@ -3,13 +3,16 @@ python:
- 2.7
- 3.4
- 3.5
- 3.6
env:
- 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
- DJANGO=django==1.9.12 DRF=3.3.1 DATABASE_URL=postgres://postgres@localhost/test
- DJANGO=django==1.10.5 DRF=3.5.3 DATABASE_URL=mysql://root@localhost/test
- DJANGO=django==1.10.5 DRF=3.5.3 DATABASE_URL=postgres://postgres@localhost/test
- DJANGO=https://github.com/django/django/archive/stable/1.11.x.tar.gz DRF=3.5.3 DATABASE_URL=mysql://root@localhost/test
- DJANGO=https://github.com/django/django/archive/stable/1.11.x.tar.gz DRF=3.5.3 DATABASE_URL=postgres://postgres@localhost/test

sudo: false
install:
Expand All @@ -24,3 +27,18 @@ script:
coverage run --source=hvad --omit='hvad/test*' runtests.py
after_success:
if [[ $COVERALLS_REPO_TOKEN ]]; then coveralls; fi

matrix:
exclude:
- python: 3.6
env: DJANGO=django==1.8.17 DRF=3.3.1 DATABASE_URL=mysql://root@localhost/test
- python: 3.6
env: DJANGO=django==1.8.17 DRF=3.3.1 DATABASE_URL=postgres://postgres@localhost/test
- python: 3.6
env: DJANGO=django==1.9.12 DRF=3.3.1 DATABASE_URL=mysql://root@localhost/test
- python: 3.6
env: DJANGO=django==1.9.12 DRF=3.3.1 DATABASE_URL=postgres://postgres@localhost/test
- python: 3.6
env: DJANGO=django==1.10.5 DRF=3.5.3 DATABASE_URL=mysql://root@localhost/test
- python: 3.6
env: DJANGO=django==1.10.5 DRF=3.5.3 DATABASE_URL=postgres://postgres@localhost/test
8 changes: 6 additions & 2 deletions hvad/admin.py
@@ -1,10 +1,14 @@
import functools
import warnings
import django
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 FieldDoesNotExist, PermissionDenied, ValidationError
from django.core.urlresolvers import reverse
if django.VERSION >= (1, 10):
from django.urls import reverse
else:
from django.core.urlresolvers import reverse
from django.db import router, transaction
from django.forms.models import model_to_dict
from django.forms.utils import ErrorList
Expand Down Expand Up @@ -169,7 +173,7 @@ def render_change_form(self, request, context, add=False, change=False,
form_url='', obj=None):
lang_code = self._language(request)
lang = get_language_info(lang_code)['name_local']
available_languages = self.get_available_languages(obj)
available_languages = [] if obj is None else obj.get_available_languages()

context.update({
'title': '%s (%s)' % (context['title'], lang),
Expand Down
181 changes: 106 additions & 75 deletions hvad/manager.py
Expand Up @@ -67,19 +67,64 @@ def _build(self, key):
#===============================================================================

if django.VERSION >= (1, 9):
from django.db.models.query import ValuesIterable, ValuesListIterable, FlatValuesListIterable
from django.db.models.query import (ModelIterable, ValuesIterable,
ValuesListIterable, FlatValuesListIterable)

class TranslatableModelIterable(ModelIterable):
def __iter__(self):
qs = self.queryset._clone()._add_language_filter()
qs._iterable_class = ModelIterable
qs._known_related_objects = {}
if qs._forced_unique_fields:
with ForcedUniqueFields(qs._forced_unique_fields):
objects = list(qs.iterator())

if type(qs.query.select_related) == dict:
for obj in objects:
qs._use_related_translations(obj, qs.query.select_related)
else:
objects = qs.iterator()

for obj in objects:
for name in qs._hvad_switch_fields:
try:
setattr(obj.master, name, getattr(obj, name))
except AttributeError: # pragma: no cover
pass
else:
delattr(obj, name)
obj = combine(obj, qs.shared_model)
# use known objects from self.queryset, not qs as we cleared it earlier
for field, rel_objs in self.queryset._known_related_objects.items():
if hasattr(obj, field.get_cache_name()):
continue # pragma: no cover (conform to Django behavior)
pk = getattr(obj, field.get_attname())
try:
rel_obj = rel_objs[pk]
except KeyError: # pragma: no cover
pass
else:
setattr(obj, field.name, rel_obj)
yield obj

class TranslatedValuesIterable(ValuesIterable):
def __iter__(self):
queryset = self.queryset
for row in super(TranslatedValuesIterable, self).__iter__():
yield queryset._reverse_translate_fieldnames_dict(row)
qs = self.queryset._clone()._add_language_filter()
qs._iterable_class = ValuesIterable
for row in qs.iterator():
yield qs._reverse_translate_fieldnames_dict(row)

iterable_overrides = {
ValuesIterable: TranslatedValuesIterable,
ValuesListIterable: ValuesListIterable,
FlatValuesListIterable: FlatValuesListIterable,
}
class TranslatedValuesListIterable(ValuesListIterable):
def __iter__(self):
qs = self.queryset._clone()._add_language_filter()
qs._iterable_class = ValuesListIterable
return qs.iterator()

class TranslatedFlatValuesListIterable(FlatValuesListIterable):
def __iter__(self):
qs = self.queryset._clone()._add_language_filter()
qs._iterable_class = FlatValuesListIterable
return qs.iterator()
else:
class ValuesMixin(object):
_skip_master_select = True
Expand All @@ -92,9 +137,6 @@ def iterator(self):
else:
yield row

class SkipMasterSelectMixin(object):
_skip_master_select = True

#===============================================================================

class ForcedUniqueFields(object):
Expand Down Expand Up @@ -193,6 +235,8 @@ def __init__(self, *args, **kwargs):
self._language_filter_tag = False
self._hvad_switch_fields = ()
super(TranslationQueryset, self).__init__(model, *args, **kwargs)
if django.VERSION >= (1, 9):
self._iterable_class = TranslatableModelIterable

#===========================================================================
# Helpers and properties (INTERNAL!)
Expand Down Expand Up @@ -294,6 +338,7 @@ def _split_kwargs(self, **kwargs):
return shared, translated

def _get_class(self, klass):
# remove whole method when we drop support for Django 1.8
for key, value in self.override_classes.items():
if issubclass(klass, key):
return type(value.__name__, (value, klass, TranslationQueryset,), {})
Expand Down Expand Up @@ -464,66 +509,42 @@ def fallbacks(self, *fallbacks):
# Queryset/Manager API that do database queries
#===========================================================================

def iterator(self):
"""
If this queryset is not filtered by a language code yet, it should be
filtered first by calling self.language.
If someone doesn't want a queryset filtered by language, they should use
Model.objects.untranslated()
"""
qs = self._clone()._add_language_filter()
qs._known_related_objects = {} # super's iterator will attempt to set them

if django.VERSION >= (1, 9) and qs._iterable_class in iterable_overrides:
qs._iterable_class = iterable_overrides[qs._iterable_class]
for obj in super(TranslationQueryset, qs).iterator():
if django.VERSION < (1, 9):
def iterator(self):
qs = self._clone()._add_language_filter()
qs._known_related_objects = {} # super's iterator will attempt to set them
if qs._forced_unique_fields:
with ForcedUniqueFields(qs._forced_unique_fields):
objects = list(super(TranslationQueryset, qs).iterator())

if type(qs.query.select_related) == dict:
for obj in objects:
qs._use_related_translations(obj, qs.query.select_related)
else:
objects = super(TranslationQueryset, qs).iterator()

for obj in objects:
for name in self._hvad_switch_fields:
try:
setattr(obj.master, name, getattr(obj, name))
except AttributeError: # pragma: no cover
pass
else:
delattr(obj, name)
obj = combine(obj, qs.shared_model)
# use known objects from self, not qs as we cleared it earlier
for field, rel_objs in self._known_related_objects.items():
if hasattr(obj, field.get_cache_name()):
# should not happen, but we conform to Django behavior
continue #pragma: no cover
pk = getattr(obj, field.get_attname())
try:
rel_obj = rel_objs[pk]
except KeyError: #pragma: no cover
pass
else:
setattr(obj, field.name, rel_obj)
yield obj
return

if qs._forced_unique_fields:
# HACK: In order for select_related to properly load data from
# translated models, we have to force django to treat
# certain fields as one-to-one relations
# before this queryset calls get_cached_row()
# We change it back so that things get reset to normal
# before execution returns to user code.
# It would be more direct and robust if we could wrap
# django.db.models.query.get_cached_row() instead, but that's not a class
# method, sadly, so we cannot override it just for this query

with ForcedUniqueFields(qs._forced_unique_fields):
# Pre-fetch all objects:
objects = list(super(TranslationQueryset, qs).iterator())

if type(qs.query.select_related) == dict:
for obj in objects:
qs._use_related_translations(obj, qs.query.select_related)
else:
objects = super(TranslationQueryset, qs).iterator()

for obj in objects:
for name in self._hvad_switch_fields:
try:
setattr(obj.master, name, getattr(obj, name))
except AttributeError: # pragma: no cover
pass
else:
delattr(obj, name)
obj = combine(obj, qs.shared_model)
# use known objects from self, not qs as we cleared it earlier
for field, rel_objs in self._known_related_objects.items():
if hasattr(obj, field.get_cache_name()):
# should not happen, but we conform to Django behavior
continue #pragma: no cover
pk = getattr(obj, field.get_attname())
try:
rel_obj = rel_objs[pk]
except KeyError: #pragma: no cover
pass
else:
setattr(obj, field.name, rel_obj)
yield obj

def create(self, **kwargs):
if 'language_code' not in kwargs:
Expand Down Expand Up @@ -638,12 +659,14 @@ def delete(self):
delete.queryset_only = True

def delete_translations(self):
qs = self._clone()._add_language_filter()
if connections[self._db].features.update_can_self_select:
qs = self._clone()._add_language_filter()
super(TranslationQueryset, qs).delete()
else:
with transaction.atomic(using=self._db, savepoint=False):
pks = list(super(TranslationQueryset, self).values_list('pk', flat=True))
qs = (super(TranslationQueryset, qs) if django.VERSION >= (1, 9) else
super(TranslationQueryset, self))
pks = list(qs.values_list('pk', flat=True))
self.model._base_manager.filter(pk__in=pks).delete()
delete_translations.alters_data = True

Expand Down Expand Up @@ -687,11 +710,19 @@ def extra(self, select=None, where=None, params=None, tables=None,

def values(self, *fields):
fields = self._translate_fieldnames(fields)
return super(TranslationQueryset, self).values(*fields)
qs = super(TranslationQueryset, self).values(*fields)
if django.VERSION >= (1, 9):
qs._iterable_class = TranslatedValuesIterable
return qs

def values_list(self, *fields, **kwargs):
fields = self._translate_fieldnames(fields)
return super(TranslationQueryset, self).values_list(*fields, **kwargs)
qs = super(TranslationQueryset, self).values_list(*fields, **kwargs)
if django.VERSION >= (1, 9):
qs._iterable_class = (TranslatedFlatValuesListIterable
if qs._iterable_class is FlatValuesListIterable else
TranslatedValuesListIterable)
return qs

def select_related(self, *fields):
if not fields:
Expand Down
15 changes: 11 additions & 4 deletions hvad/query.py
@@ -1,3 +1,4 @@
import django
from django.db.models import Q, FieldDoesNotExist
from django.db.models.expressions import Expression, Col
from django.db.models.sql.where import WhereNode, AND
Expand Down Expand Up @@ -42,10 +43,16 @@ def query_terms(model, path):

# STEP 2 -- Find out the target of the relation, if it is one
if direct: # field is on model
if field.rel: # field is a foreign key, follow it
target = field.rel.to._meta.concrete_model
else: # field is a regular field
target = None
if django.VERSION >= (1, 9):
if field.remote_field: # field is a foreign key, follow it
target = field.remote_field.model._meta.concrete_model
else:
target = None # field is a regular field
else:
if field.rel: # field is a foreign key, follow it
target = field.rel.to._meta.concrete_model
else:
target = None # field is a regular field
else: # field is a m2m or reverse fk, follow it
target = field.related_model._meta.concrete_model

Expand Down

0 comments on commit f3c21eb

Please sign in to comment.