Files
labhelper/boxes/views.py
Adrian A. Baumann ca50832b54
All checks were successful
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/labhelper) (push) Successful in 1m44s
Build containers when image tags change / build-if-image-changed (data-loader, loader, initContainers, init-container, git.baumann.gr/adebaumann/labhelper-data-loader) (push) Successful in 6s
Markdown support for description fields added; Tests updated
2026-01-05 11:00:16 +01:00

353 lines
11 KiB
Python

import bleach
import markdown
from django.contrib.auth.decorators import login_required
from django.db.models import Q
from django.http import HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from .forms import (
BoxForm,
BoxTypeForm,
ThingFileForm,
ThingFormSet,
ThingLinkForm,
ThingPictureForm,
)
from .models import Box, BoxType, Facet, Tag, Thing, ThingFile, ThingLink
def _strip_markdown(text, max_length=100):
"""Convert Markdown to plain text and truncate."""
if not text:
return ''
html = markdown.markdown(text)
plain_text = bleach.clean(html, tags=[], strip=True)
plain_text = ' '.join(plain_text.split())
if len(plain_text) > max_length:
return plain_text[:max_length].rsplit(' ', 1)[0] + '...'
return plain_text
@login_required
def index(request):
"""Home page with boxes and tags."""
boxes = Box.objects.select_related('box_type').all().order_by('id')
facets = Facet.objects.all().prefetch_related('tags')
facet_tag_counts = {}
for facet in facets:
for tag in facet.tags.all():
count = tag.things.count()
if count > 0:
if facet not in facet_tag_counts:
facet_tag_counts[facet] = []
facet_tag_counts[facet].append((tag, count))
return render(request, 'boxes/index.html', {
'boxes': boxes,
'facets': facets,
'facet_tag_counts': facet_tag_counts,
})
@login_required
def box_detail(request, box_id):
"""Display contents of a box."""
box = get_object_or_404(Box, pk=box_id)
things = box.things.prefetch_related('tags').all()
return render(request, 'boxes/box_detail.html', {
'box': box,
'things': things,
})
@login_required
def thing_detail(request, thing_id):
"""Display details of a thing."""
thing = get_object_or_404(
Thing.objects.select_related('box', 'box__box_type').prefetch_related('files', 'links', 'tags'),
pk=thing_id
)
boxes = Box.objects.select_related('box_type').all().order_by('id')
facets = Facet.objects.all().prefetch_related('tags')
picture_form = ThingPictureForm(instance=thing)
file_form = ThingFileForm()
link_form = ThingLinkForm()
if request.method == 'POST':
action = request.POST.get('action')
if action == 'move':
new_box_id = request.POST.get('new_box')
if new_box_id:
new_box = get_object_or_404(Box, pk=new_box_id)
thing.box = new_box
thing.save()
return redirect('thing_detail', thing_id=thing.id)
elif action == 'upload_picture':
picture_form = ThingPictureForm(request.POST, request.FILES, instance=thing)
if picture_form.is_valid():
picture_form.save()
return redirect('thing_detail', thing_id=thing.id)
elif action == 'delete_picture':
if thing.picture:
thing.picture.delete()
thing.picture = None
thing.save()
return redirect('thing_detail', thing_id=thing.id)
elif action == 'add_file':
file_form = ThingFileForm(request.POST, request.FILES)
if file_form.is_valid():
thing_file = file_form.save(commit=False)
thing_file.thing = thing
thing_file.save()
return redirect('thing_detail', thing_id=thing.id)
elif action == 'add_link':
link_form = ThingLinkForm(request.POST)
if link_form.is_valid():
thing_link = link_form.save(commit=False)
thing_link.thing = thing
thing_link.save()
return redirect('thing_detail', thing_id=thing.id)
elif action == 'delete_file':
file_id = request.POST.get('file_id')
if file_id:
try:
thing_file = ThingFile.objects.get(pk=file_id, thing=thing)
thing_file.file.delete()
thing_file.delete()
except ThingFile.DoesNotExist:
pass
return redirect('thing_detail', thing_id=thing.id)
elif action == 'delete_link':
link_id = request.POST.get('link_id')
if link_id:
try:
thing_link = ThingLink.objects.get(pk=link_id, thing=thing)
thing_link.delete()
except ThingLink.DoesNotExist:
pass
return redirect('thing_detail', thing_id=thing.id)
elif action == 'add_tag':
tag_id = request.POST.get('tag_id')
if tag_id:
try:
tag = Tag.objects.get(pk=tag_id)
if tag.facet.cardinality == Facet.Cardinality.SINGLE:
existing_tags = list(thing.tags.filter(facet=tag.facet))
for existing_tag in existing_tags:
thing.tags.remove(existing_tag)
thing.tags.add(tag)
except Tag.DoesNotExist:
pass
return redirect('thing_detail', thing_id=thing.id)
elif action == 'remove_tag':
tag_id = request.POST.get('tag_id')
if tag_id:
try:
tag = Tag.objects.get(pk=tag_id)
thing.tags.remove(tag)
except Tag.DoesNotExist:
pass
return redirect('thing_detail', thing_id=thing.id)
return render(request, 'boxes/thing_detail.html', {
'thing': thing,
'boxes': boxes,
'facets': facets,
'picture_form': picture_form,
'file_form': file_form,
'link_form': link_form,
})
@login_required
def search(request):
"""Search page for things."""
return render(request, 'boxes/search.html')
@login_required
def search_api(request):
"""AJAX endpoint for searching things."""
query = request.GET.get('q', '').strip()
if len(query) < 2:
return JsonResponse({'results': []})
# Check for "Facet:Word" format
if ':' in query:
parts = query.split(':',1)
facet_name = parts[0].strip()
tag_name = parts[1].strip()
# Search for things with specific facet and tag
things = Thing.objects.filter(
Q(tags__facet__name__icontains=facet_name) &
Q(tags__name__icontains=tag_name)
).prefetch_related('files', 'links', 'tags').select_related('box').distinct()[:50]
else:
# Normal search
things = Thing.objects.filter(
Q(name__icontains=query) |
Q(description__icontains=query) |
Q(files__title__icontains=query) |
Q(files__file__icontains=query) |
Q(links__title__icontains=query) |
Q(links__url__icontains=query) |
Q(tags__name__icontains=query) |
Q(tags__facet__name__icontains=query)
).prefetch_related('files', 'links', 'tags').select_related('box').distinct()[:50]
results = [
{
'id': thing.id,
'name': thing.name,
'box': thing.box.id,
'description': _strip_markdown(thing.description),
'tags': [
{
'name': tag.name,
'color': tag.facet.color,
}
for tag in thing.tags.all()
],
'files': [
{
'title': f.title,
'filename': f.filename(),
}
for f in thing.files.all()
],
'links': [
{
'title': l.title,
'url': l.url,
}
for l in thing.links.all()
],
}
for thing in things
]
return JsonResponse({'results': results})
@login_required
def add_things(request, box_id):
"""Add multiple things to a box at once."""
box = get_object_or_404(Box, pk=box_id)
success_message = None
if request.method == 'POST':
formset = ThingFormSet(request.POST, request.FILES, queryset=Thing.objects.filter(box=box))
if formset.is_valid():
things = formset.save(commit=False)
created_count = 0
for thing in things:
if thing.name or thing.description or thing.picture:
thing.box = box
thing.save()
created_count += 1
if created_count > 0:
success_message = f'Added {created_count} thing{"s" if created_count > 1 else ""} successfully.'
formset = ThingFormSet(queryset=Thing.objects.filter(box=box))
else:
formset = ThingFormSet(queryset=Thing.objects.filter(box=box))
return render(request, 'boxes/add_things.html', {
'box': box,
'formset': formset,
'success_message': success_message,
})
@login_required
def box_management(request):
"""Main page for managing boxes and box types."""
box_types = BoxType.objects.all().prefetch_related('boxes')
boxes = Box.objects.select_related('box_type').all().prefetch_related('things')
box_type_form = BoxTypeForm()
box_form = BoxForm()
return render(request, 'boxes/box_management.html', {
'box_types': box_types,
'boxes': boxes,
'box_type_form': box_type_form,
'box_form': box_form,
})
@login_required
def add_box_type(request):
"""Add a new box type."""
if request.method == 'POST':
form = BoxTypeForm(request.POST)
if form.is_valid():
form.save()
return redirect('box_management')
@login_required
def edit_box_type(request, type_id):
"""Edit an existing box type."""
box_type = get_object_or_404(BoxType, pk=type_id)
if request.method == 'POST':
form = BoxTypeForm(request.POST, instance=box_type)
if form.is_valid():
form.save()
return redirect('box_management')
@login_required
def delete_box_type(request, type_id):
"""Delete a box type."""
box_type = get_object_or_404(BoxType, pk=type_id)
if request.method == 'POST':
if box_type.boxes.exists():
return redirect('box_management')
box_type.delete()
return redirect('box_management')
@login_required
def add_box(request):
"""Add a new box."""
if request.method == 'POST':
form = BoxForm(request.POST)
if form.is_valid():
form.save()
return redirect('box_management')
@login_required
def edit_box(request, box_id):
"""Edit an existing box."""
box = get_object_or_404(Box, pk=box_id)
if request.method == 'POST':
form = BoxForm(request.POST, instance=box)
if form.is_valid():
form.save()
return redirect('box_management')
@login_required
def delete_box(request, box_id):
"""Delete a box."""
box = get_object_or_404(Box, pk=box_id)
if request.method == 'POST':
if box.things.exists():
return redirect('box_management')
box.delete()
return redirect('box_management')