Compare commits

..

7 Commits

Author SHA1 Message Date
Lukáš Kucharczyk b28c42d945
delete platform
Django CI/CD / test (push) Successful in 51s Details
Django CI/CD / build-and-push (push) Has been skipped Details
2024-08-11 20:21:44 +02:00
Lukáš Kucharczyk 3099f02145
list editions 2024-08-11 20:21:27 +02:00
Lukáš Kucharczyk 74b9d0421c
list platforms, fix editing platform 2024-08-11 18:34:50 +02:00
Lukáš Kucharczyk c61adad180
list games 2024-08-11 18:21:11 +02:00
Lukáš Kucharczyk 298ecb4092
formatting 2024-08-11 17:58:35 +02:00
Lukáš Kucharczyk 020e12e20b
remove session recent filter 2024-08-11 17:58:08 +02:00
Lukáš Kucharczyk 6ef56bfed5
list, edit, and delete devices 2024-08-11 17:53:36 +02:00
15 changed files with 489 additions and 55 deletions

View File

@ -1,8 +1,11 @@
repos: repos:
- repo: https://github.com/psf/black # disable due to incomaptible formatting between
rev: 24.8.0 # black and ruff
hooks: # TODO: replace with ruff when it works on NixOS
- id: black # - repo: https://github.com/psf/black
# rev: 24.8.0
# hooks:
# - id: black
- repo: https://github.com/pycqa/isort - repo: https://github.com/pycqa/isort
rev: 5.13.2 rev: 5.13.2
hooks: hooks:

View File

@ -1,5 +1,31 @@
from random import choices
from string import ascii_lowercase
from typing import Any from typing import Any
from django.template.loader import render_to_string
from django.utils.safestring import mark_safe
def Popover(
wrapped_content: str,
popover_content: str = "",
) -> str:
id = randomid()
if popover_content == "":
popover_content = wrapped_content
content = f"<span data-popover-target={id}>{wrapped_content}</span>"
result = mark_safe(
str(content)
+ render_to_string(
"components/popover.html",
{
"id": id,
"children": popover_content,
},
)
)
return result
def safe_division(numerator: int | float, denominator: int | float) -> int | float: def safe_division(numerator: int | float, denominator: int | float) -> int | float:
""" """
@ -31,3 +57,24 @@ def safe_getattr(obj: object, attr_chain: str, default: Any | None = None) -> ob
except AttributeError: except AttributeError:
return default return default
return obj return obj
def truncate(input_string: str, length: int = 30, ellipsis: str = "") -> str:
return (
(f"{input_string[:length-len(ellipsis)]}{ellipsis}")
if len(input_string) > 30
else input_string
)
def truncate_with_popover(input_string: str) -> str:
if (truncated := truncate(input_string)) != input_string:
print(f"Not the same after: {truncated=}")
return Popover(wrapped_content=truncated, popover_content=input_string)
else:
print("Strings are the same!")
return input_string
def randomid(seed: str = "", length: int = 10) -> str:
return seed + "".join(choices(ascii_lowercase, k=length))

89
games/deviceviews.py Normal file
View File

@ -0,0 +1,89 @@
from typing import Any
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string
from django.urls import reverse
from games.forms import DeviceForm
from games.models import Device
from games.views import dateformat
@login_required
def list_devices(request: HttpRequest) -> HttpResponse:
context: dict[Any, Any] = {}
page_number = request.GET.get("page", 1)
limit = request.GET.get("limit", 10)
devices = Device.objects.order_by("-created_at")
page_obj = None
if int(limit) != 0:
paginator = Paginator(devices, limit)
page_obj = paginator.get_page(page_number)
devices = page_obj.object_list
context = {
"title": "Manage devices",
"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": {
"columns": [
"Name",
"Type",
"Created",
"Actions",
],
"rows": [
[
device.name,
device.get_type_display(),
device.created_at.strftime(dateformat),
render_to_string(
"components/button_group_sm.html",
{
"buttons": [
{
"href": reverse("edit_device", args=[device.pk]),
"text": "Edit",
},
{
"href": reverse("delete_device", args=[device.pk]),
"text": "Delete",
"color": "red",
},
]
},
),
]
for device in devices
],
},
}
return render(request, "list_purchases.html", context)
@login_required
def edit_device(request: HttpRequest, device_id: int = 0) -> HttpResponse:
device = get_object_or_404(Device, id=device_id)
form = DeviceForm(request.POST or None, instance=device)
if form.is_valid():
form.save()
return redirect("list_devices")
context: dict[str, Any] = {"form": form, "title": "Edit device"}
return render(request, "add.html", context)
@login_required
def delete_device(request: HttpRequest, device_id: int) -> HttpResponse:
device = get_object_or_404(Device, id=device_id)
device.delete()
return redirect("list_sessions")

