Compare commits
4 Commits
e158bc0623
...
3f037b4c7c
Author | SHA1 | Date |
---|---|---|
Lukáš Kucharczyk | 3f037b4c7c | |
Lukáš Kucharczyk | 8783d1fc8e | |
Lukáš Kucharczyk | 9a1d24dbfd | |
Lukáš Kucharczyk | 4720660cff |
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
## Improved
|
## Improved
|
||||||
* game overview: improve how editions and purchases are displayed
|
* game overview: improve how editions and purchases are displayed
|
||||||
|
* add purchase: only allow choosing purchases of selected edition
|
||||||
|
|
||||||
## 1.5.1 / 2023-11-14 21:10+01:00
|
## 1.5.1 / 2023-11-14 21:10+01:00
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,10 @@ textarea:disabled {
|
||||||
@apply dark:bg-slate-700 dark:text-slate-400;
|
@apply dark:bg-slate-700 dark:text-slate-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.errorlist {
|
||||||
|
@apply mt-4 mb-1 pl-3 py-2 bg-red-600 text-slate-200 w-[300px];
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
form input,
|
form input,
|
||||||
select,
|
select,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.urls import reverse
|
||||||
from games.models import Game, Platform, Purchase, Session, Edition, Device
|
from games.models import Game, Platform, Purchase, Session, Edition, Device
|
||||||
|
|
||||||
custom_date_widget = forms.DateInput(attrs={"type": "date"})
|
custom_date_widget = forms.DateInput(attrs={"type": "date"})
|
||||||
|
@ -50,6 +50,20 @@ class IncludePlatformSelect(forms.Select):
|
||||||
|
|
||||||
|
|
||||||
class PurchaseForm(forms.ModelForm):
|
class PurchaseForm(forms.ModelForm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Automatically update related_purchase <select/>
|
||||||
|
# to only include purchases of the selected edition.
|
||||||
|
related_purchase_by_edition_url = reverse("related_purchase_by_edition")
|
||||||
|
self.fields["edition"].widget.attrs.update(
|
||||||
|
{
|
||||||
|
"hx-get": related_purchase_by_edition_url,
|
||||||
|
"hx-target": "#id_related_purchase",
|
||||||
|
"hx-swap": "outerHTML",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
edition = EditionChoiceField(
|
edition = EditionChoiceField(
|
||||||
queryset=Edition.objects.order_by("sort_name"),
|
queryset=Edition.objects.order_by("sort_name"),
|
||||||
widget=IncludePlatformSelect(attrs={"autoselect": "autoselect"}),
|
widget=IncludePlatformSelect(attrs={"autoselect": "autoselect"}),
|
||||||
|
@ -58,7 +72,8 @@ class PurchaseForm(forms.ModelForm):
|
||||||
related_purchase = forms.ModelChoiceField(
|
related_purchase = forms.ModelChoiceField(
|
||||||
queryset=Purchase.objects.filter(type=Purchase.GAME).order_by(
|
queryset=Purchase.objects.filter(type=Purchase.GAME).order_by(
|
||||||
"edition__sort_name"
|
"edition__sort_name"
|
||||||
)
|
),
|
||||||
|
required=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -82,6 +97,27 @@ class PurchaseForm(forms.ModelForm):
|
||||||
"name",
|
"name",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
purchase_type = cleaned_data.get("type")
|
||||||
|
related_purchase = cleaned_data.get("related_purchase")
|
||||||
|
name = cleaned_data.get("name")
|
||||||
|
|
||||||
|
# Set the type on the instance to use get_type_display()
|
||||||
|
# This is safe because we're not saving the instance.
|
||||||
|
self.instance.type = purchase_type
|
||||||
|
|
||||||
|
if purchase_type != Purchase.GAME:
|
||||||
|
type_display = self.instance.get_type_display()
|
||||||
|
if not related_purchase:
|
||||||
|
self.add_error(
|
||||||
|
"related_purchase",
|
||||||
|
f"{type_display} must have a related purchase.",
|
||||||
|
)
|
||||||
|
if not name:
|
||||||
|
self.add_error("name", f"{type_display} must have a name.")
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
class IncludeNameSelect(forms.Select):
|
class IncludeNameSelect(forms.Select):
|
||||||
def create_option(self, name, value, *args, **kwargs):
|
def create_option(self, name, value, *args, **kwargs):
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.1.5 on 2023-11-15 12:04
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("games", "0029_alter_purchase_related_purchase"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="purchase",
|
||||||
|
name="name",
|
||||||
|
field=models.CharField(blank=True, default="", max_length=255, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,11 +1,10 @@
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import Any
|
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
|
|
||||||
from common.time import format_duration
|
from common.time import format_duration
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import F, Manager, Sum
|
from django.db.models import F, Manager, Sum
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
|
||||||
class Game(models.Model):
|
class Game(models.Model):
|
||||||
|
@ -120,9 +119,7 @@ class Purchase(models.Model):
|
||||||
max_length=2, choices=OWNERSHIP_TYPES, default=DIGITAL
|
max_length=2, choices=OWNERSHIP_TYPES, default=DIGITAL
|
||||||
)
|
)
|
||||||
type = models.CharField(max_length=255, choices=TYPES, default=GAME)
|
type = models.CharField(max_length=255, choices=TYPES, default=GAME)
|
||||||
name = models.CharField(
|
name = models.CharField(max_length=255, default="", null=True, blank=True)
|
||||||
max_length=255, default="Unknown Name", null=True, blank=True
|
|
||||||
)
|
|
||||||
related_purchase = models.ForeignKey(
|
related_purchase = models.ForeignKey(
|
||||||
"Purchase",
|
"Purchase",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
|
@ -149,6 +146,10 @@ class Purchase(models.Model):
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self.type == Purchase.GAME:
|
if self.type == Purchase.GAME:
|
||||||
self.name = ""
|
self.name = ""
|
||||||
|
elif self.type != Purchase.GAME and not self.related_purchase:
|
||||||
|
raise ValidationError(
|
||||||
|
f"{self.get_type_display()} must have a related purchase."
|
||||||
|
)
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1231,6 +1231,19 @@ textarea:disabled) {
|
||||||
color: rgb(148 163 184 / var(--tw-text-opacity));
|
color: rgb(148 163 184 / var(--tw-text-opacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.errorlist {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
width: 300px;
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(226 232 240 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
form input,
|
form input,
|
||||||
select,
|
select,
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<link rel="stylesheet" href="{% static 'base.css' %}" />
|
<link rel="stylesheet" href="{% static 'base.css' %}" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="dark">
|
<body class="dark" hx-indicator="#indicator">
|
||||||
<img id="indicator" src="{% static 'icons/loading.png' %}" class="absolute right-3 top-3 animate-spin htmx-indicator" />
|
<img id="indicator" src="{% static 'icons/loading.png' %}" class="absolute right-3 top-3 animate-spin htmx-indicator" />
|
||||||
<div class="dark:bg-gray-800 min-h-screen">
|
<div class="dark:bg-gray-800 min-h-screen">
|
||||||
<nav class="mb-4 bg-white dark:bg-gray-900 border-gray-200 rounded">
|
<nav class="mb-4 bg-white dark:bg-gray-900 border-gray-200 rounded">
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
id="last-session-start"
|
id="last-session-start"
|
||||||
href="{% url 'start_session_same_as_last' last.id %}"
|
href="{% url 'start_session_same_as_last' last.id %}"
|
||||||
hx-get="{% url 'start_session_same_as_last' last.id %}"
|
hx-get="{% url 'start_session_same_as_last' last.id %}"
|
||||||
hx-indicator="#indicator"
|
|
||||||
hx-swap="afterbegin"
|
hx-swap="afterbegin"
|
||||||
hx-target=".responsive-table tbody"
|
hx-target=".responsive-table tbody"
|
||||||
hx-select=".responsive-table tbody tr:first-child"
|
hx-select=".responsive-table tbody tr:first-child"
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{{ form.related_purchase }}
|
|
@ -44,6 +44,11 @@ urlpatterns = [
|
||||||
views.add_purchase,
|
views.add_purchase,
|
||||||
name="add_purchase_for_edition",
|
name="add_purchase_for_edition",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"related-purchase-by-edition",
|
||||||
|
views.related_purchase_by_edition,
|
||||||
|
name="related_purchase_by_edition",
|
||||||
|
),
|
||||||
path("add-edition/", views.add_edition, name="add_edition"),
|
path("add-edition/", views.add_edition, name="add_edition"),
|
||||||
path(
|
path(
|
||||||
"add-edition-for-game/<int:game_id>",
|
"add-edition-for-game/<int:game_id>",
|
||||||
|
|
|
@ -155,7 +155,9 @@ def view_game(request, game_id=None):
|
||||||
.order_by("year_released")
|
.order_by("year_released")
|
||||||
)
|
)
|
||||||
|
|
||||||
sessions = Session.objects.filter(purchase__edition__game=game)
|
sessions = Session.objects.filter(purchase__edition__game=game).order_by(
|
||||||
|
"timestamp_start"
|
||||||
|
)
|
||||||
session_count = sessions.count()
|
session_count = sessions.count()
|
||||||
|
|
||||||
playrange_start = sessions.first().timestamp_start.strftime("%b %Y")
|
playrange_start = sessions.first().timestamp_start.strftime("%b %Y")
|
||||||
|
@ -212,6 +214,15 @@ def edit_edition(request, edition_id=None):
|
||||||
return render(request, "add.html", context)
|
return render(request, "add.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
def related_purchase_by_edition(request):
|
||||||
|
edition_id = request.GET.get("edition")
|
||||||
|
form = PurchaseForm()
|
||||||
|
form.fields["related_purchase"].queryset = Purchase.objects.filter(
|
||||||
|
edition_id=edition_id, type=Purchase.GAME
|
||||||
|
).order_by("edition__sort_name")
|
||||||
|
return render(request, "partials/related_purchase_field.html", {"form": form})
|
||||||
|
|
||||||
|
|
||||||
@use_custom_redirect
|
@use_custom_redirect
|
||||||
def start_game_session(request, game_id: int):
|
def start_game_session(request, game_id: int):
|
||||||
last_session = (
|
last_session = (
|
||||||
|
|
Loading…
Reference in New Issue