WIP: Implement filters #80
|
@ -125,14 +125,33 @@ def Div(
|
|||
return Component(tag_name="div", attributes=attributes, children=children)
|
||||
|
||||
|
||||
def Input(
|
||||
type: str = "text",
|
||||
def Label(
|
||||
attributes: list[HTMLAttribute] = [],
|
||||
children: list[HTMLTag] | HTMLTag = [],
|
||||
):
|
||||
return Component(
|
||||
tag_name="input", attributes=attributes + [("type", type)], children=children
|
||||
return Component(tag_name="label", attributes=attributes, 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(
|
||||
|
@ -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(
|
||||
name: str,
|
||||
attributes: list[HTMLAttribute] = [],
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
from datetime import date
|
||||
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:
|
||||
"""
|
||||
|
@ -64,3 +67,17 @@ def generate_split_ranges(
|
|||
|
||||
def format_float_or_int(number: int | float):
|
||||
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.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.utils import format_float_or_int
|
||||
from games.filters import apply_filters, create_filter_form
|
||||
from games.forms import PurchaseForm
|
||||
from games.models import Edition, Purchase
|
||||
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)
|
||||
limit = request.GET.get("limit", 10)
|
||||
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
|
||||
if int(limit) != 0:
|
||||
paginator = Paginator(purchases, limit)
|
||||
page_obj = paginator.get_page(page_number)
|
||||
purchases = page_obj.object_list
|
||||
|
||||
context = {
|
||||
"title": "Manage purchases",
|
||||
"page_obj": page_obj or None,
|
||||
"elided_page_range": (
|
||||
page_obj.paginator.get_elided_page_range(
|
||||
page_number, on_each_side=1, on_ends=1
|
||||
)
|
||||
if page_obj
|
||||
else None
|
||||
),
|
||||
"data": {
|
||||
"header_action": A([], Button([], "Add purchase"), url="add_purchase"),
|
||||
"columns": [
|
||||
"Name",
|
||||
"Type",
|
||||
"Price",
|
||||
"Currency",
|
||||
"Infinite",
|
||||
"Purchased",
|
||||
"Refunded",
|
||||
"Finished",
|
||||
"Dropped",
|
||||
"Created",
|
||||
"Actions",
|
||||
],
|
||||
"rows": [
|
||||
[
|
||||
LinkedNameWithPlatformIcon(
|
||||
name=purchase.edition.name,
|
||||
game_id=purchase.edition.game.pk,
|
||||
platform=purchase.platform,
|
||||
),
|
||||
purchase.get_type_display(),
|
||||
format_float_or_int(purchase.price),
|
||||
purchase.price_currency,
|
||||
purchase.infinite,
|
||||
purchase.date_purchased.strftime(dateformat),
|
||||
(
|
||||
purchase.date_refunded.strftime(dateformat)
|
||||
if purchase.date_refunded
|
||||
else "-"
|
||||
),
|
||||
(
|
||||
purchase.date_finished.strftime(dateformat)
|
||||
if purchase.date_finished
|
||||
else "-"
|
||||
),
|
||||
(
|
||||
purchase.date_dropped.strftime(dateformat)
|
||||
if purchase.date_dropped
|
||||
else "-"
|
||||
),
|
||||
purchase.created_at.strftime(dateformat),
|
||||
render_to_string(
|
||||
"cotton/button_group.html",
|
||||
{
|
||||
"buttons": [
|
||||
{
|
||||
"href": reverse(
|
||||
"finish_purchase", args=[purchase.pk]
|
||||
),
|
||||
"slot": Icon("checkmark"),
|
||||
"title": "Mark as finished",
|
||||
}
|
||||
if not purchase.date_finished
|
||||
else {},
|
||||
{
|
||||
"href": reverse(
|
||||
"drop_purchase", args=[purchase.pk]
|
||||
),
|
||||
"slot": Icon("eject"),
|
||||
"title": "Mark as dropped",
|
||||
}
|
||||
if not purchase.date_dropped
|
||||
else {},
|
||||
{
|
||||
"href": reverse(
|
||||
"refund_purchase", args=[purchase.pk]
|
||||
),
|
||||
"slot": Icon("refund"),
|
||||
"title": "Mark as refunded",
|
||||
}
|
||||
if not purchase.date_refunded
|
||||
else {},
|
||||
{
|
||||
"href": reverse(
|
||||
"edit_purchase", args=[purchase.pk]
|
||||
),
|
||||
"slot": Icon("edit"),
|
||||
"title": "Edit",
|
||||
"color": "gray",
|
||||
},
|
||||
{
|
||||
"href": reverse(
|
||||
"delete_purchase", args=[purchase.pk]
|
||||
),
|
||||
"slot": Icon("delete"),
|
||||
"title": "Delete",
|
||||
"color": "red",
|
||||
},
|
||||
]
|
||||
},
|
||||
),
|
||||
]
|
||||
for purchase in purchases
|
||||
],
|
||||
},
|
||||
}
|
||||
context = {
|
||||
"title": "Manage purchases",
|
||||
"page_obj": page_obj or None,
|
||||
"elided_page_range": (
|
||||
page_obj.paginator.get_elided_page_range(
|
||||
page_number, on_each_side=1, on_ends=1
|
||||
)
|
||||
if page_obj
|
||||
else None
|
||||
),
|
||||
"data": {
|
||||
"header_action": filter_form,
|
||||
"columns": [
|
||||
"Name",
|
||||
"Type",
|
||||
"Price",
|
||||
"Currency",
|
||||
"Infinite",
|
||||
"Purchased",
|
||||
"Refunded",
|
||||
"Finished",
|
||||
"Dropped",
|
||||
"Created",
|
||||
"Actions",
|
||||
],
|
||||
"rows": [
|
||||
[
|
||||
LinkedNameWithPlatformIcon(
|
||||
name=purchase.edition.name,
|
||||
game_id=purchase.edition.game.pk,
|
||||
platform=purchase.platform,
|
||||
),
|
||||
purchase.get_type_display(),
|
||||
format_float_or_int(purchase.price),
|
||||
purchase.price_currency,
|
||||
purchase.infinite,
|
||||
purchase.date_purchased.strftime(dateformat),
|
||||
(
|
||||
purchase.date_refunded.strftime(dateformat)
|
||||
if purchase.date_refunded
|
||||
else "-"
|
||||
),
|
||||
(
|
||||
purchase.date_finished.strftime(dateformat)
|
||||
if purchase.date_finished
|
||||
else "-"
|
||||
),
|
||||
(
|
||||
purchase.date_dropped.strftime(dateformat)
|
||||
if purchase.date_dropped
|
||||
else "-"
|
||||
),
|
||||
purchase.created_at.strftime(dateformat),
|
||||
render_to_string(
|
||||
"cotton/button_group.html",
|
||||
{
|
||||
"buttons": [
|
||||
{
|
||||
"href": reverse(
|
||||
"finish_purchase", args=[purchase.pk]
|
||||
),
|
||||
"slot": Icon("checkmark"),
|
||||
"title": "Mark as finished",
|
||||
}
|
||||
if not purchase.date_finished
|
||||
else {},
|
||||
{
|
||||
"href": reverse(
|
||||
"drop_purchase", args=[purchase.pk]
|
||||
),
|
||||
"slot": Icon("eject"),
|
||||
"title": "Mark as dropped",
|
||||
}
|
||||
if not purchase.date_dropped
|
||||
else {},
|
||||
{
|
||||
"href": reverse(
|
||||
"refund_purchase", args=[purchase.pk]
|
||||
),
|
||||
"slot": Icon("refund"),
|
||||
"title": "Mark as refunded",
|
||||
}
|
||||
if not purchase.date_refunded
|
||||
else {},
|
||||
{
|
||||
"href": reverse(
|
||||
"edit_purchase", args=[purchase.pk]
|
||||
),
|
||||
"slot": Icon("edit"),
|
||||
"title": "Edit",
|
||||
"color": "gray",
|
||||
},
|
||||
{
|
||||
"href": reverse(
|
||||
"delete_purchase", args=[purchase.pk]
|
||||
),
|
||||
"slot": Icon("delete"),
|
||||
"title": "Delete",
|
||||
"color": "red",
|
||||
},
|
||||
]
|
||||
},
|
||||
),
|
||||
]
|
||||
for purchase in purchases
|
||||
],
|
||||
},
|
||||
}
|
||||
return render(request, "list_purchases.html", context)
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue