Box sorting works

This commit is contained in:
2026-01-20 00:18:05 +01:00
parent a1bc7967c5
commit 860e80a552
6 changed files with 182 additions and 6 deletions

View File

@@ -18,4 +18,4 @@ data:
LOGIN_REDIRECT_URL: "index" LOGIN_REDIRECT_URL: "index"
LOGOUT_REDIRECT_URL: "login" LOGOUT_REDIRECT_URL: "login"
GUNICORN_OPTS: "--access-logfile -" GUNICORN_OPTS: "--access-logfile -"
IMAGE_TAG: "0.064" IMAGE_TAG: "0.066"

View File

@@ -27,7 +27,7 @@ spec:
mountPath: /data mountPath: /data
containers: containers:
- name: web - name: web
image: git.baumann.gr/adebaumann/labhelper:0.065 image: git.baumann.gr/adebaumann/labhelper:0.066
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
- containerPort: 8000 - containerPort: 8000

View File

@@ -1,8 +1,12 @@
from adminsortable2.admin import SortableAdminMixin import json
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
from django.contrib.admin import SimpleListFilter from django.contrib.admin import SimpleListFilter
from django.http import JsonResponse
from django.urls import path
from django.utils.html import format_html from django.utils.html import format_html
from django.views.decorators.http import require_POST
from .models import Box, BoxType, Facet, Tag, Thing, ThingFile, ThingLink from .models import Box, BoxType, Facet, Tag, Thing, ThingFile, ThingLink
@@ -48,12 +52,37 @@ class BoxTypeAdmin(admin.ModelAdmin):
@admin.register(Box) @admin.register(Box)
class BoxAdmin(SortableAdminMixin, admin.ModelAdmin): class BoxAdmin(admin.ModelAdmin):
"""Admin configuration for Box model.""" """Admin configuration for Box model."""
list_display = ('id', 'box_type') ordering = ['sort_order']
list_display = ('id', 'box_type', 'sort_order')
list_filter = ('box_type',) list_filter = ('box_type',)
search_fields = ('id',) search_fields = ('id',)
change_list_template = 'admin/boxes/box/change_list.html'
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path('reorder/', self.admin_site.admin_view(self.reorder_view), name='boxes_box_reorder'),
]
return custom_urls + urls
def reorder_view(self, request):
"""Handle AJAX reorder requests."""
if request.method != 'POST':
return JsonResponse({'error': 'POST required'}, status=405)
try:
data = json.loads(request.body)
order = data.get('order', [])
for index, pk in enumerate(order):
Box.objects.filter(pk=pk).update(sort_order=index)
return JsonResponse({'status': 'ok'})
except (json.JSONDecodeError, KeyError) as e:
return JsonResponse({'error': str(e)}, status=400)
class ThingFileInline(admin.TabularInline): class ThingFileInline(admin.TabularInline):

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.2.9 on 2026-01-19 23:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('boxes', '0010_remove_thingtype'),
]
operations = [
migrations.AlterModelOptions(
name='box',
options={'ordering': ['sort_order'], 'verbose_name_plural': 'boxes'},
),
migrations.AddField(
model_name='box',
name='sort_order',
field=models.PositiveIntegerField(db_index=True, default=0, help_text='Order in which boxes are displayed'),
),
]

View File

@@ -0,0 +1,126 @@
{% extends "admin/change_list.html" %}
{% block extrahead %}
{{ block.super }}
<style>
#result_list tbody tr {
cursor: move;
}
#result_list tbody tr.dragging {
opacity: 0.5;
background: #ffffd0;
}
#result_list tbody tr.drag-over {
border-top: 2px solid #417690;
}
</style>
{% endblock %}
{% block result_list %}
{{ block.super }}
<script>
document.addEventListener('DOMContentLoaded', function() {
const tbody = document.querySelector('#result_list tbody');
if (!tbody) return;
let draggedRow = null;
tbody.querySelectorAll('tr').forEach(row => {
row.draggable = true;
row.addEventListener('dragstart', function(e) {
draggedRow = this;
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
});
row.addEventListener('dragend', function() {
this.classList.remove('dragging');
tbody.querySelectorAll('tr').forEach(r => r.classList.remove('drag-over'));
draggedRow = null;
});
row.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (this !== draggedRow) {
this.classList.add('drag-over');
}
});
row.addEventListener('dragleave', function() {
this.classList.remove('drag-over');
});
row.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
if (draggedRow && this !== draggedRow) {
const allRows = Array.from(tbody.querySelectorAll('tr'));
const draggedIndex = allRows.indexOf(draggedRow);
const targetIndex = allRows.indexOf(this);
if (draggedIndex < targetIndex) {
this.parentNode.insertBefore(draggedRow, this.nextSibling);
} else {
this.parentNode.insertBefore(draggedRow, this);
}
saveOrder();
}
});
});
function saveOrder() {
const rows = tbody.querySelectorAll('tr');
const order = [];
rows.forEach(row => {
// Use the action checkbox which contains the PK
const checkbox = row.querySelector('input[name="_selected_action"]');
if (checkbox) {
order.push(checkbox.value);
}
});
fetch('{% url "admin:boxes_box_reorder" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value || getCookie('csrftoken')
},
body: JSON.stringify({ order: order })
})
.then(response => response.json())
.then(data => {
if (data.status === 'ok') {
window.location.reload();
} else {
console.error('Reorder failed:', data.error);
alert('Failed to save order');
}
})
.catch(error => {
console.error('Reorder error:', error);
alert('Failed to save order');
});
}
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
});
</script>
{% endblock %}

View File

@@ -39,7 +39,6 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'adminsortable2',
'mptt', 'mptt',
'django_mptt_admin', 'django_mptt_admin',
'sorl.thumbnail', 'sorl.thumbnail',