""" Contains small helper classes used as abstraction for various templating
macros.
"""
from __future__ import annotations
from random import choice
from lxml.html import builder, tostring
from markupsafe import Markup
from onegov.core.elements import AccessMixin, LinkGroup
from onegov.core.elements import Link as BaseLink
from onegov.org import _
from purl import URL
from typing import Any, TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Collection, Iterable
from onegov.core.elements import Trait
from onegov.core.elements import ChameleonLayout
from onegov.core.request import CoreRequest
# NOTE: We pretend to inherit from BaseLink at type checking time
# so we're not stuck in dependency hell everywhere else
# In reality we probably should actually inherit from this
# class and clean up redundancies...
_Base = BaseLink
else:
_Base = AccessMixin
[docs]
class Link(_Base):
""" Represents a link rendered in a template. """
[docs]
__slots__ = (
'active',
'attributes',
'classes',
'model',
'request_method',
'subtitle',
'text',
'url',
)
def __init__(
self,
text: str,
url: str,
classes: Collection[str] | None = None,
request_method: str = 'GET',
attributes: dict[str, Any] | None = None,
active: bool = False,
model: Any | None = None,
subtitle: str | None = None
) -> None:
#: The text of the link
[docs]
self.text: str = text
#: The fully qualified url of the link
#: Classes included in the link
#: The link method, defaults to 'GET'. Also supported is 'DELETE',
#: which will lead to the use of XHR
[docs]
self.request_method = request_method
#: HTML attributes (may override other attributes set by this class).
#: Attributes which are translatable, are transalted before rendering.
[docs]
self.attributes = attributes or {}
#: Indicate if this link is active or not (not used for rendering)
#: The model that underlies this link (to check if the link is visible)
#: Shown as a subtitle below certain links (not automatically rendered)
[docs]
self.subtitle = subtitle
[docs]
def __eq__(self, other: object) -> bool:
for attr in self.__slots__:
if getattr(self, attr) != getattr(other, attr, None):
return False
return True
[docs]
def __call__(
self,
request: ChameleonLayout | CoreRequest,
extra_classes: Iterable[str] | None = None
) -> Markup:
""" Renders the element. """
# compatibility shim for new elements
if hasattr(request, 'request'):
request = request.request
a = builder.A(request.translate(self.text))
if self.request_method == 'GET':
a.attrib['href'] = self.url
if self.request_method == 'POST':
a.attrib['ic-post-to'] = self.url
if self.request_method == 'DELETE':
url = URL(self.url).query_param(
'csrf-token', request.new_csrf_token())
a.attrib['ic-delete-from'] = url.as_string()
classes: list[str] = []
if self.classes:
classes.extend(self.classes)
if extra_classes:
classes.extend(extra_classes)
a.attrib['class'] = ' '.join(classes)
# add the access hint if needed
if self.access == 'private':
# This snippet is duplicated in the access-hint macro!
hint = builder.I()
hint.attrib['class'] = 'private-hint'
hint.attrib['title'] = request.translate(_('This site is private'))
a.append(builder.I(' '))
a.append(hint)
elif self.access == 'secret':
# This snippet is duplicated in the access-hint macro!
hint = builder.I()
hint.attrib['class'] = 'secret-hint'
hint.attrib['title'] = request.translate(_('This site is secret'))
a.append(builder.I(' '))
a.append(hint)
for key, value in self.attributes.items():
a.attrib[key] = request.translate(value)
return Markup(tostring(a, encoding=str)) # noqa: RUF035
[docs]
class QrCodeLink(BaseLink):
""" Implements a qr code link that shows a modal with the QrCode.
Thu url is sent to the qr endpoint url which generates the image
and sends it back.
"""
[docs]
__slots__ = (
'active',
'attributes',
'classes',
'text',
'url',
'title'
)
def __init__(
self,
text: str,
url: str,
title: str | None = None,
attrs: dict[str, Any] | None = None,
traits: Iterable[Trait] | Trait = (),
**props: Any
) -> None:
attrs = attrs or {}
attrs['data-payload'] = url
attrs['data-reveal-id'] = ''.join(
choice('abcdefghi') for i in range(8) # nosec B311
)
# Foundation 6 Compatibility
attrs['data-open'] = attrs['data-reveal-id']
attrs['data-image-parent'] = f"qr-{attrs['data-reveal-id']}"
super().__init__(text, '#', attrs, traits, **props)
[docs]
def __repr__(self) -> str:
return f'<QrCodeLink {self.text}>'
[docs]
class DeleteLink(Link):
def __init__(
self,
text: str,
url: str,
confirm: str,
yes_button_text: str | None = None,
no_button_text: str | None = None,
extra_information: str | None = None,
redirect_after: str | None = None,
request_method: str = 'DELETE',
classes: Collection[str] = ('confirm', 'delete-link'),
target: str | None = None
) -> None:
attr = {
'data-confirm': confirm
}
if extra_information:
attr['data-confirm-extra'] = extra_information
if yes_button_text:
attr['data-confirm-yes'] = yes_button_text
if no_button_text:
attr['data-confirm-no'] = no_button_text
else:
attr['data-confirm-no'] = _('Cancel')
if redirect_after:
attr['redirect-after'] = redirect_after
if target:
attr['ic-target'] = target
if request_method == 'GET':
attr['ic-get-from'] = url
url = '#'
elif request_method == 'POST':
attr['ic-post-to'] = url
url = '#'
elif request_method == 'DELETE':
pass
else:
raise NotImplementedError
super().__init__(
text=text,
url=url,
classes=classes,
request_method=request_method,
attributes=attr
)
[docs]
class ConfirmLink(DeleteLink):
# XXX this is some wonky class hierarchy with tons of parameters.
# We can do better!
def __init__(
self,
text: str,
url: str,
confirm: str,
yes_button_text: str | None = None,
no_button_text: str | None = None,
extra_information: str | None = None,
redirect_after: str | None = None,
request_method: str = 'POST',
classes: Collection[str] = ('confirm', )
) -> None:
super().__init__(
text, url, confirm, yes_button_text, no_button_text,
extra_information, redirect_after, request_method, classes)
__all__ = (
'AccessMixin',
'ConfirmLink',
'DeleteLink',
'Link',
'LinkGroup',
'QrCodeLink'
)
class IFrameLink(BaseLink):
""" Implements an iframe link that shows a modal with the iframe.
The url is sent to the iframe endpoint url which generates the iframe
and sends it back.
"""
id = 'iframe_link'
__slots__ = (
'active',
'attributes',
'classes',
'text',
'url',
'title'
)
def __init__(
self,
text: str,
url: str,
title: str | None = None,
attrs: dict[str, Any] | None = None,
traits: Iterable[Trait] | Trait = (),
**props: Any
) -> None:
attrs = attrs or {}
attrs['new-iframe-link'] = (
'<iframe src="'
+ url
+ '" width="100%" height="800" frameborder="0"></iframe>'
)
attrs['data-reveal-id'] = ''.join(
choice('abcdefghi') for i in range(8) # nosec B311
)
# Foundation 6 Compatibility
attrs['data-open'] = attrs['data-reveal-id']
attrs['data-image-parent'] = f"iframe-{attrs['data-reveal-id']}"
super().__init__(text, '#', attrs, traits, **props)
self.title = title
def __repr__(self) -> str:
return f'<IFrameLink {self.text}>'