Compare commits

...

2 Commits

Author SHA1 Message Date
Lukáš Kucharczyk 36dd5635b2
add icon field to platform, use everywhere
Django CI/CD / test (push) Successful in 52s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-09-14 11:07:38 +02:00
Lukáš Kucharczyk c40764a02f
fix bug in Component
A leftover from when there was only the A component function,
this bug was not found earlier because we used
templates instead of tags most of the time.
2024-09-14 10:40:03 +02:00
16 changed files with 160 additions and 80 deletions

View File

@ -4,7 +4,9 @@ from typing import Any, Callable
from django.template.loader import render_to_string
from django.urls import NoReverseMatch, reverse
from django.utils.safestring import mark_safe
from django.utils.safestring import SafeText, mark_safe
from common.utils import truncate
HTMLAttribute = tuple[str, str | int | bool]
HTMLTag = str
@ -21,11 +23,16 @@ def Component(
if isinstance(children, str):
children = [children]
childrenBlob = "\n".join(children)
attributesList = [f'{name} = "{value}"' for name, value in attributes]
attributesBlob = " ".join(attributesList)
if len(attributes) == 0:
attributesBlob = ""
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 = ""
if tag_name != "":
tag = f"<a {attributesBlob}>{childrenBlob}</a>"
tag = f"<{tag_name}{attributesBlob}>{childrenBlob}</{tag_name}>"
elif template != "":
tag = render_to_string(
template,
@ -60,6 +67,13 @@ 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(
attributes: list[HTMLAttribute] = [],
children: list[HTMLTag] | HTMLTag = [],
@ -115,3 +129,39 @@ def Icon(
attributes: list[HTMLAttribute] = [],
):
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,8 +1,6 @@
from datetime import date
from typing import Any, Generator, TypeVar
from common.components import Popover
def safe_division(numerator: int | float, denominator: int | float) -> int | float:
"""
@ -44,13 +42,6 @@ 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)

View File

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

View File

@ -0,0 +1,26 @@
# 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,6 +3,7 @@ from datetime import timedelta
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import F, Sum
from django.template.defaultfilters import slugify
from django.utils import timezone
from common.time import format_duration
@ -25,11 +26,17 @@ class Game(models.Model):
class Platform(models.Model):
name = models.CharField(max_length=255)
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)
def __str__(self):
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 Meta:

View File

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

View File

@ -1,4 +1,5 @@
<c-svg title="Playstation 4" viewbox="0 0 50 50">
<c-vars title="Playstation 4" />
<c-svg :title=title viewbox="0 0 50 50">
<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
</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 [&_: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 [&_td:last-child]:text-right">
{% if slot %}
{{ slot }}
{% else %}

View File

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

View File

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

View File

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

View File

@ -13,9 +13,8 @@ from django.template.loader import render_to_string
from django.urls import reverse
from django.utils import timezone
from common.components import A, Button, Div, Icon
from common.components import A, Button, Icon, LinkedNameWithPlatformIcon
from common.time import dateformat
from common.utils import truncate_with_popover
from games.forms import PurchaseForm
from games.models import Edition, Purchase
from games.views.general import use_custom_redirect
@ -60,35 +59,10 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
],
"rows": [
[
A(
[
(
"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})"
),
],
),
LinkedNameWithPlatformIcon(
name=purchase.edition.name,
game_id=purchase.edition.game.pk,
platform=purchase.platform,
),
purchase.get_type_display(),
purchase.price,

View File

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