From c2853a3ecce65b9b5caf977057c30328b8854d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Kucharczyk?= Date: Wed, 8 Jan 2025 21:00:19 +0100 Subject: [PATCH] purchases can now refer to multiple editions allows purchases to be for more than one game --- common/components.py | 75 ++++++++++++++++++- common/utils.py | 19 ++++- games/forms.py | 16 ++-- .../migrations/0042_purchase_editions_temp.py | 18 +++++ games/migrations/0043_auto_20250107_2117.py | 27 +++++++ games/migrations/0044_auto_20250107_2132.py | 18 +++++ .../0045_alter_purchase_editions.py | 18 +++++ games/models.py | 18 +++-- games/static/base.css | 16 ++++ games/templates/stats.html | 6 +- games/templates/view_purchase.html | 34 +++++++++ games/urls.py | 5 ++ games/views/game.py | 26 +++---- games/views/general.py | 70 ++++++++--------- games/views/purchase.py | 18 +++-- games/views/session.py | 8 +- 16 files changed, 308 insertions(+), 84 deletions(-) create mode 100644 games/migrations/0042_purchase_editions_temp.py create mode 100644 games/migrations/0043_auto_20250107_2117.py create mode 100644 games/migrations/0044_auto_20250107_2132.py create mode 100644 games/migrations/0045_alter_purchase_editions.py create mode 100644 games/templates/view_purchase.html diff --git a/common/components.py b/common/components.py index 09656fb..59db722 100644 --- a/common/components.py +++ b/common/components.py @@ -9,6 +9,7 @@ from django.urls import NoReverseMatch, reverse from django.utils.safestring import SafeText, mark_safe from common.utils import truncate +from games.models import Purchase HTMLAttribute = tuple[str, str | int | bool] HTMLTag = str @@ -71,11 +72,36 @@ def Popover( ) -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) +def PopoverTruncated( + input_string: str, + popover_content: str = "", + popover_if_not_truncated: bool = False, + length: int = 30, + ellipsis: str = "…", + endpart: str = "", +) -> str: + """ + Returns `input_string` truncated after `length` of characters + and displays the untruncated text in a popover HTML element. + The truncated text ends in `ellipsis`, and optionally + an always-visible `endpart` can be specified. + `popover_content` can be specified if: + 1. It needs to be always displayed regardless if text is truncated. + 2. It needs to differ from `input_string`. + """ + if (truncated := truncate(input_string, length, ellipsis, endpart)) != input_string: + return Popover( + wrapped_content=truncated, + popover_content=popover_content if popover_content else input_string, + ) else: - return input_string + if popover_content and popover_if_not_truncated: + return Popover( + wrapped_content=input_string, + popover_content=popover_content if popover_content else "", + ) + else: + return input_string def A( @@ -183,6 +209,47 @@ def LinkedNameWithPlatformIcon(name: str, game_id: int, platform: str) -> SafeTe ) +def LinkedPurchase(purchase: Purchase) -> SafeText: + link = reverse("view_purchase", args=[int(purchase.id)]) + link_content = "" + popover_content = "" + edition_count = purchase.editions.count() + popover_if_not_truncated = False + if edition_count == 1: + link_content += purchase.editions.first().name + popover_content = link_content + if edition_count > 1: + if purchase.name: + link_content += f"{purchase.name}" + popover_content += f"

{purchase.name}


" + else: + link_content += f"{edition_count} games" + popover_if_not_truncated = True + popover_content += f""" + + """ + icon = purchase.platform.icon if edition_count == 1 else "unspecified" + if link_content == "": + raise ValueError("link_content is empty!!") + a_content = Div( + [("class", "inline-flex gap-2 items-center")], + [ + Icon( + icon, + [("title", "Multiple")], + ), + PopoverTruncated( + input_string=link_content, + popover_content=mark_safe(popover_content), + popover_if_not_truncated=popover_if_not_truncated, + ), + ], + ) + return mark_safe(A(url=link, children=[a_content])) + + def NameWithPlatformIcon(name: str, platform: str) -> SafeText: content = Div( [("class", "inline-flex gap-2 items-center")], diff --git a/common/utils.py b/common/utils.py index c687366..80d9acb 100644 --- a/common/utils.py +++ b/common/utils.py @@ -34,7 +34,7 @@ def safe_getattr(obj: object, attr_chain: str, default: Any | None = None) -> ob return obj -def truncate(input_string: str, length: int = 30, ellipsis: str = "…") -> str: +def truncate_(input_string: str, length: int = 30, ellipsis: str = "…") -> str: return ( (f"{input_string[:length-len(ellipsis)].rstrip()}{ellipsis}") if len(input_string) > length @@ -42,6 +42,23 @@ def truncate(input_string: str, length: int = 30, ellipsis: str = "…") -> str: ) +def truncate( + input_string: str, length: int = 30, ellipsis: str = "…", endpart: str = "" +) -> str: + max_content_length = length - len(endpart) + if max_content_length < 0: + raise ValueError("Length cannot be shorter than the length of endpart.") + + if len(input_string) > max_content_length: + return f"{input_string[:max_content_length - len(ellipsis)].rstrip()}{ellipsis}{endpart}" + + return ( + f"{input_string}{endpart}" + if len(input_string) + len(endpart) <= length + else f"{input_string[:length - len(ellipsis) - len(endpart)].rstrip()}{ellipsis}{endpart}" + ) + + T = TypeVar("T", str, int, date) diff --git a/games/forms.py b/games/forms.py index 10413bf..415b0cb 100644 --- a/games/forms.py +++ b/games/forms.py @@ -16,7 +16,7 @@ class SessionForm(forms.ModelForm): # queryset=Purchase.objects.filter(date_refunded=None).order_by("edition__name") # ) purchase = forms.ModelChoiceField( - queryset=Purchase.objects.order_by("edition__sort_name"), + queryset=Purchase.objects.all(), widget=forms.Select(attrs={"autofocus": "autofocus"}), ) @@ -38,12 +38,12 @@ class SessionForm(forms.ModelForm): ] -class EditionChoiceField(forms.ModelChoiceField): +class EditionChoiceField(forms.ModelMultipleChoiceField): def label_from_instance(self, obj) -> str: return f"{obj.sort_name} ({obj.platform}, {obj.year_released})" -class IncludePlatformSelect(forms.Select): +class IncludePlatformSelect(forms.SelectMultiple): def create_option(self, name, value, *args, **kwargs): option = super().create_option(name, value, *args, **kwargs) if platform_id := safe_getattr(value, "instance.platform.id"): @@ -58,7 +58,7 @@ class PurchaseForm(forms.ModelForm): # Automatically update related_purchase