mirror of
				https://github.com/SectorLabs/django-localized-fields.git
				synced 2025-10-26 17:08:58 +03:00 
			
		
		
		
	Compare commits
	
		
			22 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 3ce57ed4cc | ||
|  | 7316d312b4 | ||
|  | 16e23963cc | ||
|  | b10472d3e9 | ||
|  | 833ceb849c | ||
|  | d7382fbf30 | ||
|  | 8ad9268426 | ||
|  | 96ddc77cfc | ||
|  | 51fc6959d2 | ||
|  | 3b28a5e707 | ||
|  | 06873afbda | ||
|  | e5d7cd25e2 | ||
|  | 236ce1648c | ||
|  | aacc712195 | ||
|  | d1790f1fc1 | ||
|  | db93b93046 | ||
|  | a352388243 | ||
|  | 8ba08c389c | ||
|  | 24079a2fcb | ||
|  | 0f4c74a9b2 | ||
|  | 5b93c5ec8f | ||
|  | 817c7e13fe | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -4,6 +4,7 @@ env/ | ||||
| # Ignore Python byte code cache | ||||
| *.pyc | ||||
| __pycache__ | ||||
| .cache/ | ||||
|  | ||||
| # Ignore coverage reports | ||||
| .coverage | ||||
|   | ||||
							
								
								
									
										92
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										92
									
								
								README.rst
									
									
									
									
									
								
							| @@ -199,6 +199,29 @@ Besides ``LocalizedField``, there's also: | ||||
