Filters (backend and frontend) #74

Open
lukas wants to merge 2 commits from filters into main
11 changed files with 91 additions and 28 deletions
Showing only changes of commit cd3e400297 - Show all commits

View File

@ -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.

View File

@ -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;
} }

View File

@ -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>

View File

@ -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 %}

View File

@ -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>

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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", "view_game",
reverse( args=[session.purchase.edition.game.pk],
"view_game", ),
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 ""}",
( (