WIP: Implement filters #80
|
@ -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] = [],
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue