WIP: Implement filters #80

Draft
lukas wants to merge 1 commits from filters_finally into main
4 changed files with 347 additions and 113 deletions

View File

@ -125,14 +125,33 @@ def Div(
return Component(tag_name="div", attributes=attributes, children=children) return Component(tag_name="div", attributes=attributes, children=children)
def Input( def Label(
type: str = "text",
attributes: list[HTMLAttribute] = [], attributes: list[HTMLAttribute] = [],
children: list[HTMLTag] | HTMLTag = [], children: list[HTMLTag] | HTMLTag = [],
): ):
return Component( return Component(tag_name="label", attributes=attributes, children=children)
tag_name="input", attributes=attributes + [("type", type)], children=children
def Input(
type: str = "text",
label: str = "",
id: str = "",
attributes: list[HTMLAttribute] = [],
children: list[HTMLTag] | HTMLTag = [],
):
input_component = Component(
tag_name="input",
attributes=attributes + [("type", type), ("id", id)],
children=children,
) )
if label != "":
if id == "":
raise ValueError("Label is set but element ID is missing.")
return Label(
attributes=[("for", id)], children=[label, input_component, *children]
)
else:
return input_component
def Form( def Form(
@ -148,6 +167,74 @@ def Form(
) )
def Fieldset(
label: str = "",
attributes: list[HTMLAttribute] = [],
children: list[HTMLTag] | HTMLTag = [],
):
if label != "":
children = [Label(children=[label, *children])]
return Component(tag_name="fieldset", attributes=attributes, children=children)
def RadioFieldset(name: str, label: str, radio_buttons: list[dict[str, str]]):
return Component(
tag_name="span",
children=[
Component(tag_name="legend", children=label),
Component(
tag_name="fieldset",
children=[
Component(
tag_name="label",
attributes=[
("for", f"{name}__{radio["value"]}"),
],
children=[
radio["label"],
Input(
type="radio",
attributes=[
("id", f"{name}__{radio["value"]}"),
("name", name),
("value", radio["value"]),
("onClick", radio.get("onclick", "")),
],
),
],
)
for radio in radio_buttons
],
),
],
)
def BooleanRadioFieldset(name: str, label: str):
return RadioFieldset(
name=name,
label=label,
radio_buttons=[
{"label": "True", "value": "true"},
{"label": "False", "value": "false"},
],
)
def SubmitButton(label: str):
return Input(type="submit", attributes=[("value", label)])
# RadioFieldset(
# name="filter__dropped",
# label="Dropped",
# radio_buttons=[
# {"label": "True", "value": "true"},
# {"label": "False", "value": "false"},
# ],
# )
def Icon( def Icon(
name: str, name: str,
attributes: list[HTMLAttribute] = [], attributes: list[HTMLAttribute] = [],

View File

@ -1,6 +1,9 @@
from datetime import date from datetime import date
from typing import Any, Generator, TypeVar from typing import Any, Generator, TypeVar
from django.apps import apps
from django.db.models import Model
def safe_division(numerator: int | float, denominator: int | float) -> int | float: def safe_division(numerator: int | float, denominator: int | float) -> int | float:
""" """
@ -64,3 +67,17 @@ def generate_split_ranges(
def format_float_or_int(number: int | float): def format_float_or_int(number: int | float):
return int(number) if float(number).is_integer() else f"{number:03.2f}" return int(number) if float(number).is_integer() else f"{number:03.2f}"
def get_model_by_string(app_label: str, model_name: str):
return apps.get_model(app_label, model_name)
def get_field(model: Model, field_name: str):
field = model._meta.get_field(field_name)
return field
def get_field_type(model: Model, field_name: str):
field = model._meta.get_field(field_name)
return type(field)

101
games/filters.py Normal file
View File

@ -0,0 +1,101 @@
from enum import Enum
from typing import Any
from django.db.models import (
BooleanField,
CharField,
FloatField,
IntegerField,
QuerySet,
TextField,
)
from django.http import HttpRequest
from common.components import *
from common.utils import get_field, get_model_by_string
filter_param_prefix = "f_"
class Modifier(Enum):
EQUALS = "__exact"
GT = "__gt"
LT = "__lt"
CONTAINS = "__contains"
REGEX = "__regex"
ISNULL = "__isnull"
BETWEEN = "__gt", "__lt"
def create_filter_form(model: str, fields: list[str]):
filter_model = get_model_by_string("games", model)
automatic_filter_form_parts = []
for field in fields:
html_field_name = f"{filter_param_prefix}{field}"
match get_field(filter_model, field):
case BooleanField():
automatic_filter_form_parts.append(
BooleanRadioFieldset(name=html_field_name, label=field)
)
case TextField():
pass
case CharField():
js = str
onclick_handler: js = """f_price_currency.disabled = true;"""
automatic_filter_form_parts.extend(
[
RadioFieldset(
name=f"{field}_switch",
label="Modifier",
radio_buttons=[
{
"label": "Equals",
"value": Modifier.EQUALS.value,
"onclick": onclick_handler,
},
{"label": "Contains", "value": Modifier.CONTAINS.value},
],
),
Input(
label=field,
id=html_field_name,
attributes=[
("name", html_field_name + str(Modifier.EQUALS.value))
],
),
]
)
case IntegerField():
pass
case FloatField():
html = Input(
label=field,
type="number",
id=html_field_name,
attributes=[("name", html_field_name)],
)
automatic_filter_form_parts.append(html)
case _:
print(f"Field type of {field} not handled yet.")
automatic_filter_form = Form(
children=[*automatic_filter_form_parts, SubmitButton("Apply")]
)
return automatic_filter_form
def apply_filters(request: HttpRequest, queryset: QuerySet[Any]):
for parameter in request.GET:
if parameter.startswith(filter_param_prefix):
field_name = parameter.removeprefix(filter_param_prefix)
field_value = request.GET.get(parameter)
if field_value == "":
continue
match field_value:
case "true":
field_value = True
case "false":
field_value = False
case _:
pass
queryset = queryset.filter(**{f"{field_name}": field_value})
return queryset

View File

@ -13,9 +13,17 @@ 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.components import A, Button, Icon, LinkedNameWithPlatformIcon from common.components import (
BooleanRadioFieldset,
Form,
Icon,
Input,
LinkedNameWithPlatformIcon,
SubmitButton,
)
from common.time import dateformat from common.time import dateformat
from common.utils import format_float_or_int from common.utils import format_float_or_int
from games.filters import apply_filters, create_filter_form
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 use_custom_redirect from games.views.general import use_custom_redirect
@ -27,120 +35,141 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
page_number = request.GET.get("page", 1) page_number = request.GET.get("page", 1)
limit = request.GET.get("limit", 10) limit = request.GET.get("limit", 10)
purchases = Purchase.objects.order_by("-date_purchased", "-created_at") purchases = Purchase.objects.order_by("-date_purchased", "-created_at")
filter_form = create_filter_form(
"Purchase", ["infinite", "price", "price_currency"]
)
purchases = apply_filters(request, purchases)
test_form = Form(
children=[
BooleanRadioFieldset(
name="filter__infinite",
label="Infinite",
),
Input(
label="Original currency",
id="filter__price_currency",
attributes=[("name", "filter__price_currency")],
),
SubmitButton("Apply"),
]
)
page_obj = None page_obj = None
if int(limit) != 0: if int(limit) != 0:
paginator = Paginator(purchases, limit) paginator = Paginator(purchases, limit)
page_obj = paginator.get_page(page_number) page_obj = paginator.get_page(page_number)
purchases = page_obj.object_list purchases = page_obj.object_list
context = { context = {
"title": "Manage purchases", "title": "Manage purchases",
"page_obj": page_obj or None, "page_obj": page_obj or None,
"elided_page_range": ( "elided_page_range": (
page_obj.paginator.get_elided_page_range( page_obj.paginator.get_elided_page_range(
page_number, on_each_side=1, on_ends=1 page_number, on_each_side=1, on_ends=1
) )
if page_obj if page_obj
else None else None
), ),
"data": { "data": {
"header_action": A([], Button([], "Add purchase"), url="add_purchase"), "header_action": filter_form,
"columns": [ "columns": [
"Name", "Name",
"Type", "Type",
"Price", "Price",
"Currency", "Currency",
"Infinite", "Infinite",
"Purchased", "Purchased",
"Refunded", "Refunded",
"Finished", "Finished",
"Dropped", "Dropped",
"Created", "Created",
"Actions", "Actions",
], ],
"rows": [ "rows": [
[ [
LinkedNameWithPlatformIcon( LinkedNameWithPlatformIcon(
name=purchase.edition.name, name=purchase.edition.name,
game_id=purchase.edition.game.pk, game_id=purchase.edition.game.pk,
platform=purchase.platform, platform=purchase.platform,
), ),
purchase.get_type_display(), purchase.get_type_display(),
format_float_or_int(purchase.price), format_float_or_int(purchase.price),
purchase.price_currency, purchase.price_currency,
purchase.infinite, purchase.infinite,
purchase.date_purchased.strftime(dateformat), purchase.date_purchased.strftime(dateformat),
( (
purchase.date_refunded.strftime(dateformat) purchase.date_refunded.strftime(dateformat)
if purchase.date_refunded if purchase.date_refunded
else "-" else "-"
), ),
( (
purchase.date_finished.strftime(dateformat) purchase.date_finished.strftime(dateformat)
if purchase.date_finished if purchase.date_finished
else "-" else "-"
), ),
( (
purchase.date_dropped.strftime(dateformat) purchase.date_dropped.strftime(dateformat)
if purchase.date_dropped if purchase.date_dropped
else "-" else "-"
), ),
purchase.created_at.strftime(dateformat), purchase.created_at.strftime(dateformat),
render_to_string( render_to_string(
"cotton/button_group.html", "cotton/button_group.html",
{ {
"buttons": [ "buttons": [
{ {
"href": reverse( "href": reverse(
"finish_purchase", args=[purchase.pk] "finish_purchase", args=[purchase.pk]
), ),
"slot": Icon("checkmark"), "slot": Icon("checkmark"),
"title": "Mark as finished", "title": "Mark as finished",
} }
if not purchase.date_finished if not purchase.date_finished
else {}, else {},
{ {
"href": reverse( "href": reverse(
"drop_purchase", args=[purchase.pk] "drop_purchase", args=[purchase.pk]
), ),
"slot": Icon("eject"), "slot": Icon("eject"),
"title": "Mark as dropped", "title": "Mark as dropped",
} }
if not purchase.date_dropped if not purchase.date_dropped
else {}, else {},
{ {
"href": reverse( "href": reverse(
"refund_purchase", args=[purchase.pk] "refund_purchase", args=[purchase.pk]
), ),
"slot": Icon("refund"), "slot": Icon("refund"),
"title": "Mark as refunded", "title": "Mark as refunded",
} }
if not purchase.date_refunded if not purchase.date_refunded
else {}, else {},
{ {
"href": reverse( "href": reverse(
"edit_purchase", args=[purchase.pk] "edit_purchase", args=[purchase.pk]
), ),
"slot": Icon("edit"), "slot": Icon("edit"),
"title": "Edit", "title": "Edit",
"color": "gray", "color": "gray",
}, },
{ {
"href": reverse( "href": reverse(
"delete_purchase", args=[purchase.pk] "delete_purchase", args=[purchase.pk]
), ),
"slot": Icon("delete"), "slot": Icon("delete"),
"title": "Delete", "title": "Delete",
"color": "red", "color": "red",
}, },
] ]
}, },
), ),
] ]
for purchase in purchases for purchase in purchases
], ],
}, },
} }
return render(request, "list_purchases.html", context) return render(request, "list_purchases.html", context)