2022-08-14 19:24:27 +00:00
|
|
|
import datetime
|
2022-02-15 09:13:35 +00:00
|
|
|
from typing import Any, Dict, Optional
|
2022-07-02 14:49:21 +00:00
|
|
|
|
2022-08-14 19:34:32 +00:00
|
|
|
from django.forms import RadioSelect, widgets
|
2022-02-15 09:13:35 +00:00
|
|
|
|
2022-02-27 20:12:01 +00:00
|
|
|
from . import models
|
2022-02-27 19:00:12 +00:00
|
|
|
|
2022-03-02 21:38:25 +00:00
|
|
|
|
2022-08-14 19:34:32 +00:00
|
|
|
class TagsEditor(widgets.Widget):
|
2022-03-05 11:47:16 +00:00
|
|
|
"""
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
|
2022-02-22 18:40:27 +00:00
|
|
|
class Media:
|
|
|
|
js = ("babybuddy/js/tags_editor.js",)
|
|
|
|
|
2022-02-27 19:36:31 +00:00
|
|
|
input_type = "hidden"
|
|
|
|
template_name = "core/widget_tag_editor.html"
|
2022-02-15 09:13:35 +00:00
|
|
|
|
|
|
|
@staticmethod
|
2022-03-02 20:53:51 +00:00
|
|
|
def __unpack_tag(tag: models.Tag):
|
2022-03-05 11:47:16 +00:00
|
|
|
"""
|
|
|
|
Tiny utility function used to translate a tag to a serializable
|
|
|
|
dictionary of strings.
|
|
|
|
"""
|
2022-02-27 19:36:31 +00:00
|
|
|
return {"name": tag.name, "color": tag.color}
|
2022-02-15 09:13:35 +00:00
|
|
|
|
|
|
|
def format_value(self, value: Any) -> Optional[str]:
|
2022-03-05 11:47:16 +00:00
|
|
|
"""
|
|
|
|
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.
|
|
|
|
"""
|
2022-02-15 09:13:35 +00:00
|
|
|
if value is not None and not isinstance(value, str):
|
|
|
|
value = [self.__unpack_tag(tag) for tag in value]
|
|
|
|
return value
|
2022-02-27 19:36:31 +00:00
|
|
|
|
2022-02-22 18:40:27 +00:00
|
|
|
def build_attrs(self, base_attrs, extra_attrs=None):
|
2022-03-05 11:47:16 +00:00
|
|
|
"""
|
2022-03-05 12:09:08 +00:00
|
|
|
Bootstrap integration adds form-control to the classes of the widget.
|
2022-03-05 11:47:16 +00:00
|
|
|
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.
|
|
|
|
"""
|
2022-02-22 18:40:27 +00:00
|
|
|
attrs = super().build_attrs(base_attrs, extra_attrs)
|
2022-02-27 19:36:31 +00:00
|
|
|
class_string = attrs.get("class", "")
|
2022-02-26 14:50:38 +00:00
|
|
|
class_string = class_string.replace("form-control", "")
|
2022-02-27 19:36:31 +00:00
|
|
|
attrs["class"] = class_string + " babybuddy-tags-editor"
|
2022-02-22 18:40:27 +00:00
|
|
|
return attrs
|
2022-02-27 19:36:31 +00:00
|
|
|
|
2022-02-15 09:13:35 +00:00
|
|
|
def get_context(self, name: str, value: Any, attrs) -> Dict[str, Any]:
|
2022-03-05 11:47:16 +00:00
|
|
|
"""
|
|
|
|
Adds extra information to the payload provided to the widget's template.
|
2022-03-05 12:09:08 +00:00
|
|
|
|
2022-03-05 11:47:16 +00:00
|
|
|
Specifically:
|
2022-03-05 12:09:08 +00:00
|
|
|
- Query a list if "recently used" tags (max 256 to not cause
|
2022-03-05 11:47:16 +00:00
|
|
|
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").
|
|
|
|
"""
|
2022-03-02 20:53:51 +00:00
|
|
|
most_tags = models.Tag.objects.order_by("-last_used").all()[:256]
|
2022-02-15 09:13:35 +00:00
|
|
|
|
|
|
|
result = super().get_context(name, value, attrs)
|
|
|
|
|
2022-02-27 19:36:31 +00:00
|
|
|
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]
|
2022-02-15 09:13:35 +00:00
|
|
|
|
2022-02-27 19:36:31 +00:00
|
|
|
result["widget"]["tag_suggestions"] = {
|
|
|
|
"quick": [
|
|
|
|
self.__unpack_tag(t)
|
|
|
|
for t in quick_suggestion_tags
|
2022-02-15 09:13:35 +00:00
|
|
|
if t.name not in tag_names
|
|
|
|
],
|
2022-02-27 19:36:31 +00:00
|
|
|
"most": [self.__unpack_tag(t) for t in most_tags],
|
2022-02-15 09:13:35 +00:00
|
|
|
}
|
2022-02-22 18:40:27 +00:00
|
|
|
return result
|
2022-07-02 14:49:21 +00:00
|
|
|
|
|
|
|
|
|
|
|
class ChildRadioSelect(RadioSelect):
|
|
|
|
input_type = "radio"
|
2022-07-05 19:23:49 +00:00
|
|
|
template_name = "core/child_radio.html"
|
2022-07-02 14:49:21 +00:00
|
|
|
option_template_name = "core/child_radio_option.html"
|
2022-07-31 03:33:42 +00:00
|
|
|
attrs = {"class": "btn-check"}
|
|
|
|
|
|
|
|
def build_attrs(self, base_attrs, extra_attrs=None):
|
|
|
|
attrs = super().build_attrs(base_attrs, extra_attrs)
|
|
|
|
attrs["class"] += " btn-check"
|
|
|
|
return attrs
|
2022-07-02 14:49:21 +00:00
|
|
|
|
2022-07-07 11:41:35 +00:00
|
|
|
def create_option(
|
|
|
|
self, name, value, label, selected, index, subindex=None, attrs=None
|
|
|
|
):
|
|
|
|
option = super().create_option(
|
|
|
|
name, value, label, selected, index, subindex, attrs
|
|
|
|
)
|
|
|
|
if value != "":
|
|
|
|
option["picture"] = value.instance.picture
|
2022-07-02 14:49:21 +00:00
|
|
|
return option
|
2022-08-14 17:37:02 +00:00
|
|
|
|
|
|
|
|
2022-08-14 19:34:32 +00:00
|
|
|
class DateTimeBaseInput(widgets.DateTimeBaseInput):
|
2022-08-14 17:37:02 +00:00
|
|
|
def format_value(self, value):
|
2022-08-14 19:24:27 +00:00
|
|
|
if isinstance(value, datetime.datetime):
|
|
|
|
value = value.isoformat()
|
|
|
|
return value
|
2022-08-14 19:34:32 +00:00
|
|
|
|
|
|
|
|
|
|
|
class DateTimeInput(DateTimeBaseInput):
|
|
|
|
input_type = "datetime-local"
|
|
|
|
|
2023-04-15 19:58:41 +00:00
|
|
|
def build_attrs(self, base_attrs, extra_attrs=None):
|
|
|
|
attrs = super().build_attrs(base_attrs, extra_attrs)
|
|
|
|
# Default to seconds granularity. Required for client validation in Safari.
|
|
|
|
if "step" not in attrs:
|
|
|
|
attrs["step"] = 1
|
|
|
|
return attrs
|
|
|
|
|
2022-08-14 19:34:32 +00:00
|
|
|
|
|
|
|
class DateInput(DateTimeBaseInput):
|
|
|
|
input_type = "date"
|
2022-08-14 23:38:59 +00:00
|
|
|
|
|
|
|
|
|
|
|
class TimeInput(DateTimeBaseInput):
|
|
|
|
input_type = "time"
|