mirror of
				https://github.com/SectorLabs/django-localized-fields.git
				synced 2025-10-25 00:28:57 +03:00 
			
		
		
		
	Compare commits
	
		
			5 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | d529da8886 | ||
|  | ca879087ea | ||
|  | 302a64a02c | ||
|  | bb11253207 | ||
|  | 5db87763fb | 
							
								
								
									
										39
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								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 | ||||
|  | ||||
|         class MyModel(models.Model): | ||||
|         class MyModel(LocalizedModel): | ||||
|             title = LocalizedField(required=True) | ||||
|  | ||||
| * Make all languages optional: | ||||
|  | ||||
|     .. code-block:: python | ||||
|  | ||||
|         class MyModel(models.Model): | ||||
|         class MyModel(LocalizedModel): | ||||
|             title = LocalizedField(null=True) | ||||
|  | ||||
| **Uniqueness** | ||||
| @@ -164,7 +164,7 @@ By default the values stored in a ``LocalizedField`` are *not unique*. You can e | ||||
|  | ||||
|     .. code-block:: python | ||||
|  | ||||
|         class MyModel(models.Model): | ||||
|         class MyModel(LocalizedModel): | ||||
|             title = LocalizedField(uniqueness=['en', 'ro']) | ||||
|  | ||||
| * 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 | ||||
|  | ||||
|         class MyModel(models.Model): | ||||
|         class MyModel(LocalizedModel): | ||||
|             title = LocalizedField(uniqueness=get_language_codes()) | ||||
|  | ||||
| * Enforce uniqueness for one ore more languages **together** (similar to Django's ``unique_together``): | ||||
|  | ||||
|     .. code-block:: python | ||||
|  | ||||
|         class MyModel(models.Model): | ||||
|         class MyModel(LocalizedModel): | ||||
|             title = LocalizedField(uniqueness=[('en', 'ro')]) | ||||
|  | ||||
| * 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 | ||||
|  | ||||
|         class MyModel(models.Model): | ||||
|         class MyModel(LocalizedModel): | ||||
|             title = LocalizedField(uniqueness=[(*get_language_codes())]) | ||||
|  | ||||
|  | ||||
| @@ -197,19 +197,20 @@ Other fields | ||||
| ^^^^^^^^^^^^ | ||||
| Besides ``LocalizedField``, there's also: | ||||
|  | ||||
| * ``LocalizedMagicSlugField`` | ||||
| * ``LocalizedUniqueSlugField`` | ||||
|     Successor of ``LocalizedAutoSlugField`` that fixes concurrency issues and enforces | ||||
|     uniqueness of slugs on a database level. Usage is the exact same: | ||||
|  | ||||
|           .. code-block:: python | ||||
|  | ||||
|               from localized_fields.models import LocalizedModel | ||||
|               from localized_fields.fields import (LocalizedField, | ||||
|                                                    LocalizedMagicSlugField) | ||||
|               from localized_fields import (LocalizedModel, | ||||
|                                             AtomicSlugRetryMixin, | ||||
|                                             LocalizedField, | ||||
|                                             LocalizedUniqueSlugField) | ||||
|  | ||||
|               class MyModel(LocalizedModel): | ||||
|               class MyModel(AtomicSlugRetryMixin, LocalizedModel): | ||||
|                    title = LocalizedField() | ||||
|                    slug = LocalizedMagicSlugField(populate_from='title') | ||||
|                    slug = LocalizedUniqueSlugField(populate_from='title') | ||||
|  | ||||
| * ``LocalizedAutoSlugField`` | ||||
|      Automatically creates a slug for every language from the specified field. | ||||
| @@ -218,15 +219,15 @@ Besides ``LocalizedField``, there's also: | ||||
|  | ||||
|           .. code-block:: python | ||||
|  | ||||
|               from localized_fields.models import LocalizedModel | ||||
|               from localized_fields.fields import (LocalizedField, | ||||
|                                                    LocalizedAutoSlugField) | ||||
|               from localized_fields import (LocalizedModel, | ||||
|                                             LocalizedField, | ||||
|                                             LocalizedUniqueSlugField) | ||||
|  | ||||
|               class MyModel(LocalizedModel): | ||||
|                    title = LocalizedField() | ||||
|                    slug = LocalizedAutoSlugField(populate_from='title') | ||||
|  | ||||
|      This implementation is **NOT** concurrency safe, prefer ``LocalizedMagicSlugField``. | ||||
|      This implementation is **NOT** concurrency safe, prefer ``LocalizedUniqueSlugField``. | ||||
|  | ||||
| * ``LocalizedBleachField`` | ||||
|      Automatically bleaches the content of the field. | ||||
| @@ -236,9 +237,9 @@ Besides ``LocalizedField``, there's also: | ||||
|  | ||||
|            .. code-block:: python | ||||
|  | ||||
|               from localized_fields.models import LocalizedModel | ||||
|               from localized_fields.fields import (LocalizedField, | ||||
|                                                    LocalizedBleachField) | ||||
|               from localized_fields import (LocalizedModel, | ||||
|                                             LocalizedField, | ||||
|                                             LocalizedBleachField) | ||||
|  | ||||
|               class MyModel(LocalizedModel): | ||||
|                    title = LocalizedField() | ||||
|   | ||||
| @@ -1,18 +1,20 @@ | ||||
| from .util import get_language_codes | ||||
| from .forms import LocalizedFieldForm, LocalizedFieldWidget | ||||
| from .fields import (LocalizedField, LocalizedBleachField, | ||||
|                      LocalizedAutoSlugField, LocalizedMagicSlugField) | ||||
| from .localized_value import LocalizedValue | ||||
|                      LocalizedAutoSlugField, LocalizedUniqueSlugField) | ||||
| from .mixins import AtomicSlugRetryMixin | ||||
| from .models import LocalizedModel | ||||
| from .localized_value import LocalizedValue | ||||
|  | ||||
| __all__ = [ | ||||
|     'get_language_codes', | ||||
|     'LocalizedField', | ||||
|     'LocalizedValue', | ||||
|     'LocalizedAutoSlugField', | ||||
|     'LocalizedMagicSlugField', | ||||
|     'LocalizedUniqueSlugField', | ||||
|     'LocalizedBleachField', | ||||
|     'LocalizedFieldWidget', | ||||
|     'LocalizedFieldForm', | ||||
|     'LocalizedModel' | ||||
|     'LocalizedModel', | ||||
|     'AtomicSlugRetryMixin' | ||||
| ] | ||||
|   | ||||
| @@ -34,14 +34,14 @@ def _get_backend_base(): | ||||
|             '\'%s\' is not a valid database back-end.' | ||||
|             ' The module does not define a DatabaseWrapper class.' | ||||
|             ' Check the value of LOCALIZED_FIELDS_DB_BACKEND_BASE.' | ||||
|         )) | ||||
|         ) % base_class_name) | ||||
|  | ||||
|     if isinstance(base_class, Psycopg2DatabaseWrapper): | ||||
|         raise ImproperlyConfigured(( | ||||
|             '\'%s\' is not a valid database back-end.' | ||||
|             ' It does inherit from the PostgreSQL back-end.' | ||||
|             ' Check the value of LOCALIZED_FIELDS_DB_BACKEND_BASE.' | ||||
|         )) | ||||
|         ) % base_class_name) | ||||
|  | ||||
|     return base_class | ||||
|  | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| from .localized_field import LocalizedField | ||||
| from .localized_autoslug_field import LocalizedAutoSlugField | ||||
| from .localized_magicslug_field import LocalizedMagicSlugField | ||||
| from .localized_uniqueslug_field import LocalizedUniqueSlugField | ||||
| from .localized_bleach_field import LocalizedBleachField | ||||
|  | ||||
|  | ||||
| __all__ = [ | ||||
|     'LocalizedField', | ||||
|     'LocalizedAutoSlugField', | ||||
|     'LocalizedMagicSlugField', | ||||
|     'LocalizedUniqueSlugField', | ||||
|     'LocalizedBleachField', | ||||
| ] | ||||
|   | ||||
| @@ -1,12 +1,14 @@ | ||||
| from django.conf import settings | ||||
| 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_autoslug_field import LocalizedAutoSlugField | ||||
| from ..util import get_language_codes | ||||
| 
 | ||||