109
games/editionviews.py Normal file
View File

@ -0,0 +1,109 @@
from typing import Any
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.http import HttpRequest, HttpResponse
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.utils import truncate_with_popover
from games.forms import EditionForm
from games.models import Edition
from games.views import dateformat
@login_required
def list_editions(request: HttpRequest) -> HttpResponse:
context: dict[Any, Any] = {}
page_number = request.GET.get("page", 1)
limit = request.GET.get("limit", 10)
editions = Edition.objects.order_by("-created_at")
page_obj = None
if int(limit) != 0:
paginator = Paginator(editions, limit)
page_obj = paginator.get_page(page_number)
editions = page_obj.object_list
context = {
"title": "Manage editions",
"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": {
"columns": [
"Game",
"Name",
"Sort Name",
"Platform",
"Year",
"Wikidata",
"Created",
"Actions",
],
"rows": [
[
truncate_with_popover(edition.game.name),
truncate_with_popover(
edition.name
if edition.game.name != edition.name
else "(identical)"
),
truncate_with_popover(
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,
edition.created_at.strftime(dateformat),
render_to_string(
"components/button_group_sm.html",
{
"buttons": [
{
"href": reverse("edit_edition", args=[edition.pk]),
"text": "Edit",
},
{
"href": reverse(
"delete_edition", args=[edition.pk]
),
"text": "Delete",
"color": "red",
},
]
},
),
]
for edition in editions
],
},
}
return render(request, "list_purchases.html", context)
@login_required
def edit_device(request: HttpRequest, edition_id: int = 0) -> HttpResponse:
edition = get_object_or_404(Edition, id=edition_id)
form = EditionForm(request.POST or None, instance=edition)
if form.is_valid():
form.save()
return redirect("list_editions")
context: dict[str, Any] = {"form": form, "title": "Edit edition"}
return render(request, "add.html", context)
@login_required
def delete_edition(request: HttpRequest, edition_id: int) -> HttpResponse:
edition = get_object_or_404(Edition, id=edition_id)
edition.delete()
return redirect("list_editions")

73
games/gameviews.py Normal file
View File

@ -0,0 +1,73 @@
from typing import Any
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from django.template.loader import render_to_string
from django.urls import reverse
from games.models import Game
from games.views import dateformat
@login_required
def list_games(request: HttpRequest) -> HttpResponse:
context: dict[Any, Any] = {}
page_number = request.GET.get("page", 1)
limit = request.GET.get("limit", 10)
games = Game.objects.order_by("-created_at")
page_obj = None
if int(limit) != 0:
paginator = Paginator(games, limit)
page_obj = paginator.get_page(page_number)
games = page_obj.object_list
context = {
"title": "Manage games",
"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": {
"columns": [
"Name",
"Sort Name",
"Year",
"Wikidata",
"Created",
"Actions",
],
"rows": [
[
game.name,
game.sort_name,
game.year_released,
game.wikidata,
game.created_at.strftime(dateformat),
render_to_string(
"components/button_group_sm.html",
{
"buttons": [
{
"href": reverse("edit_game", args=[game.pk]),
"text": "Edit",
},
{
"href": reverse("delete_game", args=[game.pk]),
"text": "Delete",
"color": "red",
},
]
},
),
]
for game in games
],
},
}
return render(request, "list_purchases.html", context)

View File

@ -0,0 +1,25 @@
# Generated by Django 5.1 on 2024-08-11 15:50
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("games", "0034_purchase_date_dropped_purchase_infinite"),
]
operations = [
migrations.AlterField(
model_name="session",
name="device",
field=models.ForeignKey(
blank=True,
default=None,
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to="games.device",
),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.1 on 2024-08-11 16:48
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('games', '0035_alter_session_device'),
]
operations = [
migrations.AlterField(
model_name='edition',
name='platform',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='games.platform'),
),
]

