Allow directly updating device in session list
This commit is contained in:
+18
-1
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user