21 Commits

Author SHA1 Message Date
fbd418882b Update exchange rates
All checks were successful
Django CI/CD / test (push) Successful in 1m0s
Django CI/CD / build-and-push (push) Has been skipped
2024-11-11 01:12:17 +01:00
6aef44d3dc Rename tasks 2024-11-11 01:12:09 +01:00
63e307d251 Run convert price task every minute 2024-11-11 00:47:48 +01:00
b0eb28618c Uppercase currency instead of lowercase 2024-11-11 00:44:47 +01:00
b05a0bd502 Improve startup setup and tasks 2024-11-11 00:38:23 +01:00
4fd5b8432b Add basic set of exchange rates 2024-11-11 00:30:34 +01:00
cbb1cbaf33 Move DEMO exclusion a bit higher 2024-11-11 00:27:15 +01:00
29b71a57cc Add logging to convert task 2024-11-11 00:27:02 +01:00
4db8d1a63b Reset converted_price when currency or price changes 2024-11-11 00:26:51 +01:00
bf9f3d5f56 Use converted price in stats 2024-11-11 00:26:33 +01:00
9a7da8a9ec Start django cluster in container 2024-11-11 00:26:11 +01:00
013375106b Convert everything to chosen currency 2024-11-11 00:07:03 +01:00
d1cb0edf87 Fix stats 2024-11-11 00:06:45 +01:00
8b939e8e6f Update tailwindcss version 2024-11-10 23:42:21 +01:00
e5f9d90127 Add more dependencies 2024-11-10 23:40:35 +01:00
3b297bfc1e Use Django ORM for scheduler 2024-11-10 23:40:23 +01:00
4a0f5761e8 Fix update_converted_prices 2024-11-10 23:40:07 +01:00
34b2c8c751 Add ExchangeRate to admin 2024-11-10 23:39:52 +01:00
05b59e41b5 Add model change 2024-11-10 23:39:43 +01:00
a32996ccc3 Schedule tasks hourly 2024-11-10 23:39:03 +01:00
527cf4ba52 Make it possible to convert prices automatically
Some checks failed
Django CI/CD / test (push) Failing after 1m19s
Django CI/CD / build-and-push (push) Has been skipped
2024-11-10 15:34:08 +01:00
7 changed files with 112 additions and 348 deletions

View File

@ -7,7 +7,6 @@
* Allow deleting purchases
* Add all-time stats
* Manage purchases
* Automatically convert purchase prices
## Improved
* mark refunded purchases red on game overview

View File

@ -125,33 +125,14 @@ def Div(
return Component(tag_name="div", attributes=attributes, children=children)
def Label(
attributes: list[HTMLAttribute] = [],
children: list[HTMLTag] | HTMLTag = [],
):
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,
return Component(
tag_name="input", attributes=attributes + [("type", type)], 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(
@ -167,74 +148,6 @@ 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] = [],

View File

@ -1,9 +1,6 @@
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:
"""
@ -67,17 +64,3 @@ 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)

View File

@ -1,101 +0,0 @@
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,17 +13,9 @@ from django.template.loader import render_to_string
from django.urls import reverse
from django.utils import timezone
from common.components import (
BooleanRadioFieldset,
Form,
Icon,
Input,
LinkedNameWithPlatformIcon,
SubmitButton,
)
from common.components import A, Button, Icon, LinkedNameWithPlatformIcon
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
@ -35,141 +27,120 @@ 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": 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
],
},
}
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
],
},
}
return render(request, "list_purchases.html", context)

2
poetry.lock generated
View File

@ -1155,4 +1155,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "b5bb46a6591964aec145637cd9a412a681f2cc5e7e4fdd6fd9ecb0fe8724b8e3"
content-hash = "f70c0aa9de08a0d7310b1379b1c5452b1a2597e57e170c9906ac85b256ec0506"

View File

@ -33,7 +33,6 @@ django-cotton = "^1.2.1"
django-q2 = "^1.7.4"
croniter = "^5.0.1"
requests = "^2.32.3"
pyyaml = "^6.0.2"
[tool.isort]
profile = "black"