mirror of
				https://github.com/SectorLabs/django-localized-fields.git
				synced 2025-10-30 02:28:57 +03:00 
			
		
		
		
	Started work on LocalizedMagicSlugField
This will be a superior version of LocalizedAutoSlugField, but one that doesn't have concurrency issues and takes advantage of the new UNIQUE CONSTRAINTs.
This commit is contained in:
		| @@ -1,7 +1,7 @@ | ||||
| from .util import get_language_codes | ||||
| from .forms import LocalizedFieldForm, LocalizedFieldWidget | ||||
| from .fields import (LocalizedField, LocalizedBleachField, | ||||
|                      LocalizedAutoSlugField) | ||||
|                      LocalizedAutoSlugField, LocalizedMagicSlugField) | ||||
| from .localized_value import LocalizedValue | ||||
| from .models import LocalizedModel | ||||
|  | ||||
| @@ -10,6 +10,7 @@ __all__ = [ | ||||
|     'LocalizedField', | ||||
|     'LocalizedValue', | ||||
|     'LocalizedAutoSlugField', | ||||
|     'LocalizedMagicSlugField', | ||||
|     'LocalizedBleachField', | ||||
|     'LocalizedFieldWidget', | ||||
|     'LocalizedFieldForm', | ||||
|   | ||||
| @@ -1,10 +1,12 @@ | ||||
| from .localized_field import LocalizedField | ||||
| from .localized_autoslug_field import LocalizedAutoSlugField | ||||
| from .localized_magicslug_field import LocalizedMagicSlugField | ||||
| from .localized_bleach_field import LocalizedBleachField | ||||
|  | ||||
|  | ||||
| __all__ = [ | ||||
|     'LocalizedField', | ||||
|     'LocalizedAutoSlugField', | ||||
|     'LocalizedBleachField' | ||||
|     'LocalizedMagicSlugField', | ||||
|     'LocalizedBleachField', | ||||
| ] | ||||
|   | ||||
| @@ -9,9 +9,8 @@ from ..localized_value import LocalizedValue | ||||
|  | ||||
|  | ||||
| class LocalizedAutoSlugField(LocalizedField): | ||||
|     """Custom version of :see:AutoSlugField that | ||||
|     can operate on :see:LocalizedField and provides | ||||
|     unique slugs for every language.""" | ||||
|     """Automatically provides slugs for a localized | ||||
|     field upon saving.""" | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         """Initializes a new instance of :see:LocalizedAutoSlugField.""" | ||||
| @@ -29,8 +28,8 @@ class LocalizedAutoSlugField(LocalizedField): | ||||
|  | ||||
|         name, path, args, kwargs = super( | ||||
|             LocalizedAutoSlugField, self).deconstruct() | ||||
|         kwargs['populate_from'] = self.populate_from | ||||
|  | ||||
|         kwargs['populate_from'] = self.populate_from | ||||
|         return name, path, args, kwargs | ||||
|  | ||||
|     def formfield(self, **kwargs): | ||||
|   | ||||
							
								
								
									
										65
									
								
								localized_fields/fields/localized_magicslug_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								localized_fields/fields/localized_magicslug_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| from django.conf import settings | ||||
| from django.utils.text import slugify | ||||
|  | ||||
| from ..localized_value import LocalizedValue | ||||
| from .localized_autoslug_field import LocalizedAutoSlugField | ||||
| from ..util import get_language_codes | ||||
|  | ||||
|  | ||||
| class LocalizedMagicSlugField(LocalizedAutoSlugField): | ||||
|     """Automatically provides slugs for a localized | ||||
|     field upon saving." | ||||
|  | ||||
|     An improved version of :see:LocalizedAutoSlugField, | ||||
|     which adds: | ||||
|  | ||||
|         - Concurrency safety | ||||
|         - Improved performance | ||||
|  | ||||
|     When in doubt, use this over :see:LocalizedAutoSlugField. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         """Initializes a new instance of :see:LocalizedMagicSlugField.""" | ||||
|  | ||||
|         self.populate_from = kwargs.pop('populate_from') | ||||
|         kwargs['uniqueness'] = kwargs.pop('uniqueness', get_language_codes()) | ||||
|  | ||||
|         super(LocalizedAutoSlugField, self).__init__( | ||||
|             *args, | ||||
|             **kwargs | ||||
|         ) | ||||
|  | ||||
|     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. | ||||
|  | ||||
|             add: | ||||
|                 Indicates whether this is a new entry | ||||
|                 to the database or an update. | ||||
|  | ||||
|         Returns: | ||||
|             The localized slug that was generated. | ||||
|         """ | ||||
|  | ||||
|         slugs = LocalizedValue() | ||||
|  | ||||
|         for lang_code, _ in settings.LANGUAGES: | ||||
|             value = self._get_populate_from_value( | ||||
|                 instance, | ||||
|                 self.populate_from, | ||||
|                 lang_code | ||||
|             ) | ||||
|  | ||||
|             if not value: | ||||
|                 continue | ||||
|  | ||||
|             slug = slugify(value, allow_unicode=True) | ||||
|             slugs.set(lang_code, slug) | ||||
|  | ||||
|         setattr(instance, self.name, slugs) | ||||
|         return slugs | ||||
| @@ -1,28 +1,23 @@ | ||||
| from django.contrib.postgres.operations import HStoreExtension | ||||
| from django.db import connection, migrations | ||||
| from django.db.migrations.executor import MigrationExecutor | ||||
| import sys | ||||
|  | ||||
| from localized_fields import (LocalizedAutoSlugField, LocalizedField, | ||||
|                               LocalizedModel) | ||||
|  | ||||
| MODEL = None | ||||
|                               LocalizedModel, LocalizedMagicSlugField) | ||||
|  | ||||
|  | ||||
| def get_fake_model(): | ||||
| def get_fake_model(name='TestModel', fields={}): | ||||
|     """Creates a fake model to use during unit tests.""" | ||||
|  | ||||
|     global MODEL | ||||
|     attributes = { | ||||
|         'app_label': 'localized_fields', | ||||
|         '__module__': __name__, | ||||
|         '__name__': name | ||||
|     } | ||||
|  | ||||
|     if MODEL: | ||||
|         return MODEL | ||||
|  | ||||
|     class TestModel(LocalizedModel): | ||||
|         """Model used for testing the :see:LocalizedAutoSlugField.""" | ||||
|  | ||||
|         app_label = 'localized_fields' | ||||
|  | ||||
|         title = LocalizedField() | ||||
|         slug = LocalizedAutoSlugField(populate_from='title') | ||||
|     attributes.update(fields) | ||||
|     TestModel = type(name, (LocalizedModel,), attributes) | ||||
|  | ||||
|     class TestProject: | ||||
|  | ||||
| @@ -43,5 +38,4 @@ def get_fake_model(): | ||||
|  | ||||
|         schema_editor.create_model(TestModel) | ||||
|  | ||||
|     MODEL = TestModel | ||||
|     return MODEL | ||||
|     return TestModel | ||||
|   | ||||
| @@ -1,94 +0,0 @@ | ||||
| from django import forms | ||||
| from django.conf import settings | ||||
| from django.test import TestCase | ||||
| from django.utils.text import slugify | ||||
|  | ||||
| from localized_fields import LocalizedAutoSlugField | ||||
|  | ||||
| from .fake_model import get_fake_model | ||||
|  | ||||
|  | ||||
| class LocalizedAutoSlugFieldTestCase(TestCase): | ||||
|     """Tests the :see:LocalizedAutoSlugField class.""" | ||||
|  | ||||
|     TestModel = None | ||||
|  | ||||
|     @classmethod | ||||
|     def setUpClass(cls): | ||||
|         """Creates the test model in the database.""" | ||||
|  | ||||
|         super(LocalizedAutoSlugFieldTestCase, cls).setUpClass() | ||||
|  | ||||
|         cls.TestModel = get_fake_model() | ||||
|  | ||||
|     def test_populate(self): | ||||
|         """Tests whether the :see:LocalizedAutoSlugField's | ||||
|         populating feature works correctly.""" | ||||
|  | ||||
|         obj = self.TestModel() | ||||
|         obj.title.en = 'this is my title' | ||||
|         obj.save() | ||||
|  | ||||
|         assert obj.slug.get('en') == slugify(obj.title.en) | ||||
|  | ||||
|     def test_populate_multiple_languages(self): | ||||
|         """Tests whether the :see:LocalizedAutoSlugField's | ||||
|         populating feature correctly works for all languages.""" | ||||
|  | ||||
|         obj = self.TestModel() | ||||
|  | ||||
|         for lang_code, lang_name in settings.LANGUAGES: | ||||
|             obj.title.set(lang_code, 'title %s' % lang_name) | ||||
|  | ||||
|         obj.save() | ||||
|  | ||||
|         for lang_code, lang_name in settings.LANGUAGES: | ||||
|             assert obj.slug.get(lang_code) == 'title-%s' % lang_name.lower() | ||||
|  | ||||
|     def test_unique_slug(self): | ||||
|         """Tests whether the :see:LocalizedAutoSlugField | ||||
|         correctly generates unique slugs.""" | ||||
|  | ||||
|         obj = self.TestModel() | ||||
|         obj.title.en = 'title' | ||||
|         obj.save() | ||||
|  | ||||
|         another_obj = self.TestModel() | ||||
|         another_obj.title.en = 'title' | ||||
|         another_obj.save() | ||||
|  | ||||
|         assert another_obj.slug.en == 'title-1' | ||||
|  | ||||
|     def test_unique_slug_utf(self): | ||||
|         """Tests whether generating a slug works | ||||
|         when the value consists completely out | ||||
|         of non-ASCII characters.""" | ||||
|  | ||||
|         obj = self.TestModel() | ||||
|         obj.title.en = 'مكاتب للايجار بشارع بورسعيد' | ||||
|         obj.save() | ||||
|  | ||||
|         assert obj.slug.en == 'مكاتب-للايجار-بشارع-بورسعيد' | ||||
|  | ||||
|     @staticmethod | ||||
|     def test_deconstruct(): | ||||
|         """Tests whether the :see:deconstruct | ||||
|         function properly retains options | ||||
|         specified in the constructor.""" | ||||
|  | ||||
|         field = LocalizedAutoSlugField(populate_from='title') | ||||
|         _, _, _, kwargs = field.deconstruct() | ||||
|  | ||||
|         assert 'populate_from' in kwargs | ||||
|         assert kwargs['populate_from'] == field.populate_from | ||||
|  | ||||
|     @staticmethod | ||||
|     def test_formfield(): | ||||
|         """Tests whether the :see:formfield method | ||||
|         returns a valid form field that is hidden.""" | ||||
|  | ||||
|         field = LocalizedAutoSlugField(populate_from='title') | ||||
|         form_field = field.formfield() | ||||
|  | ||||
|         assert isinstance(form_field, forms.CharField) | ||||
|         assert isinstance(form_field.widget, forms.HiddenInput) | ||||
| @@ -1,6 +1,6 @@ | ||||
| from django.test import TestCase | ||||
|  | ||||
| from localized_fields import LocalizedValue | ||||
| from localized_fields import LocalizedField, LocalizedValue | ||||
|  | ||||
| from .fake_model import get_fake_model | ||||
|  | ||||
| @@ -16,7 +16,12 @@ class LocalizedModelTestCase(TestCase): | ||||
|  | ||||
|         super(LocalizedModelTestCase, cls).setUpClass() | ||||
|  | ||||
|         cls.TestModel = get_fake_model() | ||||
|         cls.TestModel = get_fake_model( | ||||
|             'LocalizedModelTestCase', | ||||
|             { | ||||
|                 'title': LocalizedField() | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_defaults(cls): | ||||
| @@ -46,4 +51,4 @@ class LocalizedModelTestCase(TestCase): | ||||
|         assert isinstance(obj.title, LocalizedValue) | ||||
|         assert obj.title.en == 'english_title' | ||||
|         assert obj.title.ro == 'romanian_title' | ||||
|         assert obj.title.nl == 'dutch_title' | ||||
|         assert obj.title.nl == 'dutch_title' | ||||
|   | ||||
							
								
								
									
										158
									
								
								tests/test_localized_slug_fields.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								tests/test_localized_slug_fields.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,158 @@ | ||||
| from django import forms | ||||
| from django.conf import settings | ||||
| from django.test import TestCase | ||||
| from django.utils.text import slugify | ||||
|  | ||||
| from localized_fields import LocalizedField, LocalizedAutoSlugField, LocalizedMagicSlugField | ||||
|  | ||||
| from .fake_model import get_fake_model | ||||
|  | ||||
|  | ||||
| class LocalizedSlugFieldTestCase(TestCase): | ||||
|     """Tests the localized slug classes.""" | ||||
|  | ||||
|     AutoSlugModel = None | ||||
|     MagicSlugModel = None | ||||
|  | ||||
|     @classmethod | ||||
|     def setUpClass(cls): | ||||
|         """Creates the test models in the database.""" | ||||
|  | ||||
|         super(LocalizedSlugFieldTestCase, cls).setUpClass() | ||||
|  | ||||
|         cls.AutoSlugModel = get_fake_model( | ||||
|             'LocalizedAutoSlugFieldTestModel', | ||||
|             { | ||||
|                 'title': LocalizedField(), | ||||
|                 'slug': LocalizedAutoSlugField(populate_from='title') | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|         cls.MagicSlugModel = get_fake_model( | ||||
|             'LocalizedMagicSlugFieldTestModel', | ||||
|             { | ||||
|                 'title': LocalizedField(), | ||||
|                 'slug': LocalizedMagicSlugField(populate_from='title') | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_populate_auto(cls): | ||||
|         cls._test_populate(cls.AutoSlugModel) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_populate_magic(cls): | ||||
|         cls._test_populate(cls.MagicSlugModel) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_populate_multiple_languages_auto(cls): | ||||
|         cls._test_populate_multiple_languages(cls.AutoSlugModel) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_populate_multiple_languages_magic(cls): | ||||
|         cls._test_populate_multiple_languages(cls.MagicSlugModel) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_unique_slug_auto(cls): | ||||
|         cls._test_unique_slug(cls.AutoSlugModel) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_unique_slug_magic(cls): | ||||
|         cls._test_unique_slug(cls.MagicSlugModel) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_unique_slug_utf_auto(cls): | ||||
|         cls._test_unique_slug_utf(cls.AutoSlugModel) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_unique_slug_utf_magic(cls): | ||||
|         cls._test_unique_slug_utf(cls.MagicSlugModel) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_deconstruct_auto(cls): | ||||
|         cls._test_deconstruct(LocalizedAutoSlugField) | ||||
|         cls._test_deconstruct(LocalizedMagicSlugField) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_deconstruct_magic(cls): | ||||
|         cls._test_deconstruct(LocalizedMagicSlugField) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_formfield_auto(cls): | ||||
|         cls._test_formfield(LocalizedAutoSlugField) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_formfield_magic(cls): | ||||
|         cls._test_formfield(LocalizedMagicSlugField) | ||||
|  | ||||
|     @staticmethod | ||||
|     def _test_populate(model): | ||||
|         """Tests whether the populating feature works correctly.""" | ||||
|  | ||||
|         obj = model() | ||||
|         obj.title.en = 'this is my title' | ||||
|         obj.save() | ||||
|  | ||||
|         assert obj.slug.get('en') == slugify(obj.title.en) | ||||
|  | ||||
|     @staticmethod | ||||
|     def _test_populate_multiple_languages(model): | ||||
|         """Tests whether the populating feature correctly | ||||
|         works for all languages.""" | ||||
|  | ||||
|         obj = model() | ||||
|         for lang_code, lang_name in settings.LANGUAGES: | ||||
|             obj.title.set(lang_code, 'title %s' % lang_name) | ||||
|  | ||||
|         obj.save() | ||||
|  | ||||
|         for lang_code, lang_name in settings.LANGUAGES: | ||||
|             assert obj.slug.get(lang_code) == 'title-%s' % lang_name.lower() | ||||
|  | ||||
|     @staticmethod | ||||
|     def _test_unique_slug(model): | ||||
|         """Tests whether unique slugs are properly generated.""" | ||||
|  | ||||
|         obj = model() | ||||
|         obj.title.en = 'title' | ||||
|         obj.save() | ||||
|  | ||||
|         another_obj = model() | ||||
|         another_obj.title.en = 'title' | ||||
|         another_obj.save() | ||||
|  | ||||
|         assert another_obj.slug.en == 'title-1' | ||||
|  | ||||
|     @staticmethod | ||||
|     def _test_unique_slug_utf(model): | ||||
|         """Tests whether generating a slug works | ||||
|         when the value consists completely out | ||||
|         of non-ASCII characters.""" | ||||
|  | ||||
|         obj = model() | ||||
|         obj.title.en = 'مكاتب للايجار بشارع بورسعيد' | ||||
|         obj.save() | ||||
|  | ||||
|         assert obj.slug.en == 'مكاتب-للايجار-بشارع-بورسعيد' | ||||
|  | ||||
|     @staticmethod | ||||
|     def _test_deconstruct(field_type): | ||||
|         """Tests whether the :see:deconstruct | ||||
|         function properly retains options | ||||
|         specified in the constructor.""" | ||||
|  | ||||
|         field = field_type(populate_from='title') | ||||
|         _, _, _, kwargs = field.deconstruct() | ||||
|  | ||||
|         assert 'populate_from' in kwargs | ||||
|         assert kwargs['populate_from'] == field.populate_from | ||||
|  | ||||
|     @staticmethod | ||||
|     def _test_formfield(field_type): | ||||
|         """Tests whether the :see:formfield method | ||||
|         returns a valid form field that is hidden.""" | ||||
|  | ||||
|         form_field = field_type(populate_from='title').formfield() | ||||
|  | ||||
|         assert isinstance(form_field, forms.CharField) | ||||
|         assert isinstance(form_field.widget, forms.HiddenInput) | ||||
		Reference in New Issue
	
	Block a user