Compare commits

...

3 Commits

Author SHA1 Message Date
34148466c7
Improve forms
All checks were successful
Django CI/CD / test (push) Successful in 1m17s
Django CI/CD / build-and-push (push) Successful in 2m10s
2025-02-04 20:09:25 +01:00
b22e185d47
Add status, mastered to Game 2025-02-04 20:09:05 +01:00
b2b69339b3
Improve game choices on the session form 2025-02-04 18:34:59 +01:00
11 changed files with 298 additions and 56 deletions

View File

@ -44,9 +44,9 @@
transition: all 0.2s ease-out; transition: all 0.2s ease-out;
} */ } */
form label { /* form label {
@apply dark:text-slate-400; @apply dark:text-slate-400;
} } */
.responsive-table { .responsive-table {
@apply dark:text-white mx-auto table-fixed; @apply dark:text-white mx-auto table-fixed;
@ -90,37 +90,37 @@ form label {
} }
} }
form input, /* form input,
select, select,
textarea { textarea {
@apply dark:border dark:border-slate-900 dark:bg-slate-500 dark:text-slate-100; @apply dark:border dark:border-slate-900 dark:bg-slate-500 dark:text-slate-100;
} } */
form input:disabled, form input:disabled,
select:disabled, select:disabled,
textarea:disabled { textarea:disabled {
@apply dark:bg-slate-700 dark:text-slate-400; @apply dark:bg-slate-800 dark:text-slate-500 cursor-not-allowed;
} }
.errorlist { .errorlist {
@apply mt-4 mb-1 pl-3 py-2 bg-red-600 text-slate-200 w-[300px]; @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,
textarea { textarea {
width: 300px; width: 300px;
} }
} } */
@media screen and (max-width: 768px) { /* @media screen and (max-width: 768px) {
form input, form input,
select, select,
textarea { textarea {
width: 150px; width: 150px;
} }
} } */
#button-container button { #button-container button {
@apply mx-1; @apply mx-1;
@ -169,3 +169,27 @@ textarea:disabled {
} }
} */ } */
label {
@apply dark:text-slate-500;
}
[type="text"], [type="datetime-local"], [type="datetime"], [type="date"], [type="number"], select, textarea {
@apply dark:bg-slate-600 dark:text-slate-300;
}
[type="submit"] {
@apply dark:text-white font-bold dark:bg-blue-600 px-4 py-2;
}
form div label {
@apply dark:text-white;
}
form div {
@apply flex flex-col;
}
div [type="submit"] {
@apply mt-3;
}

View File

@ -11,14 +11,25 @@ custom_datetime_widget = forms.DateTimeInput(
autofocus_input_widget = forms.TextInput(attrs={"autofocus": "autofocus"}) autofocus_input_widget = forms.TextInput(attrs={"autofocus": "autofocus"})
class GameChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj) -> str:
return f"{obj.sort_name} ({obj.platform}, {obj.year_released})"
class SessionForm(forms.ModelForm): class SessionForm(forms.ModelForm):
game = forms.ModelChoiceField( game = GameChoiceField(
queryset=Game.objects.order_by("sort_name"), queryset=Game.objects.order_by("sort_name"),
widget=forms.Select(attrs={"autofocus": "autofocus"}), widget=forms.Select(attrs={"autofocus": "autofocus"}),
) )
device = forms.ModelChoiceField(queryset=Device.objects.order_by("name")) device = forms.ModelChoiceField(queryset=Device.objects.order_by("name"))
mark_as_played = forms.BooleanField(
required=False,
initial={"mark_as_played": True},
label="Set game status to Played if Unplayed",
)
class Meta: class Meta:
widgets = { widgets = {
"timestamp_start": custom_datetime_widget, "timestamp_start": custom_datetime_widget,
@ -33,12 +44,20 @@ class SessionForm(forms.ModelForm):
"emulated", "emulated",
"device", "device",
"note", "note",
"mark_as_played",
] ]
def save(self, commit=True):
class GameChoiceField(forms.ModelMultipleChoiceField): session = super().save(commit=False)
def label_from_instance(self, obj) -> str: if self.cleaned_data.get("mark_as_played"):
return f"{obj.sort_name} ({obj.platform}, {obj.year_released})" game_instance = session.game
if game_instance.status == "u":
game_instance.status = "p"
if commit:
game_instance.save()
if commit:
session.save()
return session
class IncludePlatformSelect(forms.SelectMultiple): class IncludePlatformSelect(forms.SelectMultiple):
@ -143,7 +162,14 @@ class GameForm(forms.ModelForm):
class Meta: class Meta:
model = Game model = Game
fields = ["name", "sort_name", "platform", "year_released", "wikidata"] fields = [
"name",
"sort_name",
"platform",
"year_released",
"status",
"wikidata",
]
widgets = {"name": autofocus_input_widget} widgets = {"name": autofocus_input_widget}

