from django.forms import Media from typing import Any, Dict, Optional from django.forms import Widget from . import models class TagsEditor(Widget): """ Custom widget that provides an alternative editor for tags provided by the taggit library. The widget makes use of bootstrap v4 and its badge/pill feature and renders a list of tags as badges that can be clicked to remove or add a tag to the list of set tags. In addition, a user can dynamically add new, custom tags, using a text editor. """ class Media: js = ("babybuddy/js/tags_editor.js",) input_type = "hidden" template_name = "core/widget_tag_editor.html" @staticmethod def __unpack_tag(tag: models.Tag): """ Tiny utility function used to translate a tag to a serializable dictionary of strings. """ return {"name": tag.name, "color": tag.color} def format_value(self, value: Any) -> Optional[str]: """ Override format_value to provide a list of dictionaries rather than a flat, comma-separated list of tags. This allows for the more complex rendering of tags provided by this plugin. """ if value is not None and not isinstance(value, str): value = [self.__unpack_tag(tag) for tag in value] return value def build_attrs(self, base_attrs, extra_attrs=None): """ Bootstrap integration adds form-control to the classes of the widget. This works only for "plain" input-based widgets however. In addition, we need to add a custom class "babybuddy-tags-editor" for the javascript file to detect the widget and take control of its contents. """ attrs = super().build_attrs(base_attrs, extra_attrs) class_string = attrs.get("class", "") class_string = class_string.replace("form-control", "") attrs["class"] = class_string + " babybuddy-tags-editor" return attrs def get_context(self, name: str, value: Any, attrs) -> Dict[str, Any]: """ Adds extra information to the payload provided to the widget's template. Specifically: - Query a list if "recently used" tags (max 256 to not cause DoS issues) from the database to be used for auto-completion. ("most") - Query a smaller list of 5 tags to be made available from a quick selection widget ("quick"). """ most_tags = models.Tag.objects.order_by("-last_used").all()[:256] result = super().get_context(name, value, attrs) tag_names = set( x["name"] for x in (result.get("widget", {}).get("value", None) or []) ) quick_suggestion_tags = [t for t in most_tags if t.name not in tag_names][:5] result["widget"]["tag_suggestions"] = { "quick": [ self.__unpack_tag(t) for t in quick_suggestion_tags if t.name not in tag_names ], "most": [self.__unpack_tag(t) for t in most_tags], } return result