From c55001ac12249a133ef6d253d005236914bd6556 Mon Sep 17 00:00:00 2001 From: seroy Date: Wed, 12 Apr 2017 13:55:42 +0300 Subject: [PATCH 01/29] Added test for "Apps aren't loaded yet." exception --- settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/settings.py b/settings.py index 4cdade5..2be9ca2 100644 --- a/settings.py +++ b/settings.py @@ -21,6 +21,7 @@ LANGUAGES = ( ) INSTALLED_APPS = ( + 'localized_fields', 'tests', ) From 8591af1f2a73b5b1e1b1bd8a8ba4caee4aceaa47 Mon Sep 17 00:00:00 2001 From: seroy Date: Wed, 12 Apr 2017 14:11:29 +0300 Subject: [PATCH 02/29] fixed "Apps aren't loaded yet." exception --- localized_fields/__init__.py | 2 -- tests/fake_model.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/localized_fields/__init__.py b/localized_fields/__init__.py index f9325cd..6d5f257 100644 --- a/localized_fields/__init__.py +++ b/localized_fields/__init__.py @@ -3,13 +3,11 @@ from .fields import (LocalizedAutoSlugField, LocalizedField, LocalizedUniqueSlugField) from .localized_value import LocalizedValue from .mixins import AtomicSlugRetryMixin -from .models import LocalizedModel from .util import get_language_codes __all__ = [ 'get_language_codes', 'LocalizedField', - 'LocalizedModel', 'LocalizedValue', 'LocalizedAutoSlugField', 'LocalizedUniqueSlugField', diff --git a/tests/fake_model.py b/tests/fake_model.py index d79e217..dc8cf8e 100644 --- a/tests/fake_model.py +++ b/tests/fake_model.py @@ -2,7 +2,7 @@ from django.db import connection, migrations from django.db.migrations.executor import MigrationExecutor from django.contrib.postgres.operations import HStoreExtension -from localized_fields import LocalizedModel +from localized_fields.models import LocalizedModel def define_fake_model(name='TestModel', fields=None): From fc80462ce7af59b6ade008471d5da73b4d9c0966 Mon Sep 17 00:00:00 2001 From: seroy Date: Thu, 13 Apr 2017 11:41:24 +0300 Subject: [PATCH 03/29] added test for str parameter to_python method --- tests/test_localized_field.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_localized_field.py b/tests/test_localized_field.py index 45d657f..7495770 100644 --- a/tests/test_localized_field.py +++ b/tests/test_localized_field.py @@ -1,3 +1,4 @@ +import json from django.conf import settings from django.db.utils import IntegrityError from django.test import TestCase @@ -232,6 +233,20 @@ class LocalizedFieldTestCase(TestCase): for lang_code, _ in settings.LANGUAGES: assert localized_value.get(lang_code) is None + @staticmethod + def test_to_python_str(): + """Tests whether the :see:to_python function produces + the expected :see:LocalizedValue when it is + passed serialized string value.""" + + serialized_str = json.dumps(get_init_values()) + localized_value = LocalizedField().to_python(serialized_str) + assert isinstance(localized_value, LocalizedValue) + + for language, value in get_init_values().items(): + assert localized_value.get(language) == value + assert getattr(localized_value, language) == value + @staticmethod def test_get_prep_value(): """"Tests whether the :see:get_prep_value function From f1798b0cc6680aaa8ed5821cd9ab6e8cbc3cec04 Mon Sep 17 00:00:00 2001 From: seroy Date: Thu, 13 Apr 2017 11:53:56 +0300 Subject: [PATCH 04/29] added ability to deserialize string value --- localized_fields/fields/localized_field.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/localized_fields/fields/localized_field.py b/localized_fields/fields/localized_field.py index 1f3d205..8798892 100644 --- a/localized_fields/fields/localized_field.py +++ b/localized_fields/fields/localized_field.py @@ -1,3 +1,5 @@ +from typing import Union + from django.conf import settings from django.db.utils import IntegrityError from django.utils import six, translation @@ -114,7 +116,7 @@ class LocalizedField(HStoreField): return cls.attr_class(value) - def to_python(self, value: dict) -> LocalizedValue: + def to_python(self, value: Union[dict, str, None]) -> LocalizedValue: """Turns the specified database value into its Python equivalent. @@ -127,7 +129,8 @@ class LocalizedField(HStoreField): A :see:LocalizedValue instance containing the data extracted from the database. """ - + # make deserialization if need by parent method + value = super(LocalizedField, self).to_python(value) if not value or not isinstance(value, dict): return self.attr_class() From cff22855c26b52537d1364caf7f8cbf9ddc948c5 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Thu, 25 May 2017 17:23:39 +0300 Subject: [PATCH 05/29] Revert "LocalizedUniqueSlugField refactored" This reverts commit 03df76d6d795f4453629bf526cc04a86c6393adc. --- README.rst | 3 +- .../fields/localized_uniqueslug_field.py | 120 ++++++------------ localized_fields/mixins.py | 47 +++++-- tests/fake_model.py | 3 +- 4 files changed, 75 insertions(+), 98 deletions(-) diff --git a/README.rst b/README.rst index bf92714..d70d996 100644 --- a/README.rst +++ b/README.rst @@ -193,10 +193,11 @@ Besides ``LocalizedField``, there's also: .. code-block:: python from localized_fields import (LocalizedModel, + AtomicSlugRetryMixin, LocalizedField, LocalizedUniqueSlugField) - class MyModel(LocalizedModel): + class MyModel(AtomicSlugRetryMixin, LocalizedModel): title = LocalizedField() slug = LocalizedUniqueSlugField(populate_from='title') diff --git a/localized_fields/fields/localized_uniqueslug_field.py b/localized_fields/fields/localized_uniqueslug_field.py index 120089a..a5ac993 100644 --- a/localized_fields/fields/localized_uniqueslug_field.py +++ b/localized_fields/fields/localized_uniqueslug_field.py @@ -1,19 +1,18 @@ from datetime import datetime from django.conf import settings -from django import forms from django.utils.text import slugify -from django.db import transaction -from django.db.utils import IntegrityError - +from django.core.exceptions import ImproperlyConfigured from ..util import get_language_codes +from ..mixins import AtomicSlugRetryMixin from ..localized_value import LocalizedValue -from .localized_field import LocalizedField +from .localized_autoslug_field import LocalizedAutoSlugField -class LocalizedUniqueSlugField(LocalizedField): - """Automatically provides slugs for a localized field upon saving." +class LocalizedUniqueSlugField(LocalizedAutoSlugField): + """Automatically provides slugs for a localized + field upon saving." An improved version of :see:LocalizedAutoSlugField, which adds: @@ -22,6 +21,8 @@ class LocalizedUniqueSlugField(LocalizedField): - Improved performance When in doubt, use this over :see:LocalizedAutoSlugField. + Inherit from :see:AtomicSlugRetryMixin in your model to + make this field work properly. """ def __init__(self, *args, **kwargs): @@ -29,11 +30,14 @@ class LocalizedUniqueSlugField(LocalizedField): kwargs['uniqueness'] = kwargs.pop('uniqueness', get_language_codes()) + super(LocalizedUniqueSlugField, self).__init__( + *args, + **kwargs + ) + self.populate_from = kwargs.pop('populate_from') self.include_time = kwargs.pop('include_time', False) - super().__init__(*args, **kwargs) - def deconstruct(self): """Deconstructs the field into something the database can store.""" @@ -45,88 +49,36 @@ class LocalizedUniqueSlugField(LocalizedField): kwargs['include_time'] = self.include_time return name, path, args, kwargs - def formfield(self, **kwargs): - """Gets the form field associated with this field. - - Because this is a slug field which is automatically - populated, it should be hidden from the form. - """ - - defaults = { - 'form_class': forms.CharField, - 'required': False - } - - defaults.update(kwargs) - - form_field = super().formfield(**defaults) - form_field.widget = forms.HiddenInput() - - return form_field - - def contribute_to_class(self, cls, name, *args, **kwargs): - """Hook that allow us to operate with model class. We overwrite save() - method to run retry logic. - - Arguments: - cls: - Model class. - - name: - Name of field in model. - """ - # apparently in inheritance cases, contribute_to_class is called more - # than once, so we have to be careful not to overwrite the original - # save method. - if not hasattr(cls, '_orig_save'): - cls._orig_save = cls.save - max_retries = getattr( - settings, - 'LOCALIZED_FIELDS_MAX_RETRIES', - 100 - ) - - def _new_save(instance, *args_, **kwargs_): - retries = 0 - while True: - with transaction.atomic(): - try: - slugs = self.populate_slugs(instance, retries) - setattr(instance, name, slugs) - instance._orig_save(*args_, **kwargs_) - break - except IntegrityError as e: - if retries >= max_retries: - raise e - # check to be sure a slug fight caused - # the IntegrityError - s_e = str(e) - if name in s_e and 'unique' in s_e: - retries += 1 - else: - raise e - - cls.save = _new_save - super().contribute_to_class(cls, name, *args, **kwargs) - - def populate_slugs(self, instance, retries=0): - """Built the slug from populate_from field. + def pre_save(self, instance, add: bool): + """Ran just before the model is saved, allows us to built + the slug. Arguments: instance: The model that is being saved. - retries: - The value of the current attempt. + add: + Indicates whether this is a new entry + to the database or an update. Returns: The localized slug that was generated. """ - slugs = LocalizedValue() - populates_slugs = getattr(instance, self.populate_from, {}) - for lang_code, _ in settings.LANGUAGES: - value = populates_slugs.get(lang_code) + if not isinstance(instance, AtomicSlugRetryMixin): + raise ImproperlyConfigured(( + 'Model \'%s\' does not inherit from AtomicSlugRetryMixin. ' + 'Without this, the LocalizedUniqueSlugField will not work.' + ) % type(instance).__name__) + + slugs = LocalizedValue() + + for lang_code, _ in settings.LANGUAGES: + value = self._get_populate_from_value( + instance, + self.populate_from, + lang_code + ) if not value: continue @@ -146,11 +98,13 @@ class LocalizedUniqueSlugField(LocalizedField): if self.include_time: slug += '-%d' % datetime.now().microsecond - if retries > 0: + if instance.retries > 0: # do not add another - if we already added time if not self.include_time: slug += '-' - slug += '%d' % retries + slug += '%d' % instance.retries slugs.set(lang_code, slug) + + setattr(instance, self.name, slugs) return slugs diff --git a/localized_fields/mixins.py b/localized_fields/mixins.py index dc3b2b2..1402715 100644 --- a/localized_fields/mixins.py +++ b/localized_fields/mixins.py @@ -1,17 +1,38 @@ -from django.core.checks import Warning +from django.db import transaction +from django.conf import settings +from django.db.utils import IntegrityError + class AtomicSlugRetryMixin: - """A Mixin keeped for backwards compatibility""" + """Makes :see:LocalizedUniqueSlugField work by retrying upon + violation of the UNIQUE constraint.""" - @classmethod - def check(cls, **kwargs): - errors = super().check(**kwargs) - errors.append( - Warning( - 'localized_fields.AtomicSlugRetryMixin is deprecated', - hint='There is no need to use ' - 'localized_fields.AtomicSlugRetryMixin', - obj=cls - ) + def save(self, *args, **kwargs): + """Saves this model instance to the database.""" + + max_retries = getattr( + settings, + 'LOCALIZED_FIELDS_MAX_RETRIES', + 100 ) - return errors + + if not hasattr(self, 'retries'): + self.retries = 0 + + with transaction.atomic(): + try: + return super().save(*args, **kwargs) + except IntegrityError as ex: + # this is as retarded as it looks, there's no + # way we can put the retry logic inside the slug + # field class... we can also not only catch exceptions + # that apply to slug fields... so yea.. this is as + # retarded as it gets... i am sorry :( + if 'slug' not in str(ex): + raise ex + + if self.retries >= max_retries: + raise ex + + self.retries += 1 + return self.save() diff --git a/tests/fake_model.py b/tests/fake_model.py index dc8cf8e..04e59d8 100644 --- a/tests/fake_model.py +++ b/tests/fake_model.py @@ -3,6 +3,7 @@ from django.db.migrations.executor import MigrationExecutor from django.contrib.postgres.operations import HStoreExtension from localized_fields.models import LocalizedModel +from localized_fields.mixins import AtomicSlugRetryMixin def define_fake_model(name='TestModel', fields=None): @@ -15,7 +16,7 @@ def define_fake_model(name='TestModel', fields=None): if fields: attributes.update(fields) - model = type(name, (LocalizedModel,), attributes) + model = type(name, (AtomicSlugRetryMixin,LocalizedModel,), attributes) return model From 093a9d58f2063ec9b683a3f50ddef2d32e70b306 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Thu, 25 May 2017 18:11:58 +0300 Subject: [PATCH 06/29] Fix to_python not working with non-json values --- localized_fields/fields/localized_field.py | 16 ++++++++++++---- tests/test_localized_field.py | 8 ++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/localized_fields/fields/localized_field.py b/localized_fields/fields/localized_field.py index 8798892..d90e23d 100644 --- a/localized_fields/fields/localized_field.py +++ b/localized_fields/fields/localized_field.py @@ -1,3 +1,5 @@ +import json + from typing import Union from django.conf import settings @@ -129,12 +131,18 @@ class LocalizedField(HStoreField): A :see:LocalizedValue instance containing the data extracted from the database. """ - # make deserialization if need by parent method - value = super(LocalizedField, self).to_python(value) - if not value or not isinstance(value, dict): + + # first let the base class handle the deserialization, this is in case we + # get specified a json string representing a dict + try: + deserialized_value = super(LocalizedField, self).to_python(value) + except json.JSONDecodeError: + deserialized_value = value + + if not deserialized_value: return self.attr_class() - return self.attr_class(value) + return self.attr_class(deserialized_value) def get_prep_value(self, value: LocalizedValue) -> dict: """Turns the specified value into something the database diff --git a/tests/test_localized_field.py b/tests/test_localized_field.py index 7495770..9d625ec 100644 --- a/tests/test_localized_field.py +++ b/tests/test_localized_field.py @@ -209,6 +209,14 @@ class LocalizedFieldTestCase(TestCase): for language, value in input_data.items(): assert localized_value.get(language) == value + @staticmethod + def test_to_python_non_json(): + """Tests whether the :see:to_python function + properly handles a string that is not JSON.""" + + localized_value = LocalizedField().to_python('my value') + assert localized_value.get() == 'my value' + @staticmethod def test_to_python_none(): """Tests whether the :see:to_python function From 3fcaece89429c7c84e22bb414cb690fa311a9f29 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Thu, 25 May 2017 18:14:41 +0300 Subject: [PATCH 07/29] Add missing docs to LocalizedField.contribute_to_class --- localized_fields/fields/localized_field.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/localized_fields/fields/localized_field.py b/localized_fields/fields/localized_field.py index d90e23d..316c9af 100644 --- a/localized_fields/fields/localized_field.py +++ b/localized_fields/fields/localized_field.py @@ -91,9 +91,18 @@ class LocalizedField(HStoreField): super(LocalizedField, self).__init__(*args, **kwargs) - def contribute_to_class(self, cls, name, **kwargs): - super(LocalizedField, self).contribute_to_class(cls, name, **kwargs) - setattr(cls, self.name, self.descriptor_class(self)) + def contribute_to_class(self, model, name, **kwargs): + """Adds this field to the specifed model. + + Arguments: + cls: + The model to add the field to. + + name: + The name of the field to add. + """ + super(LocalizedField, self).contribute_to_class(model, name, **kwargs) + setattr(model, self.name, self.descriptor_class(self)) @classmethod def from_db_value(cls, value, *_): From 2df2ec8b365dcc72e6ae52cb255a24c880aedd57 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Thu, 25 May 2017 18:45:21 +0300 Subject: [PATCH 08/29] Move LocalizedValueDescriptor into its own file --- localized_fields/fields/localized_field.py | 61 +------------------- localized_fields/localized_descriptor.py | 65 ++++++++++++++++++++++ 2 files changed, 66 insertions(+), 60 deletions(-) create mode 100644 localized_fields/localized_descriptor.py diff --git a/localized_fields/fields/localized_field.py b/localized_fields/fields/localized_field.py index 316c9af..29a23c8 100644 --- a/localized_fields/fields/localized_field.py +++ b/localized_fields/fields/localized_field.py @@ -4,71 +4,12 @@ from typing import Union from django.conf import settings from django.db.utils import IntegrityError -from django.utils import six, translation from psqlextra.fields import HStoreField from ..forms import LocalizedFieldForm from ..localized_value import LocalizedValue - - -class LocalizedValueDescriptor(object): - """ - The descriptor for the localized value attribute on the model instance. - Returns a :see:LocalizedValue when accessed so you can do stuff like:: - - >>> from myapp.models import MyModel - >>> instance = MyModel() - >>> instance.value.en = 'English value' - - Assigns a strings to active language key in :see:LocalizedValue on - assignment so you can do:: - - >>> from django.utils import translation - >>> from myapp.models import MyModel - - >>> translation.activate('nl') - >>> instance = MyModel() - >>> instance.title = 'dutch title' - >>> print(instance.title.nl) # prints 'dutch title' - """ - def __init__(self, field): - self.field = field - - def __get__(self, instance, cls=None): - if instance is None: - return self - - # This is slightly complicated, so worth an explanation. - # `instance.localizedvalue` needs to ultimately return some instance of - # `LocalizedValue`, probably a subclass. - - # The instance dict contains whatever was originally assigned - # in __set__. - if self.field.name in instance.__dict__: - value = instance.__dict__[self.field.name] - elif instance.pk is not None: - instance.refresh_from_db(fields=[self.field.name]) - value = getattr(instance, self.field.name) - else: - value = None - - if value is None: - attr = self.field.attr_class() - instance.__dict__[self.field.name] = attr - - if isinstance(value, dict): - attr = self.field.attr_class(value) - instance.__dict__[self.field.name] = attr - - return instance.__dict__[self.field.name] - - def __set__(self, instance, value): - if isinstance(value, six.string_types): - self.__get__(instance).set(translation.get_language() or - settings.LANGUAGE_CODE, value) - else: - instance.__dict__[self.field.name] = value +from ..localized_descriptor import LocalizedValueDescriptor class LocalizedField(HStoreField): diff --git a/localized_fields/localized_descriptor.py b/localized_fields/localized_descriptor.py new file mode 100644 index 0000000..5518b70 --- /dev/null +++ b/localized_fields/localized_descriptor.py @@ -0,0 +1,65 @@ +from django.conf import settings +from django.utils import six, translation + + +class LocalizedValueDescriptor: + """ + The descriptor for the localized value attribute on the model instance. + Returns a :see:LocalizedValue when accessed so you can do stuff like:: + + >>> from myapp.models import MyModel + >>> instance = MyModel() + >>> instance.value.en = 'English value' + + Assigns a strings to active language key in :see:LocalizedValue on + assignment so you can do:: + + >>> from django.utils import translation + >>> from myapp.models import MyModel + + >>> translation.activate('nl') + >>> instance = MyModel() + >>> instance.title = 'dutch title' + >>> print(instance.title.nl) # prints 'dutch title' + """ + + def __init__(self, field): + """Initializes a new instance of :see:LocalizedValueDescriptor.""" + + self.field = field + + def __get__(self, instance, cls=None): + if instance is None: + return self + + # This is slightly complicated, so worth an explanation. + # `instance.localizedvalue` needs to ultimately return some instance of + # `LocalizedValue`, probably a subclass. + + # The instance dict contains whatever was originally assigned + # in __set__. + + if self.field.name in instance.__dict__: + value = instance.__dict__[self.field.name] + elif instance.pk is not None: + instance.refresh_from_db(fields=[self.field.name]) + value = getattr(instance, self.field.name) + else: + value = None + + if value is None: + attr = self.field.attr_class() + instance.__dict__[self.field.name] = attr + + if isinstance(value, dict): + attr = self.field.attr_class(value) + instance.__dict__[self.field.name] = attr + + return instance.__dict__[self.field.name] + + def __set__(self, instance, value): + if isinstance(value, six.string_types): + self.__get__(instance).set(translation.get_language() or + settings.LANGUAGE_CODE, value) + else: + instance.__dict__[self.field.name] = value From 5e0343801f59d168ea515f8cd23464b78296cf24 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Thu, 25 May 2017 18:50:20 +0300 Subject: [PATCH 09/29] Restore LocalizedModel to its former glory as an convient way to get all the mixins --- localized_fields/models.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/localized_fields/models.py b/localized_fields/models.py index 6d9faa8..e298f04 100644 --- a/localized_fields/models.py +++ b/localized_fields/models.py @@ -1,21 +1,16 @@ -from django.db import models -from django.core.checks import Warning +from psqlextra.models import PostgresModel + +from .mixins import AtomicSlugRetryMixin -class LocalizedModel(models.Model): - """A model keeped for backwards compatibility""" +class LocalizedModel(AtomicSlugRetryMixin, PostgresModel): + """Turns a model into a model that contains LocalizedField's. - @classmethod - def check(cls, **kwargs): - errors = super().check(**kwargs) - errors.append( - Warning( - 'localized_fields.LocalizedModel is deprecated', - hint='There is no need to use localized_fields.LocalizedModel', - obj=cls - ) - ) - return errors + For basic localisation functionality, it isn't needed to inherit + from LocalizedModel. However, for certain features, this is required. + + It is definitely needed for :see:LocalizedUniqueSlugField, unless you + manually inherit from AtomicSlugRetryMixin.""" class Meta: abstract = True From bb84d7577c27f25498394c20b8db258286e93123 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Thu, 25 May 2017 18:51:11 +0300 Subject: [PATCH 10/29] BREAKING CHANGE: Empty out __init__ It is bad practice to do this in Django. If somebody imports something from the package before Django is loaded, you get a 'Apps aren't loaded yet.' exception. --- localized_fields/__init__.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/localized_fields/__init__.py b/localized_fields/__init__.py index 6d5f257..e69de29 100644 --- a/localized_fields/__init__.py +++ b/localized_fields/__init__.py @@ -1,26 +0,0 @@ -from .forms import LocalizedFieldForm, LocalizedFieldWidget -from .fields import (LocalizedAutoSlugField, LocalizedField, - LocalizedUniqueSlugField) -from .localized_value import LocalizedValue -from .mixins import AtomicSlugRetryMixin -from .util import get_language_codes - -__all__ = [ - 'get_language_codes', - 'LocalizedField', - 'LocalizedValue', - 'LocalizedAutoSlugField', - 'LocalizedUniqueSlugField', - 'LocalizedBleachField', - 'LocalizedFieldWidget', - 'LocalizedFieldForm', - 'AtomicSlugRetryMixin' -] - -try: - from .fields import LocalizedBleachField - __all__ += [ - 'LocalizedBleachField' - ] -except ImportError: - pass From a1a02552b7c95ca19de032bb319a6aca96c7d6d9 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Thu, 25 May 2017 19:06:44 +0300 Subject: [PATCH 11/29] Shorten names for everything --- README.rst | 1 + localized_fields/admin.py | 2 +- .../{localized_descriptor.py => descriptor.py} | 0 localized_fields/fields/__init__.py | 8 ++++---- .../{localized_autoslug_field.py => autoslug_field.py} | 4 ++-- .../fields/{localized_bleach_field.py => bleach_field.py} | 3 ++- localized_fields/fields/{localized_field.py => field.py} | 4 ++-- ...{localized_uniqueslug_field.py => uniqueslug_field.py} | 4 ++-- localized_fields/forms.py | 2 +- localized_fields/{localized_value.py => value.py} | 0 localized_fields/widgets.py | 2 +- tests/fake_model.py | 2 +- ...est_localized_bleach_field.py => test_bleach_field.py} | 6 ++++-- tests/{test_localized_field.py => test_field.py} | 5 ++++- tests/{test_localized_field_form.py => test_form.py} | 2 +- tests/{test_localized_model.py => test_model.py} | 3 ++- ...{test_localized_slug_fields.py => test_slug_fields.py} | 8 ++++++-- tests/{test_localized_field_widget.py => test_widget.py} | 3 ++- 18 files changed, 36 insertions(+), 23 deletions(-) rename localized_fields/{localized_descriptor.py => descriptor.py} (100%) rename localized_fields/fields/{localized_autoslug_field.py => autoslug_field.py} (97%) rename localized_fields/fields/{localized_bleach_field.py => bleach_field.py} (95%) rename localized_fields/fields/{localized_field.py => field.py} (98%) rename localized_fields/fields/{localized_uniqueslug_field.py => uniqueslug_field.py} (96%) rename localized_fields/{localized_value.py => value.py} (100%) rename tests/{test_localized_bleach_field.py => test_bleach_field.py} (96%) rename tests/{test_localized_field.py => test_field.py} (98%) rename tests/{test_localized_field_form.py => test_form.py} (94%) rename tests/{test_localized_model.py => test_model.py} (93%) rename tests/{test_localized_slug_fields.py => test_slug_fields.py} (98%) rename tests/{test_localized_field_widget.py => test_widget.py} (92%) diff --git a/README.rst b/README.rst index d70d996..4c8e6bd 100644 --- a/README.rst +++ b/README.rst @@ -228,6 +228,7 @@ Besides ``LocalizedField``, there's also: * ``LocalizedBleachField`` Automatically bleaches the content of the field. + * django-bleach Example usage: diff --git a/localized_fields/admin.py b/localized_fields/admin.py index fdb2227..5677de6 100644 --- a/localized_fields/admin.py +++ b/localized_fields/admin.py @@ -1,7 +1,7 @@ from django.contrib.admin import ModelAdmin -from .fields import LocalizedField from . import widgets +from .fields import LocalizedField FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS = { diff --git a/localized_fields/localized_descriptor.py b/localized_fields/descriptor.py similarity index 100% rename from localized_fields/localized_descriptor.py rename to localized_fields/descriptor.py diff --git a/localized_fields/fields/__init__.py b/localized_fields/fields/__init__.py index a634894..d23188e 100644 --- a/localized_fields/fields/__init__.py +++ b/localized_fields/fields/__init__.py @@ -1,6 +1,6 @@ -from .localized_field import LocalizedField -from .localized_autoslug_field import LocalizedAutoSlugField -from .localized_uniqueslug_field import LocalizedUniqueSlugField +from .field import LocalizedField +from .autoslug_field import LocalizedAutoSlugField +from .uniqueslug_field import LocalizedUniqueSlugField __all__ = [ @@ -10,7 +10,7 @@ __all__ = [ ] try: - from .localized_bleach_field import LocalizedBleachField + from .bleach_field import LocalizedBleachField __all__ += [ 'LocalizedBleachField' ] diff --git a/localized_fields/fields/localized_autoslug_field.py b/localized_fields/fields/autoslug_field.py similarity index 97% rename from localized_fields/fields/localized_autoslug_field.py rename to localized_fields/fields/autoslug_field.py index 8ce26db..d7ef10c 100644 --- a/localized_fields/fields/localized_autoslug_field.py +++ b/localized_fields/fields/autoslug_field.py @@ -5,8 +5,8 @@ from django import forms from django.conf import settings from django.utils.text import slugify -from .localized_field import LocalizedField -from ..localized_value import LocalizedValue +from .field import LocalizedField +from ..value import LocalizedValue class LocalizedAutoSlugField(LocalizedField): diff --git a/localized_fields/fields/localized_bleach_field.py b/localized_fields/fields/bleach_field.py similarity index 95% rename from localized_fields/fields/localized_bleach_field.py rename to localized_fields/fields/bleach_field.py index 92fad99..3535de7 100644 --- a/localized_fields/fields/localized_bleach_field.py +++ b/localized_fields/fields/bleach_field.py @@ -1,8 +1,9 @@ import bleach + from django.conf import settings from django_bleach.utils import get_bleach_default_options -from .localized_field import LocalizedField +from .field import LocalizedField class LocalizedBleachField(LocalizedField): diff --git a/localized_fields/fields/localized_field.py b/localized_fields/fields/field.py similarity index 98% rename from localized_fields/fields/localized_field.py rename to localized_fields/fields/field.py index 29a23c8..4b476f6 100644 --- a/localized_fields/fields/localized_field.py +++ b/localized_fields/fields/field.py @@ -8,8 +8,8 @@ from django.db.utils import IntegrityError from psqlextra.fields import HStoreField from ..forms import LocalizedFieldForm -from ..localized_value import LocalizedValue -from ..localized_descriptor import LocalizedValueDescriptor +from ..value import LocalizedValue +from ..descriptor import LocalizedValueDescriptor class LocalizedField(HStoreField): diff --git a/localized_fields/fields/localized_uniqueslug_field.py b/localized_fields/fields/uniqueslug_field.py similarity index 96% rename from localized_fields/fields/localized_uniqueslug_field.py rename to localized_fields/fields/uniqueslug_field.py index a5ac993..2b2cbe1 100644 --- a/localized_fields/fields/localized_uniqueslug_field.py +++ b/localized_fields/fields/uniqueslug_field.py @@ -4,10 +4,10 @@ from django.conf import settings from django.utils.text import slugify from django.core.exceptions import ImproperlyConfigured +from .autoslug_field import LocalizedAutoSlugField from ..util import get_language_codes from ..mixins import AtomicSlugRetryMixin -from ..localized_value import LocalizedValue -from .localized_autoslug_field import LocalizedAutoSlugField +from ..value import LocalizedValue class LocalizedUniqueSlugField(LocalizedAutoSlugField): diff --git a/localized_fields/forms.py b/localized_fields/forms.py index 2475673..86d35b3 100644 --- a/localized_fields/forms.py +++ b/localized_fields/forms.py @@ -3,7 +3,7 @@ from typing import List from django import forms from django.conf import settings -from .localized_value import LocalizedValue +from .value import LocalizedValue from .widgets import LocalizedFieldWidget diff --git a/localized_fields/localized_value.py b/localized_fields/value.py similarity index 100% rename from localized_fields/localized_value.py rename to localized_fields/value.py diff --git a/localized_fields/widgets.py b/localized_fields/widgets.py index 36e9280..ade2b6f 100644 --- a/localized_fields/widgets.py +++ b/localized_fields/widgets.py @@ -5,7 +5,7 @@ from django import forms from django.contrib.admin import widgets from django.template.loader import render_to_string -from .localized_value import LocalizedValue +from .value import LocalizedValue class LocalizedFieldWidget(forms.MultiWidget): diff --git a/tests/fake_model.py b/tests/fake_model.py index 04e59d8..5456219 100644 --- a/tests/fake_model.py +++ b/tests/fake_model.py @@ -16,7 +16,7 @@ def define_fake_model(name='TestModel', fields=None): if fields: attributes.update(fields) - model = type(name, (AtomicSlugRetryMixin,LocalizedModel,), attributes) + model = type(name, (LocalizedModel,), attributes) return model diff --git a/tests/test_localized_bleach_field.py b/tests/test_bleach_field.py similarity index 96% rename from tests/test_localized_bleach_field.py rename to tests/test_bleach_field.py index 765a507..0901d3f 100644 --- a/tests/test_localized_bleach_field.py +++ b/tests/test_bleach_field.py @@ -1,9 +1,11 @@ +import bleach + from django.conf import settings from django.test import TestCase from django_bleach.utils import get_bleach_default_options -import bleach -from localized_fields import LocalizedBleachField, LocalizedValue +from localized_fields.fields import LocalizedBleachField +from localized_fields.value import LocalizedValue class TestModel: diff --git a/tests/test_localized_field.py b/tests/test_field.py similarity index 98% rename from tests/test_localized_field.py rename to tests/test_field.py index 9d625ec..7af76c8 100644 --- a/tests/test_localized_field.py +++ b/tests/test_field.py @@ -1,10 +1,13 @@ import json + from django.conf import settings from django.db.utils import IntegrityError from django.test import TestCase from django.utils import translation -from localized_fields import LocalizedField, LocalizedFieldForm, LocalizedValue +from localized_fields.fields import LocalizedField +from localized_fields.forms import LocalizedFieldForm +from localized_fields.value import LocalizedValue def get_init_values() -> dict: diff --git a/tests/test_localized_field_form.py b/tests/test_form.py similarity index 94% rename from tests/test_localized_field_form.py rename to tests/test_form.py index abc79c7..a5b1157 100644 --- a/tests/test_localized_field_form.py +++ b/tests/test_form.py @@ -1,7 +1,7 @@ from django.conf import settings from django.test import TestCase -from localized_fields import LocalizedFieldForm +from localized_fields.forms import LocalizedFieldForm class LocalizedFieldFormTestCase(TestCase): diff --git a/tests/test_localized_model.py b/tests/test_model.py similarity index 93% rename from tests/test_localized_model.py rename to tests/test_model.py index 908684c..f9c31ce 100644 --- a/tests/test_localized_model.py +++ b/tests/test_model.py @@ -1,6 +1,7 @@ from django.test import TestCase -from localized_fields import LocalizedField, LocalizedValue +from localized_fields.fields import LocalizedField +from localized_fields.value import LocalizedValue from .fake_model import get_fake_model diff --git a/tests/test_localized_slug_fields.py b/tests/test_slug_fields.py similarity index 98% rename from tests/test_localized_slug_fields.py rename to tests/test_slug_fields.py index 51fac26..a2f25ad 100644 --- a/tests/test_localized_slug_fields.py +++ b/tests/test_slug_fields.py @@ -4,10 +4,14 @@ from django import forms from django.conf import settings from django.test import TestCase from django.db.utils import IntegrityError -from localized_fields import (LocalizedField, LocalizedAutoSlugField, - LocalizedUniqueSlugField) from django.utils.text import slugify +from localized_fields.fields import ( + LocalizedField, + LocalizedAutoSlugField, + LocalizedUniqueSlugField +) + from .fake_model import get_fake_model diff --git a/tests/test_localized_field_widget.py b/tests/test_widget.py similarity index 92% rename from tests/test_localized_field_widget.py rename to tests/test_widget.py index 2e65ac8..a1ed753 100644 --- a/tests/test_localized_field_widget.py +++ b/tests/test_widget.py @@ -1,7 +1,8 @@ from django.conf import settings from django.test import TestCase -from localized_fields import LocalizedFieldWidget, LocalizedValue +from localized_fields.value import LocalizedValue +from localized_fields.widgets import LocalizedFieldWidget class LocalizedFieldWidgetTestCase(TestCase): From 84c267330fb7b84a7c9f688fe8c5885f52b83589 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Thu, 25 May 2017 19:11:39 +0300 Subject: [PATCH 12/29] Update docs on new import style --- README.rst | 16 ++++++---------- localized_fields/admin.py | 5 +++++ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index 4c8e6bd..dc722ec 100644 --- a/README.rst +++ b/README.rst @@ -160,7 +160,7 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e .. code-block:: python - from localized_fields import get_language_codes + from localized_fields.util import get_language_codes class MyModel(models.Model): title = LocalizedField(uniqueness=get_language_codes()) @@ -176,7 +176,7 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e .. code-block:: python - from localized_fields import get_language_codes + from localized_fields.util import get_language_codes class MyModel(models.Model): title = LocalizedField(uniqueness=[(*get_language_codes())]) @@ -192,10 +192,8 @@ Besides ``LocalizedField``, there's also: .. code-block:: python - from localized_fields import (LocalizedModel, - AtomicSlugRetryMixin, - LocalizedField, - LocalizedUniqueSlugField) + from localized_fields.models import LocalizedModel + from localized_fields.fields import LocalizedField, LocalizedUniqueSlugField class MyModel(AtomicSlugRetryMixin, LocalizedModel): title = LocalizedField() @@ -217,8 +215,7 @@ Besides ``LocalizedField``, there's also: .. code-block:: python - from localized_fields import (LocalizedField, - LocalizedUniqueSlugField) + from localized_fields.fields import LocalizedField, LocalizedAutoSlugField class MyModel(models.Model): title = LocalizedField() @@ -235,8 +232,7 @@ Besides ``LocalizedField``, there's also: .. code-block:: python - from localized_fields import (LocalizedField, - LocalizedBleachField) + from localized_fields.fields import LocalizedField, LocalizedBleachField class MyModel(models.Model): title = LocalizedField() diff --git a/localized_fields/admin.py b/localized_fields/admin.py index 5677de6..4c54303 100644 --- a/localized_fields/admin.py +++ b/localized_fields/admin.py @@ -10,17 +10,22 @@ FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS = { class LocalizedFieldsAdminMixin(ModelAdmin): + """Mixin for making the fancy widgets work in Django Admin.""" + class Media: css = { 'all': ( 'localized_fields/localized-fields-admin.css', ) } + js = ( 'localized_fields/localized-fields-admin.js', ) def __init__(self, *args, **kwargs): + """Initializes a new instance of :see:LocalizedFieldsAdminMixin.""" + super(LocalizedFieldsAdminMixin, self).__init__(*args, **kwargs) overrides = FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS.copy() overrides.update(self.formfield_overrides) From 2205f9c6a4f24fd01604bbdb9b50eb6308494162 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Thu, 25 May 2017 19:16:04 +0300 Subject: [PATCH 13/29] Move LocalizedValueTest into dedicated file --- tests/data.py | 13 ++++ tests/test_field.py | 157 +------------------------------------------- tests/test_value.py | 155 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+), 156 deletions(-) create mode 100644 tests/data.py create mode 100644 tests/test_value.py diff --git a/tests/data.py b/tests/data.py new file mode 100644 index 0000000..d074ca1 --- /dev/null +++ b/tests/data.py @@ -0,0 +1,13 @@ +from django.conf import settings + + +def get_init_values() -> dict: + """Gets a test dictionary containing a key + for every language.""" + + keys = {} + + for lang_code, lang_name in settings.LANGUAGES: + keys[lang_code] = 'value in %s' % lang_name + + return keys diff --git a/tests/test_field.py b/tests/test_field.py index 7af76c8..7538c07 100644 --- a/tests/test_field.py +++ b/tests/test_field.py @@ -9,162 +9,7 @@ from localized_fields.fields import LocalizedField from localized_fields.forms import LocalizedFieldForm from localized_fields.value import LocalizedValue - -def get_init_values() -> dict: - """Gets a test dictionary containing a key - for every language.""" - - keys = {} - - for lang_code, lang_name in settings.LANGUAGES: - keys[lang_code] = 'value in %s' % lang_name - - return keys - - -class LocalizedValueTestCase(TestCase): - """Tests the :see:LocalizedValue class.""" - - @staticmethod - def tearDown(): - """Assures that the current language - is set back to the default.""" - - translation.activate(settings.LANGUAGE_CODE) - - @staticmethod - def test_init(): - """Tests whether the __init__ function - of the :see:LocalizedValue class works - as expected.""" - - keys = get_init_values() - value = LocalizedValue(keys) - - for lang_code, _ in settings.LANGUAGES: - assert getattr(value, lang_code, None) == keys[lang_code] - - @staticmethod - def test_init_default_values(): - """Tests wehther the __init__ function - of the :see:LocalizedValue accepts the - default value or an empty dict properly.""" - - value = LocalizedValue() - - for lang_code, _ in settings.LANGUAGES: - assert getattr(value, lang_code) is None - - @staticmethod - def test_get_explicit(): - """Tests whether the the :see:LocalizedValue - class's :see:get function works properly - when specifying an explicit value.""" - - keys = get_init_values() - localized_value = LocalizedValue(keys) - - for language, value in keys.items(): - assert localized_value.get(language) == value - - @staticmethod - def test_get_default_language(): - """Tests whether the :see:LocalizedValue - class's see:get function properly - gets the value in the default language.""" - - keys = get_init_values() - localized_value = LocalizedValue(keys) - - for language, _ in keys.items(): - translation.activate(language) - assert localized_value.get() == keys[settings.LANGUAGE_CODE] - - @staticmethod - def test_set(): - """Tests whether the :see:LocalizedValue - class's see:set function works properly.""" - - localized_value = LocalizedValue() - - for language, value in get_init_values(): - localized_value.set(language, value) - assert localized_value.get(language) == value - assert getattr(localized_value, language) == value - - @staticmethod - def test_str(): - """Tests whether the :see:LocalizedValue - class's __str__ works properly.""" - - keys = get_init_values() - localized_value = LocalizedValue(keys) - - for language, value in keys.items(): - translation.activate(language) - assert str(localized_value) == value - - @staticmethod - def test_eq(): - """Tests whether the __eq__ operator - of :see:LocalizedValue works properly.""" - - a = LocalizedValue({'en': 'a', 'ar': 'b'}) - b = LocalizedValue({'en': 'a', 'ar': 'b'}) - - assert a == b - - b.en = 'b' - assert a != b - - @staticmethod - def test_str_fallback(): - """Tests whether the :see:LocalizedValue - class's __str__'s fallback functionality - works properly.""" - - test_value = 'myvalue' - - localized_value = LocalizedValue({ - settings.LANGUAGE_CODE: test_value - }) - - other_language = settings.LANGUAGES[-1][0] - - # make sure that, by default it returns - # the value in the default language - assert str(localized_value) == test_value - - # make sure that it falls back to the - # primary language when there's no value - # available in the current language - translation.activate(other_language) - assert str(localized_value) == test_value - - # make sure that it's just __str__ falling - # back and that for the other language - # there's no actual value - assert localized_value.get(other_language) != test_value - - @staticmethod - def test_deconstruct(): - """Tests whether the :see:LocalizedValue - class's :see:deconstruct function works properly.""" - - keys = get_init_values() - value = LocalizedValue(keys) - - path, args, kwargs = value.deconstruct() - - assert args[0] == keys - - @staticmethod - def test_construct_string(): - """Tests whether the :see:LocalizedValue's constructor - assumes the primary language when passing a single string.""" - - value = LocalizedValue('beer') - assert value.get(settings.LANGUAGE_CODE) == 'beer' +from .data import get_init_values class LocalizedFieldTestCase(TestCase): diff --git a/tests/test_value.py b/tests/test_value.py new file mode 100644 index 0000000..1289ac1 --- /dev/null +++ b/tests/test_value.py @@ -0,0 +1,155 @@ +import json + +from django.conf import settings +from django.test import TestCase +from django.utils import translation + +from localized_fields.value import LocalizedValue + +from .data import get_init_values + + +class LocalizedValueTestCase(TestCase): + """Tests the :see:LocalizedValue class.""" + + @staticmethod + def tearDown(): + """Assures that the current language + is set back to the default.""" + + translation.activate(settings.LANGUAGE_CODE) + + @staticmethod + def test_init(): + """Tests whether the __init__ function + of the :see:LocalizedValue class works + as expected.""" + + keys = get_init_values() + value = LocalizedValue(keys) + + for lang_code, _ in settings.LANGUAGES: + assert getattr(value, lang_code, None) == keys[lang_code] + + @staticmethod + def test_init_default_values(): + """Tests wehther the __init__ function + of the :see:LocalizedValue accepts the + default value or an empty dict properly.""" + + value = LocalizedValue() + + for lang_code, _ in settings.LANGUAGES: + assert getattr(value, lang_code) is None + + @staticmethod + def test_get_explicit(): + """Tests whether the the :see:LocalizedValue + class's :see:get function works properly + when specifying an explicit value.""" + + keys = get_init_values() + localized_value = LocalizedValue(keys) + + for language, value in keys.items(): + assert localized_value.get(language) == value + + @staticmethod + def test_get_default_language(): + """Tests whether the :see:LocalizedValue + class's see:get function properly + gets the value in the default language.""" + + keys = get_init_values() + localized_value = LocalizedValue(keys) + + for language, _ in keys.items(): + translation.activate(language) + assert localized_value.get() == keys[settings.LANGUAGE_CODE] + + @staticmethod + def test_set(): + """Tests whether the :see:LocalizedValue + class's see:set function works properly.""" + + localized_value = LocalizedValue() + + for language, value in get_init_values(): + localized_value.set(language, value) + assert localized_value.get(language) == value + assert getattr(localized_value, language) == value + + @staticmethod + def test_str(): + """Tests whether the :see:LocalizedValue + class's __str__ works properly.""" + + keys = get_init_values() + localized_value = LocalizedValue(keys) + + for language, value in keys.items(): + translation.activate(language) + assert str(localized_value) == value + + @staticmethod + def test_eq(): + """Tests whether the __eq__ operator + of :see:LocalizedValue works properly.""" + + a = LocalizedValue({'en': 'a', 'ar': 'b'}) + b = LocalizedValue({'en': 'a', 'ar': 'b'}) + + assert a == b + + b.en = 'b' + assert a != b + + @staticmethod + def test_str_fallback(): + """Tests whether the :see:LocalizedValue + class's __str__'s fallback functionality + works properly.""" + + test_value = 'myvalue' + + localized_value = LocalizedValue({ + settings.LANGUAGE_CODE: test_value + }) + + other_language = settings.LANGUAGES[-1][0] + + # make sure that, by default it returns + # the value in the default language + assert str(localized_value) == test_value + + # make sure that it falls back to the + # primary language when there's no value + # available in the current language + translation.activate(other_language) + assert str(localized_value) == test_value + + # make sure that it's just __str__ falling + # back and that for the other language + # there's no actual value + assert localized_value.get(other_language) != test_value + + @staticmethod + def test_deconstruct(): + """Tests whether the :see:LocalizedValue + class's :see:deconstruct function works properly.""" + + keys = get_init_values() + value = LocalizedValue(keys) + + path, args, kwargs = value.deconstruct() + + assert args[0] == keys + + @staticmethod + def test_construct_string(): + """Tests whether the :see:LocalizedValue's constructor + assumes the primary language when passing a single string.""" + + value = LocalizedValue('beer') + assert value.get(settings.LANGUAGE_CODE) == 'beer' + From 0fa79ddbb043cdedc31fe69ca3fe39a7d7a86df1 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Thu, 25 May 2017 19:16:35 +0300 Subject: [PATCH 14/29] Bump version to 4.0 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 898e656..1fd6934 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: setup( name='django-localized-fields', - version='3.6', + version='4.0', packages=find_packages(), include_package_data=True, license='MIT License', @@ -18,7 +18,7 @@ setup( author_email='open-source@sectorlabs.ro', keywords=['django', 'localized', 'language', 'models', 'fields'], install_requires=[ - 'django-postgres-extra>=1.4' + 'django-postgres-extra>=1.9' ], classifiers=[ 'Environment :: Web Environment', From 5a4f449363a489df30f6999c36e098b8ec809ab3 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Thu, 25 May 2017 19:23:52 +0300 Subject: [PATCH 15/29] Fix support for ArrayAgg --- localized_fields/value.py | 43 +++++++++++++++++++++++++++++++++------ tests/test_value.py | 14 ++++++++++++- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/localized_fields/value.py b/localized_fields/value.py index 6bea382..50714bc 100644 --- a/localized_fields/value.py +++ b/localized_fields/value.py @@ -1,3 +1,5 @@ +import collections + from django.conf import settings from django.utils import translation @@ -15,12 +17,7 @@ class LocalizedValue(dict): different language. """ - if isinstance(keys, str): - setattr(self, settings.LANGUAGE_CODE, keys) - else: - for lang_code, _ in settings.LANGUAGES: - value = keys.get(lang_code) if keys else None - self.set(lang_code, value) + self._interpret_value(keys); def get(self, language: str=None) -> str: """Gets the underlying value in the specified or @@ -65,6 +62,40 @@ class LocalizedValue(dict): path = 'localized_fields.fields.LocalizedValue' return path, [self.__dict__], {} + def _interpret_value(self, value): + """Interprets a value passed in the constructor as + a :see:LocalizedValue. + + If string: + Assumes it's the default language. + + If dict: + Each key is a language and the value a string + in that language. + + If list: + Recurse into to apply rules above. + + Arguments: + value: + The value to interpret. + """ + + for lang_code, _ in settings.LANGUAGES: + self.set(lang_code, None) + + if isinstance(value, str): + self.set(settings.LANGUAGE_CODE, value) + + elif isinstance(value, dict): + for lang_code, _ in settings.LANGUAGES: + lang_value = value.get(lang_code) or None + self.set(lang_code, lang_value) + + elif isinstance(value, collections.Iterable): + for val in value: + self._interpret_value(val) + def __str__(self) -> str: """Gets the value in the current language, or falls back to the primary language if there's no value diff --git a/tests/test_value.py b/tests/test_value.py index 1289ac1..7d4bd3e 100644 --- a/tests/test_value.py +++ b/tests/test_value.py @@ -33,7 +33,7 @@ class LocalizedValueTestCase(TestCase): @staticmethod def test_init_default_values(): - """Tests wehther the __init__ function + """Tests whether the __init__ function of the :see:LocalizedValue accepts the default value or an empty dict properly.""" @@ -42,6 +42,18 @@ class LocalizedValueTestCase(TestCase): for lang_code, _ in settings.LANGUAGES: assert getattr(value, lang_code) is None + @staticmethod + def test_init_array(): + """Tests whether the __init__ function + of :see:LocalizedValue properly handles an + array. + + Arrays can be passed to LocalizedValue as + a result of a ArrayAgg operation.""" + + value = LocalizedValue(['my value']) + assert value.get(settings.LANGUAGE_CODE) == 'my value' + @staticmethod def test_get_explicit(): """Tests whether the the :see:LocalizedValue From 92a53bc3d76b8655330f4e06e78362bfa12f870c Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Thu, 25 May 2017 19:40:03 +0300 Subject: [PATCH 16/29] Fix various pep8/flake8/pylint errors --- localized_fields/descriptor.py | 4 +- localized_fields/fields/autoslug_field.py | 34 +++-- localized_fields/fields/uniqueslug_field.py | 9 +- localized_fields/forms.py | 3 +- localized_fields/hstore_index.py | 132 -------------------- localized_fields/value.py | 3 +- localized_fields/widgets.py | 14 +-- tests/fake_model.py | 1 - tests/test_field.py | 1 - tests/test_model.py | 1 - tests/test_value.py | 3 - 11 files changed, 39 insertions(+), 166 deletions(-) delete mode 100644 localized_fields/hstore_index.py diff --git a/localized_fields/descriptor.py b/localized_fields/descriptor.py index 5518b70..65f8512 100644 --- a/localized_fields/descriptor.py +++ b/localized_fields/descriptor.py @@ -59,7 +59,7 @@ class LocalizedValueDescriptor: def __set__(self, instance, value): if isinstance(value, six.string_types): - self.__get__(instance).set(translation.get_language() or - settings.LANGUAGE_CODE, value) + language = translation.get_language() or settings.LANGUAGE_CODE + self.__get__(instance).set(language, value) # pylint: disable=no-member else: instance.__dict__[self.field.name] = value diff --git a/localized_fields/fields/autoslug_field.py b/localized_fields/fields/autoslug_field.py index d7ef10c..537aa46 100644 --- a/localized_fields/fields/autoslug_field.py +++ b/localized_fields/fields/autoslug_field.py @@ -1,4 +1,4 @@ -from typing import Callable +from typing import Callable, Tuple from datetime import datetime from django import forms @@ -69,13 +69,7 @@ class LocalizedAutoSlugField(LocalizedField): slugs = LocalizedValue() - for lang_code, _ in settings.LANGUAGES: - value = self._get_populate_from_value( - instance, - self.populate_from, - lang_code - ) - + for lang_code, value in self._get_populate_values(instance): if not value: continue @@ -128,6 +122,30 @@ class LocalizedAutoSlugField(LocalizedField): return unique_slug + def _get_populate_values(self, instance) -> Tuple[str, str]: + """Gets all values (for each language) from the + specified's instance's `populate_from` field. + + Arguments: + instance: + The instance to get the values from. + + Returns: + A list of (lang_code, value) tuples. + """ + + return [ + ( + lang_code, + self._get_populate_from_value( + instance, + self.populate_from, + lang_code + ), + ) + for lang_code, _ in settings.LANGUAGES + ] + @staticmethod def _get_populate_from_value(instance, field_name: str, language: str): """Gets the value to create a slug from in the specified language. diff --git a/localized_fields/fields/uniqueslug_field.py b/localized_fields/fields/uniqueslug_field.py index 2b2cbe1..ab4cd30 100644 --- a/localized_fields/fields/uniqueslug_field.py +++ b/localized_fields/fields/uniqueslug_field.py @@ -1,6 +1,5 @@ from datetime import datetime -from django.conf import settings from django.utils.text import slugify from django.core.exceptions import ImproperlyConfigured @@ -73,13 +72,7 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField): slugs = LocalizedValue() - for lang_code, _ in settings.LANGUAGES: - value = self._get_populate_from_value( - instance, - self.populate_from, - lang_code - ) - + for lang_code, value in self._get_populate_values(instance): if not value: continue diff --git a/localized_fields/forms.py b/localized_fields/forms.py index 86d35b3..386f71a 100644 --- a/localized_fields/forms.py +++ b/localized_fields/forms.py @@ -7,7 +7,6 @@ from .value import LocalizedValue from .widgets import LocalizedFieldWidget - class LocalizedFieldForm(forms.MultiValueField): """Form for a localized field, allows editing the field in multiple languages.""" @@ -38,7 +37,7 @@ class LocalizedFieldForm(forms.MultiValueField): for f, w in zip(self.fields, self.widget.widgets): w.is_required = f.required - def compress(self, value: List[str]) -> value_class: + def compress(self, value: List[str]) -> LocalizedValue: """Compresses the values from individual fields into a single :see:LocalizedValue instance. diff --git a/localized_fields/hstore_index.py b/localized_fields/hstore_index.py deleted file mode 100644 index 352d03f..0000000 --- a/localized_fields/hstore_index.py +++ /dev/null @@ -1,132 +0,0 @@ -"""This module is unused, but should be contributed to Django.""" - -from typing import List - -from django.db import models - - -class HStoreIndex(models.Index): - """Allows creating a index on a specific HStore index. - - Note: pieces of code in this class have been copied - from the base class. There was no way around this.""" - - def __init__(self, field: str, keys: List[str], unique: bool=False, - name: str=''): - """Initializes a new instance of :see:HStoreIndex. - - Arguments: - field: - Name of the hstore field for - which's keys to create a index for. - - keys: - The name of the hstore keys to - create the index on. - - unique: - Whether this index should - be marked as UNIQUE. - - name: - The name of the index. If left - empty, one will be generated. - """ - - self.field = field - self.keys = keys - self.unique = unique - - # this will eventually set self.name - super(HStoreIndex, self).__init__( - fields=[field], - name=name - ) - - def get_sql_create_template_values(self, model, schema_editor, using): - """Gets the values for the SQL template. - - Arguments: - model: - The model this index applies to. - - schema_editor: - The schema editor to modify the schema. - - using: - Optional: "USING" statement. - - Returns: - Dictionary of keys to pass into the SQL template. - """ - - fields = [model._meta.get_field(field_name) for field_name, order in self.fields_orders] - tablespace_sql = schema_editor._get_index_tablespace_sql(model, fields) - quote_name = schema_editor.quote_name - - columns = [ - '(%s->\'%s\')' % (self.field, key) - for key in self.keys - ] - - return { - 'table': quote_name(model._meta.db_table), - 'name': quote_name(self.name), - 'columns': ', '.join(columns), - 'using': using, - 'extra': tablespace_sql, - } - - def create_sql(self, model, schema_editor, using=''): - """Gets the SQL to execute when creating the index. - - Arguments: - model: - The model this index applies to. - - schema_editor: - The schema editor to modify the schema. - - using: - Optional: "USING" statement. - - Returns: - SQL string to execute to create this index. - """ - - sql_create_index = schema_editor.sql_create_index - if self.unique: - sql_create_index = sql_create_index.replace('CREATE', 'CREATE UNIQUE') - sql_parameters = self.get_sql_create_template_values(model, schema_editor, using) - return sql_create_index % sql_parameters - - def remove_sql(self, model, schema_editor): - """Gets the SQL to execute to remove this index. - - Arguments: - model: - The model this index applies to. - - schema_editor: - The schema editor to modify the schema. - - Returns: - SQL string to execute to remove this index. - """ - quote_name = schema_editor.quote_name - return schema_editor.sql_delete_index % { - 'table': quote_name(model._meta.db_table), - 'name': quote_name(self.name), - } - - def deconstruct(self): - """Gets the values to pass to :see:__init__ when - re-creating this object.""" - - path = '%s.%s' % (self.__class__.__module__, self.__class__.__name__) - return (path, (), { - 'field': self.field, - 'keys': self.keys, - 'unique': self.unique, - 'name': self.name - }) diff --git a/localized_fields/value.py b/localized_fields/value.py index 50714bc..68fbbe0 100644 --- a/localized_fields/value.py +++ b/localized_fields/value.py @@ -17,7 +17,8 @@ class LocalizedValue(dict): different language. """ - self._interpret_value(keys); + super().__init__({}) + self._interpret_value(keys) def get(self, language: str=None) -> str: """Gets the underlying value in the specified or diff --git a/localized_fields/widgets.py b/localized_fields/widgets.py index ade2b6f..0d6883d 100644 --- a/localized_fields/widgets.py +++ b/localized_fields/widgets.py @@ -15,12 +15,12 @@ class LocalizedFieldWidget(forms.MultiWidget): def __init__(self, *args, **kwargs): """Initializes a new instance of :see:LocalizedFieldWidget.""" - widgets = [] + initial_widgets = [ + self.widget + for _ in settings.LANGUAGES + ] - for _ in settings.LANGUAGES: - widgets.append(self.widget) - - super(LocalizedFieldWidget, self).__init__(widgets, *args, **kwargs) + super().__init__(initial_widgets, *args, **kwargs) def decompress(self, value: LocalizedValue) -> List[str]: """Decompresses the specified value so @@ -36,7 +36,6 @@ class LocalizedFieldWidget(forms.MultiWidget): """ result = [] - for lang_code, _ in settings.LANGUAGES: if value: result.append(value.get(lang_code)) @@ -78,7 +77,8 @@ class AdminLocalizedFieldWidget(LocalizedFieldWidget): } return render_to_string(self.template, context) - def build_widget_attrs(self, widget, value, attrs): + @staticmethod + def build_widget_attrs(widget, value, attrs): attrs = dict(attrs) # Copy attrs to avoid modifying the argument. if (not widget.use_required_attribute(value) or not widget.is_required) \ and 'required' in attrs: diff --git a/tests/fake_model.py b/tests/fake_model.py index 5456219..dc8cf8e 100644 --- a/tests/fake_model.py +++ b/tests/fake_model.py @@ -3,7 +3,6 @@ from django.db.migrations.executor import MigrationExecutor from django.contrib.postgres.operations import HStoreExtension from localized_fields.models import LocalizedModel -from localized_fields.mixins import AtomicSlugRetryMixin def define_fake_model(name='TestModel', fields=None): diff --git a/tests/test_field.py b/tests/test_field.py index 7538c07..b998a46 100644 --- a/tests/test_field.py +++ b/tests/test_field.py @@ -3,7 +3,6 @@ import json from django.conf import settings from django.db.utils import IntegrityError from django.test import TestCase -from django.utils import translation from localized_fields.fields import LocalizedField from localized_fields.forms import LocalizedFieldForm diff --git a/tests/test_model.py b/tests/test_model.py index f9c31ce..974b975 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -34,7 +34,6 @@ class LocalizedModelTestCase(TestCase): assert isinstance(obj.title, LocalizedValue) - @classmethod def test_model_init_kwargs(cls): """Tests whether all :see:LocalizedField diff --git a/tests/test_value.py b/tests/test_value.py index 7d4bd3e..aaffd99 100644 --- a/tests/test_value.py +++ b/tests/test_value.py @@ -1,5 +1,3 @@ -import json - from django.conf import settings from django.test import TestCase from django.utils import translation @@ -164,4 +162,3 @@ class LocalizedValueTestCase(TestCase): value = LocalizedValue('beer') assert value.get(settings.LANGUAGE_CODE) == 'beer' - From 8c73c9ab77d375662dee34c06dd8239e79688a4b Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Fri, 26 May 2017 17:05:05 +0300 Subject: [PATCH 17/29] Fix some mistakes in the README --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index dc722ec..6a6625c 100644 --- a/README.rst +++ b/README.rst @@ -195,7 +195,7 @@ Besides ``LocalizedField``, there's also: from localized_fields.models import LocalizedModel from localized_fields.fields import LocalizedField, LocalizedUniqueSlugField - class MyModel(AtomicSlugRetryMixin, LocalizedModel): + class MyModel(LocalizedModel): title = LocalizedField() slug = LocalizedUniqueSlugField(populate_from='title') @@ -217,7 +217,7 @@ Besides ``LocalizedField``, there's also: from localized_fields.fields import LocalizedField, LocalizedAutoSlugField - class MyModel(models.Model): + class MyModel(LocalizedModel): title = LocalizedField() slug = LocalizedAutoSlugField(populate_from='title') From 4305696f1b6cdc57f52d9442beaea8e1a5fa8fc6 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Fri, 26 May 2017 17:05:52 +0300 Subject: [PATCH 18/29] Fix pep8 issue, use two spaces before inline comment --- localized_fields/descriptor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/localized_fields/descriptor.py b/localized_fields/descriptor.py index 65f8512..dd7000a 100644 --- a/localized_fields/descriptor.py +++ b/localized_fields/descriptor.py @@ -60,6 +60,6 @@ class LocalizedValueDescriptor: def __set__(self, instance, value): if isinstance(value, six.string_types): language = translation.get_language() or settings.LANGUAGE_CODE - self.__get__(instance).set(language, value) # pylint: disable=no-member + self.__get__(instance).set(language, value) # pylint: disable=no-member else: instance.__dict__[self.field.name] = value From 2d5fe0be05b4fb614855f1e8d9c6b6c759941488 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Fri, 26 May 2017 17:38:49 +0300 Subject: [PATCH 19/29] Fix documentation on INSTALLED_APPS --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 6a6625c..5b6c327 100644 --- a/README.rst +++ b/README.rst @@ -32,7 +32,7 @@ Installation .... 'django.contrib.postgres', - 'localized_fields' + 'localized_fields.apps.LocalizedFieldsConfig' ] 3. Set the database engine to ``psqlextra.backend``: From e5214b07ae60d16f796b68a519ce719ed09838fd Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Tue, 30 May 2017 13:07:07 +0300 Subject: [PATCH 20/29] Fix aggregation not expanding into a actual list --- localized_fields/fields/field.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/localized_fields/fields/field.py b/localized_fields/fields/field.py index 4b476f6..2342c6a 100644 --- a/localized_fields/fields/field.py +++ b/localized_fields/fields/field.py @@ -66,6 +66,22 @@ class LocalizedField(HStoreField): else: return cls.attr_class() + # we can get a list if an aggregation expression was used.. + # if we the expression was flattened when only one key was selected + # then we don't wrap each value in a localized value, otherwise we do + if isinstance(value, list): + result = [] + for inner_val in value: + if isinstance(inner_val, dict): + if inner_value is None: + result.append(None) + else: + result.append(cls.attr_class(inner_val)) + else: + result.append(inner_val) + + return result + return cls.attr_class(value) def to_python(self, value: Union[dict, str, None]) -> LocalizedValue: From 06f7ee15f0325ef16408cfd02017b73cefaaa63d Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Tue, 30 May 2017 13:38:27 +0300 Subject: [PATCH 21/29] Add LocalizedRef expression for extracting the value in the current language --- localized_fields/expressions.py | 25 ++++++++++++ localized_fields/fields/field.py | 6 +++ tests/test_expressions.py | 65 ++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 localized_fields/expressions.py create mode 100644 tests/test_expressions.py diff --git a/localized_fields/expressions.py b/localized_fields/expressions.py new file mode 100644 index 0000000..b0314b2 --- /dev/null +++ b/localized_fields/expressions.py @@ -0,0 +1,25 @@ +from django.conf import settings +from django.utils import translation + +from psqlextra import expressions + + +class LocalizedRef(expressions.HStoreRef): + """Expression that selects the value in a field only in + the currently active language.""" + + def __init__(self, name: str, lang: str=None): + """Initializes a new instance of :see:LocalizedRef. + + Arguments: + name: + The field/column to select from. + + lang: + The language to get the field/column in. + If not specified, the currently active language + is used. + """ + + language = lang or translation.get_language() or settings.LANGUAGE_CODE + super().__init__(name, language) diff --git a/localized_fields/fields/field.py b/localized_fields/fields/field.py index 2342c6a..a4071c7 100644 --- a/localized_fields/fields/field.py +++ b/localized_fields/fields/field.py @@ -82,6 +82,12 @@ class LocalizedField(HStoreField): return result + # this is for when you select an individual key, it will be string, + # not a dictionary, we'll give it to you as a flat value, not as a + # localized value instance + if not isinstance(value, dict): + return value + return cls.attr_class(value) def to_python(self, value: Union[dict, str, None]) -> LocalizedValue: diff --git a/tests/test_expressions.py b/tests/test_expressions.py new file mode 100644 index 0000000..a655f9a --- /dev/null +++ b/tests/test_expressions.py @@ -0,0 +1,65 @@ +from django.test import TestCase +from django.db import models +from django.utils import translation +from django.conf import settings + +from localized_fields.fields import LocalizedField +from localized_fields.value import LocalizedValue +from localized_fields.expressions import LocalizedRef + +from .fake_model import get_fake_model + + +class LocalizedExpressionsTestCase(TestCase): + """Tests whether expressions properly work with :see:LocalizedField.""" + + TestModel1 = None + TestModel2 = None + + @classmethod + def setUpClass(cls): + """Creates the test model in the database.""" + + super(LocalizedExpressionsTestCase, cls).setUpClass() + + cls.TestModel1 = get_fake_model( + 'LocalizedExpressionsTestCase2', + { + 'name': models.CharField(null=False, blank=False, max_length=255), + } + ) + + cls.TestModel2 = get_fake_model( + 'LocalizedExpressionsTestCase1', + { + 'text': LocalizedField(), + 'other': models.ForeignKey(cls.TestModel1, related_name='features') + } + ) + + @classmethod + def test_localized_ref(cls): + """Tests whether the :see:LocalizedRef expression properly works.""" + + obj = cls.TestModel1.objects.create(name='bla bla') + for i in range(0, 10): + cls.TestModel2.objects.create( + text=LocalizedValue(dict(en='text_%d_en' % i, ro='text_%d_ro' % i, nl='text_%d_nl' % i)), + other=obj + ) + + for lang_code, _ in settings.LANGUAGES: + translation.activate(lang_code) + + queryset = ( + cls.TestModel1.objects + .annotate( + mytexts=LocalizedRef('features__text') + ) + .values_list( + 'mytexts', flat=True + ) + ) + + for index, value in enumerate(queryset): + assert str(index) in value From 2741a6a2a200e60dc47ec73537267c60eee4b1cf Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Tue, 30 May 2017 13:45:43 +0300 Subject: [PATCH 22/29] Add extra tests for LocalizedRef --- tests/test_expressions.py | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/tests/test_expressions.py b/tests/test_expressions.py index a655f9a..dd01441 100644 --- a/tests/test_expressions.py +++ b/tests/test_expressions.py @@ -48,18 +48,32 @@ class LocalizedExpressionsTestCase(TestCase): other=obj ) - for lang_code, _ in settings.LANGUAGES: - translation.activate(lang_code) - - queryset = ( + def create_queryset(ref): + return ( cls.TestModel1.objects - .annotate( - mytexts=LocalizedRef('features__text') - ) - .values_list( - 'mytexts', flat=True - ) + .annotate(mytexts=ref) + .values_list('mytexts', flat=True) ) + # assert that it properly selects the currently active language + for lang_code, _ in settings.LANGUAGES: + translation.activate(lang_code) + queryset = create_queryset(LocalizedRef('features__text')) + for index, value in enumerate(queryset): + assert translation.get_language() in value assert str(index) in value + + # ensure that the default language is used in case no + # language is active at all + translation.deactivate_all() + queryset = create_queryset(LocalizedRef('features__text')) + for index, value in enumerate(queryset): + assert settings.LANGUAGE_CODE in value + assert str(index) in value + + # ensures that overriding the language works properly + queryset = create_queryset(LocalizedRef('features__text', 'ro')) + for index, value in enumerate(queryset): + assert 'ro' in value + assert str(index) in value From 3c8bea0fc39bd1311334937e5a787e6a48b46ac4 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Tue, 30 May 2017 13:47:48 +0300 Subject: [PATCH 23/29] Add test for LocalizedRef in combination with ArrayAgg --- tests/test_expressions.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_expressions.py b/tests/test_expressions.py index dd01441..91a2c98 100644 --- a/tests/test_expressions.py +++ b/tests/test_expressions.py @@ -2,6 +2,7 @@ from django.test import TestCase from django.db import models from django.utils import translation from django.conf import settings +from django.contrib.postgres.aggregates import ArrayAgg from localized_fields.fields import LocalizedField from localized_fields.value import LocalizedValue @@ -77,3 +78,9 @@ class LocalizedExpressionsTestCase(TestCase): for index, value in enumerate(queryset): assert 'ro' in value assert str(index) in value + + # ensures that using this in combination with ArrayAgg works properly + queryset = create_queryset(ArrayAgg(LocalizedRef('features__text', 'ro'))).first() + assert isinstance(queryset, list) + for value in queryset: + assert 'ro' in value From a8dc4fe837c9c480298de404d11d8adc202e8384 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Tue, 30 May 2017 13:48:43 +0300 Subject: [PATCH 24/29] Fix bug/typo in LocalizedField.from_db_value --- localized_fields/fields/field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/localized_fields/fields/field.py b/localized_fields/fields/field.py index a4071c7..28d9e62 100644 --- a/localized_fields/fields/field.py +++ b/localized_fields/fields/field.py @@ -73,7 +73,7 @@ class LocalizedField(HStoreField): result = [] for inner_val in value: if isinstance(inner_val, dict): - if inner_value is None: + if inner_val is None: result.append(None) else: result.append(cls.attr_class(inner_val)) From ea7733670da3d7503f4da2cc11a0dbdbc4bd280b Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Tue, 30 May 2017 16:38:08 +0300 Subject: [PATCH 25/29] Bump version to 4.1 --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 1fd6934..cb672e1 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: setup( name='django-localized-fields', - version='4.0', + version='4.1', packages=find_packages(), include_package_data=True, license='MIT License', @@ -18,7 +18,7 @@ setup( author_email='open-source@sectorlabs.ro', keywords=['django', 'localized', 'language', 'models', 'fields'], install_requires=[ - 'django-postgres-extra>=1.9' + 'django-postgres-extra>=1.11' ], classifiers=[ 'Environment :: Web Environment', From 1b036dc1de34e11122723caa464ea9a3748288fa Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Wed, 31 May 2017 11:19:53 +0300 Subject: [PATCH 26/29] Add simple test to verify LocalizedField can be used in bulk_create --- tests/test_bulk.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/test_bulk.py diff --git a/tests/test_bulk.py b/tests/test_bulk.py new file mode 100644 index 0000000..5459759 --- /dev/null +++ b/tests/test_bulk.py @@ -0,0 +1,33 @@ +import json + +from django.db import models +from django.conf import settings +from django.test import TestCase + +from localized_fields.fields import LocalizedField + +from .data import get_init_values +from .fake_model import get_fake_model + + +class LocalizedBulkTestCase(TestCase): + """Tests bulk operations with data structures provided + by the django-localized-fields library.""" + + @staticmethod + def test_localized_bulk_insert(): + model = get_fake_model( + 'BulkInsertModel', + { + 'name': LocalizedField(), + 'score': models.IntegerField() + } + ) + + objects = model.objects.bulk_create([ + model(name={'en': 'english name 1', 'ro': 'romanian name 1'}, score=1), + model(name={'en': 'english name 2', 'ro': 'romanian name 2'}, score=2), + model(name={'en': 'english name 3', 'ro': 'romanian name 3'}, score=3) + ]) + + assert model.objects.all().count() == 3 From b97a7f3c235552433ce78ba5a3bf32f98673b068 Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Wed, 31 May 2017 11:31:04 +0300 Subject: [PATCH 27/29] Fix crash when using LocalizedUniqueSlugField in a bulk_create --- localized_fields/fields/uniqueslug_field.py | 5 ++-- tests/test_bulk.py | 27 ++++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/localized_fields/fields/uniqueslug_field.py b/localized_fields/fields/uniqueslug_field.py index ab4cd30..ecfbafd 100644 --- a/localized_fields/fields/uniqueslug_field.py +++ b/localized_fields/fields/uniqueslug_field.py @@ -91,11 +91,12 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField): if self.include_time: slug += '-%d' % datetime.now().microsecond - if instance.retries > 0: + retries = getattr(instance, 'retries', 0) + if retries > 0: # do not add another - if we already added time if not self.include_time: slug += '-' - slug += '%d' % instance.retries + slug += '%d' % retries slugs.set(lang_code, slug) diff --git a/tests/test_bulk.py b/tests/test_bulk.py index 5459759..5dcd1ef 100644 --- a/tests/test_bulk.py +++ b/tests/test_bulk.py @@ -4,7 +4,7 @@ from django.db import models from django.conf import settings from django.test import TestCase -from localized_fields.fields import LocalizedField +from localized_fields.fields import LocalizedField, LocalizedUniqueSlugField from .data import get_init_values from .fake_model import get_fake_model @@ -16,6 +16,9 @@ class LocalizedBulkTestCase(TestCase): @staticmethod def test_localized_bulk_insert(): + """Tests that bulk inserts work properly when using + a :see:LocalizedField in the model.""" + model = get_fake_model( 'BulkInsertModel', { @@ -31,3 +34,25 @@ class LocalizedBulkTestCase(TestCase): ]) assert model.objects.all().count() == 3 + + @staticmethod + def test_localized_slug_bulk_insert(): + """Tests whether bulk inserts work properly when using + a :see:LocalizedUniqueSlugField in the model.""" + + model = get_fake_model( + 'BulkSlugInsertModel', + { + 'name': LocalizedField(), + 'slug': LocalizedUniqueSlugField(populate_from='name', include_time=True), + 'score': models.IntegerField() + } + ) + + objects = model.objects.bulk_create([ + model(name={'en': 'english name 1', 'ro': 'romanian name 1'}, score=1), + model(name={'en': 'english name 2', 'ro': 'romanian name 2'}, score=2), + model(name={'en': 'english name 3', 'ro': 'romanian name 3'}, score=3) + ]) + + assert model.objects.all().count() == 3 From 6736b3b32de38404447a4839b305dc6b6f7c0d2c Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Wed, 31 May 2017 11:33:20 +0300 Subject: [PATCH 28/29] Simplify test case for bulk_create --- tests/test_bulk.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/tests/test_bulk.py b/tests/test_bulk.py index 5dcd1ef..016d8e8 100644 --- a/tests/test_bulk.py +++ b/tests/test_bulk.py @@ -16,27 +16,6 @@ class LocalizedBulkTestCase(TestCase): @staticmethod def test_localized_bulk_insert(): - """Tests that bulk inserts work properly when using - a :see:LocalizedField in the model.""" - - model = get_fake_model( - 'BulkInsertModel', - { - 'name': LocalizedField(), - 'score': models.IntegerField() - } - ) - - objects = model.objects.bulk_create([ - model(name={'en': 'english name 1', 'ro': 'romanian name 1'}, score=1), - model(name={'en': 'english name 2', 'ro': 'romanian name 2'}, score=2), - model(name={'en': 'english name 3', 'ro': 'romanian name 3'}, score=3) - ]) - - assert model.objects.all().count() == 3 - - @staticmethod - def test_localized_slug_bulk_insert(): """Tests whether bulk inserts work properly when using a :see:LocalizedUniqueSlugField in the model.""" From c75c1764e276d1cbda61e1258eb6e09298bce3ce Mon Sep 17 00:00:00 2001 From: Swen Kooij Date: Wed, 31 May 2017 11:38:53 +0300 Subject: [PATCH 29/29] Improve test case for bulk_create --- tests/test_bulk.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/tests/test_bulk.py b/tests/test_bulk.py index 016d8e8..e62f732 100644 --- a/tests/test_bulk.py +++ b/tests/test_bulk.py @@ -1,12 +1,8 @@ -import json - from django.db import models -from django.conf import settings from django.test import TestCase from localized_fields.fields import LocalizedField, LocalizedUniqueSlugField -from .data import get_init_values from .fake_model import get_fake_model @@ -28,10 +24,22 @@ class LocalizedBulkTestCase(TestCase): } ) - objects = model.objects.bulk_create([ + to_create = [ model(name={'en': 'english name 1', 'ro': 'romanian name 1'}, score=1), model(name={'en': 'english name 2', 'ro': 'romanian name 2'}, score=2), model(name={'en': 'english name 3', 'ro': 'romanian name 3'}, score=3) - ]) + ] + model.objects.bulk_create(to_create) assert model.objects.all().count() == 3 + + for obj in to_create: + obj_db = model.objects.filter( + name__en=obj.name.en, + name__ro=obj.name.ro, + score=obj.score + ).first() + + assert obj_db + assert len(obj_db.slug.en) >= len(obj_db.name.en) + assert len(obj_db.slug.ro) >= len(obj_db.name.ro)