269 lines
12 KiB
Python
269 lines
12 KiB
Python
from django.shortcuts import render, get_object_or_404, redirect
|
|
from django.http import HttpResponse, HttpResponseBadRequest
|
|
from django.template.loader import render_to_string
|
|
import os
|
|
from .models import Dinosaur, BackgroundImage
|
|
from .forms import DinosaurBulkFormSet
|
|
from .forms import DinosaurForm
|
|
from .forms import BackgroundImageForm
|
|
|
|
|
|
def is_htmx(request):
|
|
return request.headers.get('HX-Request') == 'true'
|
|
|
|
|
|
def card_list(request):
|
|
# Basic filtering/search/sort via GET params so the UI can request
|
|
# updated lists using HTMX or a regular browser GET.
|
|
qs = Dinosaur.objects.all()
|
|
|
|
# Search by name
|
|
q = (request.GET.get('q') or '').strip()
|
|
if q:
|
|
qs = qs.filter(name__icontains=q)
|
|
|
|
# Filter by era and diet
|
|
era = request.GET.get('era')
|
|
if era and era != 'all':
|
|
qs = qs.filter(era=era)
|
|
diet = request.GET.get('diet')
|
|
if diet and diet != 'all':
|
|
qs = qs.filter(diet=diet)
|
|
|
|
# Sorting: allow a small whitelist of fields to avoid unexpected orders
|
|
sort_map = {
|
|
'name': 'name',
|
|
'created': 'created_at',
|
|
'speed': 'speed',
|
|
'weight': 'weight',
|
|
'height': 'height',
|
|
'intelligence': 'intelligence',
|
|
}
|
|
sort_by = request.GET.get('sort')
|
|
order = request.GET.get('order', 'asc')
|
|
if sort_by in sort_map:
|
|
field = sort_map[sort_by]
|
|
if order == 'desc':
|
|
field = '-' + field
|
|
qs = qs.order_by(field)
|
|
|
|
background = BackgroundImage.get_active() if BackgroundImage.objects.exists() else None
|
|
|
|
context = {
|
|
'cards': qs,
|
|
'background': background,
|
|
'eras': Dinosaur.ERA_CHOICES,
|
|
'diets': Dinosaur.DIET_CHOICES,
|
|
'current_q': q,
|
|
'current_era': era or 'all',
|
|
'current_diet': diet or 'all',
|
|
'current_sort': sort_by or '',
|
|
'current_order': order,
|
|
}
|
|
|
|
if is_htmx(request):
|
|
return render(request, 'cards/_cards_list.html', context)
|
|
return render(request, 'cards/list.html', context)
|
|
|
|
|
|
def card_create(request):
|
|
background = BackgroundImage.get_active() if BackgroundImage.objects.exists() else None
|
|
backgrounds = BackgroundImage.objects.all()
|
|
|
|
if request.method == 'POST':
|
|
form = DinosaurForm(request.POST, request.FILES)
|
|
if form.is_valid():
|
|
# Save instance with commit=False so we can set a default name
|
|
# from the uploaded image filename when the name is empty.
|
|
card = form.save(commit=False)
|
|
if not card.name:
|
|
image_file = form.cleaned_data.get('image')
|
|
if image_file and getattr(image_file, 'name', None):
|
|
base = os.path.splitext(image_file.name)[0]
|
|
# replace separators and title-case for a nicer default
|
|
card.name = base.replace('_', ' ').replace('-', ' ').strip().title()
|
|
card.save()
|
|
if is_htmx(request):
|
|
card_html = render_to_string('cards/_card_item.html', {'card': card, 'background': background}, request=request)
|
|
# Also return an out-of-band fragment to replace/clear the form container
|
|
form_html = render_to_string('cards/_form.html', {'form': DinosaurForm(), 'background': background, 'backgrounds': backgrounds}, request=request)
|
|
oob = f'<div id="htmx-form" hx-swap-oob="true">{form_html}</div>'
|
|
return HttpResponse(card_html + oob)
|
|
return redirect('cards:list')
|
|
else:
|
|
if is_htmx(request):
|
|
# Return the form as an out-of-band fragment so it updates the form container
|
|
form_html = render_to_string('cards/_form.html', {'form': form, 'background': background, 'backgrounds': backgrounds}, request=request)
|
|
oob = f'<div id="htmx-form" hx-swap-oob="true">{form_html}</div>'
|
|
return HttpResponse(oob)
|
|
else:
|
|
form = DinosaurForm()
|
|
|
|
return render(request, 'cards/_form.html', {'form': form, 'background': background, 'backgrounds': backgrounds})
|
|
|
|
|
|
def card_edit(request, pk):
|
|
card = get_object_or_404(Dinosaur, pk=pk)
|
|
background = BackgroundImage.get_active() if BackgroundImage.objects.exists() else None
|
|
backgrounds = BackgroundImage.objects.all()
|
|
if request.method == 'POST':
|
|
form = DinosaurForm(request.POST, request.FILES, instance=card)
|
|
if form.is_valid():
|
|
# Save with commit=False to allow setting a default name from an
|
|
# uploaded image if the instance has no name.
|
|
card = form.save(commit=False)
|
|
if not card.name:
|
|
image_file = form.cleaned_data.get('image')
|
|
if image_file and getattr(image_file, 'name', None):
|
|
base = os.path.splitext(image_file.name)[0]
|
|
card.name = base.replace('_', ' ').replace('-', ' ').strip().title()
|
|
card.save()
|
|
if is_htmx(request):
|
|
card_html = render_to_string('cards/_card_item.html', {'card': card, 'background': background}, request=request)
|
|
# Clear the form container via OOB so the edit form disappears after successful save
|
|
oob = '<div id="htmx-form" hx-swap-oob="true"></div>'
|
|
return HttpResponse(card_html + oob)
|
|
return redirect('cards:list')
|
|
else:
|
|
if is_htmx(request):
|
|
form_html = render_to_string('cards/_form.html', {'form': form, 'card': card, 'background': background, 'backgrounds': backgrounds}, request=request)
|
|
oob = f'<div id="htmx-form" hx-swap-oob="true">{form_html}</div>'
|
|
return HttpResponse(oob)
|
|
else:
|
|
form = DinosaurForm(instance=card)
|
|
|
|
return render(request, 'cards/_form.html', {'form': form, 'card': card, 'background': background, 'backgrounds': backgrounds})
|
|
|
|
|
|
def card_delete(request, pk):
|
|
card = get_object_or_404(Dinosaur, pk=pk)
|
|
if request.method == 'POST':
|
|
card.delete()
|
|
if is_htmx(request):
|
|
# Return an empty 200 response so HTMX can replace the
|
|
# targeted element (e.g. via `hx-swap="outerHTML"`) and
|
|
# remove it from the DOM immediately.
|
|
return HttpResponse('')
|
|
return redirect('cards:list')
|
|
return HttpResponseBadRequest('Only POST allowed')
|
|
|
|
|
|
def background_list(request):
|
|
"""Show existing backgrounds and provide a form to upload a new one."""
|
|
backgrounds = BackgroundImage.objects.all()
|
|
form = BackgroundImageForm()
|
|
if is_htmx(request):
|
|
return render(request, 'cards/_backgrounds_list.html', {'backgrounds': backgrounds, 'form': form})
|
|
return render(request, 'cards/backgrounds_list.html', {'backgrounds': backgrounds, 'form': form})
|
|
|
|
|
|
def background_create(request):
|
|
if request.method != 'POST':
|
|
return HttpResponseBadRequest('Only POST allowed')
|
|
|
|
form = BackgroundImageForm(request.POST, request.FILES)
|
|
if form.is_valid():
|
|
bg = form.save()
|
|
# If marked active, unset others
|
|
if bg.is_active:
|
|
BackgroundImage.objects.exclude(pk=bg.pk).update(is_active=False)
|
|
if is_htmx(request):
|
|
# return the updated list fragment and clear the form OOB
|
|
list_html = render_to_string('cards/_backgrounds_list.html', {'backgrounds': BackgroundImage.objects.all(), 'form': BackgroundImageForm()}, request=request)
|
|
oob = f'<div id="htmx-background-form" hx-swap-oob="true">{render_to_string("cards/_background_form.html", {"form": BackgroundImageForm()}, request=request)}</div>'
|
|
return HttpResponse(list_html + oob)
|
|
return redirect('cards:backgrounds')
|
|
else:
|
|
if is_htmx(request):
|
|
form_html = render_to_string('cards/_background_form.html', {'form': form}, request=request)
|
|
oob = f'<div id="htmx-background-form" hx-swap-oob="true">{form_html}</div>'
|
|
return HttpResponse(oob)
|
|
return render(request, 'cards/backgrounds_list.html', {'backgrounds': BackgroundImage.objects.all(), 'form': form})
|
|
|
|
|
|
def background_activate(request, pk):
|
|
if request.method != 'POST':
|
|
return HttpResponseBadRequest('Only POST allowed')
|
|
bg = get_object_or_404(BackgroundImage, pk=pk)
|
|
BackgroundImage.objects.update(is_active=False)
|
|
bg.is_active = True
|
|
bg.save()
|
|
if is_htmx(request):
|
|
list_html = render_to_string('cards/_backgrounds_list.html', {'backgrounds': BackgroundImage.objects.all(), 'form': BackgroundImageForm()}, request=request)
|
|
return HttpResponse(list_html)
|
|
return redirect('cards:backgrounds')
|
|
|
|
|
|
def background_delete(request, pk):
|
|
if request.method != 'POST':
|
|
return HttpResponseBadRequest('Only POST allowed')
|
|
bg = get_object_or_404(BackgroundImage, pk=pk)
|
|
bg.delete()
|
|
if is_htmx(request):
|
|
# See comment in `card_delete`: return empty body so HTMX can
|
|
# remove the corresponding element via `hx-swap="outerHTML"`.
|
|
return HttpResponse('')
|
|
return redirect('cards:backgrounds')
|
|
|
|
|
|
def preview_card(request, pk):
|
|
"""Render a full preview of a card. HTMX loads the modal fragment into #modal-root."""
|
|
card = get_object_or_404(Dinosaur, pk=pk)
|
|
background = BackgroundImage.get_active() if BackgroundImage.objects.exists() else None
|
|
if is_htmx(request):
|
|
return render(request, 'cards/_card_preview.html', {'card': card, 'background': background})
|
|
return render(request, 'cards/preview.html', {'card': card, 'background': background})
|
|
|
|
|
|
def cards_overview(request):
|
|
"""Overview page showing all cards in a table for quick edits."""
|
|
backgrounds = BackgroundImage.objects.all()
|
|
qs = Dinosaur.objects.order_by('name')
|
|
# Use a modelformset so the template renders the management form
|
|
formset = DinosaurBulkFormSet(queryset=qs)
|
|
return render(request, 'cards/bulk_edit.html', {'formset': formset, 'backgrounds': backgrounds})
|
|
|
|
|
|
def cards_bulk_update(request):
|
|
"""Handle POST from the bulk edit formset and save changes."""
|
|
if request.method != 'POST':
|
|
# If someone visits the bulk-update URL with GET, redirect them back
|
|
# to the overview page instead of returning a 400 Bad Request.
|
|
return redirect('cards:overview')
|
|
formset = None
|
|
qs = Dinosaur.objects.order_by('name')
|
|
try:
|
|
# Pass the same queryset so management/initial forms align with the rendered formset
|
|
formset = DinosaurBulkFormSet(request.POST, queryset=qs)
|
|
except Exception:
|
|
return HttpResponseBadRequest('Invalid submission')
|
|
if formset.is_valid():
|
|
formset.save()
|
|
return redirect('cards:overview')
|
|
# If invalid, re-render the overview with errors
|
|
backgrounds = BackgroundImage.objects.all()
|
|
return render(request, 'cards/bulk_edit.html', {'formset': formset, 'backgrounds': backgrounds})
|
|
|
|
|
|
def cards_bulk_create_images(request):
|
|
"""Create multiple Dinosaur cards from uploaded images (one card per image).
|
|
|
|
Expects a multipart POST with one or more files in the `images` field.
|
|
The card `name` is derived from the filename if not provided.
|
|
"""
|
|
if request.method != 'POST':
|
|
return HttpResponseBadRequest('Only POST allowed')
|
|
files = request.FILES.getlist('images')
|
|
if not files:
|
|
return HttpResponseBadRequest('No images uploaded')
|
|
created = []
|
|
for f in files:
|
|
# derive a friendly name from filename
|
|
base = os.path.splitext(getattr(f, 'name', '') or '')[0]
|
|
name = base.replace('_', ' ').replace('-', ' ').strip().title() or 'New Dinosaur'
|
|
d = Dinosaur(name=name, image=f)
|
|
d.save()
|
|
created.append(d)
|
|
# Redirect back to overview
|
|
return redirect('cards:overview')
|