Mix of Python and Django utility functions, classed etc.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

173 lines
6.3 KiB

from functools import reduce
from django.conf import settings
from django.core import validators
from django.db import models
from django.db.models import fields as django_fields
from django.db.models.signals import pre_save, post_save
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from dirtyfields import DirtyFieldsMixin
from ..text import slugify, downgrading_slugify
# TODO: Use english
SLUG_HELP = _("Kurzfassung des Namens für die Adresszeile im Browser. Vorzugsweise englisch, keine Umlaute, nur Bindestrich als Sonderzeichen.")
slug_re = validators._lazy_re_compile(r'^[-a-z0-9]+\Z')
validate_downgraded_slug = validators.RegexValidator(
slug_re,
_("Enter a valid 'slug' consisting of lower-case letters, numbers or hyphens."),
'invalid'
)
class AutoSlugField(django_fields.SlugField):
"""
SlugField which optionally populates the value and/or makes sure that
the value is unique. By default as stricter slugify function is used.
populate_from: Field name
unique_slug: Boolean, automatically make the field value unique
"""
def __init__(self, *args, **kwargs):
kwargs.setdefault('max_length', 50)
kwargs.setdefault('help_text', SLUG_HELP)
if 'populate_from' in kwargs:
self.populate_from = kwargs.pop('populate_from')
self.unique_slug = kwargs.pop('unique_slug', False)
# FIXME Enforce unique=True
# if self.unique_slug:
# kwargs['unique'] = True
# TODO Refactor: We need a sibling FormField which also does our pre_save work and then validates correctly (called from Model.clean_fields)
super(AutoSlugField, self).__init__(*args, **kwargs)
def slugify(self, value):
return slugify(value)
def pre_save(self, model_instance, add):
value = getattr(model_instance, self.attname)
if not value:
if hasattr(self, 'populate_from'):
if callable(self.populate_from):
value = self.populate_from(model_instance, self)
else:
# Follow dotted path (e.g. "occupation.corporation.name")
value = reduce(lambda obj, attr: getattr(obj, attr),
self.populate_from.split("."), model_instance)
if callable(value):
value = value()
value = self.slugify(value)
if not value and not self.blank:
value = model_instance._meta.model_name
if self.unique_slug:
# TODO Move import to top of file once AutoSlugField is removed from shared.utils.fields and we no longer have a circular import
from ..fields import uniquify_field_value
return uniquify_field_value(
model_instance, self.name, value, max_length=self.max_length)
else:
return value
class DowngradingSlugField(AutoSlugField):
"""
SlugField which allows only lowercase ASCII characters and the dash,
automatically downgrading/replacing the entered content.
"""
default_validators = [validate_downgraded_slug]
def __init__(self, *args, **kwargs):
kwargs['allow_unicode'] = False
super(DowngradingSlugField, self).__init__(*args, **kwargs)
def slugify(self, value):
return downgrading_slugify(value)
def to_python(self, value):
# Downgrade immediately so that validators work
value = super().to_python(value)
return self.slugify(value)
def formfield(self, **kwargs):
# Remove the slug validator from the form field so that we can modify
# the field value in the model
field = super().formfield(**kwargs)
if field.default_validators:
try:
field.validators.remove(field.default_validators[0])
except ValueError:
pass
return field
class SlugTreeMixin(DirtyFieldsMixin, models.Model):
"""
Expects a `slug` and a `parent` field.
`has_url`: The node itself has an URL. It is possible that a node does
node have an URL, but its children have.
Maintains the `slug_path` field of the node and its children, watching if
either the `slug`, `parent` or `has_url` fields have changed.
"""
slug_path = models.CharField(_("URL path"),
unique=True, max_length=2000, editable=False)
# TODO Add validator to slug_path?
# validators=[
# RegexValidator(
# regex=r"^/(|.+/)$",
# message=_("Path must start and end with a slash (/)."),
# )
# ],
# TODO Make slug_path manually overridable (see feincms3 -> use_static_path)
has_url = models.BooleanField(_("has webaddress"), default=True)
FIELDS_TO_CHECK = ['slug', 'parent', 'has_url']
class Meta:
abstract = True
def _get_slug_path(self):
if self.pk:
ancestors = self.get_ancestors(include_self=False).filter(has_url=True).values_list('slug', flat=True)
parts = list(ancestors)
else:
parts = []
if self.slug:
parts += [self.slug]
return "/".join(parts)
def _rebuild_descendants_slug_path(self):
for p in self.get_descendants():
p.slug_path = p._get_slug_path()
p.save()
@receiver(pre_save)
def slug_tree_mixin_pre_save(sender, instance, **kwargs):
if isinstance(instance, SlugTreeMixin):
# FIXME: find a way to not always call this
instance.slug = instance._meta.get_field('slug').pre_save(instance, False)
instance.slug_path = instance._get_slug_path()
@receiver(post_save)
def slug_tree_mixin_post_save(sender, instance, **kwargs):
if isinstance(instance, SlugTreeMixin):
if kwargs.get('created'):
# Always get a new database instance before saving again
# or MPTTModel.save() will interpret the newly .save as
# a forbidden tree move action
# FIXME Not clear if this is a proper solution -> get rid of the slug_path stuff altogether
instance_copy = type(instance).objects.get(pk=instance.pk)
instance_copy.slug_path = instance_copy._get_slug_path()
if 'slug_path' in instance_copy.get_dirty_fields().keys():
instance_copy.save()
elif instance.get_dirty_fields().keys() & {'slug_path'}:
instance._rebuild_descendants_slug_path()