mirror of
https://github.com/SectorLabs/django-localized-fields.git
synced 2025-04-25 03:32:55 +03:00
Added custom back-end to allow uniqueness
This commit is contained in:
parent
ed1559ec31
commit
680383b636
93
README.rst
93
README.rst
@ -24,7 +24,6 @@ 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`.
|
||||
* Make it unnecesarry to modify your migrations manually to enable the PostgreSQL HStore extension.
|
||||
|
||||
Installation
|
||||
------------
|
||||
@ -45,6 +44,17 @@ Installation
|
||||
'localized_fields'
|
||||
]
|
||||
|
||||
3. Set the database engine to ``localized_fields.db_backend``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
...
|
||||
'ENGINE': 'localized_fields.db_backend'
|
||||
}
|
||||
}
|
||||
|
||||
3. Set ``LANGUAGES` and `LANGUAGE_CODE`` in your settings:
|
||||
|
||||
.. code-block:: python
|
||||
@ -72,23 +82,6 @@ Inherit your model from ``LocalizedModel`` and declare fields on your model as `
|
||||
class MyModel(LocalizedModel):
|
||||
title = LocalizedField()
|
||||
|
||||
|
||||
Create your migrations using ``python manage.py makemigrations``. Open the generated migration in your favorite editor and setup the HStore extension before the first ``CreateModel`` or ``AddField`` operation by adding a migration with the `HStoreExtension` operation. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.contrib.postgres.operations import HStoreExtension
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
...
|
||||
|
||||
operations = [
|
||||
HStoreExtension(),
|
||||
...
|
||||
]
|
||||
|
||||
Then apply the migration using ``python manage.py migrate``.
|
||||
|
||||
``django-localized-fields`` integrates with Django's i18n system, in order for certain languages to be available you have to correctly configure the ``LANGUAGES`` and ``LANGUAGE_CODE`` settings:
|
||||
|
||||
.. code-block:: python
|
||||
@ -100,6 +93,8 @@ Then apply the migration using ``python manage.py migrate``.
|
||||
('ro', 'Romanian')
|
||||
)
|
||||
|
||||
All the ``LocalizedField`` you define now will be available in the configured languages.
|
||||
|
||||
Basic usage
|
||||
^^^^^^^^^^^
|
||||
.. code-block:: python
|
||||
@ -141,22 +136,70 @@ You can also explicitly set a value in a certain language:
|
||||
|
||||
Constraints
|
||||
^^^^^^^^^^^
|
||||
By default, the following constraints apply to a ``LocalizedField``:
|
||||
|
||||
* Only the default language is ``required``. The other languages are optional and can be ``NULL``.
|
||||
* If ``null=True`` is specified on the ``LocalizedField``, then none of the languages are required.
|
||||
**Required/Optional**
|
||||
|
||||
At the moment, it is not possible to select two languages to be marked as required. The constraint is **not** enforced on a database level.
|
||||
|
||||
* Make the primary language **required** and the others optional (this is the **default**):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyModel(models.Model):
|
||||
title = LocalizedField(required=True)
|
||||
|
||||
* Make all languages optional:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyModel(models.Model):
|
||||
title = LocalizedField(null=True)
|
||||
|
||||
**Uniqueness**
|
||||
|
||||
By default the values stored in a ``LocalizedField`` are *not unique*. You can enforce uniqueness for certain languages. This uniqueness constraint is enforced on a database level using a ``UNIQUE INDEX``.
|
||||
|
||||
* Enforce uniqueness for one or more languages:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyModel(models.Model):
|
||||
title = LocalizedField(uniqueness=['en', 'ro'])
|
||||
|
||||
* Enforce uniqueness for **all** languages:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from localized_fields import get_language_codes
|
||||
|
||||
class MyModel(models.Model):
|
||||
title = LocalizedField(uniqueness=get_language_codes())
|
||||
|
||||
* Enforce uniqueness for one ore more languages **together** (similar to Django's ``unique_together``):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyModel(models.Model):
|
||||
title = LocalizedField(uniqueness=[('en', 'ro')])
|
||||
|
||||
* Enforce uniqueness for **all** languages **together**:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from localized_fields import get_language_codes
|
||||
|
||||
class MyModel(models.Model):
|
||||
title = LocalizedField(uniqueness=[(*get_language_codes())])
|
||||
|
||||
At the moment it is *not* possible to specifically instruct ``LocalizedField`` to mark certain languages as required or optional.
|
||||
|
||||
Other fields
|
||||
^^^^^^^^^^^^
|
||||
Besides ``LocalizedField``, there's also:
|
||||
|
||||
* ``LocalizedAutoSlugField``
|
||||
Automatically creates a slug for every language from the specified field. Depends upon:
|
||||
* django-autoslug
|
||||
Automatically creates a slug for every language from the specified field.
|
||||
|
||||
Currently only supports `populate_from`. Example usage:
|
||||
Currently only supports ``populate_from``. Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
0
localized_fields/db_backend/__init__.py
Normal file
0
localized_fields/db_backend/__init__.py
Normal file
176
localized_fields/db_backend/base.py
Normal file
176
localized_fields/db_backend/base.py
Normal file
@ -0,0 +1,176 @@
|
||||
import importlib
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db.backends.postgresql.base import \
|
||||
DatabaseWrapper as Psycopg2DatabaseWrapper
|
||||
|
||||
|
||||
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.'
|
||||
))
|
||||
|
||||
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.'
|
||||
))
|
||||
|
||||
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 _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)
|
||||
|
||||
def _compose_keys(constraint):
|
||||
if isinstance(constraint, str):
|
||||
return [constraint]
|
||||
|
||||
return constraint
|
||||
|
||||
# drop any old uniqueness constraints
|
||||
if old_uniqueness:
|
||||
for keys in old_uniqueness:
|
||||
self._drop_hstore_unique(
|
||||
model,
|
||||
old_field,
|
||||
_compose_keys(keys)
|
||||
)
|
||||
|
||||
# (re-)create uniqueness constraints
|
||||
if new_uniqueness:
|
||||
for keys in new_uniqueness:
|
||||
self._create_hstore_unique(
|
||||
model,
|
||||
old_field,
|
||||
_compose_keys(keys)
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
self._update_hstore_constraints(model, old_field, new_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')
|
@ -4,7 +4,6 @@ from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.text import slugify
|
||||
|
||||
from ..forms import LocalizedFieldForm
|
||||
from .localized_field import LocalizedField
|
||||
from .localized_value import LocalizedValue
|
||||
|
||||
|
@ -14,11 +14,13 @@ class LocalizedField(HStoreField):
|
||||
|
||||
Meta = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args, uniqueness=None, **kwargs):
|
||||
"""Initializes a new instance of :see:LocalizedValue."""
|
||||
|
||||
super(LocalizedField, self).__init__(*args, **kwargs)
|
||||
|
||||
self.uniqueness = uniqueness
|
||||
|
||||
@staticmethod
|
||||
def from_db_value(value, *_):
|
||||
"""Turns the specified database value into its Python
|
||||
@ -157,3 +159,14 @@ class LocalizedField(HStoreField):
|
||||
|
||||
defaults.update(kwargs)
|
||||
return super().formfield(**defaults)
|
||||
|
||||
def deconstruct(self):
|
||||
"""Gets the values to pass to :see:__init__ when
|
||||
re-creating this object."""
|
||||
|
||||
values = super(LocalizedField, self).deconstruct()
|
||||
values[3].update({
|
||||
'uniqueness': self.uniqueness
|
||||
})
|
||||
|
||||
return values
|
||||
|
@ -1,3 +1,5 @@
|
||||
"""This module is unused, but should be contributed to Django."""
|
||||
|
||||
from typing import List
|
||||
|
||||
from django.db import models
|
||||
|
Loading…
x
Reference in New Issue
Block a user