Source code for abilian.web.tags.extension

""""""
from typing import Any

from flask import Flask

from abilian.core.entities import Entity
from abilian.core.models.tag import TAGS_ATTR, Tag, supports_tagging
from abilian.i18n import _l
from abilian.web import url_for
from abilian.web.forms import Form
from abilian.web.views.object import EDIT_BUTTON

from .forms import TagsField
from .views import bp as tags_bp
from .views import entity_bp

ENTITY_DEFAULT_NS_ATTR = "__tags_default_ns__"


def ns(ns):
    """Class decorator that sets default tags namespace to use with its
    instances."""

    def setup_ns(cls):
        setattr(cls, ENTITY_DEFAULT_NS_ATTR, ns)
        return cls

    return setup_ns


class _TagsForm(Form):
    """Form to workaround a wtforms limitation: fields cannot start with an
    underscore, so '__tags__' is not accepted.

    This form help process tags (and only tags).

    .. seealso:: :py:meth:`~TagsExtension.entity_tags_form`
    """

    def process(self, formdata=None, obj=None, data=None, **kwargs):
        tags = None
        if obj is not None:
            tags = getattr(obj, TAGS_ATTR, set())

        super().process(formdata=formdata, obj=None, data=data, tags=tags, **kwargs)


[docs]class TagsExtension: """API for tags, installed as an application extension. It is also available in templates as `tags`. """ def __init__(self, app: Flask) -> None: app.extensions["tags"] = self app.add_template_global(self, "tags") app.register_blueprint(tags_bp) app.register_blueprint(entity_bp)
[docs] def supports_tagging(self, entity): return supports_tagging(entity)
[docs] def entity_tags(self, entity): return getattr(entity, TAGS_ATTR)
[docs] def tags_from_hit(self, tag_ids): """ :param tag_ids: indexed ids of tags in hit result. Do not pass hit instances. :returns: an iterable of :class:`Tag` instances. """ ids = [] for t in tag_ids.split(): t = t.strip() try: t = int(t) except ValueError: pass else: ids.append(t) if not ids: return [] return Tag.query.filter(Tag.id.in_(ids)).all()
[docs] def entity_default_ns(self, entity): return getattr(entity, ENTITY_DEFAULT_NS_ATTR, "default")
[docs] def entity_tags_form(self, entity, ns=None): """Construct a form class with a field for tags in namespace `ns`.""" if ns is None: ns = self.entity_default_ns(entity) field = TagsField(label=_l("Tags"), ns=ns) cls = type("EntityNSTagsForm", (_TagsForm,), {"tags": field}) return cls
[docs] def get(self, ns, label=None): """Return :class:`tags instances<~Tag>` for the namespace `ns`, ordered by label. If `label` is not None the only one instance may be returned, or `None` if no tags exists for this label. """ query = Tag.query.filter(Tag.ns == ns) if label is not None: return query.filter(Tag.label == label).first() return query.all()
# pyre-fixme[9]: tag has type `Tag`; used as `None`.
[docs] def add( self, entity: Entity, tag: Tag = None, ns: Any = None, label: Any = None ) -> Tag: if tag is None: assert None not in (ns, label) tag = self.get(ns, label) if tag is None: tag = Tag(ns=ns, label=label) tags = self.entity_tags(entity) tags.add(tag) return tag
[docs] def remove(self, entity, tag=None, ns=None, label=None): if tag is None: assert None not in (ns, label) tag = self.get(ns, label) tags = self.entity_tags(entity) try: tags.remove(tag) except KeyError: pass
[docs] def get_form_context(self, obj, ns=None): """Return a dict: form instance, action button, submit url... Used by macro m_tags_form(entity) """ return { "url": url_for("entity_tags.edit", object_id=obj.id), "form": self.entity_tags_form(obj)(obj=obj, ns=ns), "buttons": [EDIT_BUTTON], }