Compare commits

..

No commits in common. "36dd5635b2ff4187adfd4f6cf0a4ba8a2e8eb9c9" and "649351efde3d1f770e5cabdb3a627cdef21a4889" have entirely different histories.

16 changed files with 80 additions and 160 deletions

View File

@ -4,9 +4,7 @@ from typing import Any, Callable
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import NoReverseMatch, reverse from django.urls import NoReverseMatch, reverse
from django.utils.safestring import SafeText, mark_safe from django.utils.safestring import mark_safe
from common.utils import truncate
HTMLAttribute = tuple[str, str | int | bool] HTMLAttribute = tuple[str, str | int | bool]
HTMLTag = str HTMLTag = str
@ -23,16 +21,11 @@ def Component(
if isinstance(children, str): if isinstance(children, str):
children = [children] children = [children]
childrenBlob = "\n".join(children) childrenBlob = "\n".join(children)
if len(attributes) == 0: attributesList = [f'{name} = "{value}"' for name, value in attributes]
attributesBlob = "" attributesBlob = " ".join(attributesList)
else:
attributesList = [f'{name}="{value}"' for name, value in attributes]
# make attribute list into a string
# and insert space between tag and attribute list
attributesBlob = f" {" ".join(attributesList)}"
tag: str = "" tag: str = ""
if tag_name != "": if tag_name != "":
tag = f"<{tag_name}{attributesBlob}>{childrenBlob}</{tag_name}>" tag = f"<a {attributesBlob}>{childrenBlob}</a>"
elif template != "": elif template != "":
tag = render_to_string( tag = render_to_string(
template, template,
@ -67,13 +60,6 @@ def Popover(
) )
def PopoverTruncated(input_string: str) -> str:
if (truncated := truncate(input_string)) != input_string:
return Popover(wrapped_content=truncated, popover_content=input_string)
else:
return input_string
def A( def A(
attributes: list[HTMLAttribute] = [], attributes: list[HTMLAttribute] = [],
children: list[HTMLTag] | HTMLTag = [], children: list[HTMLTag] | HTMLTag = [],
@ -129,39 +115,3 @@ def Icon(
attributes: list[HTMLAttribute] = [], attributes: list[HTMLAttribute] = [],
): ):
return Component(template=f"cotton/icon/{name}.html", attributes=attributes) return Component(template=f"cotton/icon/{name}.html", attributes=attributes)
def LinkedNameWithPlatformIcon(name: str, game_id: int, platform: str) -> SafeText:
link = reverse("view_game", args=[int(game_id)])
a_content = Div(
[("class", "inline-flex gap-2 items-center")],
[
Icon(
platform.icon,
[("title", platform.name)],
),
PopoverTruncated(name),
],
)
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")],
[
Icon(
platform.icon,
[("title", platform.name)],
),
PopoverTruncated(name),
],
)
return mark_safe(content)

View File

@ -1,6 +1,8 @@
from datetime import date from datetime import date
from typing import Any, Generator, TypeVar from typing import Any, Generator, TypeVar
from common.components import Popover
def safe_division(numerator: int | float, denominator: int | float) -> int | float: def safe_division(numerator: int | float, denominator: int | float) -> int | float:
""" """
@ -42,6 +44,13 @@ def truncate(input_string: str, length: int = 30, ellipsis: str = "…") -> str:
) )
def truncate_with_popover(input_string: str) -> str:
if (truncated := truncate(input_string)) != input_string:
return Popover(wrapped_content=truncated, popover_content=input_string)
else:
return input_string
T = TypeVar("T", str, int, date) T = TypeVar("T", str, int, date)

View File

@ -164,11 +164,7 @@ class GameForm(forms.ModelForm):
class PlatformForm(forms.ModelForm): class PlatformForm(forms.ModelForm):
class Meta: class Meta:
model = Platform model = Platform
fields = [ fields = ["name", "group"]
"name",
"icon",
"group",
]
widgets = {"name": autofocus_input_widget} widgets = {"name": autofocus_input_widget}

View File

@ -1,26 +0,0 @@
# Generated by Django 5.1.1 on 2024-09-14 07:05
from django.db import migrations, models
from django.utils.text import slugify
def update_empty_icons(apps, schema_editor):
Platform = apps.get_model("games", "Platform")
for platform in Platform.objects.filter(icon=""):
platform.icon = slugify(platform.name)
platform.save()
class Migration(migrations.Migration):
dependencies = [
("games", "0036_alter_edition_platform"),
]
operations = [
migrations.AddField(
model_name="platform",
name="icon",
field=models.SlugField(blank=True),
),
migrations.RunPython(update_empty_icons),
]