View File

@ -39,7 +39,7 @@ class Edition(models.Model):
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
sort_name = models.CharField(max_length=255, null=True, blank=True, default=None) sort_name = models.CharField(max_length=255, null=True, blank=True, default=None)
platform = models.ForeignKey( platform = models.ForeignKey(
Platform, on_delete=models.CASCADE, null=True, blank=True, default=None Platform, on_delete=models.SET_DEFAULT, null=True, blank=True, default=None
) )
year_released = models.IntegerField(null=True, blank=True, default=None) year_released = models.IntegerField(null=True, blank=True, default=None)
wikidata = models.CharField(max_length=50, null=True, blank=True, default=None) wikidata = models.CharField(max_length=50, null=True, blank=True, default=None)
@ -182,7 +182,7 @@ class Session(models.Model):
duration_calculated = models.DurationField(blank=True, null=True) duration_calculated = models.DurationField(blank=True, null=True)
device = models.ForeignKey( device = models.ForeignKey(
"Device", "Device",
on_delete=models.CASCADE, on_delete=models.SET_DEFAULT,
null=True, null=True,
blank=True, blank=True,
default=None, default=None,

80
games/platformviews.py Normal file
View File

@ -0,0 +1,80 @@
from typing import Any
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string
from django.urls import reverse
from games.models import Platform
from games.views import dateformat
@login_required
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")
page_obj = None
if int(limit) != 0:
paginator = Paginator(platforms, limit)
page_obj = paginator.get_page(page_number)
platforms = page_obj.object_list
context = {
"title": "Manage platforms",
"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": {
"columns": [
"Name",
"Group",
"Created",
"Actions",
],
"rows": [
[
platform.name,
platform.group,
platform.created_at.strftime(dateformat),
render_to_string(
"components/button_group_sm.html",
{
"buttons": [
{
"href": reverse(
"edit_platform", args=[platform.pk]
),
"text": "Edit",
},
{
"href": reverse(
"delete_platform", args=[platform.pk]
),
"text": "Delete",
"color": "red",
},
]
},
),
]
for platform in platforms
],
},
}
return render(request, "list_purchases.html", context)
@login_required
def delete_platform(request: HttpRequest, platform_id: int) -> HttpResponse:
platform = get_object_or_404(Platform, id=platform_id)
platform.delete()
return redirect("list_platforms")

View File

@ -1736,10 +1736,6 @@ input:checked + .toggle-bg {
white-space: nowrap; white-space: nowrap;
} }
.text-ellipsis {
text-overflow: ellipsis;
}
.whitespace-nowrap { .whitespace-nowrap {
white-space: nowrap; white-space: nowrap;
} }
@ -2215,10 +2211,6 @@ input:checked + .toggle-bg {
min-width: 30ch; min-width: 30ch;
} }
.max-w-30char {
max-width: 30ch;
}
.\[a-zA-Z\:\\-\] { .\[a-zA-Z\:\\-\] {
a-z-a--z: \-; a-z-a--z: \-;
} }

View File

@ -1,6 +1,3 @@
<!-- needs data-popover-target on triggering block -->
<!-- id -->
<!-- children -->
<div data-popover <div data-popover
id="{{ id }}" id="{{ id }}"
role="tooltip" role="tooltip"

View File

