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 ninja import Field, ModelSchema, NinjaAPI, Router, Schema
from games.models import Game, PlayEvent
from games.models import Game, PlayEvent, Session
api = NinjaAPI()
playevent_router = Router()
@@ -93,3 +93,20 @@ def delete_playevent(request, playevent_id: int):
api.add_router("/playevent", playevent_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 {
color: var(--color-gray-900);
}
.text-green-600 {
color: var(--color-green-600);
}
.text-heading {
color: var(--color-heading);
}
.text-red-600 {
color: var(--color-red-600);
}
.text-slate-300 {
color: var(--color-slate-300);
}
@@ -3333,6 +3339,16 @@
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 {
&:is(.dark *) {
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 %}
{{ 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 %}
{% for td in data %}
{% if forloop.first %}
<th scope="row"
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">{{ td }}</th>
<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 }}
@@ -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>
+20 -4
View File
@@ -25,7 +25,7 @@ from common.time import (
)
from common.utils import truncate
from games.forms import SessionForm
from games.models import Game, Session
from games.models import Device, Game, Session
@login_required
@@ -34,6 +34,7 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
page_number = request.GET.get("page", 1)
limit = request.GET.get("limit", 10)
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)
if search_string != "":
sessions = sessions.filter(
@@ -123,11 +124,25 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
"Actions",
],
"rows": [
[
{
"row_id": f"session-row-{session.pk}",
"hx_trigger": "device-changed from:body",
"hx_get": "",
"hx_select": f"#session-row-{session.pk}",
"hx_swap": "outerHTML",
"cell_data": [
NameWithIcon(session_id=session.pk),
f"{local_strftime(session.timestamp_start)}{f'{local_strftime(session.timestamp_end, timeformat)}' if session.timestamp_end else ''}",
session.duration_formatted_with_mark,
session.device,
render_to_string(
"partials/sessiondevice_selector.html",
{
"session": session,
"session_device": session.device,
"session_devices": device_list,
},
request=request,
),
session.created_at.strftime(dateformat),
render_to_string(
"cotton/button_group.html",
@@ -167,7 +182,8 @@ def list_sessions(request: HttpRequest, search_string: str = "") -> HttpResponse
]
},
),
]
],
}
for session in sessions
],
},