|                    title = LocalizedField() | ||||
|                    slug = LocalizedUniqueSlugField(populate_from='title') | ||||
|  | ||||
|     ``populate_from`` can be: | ||||
|  | ||||
|         - The name of a field. | ||||
|  | ||||
|            .. code-block:: python | ||||
|  | ||||
|                slug = LocalizedUniqueSlugField(populate_from='name', include_time=True) | ||||
|  | ||||
|         - A callable. | ||||
|  | ||||
|            .. code-block:: python | ||||
|  | ||||
|                def generate_slug(instance): | ||||
|                    return instance.title | ||||
|  | ||||
|                slug = LocalizedUniqueSlugField(populate_from=generate_slug, include_time=True) | ||||
|  | ||||
|         - A tuple of names of fields. | ||||
|  | ||||
|            .. code-block:: python | ||||
|  | ||||
|                slug = LocalizedUniqueSlugField(populate_from=('name', 'beer') include_time=True) | ||||
|  | ||||
|     By setting the option ``include_time=True`` | ||||
|  | ||||
|           .. code-block:: python | ||||
| @@ -208,21 +231,6 @@ Besides ``LocalizedField``, there's also: | ||||
|     You can instruct the field to include a part of the current time into | ||||
|     the resulting slug. This is useful if you're running into a lot of collisions. | ||||
|  | ||||
| * ``LocalizedAutoSlugField`` | ||||
|      Automatically creates a slug for every language from the specified field. | ||||
|  | ||||
|      Currently only supports ``populate_from``. Example usage: | ||||
|  | ||||
|           .. code-block:: python | ||||
|  | ||||
|               from localized_fields.fields import LocalizedField, LocalizedAutoSlugField | ||||
|  | ||||
|               class MyModel(LocalizedModel): | ||||
|                    title = LocalizedField() | ||||
|                    slug = LocalizedAutoSlugField(populate_from='title') | ||||
|  | ||||
|      This implementation is **NOT** concurrency safe, prefer ``LocalizedUniqueSlugField``. | ||||
|  | ||||
| * ``LocalizedBleachField`` | ||||
|      Automatically bleaches the content of the field. | ||||
|  | ||||
| @@ -238,6 +246,60 @@ Besides ``LocalizedField``, there's also: | ||||
|                    title = LocalizedField() | ||||
|                    description = LocalizedBleachField() | ||||
|  | ||||
| * ``LocalizedCharField`` and ``LocalizedTextField`` | ||||
|     This fields following the Django convention for string-based fields use the empty string as value for “no data”, not NULL. | ||||
|     ``LocalizedCharField`` uses ``TextInput`` (``<input type="text">``) widget for render. | ||||
|  | ||||
|     Example usage: | ||||
|  | ||||
|            .. code-block:: python | ||||
|  | ||||
|               from localized_fields import (LocalizedCharField, | ||||
|                                             LocalizedTextField) | ||||
|  | ||||
|               class MyModel(models.Model): | ||||
|                    title = LocalizedCharField() | ||||
|                    description = LocalizedTextField() | ||||
|  | ||||
| * ``LocalizedFileField`` | ||||
|     A file-upload field | ||||
|  | ||||
|     Parameter ``upload_to`` supports ``lang`` parameter for string formatting or as function argument (in case if ``upload_to`` is callable). | ||||
|  | ||||
|     Example usage: | ||||
|  | ||||
|            .. code-block:: python | ||||
|  | ||||
|               from localized_fields import LocalizedFileField | ||||
|  | ||||
|               def my_directory_path(instance, filename, lang): | ||||
|                 # file will be uploaded to MEDIA_ROOT/<lang>/<id>_<filename> | ||||
|                 return '{0}/{0}_{1}'.format(lang, instance.id, filename) | ||||
|  | ||||
|               class MyModel(models.Model): | ||||
|                    file1 = LocalizedFileField(upload_to='uploads/{lang}/') | ||||
|                    file2 = LocalizedFileField(upload_to=my_directory_path) | ||||
|  | ||||
|     In template you can access to file attributes: | ||||
|  | ||||
|             .. code-block:: django | ||||
|  | ||||
|               {# For current active language: #} | ||||
|  | ||||
|               {{ model.file.url }}  {# output file url #} | ||||
|               {{ model.file.name }} {# output file name #} | ||||
|  | ||||
|               {# Or get it in a specific language: #} | ||||
|  | ||||
|               {{ model.file.ro.url }}  {# output file url for romanian language #} | ||||
|               {{ model.file.ro.name }} {# output file name for romanian language #} | ||||
|  | ||||
|     To get access to file instance for current active language use ``localized`` method: | ||||
|  | ||||
|             .. code-block:: python | ||||
|  | ||||
|               model.file.localized() | ||||
|  | ||||
| Experimental feature | ||||
| ^^^^^^^^^^^^^^^^^^^^ | ||||
| Enables the following experimental features: | ||||
|   | ||||
| @@ -1,11 +1,15 @@ | ||||
| from django.contrib.admin import ModelAdmin | ||||
|  | ||||
| from . import widgets | ||||
| from .fields import LocalizedField | ||||
| from .fields import LocalizedField, LocalizedCharField, LocalizedTextField, \ | ||||
|     LocalizedFileField | ||||
|  | ||||
|  | ||||
| FORMFIELD_FOR_LOCALIZED_FIELDS_DEFAULTS = { | ||||
|     LocalizedField: {'widget': widgets.AdminLocalizedFieldWidget}, | ||||
|     LocalizedCharField: {'widget': widgets.AdminLocalizedCharFieldWidget}, | ||||
|     LocalizedTextField: {'widget': widgets.AdminLocalizedFieldWidget}, | ||||
|     LocalizedFileField: {'widget': widgets.AdminLocalizedFileFieldWidget}, | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,12 +1,18 @@ | ||||
| from .field import LocalizedField | ||||
| from .autoslug_field import LocalizedAutoSlugField | ||||
| from .uniqueslug_field import LocalizedUniqueSlugField | ||||
| from .char_field import LocalizedCharField | ||||
| from .text_field import LocalizedTextField | ||||
| from .file_field import LocalizedFileField | ||||
|  | ||||
|  | ||||
| __all__ = [ | ||||
|     'LocalizedField', | ||||
|     'LocalizedAutoSlugField', | ||||
|     'LocalizedUniqueSlugField', | ||||
|     'LocalizedCharField', | ||||
|     'LocalizedTextField', | ||||
|     'LocalizedFileField' | ||||
| ] | ||||
|  | ||||
| try: | ||||
|   | ||||
| @@ -1,12 +1,16 @@ | ||||
| from typing import Callable, Tuple | ||||
| import warnings | ||||
|  | ||||
| from typing import Callable, Tuple, Union | ||||
| from datetime import datetime | ||||
|  | ||||
| from django import forms | ||||
| from django.conf import settings | ||||
| from django.utils import translation | ||||
| from django.utils.text import slugify | ||||
|  | ||||
| from .field import LocalizedField | ||||
| from ..value import LocalizedValue | ||||
| from ..util import resolve_object_property | ||||
|  | ||||
|  | ||||
| class LocalizedAutoSlugField(LocalizedField): | ||||
| @@ -19,6 +23,11 @@ class LocalizedAutoSlugField(LocalizedField): | ||||
|         self.populate_from = kwargs.pop('populate_from', None) | ||||
|         self.include_time = kwargs.pop('include_time', False) | ||||
|  | ||||
|         warnings.warn( | ||||
|             'LocalizedAutoSlug is deprecated and will be removed in the next major version.', | ||||
|             DeprecationWarning | ||||
|         ) | ||||
|  | ||||
|         super(LocalizedAutoSlugField, self).__init__( | ||||
|             *args, | ||||
|             **kwargs | ||||
| @@ -147,7 +156,7 @@ class LocalizedAutoSlugField(LocalizedField): | ||||
|         ] | ||||
|  | ||||
|     @staticmethod | ||||
|     def _get_populate_from_value(instance, field_name: str, language: str): | ||||
|     def _get_populate_from_value(instance, field_name: Union[str, Tuple[str]], language: str): | ||||
|         """Gets the value to create a slug from in the specified language. | ||||
|  | ||||
|         Arguments: | ||||
| @@ -164,5 +173,20 @@ class LocalizedAutoSlugField(LocalizedField): | ||||
|             The text to generate a slug for. | ||||
|         """ | ||||
|  | ||||
|         value = getattr(instance, field_name, None) | ||||
|         return value.get(language) | ||||
|         if callable(field_name): | ||||
|             return field_name(instance) | ||||
|  | ||||
|         def get_field_value(name): | ||||
|             value = resolve_object_property(instance, name) | ||||
|             with translation.override(language): | ||||
|                 return str(value) | ||||
|  | ||||
|         if isinstance(field_name, tuple) or isinstance(field_name, list): | ||||
|             value = '-'.join([ | ||||
|                 value | ||||
|                 for value in [get_field_value(name) for name in field_name] | ||||
|                 if value | ||||
|             ]) | ||||
|             return value | ||||
|  | ||||
|         return get_field_value(field_name) | ||||
|   | ||||
							
								
								
									
										16
									
								
								localized_fields/fields/char_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								localized_fields/fields/char_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| from ..forms import LocalizedCharFieldForm | ||||
| from .field import LocalizedField | ||||
| from ..value import LocalizedStringValue | ||||
|  | ||||
|  | ||||
| class LocalizedCharField(LocalizedField): | ||||
|     attr_class = LocalizedStringValue | ||||
|  | ||||
|     def formfield(self, **kwargs): | ||||
|         """Gets the form field associated with this field.""" | ||||
|         defaults = { | ||||
|             'form_class': LocalizedCharFieldForm | ||||
|         } | ||||
|  | ||||
|         defaults.update(kwargs) | ||||
|         return super().formfield(**defaults) | ||||
| @@ -170,7 +170,10 @@ class LocalizedField(HStoreField): | ||||
|         # are any of the language fiels None/empty? | ||||
|         is_all_null = True | ||||
|         for lang_code, _ in settings.LANGUAGES: | ||||
|             if value.get(lang_code): | ||||
|             # NOTE(seroy): use check for None, instead of | ||||
|             # `bool(value.get(lang_code))==True` condition, cause in this way | ||||
|             # we can not save '' value | ||||
|             if value.get(lang_code) is not None: | ||||
|                 is_all_null = False | ||||
|                 break | ||||
|  | ||||
| @@ -198,7 +201,9 @@ class LocalizedField(HStoreField): | ||||
|  | ||||
|         primary_lang_val = getattr(value, settings.LANGUAGE_CODE) | ||||
|  | ||||
|         if not primary_lang_val: | ||||
|         # NOTE(seroy): use check for None, instead of `not primary_lang_val` | ||||
|         # condition, cause in this way we can not save '' value | ||||
|         if primary_lang_val is None: | ||||
|             raise IntegrityError( | ||||
|                 'null value in column "%s.%s" violates not-null constraint' % ( | ||||
|                     self.name, | ||||
|   | ||||
							
								
								
									
										151
									
								
								localized_fields/fields/file_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								localized_fields/fields/file_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | ||||
| import datetime | ||||
| import posixpath | ||||
|  | ||||
| from django.core.files import File | ||||
| from django.db.models.fields.files import FieldFile | ||||
| from django.utils import six | ||||
| from django.core.files.storage import default_storage | ||||
| from django.utils.encoding import force_str, force_text | ||||
|  | ||||
| from localized_fields.fields import LocalizedField | ||||
| from localized_fields.fields.field import LocalizedValueDescriptor | ||||
| from localized_fields.value import LocalizedValue | ||||
|  | ||||
| from ..value import LocalizedFileValue | ||||
| from ..forms import LocalizedFileFieldForm | ||||
|  | ||||
|  | ||||
| class LocalizedFieldFile(FieldFile): | ||||
|  | ||||
|     def __init__(self, instance, field, name, lang): | ||||
|         super().__init__(instance, field, name) | ||||
|         self.lang = lang | ||||
|  | ||||
|     def save(self, name, content, save=True): | ||||
|         name = self.field.generate_filename(self.instance, name, self.lang) | ||||
|         self.name = self.storage.save(name, content, | ||||
|                                       max_length=self.field.max_length) | ||||
|         self._committed = True | ||||
|  | ||||
|         if save: | ||||
|             self.instance.save() | ||||
|  | ||||
|     save.alters_data = True | ||||
|  | ||||
|     def delete(self, save=True): | ||||
|         if not self: | ||||
|             return | ||||
|  | ||||
|         if hasattr(self, '_file'): | ||||
|             self.close() | ||||
|             del self.file | ||||
|  | ||||
|         self.storage.delete(self.name) | ||||
|  | ||||
|         self.name = None | ||||
|         self._committed = False | ||||
|  | ||||
|         if save: | ||||
|             self.instance.save() | ||||
|  | ||||
|     delete.alters_data = True | ||||
|  | ||||
|  | ||||
| class LocalizedFileValueDescriptor(LocalizedValueDescriptor): | ||||
|     def __get__(self, instance, cls=None): | ||||
|         value = super().__get__(instance, cls) | ||||
|         for lang, file in value.__dict__.items(): | ||||
|             if isinstance(file, six.string_types) or file is None: | ||||
|                 file = self.field.value_class(instance, self.field, file, lang) | ||||
|                 value.set(lang, file) | ||||
|  | ||||
|             elif isinstance(file, File) and \ | ||||
|                     not isinstance(file, LocalizedFieldFile): | ||||
|                 file_copy = self.field.value_class(instance, self.field, | ||||
|                                                    file.name, lang) | ||||
|                 file_copy.file = file | ||||
|                 file_copy._committed = False | ||||
|                 value.set(lang, file_copy) | ||||
|  | ||||
|             elif isinstance(file, LocalizedFieldFile) and \ | ||||
|                     not hasattr(file, 'field'): | ||||
|                 file.instance = instance | ||||
|                 file.field = self.field | ||||
|                 file.storage = self.field.storage | ||||
|                 file.lang = lang | ||||
|  | ||||
|             # Make sure that the instance is correct. | ||||
|             elif isinstance(file, LocalizedFieldFile) \ | ||||
|                     and instance is not file.instance: | ||||
|                 file.instance = instance | ||||
|                 file.lang = lang | ||||
|         return value | ||||
|  | ||||
|  | ||||
| class LocalizedFileField(LocalizedField): | ||||
|     descriptor_class = LocalizedFileValueDescriptor | ||||
|     attr_class = LocalizedFileValue | ||||
|     value_class = LocalizedFieldFile | ||||
|  | ||||
|     def __init__(self, verbose_name=None, name=None, upload_to='', storage=None, | ||||
|                  **kwargs): | ||||
|  | ||||
|         self.storage = storage or default_storage | ||||
|         self.upload_to = upload_to | ||||
|  | ||||
|         super().__init__(verbose_name, name, **kwargs) | ||||
|  | ||||
|     def deconstruct(self): | ||||
|         name, path, args, kwargs = super().deconstruct() | ||||
|         kwargs['upload_to'] = self.upload_to | ||||
|         if self.storage is not default_storage: | ||||
|             kwargs['storage'] = self.storage | ||||
|         return name, path, args, kwargs | ||||
|  | ||||
|     def get_prep_value(self, value): | ||||
|         """Returns field's value prepared for saving into a database.""" | ||||
|  | ||||
|         if isinstance(value, LocalizedValue): | ||||
|             prep_value = LocalizedValue() | ||||
|             for k, v in value.__dict__.items(): | ||||
|                 if v is None: | ||||
|                     prep_value.set(k, '') | ||||
|                 else: | ||||
|                     # Need to convert File objects provided via a form to | ||||
|                     # unicode for database insertion | ||||
|                     prep_value.set(k, six.text_type(v)) | ||||
|             return super().get_prep_value(prep_value) | ||||
|         return super().get_prep_value(value) | ||||
|  | ||||
|     def pre_save(self, model_instance, add): | ||||
|         """Returns field's value just before saving.""" | ||||
|         value = super().pre_save(model_instance, add) | ||||
|         if isinstance(value, LocalizedValue): | ||||
|             for file in value.__dict__.values(): | ||||
|                 if file and not file._committed: | ||||
|                     file.save(file.name, file, save=False) | ||||
|         return value | ||||
|  | ||||
|     def generate_filename(self, instance, filename, lang): | ||||
|         if callable(self.upload_to): | ||||
|             filename = self.upload_to(instance, filename, lang) | ||||
|         else: | ||||
|             now = datetime.datetime.now() | ||||
|             dirname = force_text(now.strftime(force_str(self.upload_to))) | ||||
|             dirname = dirname.format(lang=lang) | ||||
|             filename = posixpath.join(dirname, filename) | ||||
|         return self.storage.generate_filename(filename) | ||||
|  | ||||
|     def save_form_data(self, instance, data): | ||||
|         if isinstance(data, LocalizedValue): | ||||
|             for k, v in data.__dict__.items(): | ||||
|                 if v is not None and not v: | ||||
|                     data.set(k, '') | ||||
|             setattr(instance, self.name, data) | ||||
|  | ||||
|     def formfield(self, **kwargs): | ||||
|         defaults = {'form_class': LocalizedFileFieldForm} | ||||
|         if 'initial' in kwargs: | ||||
|             defaults['required'] = False | ||||
|         defaults.update(kwargs) | ||||
|         return super().formfield(**defaults) | ||||
							
								
								
									
										14
									
								
								localized_fields/fields/text_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								localized_fields/fields/text_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| from ..forms import LocalizedTextFieldForm | ||||
| from .char_field import LocalizedCharField | ||||
|  | ||||
|  | ||||
| class LocalizedTextField(LocalizedCharField): | ||||
|     def formfield(self, **kwargs): | ||||
|         """Gets the form field associated with this field.""" | ||||
|  | ||||
|         defaults = { | ||||
|             'form_class': LocalizedTextFieldForm | ||||
|         } | ||||
|  | ||||
|         defaults.update(kwargs) | ||||
|         return super().formfield(**defaults) | ||||
| @@ -2,9 +2,13 @@ from typing import List | ||||
|  | ||||
| from django import forms | ||||
| from django.conf import settings | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.forms.widgets import FILE_INPUT_CONTRADICTION | ||||
|  | ||||
| from .value import LocalizedValue | ||||
| from .widgets import LocalizedFieldWidget | ||||
| from .value import LocalizedValue, LocalizedStringValue, \ | ||||
|     LocalizedFileValue | ||||
| from .widgets import LocalizedFieldWidget, LocalizedCharFieldWidget, \ | ||||
|     LocalizedFileWidget | ||||
|  | ||||
|  | ||||
| class LocalizedFieldForm(forms.MultiValueField): | ||||
| @@ -12,6 +16,7 @@ class LocalizedFieldForm(forms.MultiValueField): | ||||
|     the field in multiple languages.""" | ||||
|  | ||||
|     widget = LocalizedFieldWidget | ||||
|     field_class = forms.fields.CharField | ||||
|     value_class = LocalizedValue | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
| @@ -26,7 +31,7 @@ class LocalizedFieldForm(forms.MultiValueField): | ||||
|                 field_options['required'] = kwargs.get('required', True) | ||||
|  | ||||
|             field_options['label'] = lang_code | ||||
|             fields.append(forms.fields.CharField(**field_options)) | ||||
|             fields.append(self.field_class(**field_options)) | ||||
|  | ||||
|         super(LocalizedFieldForm, self).__init__( | ||||
|             fields, | ||||
| @@ -37,7 +42,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]) -> LocalizedValue: | ||||
|     def compress(self, value: List[str]) -> value_class: | ||||
|         """Compresses the values from individual fields | ||||
|         into a single :see:LocalizedValue instance. | ||||
|  | ||||
| @@ -56,3 +61,107 @@ class LocalizedFieldForm(forms.MultiValueField): | ||||
|             localized_value.set(lang_code, value) | ||||
|  | ||||
|         return localized_value | ||||
|  | ||||
|  | ||||
| class LocalizedCharFieldForm(LocalizedFieldForm): | ||||
|     """Form for a localized char field, allows editing | ||||
|     the field in multiple languages.""" | ||||
|  | ||||
|     widget = LocalizedCharFieldWidget | ||||
|     value_class = LocalizedStringValue | ||||
|  | ||||
|  | ||||
| class LocalizedTextFieldForm(LocalizedFieldForm): | ||||
|     """Form for a localized text field, allows editing | ||||
|     the field in multiple languages.""" | ||||
|  | ||||
|     value_class = LocalizedStringValue | ||||
|  | ||||
|  | ||||
| class LocalizedFileFieldForm(LocalizedFieldForm, forms.FileField): | ||||
|     """Form for a localized file field, allows editing | ||||
|     the field in multiple languages.""" | ||||
|  | ||||
|     widget = LocalizedFileWidget | ||||
|     field_class = forms.fields.FileField | ||||
|     value_class = LocalizedFileValue | ||||
|  | ||||
|     def clean(self, value, initial=None): | ||||
|         """ | ||||
|         Most part of this method is a copy of | ||||
|         django.forms.MultiValueField.clean, with the exception of initial | ||||
|         value handling (this need for correct processing FileField's). | ||||
|         All original comments saved. | ||||
|         """ | ||||
|         if initial is None: | ||||
|             initial = [None for x in range(0, len(value))] | ||||
|         else: | ||||
|             if not isinstance(initial, list): | ||||
|                 initial = self.widget.decompress(initial) | ||||
|  | ||||
|         clean_data = [] | ||||
|         errors = [] | ||||
|         if not value or isinstance(value, (list, tuple)): | ||||
|             if (not value or not [v for v in value if | ||||
|                                   v not in self.empty_values]) \ | ||||
|                     and (not initial or not [v for v in initial if | ||||
|                                              v not in self.empty_values]): | ||||
|                 if self.required: | ||||
|                     raise ValidationError(self.error_messages['required'], | ||||
|                                           code='required') | ||||
|         else: | ||||
|             raise ValidationError(self.error_messages['invalid'], | ||||
|                                   code='invalid') | ||||
|         for i, field in enumerate(self.fields): | ||||
|             try: | ||||
|                 field_value = value[i] | ||||
|             except IndexError: | ||||
|                 field_value = None | ||||
|             try: | ||||
|                 field_initial = initial[i] | ||||
|             except IndexError: | ||||
|                 field_initial = None | ||||
|  | ||||
|             if field_value in self.empty_values and \ | ||||
|                     field_initial in self.empty_values: | ||||
|                 if self.require_all_fields: | ||||
|                     # Raise a 'required' error if the MultiValueField is | ||||
|                     # required and any field is empty. | ||||
|                     if self.required: | ||||
|                         raise ValidationError(self.error_messages['required'], | ||||
|                                               code='required') | ||||
|                 elif field.required: | ||||
|                     # Otherwise, add an 'incomplete' error to the list of | ||||
|                     # collected errors and skip field cleaning, if a required | ||||
|                     # field is empty. | ||||
|                     if field.error_messages['incomplete'] not in errors: | ||||
|                         errors.append(field.error_messages['incomplete']) | ||||
|                     continue | ||||
|             try: | ||||
|                 clean_data.append(field.clean(field_value, field_initial)) | ||||
|             except ValidationError as e: | ||||
|                 # Collect all validation errors in a single list, which we'll | ||||
|                 # raise at the end of clean(), rather than raising a single | ||||
|                 # exception for the first error we encounter. Skip duplicates. | ||||
|                 errors.extend(m for m in e.error_list if m not in errors) | ||||
|         if errors: | ||||
|             raise ValidationError(errors) | ||||
|  | ||||
|         out = self.compress(clean_data) | ||||
|         self.validate(out) | ||||
|         self.run_validators(out) | ||||
|         return out | ||||
|  | ||||
|     def bound_data(self, data, initial): | ||||
|         bound_data = [] | ||||
|         if initial is None: | ||||
|             initial = [None for x in range(0, len(data))] | ||||
|         else: | ||||
|             if not isinstance(initial, list): | ||||
|                 initial = self.widget.decompress(initial) | ||||
|         for d, i in zip(data, initial): | ||||
|             if d in (None, FILE_INPUT_CONTRADICTION): | ||||
|                 bound_data.append(i) | ||||
|             else: | ||||
|                 bound_data.append(d) | ||||
|         return bound_data | ||||
|   | ||||
| @@ -45,3 +45,7 @@ | ||||
|     border-color: #79aec8; | ||||
|     opacity: 1; | ||||
| } | ||||
|  | ||||
| .localized-fields-widget p.file-upload { | ||||
|     margin-left: 0; | ||||
| } | ||||
|   | ||||
| @@ -19,3 +19,26 @@ def get_language_codes() -> List[str]: | ||||
|         lang_code | ||||
|         for lang_code, _ in settings.LANGUAGES | ||||
|     ] | ||||
|  | ||||
|  | ||||
| def resolve_object_property(obj, path: str): | ||||
|     """Resolves the value of a property on an object. | ||||
|  | ||||
|     Is able to resolve nested properties. For example, | ||||
|     a path can be specified: | ||||
|  | ||||
|         'other.beer.name' | ||||
|  | ||||
|     Raises: | ||||
|         AttributeError: | ||||
|             In case the property could not be resolved. | ||||
|  | ||||
|     Returns: | ||||
|         The value of the specified property. | ||||
|     """ | ||||
|  | ||||
|     value = obj | ||||
|     for path_part in path.split('.'): | ||||
|         value = getattr(value, path_part) | ||||
|  | ||||
|     return value | ||||
|   | ||||
| @@ -6,6 +6,7 @@ from django.utils import translation | ||||
|  | ||||
| class LocalizedValue(dict): | ||||
|     """Represents the value of a :see:LocalizedField.""" | ||||
|     default_value = None | ||||
|  | ||||
|     def __init__(self, keys: dict=None): | ||||
|         """Initializes a new instance of :see:LocalizedValue. | ||||
| @@ -20,7 +21,7 @@ class LocalizedValue(dict): | ||||
|         super().__init__({}) | ||||
|         self._interpret_value(keys) | ||||
|  | ||||
|     def get(self, language: str=None) -> str: | ||||
|     def get(self, language: str=None, default: str=None) -> str: | ||||
|         """Gets the underlying value in the specified or | ||||
|         primary language. | ||||
|  | ||||
| @@ -35,7 +36,7 @@ class LocalizedValue(dict): | ||||
|         """ | ||||
|  | ||||
|         language = language or settings.LANGUAGE_CODE | ||||
|         return super().get(language, None) | ||||
|         return super().get(language, default) | ||||
|  | ||||
|     def set(self, language: str, value: str): | ||||
|         """Sets the value in the specified language. | ||||
| @@ -60,7 +61,7 @@ class LocalizedValue(dict): | ||||
|             contained in this instance. | ||||
|         """ | ||||
|  | ||||
|         path = 'localized_fields.fields.LocalizedValue' | ||||
|         path = 'localized_fields.localized_value.%s' % self.__class__.__name__ | ||||
|         return path, [self.__dict__], {} | ||||
|  | ||||
|     def _interpret_value(self, value): | ||||
| @@ -83,14 +84,14 @@ class LocalizedValue(dict): | ||||
|         """ | ||||
|  | ||||
|         for lang_code, _ in settings.LANGUAGES: | ||||
|             self.set(lang_code, None) | ||||
|             self.set(lang_code, self.default_value) | ||||
|  | ||||
|         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 | ||||
|                 lang_value = value.get(lang_code, self.default_value) | ||||
|                 self.set(lang_code, lang_value) | ||||
|  | ||||
|         elif isinstance(value, collections.Iterable): | ||||
| @@ -102,13 +103,19 @@ class LocalizedValue(dict): | ||||
|         back to the primary language if there's no value | ||||
|         in the current language.""" | ||||
|  | ||||
|         value = self.get(translation.get_language()) | ||||
|         fallbacks = getattr(settings, 'LOCALIZED_FIELDS_FALLBACKS', {}) | ||||
|  | ||||
|         if not value: | ||||
|             value = self.get(settings.LANGUAGE_CODE) | ||||
|         language = translation.get_language() or settings.LANGUAGE_CODE | ||||
|         languages = fallbacks.get(language, [settings.LANGUAGE_CODE])[:] | ||||
|         languages.insert(0, language) | ||||
|  | ||||
|         for lang_code in languages: | ||||
|             value = self.get(lang_code) | ||||
|             if value: | ||||
|                 return value or '' | ||||
|  | ||||
|         return '' | ||||
|  | ||||
|     def __eq__(self, other): | ||||
|         """Compares :paramref:self to :paramref:other for | ||||
|         equality. | ||||
| @@ -156,4 +163,28 @@ class LocalizedValue(dict): | ||||
|     def __repr__(self):  # pragma: no cover | ||||
|         """Gets a textual representation of this object.""" | ||||
|  | ||||
|         return 'LocalizedValue<%s> 0x%s' % (dict(self), id(self)) | ||||
|         return '%s<%s> 0x%s' % (self.__class__.__name__, | ||||
|                                 self.__dict__, id(self)) | ||||
|  | ||||
|  | ||||
| class LocalizedStringValue(LocalizedValue): | ||||
|     default_value = '' | ||||
|  | ||||
|  | ||||
| class LocalizedFileValue(LocalizedValue): | ||||
|     def __getattr__(self, name: str): | ||||
|         """Proxies access to attributes to attributes of LocalizedFile""" | ||||
|  | ||||
|         value = self.get(translation.get_language()) | ||||
|         if hasattr(value, name): | ||||
|             return getattr(value, name) | ||||
|         raise AttributeError("'{}' object has no attribute '{}'". | ||||
|                              format(self.__class__.__name__, name)) | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         """Returns string representation of value""" | ||||
|         return str(super().__str__()) | ||||
|  | ||||
|     def localized(self): | ||||
|         """Returns value for current language""" | ||||
|         return self.get(translation.get_language()) | ||||
|   | ||||
| @@ -45,6 +45,16 @@ class LocalizedFieldWidget(forms.MultiWidget): | ||||
|         return result | ||||
|  | ||||
|  | ||||
| class LocalizedCharFieldWidget(LocalizedFieldWidget): | ||||
|     """Widget that has an input box for every language.""" | ||||
|     widget = forms.TextInput | ||||
|  | ||||
|  | ||||
| class LocalizedFileWidget(LocalizedFieldWidget): | ||||
|     """Widget that has an file input box for every language.""" | ||||
|     widget = forms.ClearableFileInput | ||||
|  | ||||
|  | ||||
| class AdminLocalizedFieldWidget(LocalizedFieldWidget): | ||||
|     widget = widgets.AdminTextareaWidget | ||||
|     template = 'localized_fields/admin/widget.html' | ||||
| @@ -84,3 +94,11 @@ class AdminLocalizedFieldWidget(LocalizedFieldWidget): | ||||
|                 and 'required' in attrs: | ||||
|             del attrs['required'] | ||||
|         return attrs | ||||
|  | ||||
|  | ||||
| class AdminLocalizedCharFieldWidget(AdminLocalizedFieldWidget): | ||||
|     widget = widgets.AdminTextInputWidget | ||||
|  | ||||
|  | ||||
| class AdminLocalizedFileFieldWidget(AdminLocalizedFieldWidget): | ||||
|     widget = widgets.AdminFileWidget | ||||
|   | ||||
							
								
								
									
										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='4.2', | ||||
|     version='4.3', | ||||
|     packages=find_packages(exclude=['tests']), | ||||
|     include_package_data=True, | ||||
|     license='MIT License', | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| import uuid | ||||
|  | ||||
| from django.db import connection, migrations | ||||
| from django.db.migrations.executor import MigrationExecutor | ||||
| from django.contrib.postgres.operations import HStoreExtension | ||||
| @@ -5,24 +7,27 @@ from django.contrib.postgres.operations import HStoreExtension | ||||
| from localized_fields.models import LocalizedModel | ||||
|  | ||||
|  | ||||
| def define_fake_model(name='TestModel', fields=None): | ||||
| def define_fake_model(fields=None, model_base=LocalizedModel, meta_options={}): | ||||
|     name = str(uuid.uuid4()).replace('-', '')[:8] | ||||
|  | ||||
|     attributes = { | ||||
|         'app_label': 'localized_fields', | ||||
|         'app_label': 'tests', | ||||
|         '__module__': __name__, | ||||
|         '__name__': name | ||||
|         '__name__': name, | ||||
|         'Meta': type('Meta', (object,), meta_options) | ||||
|     } | ||||
|  | ||||
|     if fields: | ||||
|         attributes.update(fields) | ||||
|     model = type(name, (model_base,), attributes) | ||||
|  | ||||
|     model = type(name, (LocalizedModel,), attributes) | ||||
|     return model | ||||
|  | ||||
|  | ||||
| def get_fake_model(name='TestModel', fields=None): | ||||
| def get_fake_model(fields=None, model_base=LocalizedModel, meta_options={}): | ||||
|     """Creates a fake model to use during unit tests.""" | ||||
|  | ||||
|     model = define_fake_model(name, fields) | ||||
|     model = define_fake_model(fields, model_base, meta_options) | ||||
|  | ||||
|     class TestProject: | ||||
|  | ||||
| @@ -39,7 +44,7 @@ def get_fake_model(name='TestModel', fields=None): | ||||
|     with connection.schema_editor() as schema_editor: | ||||
|         migration_executor = MigrationExecutor(schema_editor.connection) | ||||
|         migration_executor.apply_migration( | ||||
|             TestProject(), TestMigration('eh', 'localized_fields')) | ||||
|             TestProject(), TestMigration('eh', 'postgres_extra')) | ||||
|  | ||||
|         schema_editor.create_model(model) | ||||
|  | ||||
|   | ||||
| @@ -16,7 +16,6 @@ class LocalizedBulkTestCase(TestCase): | ||||
|         a :see:LocalizedUniqueSlugField in the model.""" | ||||
|  | ||||
|         model = get_fake_model( | ||||
|             'BulkSlugInsertModel', | ||||
|             { | ||||
|                 'name': LocalizedField(), | ||||
|                 'slug': LocalizedUniqueSlugField(populate_from='name', include_time=True), | ||||
|   | ||||
| @@ -24,14 +24,12 @@ class LocalizedExpressionsTestCase(TestCase): | ||||
|         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') | ||||
|   | ||||
							
								
								
									
										151
									
								
								tests/test_file_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								tests/test_file_field.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | ||||
| import os | ||||
| import shutil | ||||
| import tempfile as sys_tempfile | ||||
| import pickle | ||||
|  | ||||
| from django import forms | ||||
| from django.test import TestCase, override_settings | ||||
| from django.core.files.base import File, ContentFile | ||||
| from django.core.files import temp as tempfile | ||||
| from localized_fields.fields import LocalizedFileField | ||||
| from localized_fields.value import LocalizedValue | ||||
| from localized_fields.fields.file_field import LocalizedFieldFile | ||||
| from localized_fields.forms import LocalizedFileFieldForm | ||||
| from localized_fields.value import LocalizedFileValue | ||||
| from localized_fields.widgets import LocalizedFileWidget | ||||
| from .fake_model import get_fake_model | ||||
|  | ||||
|  | ||||
| MEDIA_ROOT = sys_tempfile.mkdtemp() | ||||
|  | ||||
|  | ||||
| @override_settings(MEDIA_ROOT=MEDIA_ROOT) | ||||
| class LocalizedFileFieldTestCase(TestCase): | ||||
|     """Tests the localized slug classes.""" | ||||
|  | ||||
|     @classmethod | ||||
|     def setUpClass(cls): | ||||
|         """Creates the test models in the database.""" | ||||
|  | ||||
|         super().setUpClass() | ||||
|  | ||||
|         cls.FileFieldModel = get_fake_model( | ||||
|             { | ||||
|                 'file': LocalizedFileField(), | ||||
|             } | ||||
|         ) | ||||
|         if not os.path.isdir(MEDIA_ROOT): | ||||
|             os.makedirs(MEDIA_ROOT) | ||||
|  | ||||
|     @classmethod | ||||
|     def tearDownClass(cls): | ||||
|         super().tearDownClass() | ||||
|         shutil.rmtree(MEDIA_ROOT) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_assign(cls): | ||||
|         """Tests whether the :see:LocalizedFileValueDescriptor works properly""" | ||||
|  | ||||
|         temp_file = tempfile.NamedTemporaryFile(dir=MEDIA_ROOT) | ||||
|         instance = cls.FileFieldModel() | ||||
|         instance.file = {'en': temp_file.name} | ||||
|         assert isinstance(instance.file.en, LocalizedFieldFile) | ||||
|         assert instance.file.en.name == temp_file.name | ||||
|  | ||||
|         field_dump = pickle.dumps(instance.file) | ||||
|         instance = cls.FileFieldModel() | ||||
|         instance.file = pickle.loads(field_dump) | ||||
|         assert instance.file.en.field == instance._meta.get_field('file') | ||||
|         assert instance.file.en.instance == instance | ||||
|         assert isinstance(instance.file.en, LocalizedFieldFile) | ||||
|  | ||||
|         instance = cls.FileFieldModel() | ||||
|         instance.file = {'en': ContentFile("test", "testfilename")} | ||||
|         assert isinstance(instance.file.en, LocalizedFieldFile) | ||||
|         assert instance.file.en.name == "testfilename" | ||||
|  | ||||
|         another_instance = cls.FileFieldModel() | ||||
|         another_instance.file = {'ro': instance.file.en} | ||||
|         assert another_instance == another_instance.file.ro.instance | ||||
|         assert another_instance.file.ro.lang == 'ro' | ||||
|  | ||||
|     @classmethod | ||||
|     def test_save_form_data(cls): | ||||
|         """Tests whether the :see:save_form_data function correctly set  | ||||
|         a valid value.""" | ||||
|  | ||||
|         instance = cls.FileFieldModel() | ||||
|         data = LocalizedFileValue({'en': False}) | ||||
|         instance._meta.get_field('file').save_form_data(instance, data) | ||||
|         assert instance.file.en == '' | ||||
|  | ||||
|     @classmethod | ||||
|     def test_pre_save(cls): | ||||
|         """Tests whether the :see:pre_save function works properly.""" | ||||
|  | ||||
|         instance = cls.FileFieldModel() | ||||
|         instance.file = {'en': ContentFile("test", "testfilename")} | ||||
|         instance._meta.get_field('file').pre_save(instance, False) | ||||
|         assert instance.file.en._committed == True | ||||
|  | ||||
|     @classmethod | ||||
|     def test_file_methods(cls): | ||||
|         """Tests whether the :see:LocalizedFieldFile.delete method works | ||||
|         correctly.""" | ||||
|  | ||||
|         temp_file = File(tempfile.NamedTemporaryFile()) | ||||
|         instance = cls.FileFieldModel() | ||||
|         # Calling delete on an unset FileField should not call the file deletion | ||||
|         # process, but fail silently | ||||
|         instance.file.en.delete() | ||||
|         instance.file.en.save('testfilename', temp_file) | ||||
|         assert instance.file.en.name == 'testfilename' | ||||
|         instance.file.en.delete() | ||||
|         assert instance.file.en.name is None | ||||
|  | ||||
|     @classmethod | ||||
|     def test_generate_filename(cls): | ||||
|         """Tests whether the :see:LocalizedFieldFile.generate_filename method | ||||
|         works correctly.""" | ||||
|  | ||||
|         instance = cls.FileFieldModel() | ||||
|         field = instance._meta.get_field('file') | ||||
|         field.upload_to = '{lang}/' | ||||
|         filename = field.generate_filename(instance, 'test', 'en') | ||||
|         assert filename == 'en/test' | ||||
|         field.upload_to = lambda instance, filename, lang: \ | ||||
|             '%s_%s' % (lang, filename) | ||||
|         filename = field.generate_filename(instance, 'test', 'en') | ||||
|         assert filename == 'en_test' | ||||
|  | ||||
|     @staticmethod | ||||
|     def test_get_prep_value(): | ||||
|         """Tests whether the :see:get_prep_value function returns correctly  | ||||
|         value.""" | ||||
|  | ||||
|         value = LocalizedValue({'en': None}) | ||||
|         assert LocalizedFileField().get_prep_value(None) == None | ||||
|         assert isinstance(LocalizedFileField().get_prep_value(value), dict) | ||||
|         assert LocalizedFileField().get_prep_value(value)['en'] == '' | ||||
|  | ||||
|     @staticmethod | ||||
|     def test_formfield(): | ||||
|         """Tests whether the :see:formfield function correctly returns  | ||||
|         a valid form.""" | ||||
|  | ||||
|         form_field = LocalizedFileField().formfield() | ||||
|         assert isinstance(form_field, LocalizedFileFieldForm) | ||||
|         assert isinstance(form_field, forms.FileField) | ||||
|         assert isinstance(form_field.widget, LocalizedFileWidget) | ||||
|  | ||||
|     @staticmethod | ||||
|     def test_deconstruct(): | ||||
|         """Tests whether the :see:LocalizedFileField | ||||
|         class's :see:deconstruct function works properly.""" | ||||
|  | ||||
|         name, path, args, kwargs = LocalizedFileField().deconstruct() | ||||
|         assert 'upload_to' in kwargs | ||||
|         assert 'storage' not in kwargs | ||||
|         name, path, \ | ||||
|         args, kwargs = LocalizedFileField(storage='test').deconstruct() | ||||
|         assert 'storage' in kwargs | ||||
							
								
								
									
										41
									
								
								tests/test_file_field_form.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								tests/test_file_field_form.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| from django.conf import settings | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.forms.widgets import FILE_INPUT_CONTRADICTION | ||||
| from django.test import TestCase | ||||
|  | ||||
| from localized_fields.forms import LocalizedFileFieldForm | ||||
|  | ||||
|  | ||||
| class LocalizedFileFieldFormTestCase(TestCase): | ||||
|     """Tests the workings of the :see:LocalizedFileFieldForm class.""" | ||||
|  | ||||
|     def test_clean(self): | ||||
|         """Tests whether the :see:clean function is working properly.""" | ||||
|  | ||||
|         formfield = LocalizedFileFieldForm(required=True) | ||||
|         with self.assertRaises(ValidationError): | ||||
|             formfield.clean([]) | ||||
|         with self.assertRaises(ValidationError): | ||||
|             formfield.clean([], {'en': None}) | ||||
|         with self.assertRaises(ValidationError): | ||||
|             formfield.clean("badvalue") | ||||
|         with self.assertRaises(ValidationError): | ||||
|             value = [FILE_INPUT_CONTRADICTION] * len(settings.LANGUAGES) | ||||
|             formfield.clean(value) | ||||
|  | ||||
|         formfield = LocalizedFileFieldForm(required=False) | ||||
|         formfield.clean([''] * len(settings.LANGUAGES)) | ||||
|         formfield.clean(['', ''], ['', '']) | ||||
|  | ||||
|     def test_bound_data(self): | ||||
|         """Tests whether the :see:bound_data function is returns correctly  | ||||
|         value""" | ||||
|  | ||||
|         formfield = LocalizedFileFieldForm() | ||||
|         assert formfield.bound_data([''], None) == [''] | ||||
|  | ||||
|         initial = dict([(lang, '') for lang, _ in settings.LANGUAGES]) | ||||
|         value = [None] * len(settings.LANGUAGES) | ||||
|         expected_value = [''] * len(settings.LANGUAGES) | ||||
|         assert formfield.bound_data(value, initial) == expected_value | ||||
|  | ||||
| @@ -18,7 +18,6 @@ class LocalizedModelTestCase(TestCase): | ||||
|         super(LocalizedModelTestCase, cls).setUpClass() | ||||
|  | ||||
|         cls.TestModel = get_fake_model( | ||||
|             'LocalizedModelTestCase', | ||||
|             { | ||||
|                 'title': LocalizedField() | ||||
|             } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import copy | ||||
|  | ||||
| from django import forms | ||||
| from django.db import models | ||||
| from django.conf import settings | ||||
| from django.test import TestCase | ||||
| from django.db.utils import IntegrityError | ||||
| @@ -8,7 +9,6 @@ from django.utils.text import slugify | ||||
|  | ||||
| from localized_fields.fields import ( | ||||
|     LocalizedField, | ||||
|     LocalizedAutoSlugField, | ||||
|     LocalizedUniqueSlugField | ||||
| ) | ||||
|  | ||||
| @@ -19,7 +19,7 @@ class LocalizedSlugFieldTestCase(TestCase): | ||||
|     """Tests the localized slug classes.""" | ||||
|  | ||||
|     AutoSlugModel = None | ||||
|     MagicSlugModel = None | ||||
|     Model = None | ||||
|  | ||||
|     @classmethod | ||||
|     def setUpClass(cls): | ||||
| @@ -27,46 +27,14 @@ class LocalizedSlugFieldTestCase(TestCase): | ||||
|  | ||||
|         super(LocalizedSlugFieldTestCase, cls).setUpClass() | ||||
|  | ||||
|         cls.AutoSlugModel = get_fake_model( | ||||
|             'LocalizedAutoSlugFieldTestModel', | ||||
|             { | ||||
|                 'title': LocalizedField(), | ||||
|                 'slug': LocalizedAutoSlugField(populate_from='title') | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|         cls.MagicSlugModel = get_fake_model( | ||||
|             'LocalizedUniqueSlugFieldTestModel', | ||||
|         cls.Model = get_fake_model( | ||||
|             { | ||||
|                 'title': LocalizedField(), | ||||
|                 'name': models.CharField(max_length=255), | ||||
|                 'slug': LocalizedUniqueSlugField(populate_from='title') | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_populate_auto(cls): | ||||
|         cls._test_populate(cls.AutoSlugModel) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_populate_unique(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_unique(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_unique(cls): | ||||
|         cls._test_unique_slug(cls.MagicSlugModel) | ||||
|  | ||||
|     @staticmethod | ||||
|     def test_unique_slug_with_time(): | ||||
|         """Tests whether the primary key is included in | ||||
| @@ -75,7 +43,6 @@ class LocalizedSlugFieldTestCase(TestCase): | ||||
|         title = 'myuniquetitle' | ||||
|  | ||||
|         PkModel = get_fake_model( | ||||
|             'PkModel', | ||||
|             { | ||||
|                 'title': LocalizedField(), | ||||
|                 'slug': LocalizedUniqueSlugField(populate_from='title', include_time=True) | ||||
| @@ -93,7 +60,6 @@ class LocalizedSlugFieldTestCase(TestCase): | ||||
|         """Tests whether slugs are not re-generated if not needed.""" | ||||
|  | ||||
|         NoChangeSlugModel = get_fake_model( | ||||
|             'NoChangeSlugModel', | ||||
|             { | ||||
|                 'title': LocalizedField(), | ||||
|                 'slug': LocalizedUniqueSlugField(populate_from='title', include_time=True) | ||||
| @@ -115,62 +81,47 @@ class LocalizedSlugFieldTestCase(TestCase): | ||||
|         assert old_slug_en == obj.slug.en | ||||
|         assert old_slug_nl != obj.slug.nl | ||||
|  | ||||
|     def test_unique_slug_unique_max_retries(self): | ||||
|     @classmethod | ||||
|     def test_unique_slug_unique_max_retries(cls): | ||||
|         """Tests whether the unique slug implementation doesn't | ||||
|         try to find a slug forever and gives up after a while.""" | ||||
|  | ||||
|         title = 'myuniquetitle' | ||||
|  | ||||
|         obj = self.MagicSlugModel() | ||||
|         obj = cls.Model() | ||||
|         obj.title.en = title | ||||
|         obj.save() | ||||
|  | ||||
|         with self.assertRaises(IntegrityError): | ||||
|         with cls.assertRaises(cls, IntegrityError): | ||||
|             for _ in range(0, settings.LOCALIZED_FIELDS_MAX_RETRIES + 1): | ||||
|                 another_obj = self.MagicSlugModel() | ||||
|                 another_obj = cls.Model() | ||||
|                 another_obj.title.en = title | ||||
|                 another_obj.save() | ||||
|  | ||||
|     @classmethod | ||||
|     def test_unique_slug_utf_auto(cls): | ||||
|         cls._test_unique_slug_utf(cls.AutoSlugModel) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_unique_slug_utf_unique(cls): | ||||
|         cls._test_unique_slug_utf(cls.MagicSlugModel) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_deconstruct_auto(cls): | ||||
|         cls._test_deconstruct(LocalizedAutoSlugField) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_deconstruct_unique(cls): | ||||
|         cls._test_deconstruct(LocalizedUniqueSlugField) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_formfield_auto(cls): | ||||
|         cls._test_formfield(LocalizedAutoSlugField) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_formfield_unique(cls): | ||||
|         cls._test_formfield(LocalizedUniqueSlugField) | ||||
|  | ||||
|     @staticmethod | ||||
|     def _test_populate(model): | ||||
|     def test_populate(cls): | ||||
|         """Tests whether the populating feature works correctly.""" | ||||
|  | ||||
|         obj = model() | ||||
|         obj = cls.Model() | ||||
|         obj.title.en = 'this is my title' | ||||
|         obj.save() | ||||
|  | ||||
|         assert obj.slug.get('en') == slugify(obj.title) | ||||
|  | ||||
|     @staticmethod | ||||
|     def _test_populate_multiple_languages(model): | ||||
|         """Tests whether the populating feature correctly | ||||
|         works for all languages.""" | ||||
|     @classmethod | ||||
|     def test_populate_callable(cls): | ||||
|         """Tests whether the populating feature works correctly | ||||
|         when you specify a callable.""" | ||||
|  | ||||
|         obj = model() | ||||
|         def generate_slug(instance): | ||||
|             return instance.title | ||||
|  | ||||
|         model = get_fake_model({ | ||||
|             'title': LocalizedField(), | ||||
|             'slug': LocalizedUniqueSlugField(populate_from=generate_slug) | ||||
|         }) | ||||
|  | ||||
|         obj = cls.Model() | ||||
|         for lang_code, lang_name in settings.LANGUAGES: | ||||
|             obj.title.set(lang_code, 'title %s' % lang_name) | ||||
|  | ||||
| @@ -179,53 +130,122 @@ class LocalizedSlugFieldTestCase(TestCase): | ||||
|         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): | ||||
|     def test_populate_multiple_from_fields(): | ||||
|         """Tests whether populating the slug from multiple | ||||
|         fields works correctly.""" | ||||
|  | ||||
|         model = get_fake_model( | ||||
|             { | ||||
|                 'title': LocalizedField(), | ||||
|                 'name': models.CharField(max_length=255), | ||||
|                 'slug': LocalizedUniqueSlugField(populate_from=('title', 'name')) | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|         obj = model() | ||||
|         for lang_code, lang_name in settings.LANGUAGES: | ||||
|             obj.name = 'swen' | ||||
|             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-swen' % lang_name.lower() | ||||
|  | ||||
|     @staticmethod | ||||
|     def test_populate_multiple_from_fields_fk(): | ||||
|         """Tests whether populating the slug from multiple | ||||
|         fields works correctly.""" | ||||
|  | ||||
|         model_fk = get_fake_model( | ||||
|             { | ||||
|                 'name': LocalizedField(), | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|         model = get_fake_model( | ||||
|             { | ||||
|                 'title': LocalizedField(), | ||||
|                 'other': models.ForeignKey(model_fk), | ||||
|                 'slug': LocalizedUniqueSlugField(populate_from=('title', 'other.name')) | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|         other = model_fk.objects.create(name={settings.LANGUAGE_CODE: 'swen'}) | ||||
|  | ||||
|         obj = model() | ||||
|         for lang_code, lang_name in settings.LANGUAGES: | ||||
|             obj.other_id = other.id | ||||
|             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-swen' % lang_name.lower() | ||||
|  | ||||
|     @classmethod | ||||
|     def test_populate_multiple_languages(cls): | ||||
|         """Tests whether the populating feature correctly | ||||
|         works for all languages.""" | ||||
|  | ||||
|         obj = cls.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() | ||||
|  | ||||
|     @classmethod | ||||
|     def test_unique_slug(cls): | ||||
|         """Tests whether unique slugs are properly generated.""" | ||||
|  | ||||
|         title = 'myuniquetitle' | ||||
|  | ||||
|         obj = model() | ||||
|         obj = cls.Model() | ||||
|         obj.title.en = title | ||||
|         obj.save() | ||||
|  | ||||
|         for i in range(1, settings.LOCALIZED_FIELDS_MAX_RETRIES - 1): | ||||
|             another_obj = model() | ||||
|             another_obj = cls.Model() | ||||
|             another_obj.title.en = title | ||||
|             another_obj.save() | ||||
|  | ||||
|             assert another_obj.slug.en == '%s-%d' % (title, i) | ||||
|  | ||||
|     @staticmethod | ||||
|     def _test_unique_slug_utf(model): | ||||
|     @classmethod | ||||
|     def test_unique_slug_utf(cls): | ||||
|         """Tests whether generating a slug works | ||||
|         when the value consists completely out | ||||
|         of non-ASCII characters.""" | ||||
|  | ||||
|         obj = model() | ||||
|         obj = cls.Model() | ||||
|         obj.title.en = 'مكاتب للايجار بشارع بورسعيد' | ||||
|         obj.save() | ||||
|  | ||||
|         assert obj.slug.en == 'مكاتب-للايجار-بشارع-بورسعيد' | ||||
|  | ||||
|     @staticmethod | ||||
|     def _test_deconstruct(field_type): | ||||
|     def test_deconstruct(): | ||||
|         """Tests whether the :see:deconstruct | ||||
|         function properly retains options | ||||
|         specified in the constructor.""" | ||||
|  | ||||
|         field = field_type(populate_from='title') | ||||
|         field = LocalizedUniqueSlugField(populate_from='title') | ||||
|         _, _, _, kwargs = field.deconstruct() | ||||
|  | ||||
|         assert 'populate_from' in kwargs | ||||
|         assert kwargs['populate_from'] == field.populate_from | ||||
|  | ||||
|     @staticmethod | ||||
|     def _test_formfield(field_type): | ||||
|     def test_formfield(): | ||||
|         """Tests whether the :see:formfield method | ||||
|         returns a valid form field that is hidden.""" | ||||
|  | ||||
|         form_field = field_type(populate_from='title').formfield() | ||||
|         form_field = LocalizedUniqueSlugField(populate_from='title').formfield() | ||||
|  | ||||
|         assert isinstance(form_field, forms.CharField) | ||||
|         assert isinstance(form_field.widget, forms.HiddenInput) | ||||
|   | ||||
| @@ -143,6 +143,26 @@ class LocalizedValueTestCase(TestCase): | ||||
|         # there's no actual value | ||||
|         assert localized_value.get(other_language) != test_value | ||||
|  | ||||
|     @staticmethod | ||||
|     def test_str_fallback_custom_fallback(): | ||||
|         """Tests whether the :see:LocalizedValue class's | ||||
|         __str__'s fallback functionality properly respects | ||||
|         the LOCALIZED_FIELDS_FALLBACKS setting.""" | ||||
|  | ||||
|         test_value = 'myvalue' | ||||
|  | ||||
|         settings.LOCALIZED_FIELDS_FALLBACKS = { | ||||
|             'nl': ['ro'] | ||||
|         } | ||||
|  | ||||
|         localized_value = LocalizedValue({ | ||||
|             settings.LANGUAGE_CODE: settings.LANGUAGE_CODE, | ||||
|             'ro': 'ro' | ||||
|         }) | ||||
|  | ||||
|         with translation.override('nl'): | ||||
|             assert str(localized_value) == 'ro' | ||||
|  | ||||
|     @staticmethod | ||||
|     def test_deconstruct(): | ||||
|         """Tests whether the :see:LocalizedValue | ||||
|   | ||||
		Reference in New Issue
	
	Block a user