mirror of
				https://github.com/SectorLabs/django-localized-fields.git
				synced 2025-10-27 09:28:57 +03:00 
			
		
		
		
	Compare commits
	
		
			5 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | d529da8886 | ||
|  | ca879087ea | ||
|  | 302a64a02c | ||
|  | bb11253207 | ||
|  | 5db87763fb | 
							
								
								
									
										37
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										37
									
								
								README.rst
									
									
									
									
									
								
							| @@ -146,14 +146,14 @@ At the moment, it is not possible to select two languages to be marked as requir | |||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         class MyModel(models.Model): |         class MyModel(LocalizedModel): | ||||||
|             title = LocalizedField(required=True) |             title = LocalizedField(required=True) | ||||||
|  |  | ||||||
| * Make all languages optional: | * Make all languages optional: | ||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         class MyModel(models.Model): |         class MyModel(LocalizedModel): | ||||||
|             title = LocalizedField(null=True) |             title = LocalizedField(null=True) | ||||||
|  |  | ||||||
| **Uniqueness** | **Uniqueness** | ||||||
| @@ -164,7 +164,7 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e | |||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         class MyModel(models.Model): |         class MyModel(LocalizedModel): | ||||||
|             title = LocalizedField(uniqueness=['en', 'ro']) |             title = LocalizedField(uniqueness=['en', 'ro']) | ||||||
|  |  | ||||||
| * Enforce uniqueness for **all** languages: | * Enforce uniqueness for **all** languages: | ||||||
| @@ -173,14 +173,14 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e | |||||||
|  |  | ||||||
|         from localized_fields import get_language_codes |         from localized_fields import get_language_codes | ||||||
|  |  | ||||||
|         class MyModel(models.Model): |         class MyModel(LocalizedModel): | ||||||
|             title = LocalizedField(uniqueness=get_language_codes()) |             title = LocalizedField(uniqueness=get_language_codes()) | ||||||
|  |  | ||||||
| * Enforce uniqueness for one ore more languages **together** (similar to Django's ``unique_together``): | * Enforce uniqueness for one ore more languages **together** (similar to Django's ``unique_together``): | ||||||
|  |  | ||||||
|     .. code-block:: python |     .. code-block:: python | ||||||
|  |  | ||||||
|         class MyModel(models.Model): |         class MyModel(LocalizedModel): | ||||||
|             title = LocalizedField(uniqueness=[('en', 'ro')]) |             title = LocalizedField(uniqueness=[('en', 'ro')]) | ||||||
|  |  | ||||||
| * Enforce uniqueness for **all** languages **together**: | * Enforce uniqueness for **all** languages **together**: | ||||||
| @@ -189,7 +189,7 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e | |||||||
|  |  | ||||||
|         from localized_fields import get_language_codes |         from localized_fields import get_language_codes | ||||||
|  |  | ||||||
|         class MyModel(models.Model): |         class MyModel(LocalizedModel): | ||||||
|             title = LocalizedField(uniqueness=[(*get_language_codes())]) |             title = LocalizedField(uniqueness=[(*get_language_codes())]) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -197,19 +197,20 @@ Other fields | |||||||
| ^^^^^^^^^^^^ | ^^^^^^^^^^^^ | ||||||
| Besides ``LocalizedField``, there's also: | Besides ``LocalizedField``, there's also: | ||||||
|  |  | ||||||
| * ``LocalizedMagicSlugField`` | * ``LocalizedUniqueSlugField`` | ||||||
|     Successor of ``LocalizedAutoSlugField`` that fixes concurrency issues and enforces |     Successor of ``LocalizedAutoSlugField`` that fixes concurrency issues and enforces | ||||||
|     uniqueness of slugs on a database level. Usage is the exact same: |     uniqueness of slugs on a database level. Usage is the exact same: | ||||||
|  |  | ||||||
|           .. code-block:: python |           .. code-block:: python | ||||||
|  |  | ||||||
|               from localized_fields.models import LocalizedModel |               from localized_fields import (LocalizedModel, | ||||||
|               from localized_fields.fields import (LocalizedField, |                                             AtomicSlugRetryMixin, | ||||||
|                                                    LocalizedMagicSlugField) |                                             LocalizedField, | ||||||
|  |                                             LocalizedUniqueSlugField) | ||||||
|  |  | ||||||
|               class MyModel(LocalizedModel): |               class MyModel(AtomicSlugRetryMixin, LocalizedModel): | ||||||
|                    title = LocalizedField() |                    title = LocalizedField() | ||||||
|                    slug = LocalizedMagicSlugField(populate_from='title') |                    slug = LocalizedUniqueSlugField(populate_from='title') | ||||||
|  |  | ||||||
| * ``LocalizedAutoSlugField`` | * ``LocalizedAutoSlugField`` | ||||||
|      Automatically creates a slug for every language from the specified field. |      Automatically creates a slug for every language from the specified field. | ||||||
| @@ -218,15 +219,15 @@ Besides ``LocalizedField``, there's also: | |||||||
|  |  | ||||||
|           .. code-block:: python |           .. code-block:: python | ||||||
|  |  | ||||||
|               from localized_fields.models import LocalizedModel |               from localized_fields import (LocalizedModel, | ||||||
|               from localized_fields.fields import (LocalizedField, |                                             LocalizedField, | ||||||
|                                                    LocalizedAutoSlugField) |                                             LocalizedUniqueSlugField) | ||||||
|  |  | ||||||
|               class MyModel(LocalizedModel): |               class MyModel(LocalizedModel): | ||||||
|                    title = LocalizedField() |                    title = LocalizedField() | ||||||
|                    slug = LocalizedAutoSlugField(populate_from='title') |                    slug = LocalizedAutoSlugField(populate_from='title') | ||||||
|  |  | ||||||
|      This implementation is **NOT** concurrency safe, prefer ``LocalizedMagicSlugField``. |      This implementation is **NOT** concurrency safe, prefer ``LocalizedUniqueSlugField``. | ||||||
|  |  | ||||||
| * ``LocalizedBleachField`` | * ``LocalizedBleachField`` | ||||||
|      Automatically bleaches the content of the field. |      Automatically bleaches the content of the field. | ||||||
| @@ -236,8 +237,8 @@ Besides ``LocalizedField``, there's also: | |||||||
|  |  | ||||||
|            .. code-block:: python |            .. code-block:: python | ||||||
|  |  | ||||||
|               from localized_fields.models import LocalizedModel |               from localized_fields import (LocalizedModel, | ||||||
|               from localized_fields.fields import (LocalizedField, |                                             LocalizedField, | ||||||
|                                             LocalizedBleachField) |                                             LocalizedBleachField) | ||||||
|  |  | ||||||
|               class MyModel(LocalizedModel): |               class MyModel(LocalizedModel): | ||||||
|   | |||||||
| @@ -1,18 +1,20 @@ | |||||||
| from .util import get_language_codes | from .util import get_language_codes | ||||||
| from .forms import LocalizedFieldForm, LocalizedFieldWidget | from .forms import LocalizedFieldForm, LocalizedFieldWidget | ||||||
| from .fields import (LocalizedField, LocalizedBleachField, | from .fields import (LocalizedField, LocalizedBleachField, | ||||||
|                      LocalizedAutoSlugField, LocalizedMagicSlugField) |                      LocalizedAutoSlugField, LocalizedUniqueSlugField) | ||||||
| from .localized_value import LocalizedValue | from .mixins import AtomicSlugRetryMixin | ||||||
| from .models import LocalizedModel | from .models import LocalizedModel | ||||||
|  | from .localized_value import LocalizedValue | ||||||
|  |  | ||||||
| __all__ = [ | __all__ = [ | ||||||
|     'get_language_codes', |     'get_language_codes', | ||||||
|     'LocalizedField', |     'LocalizedField', | ||||||
|     'LocalizedValue', |     'LocalizedValue', | ||||||
|     'LocalizedAutoSlugField', |     'LocalizedAutoSlugField', | ||||||
|     'LocalizedMagicSlugField', |     'LocalizedUniqueSlugField', | ||||||
|     'LocalizedBleachField', |     'LocalizedBleachField', | ||||||
|     'LocalizedFieldWidget', |     'LocalizedFieldWidget', | ||||||
|     'LocalizedFieldForm', |     'LocalizedFieldForm', | ||||||
|     'LocalizedModel' |     'LocalizedModel', | ||||||
|  |     'AtomicSlugRetryMixin' | ||||||
| ] | ] | ||||||
|   | |||||||
| @@ -34,14 +34,14 @@ def _get_backend_base(): | |||||||
|             '\'%s\' is not a valid database back-end.' |             '\'%s\' is not a valid database back-end.' | ||||||
|             ' The module does not define a DatabaseWrapper class.' |             ' The module does not define a DatabaseWrapper class.' | ||||||
|             ' Check the value of LOCALIZED_FIELDS_DB_BACKEND_BASE.' |             ' Check the value of LOCALIZED_FIELDS_DB_BACKEND_BASE.' | ||||||
|         )) |         ) % base_class_name) | ||||||
|  |  | ||||||
|     if isinstance(base_class, Psycopg2DatabaseWrapper): |     if isinstance(base_class, Psycopg2DatabaseWrapper): | ||||||
|         raise ImproperlyConfigured(( |         raise ImproperlyConfigured(( | ||||||
|             '\'%s\' is not a valid database back-end.' |             '\'%s\' is not a valid database back-end.' | ||||||
|             ' It does inherit from the PostgreSQL back-end.' |             ' It does inherit from the PostgreSQL back-end.' | ||||||
|             ' Check the value of LOCALIZED_FIELDS_DB_BACKEND_BASE.' |             ' Check the value of LOCALIZED_FIELDS_DB_BACKEND_BASE.' | ||||||
|         )) |         ) % base_class_name) | ||||||
|  |  | ||||||
|     return base_class |     return base_class | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,12 +1,12 @@ | |||||||
| from .localized_field import LocalizedField | from .localized_field import LocalizedField | ||||||
| from .localized_autoslug_field import LocalizedAutoSlugField | from .localized_autoslug_field import LocalizedAutoSlugField | ||||||
| from .localized_magicslug_field import LocalizedMagicSlugField | from .localized_uniqueslug_field import LocalizedUniqueSlugField | ||||||
| from .localized_bleach_field import LocalizedBleachField | from .localized_bleach_field import LocalizedBleachField | ||||||
|  |  | ||||||
|  |  | ||||||
| __all__ = [ | __all__ = [ | ||||||
|     'LocalizedField', |     'LocalizedField', | ||||||
|     'LocalizedAutoSlugField', |     'LocalizedAutoSlugField', | ||||||
|     'LocalizedMagicSlugField', |     'LocalizedUniqueSlugField', | ||||||
|     'LocalizedBleachField', |     'LocalizedBleachField', | ||||||
| ] | ] | ||||||
|   | |||||||
| @@ -1,12 +1,14 @@ | |||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.utils.text import slugify | from django.utils.text import slugify | ||||||
|  | from django.core.exceptions import ImproperlyConfigured | ||||||
| 
 | 
 | ||||||
