mirror of
				https://github.com/SectorLabs/django-localized-fields.git
				synced 2025-10-26 17:08:58 +03:00 
			
		
		
		
	Compare commits
	
		
			16 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | f453c44a73 | ||
|  | 3cb8b04195 | ||
|  | 3f8fc77c4d | ||
|  | 9245c85e5d | ||
|  | 818a0a2fe3 | ||
|  | c206005cae | ||
|  | 3850c34374 | ||
|  | 99c8830f10 | ||
|  | d7bd217a90 | ||
|  | b9a4d3be2c | ||
|  | 6d7a937eac | ||
|  | 2e9b83e49b | ||
|  | 679dcafef6 | ||
|  | ad2ef34546 | ||
|  | 1317023160 | ||
|  | ca6b1c88fa | 
| @@ -1,6 +1,3 @@ | ||||
| [MASTER] | ||||
| load-plugins=pylint_common, pylint_django | ||||
|  | ||||
| [FORMAT] | ||||
| max-line-length=120 | ||||
|  | ||||
|   | ||||
							
								
								
									
										31
									
								
								.scrutinizer.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								.scrutinizer.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| checks: | ||||
|     python: | ||||
|         code_rating: true | ||||
|         duplicate_code: true | ||||
| tools: | ||||
|     pylint: | ||||
|         python_version: '3' | ||||
|         config_file: .pylintrc | ||||
| filter: | ||||
|     excluded_paths: | ||||
|         - '*/tests/*' | ||||
|         - '*/migrations/*' | ||||
| build: | ||||
|     environment: | ||||
|         python: '3.5.0' | ||||
|         node: 'v6.2.0' | ||||
|         variables: | ||||
|             DJANGO_SETTINGS_MODULES: settings | ||||
|             DATABASE_URL: postgres://scrutinizer:scrutinizer@localhost:5434/localized_fields | ||||
|         postgresql: true | ||||
|         redis: true | ||||
|     dependencies: | ||||
|         override: | ||||
|             - 'pip install -r requirements/test.txt' | ||||
|     tests: | ||||
|         override: | ||||
|             - | ||||
|                 command: coverage run manage.py test | ||||
|                 coverage: | ||||
|                     file: '.coverage' | ||||
|                     format: 'py-cc' | ||||
							
								
								
									
										49
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								README.rst
									
									
									
									
									
								
							| @@ -7,24 +7,14 @@ django-localized-fields | ||||
