Allow directly updating device in session list
Django CI/CD / test (push) Successful in 30s
Django CI/CD / build-and-push (push) Successful in 1m12s

This commit is contained in:
2026-05-11 12:54:42 +02:00
parent a549050860
commit 4e3b0ddb08
5 changed files with 170 additions and 50 deletions
+18 -1
View File
@@ -5,7 +5,7 @@ from django.shortcuts import get_object_or_404
from django.utils.timezone import now as django_timezone_now from django.utils.timezone import now as django_timezone_now
from ninja import Field, ModelSchema, NinjaAPI, Router, Schema from ninja import Field, ModelSchema, NinjaAPI, Router, Schema
from games.models import Game, PlayEvent from games.models import Game, PlayEvent, Session
api = NinjaAPI() api = NinjaAPI()
playevent_router = Router() playevent_router = Router()
@@ -93,3 +93,20 @@ def delete_playevent(request, playevent_id: int):
api.add_router("/playevent", playevent_router) api.add_router("/playevent", playevent_router)
api.add_router("/games", game_router) api.add_router("/games", game_router)
session_router = Router()
class SessionDeviceUpdate(Schema):
device_id: int
@session_router.patch("/{session_id}/device", response={204: None})
def partial_update_session_device(request, session_id: int, payload: SessionDeviceUpdate):
session = get_object_or_404(Session, id=session_id)
session.device_id = payload.device_id
session.save()
return 204, None
api.add_router("/session", session_router)
+16
View File
@@ -2490,9 +2490,15 @@
.text-gray-900 { .text-gray-900 {
color: var(--color-gray-900); color: var(--color-gray-900);
} }
.text-green-600 {
color: var(--color-green-600);
}
.text-heading { .text-heading {
color: var(--color-heading); color: var(--color-heading);
} }
.text-red-600 {
color: var(--color-red-600);
}
.text-slate-300 { .text-slate-300 {
color: var(--color-slate-300); color: var(--color-slate-300);
} }
@@ -3333,6 +3339,16 @@
color: var(--color-gray-600); color: var(--color-gray-600);
} }
} }
.dark\:text-green-400 {
&:is(.dark *) {
color: var(--color-green-400);
}
}
.dark\:text-red-400 {
&:is(.dark *) {
color: var(--color-red-400);
}
}
.dark\:text-slate-300 { .dark\:text-slate-300 {
&:is(.dark *) { &:is(.dark *) {
color: var(--color-slate-300); color: var(--color-slate-300);
+18 -3
View File
@@ -1,11 +1,26 @@
<tr class="odd:bg-white dark:odd:bg-gray-900 even:bg-gray-50 dark:even: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"> <tr class="odd:bg-white dark:odd:bg-gray-900 even:bg-gray-50 dark:even: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 data.row_id %}id="{{ data.row_id }}"{% endif %}
{% if data.hx_trigger %}hx-trigger="{{ data.hx_trigger }}"{% endif %}
{% if data.hx_get %}hx-get="{{ data.hx_get }}"{% endif %}
{% if data.hx_select %}hx-select="{{ data.hx_select }}"{% endif %}
{% if data.hx_swap %}hx-swap="{{ data.hx_swap }}"{% endif %}
>
{% if slot %} {% if slot %}
{{ slot }} {{ slot }}
{% elif data.row_id %}
{% for td in data.cell_data %}
{% if forloop.first %}
<th scope="row" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">{{ td }}</th>
{% else %}
<c-table-td>
{{ td }}
</c-table-td>
{% endif %}
{% endfor %}
{% else %} {% else %}
{% 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">{{ td }}</th>
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">{{ td }}</th>
{% else %} {% else %}
<c-table-td> <c-table-td>
{{ td }} {{ td }}
@@ -0,0 +1,56 @@
<div class="flex gap-2 items-center"
x-data="{
originalDeviceId: {{ session.device.id|default:'null' }},
originalDeviceName: '{{ session.device.name|default:'Unknown'|escapejs }}',
deviceId: {{ session.device.id|default:'null' }},
deviceName: '{{ session.device.name|default:'Unknown'|escapejs }}',
open: false,
saving: false,
error: '',
success: false,
setDevice(newDeviceId, newDeviceName) {
this.deviceId = newDeviceId;
this.deviceName = newDeviceName;
this.saving = true;
this.error = '';
this.success = false;
fetch(`/api/session/{{ session.id }}/device`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify({ device_id: newDeviceId })
})
.then(() => {
this.success = true;
this.error = '';
document.body.dispatchEvent(new CustomEvent('device-changed'));
})
.catch(() => {
this.error = 'Failed to update device';
this.deviceName = this.originalDeviceName;
this.deviceId = this.originalDeviceId;
})
.finally(() => this.saving = false);
}
}"
>
<div class="inline-flex rounded-md shadow-2xs" role="group" @click.outside="open = false">
<button type="button" @click="open = !open" class="relative px-4 py-2 text-sm font-medium bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white align-middle hover:cursor-pointer">
<span class="flex flex-row gap-4 justify-between items-center">
<span x-text="deviceName"></span>
<c-icon.arrowdown />
</span>
<div class="absolute top-[105%] left-0 w-full whitespace-nowrap z-10 text-sm font-medium bg-gray-800/20 backdrop-blur-lg rounded-md rounded-t-none border border-gray-200 dark:border-gray-700" x-show="open" style="display: none;">
<ul class="[&_li:first-of-type_a]:rounded-none [&_li:last-of-type_a]:rounded-t-none">
{% for device in session_devices %}
<li><a href="#" @click.prevent.stop="setDevice({{ device.id }}, '{{ device.name|escapejs }}'); open = false;" class="block px-4 py-2 dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white rounded-sm no-underline! border-0!" :class="{ 'font-bold': deviceId === {{ device.id }} }">{{ device.name }}</a></li>
{% endfor %}
</ul>
</div>
</button>
</div>
<div x-show="success" class="text-xs text-green-600 dark:text-green-400" style="display: none;">Saved</div>
<div x-show="error" x-text="error" class="text-xs text-red-600 dark:text-red-400" style="display: none;"></div>
</div>
+62 -46
View File
@@ -25,7 +25,7 @@ from common.time import (
) )
from common.utils import truncate from common.utils import truncate
from games.forms import SessionForm from games.forms import SessionForm
from games.models import Game, Session from games.models import Device, Game, Session
@login_required @login_required
@@ -34,6 +34,7 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
page_number = request.GET.get("page", 1) page_number = request.GET.get("page", 1)
limit = request.GET.get("limit", 10) limit = request.GET.get("limit", 10)
sessions = Session.objects.order_by("-timestamp_start", "created_at") sessions = Session.objects.order_by("-timestamp_start", "created_at")
device_list = Device.objects.order_by("name")
search_string = request.GET.get("search_string", search_string) search_string = request.GET.get("search_string", search_string)
if search_string != "": if search_string != "":
sessions = sessions.filter( sessions = sessions.filter(
@@ -123,51 +124,66 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
"Actions", "Actions",
], ],
"rows": [ "rows": [
[ {
NameWithIcon(session_id=session.pk), "row_id": f"session-row-{session.pk}",
f"{local_strftime(session.timestamp_start)}{f'{local_strftime(session.timestamp_end, timeformat)}' if session.timestamp_end else ''}", "hx_trigger": "device-changed from:body",
session.duration_formatted_with_mark, "hx_get": "",
session.device, "hx_select": f"#session-row-{session.pk}",
session.created_at.strftime(dateformat), "hx_swap": "outerHTML",
render_to_string( "cell_data": [
"cotton/button_group.html", NameWithIcon(session_id=session.pk),
{ f"{local_strftime(session.timestamp_start)}{f'{local_strftime(session.timestamp_end, timeformat)}' if session.timestamp_end else ''}",
"buttons": [ session.duration_formatted_with_mark,
{ render_to_string(
"href": reverse( "partials/sessiondevice_selector.html",
"list_sessions_end_session", args=[session.pk] {
), "session": session,
"slot": Icon("end"), "session_device": session.device,
"title": "Finish session now", "session_devices": device_list,
"color": "green", },
"hover": "green", request=request,
} ),
if session.timestamp_end is None session.created_at.strftime(dateformat),
# this only works without leaving an empty render_to_string(
# a element and wrong rounding of button edges "cotton/button_group.html",
# because we check if button.href is not None {
# in the button group component "buttons": [
else {}, {
{ "href": reverse(
"href": reverse("edit_session", args=[session.pk]), "list_sessions_end_session", args=[session.pk]
"slot": Icon("edit"), ),
"title": "Edit", "slot": Icon("end"),
# "color": "gray", "title": "Finish session now",
"hover": "green", "color": "green",
}, "hover": "green",
{ }
"href": reverse( if session.timestamp_end is None
"delete_session", args=[session.pk] # this only works without leaving an empty
), # a element and wrong rounding of button edges
"slot": Icon("delete"), # because we check if button.href is not None
"title": "Delete", # in the button group component
"color": "red", else {},
"hover": "red", {
}, "href": reverse("edit_session", args=[session.pk]),
] "slot": Icon("edit"),
}, "title": "Edit",
), # "color": "gray",
] "hover": "green",
},
{
"href": reverse(
"delete_session", args=[session.pk]
),
"slot": Icon("delete"),
"title": "Delete",
"color": "red",
"hover": "red",
},
]
},
),
],
}
for session in sessions for session in sessions
], ],
}, },