207 lines
5.7 KiB
Python
207 lines
5.7 KiB
Python
from random import choices as random_choices
|
|
from string import ascii_lowercase
|
|
from typing import Any, Callable
|
|
|
|
from django.template import TemplateDoesNotExist
|
|
from django.template.defaultfilters import floatformat
|
|
from django.template.loader import render_to_string
|
|
from django.urls import NoReverseMatch, reverse
|
|
from django.utils.safestring import SafeText, mark_safe
|
|
|
|
from common.utils import truncate
|
|
|
|
HTMLAttribute = tuple[str, str | int | bool]
|
|
HTMLTag = str
|
|
|
|
|
|
def Component(
|
|
attributes: list[HTMLAttribute] = [],
|
|
children: list[HTMLTag] | HTMLTag = [],
|
|
template: str = "",
|
|
tag_name: str = "",
|
|
) -> HTMLTag:
|
|
if not tag_name and not template:
|
|
raise ValueError("One of template or tag_name is required.")
|
|
if isinstance(children, str):
|
|
children = [children]
|
|
childrenBlob = "\n".join(children)
|
|
if len(attributes) == 0:
|
|
attributesBlob = ""
|
|
else:
|
|
attributesList = [f'{name}="{value}"' for name, value in attributes]
|
|
# make attribute list into a string
|
|
# and insert space between tag and attribute list
|
|
attributesBlob = f" {" ".join(attributesList)}"
|
|
tag: str = ""
|
|
if tag_name != "":
|
|
tag = f"<{tag_name}{attributesBlob}>{childrenBlob}</{tag_name}>"
|
|
elif template != "":
|
|
tag = render_to_string(
|
|
template,
|
|
{name: value for name, value in attributes}
|
|
| {"slot": mark_safe("\n".join(children))},
|
|
)
|
|
return mark_safe(tag)
|
|
|
|
|
|
def randomid(seed: str = "", length: int = 10) -> str:
|
|
return seed + "".join(random_choices(ascii_lowercase, k=length))
|
|
|
|
|
|
def Popover(
|
|
popover_content: str,
|
|
wrapped_content: str = "",
|
|
wrapped_classes: str = "",
|
|
children: list[HTMLTag] = [],
|
|
attributes: list[HTMLAttribute] = [],
|
|
) -> str:
|
|
if not wrapped_content and not children:
|
|
raise ValueError("One of wrapped_content or children is required.")
|
|
id = randomid()
|
|
return Component(
|
|
attributes=attributes
|
|
+ [
|
|
("id", id),
|
|
("wrapped_content", wrapped_content),
|
|
("popover_content", popover_content),
|
|
("wrapped_classes", wrapped_classes),
|
|
],
|
|
children=children,
|
|
template="cotton/popover.html",
|
|
)
|
|
|
|
|
|
def PopoverTruncated(input_string: str, length: int = 30, ellipsis: str = "…") -> str:
|
|
if (truncated := truncate(input_string, length, ellipsis)) != input_string:
|
|
return Popover(wrapped_content=truncated, popover_content=input_string)
|
|
else:
|
|
return input_string
|
|
|
|
|
|
def A(
|
|
attributes: list[HTMLAttribute] = [],
|
|
children: list[HTMLTag] | HTMLTag = [],
|
|
url: str | Callable[..., Any] = "",
|
|
):
|
|
"""
|
|
Returns the HTML tag "a".
|
|
"url" can either be:
|
|
- URL (string)
|
|
- path name passed to reverse() (string)
|
|
- function
|
|
"""
|
|
additional_attributes = []
|
|
if url:
|
|
if type(url) is str:
|
|
try:
|
|
url_result = reverse(url)
|
|
except NoReverseMatch:
|
|
url_result = url
|
|
elif callable(url):
|
|
url_result = url()
|
|
else:
|
|
raise TypeError("'url' is neither str nor function.")
|
|
additional_attributes = [("href", url_result)]
|
|
return Component(
|
|
tag_name="a", attributes=attributes + additional_attributes, children=children
|
|
)
|
|
|
|
|
|
def Button(
|
|
attributes: list[HTMLAttribute] = [],
|
|
children: list[HTMLTag] | HTMLTag = [],
|
|
size: str = "base",
|
|
icon: bool = False,
|
|
color: str = "blue",
|
|
):
|
|
return Component(
|
|
template="cotton/button.html",
|
|
attributes=attributes + [("size", size), ("icon", icon), ("color", color)],
|
|
children=children,
|
|
)
|
|
|
|
|
|
def Div(
|
|
attributes: list[HTMLAttribute] = [],
|
|
children: list[HTMLTag] | HTMLTag = [],
|
|
):
|
|
return Component(tag_name="div", attributes=attributes, children=children)
|
|
|
|
|
|
def Input(
|
|
type: str = "text",
|
|
attributes: list[HTMLAttribute] = [],
|
|
children: list[HTMLTag] | HTMLTag = [],
|
|
):
|
|
return Component(
|
|
tag_name="input", attributes=attributes + [("type", type)], children=children
|
|
)
|
|
|
|
|
|
def Form(
|
|
action="",
|
|
method="get",
|
|
attributes: list[HTMLAttribute] = [],
|
|
children: list[HTMLTag] | HTMLTag = [],
|
|
):
|
|
return Component(
|
|
tag_name="form",
|
|
attributes=attributes + [("action", action), ("method", method)],
|
|
children=children,
|
|
)
|
|
|
|
|
|
def Icon(
|
|
name: str,
|
|
attributes: list[HTMLAttribute] = [],
|
|
):
|
|
try:
|
|
result = Component(template=f"cotton/icon/{name}.html", attributes=attributes)
|
|
except TemplateDoesNotExist:
|
|
result = Icon(name="unspecified", attributes=attributes)
|
|
return result
|
|
|
|
|
|
def LinkedNameWithPlatformIcon(name: str, game_id: int, platform: str) -> SafeText:
|
|
link = reverse("view_game", args=[int(game_id)])
|
|
a_content = Div(
|
|
[("class", "inline-flex gap-2 items-center")],
|
|
[
|
|
Icon(
|
|
platform.icon,
|
|
[("title", platform.name)],
|
|
),
|
|
PopoverTruncated(name),
|
|
],
|
|
)
|
|
|
|
return mark_safe(
|
|
A(
|
|
url=link,
|
|
children=[a_content],
|
|
),
|
|
)
|
|
|
|
|
|
def NameWithPlatformIcon(name: str, platform: str) -> SafeText:
|
|
content = Div(
|
|
[("class", "inline-flex gap-2 items-center")],
|
|
[
|
|
Icon(
|
|
platform.icon,
|
|
[("title", platform.name)],
|
|
),
|
|
PopoverTruncated(name),
|
|
],
|
|
)
|
|
|
|
return mark_safe(content)
|
|
|
|
|
|
def PurchasePrice(purchase) -> str:
|
|
return Popover(
|
|
popover_content=f"{floatformat(purchase.price)} {purchase.price_currency}",
|
|
wrapped_content=f"{floatformat(purchase.converted_price)} {purchase.converted_currency}",
|
|
wrapped_classes="underline decoration-dotted",
|
|
)
|