"""
Implementation of :class:`Field` and subclasses. They are used in the :attr:`templates.template.fields` attribute.
Each one is responsible handling a part of data-procesing in the card-generation process. This allows easy
customization of :class:`templates.Template`.
"""
from typing import Callable
import attr
import requests
from bidict import bidict
from kivy.clock import mainthread
from kivy.lang import Builder
from pony.orm import db_session
from .design_patterns.factory import CookBook
from .utils import compress_img_bytes
field_cookbook = CookBook()
[docs]@attr.s
class TranslationMixin:
"""Mixin-Class that adds missing translations to the ``target_field`` of the ``template.data`` dict."""
[docs] template = attr.ib(type=object)
"""Instance of :class:`templates.template`."""
[docs] trans_callback = attr.ib(type=Callable)
"""Function that is applied to the data in source field to obtain a translation."""
[docs] src_field = attr.ib(default="")
"""Name of the source_field of :attr:`template`.data."""
[docs] target_field = attr.ib(default="")
"""Name of the target_field of :attr:`template`.data."""
_kv_bidict = attr.ib()
"""Mapping between fields and kv_attributes of widgets."""
@_kv_bidict.default
def _get_kv_dict_default(self):
return bidict({self.src_field: "text_orig", self.target_field: "text_trans"})
@trans_callback.default
def _default_trans_callback(self):
return self.template.translate
[docs] def pre_process(self):
"""
Add translations, where they are missing.
Handles the cases that source and target are strings or list of strings.
"""
if hasattr(self, "pre_process"):
super().pre_process()
if self.template.data and self.src_field in self.template.data:
src_data = self.template.data[self.src_field]
if isinstance(src_data, str):
self.template.data[self.target_field] = self.template.data.get(
self.target_field, None
) or self.trans_callback(src_data)
if isinstance(src_data, list):
target_data = self.template.data.get(self.target_field, None) or [
None for _ in src_data
]
if len(target_data) < len(src_data):
target_data += [None] * (len(target_data) - len(src_data))
self.template.data[self.target_field] = [
trans or self.trans_callback(source)
for source, trans in zip(src_data, target_data)
]
[docs]@attr.s
class Field:
"""
Base-class for fields.
Accesses the data in :attr:`template`.data, can perform actions on the obtained data and/or display it in a
widget to allow user input to change it.
"""
"""Reference to :class:`templates.Template`."""
[docs] field_name = attr.ib(default="default_field")
"""Key of :attr:`template`.data which this field is handling."""
[docs] heading = attr.ib(default=None)
"""Heading to be shown over widget."""
"""Gets constructed by :meth:`~kivy.lang.Builder.load_string` from :attr:`widget_kv` if set."""
"""If set, widget gets constructed."""
_kv_bidict = attr.ib()
"""Mapping between attributes of kivy-widget and field_name."""
@property
[docs] def kv_bidict(self):
"""Getter function for :attr:`_audio_url`."""
return self._kv_bidict
@kv_bidict.setter
def kv_bidict(self, value):
"""Set :attr:`_audio_url` and download file."""
if not isinstance(value, bidict):
value = bidict(value)
self._kv_bidict = value
@_kv_bidict.default
def _default_kv_dict(self):
return bidict({self.field_name: "text"})
def __attrs_post_init__(self):
self.pre_process()
if self.widget_kv:
self.construct_widget()
"""Placeholder-function."""
[docs] def post_process(self, content): # pylint: disable=no-self-use
"""Placeholder-function."""
return content
[docs] def get_data(self):
"""Get dictionary to construct :attr:`widget`."""
return {
value: self.template.data[key]
if self.template.data and key in self.template.data
else ""
for key, value in self.kv_bidict.items()
}
[docs] def update(self):
"""Apply :meth:`pre_process` and :meth:`update_widget_data`."""
self.pre_process()
self.update_widget_data()
@mainthread
[docs] def get_content(self):
"""If :attr:`widget` is set, use :attr:`kv_bidict` to extract.
Calls :meth:`post_process` on the data before returning it.
"""
if self.widget:
content = {
key: getattr(self.widget, value)
for key, value in self.kv_bidict.items()
}
else:
content = {self.field_name: self.template.data[self.field_name]}
return self.post_process(content)
[docs]@attr.s(auto_attribs=True)
class DisplayTextField(Field):
"""Only displays text."""
widget_kv: str = """
MDLabel:
size_hint:1, None
size: self.texture_size
"""
[docs]@attr.s
class TextInputField(Field):
"""
Displays text and lets user edit it.
If callback is set, it will be called ``on_text_validate``, i.e. when the user presses enter while field is in
focus.
Useful to bind to :meth:`templates.Template.search` or `templates.Template.update_from_single_parser`.
"""
[docs] callback = attr.ib(default=None, type=Callable)
"""Gets called ``on_text_validate`` of the kivy-widget."""
[docs] widget_kv = attr.ib(default="MDTextField")
"""kv-string describing the widget."""
[docs] def construct_widget(self):
"""Furthermore add :attr:`field_name` as hint-text and bind ``on_text_validate``."""
super().construct_widget()
self.widget.hint_text = self.field_name
self.widget.bind(on_text_validate=self.on_text_validate)
[docs] def on_text_validate(self, widget):
"""Wrapper-function for the possible call of :attr:`callback`."""
text = widget.text
if self.callback:
self.callback(text)
[docs]@attr.s
class OptionsField(Field):
"""Base-class for a field with multiple options to choose from."""
get_selection = attr.ib(default=None, type=Callable)
display_limit = attr.ib(default=None)
[docs] def get_data(self):
"""Get dictionaries to construct children of :attr:`widget`."""
if self.template.data:
min_len = min(
len(self.template.data[key]) if key in self.template.data else 0
for key in self.kv_bidict.keys()
)
else:
min_len = 0
if self.display_limit:
min_len = min(self.display_limit, min_len)
return [
{value: self.template.data[key][i] for key, value in self.kv_bidict.items()}
for i in range(min_len)
]
@mainthread
[docs] def get_content(self):
"""
If :attr:`widget` is set, gets content from widget using :attr:`kv_bidict`.
If :attr:`selection_callback` is set, get content from call.
Else simply get the first option as default.
"""
if self.widget_kv and hasattr(self.widget, "get_checked"):
content = {
field: ", ".join(
[
getattr(widget, kv_attr)
for widget in self.widget.get_checked()
if widget
]
)
for field, kv_attr in self.kv_bidict.items()
}
elif self.get_selection:
content = self.get_selection()
else:
options = self.template.data[self.field_name]
content = {
self.field_name: (
options[0]
if isinstance(options, list) and len(options) >= 1
else options
)
}
return self.post_process(content)
[docs]@attr.s(auto_attribs=True)
class CheckChipOptionsField(OptionsField):
r"""Pick multiple options using :class:`custom_widgets.selection_widgets.CheckChip`\ s."""
widget_kv: str = "MyCheckChipContainer"
[docs]@attr.s(auto_attribs=True)
class TransChipOptionsField(TranslationMixin, OptionsField):
r"""
Pick a single options using :class:`custom_widgets.selection_widgets.MyTransChip`\ s.
Inheritance from :class:`TranslationMixin` guarantees that translations are available.
"""
widget_kv: str = """
MyCheckChipContainer
child_class_name: "MyTransChip"
check_one: True"""
[docs]@attr.s(auto_attribs=True)
class DualLongTextField(TranslationMixin, OptionsField):
"""
Pick a single options using :class:`custom_widgets.selection_widgets.CardCarousel`.
Useful for longer text, i.e. examples or explanations.
Inheritance from :class:`TranslationMixin` guarantees that translations are available.
"""
widget_kv: str = "CardCarousel"
[docs]@attr.s
class ImgField(OptionsField, MediaField):
"""Let user choose between multiple images."""
file_type = attr.ib(default="jpg")
widget_kv = attr.ib(default="ImageCarousel:\n\theight:dp(250)")
display_limit = attr.ib(default=10)
_kv_bidict = attr.ib()
@_kv_bidict.default
def _get_kv_dict_default(self):
return bidict({self.field_name: "source"})
[docs] def post_process(self, content):
"""Download user choice and save to data-base."""
url = content[self.field_name]
if url:
img_file = self.get_media_file(url)
if img_file:
img_file = compress_img_bytes(img_file)
self.save_media_file(media_file=img_file)
return super().post_process(content)
[docs] def on_error(self, _widget, child, *_):
"""Remove urls that could not be loaded from :attr:`template`.data."""
self.template.data[self.field_name].remove(child.source)
self.update()
self.template.save_base_data_to_db()