Started work on LocalizedMagicSlugField

This will be a superior version of LocalizedAutoSlugField, but one
that doesn't have concurrency issues and takes advantage of the
new UNIQUE CONSTRAINTs.
This commit is contained in:
Swen Kooij 2017-02-01 16:59:13 +02:00
parent 105c1e7b6b
commit e14350fbf3
8 changed files with 250 additions and 120 deletions

View File

@ -1,7 +1,7 @@
from .util import get_language_codes
from .forms import LocalizedFieldForm, LocalizedFieldWidget
from .fields import (LocalizedField, LocalizedBleachField,
LocalizedAutoSlugField)
LocalizedAutoSlugField, LocalizedMagicSlugField)
from .localized_value import LocalizedValue
from .models import LocalizedModel
@ -10,6 +10,7 @@ __all__ = [
'LocalizedField',
'LocalizedValue',
'LocalizedAutoSlugField',
'LocalizedMagicSlugField',
'LocalizedBleachField',
'LocalizedFieldWidget',
'LocalizedFieldForm',

View File

@ -1,10 +1,12 @@
from .localized_field import LocalizedField
from .localized_autoslug_field import LocalizedAutoSlugField
from .localized_magicslug_field import LocalizedMagicSlugField
from .localized_bleach_field import LocalizedBleachField
__all__ = [
'LocalizedField',
'LocalizedAutoSlugField',
'LocalizedBleachField'
'LocalizedMagicSlugField',
'LocalizedBleachField',
]

View File

@ -9,9 +9,8 @@ from ..localized_value import LocalizedValue
class LocalizedAutoSlugField(LocalizedField):
"""Custom version of :see:AutoSlugField that
can operate on :see:LocalizedField and provides
unique slugs for every language."""
"""Automatically provides slugs for a localized
field upon saving."""
def __init__(self, *args, **kwargs):
"""Initializes a new instance of :see:LocalizedAutoSlugField."""
@ -29,8 +28,8 @@ class LocalizedAutoSlugField(LocalizedField):
name, path, args, kwargs = super(
LocalizedAutoSlugField, self).deconstruct()
kwargs['populate_from'] = self.populate_from
kwargs['populate_from'] = self.populate_from
return name, path, args, kwargs
def formfield(self, **kwargs):

View File

@ -0,0 +1,65 @@
from django.conf import settings
from django.utils.text import slugify
from ..localized_value import LocalizedValue
from .localized_autoslug_field import LocalizedAutoSlugField
from ..util import get_language_codes
class LocalizedMagicSlugField(LocalizedAutoSlugField):
"""Automatically provides slugs for a localized
field upon saving."
An improved version of :see:LocalizedAutoSlugField,
which adds:
- Concurrency safety
- Improved performance
When in doubt, use this over :see:LocalizedAutoSlugField.
"""
def __init__(self, *args, **kwargs):
"""Initializes a new instance of :see:LocalizedMagicSlugField."""
self.populate_from = kwargs.pop('populate_from')
kwargs['uniqueness'] = kwargs.pop('uniqueness', get_language_codes())
super(LocalizedAutoSlugField, self).__init__(
*args,
**kwargs
)
def pre_save(self, instance, add: bool):
"""Ran just before the model is saved, allows us to built
the slug.
Arguments:
instance:
The model that is being saved.
add:
Indicates whether this is a new entry
to the database or an update.
Returns:
The localized slug that was generated.
"""
slugs = LocalizedValue()
for lang_code, _ in settings.LANGUAGES:
value = self._get_populate_from_value(
instance,
self.populate_from,
lang_code
)
if not value:
continue
slug = slugify(value, allow_unicode=True)
slugs.set(lang_code, slug)
setattr(instance, self.name, slugs)
return slugs

View File

@ -1,28 +1,23 @@
from django.contrib.postgres.operations import HStoreExtension
from django.db import connection, migrations
from django.db.migrations.executor import MigrationExecutor
import sys
from localized_fields import (LocalizedAutoSlugField, LocalizedField,
LocalizedModel)
MODEL = None
LocalizedModel, LocalizedMagicSlugField)
def get_fake_model():
def get_fake_model(name='TestModel', fields={}):
"""Creates a fake model to use during unit tests."""
global MODEL
attributes = {
'app_label': 'localized_fields',
'__module__': __name__,
'__name__': name
}
if MODEL:
return MODEL
class TestModel(LocalizedModel):
"""Model used for testing the :see:LocalizedAutoSlugField."""
app_label = 'localized_fields'
title = LocalizedField()
slug = LocalizedAutoSlugField(populate_from='title')
attributes.update(fields)
TestModel = type(name, (LocalizedModel,), attributes)
class TestProject:
@ -43,5 +38,4 @@ def get_fake_model():
schema_editor.create_model(TestModel)
MODEL = TestModel
return MODEL
return TestModel

View File