| 
 | ||||
| class LocalizedMagicSlugField(LocalizedAutoSlugField): | ||||
| class LocalizedUniqueSlugField(LocalizedAutoSlugField): | ||||
|     """Automatically provides slugs for a localized | ||||
|     field upon saving." | ||||
| 
 | ||||
| @@ -17,19 +19,22 @@ class LocalizedMagicSlugField(LocalizedAutoSlugField): | ||||
|         - 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): | ||||
|         """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()) | ||||
| 
 | ||||
|         super(LocalizedAutoSlugField, self).__init__( | ||||
|         super(LocalizedUniqueSlugField, self).__init__( | ||||
|             *args, | ||||
|             **kwargs | ||||
|         ) | ||||
| 
 | ||||
|         self.populate_from = kwargs.pop('populate_from') | ||||
| 
 | ||||
|     def pre_save(self, instance, add: bool): | ||||
|         """Ran just before the model is saved, allows us to built | ||||
|         the slug. | ||||
| @@ -46,6 +51,12 @@ class LocalizedMagicSlugField(LocalizedAutoSlugField): | ||||
|             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() | ||||
| 
 | ||||
|         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() | ||||
|  | ||||
|             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( | ||||
|     name='django-localized-fields', | ||||
|     version='2.5', | ||||
|     version='2.6', | ||||
|     packages=find_packages(), | ||||
|     include_package_data=True, | ||||
|     license='MIT License', | ||||
|   | ||||
| @@ -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 import LocalizedModel, AtomicSlugRetryMixin | ||||
|  | ||||
|  | ||||
| def define_fake_model(name='TestModel', fields=None): | ||||
| @@ -14,7 +14,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 | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ from django.db.utils import IntegrityError | ||||
| from django.utils.text import slugify | ||||
|  | ||||
| from localized_fields import (LocalizedField, LocalizedAutoSlugField, | ||||
|                               LocalizedMagicSlugField) | ||||
|                               LocalizedUniqueSlugField) | ||||
|  | ||||
| from .fake_model import get_fake_model | ||||
|  | ||||
| @@ -31,10 +31,10 @@ class LocalizedSlugFieldTestCase(TestCase): | ||||
|         ) | ||||
|  | ||||
|         cls.MagicSlugModel = get_fake_model( | ||||
|             'LocalizedMagicSlugFieldTestModel', | ||||
|             'LocalizedUniqueSlugFieldTestModel', | ||||
|             { | ||||
|                 'title': LocalizedField(), | ||||
|                 'slug': LocalizedMagicSlugField(populate_from='title') | ||||
|                 'slug': LocalizedUniqueSlugField(populate_from='title') | ||||
|             } | ||||
|         ) | ||||
|  | ||||
| @@ -92,7 +92,7 @@ class LocalizedSlugFieldTestCase(TestCase): | ||||
|  | ||||
|     @classmethod | ||||
|     def test_deconstruct_magic(cls): | ||||
|         cls._test_deconstruct(LocalizedMagicSlugField) | ||||
|         cls._test_deconstruct(LocalizedUniqueSlugField) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_formfield_auto(cls): | ||||
| @@ -100,7 +100,7 @@ class LocalizedSlugFieldTestCase(TestCase): | ||||
|  | ||||
|     @classmethod | ||||
|     def test_formfield_magic(cls): | ||||
|         cls._test_formfield(LocalizedMagicSlugField) | ||||
|         cls._test_formfield(LocalizedUniqueSlugField) | ||||
|  | ||||
|     @staticmethod | ||||
|     def _test_populate(model): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user