Filters (backend and frontend) #74
|
@ -1,8 +1,9 @@
|
||||||
from random import choices
|
from random import choices
|
||||||
from string import ascii_lowercase
|
from string import ascii_lowercase
|
||||||
from typing import Any
|
from typing import Any, Callable
|
||||||
|
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
|
from django.urls import NoReverseMatch, reverse
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,16 +32,68 @@ HTMLAttribute = tuple[str, str]
|
||||||
HTMLTag = str
|
HTMLTag = str
|
||||||
|
|
||||||
|
|
||||||
def A(attributes: list[HTMLAttribute], children: list[HTMLTag] | HTMLTag) -> HTMLTag:
|
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):
|
if isinstance(children, str):
|
||||||
children = [children]
|
children = [children]
|
||||||
childrenBlob = "\n".join(children)
|
childrenBlob = "\n".join(children)
|
||||||
attributesList = [f'{name} = "{value}"' for name, value in attributes]
|
attributesList = [f'{name} = "{value}"' for name, value in attributes]
|
||||||
attributesBlob = " ".join(attributesList)
|
attributesBlob = " ".join(attributesList)
|
||||||
tag: str = f"<a {attributesBlob}>{childrenBlob}</a>"
|
tag: str = ""
|
||||||
|
if tag_name != "":
|
||||||
|
tag = f"<a {attributesBlob}>{childrenBlob}</a>"
|
||||||
|
elif template != "":
|
||||||
|
tag = render_to_string(
|
||||||
|
template,
|
||||||
|
{name: value for name, value in attributes} | {"slot": "\n".join(children)},
|
||||||
|
)
|
||||||
return mark_safe(tag)
|
return mark_safe(tag)
|
||||||
|
|
||||||
|
|
||||||
|
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 = [],
|
||||||
|
):
|
||||||
|
return Component(
|
||||||
|
template="cotton/button.html", attributes=attributes, children=children
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def safe_division(numerator: int | float, denominator: int | float) -> int | float:
|
def safe_division(numerator: int | float, denominator: int | float) -> int | float:
|
||||||
"""
|
"""
|
||||||
Divides without triggering division by zero exception.
|
Divides without triggering division by zero exception.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
! tailwindcss v3.4.10 | MIT License | https://tailwindcss.com
|
! tailwindcss v3.4.7 | MIT License | https://tailwindcss.com
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -1816,11 +1816,6 @@ input:checked + .toggle-bg {
|
||||||
border-color: rgb(220 215 254 / var(--tw-border-opacity));
|
border-color: rgb(220 215 254 / var(--tw-border-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
.\!bg-gray-50 {
|
|
||||||
--tw-bg-opacity: 1 !important;
|
|
||||||
background-color: rgb(249 250 251 / var(--tw-bg-opacity)) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-blue-100 {
|
.bg-blue-100 {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(225 239 254 / var(--tw-bg-opacity));
|
background-color: rgb(225 239 254 / var(--tw-bg-opacity));
|
||||||
|
@ -1966,6 +1961,10 @@ input:checked + .toggle-bg {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
.align-top {
|
.align-top {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
@ -2722,11 +2721,6 @@ textarea:disabled:is(.dark *) {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark\:\!bg-gray-700:is(.dark *) {
|
|
||||||
--tw-bg-opacity: 1 !important;
|
|
||||||
background-color: rgb(55 65 81 / var(--tw-bg-opacity)) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark\:bg-blue-200:is(.dark *) {
|
.dark\:bg-blue-200:is(.dark *) {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(195 221 253 / var(--tw-bg-opacity));
|
background-color: rgb(195 221 253 / var(--tw-bg-opacity));
|
||||||
|
@ -3075,6 +3069,10 @@ textarea:disabled:is(.dark *) {
|
||||||
--tw-space-x-reverse: 1;
|
--tw-space-x-reverse: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rtl\:text-left:where([dir="rtl"], [dir="rtl"] *) {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
.rtl\:text-right:where([dir="rtl"], [dir="rtl"] *) {
|
.rtl\:text-right:where([dir="rtl"], [dir="rtl"] *) {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 mt-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 mt-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">
|
||||||
{{ text }}
|
{{ text }}
|
||||||
|
{{ slot }}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -2,6 +2,11 @@
|
||||||
<div class="shadow-md sm:rounded-lg" hx-boost="false">
|
<div class="shadow-md sm:rounded-lg" hx-boost="false">
|
||||||
<div class="relative overflow-x-auto sm:rounded-lg">
|
<div class="relative overflow-x-auto sm:rounded-lg">
|
||||||
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
||||||
|
{% if header_action %}
|
||||||
|
<c-table-header>
|
||||||
|
{{ header_action }}
|
||||||
|
</c-table-header>
|
||||||
|
{% endif %}
|
||||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||||
<tr>
|
<tr>
|
||||||
{% for column in columns %}<th scope="col" class="px-6 py-3">{{ column }}</th>{% endfor %}
|
{% for column in columns %}<th scope="col" class="px-6 py-3">{{ column }}</th>{% endfor %}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<caption class="p-2 text-lg font-semibold rtl:text-left text-right text-gray-900 bg-white dark:text-white dark:bg-gray-900">
|
||||||
|
{{ slot }}
|
||||||
|
</caption>
|
|
@ -7,6 +7,7 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from common.utils import A, Button
|
||||||
from games.forms import DeviceForm
|
from games.forms import DeviceForm
|
||||||
from games.models import Device
|
from games.models import Device
|
||||||
from games.views.general import dateformat
|
from games.views.general import dateformat
|
||||||
|
@ -35,6 +36,7 @@ def list_devices(request: HttpRequest) -> HttpResponse:
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
"data": {
|
"data": {
|
||||||
|
"header_action": A([], Button([], "Add device"), url="add_device"),
|
||||||
"columns": [
|
"columns": [
|
||||||
"Name",
|
"Name",
|
||||||
"Type",
|
"Type",
|
||||||
|
|
|
@ -7,7 +7,7 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from common.utils import A, truncate_with_popover
|
from common.utils import A, Button, truncate_with_popover
|
||||||
from games.forms import EditionForm
|
from games.forms import EditionForm
|
||||||
from games.models import Edition, Game
|
from games.models import Edition, Game
|
||||||
from games.views.general import dateformat
|
from games.views.general import dateformat
|
||||||
|
@ -36,6 +36,7 @@ def list_editions(request: HttpRequest) -> HttpResponse:
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
"data": {
|
"data": {
|
||||||
|
"header_action": A([], Button([], "Add edition"), url="add_edition"),
|
||||||
"columns": [
|
"columns": [
|
||||||
"Game",
|
"Game",
|
||||||
"Name",
|
"Name",
|
||||||
|
|
|
@ -9,7 +9,7 @@ from django.template.loader import render_to_string
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from common.time import format_duration
|
from common.time import format_duration
|
||||||
from common.utils import A, safe_division, truncate_with_popover
|
from common.utils import A, Button, safe_division, truncate_with_popover
|
||||||
from games.forms import GameForm
|
from games.forms import GameForm
|
||||||
from games.models import Edition, Game, Purchase, Session
|
from games.models import Edition, Game, Purchase, Session
|
||||||
from games.views.general import (
|
from games.views.general import (
|
||||||
|
@ -45,6 +45,7 @@ def list_games(request: HttpRequest) -> HttpResponse:
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
"data": {
|
"data": {
|
||||||
|
"header_action": A([], Button([], "Add game"), url="add_game"),
|
||||||
"columns": [
|
"columns": [
|
||||||
"Name",
|
"Name",
|
||||||
"Sort Name",
|
"Sort Name",
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from common.utils import A, Button
|
||||||
from games.forms import PlatformForm
|
from games.forms import PlatformForm
|
||||||
from games.models import Platform
|
from games.models import Platform
|
||||||
from games.views.general import dateformat, use_custom_redirect
|
from games.views.general import dateformat, use_custom_redirect
|
||||||
|
@ -35,6 +36,7 @@ def list_platforms(request: HttpRequest) -> HttpResponse:
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
"data": {
|
"data": {
|
||||||
|
"header_action": A([], Button([], "Add platform"), url="add_platform"),
|
||||||
"columns": [
|
"columns": [
|
||||||
"Name",
|
"Name",
|
||||||
"Group",
|
"Group",
|
||||||
|
|
|
@ -13,7 +13,7 @@ from django.template.loader import render_to_string
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from common.utils import A, truncate_with_popover
|
from common.utils import A, Button, truncate_with_popover
|
||||||
from games.forms import PurchaseForm
|
from games.forms import PurchaseForm
|
||||||
from games.models import Edition, Purchase
|
from games.models import Edition, Purchase
|
||||||
from games.views.general import dateformat, use_custom_redirect
|
from games.views.general import dateformat, use_custom_redirect
|
||||||
|
@ -42,6 +42,7 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
"data": {
|
"data": {
|
||||||
|
"header_action": A([], Button([], "Add purchase"), url="add_purchase"),
|
||||||
"columns": [
|
"columns": [
|
||||||
"Name",
|
"Name",
|
||||||
"Type",
|
"Type",
|
||||||
|
|
|
@ -9,7 +9,7 @@ from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from common.time import format_duration
|
from common.time import format_duration
|
||||||
from common.utils import A, truncate_with_popover
|
from common.utils import A, Button, truncate_with_popover
|
||||||
from games.forms import SessionForm
|
from games.forms import SessionForm
|
||||||
from games.models import Purchase, Session
|
from games.models import Purchase, Session
|
||||||
from games.views.general import (
|
from games.views.general import (
|
||||||
|
@ -45,6 +45,7 @@ def list_sessions(request: HttpRequest) -> HttpResponse:
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
"data": {
|
"data": {
|
||||||
|
"header_action": A([], Button([], "Add session"), url="add_session"),
|
||||||
"columns": [
|
"columns": [
|
||||||
"Name",
|
"Name",
|
||||||
"Date",
|
"Date",
|
||||||
|
@ -57,16 +58,11 @@ def list_sessions(request: HttpRequest) -> HttpResponse:
|
||||||
"rows": [
|
"rows": [
|
||||||
[
|
[
|
||||||
A(
|
A(
|
||||||
[
|
children=truncate_with_popover(session.purchase.edition.name),
|
||||||
(
|
url=reverse(
|
||||||
"href",
|
|
||||||
reverse(
|
|
||||||
"view_game",
|
"view_game",
|
||||||
args=[session.purchase.edition.game.pk],
|
args=[session.purchase.edition.game.pk],
|
||||||
),
|
),
|
||||||
)
|
|
||||||
],
|
|
||||||
truncate_with_popover(session.purchase.edition.name),
|
|
||||||
),
|
),
|
||||||
f"{session.timestamp_start.strftime(datetimeformat)}{f" — {session.timestamp_end.strftime(timeformat)}" if session.timestamp_end else ""}",
|
f"{session.timestamp_start.strftime(datetimeformat)}{f" — {session.timestamp_end.strftime(timeformat)}" if session.timestamp_end else ""}",
|
||||||
(
|
(
|
||||||
|
|
Loading…
Reference in New Issue