Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Address comments of https://github.com/django-parler/django-parler/pull/216 #1

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
1 change: 0 additions & 1 deletion example/article/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,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
Expand Down
40 changes: 0 additions & 40 deletions parler/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@

import django
from django.forms.forms import pretty_name
from parler.utils.i18n import get_language

if (1, 8) <= django.VERSION < (2, 0):
from compositefk.fields import RawFieldValue, CompositeOneToOneField


# TODO: inherit RelatedField?
Expand Down Expand Up @@ -166,39 +162,3 @@ def __set__(self, instance, value):

def __delete__(self, instance):
raise AttributeError("The 'language_code' attribute cannot be deleted!")


class DONOTHING(object):
pass


if (1, 8) <= django.VERSION < (2, 0):
class CompositeOneToOneVirtualField(CompositeOneToOneField):
"""
Class to fix problem with creation repetitive migrations
"""
def deconstruct(self):
name, path, args, kwargs = super(CompositeOneToOneVirtualField, self).deconstruct()
if 'to_fields' in kwargs:
kwargs['to_fields'] = {'master_id': None, 'language_code': None} # hack: Need always the same dict
if "on_delete" in kwargs:
kwargs['on_delete'] = DONOTHING # hack: Need always the same global object with __module__ attr
if "null_if_equal" in kwargs:
del kwargs['null_if_equal']
return name, path, args, kwargs


class RawActiveLangFieldValue(RawFieldValue):
"""
Raw value with active language
"""
def __init__(self):
super(RawActiveLangFieldValue, self).__init__(None)

@property
def value(self):
return get_language()

@value.setter
def value(self, value):
pass
96 changes: 61 additions & 35 deletions parler/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from django.utils import six
from parler import appsettings
from parler.utils import get_active_language_choices
from parler.utils.fields import get_extra_related_translalation_paths
from parler.utils.fields import get_extra_related_translation_paths


class SelectRelatedTranslationsQuerySetMixin(object):
Expand All @@ -24,7 +24,7 @@ class SelectRelatedTranslationsQuerySetMixin(object):
def select_related(self, *fields):
extra_paths = []
for field in fields:
extra_paths += get_extra_related_translalation_paths(self.model, field)
extra_paths += get_extra_related_translation_paths(self.model, field)
if extra_paths:
fields = tuple(set(extra_paths)) + fields
return super(SelectRelatedTranslationsQuerySetMixin, self).select_related(*fields)
Expand All @@ -37,39 +37,66 @@ class TranslatableQuerySet(QuerySet):
When using this class in combination with *django-polymorphic*, make sure this
class is first in the chain of inherited classes.