View File

@ -0,0 +1,38 @@
# Generated by Django 5.1.5 on 2025-02-01 19:18
from django.db import migrations, models
def set_finished_status(apps, schema_editor):
Game = apps.get_model("games", "Game")
Game.objects.filter(purchases__date_finished__isnull=False).update(status="f")
class Migration(migrations.Migration):
dependencies = [
("games", "0004_purchase_num_purchases"),
]
operations = [
migrations.AddField(
model_name="game",
name="mastered",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="game",
name="status",
field=models.CharField(
choices=[
("u", "Unplayed"),
("p", "Played"),
("f", "Finished"),
("r", "Retired"),
("a", "Abandoned"),
],
default="u",
max_length=1,
),
),
migrations.RunPython(set_finished_status),
]

View File

@ -23,6 +23,31 @@ class Game(models.Model):
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
class Status(models.TextChoices):
UNPLAYED = (
"u",
"Unplayed",
)
PLAYED = (
"p",
"Played",
)
FINISHED = (
"f",
"Finished",
)
RETIRED = (
"r",
"Retired",
)
ABANDONED = (
"a",
"Abandoned",
)
status = models.CharField(max_length=1, choices=Status, default=Status.UNPLAYED)
mastered = models.BooleanField(default=False)
session_average: float | int | timedelta | None session_average: float | int | timedelta | None
session_count: int | None session_count: int | None

View File