| .. image:: https://scrutinizer-ci.com/g/SectorLabs/django-localized-fields/badges/coverage.png | ||||
|     :target: https://scrutinizer-ci.com/g/SectorLabs/django-localized-fields/ | ||||
|  | ||||
| .. image:: https://travis-ci.com/SectorLabs/django-localized-fields.svg?token=sFgxhDFpypxkMcvhRoSz&branch=master | ||||
|     :target: https://travis-ci.com/SectorLabs/django-localized-fields | ||||
| .. image:: https://img.shields.io/github/license/SectorLabs/django-localized-fields.svg | ||||
|  | ||||
| .. image:: https://badge.fury.io/py/django-localized-fields.svg | ||||
|     :target: https://pypi.python.org/pypi/django-localized-fields | ||||
|  | ||||
| .. image:: https://img.shields.io/github/license/SectorLabs/django-localized-fields.svg | ||||
|  | ||||
| ``django-localized-fields`` is an implementation of a field class for Django models that allows the field's value to be set in multiple languages. It does this by utilizing the ``hstore`` type (PostgreSQL specific), which is available as ``models.HStoreField`` in Django 1.10. | ||||
|  | ||||
| This package requires Python 3.5 or newer and Django 1.10 or newer. | ||||
|  | ||||
| In the pipeline | ||||
| --------------- | ||||
| We're working on making this easier to setup and use. Any feedback is apreciated. Here's a short list of things we're working to improve: | ||||
|  | ||||
| * Make it unnecesarry to add anything to your `INSTALLED_APPS`. | ||||
| * Move generic PostgreSQL code to a separate package. | ||||
| This package requires Python 3.5 or newer, Django 1.10 or newer and PostgreSQL 9.6 or newer. | ||||
|  | ||||
| Installation | ||||
| ------------ | ||||
| @@ -45,14 +35,14 @@ Installation | ||||
|             'localized_fields' | ||||
|         ] | ||||
|  | ||||
| 3. Set the database engine to ``localized_fields.db_backend``: | ||||
| 3. Set the database engine to ``psqlextra.backend``: | ||||
|  | ||||
|     .. code-block:: python | ||||
|  | ||||
|         DATABASES = { | ||||
|             'default': { | ||||
|                 ... | ||||
|                 'ENGINE': 'localized_fields.db_backend' | ||||
|                 'ENGINE': 'psqlextra.backend' | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -212,6 +202,15 @@ Besides ``LocalizedField``, there's also: | ||||
|                    title = LocalizedField() | ||||
|                    slug = LocalizedUniqueSlugField(populate_from='title') | ||||
|  | ||||
|     By setting the option ``include_time=True`` | ||||
|  | ||||
|           .. code-block:: python | ||||
|  | ||||
|                slug = LocalizedUniqueSlugField(populate_from='title', include_time=True) | ||||
|  | ||||
|     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. | ||||
|  | ||||
| @@ -248,34 +247,26 @@ Besides ``LocalizedField``, there's also: | ||||
| Frequently asked questions (FAQ) | ||||
| -------------------------------- | ||||
|  | ||||
| 1. Why do I need to change the database back-end/engine? | ||||
|  | ||||
|     We utilize PostgreSQL's `hstore` data type, which allows you to store key-value pairs in a column.  In order to create `UNIQUE` constraints on specific key, we need to create a special type of index. We could do this without a custom database back-end, but it would require everyone to manually write their migrations. By using a custom database back-end, we added support for this. When changing the `uniqueness` constraint on a `LocalizedField`, our custom database back-end takes care of creating, updating and deleting these constraints/indexes in the database. | ||||
|  | ||||
| 2. I am already using a custom database back-end, can I still use yours? | ||||
|  | ||||
|     Yes. You can set the ``LOCALIZED_FIELDS_DB_BACKEND_BASE`` setting to your current back-end. This will instruct our custom database back-end to inherit from the database back-end you specified. **Warning**: this will only work if the base you specified indirectly inherits from the standard PostgreSQL database back-end. | ||||
|  | ||||
| 3. Does this package work with Python 2? | ||||
| 1. Does this package work with Python 2? | ||||
|  | ||||
|     No. Only Python 3.5 or newer is supported. We're using type hints. These do not work well under older versions of Python. | ||||
|  | ||||
| 4. Does this package work with Django 1.X? | ||||
| 2. Does this package work with Django 1.X? | ||||
|  | ||||
|     No. Only Django 1.10 or newer is supported. This is because we rely on Django's ``HStoreField``. | ||||
|  | ||||
| 5. Does this package come with support for Django Admin? | ||||
| 3. Does this package come with support for Django Admin? | ||||
|  | ||||
|     Yes. Our custom fields come with a special form that will automatically be used in Django Admin if the field is of ``LocalizedField``. | ||||
|  | ||||
| 7. Why should I pick this over any of the other translation packages out there? | ||||
| 4. Why should I pick this over any of the other translation packages out there? | ||||
|  | ||||
|     You should pick whatever you feel comfortable with. This package stores translations in your database without having to have translation tables. It however only works on PostgreSQL. | ||||
|  | ||||
| 8. I am using PostgreSQL <8.4, can I use this? | ||||
| 5. I am using PostgreSQL <9.6, can I use this? | ||||
|  | ||||
|     No. The ``hstore`` data type was introduced in PostgreSQL 8.4. | ||||
|     No. The ``hstore`` data type was introduced in PostgreSQL 9.6. | ||||
|  | ||||
| 9. I am using this package. Can I give you some beer? | ||||
| 6. I am using this package. Can I give you some beer? | ||||
|  | ||||
|     Yes! If you're ever in the area of Cluj-Napoca, Romania, swing by :) | ||||
|   | ||||
| @@ -1,224 +0,0 @@ | ||||
| import importlib | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.core.exceptions import ImproperlyConfigured | ||||
| from django.db.backends.postgresql.base import \ | ||||
|     DatabaseWrapper as Psycopg2DatabaseWrapper | ||||
|  | ||||
| from ..fields import LocalizedField | ||||
|  | ||||
|  | ||||
| def _get_backend_base(): | ||||
|     """Gets the base class for the custom database back-end. | ||||
|  | ||||
|     This should be the Django PostgreSQL back-end. However, | ||||
|     some people are already using a custom back-end from | ||||
|     another package. We are nice people and expose an option | ||||
|     that allows them to configure the back-end we base upon. | ||||
|  | ||||
|     As long as the specified base eventually also has | ||||
|     the PostgreSQL back-end as a base, then everything should | ||||
|     work as intended. | ||||
|     """ | ||||
|     base_class_name = getattr( | ||||
|         settings, | ||||
|         'LOCALIZED_FIELDS_DB_BACKEND_BASE', | ||||
|         'django.db.backends.postgresql' | ||||
|     ) | ||||
|  | ||||
|     base_class_module = importlib.import_module(base_class_name + '.base') | ||||
|     base_class = getattr(base_class_module, 'DatabaseWrapper', None) | ||||
|  | ||||
|     if not base_class: | ||||
|         raise ImproperlyConfigured(( | ||||
|             '\'%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 | ||||
|  | ||||
|  | ||||
| def _get_schema_editor_base(): | ||||
|     """Gets the base class for the schema editor. | ||||
|  | ||||
|     We have to use the configured base back-end's | ||||
|     schema editor for this.""" | ||||
|     return _get_backend_base().SchemaEditorClass | ||||
|  | ||||
|  | ||||
| class SchemaEditor(_get_schema_editor_base()): | ||||
|     """Custom schema editor for hstore indexes. | ||||
|  | ||||
|     This allows us to put UNIQUE constraints for | ||||
|     localized fields.""" | ||||
|  | ||||
|     sql_hstore_unique_create = "CREATE UNIQUE INDEX {name} ON {table}{using} ({columns}){extra}" | ||||
|     sql_hstore_unique_drop = "DROP INDEX IF EXISTS {name}" | ||||
|  | ||||
|     @staticmethod | ||||
|     def _hstore_unique_name(model, field, keys): | ||||
|         """Gets the name for a UNIQUE INDEX that applies | ||||
|         to one or more keys in a hstore field. | ||||
|  | ||||
|         Arguments: | ||||
|             model: | ||||
|                 The model the field is a part of. | ||||
|  | ||||
|             field: | ||||
|                 The hstore field to create a | ||||
|                 UNIQUE INDEX for. | ||||
|  | ||||
|             key: | ||||
|                 The name of the hstore key | ||||
|                 to create the name for. | ||||
|  | ||||
|                 This can also be a tuple | ||||
|                 of multiple names. | ||||
|  | ||||
|         Returns: | ||||
|             The name for the UNIQUE index. | ||||
|         """ | ||||
|         postfix = '_'.join(keys) | ||||
|         return '{table_name}_{field_name}_unique_{postfix}'.format( | ||||
|             table_name=model._meta.db_table, | ||||
|             field_name=field.column, | ||||
|             postfix=postfix | ||||
|         ) | ||||
|  | ||||
|     def _drop_hstore_unique(self, model, field, keys): | ||||
|         """Drops a UNIQUE constraint for the specified hstore keys.""" | ||||
|  | ||||
|         name = self._hstore_unique_name(model, field, keys) | ||||
|         sql = self.sql_hstore_unique_drop.format(name=name) | ||||
|         self.execute(sql) | ||||
|  | ||||
|     def _create_hstore_unique(self, model, field, keys): | ||||
|         """Creates a UNIQUE constraint for the specified hstore keys.""" | ||||
|  | ||||
|         name = self._hstore_unique_name(model, field, keys) | ||||
|         columns = [ | ||||
|             '(%s->\'%s\')' % (field.column, key) | ||||
|             for key in keys | ||||
|         ] | ||||
|         sql = self.sql_hstore_unique_create.format( | ||||
|             name=name, | ||||
|             table=model._meta.db_table, | ||||
|             using='', | ||||
|             columns=','.join(columns), | ||||
|             extra='' | ||||
|         ) | ||||
|         self.execute(sql) | ||||
|  | ||||
|     def _apply_hstore_constraints(self, method, model, field): | ||||
|         """Creates/drops UNIQUE constraints for a field.""" | ||||
|  | ||||
|         def _compose_keys(constraint): | ||||
|             if isinstance(constraint, str): | ||||
|                 return [constraint] | ||||
|  | ||||
|             return constraint | ||||
|  | ||||
|         uniqueness = getattr(field, 'uniqueness', None) | ||||
|         if not uniqueness: | ||||
|             return | ||||
|  | ||||
|         for keys in uniqueness: | ||||
|             method( | ||||
|                 model, | ||||
|                 field, | ||||
|                 _compose_keys(keys) | ||||
|             ) | ||||
|  | ||||
|     def _update_hstore_constraints(self, model, old_field, new_field): | ||||
|         """Updates the UNIQUE constraints for the specified field.""" | ||||
|  | ||||
|         old_uniqueness = getattr(old_field, 'uniqueness', None) | ||||
|         new_uniqueness = getattr(new_field, 'uniqueness', None) | ||||
|  | ||||
|         # drop any old uniqueness constraints | ||||
|         if old_uniqueness: | ||||
|             self._apply_hstore_constraints( | ||||
|                 self._drop_hstore_unique, | ||||
|                 model, | ||||
|                 old_field | ||||
|             ) | ||||
|  | ||||
|         # (re-)create uniqueness constraints | ||||
|         if new_uniqueness: | ||||
|             self._apply_hstore_constraints( | ||||
|                 self._create_hstore_unique, | ||||
|                 model, | ||||
|                 new_field | ||||
|             ) | ||||
|  | ||||
|     def _alter_field(self, model, old_field, new_field, *args, **kwargs): | ||||
|         """Ran when the configuration on a field changed.""" | ||||
|  | ||||
|         super()._alter_field( | ||||
|             model, old_field, new_field, | ||||
|             *args, **kwargs | ||||
|         ) | ||||
|  | ||||
|         is_old_field_localized = isinstance(old_field, LocalizedField) | ||||
|         is_new_field_localized = isinstance(new_field, LocalizedField) | ||||
|  | ||||
|         if is_old_field_localized or is_new_field_localized: | ||||
|             self._update_hstore_constraints(model, old_field, new_field) | ||||
|  | ||||
|     def create_model(self, model): | ||||
|         """Ran when a new model is created.""" | ||||
|  | ||||
|         super().create_model(model) | ||||
|  | ||||
|         for field in model._meta.local_fields: | ||||
|             if not isinstance(field, LocalizedField): | ||||
|                 continue | ||||
|  | ||||
|             self._apply_hstore_constraints( | ||||
|                 self._create_hstore_unique, | ||||
|                 model, | ||||
|                 field | ||||
|             ) | ||||
|  | ||||
|     def delete_model(self, model): | ||||
|         """Ran when a model is being deleted.""" | ||||
|  | ||||
|         super().delete_model(model) | ||||
|  | ||||
|         for field in model._meta.local_fields: | ||||
|             if not isinstance(field, LocalizedField): | ||||
|                 continue | ||||
|  | ||||
|             self._apply_hstore_constraints( | ||||
|                 self._drop_hstore_unique, | ||||
|                 model, | ||||
|                 field | ||||
|             ) | ||||
|  | ||||
|  | ||||
| class DatabaseWrapper(_get_backend_base()): | ||||
|     """Wraps the standard PostgreSQL database back-end. | ||||
|  | ||||
|     Overrides the schema editor with our custom | ||||
|     schema editor and makes sure the `hstore` | ||||
|     extension is enabled.""" | ||||
|  | ||||
|     SchemaEditorClass = SchemaEditor | ||||
|  | ||||
|     def prepare_database(self): | ||||
|         """Ran to prepare the configured database. | ||||
|  | ||||
|         This is where we enable the `hstore` extension | ||||
|         if it wasn't enabled yet.""" | ||||
|  | ||||
|         super().prepare_database() | ||||
|         with self.cursor() as cursor: | ||||
|             cursor.execute('CREATE EXTENSION IF NOT EXISTS hstore') | ||||
| @@ -1,4 +1,5 @@ | ||||
| from typing import Callable | ||||
| from datetime import datetime | ||||
|  | ||||
| from django import forms | ||||
| from django.conf import settings | ||||
| @@ -16,6 +17,7 @@ class LocalizedAutoSlugField(LocalizedField): | ||||
|         """Initializes a new instance of :see:LocalizedAutoSlugField.""" | ||||
|  | ||||
|         self.populate_from = kwargs.pop('populate_from', None) | ||||
|         self.include_time = kwargs.pop('include_time', False) | ||||
|  | ||||
|         super(LocalizedAutoSlugField, self).__init__( | ||||
|             *args, | ||||
| @@ -30,6 +32,7 @@ class LocalizedAutoSlugField(LocalizedField): | ||||
|             LocalizedAutoSlugField, self).deconstruct() | ||||
|  | ||||
|         kwargs['populate_from'] = self.populate_from | ||||
|         kwargs['include_time'] = self.include_time | ||||
|         return name, path, args, kwargs | ||||
|  | ||||
|     def formfield(self, **kwargs): | ||||
| @@ -76,6 +79,9 @@ class LocalizedAutoSlugField(LocalizedField): | ||||
|             if not value: | ||||
|                 continue | ||||
|  | ||||
|             if self.include_time: | ||||
|                 value += '-%s' % datetime.now().microsecond | ||||
|  | ||||
|             def is_unique(slug: str, language: str) -> bool: | ||||
|                 """Gets whether the specified slug is unique.""" | ||||
|  | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| from django.conf import settings | ||||
| from django.contrib.postgres.fields import HStoreField | ||||
| from django.db.utils import IntegrityError | ||||
|  | ||||
| from localized_fields import LocalizedFieldForm | ||||
| from psqlextra.fields import HStoreField | ||||
|  | ||||
| from ..localized_value import LocalizedValue | ||||
|  | ||||
|  | ||||
| @@ -14,12 +14,11 @@ class LocalizedField(HStoreField): | ||||
|  | ||||
|     Meta = None | ||||
|  | ||||
|     def __init__(self, *args, uniqueness=None, **kwargs): | ||||
|         """Initializes a new instance of :see:LocalizedValue.""" | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         """Initializes a new instance of :see:LocalizedField.""" | ||||
|  | ||||
|         super(LocalizedField, self).__init__(*args, **kwargs) | ||||
|  | ||||
|         self.uniqueness = uniqueness | ||||
|         super(LocalizedField, self).__init__( | ||||
|             *args, required=[settings.LANGUAGE_CODE], **kwargs) | ||||
|  | ||||
|     @staticmethod | ||||
|     def from_db_value(value, *_): | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| from datetime import datetime | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.utils.text import slugify | ||||
| from django.core.exceptions import ImproperlyConfigured | ||||
| @@ -34,6 +36,18 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField): | ||||
|         ) | ||||
|  | ||||
|         self.populate_from = kwargs.pop('populate_from') | ||||
|         self.include_time = kwargs.pop('include_time', False) | ||||
|  | ||||
|     def deconstruct(self): | ||||
|         """Deconstructs the field into something the database | ||||
|         can store.""" | ||||
|  | ||||
|         name, path, args, kwargs = super( | ||||
|             LocalizedUniqueSlugField, self).deconstruct() | ||||
|  | ||||
|         kwargs['populate_from'] = self.populate_from | ||||
|         kwargs['include_time'] = self.include_time | ||||
|         return name, path, args, kwargs | ||||
|  | ||||
|     def pre_save(self, instance, add: bool): | ||||
|         """Ran just before the model is saved, allows us to built | ||||
| @@ -70,8 +84,25 @@ class LocalizedUniqueSlugField(LocalizedAutoSlugField): | ||||
|                 continue | ||||
|  | ||||
|             slug = slugify(value, allow_unicode=True) | ||||
|  | ||||
|             # verify whether it's needed to re-generate a slug, | ||||
|             # if not, re-use the same slug | ||||
|             if instance.pk is not None: | ||||
|                 current_slug = getattr(instance, self.name).get(lang_code) | ||||
|                 if current_slug is not None: | ||||
|                     stripped_slug = current_slug[0:current_slug.rfind('-')] | ||||
|                     if slug == stripped_slug: | ||||
|                         slugs.set(lang_code, current_slug) | ||||
|                         continue | ||||
|  | ||||
|             if self.include_time: | ||||
|                 slug += '-%d' % datetime.now().microsecond | ||||
|  | ||||
|             if instance.retries > 0: | ||||
|                 slug += '-%d' % instance.retries | ||||
|                 # do not add another - if we already added time | ||||
|                 if not self.include_time: | ||||
|                     slug += '-' | ||||
|                 slug += '%d' % instance.retries | ||||
|  | ||||
|             slugs.set(lang_code, slug) | ||||
|  | ||||
|   | ||||
							
								
								
									
										1
									
								
								requirements/base.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								requirements/base.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| django-postgres-extra==1.2 | ||||
| @@ -1,3 +1,5 @@ | ||||
| -r base.txt | ||||
|  | ||||
| coverage==4.2 | ||||
| Django==1.10.2 | ||||
| django-autoslug==1.9.3 | ||||
|   | ||||
| @@ -11,7 +11,7 @@ DATABASES = { | ||||
|     'default': dj_database_url.config(default='postgres:///localized_fields') | ||||
| } | ||||
|  | ||||
| DATABASES['default']['ENGINE'] = 'localized_fields.db_backend' | ||||
| DATABASES['default']['ENGINE'] = 'psqlextra.backend' | ||||
|  | ||||
| LANGUAGE_CODE = 'en' | ||||
| LANGUAGES = ( | ||||
|   | ||||
							
								
								
									
										5
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								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.7', | ||||
|     version='3.1', | ||||
|     packages=find_packages(), | ||||
|     include_package_data=True, | ||||
|     license='MIT License', | ||||
| @@ -17,6 +17,9 @@ setup( | ||||
|     author='Sector Labs', | ||||
|     author_email='open-source@sectorlabs.ro', | ||||
|     keywords=['django', 'localized', 'language', 'models', 'fields'], | ||||
|     install_requires=[ | ||||
|         'django-postgres-extra>=1.2' | ||||
|     ], | ||||
|     classifiers=[ | ||||
|         'Environment :: Web Environment', | ||||
|         'Framework :: Django', | ||||
|   | ||||
| @@ -1,87 +0,0 @@ | ||||
| from unittest import mock | ||||
|  | ||||
| from django.db import connection | ||||
| from django.apps import apps | ||||
| from django.conf import settings | ||||
| from django.test import TestCase | ||||
| from django.db.backends.base.schema import BaseDatabaseSchemaEditor | ||||
|  | ||||
| from localized_fields import LocalizedField, get_language_codes | ||||
|  | ||||
| from .fake_model import define_fake_model | ||||
|  | ||||
|  | ||||
| class DBBackendTestCase(TestCase): | ||||
|     """Tests the custom database back-end.""" | ||||
|  | ||||
|     @staticmethod | ||||
|     def test_hstore_extension_enabled(): | ||||
|         """Tests whether the `hstore` extension was | ||||
|         enabled automatically.""" | ||||
|  | ||||
|         with connection.cursor() as cursor: | ||||
|             cursor.execute(( | ||||
|                 'SELECT count(*) FROM pg_extension ' | ||||
|                 'WHERE extname = \'hstore\'' | ||||
|             )) | ||||
|  | ||||
|             assert cursor.fetchone()[0] == 1 | ||||
|  | ||||
|     @classmethod | ||||
|     def test_migration_create_drop_model(cls): | ||||
|         """Tests whether models containing a :see:LocalizedField | ||||
|         with a `uniqueness` constraint get created properly, | ||||
|         with the contraints in the database.""" | ||||
|  | ||||
|         model = define_fake_model('NewModel', { | ||||
|             'title': LocalizedField(uniqueness=get_language_codes()) | ||||
|         }) | ||||
|  | ||||
|         # create the model in db and verify the indexes are being created | ||||
|         with mock.patch.object(BaseDatabaseSchemaEditor, 'execute') as execute: | ||||
|             with connection.schema_editor() as schema_editor: | ||||
|                 schema_editor.create_model(model) | ||||
|  | ||||
|             create_index_calls = [ | ||||
|                 call for call in execute.mock_calls if 'CREATE UNIQUE INDEX' in str(call) | ||||
|             ] | ||||
|  | ||||
|             assert len(create_index_calls) == len(settings.LANGUAGES) | ||||
|  | ||||
|         # delete the model in the db and verify the indexes are being deleted | ||||
|         with mock.patch.object(BaseDatabaseSchemaEditor, 'execute') as execute: | ||||
|             with connection.schema_editor() as schema_editor: | ||||
|                 schema_editor.delete_model(model) | ||||
|  | ||||
|             drop_index_calls = [ | ||||
|                 call for call in execute.mock_calls if 'DROP INDEX' in str(call) | ||||
|             ] | ||||
|  | ||||
|             assert len(drop_index_calls) == len(settings.LANGUAGES) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_migration_alter_field(cls): | ||||
|         """Tests whether the back-end correctly removes and | ||||
|         adds `uniqueness` constraints when altering a :see:LocalizedField.""" | ||||
|  | ||||
|         define_fake_model('ExistingModel', { | ||||
|             'title': LocalizedField(uniqueness=get_language_codes()) | ||||
|         }) | ||||
|  | ||||
|         app_config = apps.get_app_config('tests') | ||||
|  | ||||
|         with mock.patch.object(BaseDatabaseSchemaEditor, 'execute') as execute: | ||||
|             with connection.schema_editor() as schema_editor: | ||||
|                 dynmodel = app_config.get_model('ExistingModel') | ||||
|                 schema_editor.alter_field( | ||||
|                     dynmodel, | ||||
|                     dynmodel._meta.fields[1], | ||||
|                     dynmodel._meta.fields[1] | ||||
|                 ) | ||||
|  | ||||
|             index_calls = [ | ||||
|                 call for call in execute.mock_calls | ||||
|                 if 'INDEX' in str(call) and 'title' in str(call) | ||||
|             ] | ||||
|  | ||||
|             assert len(index_calls) == len(settings.LANGUAGES) * 2 | ||||
| @@ -1,11 +1,12 @@ | ||||
| import copy | ||||
|  | ||||
| from django import forms | ||||
| from django.conf import settings | ||||
| from django.test import TestCase | ||||
| from django.db.utils import IntegrityError | ||||
| from django.utils.text import slugify | ||||
|  | ||||
| from localized_fields import (LocalizedField, LocalizedAutoSlugField, | ||||
|                               LocalizedUniqueSlugField) | ||||
| from django.utils.text import slugify | ||||
|  | ||||
| from .fake_model import get_fake_model | ||||
|  | ||||
| @@ -43,7 +44,7 @@ class LocalizedSlugFieldTestCase(TestCase): | ||||
|         cls._test_populate(cls.AutoSlugModel) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_populate_magic(cls): | ||||
|     def test_populate_unique(cls): | ||||
|         cls._test_populate(cls.MagicSlugModel) | ||||
|  | ||||
|     @classmethod | ||||
| @@ -51,7 +52,7 @@ class LocalizedSlugFieldTestCase(TestCase): | ||||
|         cls._test_populate_multiple_languages(cls.AutoSlugModel) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_populate_multiple_languages_magic(cls): | ||||
|     def test_populate_multiple_languages_unique(cls): | ||||
|         cls._test_populate_multiple_languages(cls.MagicSlugModel) | ||||
|  | ||||
|     @classmethod | ||||
| @@ -59,14 +60,62 @@ class LocalizedSlugFieldTestCase(TestCase): | ||||
|         cls._test_unique_slug(cls.AutoSlugModel) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_unique_slug_magic(cls): | ||||
|     def test_unique_slug_unique(cls): | ||||
|         cls._test_unique_slug(cls.MagicSlugModel) | ||||
|  | ||||
|     def test_unique_slug_magic_max_retries(self): | ||||
|         """Tests whether the magic slug implementation doesn't | ||||
|     @staticmethod | ||||
|     def test_unique_slug_with_time(): | ||||
|         """Tests whether the primary key is included in | ||||
|         the slug when the 'use_pk' option is enabled.""" | ||||
|  | ||||
|         title = 'myuniquetitle' | ||||
|  | ||||
|         PkModel = get_fake_model( | ||||
|             'PkModel', | ||||
|             { | ||||
|                 'title': LocalizedField(), | ||||
|                 'slug': LocalizedUniqueSlugField(populate_from='title', include_time=True) | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|         obj = PkModel() | ||||
|         obj.title.en = title | ||||
|         obj.save() | ||||
|  | ||||
|         assert obj.slug.en.startswith('%s-' % title) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_uniue_slug_no_change(cls): | ||||
|         """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) | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|         title = 'myuniquetitle' | ||||
|  | ||||
|         obj = NoChangeSlugModel() | ||||
|         obj.title.en = title | ||||
|         obj.title.nl = title | ||||
|         obj.save() | ||||
|  | ||||
|         old_slug_en = copy.deepcopy(obj.slug.en) | ||||
|         old_slug_nl = copy.deepcopy(obj.slug.nl) | ||||
|         obj.title.nl += 'beer' | ||||
|         obj.save() | ||||
|  | ||||
|         assert old_slug_en == obj.slug.en | ||||
|         assert old_slug_nl != obj.slug.nl | ||||
|  | ||||
|     def test_unique_slug_unique_max_retries(self): | ||||
|         """Tests whether the unique slug implementation doesn't | ||||
|         try to find a slug forever and gives up after a while.""" | ||||
|  | ||||
|         title = 'mymagictitle' | ||||
|         title = 'myuniquetitle' | ||||
|  | ||||
|         obj = self.MagicSlugModel() | ||||
|         obj.title.en = title | ||||
| @@ -83,7 +132,7 @@ class LocalizedSlugFieldTestCase(TestCase): | ||||
|         cls._test_unique_slug_utf(cls.AutoSlugModel) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_unique_slug_utf_magic(cls): | ||||
|     def test_unique_slug_utf_unique(cls): | ||||
|         cls._test_unique_slug_utf(cls.MagicSlugModel) | ||||
|  | ||||
|     @classmethod | ||||
| @@ -91,7 +140,7 @@ class LocalizedSlugFieldTestCase(TestCase): | ||||
|         cls._test_deconstruct(LocalizedAutoSlugField) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_deconstruct_magic(cls): | ||||
|     def test_deconstruct_unique(cls): | ||||
|         cls._test_deconstruct(LocalizedUniqueSlugField) | ||||
|  | ||||
|     @classmethod | ||||
| @@ -99,7 +148,7 @@ class LocalizedSlugFieldTestCase(TestCase): | ||||
|         cls._test_formfield(LocalizedAutoSlugField) | ||||
|  | ||||
|     @classmethod | ||||
|     def test_formfield_magic(cls): | ||||
|     def test_formfield_unique(cls): | ||||
|         cls._test_formfield(LocalizedUniqueSlugField) | ||||
|  | ||||
|     @staticmethod | ||||
| @@ -130,7 +179,7 @@ class LocalizedSlugFieldTestCase(TestCase): | ||||
|     def _test_unique_slug(model): | ||||
|         """Tests whether unique slugs are properly generated.""" | ||||
|  | ||||
|         title = 'mymagictitle' | ||||
|         title = 'myuniquetitle' | ||||
|  | ||||
|         obj = model() | ||||
|         obj.title.en = title | ||||
|   | ||||
		Reference in New Issue
	
	Block a user