When force_select_related_translations set to True in your classes it will always
adds active and default languages to select_related. It could break values_list method in django 1.9+
You can always add translated models to select_related manually. When you call it with rel_name e.g: 'translations'
it automatically adds active and default virtual composite FKs.
When select_related_translations set to True it will always adds translated models with active and
default languages by using virtual composite FKs.
In light version QS with force select related False you can always add translated models to select_related manually.
When you call select_related with translations rel_name e.g: 'translations' it automatically adds active and
default translated models to select_related.
"""

force_select_related_translations = False
select_related_translations = True

def __init__(self, *args, **kwargs):
super(TranslatableQuerySet, self).__init__(*args, **kwargs)
self._use_values = False
self._language = None

def select_related(self, *fields):
"""
Updates select_related to have active and default always together
Replaces main field refer to translations ('translations') with 'translations_active' and 'translations_default'
"""
fields_to_add = set()
fields_to_exclude = set([None]) # if rel_name_active, rel_name_default is None
fields_to_exclude = set()
for extension in self.model._parler_meta:
if extension.rel_name in fields:
select_related_translations_fields = extension.get_select_related_translations_fields()
fields_to_search = set(select_related_translations_fields + [extension.rel_name])
if fields_to_search.intersection(fields):
fields_to_exclude.add(extension.rel_name) # Can not select related OneToMany field
fields_to_add.add(extension.rel_name_active)
fields_to_add.add(extension.rel_name_default)
if extension.rel_name_active in fields:
fields_to_add.add(extension.rel_name_default)
if extension.rel_name_default in fields:
fields_to_add.add(extension.rel_name_active)
fields_to_add.update(select_related_translations_fields)
fields = set(fields).union(fields_to_add).difference(fields_to_exclude)
return super(TranslatableQuerySet, self).select_related(*tuple(fields))

def only(self, *fields):
"""
Replaces translated fields with 'translations_active' and 'translations_default'
pretending they are in original model so we can use .only
for translated fields as usual: .objects.only('some_translated_field')
"""
fields_to_add = set()
fields_to_exclude = set()
for extension in self.model._parler_meta:
select_related_translations_fields = extension.get_select_related_translations_fields()
translated_fields = set(extension.get_translated_fields()).intersection(fields)
if translated_fields:
fields_to_exclude.update(translated_fields) # Can not select related field form translated model (o2m)
fields_to_add.update(select_related_translations_fields)
fields = set(fields).union(fields_to_add).difference(fields_to_exclude)
return super(TranslatableQuerySet, self).only(*tuple(fields))

if (1, 9) <= django.VERSION:
def _values(self, *fields):
result = super(TranslatableQuerySet, self)._values(*fields)
result._use_values = True
return result

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)
c._language = self._language
c._use_values = self._use_values
return c

def create(self, **kwargs):
Expand All @@ -80,46 +107,45 @@ def create(self, **kwargs):
return super(TranslatableQuerySet, self).create(**kwargs)

def _add_active_default_select_related(self):
"""
Auto-adds select_related for active and default languages.
Takes in account deferred fields.
"""
existing, defer = self.query.deferred_loading
related_to_add = set()
for extension in self.model._parler_meta:
if not extension.rel_name:
continue
if extension.rel_name_active:
related_to_add.add(extension.rel_name_active)
if extension.rel_name_default:
related_to_add.add(extension.rel_name_default)
select_related_translations_fields = extension.get_select_related_translations_fields()
related_to_add.update(select_related_translations_fields)
if defer:
related_to_add = related_to_add.difference(existing)
elif existing:
related_to_add = related_to_add.intersection(existing)
self.query.add_select_related(related_to_add)

@property
def select_related_not_applicable(self):
def select_related_is_applicable(self):
# type: () -> Union[bool, None]
"""
Returns is select_related not applicable for current qs.
Currently determine only for django ver 1.8, for others returns None
Returns is select_related is applicable for current qs.
We can not use select_related with values_list, this function checks it.
"""
result = None
if self.model._meta.proxy:
return True
return False

if (1, 7) < django.VERSION < (1, 9):
if (1, 8) <= django.VERSION < (1, 9):
ValuesListQuerySet = getattr(django.db.models.query, 'ValuesListQuerySet')
result = isinstance(self, ValuesListQuerySet)
result = not isinstance(self, ValuesListQuerySet)
if (1, 9) <= django.VERSION:
result = not self._use_values

return result

def _fetch_all(self):
# For django ver > 1.8 when values_list method is used
# _iterable_class (FlatValuesListIterable, ValuesListIterable) is known only in iteration stage not here yet
# TODO: figure out how to determine non qs methods or
# place _add_active_default_select_related in some other place
if self.force_select_related_translations and not self.select_related_not_applicable:
if self.select_related_translations and self.select_related_is_applicable:
self._add_active_default_select_related()

# 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:
Expand Down Expand Up @@ -263,12 +289,12 @@ def active_translations(self, language_code=None, **translated_fields):
return self.all().active_translations(language_code, **translated_fields)


class TranslatableAutoSelectRelatedQuerySet(TranslatableQuerySet):
force_select_related_translations = True
class TranslatableLightSelectRelatedQuerySet(TranslatableQuerySet):
select_related_translations = False


class TranslatableAutoSelectRelatedManager(TranslatableManager):
queryset_class = TranslatableAutoSelectRelatedQuerySet
class TranslatableLightSelectRelatedManager(TranslatableManager):
queryset_class = TranslatableLightSelectRelatedQuerySet


# Export the names in django-hvad style too:
Expand Down
45 changes: 30 additions & 15 deletions parler/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ class Meta:
is_missing,
)
from parler.fields import TranslatedField, LanguageCodeDescriptor, TranslatedFieldDescriptor
from parler.managers import TranslatableManager, TranslatableAutoSelectRelatedManager
from parler.managers import TranslatableManager
from parler.utils import compat
from parler.utils.i18n import (normalize_language_code, get_language, get_language_settings, get_language_title,
get_null_language_error)
Expand All @@ -94,9 +94,8 @@ class Meta:
else:
from django.db.models.fields.related import ReverseSingleRelatedObjectDescriptor as ForwardManyToOneDescriptor

if (1, 8) <= django.VERSION < (2, 0):
from compositefk.fields import RawFieldValue
from parler.fields import CompositeOneToOneVirtualField, RawActiveLangFieldValue
if django.VERSION >= (1, 8):
from compositefk.fields import RawFieldValue, FunctionBasedFieldValue, CompositeOneToOneField


__all__ = (
Expand Down Expand Up @@ -141,34 +140,41 @@ def create_translations_composite_fk(shared_model, related_name, translated_mode
to get data in certain (active) language.
Does not cover fallback languages, for which you can use prefetch_related

Note: django-composite-foreignkey does not work in django 1.7 and 2+
Note: django-composite-foreignkey only works for Django version >= 1.8
"""
if not (1, 8) <= django.VERSION < (2, 0):
if django.VERSION < (1, 8):
return

