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.
177 lines
6.4 KiB
177 lines
6.4 KiB
# -*- coding: utf-8 -*- |
|
from __future__ import unicode_literals |
|
import six |
|
|
|
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 ugettext_lazy as _ |
|
|
|
from dirtyfields import DirtyFieldsMixin |
|
|
|
from ..text import slugify, downgrading_slugify, django_slugify |
|
|
|
if six.PY3: |
|
from functools import reduce |
|
|
|
|
|
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 |
|
# not allowed 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()
|
|
|