@ -1,94 +0,0 @@
from django import forms
from django.conf import settings
from django.test import TestCase
from django.utils.text import slugify
from localized_fields import LocalizedAutoSlugField
from .fake_model import get_fake_model
class LocalizedAutoSlugFieldTestCase(TestCase):
"""Tests the :see:LocalizedAutoSlugField class."""
TestModel = None
@classmethod
def setUpClass(cls):
"""Creates the test model in the database."""
super(LocalizedAutoSlugFieldTestCase, cls).setUpClass()
cls.TestModel = get_fake_model()
def test_populate(self):
"""Tests whether the :see:LocalizedAutoSlugField's
populating feature works correctly."""
obj = self.TestModel()
obj.title.en = 'this is my title'
obj.save()
assert obj.slug.get('en') == slugify(obj.title.en)
def test_populate_multiple_languages(self):
"""Tests whether the :see:LocalizedAutoSlugField's
populating feature correctly works for all languages."""
obj = self.TestModel()
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()
def test_unique_slug(self):
"""Tests whether the :see:LocalizedAutoSlugField
correctly generates unique slugs."""
obj = self.TestModel()
obj.title.en = 'title'
obj.save()
another_obj = self.TestModel()
another_obj.title.en = 'title'
another_obj.save()
assert another_obj.slug.en == 'title-1'
def test_unique_slug_utf(self):
"""Tests whether generating a slug works
when the value consists completely out
of non-ASCII characters."""
obj = self.TestModel()
obj.title.en = 'مكاتب للايجار بشارع بورسعيد'
obj.save()
assert obj.slug.en == 'مكاتب-للايجار-بشارع-بورسعيد'
@staticmethod
def test_deconstruct():
"""Tests whether the :see:deconstruct
function properly retains options
specified in the constructor."""
field = LocalizedAutoSlugField(populate_from='title')
_, _, _, kwargs = field.deconstruct()
assert 'populate_from' in kwargs
assert kwargs['populate_from'] == field.populate_from
@staticmethod
def test_formfield():
"""Tests whether the :see:formfield method
returns a valid form field that is hidden."""
field = LocalizedAutoSlugField(populate_from='title')
form_field = field.formfield()
assert isinstance(form_field, forms.CharField)
assert isinstance(form_field.widget, forms.HiddenInput)

View File

@ -1,6 +1,6 @@
from django.test import TestCase
from localized_fields import LocalizedValue
from localized_fields import LocalizedField, LocalizedValue
from .fake_model import get_fake_model
@ -16,7 +16,12 @@ class LocalizedModelTestCase(TestCase):
super(LocalizedModelTestCase, cls).setUpClass()
cls.TestModel = get_fake_model()
cls.TestModel = get_fake_model(
'LocalizedModelTestCase',
{
'title': LocalizedField()
}
)
@classmethod
def test_defaults(cls):

View File

@ -0,0 +1,158 @@
from django import forms
from django.conf import settings
from django.test import TestCase
from django.utils.text import slugify
from localized_fields import LocalizedField, LocalizedAutoSlugField, LocalizedMagicSlugField
from .fake_model import get_fake_model
class LocalizedSlugFieldTestCase(TestCase):
"""Tests the localized slug classes."""
AutoSlugModel = None
MagicSlugModel = None
@classmethod
def setUpClass(cls):
"""Creates the test models in the database."""
super(LocalizedSlugFieldTestCase, cls).setUpClass()
cls.AutoSlugModel = get_fake_model(
'LocalizedAutoSlugFieldTestModel',
{
'title': LocalizedField(),
'slug': LocalizedAutoSlugField(populate_from='title')
}
)
cls.MagicSlugModel = get_fake_model(
'LocalizedMagicSlugFieldTestModel',
{
'title': LocalizedField(),
'slug': LocalizedMagicSlugField(populate_from='title')
}
)
@classmethod
def test_populate_auto(cls):
cls._test_populate(cls.AutoSlugModel)
@classmethod
def test_populate_magic(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_magic(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_magic(cls):
cls._test_unique_slug(cls.MagicSlugModel)
@classmethod
def test_unique_slug_utf_auto(cls):
cls._test_unique_slug_utf(cls.AutoSlugModel)
@classmethod
def test_unique_slug_utf_magic(cls):
cls._test_unique_slug_utf(cls.MagicSlugModel)
@classmethod
def test_deconstruct_auto(cls):
cls._test_deconstruct(LocalizedAutoSlugField)
cls._test_deconstruct(LocalizedMagicSlugField)
@classmethod
def test_deconstruct_magic(cls):
cls._test_deconstruct(LocalizedMagicSlugField)
@classmethod
def test_formfield_auto(cls):
cls._test_formfield(LocalizedAutoSlugField)
@classmethod
def test_formfield_magic(cls):
cls._test_formfield(LocalizedMagicSlugField)
@staticmethod
def _test_populate(model):
"""Tests whether the populating feature works correctly."""
obj = model()
obj.title.en = 'this is my title'
obj.save()
assert obj.slug.get('en') == slugify(obj.title.en)
@staticmethod
def _test_populate_multiple_languages(model):
"""Tests whether the populating feature correctly
works for all languages."""
obj = 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()
@staticmethod
def _test_unique_slug(model):
"""Tests whether unique slugs are properly generated."""
obj = model()
obj.title.en = 'title'
obj.save()
another_obj = model()
another_obj.title.en = 'title'
another_obj.save()
assert another_obj.slug.en == 'title-1'
@staticmethod
def _test_unique_slug_utf(model):
"""Tests whether generating a slug works
when the value consists completely out
of non-ASCII characters."""
obj = model()
obj.title.en = 'مكاتب للايجار بشارع بورسعيد'
obj.save()
assert obj.slug.en == 'مكاتب-للايجار-بشارع-بورسعيد'
@staticmethod
def _test_deconstruct(field_type):
"""Tests whether the :see:deconstruct
function properly retains options
specified in the constructor."""
field = field_type(populate_from='title')
_, _, _, kwargs = field.deconstruct()
assert 'populate_from' in kwargs
assert kwargs['populate_from'] == field.populate_from
@staticmethod
def _test_formfield(field_type):
"""Tests whether the :see:formfield method
returns a valid form field that is hidden."""
form_field = field_type(populate_from='title').formfield()
assert isinstance(form_field, forms.CharField)
assert isinstance(form_field.widget, forms.HiddenInput)