meta = shared_model._parler_meta._get_extension_by_related_name(related_name)
meta.rel_name_active = related_name + '_active'
meta.rel_name_default = related_name + '_default'
translations_active = CompositeOneToOneVirtualField(
translations_active = CompositeOneToOneField(
translated_model,
null=True,
on_delete=models.DO_NOTHING,
related_name='master_active',
related_name='+',
to_fields={
'master_id': shared_model._meta.pk.name,
'language_code': RawActiveLangFieldValue()
'language_code': FunctionBasedFieldValue(get_language)
})
# Needs hack here.
# Set one_to_one = False as Django treat this field as a reversed
# see: django.db.models.sql.query.is_reverse_o2o
# Django does not include this field to 'must query fields', so it became deferred field if it used with only.
# To be able use the field in select_related field must be not deferred.
translations_active.one_to_one = False
translations_active.contribute_to_class(shared_model, meta.rel_name_active)

translations_default = CompositeOneToOneVirtualField(
translations_default = CompositeOneToOneField(
translated_model,
null=True,
on_delete=models.DO_NOTHING,
related_name='master_default',
related_name='+',
to_fields={
'master_id': shared_model._meta.pk.name,
'language_code': RawFieldValue(appsettings.PARLER_LANGUAGES.get_default_language())
})
translations_default.one_to_one = False
translations_default.contribute_to_class(shared_model, meta.rel_name_default)


Expand Down Expand Up @@ -890,10 +896,7 @@ class Meta:
abstract = True

# change the default manager to the translation manager
if (1, 8) <= django.VERSION < (1, 9):
objects = TranslatableAutoSelectRelatedManager()
else:
objects = TranslatableManager()
objects = TranslatableManager()


class TranslatedFieldsModelBase(ModelBase):
Expand Down Expand Up @@ -1178,6 +1181,14 @@ def __repr__(self):
self.model.__name__
)

def get_select_related_translations_fields(self):
result = []
if self.rel_name_active:
result.append(self.rel_name_active)
if self.rel_name_default:
result.append(self.rel_name_default)
return result


class ParlerOptions(object):
"""
Expand Down Expand Up @@ -1301,6 +1312,10 @@ def get_translated_fields(self, related_name=None):
meta = self._get_extension_by_related_name(related_name)
return meta.get_translated_fields()

def get_select_related_translations_fields(self, related_name=None):
meta = self._get_extension_by_related_name(related_name)
return meta.get_select_related_translations_fields()

def get_model_by_field(self, name):
"""
Find the :class:`TranslatedFieldsModel` that contains the given field.
Expand Down
9 changes: 1 addition & 8 deletions parler/tests/test_model_construction.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
from functools import wraps

from unittest import expectedFailure, skipIf
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

Expand Down
Loading