@ -1299,6 +1299,10 @@ input:checked + .toggle-bg {
bottom: 0px; bottom: 0px;
} }
.-left-3 {
left: -0.75rem;
}
.bottom-0 { .bottom-0 {
bottom: 0px; bottom: 0px;
} }
@ -1335,6 +1339,10 @@ input:checked + .toggle-bg {
top: 0px; top: 0px;
} }
.top-2 {
top: 0.5rem;
}
.top-3 { .top-3 {
top: 0.75rem; top: 0.75rem;
} }
@ -1415,6 +1423,10 @@ input:checked + .toggle-bg {
margin-inline-end: 0.5rem; margin-inline-end: 0.5rem;
} }
.ml-3 {
margin-left: 0.75rem;
}
.mr-4 { .mr-4 {
margin-right: 1rem; margin-right: 1rem;
} }
@ -1488,6 +1500,10 @@ input:checked + .toggle-bg {
height: 3rem; height: 3rem;
} }
.h-2 {
height: 0.5rem;
}
.h-2\.5 { .h-2\.5 {
height: 0.625rem; height: 0.625rem;
} }
@ -1524,6 +1540,10 @@ input:checked + .toggle-bg {
width: 2.5rem; width: 2.5rem;
} }
.w-2 {
width: 0.5rem;
}
.w-2\.5 { .w-2\.5 {
width: 0.625rem; width: 0.625rem;
} }
@ -1580,6 +1600,14 @@ input:checked + .toggle-bg {
max-width: 20rem; max-width: 20rem;
} }
.max-w-full {
max-width: 100%;
}
.max-w-xl {
max-width: 36rem;
}
.flex-1 { .flex-1 {
flex: 1 1 0%; flex: 1 1 0%;
} }
@ -1787,6 +1815,10 @@ input:checked + .toggle-bg {
border-radius: 0.125rem; border-radius: 0.125rem;
} }
.rounded-xl {
border-radius: 0.75rem;
}
.rounded-e-lg { .rounded-e-lg {
border-start-end-radius: 0.5rem; border-start-end-radius: 0.5rem;
border-end-end-radius: 0.5rem; border-end-end-radius: 0.5rem;
@ -1880,6 +1912,11 @@ input:checked + .toggle-bg {
background-color: rgb(249 250 251 / var(--tw-bg-opacity)); background-color: rgb(249 250 251 / var(--tw-bg-opacity));
} }
.bg-gray-500 {
--tw-bg-opacity: 1;
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
}
.bg-gray-800 { .bg-gray-800 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity)); background-color: rgb(31 41 55 / var(--tw-bg-opacity));
@ -1889,6 +1926,11 @@ input:checked + .toggle-bg {
background-color: rgb(17 24 39 / 0.5); background-color: rgb(17 24 39 / 0.5);
} }
.bg-green-500 {
--tw-bg-opacity: 1;
background-color: rgb(14 159 110 / var(--tw-bg-opacity));
}
.bg-green-600 { .bg-green-600 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(5 122 85 / var(--tw-bg-opacity)); background-color: rgb(5 122 85 / var(--tw-bg-opacity));
@ -1899,6 +1941,21 @@ input:checked + .toggle-bg {
background-color: rgb(4 108 78 / var(--tw-bg-opacity)); background-color: rgb(4 108 78 / var(--tw-bg-opacity));
} }
.bg-orange-400 {
--tw-bg-opacity: 1;
background-color: rgb(255 138 76 / var(--tw-bg-opacity));
}
.bg-purple-500 {
--tw-bg-opacity: 1;
background-color: rgb(144 97 249 / var(--tw-bg-opacity));
}
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(240 82 82 / var(--tw-bg-opacity));
}
.bg-red-700 { .bg-red-700 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(200 30 30 / var(--tw-bg-opacity)); background-color: rgb(200 30 30 / var(--tw-bg-opacity));
@ -2177,6 +2234,11 @@ input:checked + .toggle-bg {
color: rgb(203 213 225 / var(--tw-text-opacity)); color: rgb(203 213 225 / var(--tw-text-opacity));
} }
.text-slate-400 {
--tw-text-opacity: 1;
color: rgb(148 163 184 / var(--tw-text-opacity));
}
.text-slate-500 { .text-slate-500 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(100 116 139 / var(--tw-text-opacity)); color: rgb(100 116 139 / var(--tw-text-opacity));
@ -2349,10 +2411,9 @@ input:checked + .toggle-bg {
transition: all 0.2s ease-out; transition: all 0.2s ease-out;
} */ } */
form label:is(.dark *) { /* form label {
--tw-text-opacity: 1; @apply dark:text-slate-400;
color: rgb(148 163 184 / var(--tw-text-opacity)); } */
}
.responsive-table { .responsive-table {
margin-left: auto; margin-left: auto;
@ -2391,25 +2452,25 @@ form label:is(.dark *) {
border-left-color: rgb(100 116 139 / var(--tw-border-opacity)); border-left-color: rgb(100 116 139 / var(--tw-border-opacity));
} }
form input:is(.dark *), /* form input,
select:is(.dark *), select,
textarea:is(.dark *) { textarea {
border-width: 1px; @apply dark:border dark:border-slate-900 dark:bg-slate-500 dark:text-slate-100;
--tw-border-opacity: 1; } */
border-color: rgb(15 23 42 / var(--tw-border-opacity));
--tw-bg-opacity: 1; form input:disabled,
background-color: rgb(100 116 139 / var(--tw-bg-opacity)); select:disabled,
--tw-text-opacity: 1; textarea:disabled {
color: rgb(241 245 249 / var(--tw-text-opacity)); cursor: not-allowed;
} }
form input:disabled:is(.dark *), form input:disabled:is(.dark *),
select:disabled:is(.dark *), select:disabled:is(.dark *),
textarea:disabled:is(.dark *) { textarea:disabled:is(.dark *) {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(51 65 85 / var(--tw-bg-opacity)); background-color: rgb(30 41 59 / var(--tw-bg-opacity));
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(148 163 184 / var(--tw-text-opacity)); color: rgb(100 116 139 / var(--tw-text-opacity));
} }
.errorlist { .errorlist {
@ -2425,21 +2486,21 @@ textarea:disabled:is(.dark *) {
color: rgb(226 232 240 / var(--tw-text-opacity)); 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,
textarea { textarea {
width: 300px; width: 300px;
} }
} } */
@media screen and (max-width: 768px) { /* @media screen and (max-width: 768px) {
form input, form input,
select, select,
textarea { textarea {
width: 150px; width: 150px;
} }
} } */
#button-container button { #button-container button {
margin-left: 0.25rem; margin-left: 0.25rem;
@ -2548,6 +2609,47 @@ textarea:disabled:is(.dark *) {
} }
} */ } */
label:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(100 116 139 / var(--tw-text-opacity));
}
[type="text"]:is(.dark *), [type="datetime-local"]:is(.dark *), [type="datetime"]:is(.dark *), [type="date"]:is(.dark *), [type="number"]:is(.dark *), select:is(.dark *), textarea:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(71 85 105 / var(--tw-bg-opacity));
--tw-text-opacity: 1;
color: rgb(203 213 225 / var(--tw-text-opacity));
}
[type="submit"] {
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
font-weight: 700;
}
[type="submit"]:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(28 100 242 / var(--tw-bg-opacity));
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
form div label:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
form div {
display: flex;
flex-direction: column;
}
div [type="submit"] {
margin-top: 0.75rem;
}
.odd\:bg-white:nth-child(odd) { .odd\:bg-white:nth-child(odd) {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity)); background-color: rgb(255 255 255 / var(--tw-bg-opacity));