View File

@ -3,7 +3,6 @@ from datetime import timedelta
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import F, Sum from django.db.models import F, Sum
from django.template.defaultfilters import slugify
from django.utils import timezone from django.utils import timezone
from common.time import format_duration from common.time import format_duration
@ -26,17 +25,11 @@ class Game(models.Model):
class Platform(models.Model): class Platform(models.Model):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
group = models.CharField(max_length=255, null=True, blank=True, default=None) group = models.CharField(max_length=255, null=True, blank=True, default=None)
icon = models.SlugField(blank=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
return self.name return self.name
def save(self, *args, **kwargs):
if not self.icon:
self.icon = slugify(self.name)
super().save(*args, **kwargs)
class Edition(models.Model): class Edition(models.Model):
class Meta: class Meta:

View File

@ -3207,7 +3207,3 @@ textarea:disabled:is(.dark *) {
.\[\&_h1\]\:mb-2 h1 { .\[\&_h1\]\:mb-2 h1 {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.\[\&_td\:last-child\]\:text-right td:last-child {
text-align: right;
}

View File

@ -1,5 +1,4 @@
<c-vars title="Playstation 4" /> <c-svg title="Playstation 4" viewbox="0 0 50 50">
<c-svg :title=title viewbox="0 0 50 50">
<c-slot name="path"> <c-slot name="path">
M 1 19 A 1.0001 1.0001 0 1 0 1 21 L 12.5 21 C 13.340812 21 14 21.659188 14 22.5 C 14 23.340812 13.340812 24 12.5 24 L 3 24 C 1.3550302 24 0 25.35503 0 27 L 0 30 A 1.0001 1.0001 0 1 0 2 30 L 2 27 C 2 26.43497 2.4349698 26 3 26 L 12.5 26 C 14.28508 26 15.719786 24.619005 15.921875 22.884766 A 1.0001 1.0001 0 0 0 16 22.5 C 16 20.578812 14.421188 19 12.5 19 L 1 19 z M 26 19 C 24.35503 19 23 20.35503 23 22 L 23 28 C 23 28.56503 22.56503 29 22 29 L 16 29 A 1.0001 1.0001 0 1 0 16 31 L 22 31 C 23.64497 31 25 29.64497 25 28 L 25 22 C 25 21.43497 25.43497 21 26 21 L 32 21 A 1.0001 1.0001 0 1 0 32 19 L 26 19 z M 46.970703 19 A 1.0001 1.0001 0 0 0 46.503906 19.130859 L 32.503906 27.130859 A 1.0001 1.0001 0 0 0 33 29 L 46 29 L 46 30 A 1.0001 1.0001 0 1 0 48 30 L 48 29 L 49 29 A 1.0001 1.0001 0 1 0 49 27 L 48 27 L 48 20 A 1.0001 1.0001 0 0 0 46.970703 19 z M 46 21.724609 L 46 27 L 36.767578 27 L 46 21.724609 z M 1 19 A 1.0001 1.0001 0 1 0 1 21 L 12.5 21 C 13.340812 21 14 21.659188 14 22.5 C 14 23.340812 13.340812 24 12.5 24 L 3 24 C 1.3550302 24 0 25.35503 0 27 L 0 30 A 1.0001 1.0001 0 1 0 2 30 L 2 27 C 2 26.43497 2.4349698 26 3 26 L 12.5 26 C 14.28508 26 15.719786 24.619005 15.921875 22.884766 A 1.0001 1.0001 0 0 0 16 22.5 C 16 20.578812 14.421188 19 12.5 19 L 1 19 z M 26 19 C 24.35503 19 23 20.35503 23 22 L 23 28 C 23 28.56503 22.56503 29 22 29 L 16 29 A 1.0001 1.0001 0 1 0 16 31 L 22 31 C 23.64497 31 25 29.64497 25 28 L 25 22 C 25 21.43497 25.43497 21 26 21 L 32 21 A 1.0001 1.0001 0 1 0 32 19 L 26 19 z M 46.970703 19 A 1.0001 1.0001 0 0 0 46.503906 19.130859 L 32.503906 27.130859 A 1.0001 1.0001 0 0 0 33 29 L 46 29 L 46 30 A 1.0001 1.0001 0 1 0 48 30 L 48 29 L 49 29 A 1.0001 1.0001 0 1 0 49 27 L 48 27 L 48 20 A 1.0001 1.0001 0 0 0 46.970703 19 z M 46 21.724609 L 46 27 L 36.767578 27 L 46 21.724609 z
</c-slot> </c-slot>

View File

@ -1,4 +1,4 @@
<tr class="odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 [&_a]:underline [&_a]:underline-offset-4 [&_a]:decoration-2 [&_td:last-child]:text-right"> <tr class="odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 [&_a]:underline [&_a]:underline-offset-4 [&_a]:decoration-2 [&_:last-child]:text-right">
{% if slot %} {% if slot %}
{{ slot }} {{ slot }}
{% else %} {% else %}

View File

@ -7,14 +7,9 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse from django.urls import reverse
from common.components import ( from common.components import A, Button, Icon
A,
Button,
Icon,
LinkedNameWithPlatformIcon,
PopoverTruncated,
)
from common.time import dateformat, local_strftime from common.time import dateformat, local_strftime
from common.utils import truncate_with_popover
from games.forms import EditionForm from games.forms import EditionForm
from games.models import Edition, Game from games.models import Edition, Game
@ -55,22 +50,30 @@ def list_editions(request: HttpRequest) -> HttpResponse:
], ],
"rows": [ "rows": [
[ [
LinkedNameWithPlatformIcon( A(
name=edition.name, [
game_id=edition.game.id, (
platform=edition.platform, "href",
reverse(
"view_game",
args=[edition.game.pk],
),
)
],
truncate_with_popover(edition.game.name),
), ),
PopoverTruncated( truncate_with_popover(
edition.name edition.name
if edition.game.name != edition.name if edition.game.name != edition.name
else "(identical)" else "(identical)"
), ),
PopoverTruncated( truncate_with_popover(
edition.sort_name edition.sort_name
if edition.sort_name is not None if edition.sort_name is not None
and edition.game.name != edition.sort_name and edition.game.name != edition.sort_name
else "(identical)" else "(identical)"
), ),
truncate_with_popover(str(edition.platform)),
edition.year_released, edition.year_released,
edition.wikidata, edition.wikidata,
local_strftime(edition.created_at, dateformat), local_strftime(edition.created_at, dateformat),

View File

@ -8,15 +8,7 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse from django.urls import reverse
from common.components import ( from common.components import A, Button, Div, Icon, Popover
A,
Button,
Div,
Icon,
NameWithPlatformIcon,
Popover,
PopoverTruncated,
)
from common.time import ( from common.time import (
dateformat, dateformat,
durationformat, durationformat,
@ -25,7 +17,7 @@ from common.time import (
local_strftime, local_strftime,
timeformat, timeformat,
) )
from common.utils import safe_division, truncate from common.utils import safe_division, truncate, truncate_with_popover
from games.forms import GameForm from games.forms import GameForm
from games.models import Edition, Game, Purchase, Session from games.models import Edition, Game, Purchase, Session
from games.views.general import use_custom_redirect from games.views.general import use_custom_redirect
@ -75,9 +67,9 @@ def list_games(request: HttpRequest) -> HttpResponse:
), ),
) )
], ],
PopoverTruncated(game.name), truncate_with_popover(game.name),
), ),
PopoverTruncated( truncate_with_popover(
game.sort_name game.sort_name
if game.sort_name is not None and game.name != game.sort_name if game.sort_name is not None and game.name != game.sort_name
else "(identical)" else "(identical)"
@ -205,15 +197,14 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
edition_data: dict[str, Any] = { edition_data: dict[str, Any] = {
"columns": [ "columns": [
"Name", "Name",
"Platform",
"Year Released", "Year Released",
"Actions", "Actions",
], ],
"rows": [ "rows": [
[ [
NameWithPlatformIcon( edition.name,
name=edition.name, Icon(str(edition.platform).lower().replace(".", "")),
platform=edition.platform,
),
edition.year_released, edition.year_released,
render_to_string( render_to_string(
"cotton/button_group.html", "cotton/button_group.html",
@ -241,10 +232,7 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
"columns": ["Name", "Type", "Date", "Price", "Actions"], "columns": ["Name", "Type", "Date", "Price", "Actions"],
"rows": [ "rows": [
[ [
NameWithPlatformIcon( purchase.name if purchase.name else purchase.edition.name,
name=purchase.name if purchase.name else purchase.edition.name,
platform=purchase.platform,
),
purchase.get_type_display(), purchase.get_type_display(),
purchase.date_purchased.strftime(dateformat), purchase.date_purchased.strftime(dateformat),
f"{purchase.price} {purchase.price_currency}", f"{purchase.price} {purchase.price_currency}",
@ -313,15 +301,9 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
), ),
], ],
), ),
"columns": ["Edition", "Date", "Duration", "Actions"], "columns": ["Date", "Duration", "Actions"],
"rows": [ "rows": [
[ [
NameWithPlatformIcon(
name=session.purchase.name
if session.purchase.name
else session.purchase.edition.name,
platform=session.purchase.platform,
),
f"{local_strftime(session.timestamp_start)}{f"{session.timestamp_end.strftime(timeformat)}" if session.timestamp_end else ""}", f"{local_strftime(session.timestamp_start)}{f"{session.timestamp_end.strftime(timeformat)}" if session.timestamp_end else ""}",
( (
format_duration(session.duration_calculated, durationformat) format_duration(session.duration_calculated, durationformat)

View File

@ -40,7 +40,6 @@ def list_platforms(request: HttpRequest) -> HttpResponse:
"header_action": A([], Button([], "Add platform"), url="add_platform"), "header_action": A([], Button([], "Add platform"), url="add_platform"),
"columns": [ "columns": [
"Name", "Name",
"Icon",
"Group", "Group",
"Created", "Created",
"Actions", "Actions",
@ -48,7 +47,6 @@ def list_platforms(request: HttpRequest) -> HttpResponse:
"rows": [ "rows": [
[ [
platform.name, platform.name,
Icon(platform.icon),
platform.group, platform.group,
local_strftime(platform.created_at, dateformat), local_strftime(platform.created_at, dateformat),
render_to_string( render_to_string(

View File

@ -13,8 +13,9 @@ 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 A, Button, Div, Icon
from common.time import dateformat from common.time import dateformat
from common.utils import truncate_with_popover
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
@ -59,10 +60,35 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
], ],
"rows": [ "rows": [
[ [
LinkedNameWithPlatformIcon( A(
name=purchase.edition.name, [
game_id=purchase.edition.game.pk, (
platform=purchase.platform, "href",
reverse(
"view_game",
args=[purchase.edition.game.pk],
),
),
],
Div(
attributes=[("class", "inline-flex gap-2 items-center")],
children=[
Icon(
str(purchase.platform)
.lower()
.translate(
str(purchase.platform)
.lower()
.maketrans("", "", ". /()")
)
),
truncate_with_popover(
purchase.edition.game.name
if purchase.type == "game"
else f"{purchase.edition.game.name} ({purchase.name})"
),
],
),
), ),
purchase.get_type_display(), purchase.get_type_display(),
purchase.price, purchase.price,

View File

@ -8,7 +8,7 @@ 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, Div, Icon, LinkedNameWithPlatformIcon, Popover from common.components import A, Button, Div, Icon, Popover
from common.time import ( from common.time import (
dateformat, dateformat,
durationformat, durationformat,
@ -17,7 +17,7 @@ from common.time import (
local_strftime, local_strftime,
timeformat, timeformat,
) )
from common.utils import truncate from common.utils import truncate, truncate_with_popover
from games.forms import SessionForm from games.forms import SessionForm
from games.models import Purchase, Session from games.models import Purchase, Session
from games.views.general import use_custom_redirect from games.views.general import use_custom_redirect
@ -91,10 +91,12 @@ def list_sessions(request: HttpRequest) -> HttpResponse:
], ],
"rows": [ "rows": [
[ [
LinkedNameWithPlatformIcon( A(
name=session.purchase.edition.name, children=truncate_with_popover(session.purchase.edition.name),
game_id=session.purchase.edition.game.pk, url=reverse(
platform=session.purchase.platform, "view_game",
args=[session.purchase.edition.game.pk],
),
), ),
f"{local_strftime(session.timestamp_start)}{f"{local_strftime(session.timestamp_end, timeformat)}" if session.timestamp_end else ""}", f"{local_strftime(session.timestamp_start)}{f"{local_strftime(session.timestamp_end, timeformat)}" if session.timestamp_end else ""}",
( (
@ -236,14 +238,6 @@ def end_session(
return redirect("list_sessions") return redirect("list_sessions")
@login_required
def delete_session(request: HttpRequest, session_id: int = 0) -> HttpResponse:
session = get_object_or_404(Session, id=session_id)
session.delete()
return redirect("list_sessions")
return redirect("list_sessions")
@login_required @login_required
def delete_session(request: HttpRequest, session_id: int = 0) -> HttpResponse: def delete_session(request: HttpRequest, session_id: int = 0) -> HttpResponse:
session = get_object_or_404(Session, id=session_id) session = get_object_or_404(Session, id=session_id)