From ef298de0de938a02a94010e5c63083961c7e9b0b Mon Sep 17 00:00:00 2001 From: Sergey Tereschenko Date: Wed, 13 Feb 2019 17:21:47 +0200 Subject: [PATCH 1/7] ignore dev pipfiles --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index ce17a1e..3eb3155 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,10 @@ MANIFEST # Installer logs pip-log.txt +# Development pipfiles +Pipfile +Pipfile.lock + # Unit test / coverage reports .coverage .tox From b8c02121e29f88e9c2c518d274594f34cf3e64a7 Mon Sep 17 00:00:00 2001 From: Sergey Tereschenko Date: Wed, 13 Feb 2019 17:37:20 +0200 Subject: [PATCH 2/7] remove old django version --- modeltranslation/tests/translation.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modeltranslation/tests/translation.py b/modeltranslation/tests/translation.py index 7f89475..113861a 100644 --- a/modeltranslation/tests/translation.py +++ b/modeltranslation/tests/translation.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from django import VERSION from django.conf import settings from django.utils.translation import ugettext_lazy @@ -221,7 +220,7 @@ translator.register(ModelY, ModelYOptions) # ######### 3-rd party with custom manager -if VERSION >= (1, 8) and "django.contrib.auth" in settings.INSTALLED_APPS: +if "django.contrib.auth" in settings.INSTALLED_APPS: from django.contrib.auth.models import Group @register(Group) From 55aa0ef2e31055e452861190bf79de19724b506b Mon Sep 17 00:00:00 2001 From: Sergey Tereschenko Date: Sat, 16 Feb 2019 15:11:47 +0200 Subject: [PATCH 3/7] tox: accept args --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 7906f6d..11a0ee8 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ envlist = downloadcache = {toxworkdir}/_download/ commands = django-admin.py --version - {envpython} runtests.py + {envpython} runtests.py {posargs} basepython = py27: python2.7 py34: python3.4 From a533b13f564a532f624f102397b581fcdc88902d Mon Sep 17 00:00:00 2001 From: Sergey Tereschenko Date: Sat, 16 Feb 2019 15:12:53 +0200 Subject: [PATCH 4/7] cleanup: remove code for old djago --- modeltranslation/translator.py | 169 ++++++++++----------------------- 1 file changed, 51 insertions(+), 118 deletions(-) diff --git a/modeltranslation/translator.py b/modeltranslation/translator.py index 962d25d..f6f6b57 100644 --- a/modeltranslation/translator.py +++ b/modeltranslation/translator.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from functools import partial -from django import VERSION +import django from django.utils.six import with_metaclass from django.core.exceptions import ImproperlyConfigured from django.db.models import Manager, ForeignKey, OneToOneField @@ -17,10 +17,6 @@ from modeltranslation.manager import (MultilingualManager, MultilingualQuerysetM from modeltranslation.utils import build_localized_fieldname, parse_field -NEW_RELATED_API = VERSION >= (1, 9) -NEW_DEFERRED_API = NEW_MANAGER_API = NEW_ABSTRACT_API = VERSION >= (1, 10) - - class AlreadyRegistered(Exception): pass @@ -163,12 +159,8 @@ def add_translation_fields(model, opts): # Rebuild information about parents fields. If there are opts.local_fields, field cache would be # invalidated (by model._meta.add_field() function). Otherwise, we need to do it manually. if len(opts.local_fields) == 0: - try: - model._meta._fill_fields_cache() - except AttributeError: - # Django 1.8 removed _fill_fields_cache - model._meta._expire_cache() - model._meta.get_fields() + model._meta._expire_cache() + model._meta.get_fields() def has_custom_queryset(manager): @@ -178,6 +170,29 @@ def has_custom_queryset(manager): return old_diff or new_diff +def patch_manager_class(manager): + if isinstance(manager, MultilingualManager): + return + if manager.__class__ is Manager: + manager.__class__ = MultilingualManager + else: + class NewMultilingualManager(MultilingualManager, manager.__class__, + MultilingualQuerysetManager): + _old_module = manager.__module__ + _old_class = manager.__class__.__name__ + + def deconstruct(self): + return ( + False, # as_manager + '%s.%s' % (self._old_module, self._old_class), # manager_class + None, # qs_class + self._constructor_args[0], # args + self._constructor_args[1], # kwargs + ) + + manager.__class__ = NewMultilingualManager + + def add_manager(model): """ Monkey patches the original model to use MultilingualManager instead of @@ -188,51 +203,18 @@ def add_manager(model): if model._meta.abstract: return - def patch_manager_class(manager): - if isinstance(manager, MultilingualManager): - return - if manager.__class__ is Manager: - manager.__class__ = MultilingualManager - else: - class NewMultilingualManager(MultilingualManager, manager.__class__, - MultilingualQuerysetManager): - if VERSION < (1, 10): - use_for_related_fields = getattr( - manager.__class__, - "use_for_related_fields", - not has_custom_queryset(manager), - ) - _old_module = manager.__module__ - _old_class = manager.__class__.__name__ - - def deconstruct(self): - return ( - False, # as_manager - '%s.%s' % (self._old_module, self._old_class), # manager_class - None, # qs_class - self._constructor_args[0], # args - self._constructor_args[1], # kwargs - ) - - manager.__class__ = NewMultilingualManager - - if NEW_MANAGER_API: - # Inspired by django.db.models.options.Options.managers (find all - # managers by following the normal Python MRO rules), but keeps the - # original managers instead of making copies. - managers = [] - seen = set() - bases = (b for b in model.mro() if hasattr(b, '_meta')) - for base in bases: - for manager in base._meta.local_managers: - if manager.name in seen: - continue - managers.append(manager) - seen.add(manager.name) - - else: - managers = ((getattr(model, x[1]) for x in - model._meta.concrete_managers + model._meta.abstract_managers)) + # Inspired by django.db.models.options.Options.managers (find all + # managers by following the normal Python MRO rules), but keeps the + # original managers instead of making copies. + managers = [] + seen = set() + bases = [b for b in model.mro() if hasattr(b, '_meta')] + for base in bases: + for manager in base._meta.local_managers: + if manager.name in seen: + continue + managers.append(manager) + seen.add(manager.name) for current_manager in managers: prev_class = current_manager.__class__ @@ -245,10 +227,8 @@ def add_manager(model): # share the same class. model._default_manager.__class__ = current_manager.__class__ patch_manager_class(model._base_manager) - if VERSION >= (1, 10): - model._meta.base_manager_name = 'objects' - if hasattr(model._meta, "_expire_cache"): - model._meta._expire_cache() + model._meta.base_manager_name = 'objects' + model._meta._expire_cache() def patch_constructor(model): @@ -259,12 +239,11 @@ def patch_constructor(model): def new_init(self, *args, **kwargs): self._mt_init = True - if NEW_DEFERRED_API or not self._deferred: - populate_translation_fields(self.__class__, kwargs) - for key, val in list(kwargs.items()): - new_key = rewrite_lookup_key(model, key) - # Old key is intentionally left in case old_init wants to play with it - kwargs.setdefault(new_key, val) + populate_translation_fields(self.__class__, kwargs) + for key, val in list(kwargs.items()): + new_key = rewrite_lookup_key(model, key) + # Old key is intentionally left in case old_init wants to play with it + kwargs.setdefault(new_key, val) old_init(self, *args, **kwargs) model.__init__ = new_init @@ -328,37 +307,6 @@ def patch_refresh_from_db(model): model.refresh_from_db = new_refresh_from_db -def patch_metaclass(model): - """ - Monkey patches original model metaclass to exclude translated fields on deferred subclasses. - """ - old_mcs = model.__class__ - - class translation_deferred_mcs(old_mcs): - """ - This metaclass is essential for deferred subclasses (obtained via only/defer) to work. - - When deferred subclass is created, some translated fields descriptors could be overridden - by DeferredAttribute - which would cause translation retrieval to fail. - Prevent this from happening with deleting those attributes from class being created. - This metaclass would be called from django.db.models.query_utils.deferred_class_factory - """ - def __new__(cls, name, bases, attrs): - if attrs.get('_deferred', False): - opts = translator.get_options_for_model(model) - were_deferred = set() - for field_name in opts.fields.keys(): - if attrs.pop(field_name, None): - # Field was deferred. Store this for future reference. - were_deferred.add(field_name) - if len(were_deferred): - attrs['_fields_were_deferred'] = were_deferred - return super(translation_deferred_mcs, cls).__new__(cls, name, bases, attrs) - # Assign to __metaclass__ wouldn't work, since metaclass search algorithm check for __class__. - # http://docs.python.org/2/reference/datamodel.html#__metaclass__ - model.__class__ = translation_deferred_mcs - - def delete_cache_fields(model): opts = model._meta cached_attrs = ('_field_cache', '_field_name_cache', '_name_map', 'fields', 'concrete_fields', @@ -431,7 +379,7 @@ def patch_related_object_descriptor_caching(ro_descriptor): class NewSingleObjectDescriptor(LanguageCacheSingleObjectDescriptor, ro_descriptor.__class__): pass - if VERSION[0] == 2: + if django.VERSION[0] == 2: ro_descriptor.related.get_cache_name = partial( NewSingleObjectDescriptor.get_cache_name, ro_descriptor, @@ -505,11 +453,11 @@ class Translator(object): add_translation_fields(model, opts) # Delete all fields cache for related model (parent and children) - related = (( + related = ( f for f in model._meta.get_fields() if (f.one_to_many or f.one_to_one) and f.auto_created - ) if NEW_RELATED_API else model._meta.get_all_related_objects()) + ) for related_obj in related: delete_cache_fields(related_obj.model) @@ -521,19 +469,12 @@ class Translator(object): patch_constructor(model) # Connect signal for model - if NEW_DEFERRED_API: - post_init.connect(delete_mt_init, sender=model) - else: - # deferred models have their own classes and the `sender` does not match. - # Connect signal for all models. - post_init.connect(delete_mt_init, dispatch_uid="modeltranslation") + post_init.connect(delete_mt_init, sender=model) # Patch clean_fields to verify form field clearing patch_clean_fields(model) # Patch __metaclass__ and other methods to allow deferring to work - if not NEW_DEFERRED_API: - patch_metaclass(model) patch_get_deferred_fields(model) patch_refresh_from_db(model) @@ -559,24 +500,18 @@ class Translator(object): setattr(model, field.get_attname(), desc) # Set related field names on other model - if NEW_RELATED_API and not field.remote_field.is_hidden(): + if not field.remote_field.is_hidden(): other_opts = self._get_options_for_model(field.remote_field.model) other_opts.related = True other_opts.related_fields.append(field.related_query_name()) # Add manager in case of non-registered model add_manager(field.remote_field.model) - elif not NEW_RELATED_API and not field.rel.is_hidden(): - other_opts = self._get_options_for_model(field.rel.to) - other_opts.related = True - other_opts.related_fields.append(field.related_query_name()) - add_manager(field.rel.to) # Add manager in case of non-registered model if isinstance(field, OneToOneField): # Fix translated_field caching for SingleRelatedObjectDescriptor sro_descriptor = ( getattr(field.remote_field.model, field.remote_field.get_accessor_name()) - if NEW_RELATED_API - else getattr(field.rel.to, field.related.get_accessor_name())) + ) patch_related_object_descriptor_caching(sro_descriptor) def unregister(self, model_or_iterable): @@ -619,8 +554,6 @@ class Translator(object): Returns an instance of translation options with translated fields defined for the ``model`` and inherited from superclasses. """ - if not NEW_DEFERRED_API and model._deferred: - model = model._meta.proxy_for_model if model not in self._registry: # Create a new type for backwards compatibility. opts = type("%sTranslationOptions" % model.__name__, From 68ea5a870da237db3af2b9ec2e4558744396d9ed Mon Sep 17 00:00:00 2001 From: Sergey Tereschenko Date: Sat, 16 Feb 2019 16:53:21 +0200 Subject: [PATCH 5/7] tests update - use decorators by default - remove extra decorator registration test - use models as module instead of importing every model --- modeltranslation/tests/models.py | 6 --- modeltranslation/tests/translation.py | 74 +++++++++++---------------- 2 files changed, 30 insertions(+), 50 deletions(-) diff --git a/modeltranslation/tests/models.py b/modeltranslation/tests/models.py index 81b0214..1e286e0 100644 --- a/modeltranslation/tests/models.py +++ b/modeltranslation/tests/models.py @@ -321,12 +321,6 @@ class RequiredModel(models.Model): req_en_reg = models.CharField(max_length=10) -# ######### Decorated registration testing - -class DecoratedModel(models.Model): - title = models.CharField(ugettext_lazy('title'), max_length=255) - - # ######### Name collision registration testing class ConflictModel(models.Model): diff --git a/modeltranslation/tests/translation.py b/modeltranslation/tests/translation.py index 113861a..0fb4272 100644 --- a/modeltranslation/tests/translation.py +++ b/modeltranslation/tests/translation.py @@ -3,147 +3,140 @@ from django.conf import settings from django.utils.translation import ugettext_lazy from modeltranslation.translator import translator, register, TranslationOptions -from modeltranslation.tests.models import ( - TestModel, FallbackModel, FallbackModel2, FileFieldsModel, ForeignKeyModel, OtherFieldsModel, - DescriptorModel, AbstractModelA, AbstractModelB, Slugged, MetaData, Displayable, Page, - RichText, RichTextPage, MultitableModelA, MultitableModelB, MultitableModelC, ManagerTestModel, - CustomManagerTestModel, CustomManager2TestModel, GroupFieldsetsModel, NameModel, - ThirdPartyRegisteredModel, ProxyTestModel, UniqueNullableModel, OneToOneFieldModel, - RequiredModel, DecoratedModel, ModelX, ModelY) +from modeltranslation.tests import models +@register(models.TestModel) class TestTranslationOptions(TranslationOptions): fields = ('title', 'text', 'url', 'email',) empty_values = '' -translator.register(TestModel, TestTranslationOptions) +@register(models.UniqueNullableModel) class UniqueNullableTranslationOptions(TranslationOptions): fields = ('title',) -translator.register(UniqueNullableModel, UniqueNullableTranslationOptions) # ######### Proxy model testing +@register(models.ProxyTestModel) class ProxyTestTranslationOptions(TranslationOptions): fields = ('title', 'text', 'url', 'email',) -translator.register(ProxyTestModel, ProxyTestTranslationOptions) # ######### Fallback values testing +@register(models.FallbackModel) class FallbackModelTranslationOptions(TranslationOptions): fields = ('title', 'text', 'url', 'email', 'description') fallback_values = "fallback" -translator.register(FallbackModel, FallbackModelTranslationOptions) +@register(models.FallbackModel2) class FallbackModel2TranslationOptions(TranslationOptions): fields = ('title', 'text', 'url', 'email',) fallback_values = {'text': ugettext_lazy('Sorry, translation is not available.')} fallback_undefined = {'title': 'no title'} -translator.register(FallbackModel2, FallbackModel2TranslationOptions) # ######### File fields testing +@register(models.FileFieldsModel) class FileFieldsModelTranslationOptions(TranslationOptions): fields = ('title', 'file', 'file2', 'image',) -translator.register(FileFieldsModel, FileFieldsModelTranslationOptions) # ######### Foreign Key / OneToOneField testing +@register(models.ForeignKeyModel) class ForeignKeyModelTranslationOptions(TranslationOptions): fields = ('title', 'test', 'optional', 'hidden', 'non',) -translator.register(ForeignKeyModel, ForeignKeyModelTranslationOptions) +@register(models.OneToOneFieldModel) class OneToOneFieldModelTranslationOptions(TranslationOptions): fields = ('title', 'test', 'optional', 'non',) -translator.register(OneToOneFieldModel, OneToOneFieldModelTranslationOptions) # ######### Custom fields testing +@register(models.OtherFieldsModel) class OtherFieldsModelTranslationOptions(TranslationOptions): fields = ('int', 'boolean', 'nullboolean', 'csi', 'float', 'decimal', 'ip', 'genericip', 'date', 'datetime', 'time',) -translator.register(OtherFieldsModel, OtherFieldsModelTranslationOptions) +@register(models.DescriptorModel) class DescriptorModelTranslationOptions(TranslationOptions): fields = ('trans',) -translator.register(DescriptorModel, DescriptorModelTranslationOptions) # ######### Multitable inheritance testing +@register(models.MultitableModelA) class MultitableModelATranslationOptions(TranslationOptions): fields = ('titlea',) -translator.register(MultitableModelA, MultitableModelATranslationOptions) +@register(models.MultitableModelB) class MultitableModelBTranslationOptions(TranslationOptions): fields = ('titleb',) -translator.register(MultitableModelB, MultitableModelBTranslationOptions) +@register(models.MultitableModelC) class MultitableModelCTranslationOptions(TranslationOptions): fields = ('titlec',) -translator.register(MultitableModelC, MultitableModelCTranslationOptions) # ######### Abstract inheritance testing +@register(models.AbstractModelA) class AbstractModelATranslationOptions(TranslationOptions): fields = ('titlea',) -translator.register(AbstractModelA, AbstractModelATranslationOptions) +@register(models.AbstractModelB) class AbstractModelBTranslationOptions(TranslationOptions): fields = ('titleb',) -translator.register(AbstractModelB, AbstractModelBTranslationOptions) # ######### Fields inheritance testing +@register(models.Slugged) class SluggedTranslationOptions(TranslationOptions): fields = ('slug',) +@register(models.MetaData) class MetaDataTranslationOptions(TranslationOptions): fields = ('keywords',) +@register(models.RichText) class RichTextTranslationOptions(TranslationOptions): fields = ('content',) +@register(models.Page) class PageTranslationOptions(TranslationOptions): fields = ('title',) # BasePage left unregistered intentionally. -translator.register(Slugged, SluggedTranslationOptions) -translator.register(MetaData, MetaDataTranslationOptions) -translator.register(RichText, RichTextTranslationOptions) -translator.register(Displayable) -translator.register(Page, PageTranslationOptions) -translator.register(RichTextPage) +translator.register(models.Displayable) +translator.register(models.RichTextPage) # ######### Manager testing +@register(models.ManagerTestModel) class ManagerTestModelTranslationOptions(TranslationOptions): fields = ('title', 'visits', 'description') -translator.register(ManagerTestModel, ManagerTestModelTranslationOptions) +@register([models.CustomManagerTestModel, models.CustomManager2TestModel]) class CustomManagerTestModelTranslationOptions(TranslationOptions): fields = ('title',) -translator.register([CustomManagerTestModel, CustomManager2TestModel], - CustomManagerTestModelTranslationOptions) # ######### TranslationOptions field inheritance testing @@ -171,51 +164,44 @@ class FieldInheritanceETranslationOptions(FieldInheritanceCTranslationOptions, # ######### Integration testing +@register(models.ThirdPartyRegisteredModel) class ThirdPartyTranslationOptions(TranslationOptions): fields = ('name',) -translator.register(ThirdPartyRegisteredModel, ThirdPartyTranslationOptions) # ######### Admin testing +@register(models.GroupFieldsetsModel) class GroupFieldsetsTranslationOptions(TranslationOptions): fields = ('title', 'text',) -translator.register(GroupFieldsetsModel, GroupFieldsetsTranslationOptions) +@register(models.NameModel) class NameTranslationOptions(TranslationOptions): fields = ('firstname', 'lastname', 'slug2') -translator.register(NameModel, NameTranslationOptions) # ######### Required fields testing +@register(models.RequiredModel) class RequiredTranslationOptions(TranslationOptions): fields = ('non_req', 'req', 'req_reg', 'req_en_reg') required_languages = { 'en': ('req_reg', 'req_en_reg',), 'default': ('req_reg',), # for all other languages } -translator.register(RequiredModel, RequiredTranslationOptions) - - -# ######### Decorated registration testing - -@register(DecoratedModel) -class DecoratedTranslationOptions(TranslationOptions): - fields = ('title',) # ######### Complex M2M with abstract classes and custom managers +@register(models.ModelX) class ModelXOptions(TranslationOptions): fields = ('name',) -translator.register(ModelX, ModelXOptions) +@register(models.ModelY) class ModelYOptions(TranslationOptions): fields = ('title',) -translator.register(ModelY, ModelYOptions) # ######### 3-rd party with custom manager From 786a876f0d449248d3ab842134150fb21e7c5bca Mon Sep 17 00:00:00 2001 From: Sergey Tereschenko Date: Sat, 16 Feb 2019 17:17:00 +0200 Subject: [PATCH 6/7] fix tests --- modeltranslation/tests/translation.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/modeltranslation/tests/translation.py b/modeltranslation/tests/translation.py index 0fb4272..438b173 100644 --- a/modeltranslation/tests/translation.py +++ b/modeltranslation/tests/translation.py @@ -102,28 +102,28 @@ class AbstractModelBTranslationOptions(TranslationOptions): # ######### Fields inheritance testing -@register(models.Slugged) class SluggedTranslationOptions(TranslationOptions): fields = ('slug',) -@register(models.MetaData) class MetaDataTranslationOptions(TranslationOptions): fields = ('keywords',) -@register(models.RichText) class RichTextTranslationOptions(TranslationOptions): fields = ('content',) -@register(models.Page) class PageTranslationOptions(TranslationOptions): fields = ('title',) # BasePage left unregistered intentionally. +translator.register(models.Slugged, SluggedTranslationOptions) +translator.register(models.MetaData, MetaDataTranslationOptions) +translator.register(models.RichText, RichTextTranslationOptions) translator.register(models.Displayable) +translator.register(models.Page, PageTranslationOptions) translator.register(models.RichTextPage) @@ -134,7 +134,10 @@ class ManagerTestModelTranslationOptions(TranslationOptions): fields = ('title', 'visits', 'description') -@register([models.CustomManagerTestModel, models.CustomManager2TestModel]) +@register([ + models.CustomManagerTestModel, + models.CustomManager2TestModel, +]) class CustomManagerTestModelTranslationOptions(TranslationOptions): fields = ('title',) From 6dfbe321fa5cb3b37fe317f1177482a298008f4f Mon Sep 17 00:00:00 2001 From: Sergey Tereschenko Date: Sun, 17 Feb 2019 19:21:15 +0200 Subject: [PATCH 7/7] tests: fix models count --- modeltranslation/tests/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modeltranslation/tests/tests.py b/modeltranslation/tests/tests.py index 1c7ed3b..7abb7f3 100644 --- a/modeltranslation/tests/tests.py +++ b/modeltranslation/tests/tests.py @@ -39,7 +39,7 @@ models = translation = None request = None # How many models are registered for tests. -TEST_MODELS = 31 + (1 if MIGRATIONS else 0) +TEST_MODELS = 30 + (1 if MIGRATIONS else 0) class reload_override_settings(override_settings):