# This file is a part of the FeretUI project
#
# Copyright (C) 2023-2024 Jean-Sebastien SUZANNE <js.suzanne@gmail.com>
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file,You can
# obtain one at http://mozilla.org/MPL/2.0/.
"""feretui.resources.common's module.
* :class:`.ActionsMixinForView`
* :class:`.MultiView`
* :class:`.TemplateMixinForView`
"""
from typing import TYPE_CHECKING
from lxml.etree import Element, SubElement, tostring
from lxml.html import fromstring
from markupsafe import Markup
from polib import POFile
from feretui.exceptions import ViewActionError, ViewError
from feretui.form import FeretUIForm
from feretui.request import Request
from feretui.resources.actions import Actionset
from feretui.resources.view import view_action_validator
from feretui.response import Response
from feretui.session import Session
from feretui.template import Template, decode_html
from feretui.translation import Translation
if TYPE_CHECKING:
from feretui.feretui import FeretUI
[docs]
class LabelMixinForView:
"""LabelMixinForView class."""
label: str = None
[docs]
def export_catalog(
self: "LabelMixinForView",
translation: Translation,
po: POFile,
) -> None:
"""Export the translations in the catalog.
:param translation: The translation instance to add also inside it.
:type translation: :class:`.Translation`
:param po: The catalog instance
:type po: PoFile_
"""
super().export_catalog(translation, po)
if self.label:
po.append(translation.define(f"{self.context}:label", self.label))
[docs]
def get_label(
self: "LabelMixinForView",
feretui: "FeretUI",
session: Session,
) -> str:
"""Return the translated label.
:param feretui: The feretui client
:type feretui: :class:`feretui.feretui.FeretUI`
:param session: The Session
:type session: :class:`feretui.session.Session`
:rtype: str.
"""
if not self.label:
return super().get_label(feretui, session)
return feretui.translation.get(
session.lang,
f"{self.context}:label",
self.label,
)
[docs]
class ActionsMixinForView:
"""ActionsMixinForView class.
Render the actions buttons aside
"""
actions: list[Actionset] = []
def __init__(
self: "ActionsMixinForView",
*args: tuple,
**kwargs: dict,
) -> None:
"""ActionsMixinForView constructor."""
super().__init__(*args, **kwargs)
for actionset in self.actions:
actionset.context = f"{self.context}:actionset"
for action in actionset.actions:
action.context = f"{self.context}:action:{action.method}"
[docs]
def export_catalog(
self: "ActionsMixinForView",
translation: "Translation",
po: POFile,
) -> None:
"""Export the translations in the catalog.
:param translation: The translation instance to add also inside it.
:type translation: :class:`.Translation`
:param po: The catalog instance
:type po: PoFile_
"""
super().export_catalog(translation, po)
for actionset in self.actions:
actionset.export_catalog(translation, po)
[docs]
def get_actions(
self: "ActionsMixinForView",
feretui: "FeretUI",
session: Session,
options: dict,
) -> list[Markup]:
"""Return the actionset list renderer.
:param feretui: The feretui client
:type feretui: :class:`feretui.feretui.FeretUI`
:param session: The Session
:type session: :class:`feretui.session.Session`
:param options: The querystring
:type options: dict
:return: The html pages
:rtype: list[str]
"""
return [
actionset.render(
feretui,
session,
options,
self.resource.code,
self.code,
)
for actionset in self.actions
if actionset.is_visible(session)
]
[docs]
def get_call_kwargs(
self: "ActionsMixinForView",
request: Request, # noqa: ARG002
) -> dict:
"""Return the kwargs for the call with this view.
:param request: request params
:type request: :class:`feretui.request.Request`
:return: the kwargs from params
:rtype: dict
"""
return {}
[docs]
@view_action_validator(methods=Request.POST)
def call(
self: "ActionsMixinForView",
feretui: "FeretUI",
request: Request,
) -> Response:
"""Call a method on the resource.
.. note::
If the method called return None then the
view is rerendering
:param feretui: The feretui client
:type feretui: :class:`feretui.feretui.FeretUI`
:param request: The request
:type request: :class:`feretui.request.Request`
:return: The page to display
:rtype: :class:`feretui.response.Response`
:exception: ViewActionError
"""
method = request.params.get("method")
if isinstance(method, list):
method = method[0]
is_declared = False
view_kwargs = {}
for actionset in self.actions:
for action in actionset.actions:
if (
action.is_visible(request.session)
and action.method == method
):
view_kwargs.update(self.get_call_kwargs(request))
is_declared = True
break
if not is_declared:
raise ViewActionError(
f"The method {method} is not declared in the {self}",
)
func = getattr(self.resource, method)
res = func(feretui, request, **view_kwargs)
if not res:
qs = request.get_query_string_from_current_url()
res = Response(
Markup.unescape(self.render(feretui, request.session, qs)),
)
if not isinstance(res, Response):
res = Response(res)
return res
[docs]
class MultiView(ActionsMixinForView):
"""MultiView class.
Render the buttons for view multi.
"""
[docs]
class Filter:
"""Filter's Form."""
def __init__(self: "MultiView", *args: tuple, **kwargs: dict) -> None:
"""MultiView constructor."""
super().__init__(*args, **kwargs)
if not hasattr(self, "limit"):
raise ViewError("The view has not a 'limit : int' class attribute")
if not hasattr(self, "do_click_on_entry_redirect_to"):
raise ViewError(
"The view has not a 'do_click_on_entry_redirect_to : str' "
"class attribute",
)
if not hasattr(self, "create_button_redirect_to"):
raise ViewError(
"The view has not a 'create_button_redirect_to : str' "
"class attribute",
)
if not hasattr(self, "delete_button_redirect_to"):
raise ViewError(
"The view has not a 'delete_button_redirect_to : str' "
"class attribute",
)
if not hasattr(self.resource, "filtered_reads"):
raise ViewError(
"The resource has not a 'filtered_reads' method\n"
"def filtered_reads(self, form_cls, filters, offset, limit):\n"
" return {'total': 0, 'forms': []}",
)
self.filter_cls = self.get_filter_cls()
[docs]
def export_catalog(
self: "MultiView",
translation: "Translation",
po: POFile,
) -> None:
"""Export the translations in the catalog.
:param translation: The translation instance to add also inside it.
:type translation: :class:`.Translation`
:param po: The catalog instance
:type po: PoFile_
"""
super().export_catalog(translation, po)
self.filter_cls.export_catalog(translation, po)
[docs]
def get_filter_cls(self: "MultiView") -> FeretUIForm:
"""Return the Filter Form for the view."""
return type(
f"Filter_{self.resource.code}_{self.code}",
(self.Filter, self.form_cls),
{"view": self},
)
# ---------- Render -------------------
[docs]
def get_actions(
self: "MultiView",
feretui: "FeretUI",
session: Session,
options: dict,
) -> list[Markup]:
"""Return the actionset list renderer.
:param feretui: The feretui client
:type feretui: :class:`feretui.feretui.FeretUI`
:param session: The Session
:type session: :class:`feretui.session.Session`
:param options: The querystring
:type options: dict
:return: The html pages
:rtype: list[str]
"""
res = [
Markup.unescape(
feretui.render_template(
session,
"view-filter",
form=self.filter_cls(),
hx_post=f"{feretui.base_url}/action/resource?action=filters",
),
),
]
res.extend(super().get_actions(feretui, session, options))
return res
[docs]
def render_kwargs(
self: "MultiView",
feretui: "FeretUI",
session: Session,
options: dict,
) -> dict:
"""Return the dict of parameter need for the muti view.
:param feretui: The feretui client
:type feretui: :class:`feretui.feretui.FeretUI`
:param session: The Session
:type session: :class:`feretui.session.Session`
:param options: The options come from the body or the query string
:type options: dict
:return: the named attributes
:rtype: dict
"""
filters = [
(key[7:-1], values)
for key, values in options.items()
if (key.startswith("filter[") and key[-1] == "]")
]
offset = options.get("offset", 0)
if isinstance(offset, list):
offset = offset[0]
offset = int(offset)
dataset = self.resource.filtered_reads(
self.form_cls,
filters,
offset,
self.limit,
)
paginations = range(0, dataset["total"], self.limit)
open_view_url = (
self.get_transition_url(
feretui,
options,
pk=None,
view=self.do_click_on_entry_redirect_to,
)
if self.do_click_on_entry_redirect_to
else None
)
return {
"rcode": self.resource.code,
"vcode": self.code,
"label": self.get_label(feretui, session),
"form": self.form_cls(),
"offset": offset,
"limit": self.limit,
"paginations": paginations,
"dataset": dataset,
"filters": filters,
"open_view_url": open_view_url,
"header_buttons": self.get_header_buttons(
feretui,
session,
options,
),
"actions": self.get_actions(feretui, session, options),
}
# ---------------- View actions -------------------------
[docs]
@view_action_validator(methods=[Request.POST, Request.DELETE])
def filters(
self: "MultiView",
feretui: "FeretUI",
request: Request,
) -> Response:
"""Change the filters and rerender.
The type of modification of the filter depend of the request method:
* POST : add a filter
* DELETE : remove a filter
:param feretui: The feretui client
:type feretui: :class:`feretui.feretui.FeretUI`
:param request: The request
:type request: :class:`feretui.request.Request`
:return: The page to display
:rtype: :class:`feretui.response.Response`
"""
qs = request.get_query_string_from_current_url()
qs["offset"] = 0
for param, values in request.params.items():
if param == "action":
continue
existing = qs.setdefault(f"filter[{param}]", [])
if request.method == Request.POST:
for value in values:
if value not in existing:
existing.append(value)
elif request.method == Request.DELETE:
for value in values:
if value in existing:
existing.remove(value)
if not existing:
del qs[f"filter[{param}]"]
base_url = request.get_base_url_from_current_url()
url = request.get_url_from_dict(base_url=base_url, querystring=qs)
return Response(
self.render(feretui, request.session, qs),
headers={
"HX-Push-Url": url,
},
)
[docs]
@view_action_validator(methods=[Request.POST]) # USE POST cause of params
def goto_delete(
self: "MultiView",
feretui: "FeretUI",
request: Request,
) -> Response:
"""Goto the delete page.
:param feretui: The feretui client
:type feretui: :class:`feretui.feretui.FeretUI`
:param request: The request
:type request: :class:`feretui.request.Request`
:return: The page to display
:rtype: :class:`feretui.response.Response`
"""
view_kwargs = self.get_call_kwargs(request)
newqs = request.get_query_string_from_current_url().copy()
base_url = request.get_base_url_from_current_url()
newqs.update(
{
"view": self.delete_button_redirect_to,
"pk": view_kwargs["pks"],
},
)
return Response(
self.resource.views[self.delete_button_redirect_to].render(
feretui,
request.session,
newqs,
),
headers={
"HX-Push-Url": request.get_url_from_dict(base_url, newqs),
},
)
[docs]
class TemplateMixinForView:
"""TemplateMixinForView class."""
header_template_id: str = "view-buttons-header"
header_template: str = None
body_template_id: str = "view-readwrite-form"
body_template: str = None
footer_template_id: str = "view-buttons-header"
footer_template: str = None
# ----- translation ------
[docs]
def export_catalog(
self: "ActionsMixinForView",
translation: "Translation",
po: POFile,
) -> None:
"""Export the translations in the catalog.
:param translation: The translation instance to add also inside it.
:type translation: :class:`.Translation`
:param po: The catalog instance
:type po: PoFile_
"""
super().export_catalog(translation, po)
template_id = f"resource-{self.resource.code}-view-{self.code}"
tmpls = Template(Translation(translation.feretui))
tmpls.load_template_from_str(
self.get_compiled_template(
translation.feretui,
Session(),
template_id,
),
)
tmpls.export_catalog(po)
# ----- templating ------
[docs]
def compile(self: "TemplateMixinForView", node: Element) -> Element:
"""Transform the template to add some javascript behaviours.
:param node: The main node to transform
:type node: HtmlElement_
:return: new node
:rtype: HtmlElement_
"""
return node
[docs]
def set_body_template(
self: "TemplateMixinForView",
feretui: "FeretUI", # noqa: ARG002
session: Session,
parent: Element,
) -> Element:
"""Add the body template.
:param feretui: The feretui client
:type feretui: :class:`feretui.feretui.FeretUI`
:param session: The feretui session
:type session: :class:`feretui.session.Session`
:param parent: The parent node
:type parent: HtmlElement_
"""
body = None
if self.body_template:
body = fromstring(self.body_template)
elif self.body_template_id:
body = feretui.template.get_template(
self.body_template_id,
session.lang,
tostring=False,
)
if body is not None:
body = self.compile(body)
parent.append(body)
[docs]
def get_compiled_template(
self: "TemplateMixinForView",
feretui: "FeretUI",
session: Session,
template_id: str,
) -> str:
"""Get the template for this view.
:param feretui: The feretui client
:type feretui: :class:`feretui.feretui.FeretUI`
:param session: The feretui session
:type session: :class:`feretui.session.Session`
:param template_id: The id of the template
:type template_id: str
"""
template = Element("template")
template.set("id", template_id)
div = SubElement(template, "div")
div.set("id", template_id)
div.set("class", "container is-fluid content")
form = self.set_form_template(feretui, session, div)
self.set_header_template(feretui, session, form)
self.set_body_template(feretui, session, form)
self.set_footer_template(feretui, session, form)
return tostring(template)
# ----- render -----
[docs]
def render_kwargs(
self: "TemplateMixinForView",
feretui: "FeretUI",
session: Session,
options: dict,
) -> dict:
"""Get kwarg of the view for render.
:param feretui: The feretui client
:type feretui: :class:`feretui.feretui.FeretUI`
:param session: The Session
:type session: :class:`feretui.session.Session`
:param options: The options come from the body or the query string
:type options: dict
:return: The kwargs
:rtype: dict.
"""
return {
"resource": self.resource,
"header_buttons": self.get_header_buttons(
feretui,
session,
options,
),
"rcode": self.resource.code,
"vcode": self.code,
}
[docs]
def render(
self: "TemplateMixinForView",
feretui: "FeretUI",
session: Session,
options: dict,
) -> str:
"""Render the view.
:param feretui: The feretui client
:type feretui: :class:`feretui.feretui.FeretUI`
:param session: The Session
:type session: :class:`feretui.session.Session`
:param options: The options come from the body or the query string
:type options: dict
:return: The html page in
:rtype: str.
"""
template_id = f"resource-{self.resource.code}-view-{self.code}"
if not feretui.template.has_template(template_id):
feretui.template.load_template_from_str(
self.get_compiled_template(feretui, session, template_id),
)
return Markup.unescape(
feretui.render_template(
session,
template_id,
**self.render_kwargs(feretui, session, options),
),
)