|  | from ..util import get_language_codes | ||||||
|  | from ..mixins import AtomicSlugRetryMixin | ||||||
| from ..localized_value import LocalizedValue | from ..localized_value import LocalizedValue | ||||||
| from .localized_autoslug_field import LocalizedAutoSlugField | from .localized_autoslug_field import LocalizedAutoSlugField | ||||||
| from ..util import get_language_codes |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class LocalizedMagicSlugField(LocalizedAutoSlugField): | class LocalizedUniqueSlugField(LocalizedAutoSlugField): | ||||||
|     """Automatically provides slugs for a localized |     """Automatically provides slugs for a localized | ||||||
|     field upon saving." |     field upon saving." | ||||||
| 
 | 
 | ||||||
| @@ -17,19 +19,22 @@ class LocalizedMagicSlugField(LocalizedAutoSlugField): | |||||||
|         - Improved performance |         - Improved performance | ||||||
| 
 | 
 | ||||||
|     When in doubt, use this over :see:LocalizedAutoSlugField. |     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): |     def __init__(self, *args, **kwargs): | ||||||
|         """Initializes a new instance of :see:LocalizedMagicSlugField.""" |         """Initializes a new instance of :see:LocalizedUniqueSlugField.""" | ||||||
| 
 | 
 | ||||||
