Compare commits
6 Commits
filters_fi
...
main
Author | SHA1 | Date |
---|---|---|
Lukáš Kucharczyk | fc0d8db8e8 | |
Lukáš Kucharczyk | 8acc4f9c5b | |
Lukáš Kucharczyk | 6b7a96dc06 | |
Lukáš Kucharczyk | 5c5fd5f26a | |
Lukáš Kucharczyk | 7181b6472c | |
Lukáš Kucharczyk | af06d07ee3 |
|
@ -6,11 +6,11 @@ repos:
|
|||
# rev: 24.8.0
|
||||
# hooks:
|
||||
# - id: black
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.13.2
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
# - repo: https://github.com/pycqa/isort
|
||||
# rev: 5.13.2
|
||||
# hooks:
|
||||
# - id: isort
|
||||
# name: isort (python)
|
||||
- repo: https://github.com/Riverside-Healthcare/djLint
|
||||
rev: v1.34.0
|
||||
hooks:
|
||||
|
|
|
@ -3,6 +3,7 @@ from string import ascii_lowercase
|
|||
from typing import Any, Callable
|
||||
|
||||
from django.template import TemplateDoesNotExist
|
||||
from django.template.defaultfilters import floatformat
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from django.utils.safestring import SafeText, mark_safe
|
||||
|
@ -50,6 +51,7 @@ def randomid(seed: str = "", length: int = 10) -> str:
|
|||
def Popover(
|
||||
popover_content: str,
|
||||
wrapped_content: str = "",
|
||||
wrapped_classes: str = "",
|
||||
children: list[HTMLTag] = [],
|
||||
attributes: list[HTMLAttribute] = [],
|
||||
) -> str:
|
||||
|
@ -62,14 +64,15 @@ def Popover(
|
|||
("id", id),
|
||||
("wrapped_content", wrapped_content),
|
||||
("popover_content", popover_content),
|
||||
("wrapped_classes", wrapped_classes),
|
||||
],
|
||||
children=children,
|
||||
template="cotton/popover.html",
|
||||
)
|
||||
|
||||
|
||||
def PopoverTruncated(input_string: str) -> str:
|
||||
if (truncated := truncate(input_string)) != input_string:
|
||||
def PopoverTruncated(input_string: str, length: int = 30, ellipsis: str = "…") -> str:
|
||||
if (truncated := truncate(input_string, length, ellipsis)) != input_string:
|
||||
return Popover(wrapped_content=truncated, popover_content=input_string)
|
||||
else:
|
||||
return input_string
|
||||
|
@ -125,33 +128,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 +151,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] = [],
|
||||
|
@ -280,3 +196,11 @@ def NameWithPlatformIcon(name: str, platform: str) -> SafeText:
|
|||
)
|
||||
|
||||
return mark_safe(content)
|
||||
|
||||
|
||||
def PurchasePrice(purchase) -> str:
|
||||
return Popover(
|
||||
popover_content=f"{floatformat(purchase.price)} {purchase.price_currency}",
|
||||
wrapped_content=f"{floatformat(purchase.converted_price)} {purchase.converted_currency}",
|
||||
wrapped_classes="underline decoration-dotted",
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
"""
|
||||
|
@ -39,8 +36,8 @@ def safe_getattr(obj: object, attr_chain: str, default: Any | None = None) -> ob
|
|||
|
||||
def truncate(input_string: str, length: int = 30, ellipsis: str = "…") -> str:
|
||||
return (
|
||||
(f"{input_string[:length-len(ellipsis)]}{ellipsis}")
|
||||
if len(input_string) > 30
|
||||
(f"{input_string[:length-len(ellipsis)].rstrip()}{ellipsis}")
|
||||
if len(input_string) > length
|
||||
else input_string
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
|
|
101
games/filters.py
101
games/filters.py
|
@ -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
|
|
@ -2192,6 +2192,10 @@ input:checked + .toggle-bg {
|
|||
text-decoration-color: #64748b;
|
||||
}
|
||||
|
||||
.decoration-dotted {
|
||||
text-decoration-style: dotted;
|
||||
}
|
||||
|
||||
.opacity-0 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
@ -3117,6 +3121,10 @@ textarea:disabled:is(.dark *) {
|
|||
flex-direction: row;
|
||||
}
|
||||
|
||||
.md\:justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.md\:space-x-8 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(2rem * var(--tw-space-x-reverse));
|
||||
|
@ -3260,3 +3268,13 @@ textarea:disabled:is(.dark *) {
|
|||
.\[\&_td\:last-child\]\:text-right td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media not all and (min-width: 640px) {
|
||||
.\[\&_td\:not\(\:first-child\)\:not\(\:last-child\)\]\:max-sm\:hidden td:not(:first-child):not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.\[\&_th\:not\(\:first-child\)\:not\(\:last-child\)\]\:max-sm\:hidden th:not(:first-child):not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<span data-popover-target={{ id }} class="{{ class }}">{{ wrapped_content|default:slot }}</span>
|
||||
<span data-popover-target={{ id }} class="{{ wrapped_classes }}">{{ wrapped_content|default:slot }}</span>
|
||||
<div data-popover
|
||||
id="{{ id }}"
|
||||
role="tooltip"
|
||||
class="absolute z-10 invisible inline-block text-sm text-white transition-opacity duration-300 bg-white border border-purple-200 rounded-lg shadow-sm opacity-0 dark:text-white dark:border-purple-600 dark:bg-purple-800">
|
||||
<div class="px-3 py-2">{{ popover_content }}</div>
|
||||
<div data-popper-arrow></div>
|
||||
<!-- for Tailwind CSS to generate decoration-dotted CSS from Python component -->
|
||||
<span class="hidden decoration-dotted"></span>
|
||||
</div>
|
||||
|
|
|
@ -7,20 +7,20 @@
|
|||
{{ 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 [&_th:not(:first-child):not(:last-child)]:max-sm:hidden">
|
||||
<tr>
|
||||
{% for column in columns %}<th scope="col" class="px-6 py-3">{{ column }}</th>{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="dark:divide-y">
|
||||
<tbody class="dark:divide-y [&_td:not(:first-child):not(:last-child)]:max-sm:hidden">
|
||||
{% for row in rows %}<c-table-row :data=row />{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if page_obj and elided_page_range %}
|
||||
<nav class="flex items-center flex-column md:flex-row justify-between px-6 py-4 dark:bg-gray-900 sm:rounded-b-lg"
|
||||
<nav class="flex items-center flex-col md:flex-row md:justify-between px-6 py-4 dark:bg-gray-900 sm:rounded-b-lg"
|
||||
aria-label="Table navigation">
|
||||
<span class="text-sm font-normal text-gray-500 dark:text-gray-400 mb-4 md:mb-0 block w-full md:inline md:w-auto">Showing <span class="font-semibold text-gray-900 dark:text-white">{{ page_obj.start_index }}</span>—<span class="font-semibold text-gray-900 dark:text-white">{{ page_obj.end_index }}</span> of <span class="font-semibold text-gray-900 dark:text-white">{{ page_obj.paginator.count }}</span></span>
|
||||
<span class="text-sm text-center font-normal text-gray-500 dark:text-gray-400 mb-4 md:mb-0 block w-full md:inline md:w-auto"><span class="font-semibold text-gray-900 dark:text-white">{{ page_obj.start_index }}</span>—<span class="font-semibold text-gray-900 dark:text-white">{{ page_obj.end_index }}</span> of <span class="font-semibold text-gray-900 dark:text-white">{{ page_obj.paginator.count }}</span></span>
|
||||
<ul class="inline-flex -space-x-px rtl:space-x-reverse text-sm h-8">
|
||||
<li>
|
||||
{% if page_obj.has_previous %}
|
||||
|
|
|
@ -142,7 +142,9 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2">Spendings ({{ total_spent_currency }})</td>
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ total_spent }} ({{ spent_per_game }}/game)</td>
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">
|
||||
{{ total_spent | floatformat }} ({{ spent_per_game | floatformat }}/game)
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -253,7 +255,7 @@
|
|||
{% for purchase in purchased_unfinished %}
|
||||
<tr>
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{% partial purchase-name %}</td>
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.converted_price }}</td>
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.converted_price | floatformat }}</td>
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_purchased | date:"d/m/Y" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -274,7 +276,7 @@
|
|||
{% for purchase in all_purchased_this_year %}
|
||||
<tr>
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{% partial purchase-name %}</td>
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.converted_price }}</td>
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.converted_price | floatformat }}</td>
|
||||
<td class="px-2 sm:px-4 md:px-6 md:py-2 font-mono">{{ purchase.date_purchased | date:"d/m/Y" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
|
|
@ -16,6 +16,7 @@ from common.components import (
|
|||
NameWithPlatformIcon,
|
||||
Popover,
|
||||
PopoverTruncated,
|
||||
PurchasePrice,
|
||||
)
|
||||
from common.time import (
|
||||
dateformat,
|
||||
|
@ -25,7 +26,7 @@ from common.time import (
|
|||
local_strftime,
|
||||
timeformat,
|
||||
)
|
||||
from common.utils import format_float_or_int, safe_division, truncate
|
||||
from common.utils import safe_division, truncate
|
||||
from games.forms import GameForm
|
||||
from games.models import Edition, Game, Purchase, Session
|
||||
from games.views.general import use_custom_redirect
|
||||
|
@ -247,7 +248,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
|
|||
),
|
||||
purchase.get_type_display(),
|
||||
purchase.date_purchased.strftime(dateformat),
|
||||
f"{format_float_or_int(purchase.price)} {purchase.price_currency}",
|
||||
PurchasePrice(purchase),
|
||||
render_to_string(
|
||||
"cotton/button_group.html",
|
||||
{
|
||||
|
|
|
@ -13,17 +13,8 @@ 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, PurchasePrice
|
||||
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 +26,118 @@ 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",
|
||||
"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(),
|
||||
PurchasePrice(purchase),
|
||||
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