2024-09-08 19:03:37 +00:00
|
|
|
from random import choices as random_choices
|
|
|
|
from string import ascii_lowercase
|
|
|
|
from typing import Any, Callable
|
|
|
|
|
2024-11-09 21:28:52 +00:00
|
|
|
from django.template import TemplateDoesNotExist
|
2024-11-15 17:03:08 +00:00
|
|
|
from django.template.defaultfilters import floatformat
|
2024-09-08 19:03:37 +00:00
|
|
|
from django.template.loader import render_to_string
|
|
|
|
from django.urls import NoReverseMatch, reverse
|
2024-09-14 09:07:38 +00:00
|
|
|
from django.utils.safestring import SafeText, mark_safe
|
|
|
|
|
|
|
|
from common.utils import truncate
|
2025-01-08 20:00:19 +00:00
|
|
|
from games.models import Purchase
|
2024-09-08 19:03:37 +00:00
|
|
|
|
|
|
|
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)
|
2024-09-14 08:40:03 +00:00
|
|
|
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)}"
|
2024-09-08 19:03:37 +00:00
|
|
|
tag: str = ""
|
|
|
|
if tag_name != "":
|
2024-09-14 08:40:03 +00:00
|
|
|
tag = f"<{tag_name}{attributesBlob}>{childrenBlob}</{tag_name}>"
|
2024-09-08 19:03:37 +00:00
|
|
|
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 = "",
|
2024-11-15 17:03:08 +00:00
|
|
|
wrapped_classes: str = "",
|
2024-09-08 19:03:37 +00:00
|
|
|
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),
|
2024-11-15 17:03:08 +00:00
|
|
|
("wrapped_classes", wrapped_classes),
|
2024-09-08 19:03:37 +00:00
|
|
|
],
|
|
|
|
children=children,
|
|
|
|
template="cotton/popover.html",
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2025-01-08 20:00:19 +00:00
|
|
|
def PopoverTruncated(
|
|
|
|
input_string: str,
|
|
|
|
popover_content: str = "",
|
|
|
|
popover_if_not_truncated: bool = False,
|
|
|
|
length: int = 30,
|
|
|
|
ellipsis: str = "…",
|
|
|
|
endpart: str = "",
|
|
|
|
) -> str:
|
|
|
|
"""
|
|
|
|
Returns `input_string` truncated after `length` of characters
|
|
|
|
and displays the untruncated text in a popover HTML element.
|
|
|
|
The truncated text ends in `ellipsis`, and optionally
|
|
|
|
an always-visible `endpart` can be specified.
|
|
|
|
`popover_content` can be specified if:
|
|
|
|
1. It needs to be always displayed regardless if text is truncated.
|
|
|
|
2. It needs to differ from `input_string`.
|
|
|
|
"""
|
|
|
|
if (truncated := truncate(input_string, length, ellipsis, endpart)) != input_string:
|
|
|
|
return Popover(
|
|
|
|
wrapped_content=truncated,
|
|
|
|
popover_content=popover_content if popover_content else input_string,
|
|
|
|
)
|
2024-09-14 09:07:38 +00:00
|
|
|
else:
|
2025-01-08 20:00:19 +00:00
|
|
|
if popover_content and popover_if_not_truncated:
|
|
|
|
return Popover(
|
|
|
|
wrapped_content=input_string,
|
|
|
|
popover_content=popover_content if popover_content else "",
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
return input_string
|
2024-09-14 09:07:38 +00:00
|
|
|
|
|
|
|
|
2024-09-08 19:03:37 +00:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
2024-11-09 21:28:52 +00:00
|
|
|
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,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-09-08 19:03:37 +00:00
|
|
|
def Icon(
|
|
|
|
name: str,
|
|
|
|
attributes: list[HTMLAttribute] = [],
|
|
|
|
):
|
2024-11-09 21:28:52 +00:00
|
|
|
try:
|
|
|
|
result = Component(template=f"cotton/icon/{name}.html", attributes=attributes)
|
|
|
|
except TemplateDoesNotExist:
|
|
|
|
result = Icon(name="unspecified", attributes=attributes)
|
|
|
|
return result
|
2024-09-14 09:07:38 +00:00
|
|
|
|
|
|
|
|
|
|
|
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],
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2025-01-08 20:00:19 +00:00
|
|
|
def LinkedPurchase(purchase: Purchase) -> SafeText:
|
|
|
|
link = reverse("view_purchase", args=[int(purchase.id)])
|
|
|
|
link_content = ""
|
|
|
|
popover_content = ""
|
|
|
|
edition_count = purchase.editions.count()
|
|
|
|
popover_if_not_truncated = False
|
|
|
|
if edition_count == 1:
|
|
|
|
link_content += purchase.editions.first().name
|
|
|
|
popover_content = link_content
|
|
|
|
if edition_count > 1:
|
|
|
|
if purchase.name:
|
|
|
|
link_content += f"{purchase.name}"
|
|
|
|
popover_content += f"<h1>{purchase.name}</h1><br>"
|
|
|
|
else:
|
|
|
|
link_content += f"{edition_count} games"
|
|
|
|
popover_if_not_truncated = True
|
|
|
|
popover_content += f"""
|
|
|
|
<ul class="list-disc list-inside">
|
|
|
|
{"".join(f"<li>{edition.name}</li>" for edition in purchase.editions.all())}
|
|
|
|
</ul>
|
|
|
|
"""
|
|
|
|
icon = purchase.platform.icon if edition_count == 1 else "unspecified"
|
|
|
|
if link_content == "":
|
|
|
|
raise ValueError("link_content is empty!!")
|
|
|
|
a_content = Div(
|
|
|
|
[("class", "inline-flex gap-2 items-center")],
|
|
|
|
[
|
|
|
|
Icon(
|
|
|
|
icon,
|
|
|
|
[("title", "Multiple")],
|
|
|
|
),
|
|
|
|
PopoverTruncated(
|
|
|
|
input_string=link_content,
|
|
|
|
popover_content=mark_safe(popover_content),
|
|
|
|
popover_if_not_truncated=popover_if_not_truncated,
|
|
|
|
),
|
|
|
|
],
|
|
|
|
)
|
|
|
|
return mark_safe(A(url=link, children=[a_content]))
|
|
|
|
|
|
|
|
|
2024-09-14 09:07:38 +00:00
|
|
|
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)
|
2024-11-15 17:03:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
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",
|
|
|
|
)
|