View File

@ -1,12 +1,7 @@
<c-layouts.add> <c-layouts.add>
<c-slot name="additional_row"> <c-slot name="additional_row">
<tr> <input type="submit"
<td></td> name="submit_and_redirect"
<td> value="Submit & Create Purchase" />
<input type="submit"
name="submit_and_redirect"
value="Submit & Create Purchase" />
</td>
</tr>
</c-slot> </c-slot>
</c-layouts.add> </c-layouts.add>

View File

@ -0,0 +1,16 @@
<div class="relative ml-3">
<span class="rounded-xl w-2 h-2 absolute -left-3 top-2
{% if status == "u" %}
bg-gray-500
{% elif status == "p" %}
bg-orange-400
{% elif status == "f" %}
bg-green-500
{% elif status == "a" %}
bg-red-500
{% elif status == "r" %}
bg-purple-500
{% endif %}
">&nbsp;</span>
{{ slot }}
</div>

View File

@ -3,19 +3,18 @@
{% if form_content %} {% if form_content %}
{{ form_content }} {{ form_content }}
{% else %} {% else %}
<form method="post" enctype="multipart/form-data"> <div class="max-width-container">
<table class="mx-auto"> <div class="form-container max-w-xl mx-auto">
<form method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{{ form.as_table }} {{ form.as_div }}
<tr> <div><input type="submit" value="Submit" /></div>
<td></td> <div class="submit-button-container">
<td> {{ additional_row }}
<input type="submit" value="Submit" /> </div>
</td> </form>
</tr> </div>
{{ additional_row }} </div>
</table>
</form>
{% endif %} {% endif %}
<c-slot name="scripts"> <c-slot name="scripts">
{% if script_name %} {% if script_name %}

View File

@ -52,6 +52,18 @@
{{ playrange }} {{ playrange }}
</c-popover> </c-popover>
</div> </div>
<div class="mb-6 text-slate-400">
<div class="flex gap-2 items-center">
<span class="uppercase font-bold text-slate-300">Status</span>
<c-gamestatus :status="game.status">
{{ game.get_status_display }}
</c-gamestatus>
</div>
<div class="flex gap-2 items-center">
<span class="uppercase font-bold text-slate-300">Platform</span>
<span>{{ game.platform }}</span>
</div>
</div>
<div class="inline-flex rounded-md shadow-sm mb-3" role="group"> <div class="inline-flex rounded-md shadow-sm mb-3" role="group">
<a href="{% url 'edit_game' game.id %}"> <a href="{% url 'edit_game' game.id %}">
<button type="button" <button type="button"

View File

@ -61,6 +61,7 @@ def list_games(request: HttpRequest) -> HttpResponse:
"Name", "Name",
"Sort Name", "Sort Name",
"Year", "Year",
"Status",
"Wikidata", "Wikidata",
"Created", "Created",
"Actions", "Actions",
@ -74,6 +75,10 @@ def list_games(request: HttpRequest) -> HttpResponse:
else "(identical)" else "(identical)"
), ),
game.year_released, game.year_released,
render_to_string(
"cotton/gamestatus.html",
{"status": game.status, "slot": game.get_status_display()},
),
game.wikidata, game.wikidata,
local_strftime(game.created_at, dateformat), local_strftime(game.created_at, dateformat),
render_to_string( render_to_string(

View File

@ -217,7 +217,7 @@ def add_session(request: HttpRequest, game_id: int = 0) -> HttpResponse:
context["title"] = "Add New Session" context["title"] = "Add New Session"
context["script_name"] = "add_session.js" context["script_name"] = "add_session.js"
context["form"] = form context["form"] = form
return render(request, "add_session.html", context) return render(request, "add.html", context)
@login_required @login_required