Compare commits
	
		
			5 Commits
		
	
	
		
			filters-wi
			...
			60d3ba6569
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						60d3ba6569
	
				 | 
					
					
						|||
| 
						
						
							
						
						bcb845adac
	
				 | 
					
					
						|||
| 
						
						
							
						
						bd222f253e
	
				 | 
					
					
						|||
| 
						
						
							
						
						45e3cfed00
	
				 | 
					
					
						|||
| 
						
						
							
						
						36dd5635b2
	
				 | 
					
					
						
@ -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
 | 
			
		||||
@ -65,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 = [],
 | 
			
		||||
@ -120,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)
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										26
									
								
								games/migrations/0037_platform_icon.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								games/migrations/0037_platform_icon.py
									
									
									
									
									
										Normal 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),
 | 
			
		||||
    ]
 | 
			
		||||
@ -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:
 | 
			
		||||
 | 
			
		||||
@ -3188,10 +3188,6 @@ textarea:disabled:is(.dark *) {
 | 
			
		||||
  border-end-end-radius: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.\[\&_\:last-child\]\:text-right :last-child {
 | 
			
		||||
  text-align: right;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.\[\&_a\]\:underline a {
 | 
			
		||||
  text-decoration-line: underline;
 | 
			
		||||
}
 | 
			
		||||
@ -3207,3 +3203,7 @@ textarea:disabled:is(.dark *) {
 | 
			
		||||
.\[\&_h1\]\:mb-2 h1 {
 | 
			
		||||
  margin-bottom: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.\[\&_td\:last-child\]\:text-right td:last-child {
 | 
			
		||||
  text-align: right;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
<c-svg title="Epic Games Store" viewbox="0 0 50 50">
 | 
			
		||||
<c-vars title="Epic Games Store" />
 | 
			
		||||
<c-svg :title=title viewbox="0 0 50 50">
 | 
			
		||||
<c-slot name="path">
 | 
			
		||||
M 10 3 C 6.69 3 4 5.69 4 9 L 4 41.240234 L 25 47.539062 L 46 41.240234 L 46 9 C 46 5.69 43.31 3 40 3 L 10 3 z M 11 8 L 15 8 L 15 11 L 11 11 L 11 18 L 14 18 L 14 21 L 11 21 L 11 28 L 15 28 L 15 31 L 11 31 C 9.34 31 8 29.66 8 28 L 8 11 C 8 9.34 9.34 8 11 8 z M 17 8 L 23 8 C 24.66 8 26 9.34 26 11 L 26 18 C 26 19.66 24.66 21 23 21 L 20 21 L 20 31 L 17 31 L 17 8 z M 28 8 L 31 8 L 31 31 L 28 31 L 28 8 z M 36 8 L 39 8 C 40.66 8 42 9.34 42 11 L 42 15 L 39 15 L 39 11 L 36 11 L 36 28 L 39 28 L 39 24 L 42 24 L 42 28 C 42 29.66 40.66 31 39 31 L 36 31 C 34.34 31 33 29.66 33 28 L 33 11 C 33 9.34 34.34 8 36 8 z M 20 11 L 20 18 L 23 18 L 23 11 L 20 11 z M 9 34 L 13 34 C 13.55 34 14 34.45 14 35 L 14 36 L 13 36 L 13 35.25 C 13 35.11 12.89 35 12.75 35 L 9.25 35 C 9.11 35 9 35.11 9 35.25 L 9 38.75 C 9 38.89 9.11 39 9.25 39 L 12.75 39 C 12.89 39 13 38.89 13 38.75 L 13 38 L 12 38 L 12 37 L 14 37 L 14 39 C 14 39.55 13.55 40 13 40 L 9 40 C 8.45 40 8 39.55 8 39 L 8 35 C 8 34.45 8.45 34 9 34 z M 18 34 L 19 34 L 22 40 L 21 40 L 20.5 39 L 16.5 39 L 16 40 L 15 40 L 18 34 z M 23 34 L 24 34 L 26 38 L 28 34 L 29 34 L 29 40 L 28 40 L 28 36 L 26.5 39 L 25.5 39 L 24 36 L 24 40 L 23 40 L 23 34 z M 30 34 L 35 34 L 35 35 L 31 35 L 31 36.5 L 33 36.5 L 33 37.5 L 31 37.5 L 31 39 L 35 39 L 35 40 L 30 40 L 30 34 z M 37 34 L 41 34 C 41.55 34 42 34.45 42 35 L 42 35.5 L 41 35.5 L 41 35.25 C 41 35.11 40.89 35 40.75 35 L 37.25 35 C 37.11 35 37 35.11 37 35.25 L 37 36.25 C 37 36.39 37.11 36.5 37.25 36.5 L 41 36.5 C 41.55 36.5 42 36.95 42 37.5 L 42 39 C 42 39.55 41.55 40 41 40 L 37 40 C 36.45 40 36 39.55 36 39 L 36 38.5 L 37 38.5 L 37 38.75 C 37 38.89 37.11 39 37.25 39 L 40.75 39 C 40.89 39 41 38.89 41 38.75 L 41 37.75 C 41 37.61 40.89 37.5 40.75 37.5 L 37 37.5 C 36.45 37.5 36 37.05 36 36.5 L 36 35 C 36 34.45 36.45 34 37 34 z M 18.5 35 L 17 38 L 20 38 L 18.5 35 z
 | 
			
		||||
</c-slot>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								games/templates/cotton/icon/nintendo-3ds.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								games/templates/cotton/icon/nintendo-3ds.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
<c-icon.nintendo />
 | 
			
		||||
							
								
								
									
										6
									
								
								games/templates/cotton/icon/nintendo.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								games/templates/cotton/icon/nintendo.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
			
		||||
<c-vars title="Nintendo" />
 | 
			
		||||
<c-svg viewBox="0 0 24 24">
 | 
			
		||||
    <c-slot name="path">
 | 
			
		||||
    M0 .6h7.1l9.85 15.9V.6H24v22.8h-7.04L7.06 7.5v15.9H0V.6
 | 
			
		||||
    </c-slot>
 | 
			
		||||
</c-svg>
 | 
			
		||||
							
								
								
									
										6
									
								
								games/templates/cotton/icon/playstation.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								games/templates/cotton/icon/playstation.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
			
		||||
<c-vars title="Playstation 1" />
 | 
			
		||||
<c-svg viewBox="0 0 50 50">
 | 
			
		||||
    <c-slot name="path">
 | 
			
		||||
        M 19.3125 4 C 19.011719 4 18.707031 3.988281 18.40625 4.1875 C 18.105469 4.386719 18 4.699219 18 5 L 18 41.59375 C 18 41.992188 18.289063 42.394531 18.6875 42.59375 L 26.6875 45 L 27 45 C 27.199219 45 27.394531 44.914063 27.59375 44.8125 C 27.894531 44.613281 28 44.300781 28 44 L 28 13.40625 C 28.601563 13.707031 29 14.300781 29 15 L 29 26.09375 C 29 26.394531 29.199219 26.804688 29.5 26.90625 C 29.699219 27.007813 31.199219 27.90625 34 27.90625 C 36.699219 27.90625 40 26.414063 40 19.3125 C 40 13.613281 36.8125 9.292969 31.3125 7.59375 Z M 17 26.40625 L 5.90625 30.40625 L 4.3125 31 C 1.613281 32.101563 0 33.886719 0 35.6875 C 0 39.488281 2.699219 41.6875 7.5 41.6875 C 10.101563 41.6875 13.300781 41.113281 17 39.8125 L 17 36 C 16.101563 36.300781 15.113281 36.699219 14.3125 37 C 12.710938 37.601563 11.5 37.8125 10.5 37.8125 C 9 37.8125 8.300781 37.300781 8 37 C 7.601563 36.699219 7.398438 36.3125 7.5 35.8125 C 7.601563 34.8125 8.800781 33.894531 11 33.09375 C 11.5 32.894531 14.898438 31.699219 17 31 Z M 36.5 28.90625 C 34.101563 29.007813 31.601563 29.394531 29 30.09375 L 29 34.6875 C 30.101563 34.289063 31.585938 33.800781 33.6875 33 C 38.488281 31.300781 40.492188 31.488281 41.09375 31.6875 C 42.292969 31.789063 42.800781 32.5 43 33 C 43.5 34.5 41.613281 35.1875 38.8125 36.1875 C 37.511719 36.6875 31.898438 38.6875 29 39.6875 L 29 44.3125 L 44.5 38.8125 L 45.6875 38.3125 C 47.6875 37.613281 50.199219 36.300781 50 34 C 49.898438 31.800781 47.210938 30.695313 45.3125 30.09375 C 42.511719 29.195313 39.5 28.804688 36.5 28.90625 Z
 | 
			
		||||
    </c-slot>
 | 
			
		||||
</c-svg>
 | 
			
		||||
							
								
								
									
										1
									
								
								games/templates/cotton/icon/ps1.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								games/templates/cotton/icon/ps1.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
<c-icon.playstation />
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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 %}
 | 
			
		||||
 | 
			
		||||
@ -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),
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@ def list_platforms(request: HttpRequest) -> HttpResponse:
 | 
			
		||||
    context: dict[Any, Any] = {}
 | 
			
		||||
    page_number = request.GET.get("page", 1)
 | 
			
		||||
    limit = request.GET.get("limit", 10)
 | 
			
		||||
    platforms = Platform.objects.order_by("-created_at")
 | 
			
		||||
    platforms = Platform.objects.order_by("name")
 | 
			
		||||
    page_obj = None
 | 
			
		||||
    if int(limit) != 0:
 | 
			
		||||
        paginator = Paginator(platforms, limit)
 | 
			
		||||
@ -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(
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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 ""}",
 | 
			
		||||
                    (
 | 
			
		||||
@ -243,3 +241,10 @@ 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")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@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")
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user