|         self.populate_from = kwargs.pop('populate_from') |  | ||||||
|         kwargs['uniqueness'] = kwargs.pop('uniqueness', get_language_codes()) |         kwargs['uniqueness'] = kwargs.pop('uniqueness', get_language_codes()) | ||||||
| 
 | 
 | ||||||
|         super(LocalizedAutoSlugField, self).__init__( |         super(LocalizedUniqueSlugField, self).__init__( | ||||||
|             *args, |             *args, | ||||||
|             **kwargs |             **kwargs | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  |         self.populate_from = kwargs.pop('populate_from') | ||||||
|  | 
 | ||||||
|     def pre_save(self, instance, add: bool): |     def pre_save(self, instance, add: bool): | ||||||
|         """Ran just before the model is saved, allows us to built |         """Ran just before the model is saved, allows us to built | ||||||
|         the slug. |         the slug. | ||||||
| @@ -46,6 +51,12 @@ class LocalizedMagicSlugField(LocalizedAutoSlugField): | |||||||
|             The localized slug that was generated. |             The localized slug that was generated. | ||||||
|         """ |         """ | ||||||
| 
 | 
 | ||||||
|  |         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() |         slugs = LocalizedValue() | ||||||
| 
 | 
 | ||||||
|         for lang_code, _ in settings.LANGUAGES: |         for lang_code, _ in settings.LANGUAGES: | ||||||
							
								
								
									
										38
									
								
								localized_fields/mixins.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								localized_fields/mixins.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | from django.db import transaction | ||||||
|  | from django.conf import settings | ||||||
|  | from django.db.utils import IntegrityError | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AtomicSlugRetryMixin: | ||||||
|  |     """Makes :see:LocalizedUniqueSlugField work by retrying upon | ||||||
|  |     violation of the UNIQUE constraint.""" | ||||||
|  |  | ||||||
|  |     def save(self, *args, **kwargs): | ||||||
|  |         """Saves this model instance to the database.""" | ||||||
|  |  | ||||||
|  |         max_retries = getattr( | ||||||
|  |             settings, | ||||||
|  |             'LOCALIZED_FIELDS_MAX_RETRIES', | ||||||
|  |             100 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         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() | ||||||
| @@ -34,33 +34,3 @@ class LocalizedModel(models.Model): | |||||||
|                     value = LocalizedValue() |                     value = LocalizedValue() | ||||||
|  |  | ||||||
|             setattr(self, field.name, value) |             setattr(self, field.name, value) | ||||||
|  |  | ||||||
|     def save(self, *args, **kwargs): |  | ||||||
|         """Saves this model instance to the database.""" |  | ||||||
|  |  | ||||||
|         max_retries = getattr( |  | ||||||
|             settings, |  | ||||||
|             'LOCALIZED_FIELDS_MAX_RETRIES', |  | ||||||
|             100 |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         if not hasattr(self, 'retries'): |  | ||||||
|             self.retries = 0 |  | ||||||
|  |  | ||||||
|         with transaction.atomic(): |  | ||||||
|             try: |  | ||||||
|                 return super(LocalizedModel, self).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() |  | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								setup.py
									
									
									
									
									
								
							| @@ -7,7 +7,7 @@ with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: | |||||||
|  |  | ||||||
| setup( | setup( | ||||||
|     name='django-localized-fields', |     name='django-localized-fields', | ||||||
|     version='2.5', |     version='2.6', | ||||||
|     packages=find_packages(), |     packages=find_packages(), | ||||||
|     include_package_data=True, |     include_package_data=True, | ||||||
|     license='MIT License', |     license='MIT License', | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ from django.db import connection, migrations | |||||||
| from django.db.migrations.executor import MigrationExecutor | from django.db.migrations.executor import MigrationExecutor | ||||||
| from django.contrib.postgres.operations import HStoreExtension | from django.contrib.postgres.operations import HStoreExtension | ||||||
|  |  | ||||||
| from localized_fields import LocalizedModel | from localized_fields import LocalizedModel, AtomicSlugRetryMixin | ||||||
|  |  | ||||||
|  |  | ||||||
| def define_fake_model(name='TestModel', fields=None): | def define_fake_model(name='TestModel', fields=None): | ||||||
| @@ -14,7 +14,7 @@ def define_fake_model(name='TestModel', fields=None): | |||||||
|  |  | ||||||
|     if fields: |     if fields: | ||||||
|         attributes.update(fields) |         attributes.update(fields) | ||||||
|     model = type(name, (LocalizedModel,), attributes) |     model = type(name, (AtomicSlugRetryMixin,LocalizedModel,), attributes) | ||||||
|  |  | ||||||
|     return model |     return model | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ from django.db.utils import IntegrityError | |||||||
| from django.utils.text import slugify | from django.utils.text import slugify | ||||||
|  |  | ||||||
| from localized_fields import (LocalizedField, LocalizedAutoSlugField, | from localized_fields import (LocalizedField, LocalizedAutoSlugField, | ||||||
|                               LocalizedMagicSlugField) |                               LocalizedUniqueSlugField) | ||||||
|  |  | ||||||
| from .fake_model import get_fake_model | from .fake_model import get_fake_model | ||||||
|  |  | ||||||
| @@ -31,10 +31,10 @@ class LocalizedSlugFieldTestCase(TestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         cls.MagicSlugModel = get_fake_model( |         cls.MagicSlugModel = get_fake_model( | ||||||
|             'LocalizedMagicSlugFieldTestModel', |             'LocalizedUniqueSlugFieldTestModel', | ||||||
|             { |             { | ||||||
|                 'title': LocalizedField(), |                 'title': LocalizedField(), | ||||||
|                 'slug': LocalizedMagicSlugField(populate_from='title') |                 'slug': LocalizedUniqueSlugField(populate_from='title') | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @@ -92,7 +92,7 @@ class LocalizedSlugFieldTestCase(TestCase): | |||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def test_deconstruct_magic(cls): |     def test_deconstruct_magic(cls): | ||||||
|         cls._test_deconstruct(LocalizedMagicSlugField) |         cls._test_deconstruct(LocalizedUniqueSlugField) | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def test_formfield_auto(cls): |     def test_formfield_auto(cls): | ||||||
| @@ -100,7 +100,7 @@ class LocalizedSlugFieldTestCase(TestCase): | |||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def test_formfield_magic(cls): |     def test_formfield_magic(cls): | ||||||
|         cls._test_formfield(LocalizedMagicSlugField) |         cls._test_formfield(LocalizedUniqueSlugField) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def _test_populate(model): |     def _test_populate(model): | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user