@ -1,17 +1,8 @@
{% fragment as default_content %} {% fragment as default_content %}
{% load randomid %}
{% for td in data %} {% for td in data %}
{% if forloop.first %} {% if forloop.first %}
<th scope="row" <th scope="row"
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white min-w-30char"> class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">{{ td }}</th>
{% randomid td as th_popover_id %}
<span data-popover-target="{{ th_popover_id }}">{{ td|truncatechars:30 }}</span>
{% if td|length > 30 %}
{% #popover id=th_popover_id %}
{{ td }}
{% /popover %}
{% endif %}
</th>
{% else %} {% else %}
{% #table_td %} {% #table_td %}
{{ td }} {{ td }}

View File

@ -95,19 +95,19 @@
<ul class="py-2 text-sm text-gray-700 dark:text-gray-400" <ul class="py-2 text-sm text-gray-700 dark:text-gray-400"
aria-labelledby="dropdownLargeButton"> aria-labelledby="dropdownLargeButton">
<li> <li>
<a href="{% url 'add_device' %}" <a href="{% url 'list_devices' %}"
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Devices</a> class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Devices</a>
</li> </li>
<li> <li>
<a href="{% url 'add_game' %}" <a href="{% url 'list_games' %}"
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Games</a> class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Games</a>
</li> </li>
<li> <li>
<a href="{% url 'add_edition' %}" <a href="{% url 'list_editions' %}"
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Editions</a> class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Editions</a>
</li> </li>
<li> <li>
<a href="{% url 'add_platform' %}" <a href="{% url 'list_platforms' %}"
class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Platforms</a> class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Platforms</a>
</li> </li>
<li> <li>

View File

@ -1,10 +1,23 @@
from django.urls import path from django.urls import path
from games import purchaseviews, sessionviews, views from games import (
deviceviews,
editionviews,
gameviews,
platformviews,
purchaseviews,
sessionviews,
views,
)
urlpatterns = [ urlpatterns = [
path("", views.index, name="index"), path("", views.index, name="index"),
path("device/add", views.add_device, name="add_device"), path("device/add", views.add_device, name="add_device"),
path(
"device/delete/<int:device_id>", deviceviews.delete_device, name="delete_device"
),
path("device/edit/<int:device_id>", deviceviews.edit_device, name="edit_device"),
path("device/list", deviceviews.list_devices, name="list_devices"),
path("edition/add", views.add_edition, name="add_edition"), path("edition/add", views.add_edition, name="add_edition"),
path( path(
"edition/add/for-game/<int:game_id>", "edition/add/for-game/<int:game_id>",
@ -12,12 +25,25 @@ urlpatterns = [
name="add_edition_for_game", name="add_edition_for_game",
), ),
path("edition/<int:edition_id>/edit", views.edit_edition, name="edit_edition"), path("edition/<int:edition_id>/edit", views.edit_edition, name="edit_edition"),
path("edition/list", editionviews.list_editions, name="list_editions"),
path(
"edition/<int:edition_id>/delete",
editionviews.delete_edition,
name="delete_edition",
),
path("game/add", views.add_game, name="add_game"), path("game/add", views.add_game, name="add_game"),
path("game/<int:game_id>/edit", views.edit_game, name="edit_game"), path("game/<int:game_id>/edit", views.edit_game, name="edit_game"),
path("game/<int:game_id>/view", views.view_game, name="view_game"), path("game/<int:game_id>/view", views.view_game, name="view_game"),
path("game/<int:game_id>/delete", views.delete_game, name="delete_game"), path("game/<int:game_id>/delete", views.delete_game, name="delete_game"),
path("game/list", gameviews.list_games, name="list_games"),
path("platform/add", views.add_platform, name="add_platform"), path("platform/add", views.add_platform, name="add_platform"),
path("platform/<int:platform_id>/edit", views.edit_platform, name="edit_platform"), path("platform/<int:platform_id>/edit", views.edit_platform, name="edit_platform"),
path(
"platform/<int:platform_id>/delete",
platformviews.delete_platform,
name="delete_platform",
),
path("platform/list", platformviews.list_platforms, name="list_platforms"),
path("purchase/add", views.add_purchase, name="add_purchase"), path("purchase/add", views.add_purchase, name="add_purchase"),
path("purchase/<int:purchase_id>/edit", views.edit_purchase, name="edit_purchase"), path("purchase/<int:purchase_id>/edit", views.edit_purchase, name="edit_purchase"),
path( path(
@ -77,12 +103,6 @@ urlpatterns = [
name="list_sessions_end_session", name="list_sessions_end_session",
), ),
path("session/list", sessionviews.list_sessions, name="list_sessions"), path("session/list", sessionviews.list_sessions, name="list_sessions"),
path(
"session/list/recent",
views.list_sessions,
{"filter": "recent"},
name="list_sessions_recent",
),
path( path(
"session/list/by-purchase/<int:purchase_id>", "session/list/by-purchase/<int:purchase_id>",
views.list_sessions, views.list_sessions,

View File

@ -1,4 +1,3 @@
from datetime import datetime
from typing import Any, Callable from typing import Any, Callable
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
@ -225,11 +224,11 @@ def view_game(request: HttpRequest, game_id: int) -> HttpResponse:
@use_custom_redirect @use_custom_redirect
def edit_platform(request: HttpRequest, platform_id: int) -> HttpResponse: def edit_platform(request: HttpRequest, platform_id: int) -> HttpResponse:
context = {} context = {}
purchase = get_object_or_404(Purchase, id=platform_id) platform = get_object_or_404(Platform, id=platform_id)
form = PlatformForm(request.POST or None, instance=purchase) form = PlatformForm(request.POST or None, instance=platform)
if form.is_valid(): if form.is_valid():
form.save() form.save()
return redirect("list_sessions") return redirect("list_platforms")
context["title"] = "Edit Platform" context["title"] = "Edit Platform"
context["form"] = form context["form"] = form
return render(request, "add.html", context) return render(request, "add.html", context)
@ -342,12 +341,6 @@ def list_sessions(
elif filter == "ownership_type": elif filter == "ownership_type":
dataset = all_sessions.filter(purchase__ownership_type=ownership_type) dataset = all_sessions.filter(purchase__ownership_type=ownership_type)
context["ownership_type"] = dict(Purchase.OWNERSHIP_TYPES)[ownership_type] context["ownership_type"] = dict(Purchase.OWNERSHIP_TYPES)[ownership_type]
elif filter == "recent":
current_year = timezone.now().year
first_day_of_year = timezone.make_aware(datetime(current_year, 1, 1))
dataset = all_sessions.filter(timestamp_start__gte=first_day_of_year).order_by(
"-timestamp_start"
)
context["title"] = "This year" context["title"] = "This year"
else: else:
dataset = all_sessions dataset = all_sessions
@ -757,15 +750,11 @@ def stats(request: HttpRequest, year: int = 0) -> HttpResponse:
"all_finished_this_year_count": purchases_finished_this_year.count(), "all_finished_this_year_count": purchases_finished_this_year.count(),
"this_year_finished_this_year": purchases_finished_this_year_released_this_year.select_related( "this_year_finished_this_year": purchases_finished_this_year_released_this_year.select_related(
"edition" "edition"
).order_by( ).order_by("date_finished"),
"date_finished"
),
"this_year_finished_this_year_count": purchases_finished_this_year_released_this_year.count(), "this_year_finished_this_year_count": purchases_finished_this_year_released_this_year.count(),
"purchased_this_year_finished_this_year": purchased_this_year_finished_this_year.select_related( "purchased_this_year_finished_this_year": purchased_this_year_finished_this_year.select_related(
"edition" "edition"
).order_by( ).order_by("date_finished"),
"date_finished"
),
"total_sessions": this_year_sessions.count(), "total_sessions": this_year_sessions.count(),
"unique_days": unique_days["dates"], "unique_days": unique_days["dates"],
"unique_days_percent": int(unique_days["dates"] / 365 * 100), "unique_days_percent": int(unique_days["dates"] / 365 * 100),
@ -949,4 +938,4 @@ def add_device(request: HttpRequest) -> HttpResponse:
@login_required @login_required
def index(request: HttpRequest) -> HttpResponse: def index(request: HttpRequest) -> HttpResponse:
return redirect("list_sessions_recent") return redirect("list_sessions")