.
This commit is contained in:
@@ -47,7 +47,7 @@
|
||||
<div class="stat-label">Intelligence <small class="is-size-7">(score)</small></div>
|
||||
<div class="stat-value" style="margin-left:8px;font-weight:700">
|
||||
<div style="display:flex;align-items:center;gap:8px"><div class="is-size-7" style="font-weight:600"><div class="stars">{% for t in card.intelligence_star_types %}<span class="star {{ t }}">{% if t == 'full' %}★{% else %}☆{% endif %}</span>{% endfor %}</div></div>
|
||||
<div class="is-size-7" style="font-weight:500">(orig: {{ card.intelligence_orig }})</div></div>
|
||||
<div class="is-size-7" style="font-weight:500">(orig: {{ card.intelligence_orig }})</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
<div class="level-item">
|
||||
<a class="button is-light" href="{% url 'cards:list' %}">Back to cards</a>
|
||||
</div>
|
||||
<div class="level-item">
|
||||
<a class="button is-link" href="{% url 'cards:backgrounds_download_all' %}">Download all backgrounds</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{% extends "cards/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<h2 class="title is-4">Cards overview — bulk edit</h2>
|
||||
<section class="section">
|
||||
<h2 class="title is-4">Cards overview — bulk edit{% if card_count %} ({{ card_count }} card{{ card_count|pluralize }}){% endif %}</h2>
|
||||
<p class="subtitle is-6">Edit multiple cards inline and save changes in one go.</p>
|
||||
|
||||
<style>
|
||||
@@ -39,108 +39,108 @@
|
||||
{% csrf_token %}
|
||||
<div class="table-container">
|
||||
<table class="table is-fullwidth is-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Preview</th>
|
||||
<th>Name</th>
|
||||
<th>Background</th>
|
||||
<th>Speed</th>
|
||||
<th>Weight</th>
|
||||
<th>Height</th>
|
||||
<th>Intel</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if formset %}
|
||||
{{ formset.management_form }}
|
||||
{% for form in formset.forms %}
|
||||
{{ form.id }}
|
||||
<tr>
|
||||
<td>
|
||||
{% if form.instance.image %}
|
||||
<img class="bulk-thumb" src="{{ form.instance.image.url }}" alt="{{ form.instance.name }}">
|
||||
{% else %}
|
||||
<div class="bulk-thumb empty"></div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ form.name }}</td>
|
||||
<td>{{ form.background }}</td>
|
||||
<td style="width:90px">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">{{ form.speed }}</div>
|
||||
<div class="control"><a class="button is-static">km/h</a></div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="width:90px">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">{{ form.weight }}</div>
|
||||
<div class="control"><a class="button is-static">kg</a></div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="width:90px">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">{{ form.height }}</div>
|
||||
<div class="control"><a class="button is-static">m</a></div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="width:90px">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">{{ form.intelligence }}</div>
|
||||
<div class="control"><a class="button is-static">score</a></div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="width:90px">ID: {{ form.instance.pk }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for card in cards %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if card.image %}
|
||||
<img class="bulk-thumb" src="{{ card.image.url }}" alt="{{ card.name }}">
|
||||
{% else %}
|
||||
<div class="bulk-thumb empty"></div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><input class="input" name="name-{{ card.pk }}" value="{{ card.name }}"></td>
|
||||
<td>
|
||||
<div class="select"><select name="background-{{ card.pk }}">
|
||||
<option value="">Site default</option>
|
||||
{% for bg in backgrounds %}
|
||||
<option value="{{ bg.pk }}"{% if card.background and card.background.pk == bg.pk %} selected{% endif %}>{{ bg.title|default:'Background' }}</option>
|
||||
{% endfor %}
|
||||
</select></div>
|
||||
</td>
|
||||
<td style="width:90px">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded"><input class="input" name="speed-{{ card.pk }}" value="{{ card.speed }}"></div>
|
||||
<div class="control"><a class="button is-static">km/h</a></div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="width:90px">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded"><input class="input" name="weight-{{ card.pk }}" value="{{ card.weight }}"></div>
|
||||
<div class="control"><a class="button is-static">kg</a></div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="width:90px">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded"><input class="input" name="height-{{ card.pk }}" value="{{ card.height }}"></div>
|
||||
<div class="control"><a class="button is-static">m</a></div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="width:90px">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded"><input class="input" name="intelligence-{{ card.pk }}" value="{{ card.intelligence }}"></div>
|
||||
<div class="control"><a class="button is-static">score</a></div>
|
||||
</div>
|
||||
</td>
|
||||
<td>ID: {{ card.pk }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</tbody>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Preview</th>
|
||||
<th>Name</th>
|
||||
<th>Background</th>
|
||||
<th>Speed</th>
|
||||
<th>Weight</th>
|
||||
<th>Height</th>
|
||||
<th>Intel</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if formset %}
|
||||
{{ formset.management_form }}
|
||||
{% for form in formset.forms %}
|
||||
{{ form.id }}
|
||||
<tr>
|
||||
<td>
|
||||
{% if form.instance.image %}
|
||||
<img class="bulk-thumb" src="{{ form.instance.image.url }}" alt="{{ form.instance.name }}">
|
||||
{% else %}
|
||||
<div class="bulk-thumb empty"></div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ form.name }}</td>
|
||||
<td>{{ form.background }}</td>
|
||||
<td style="width:90px">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">{{ form.speed }}</div>
|
||||
<div class="control"><a class="button is-static">km/h</a></div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="width:90px">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">{{ form.weight }}</div>
|
||||
<div class="control"><a class="button is-static">kg</a></div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="width:90px">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">{{ form.height }}</div>
|
||||
<div class="control"><a class="button is-static">m</a></div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="width:90px">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">{{ form.intelligence }}</div>
|
||||
<div class="control"><a class="button is-static">score</a></div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="width:90px">ID: {{ form.instance.pk }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for card in cards %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if card.image %}
|
||||
<img class="bulk-thumb" src="{{ card.image.url }}" alt="{{ card.name }}">
|
||||
{% else %}
|
||||
<div class="bulk-thumb empty"></div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><input class="input" name="name-{{ card.pk }}" value="{{ card.name }}"></td>
|
||||
<td>
|
||||
<div class="select"><select name="background-{{ card.pk }}">
|
||||
<option value="">Site default</option>
|
||||
{% for bg in backgrounds %}
|
||||
<option value="{{ bg.pk }}"{% if card.background and card.background.pk == bg.pk %} selected{% endif %}>{{ bg.title|default:'Background' }}</option>
|
||||
{% endfor %}
|
||||
</select></div>
|
||||
</td>
|
||||
<td style="width:90px">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded"><input class="input" name="speed-{{ card.pk }}" value="{{ card.speed }}"></div>
|
||||
<div class="control"><a class="button is-static">km/h</a></div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="width:90px">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded"><input class="input" name="weight-{{ card.pk }}" value="{{ card.weight }}"></div>
|
||||
<div class="control"><a class="button is-static">kg</a></div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="width:90px">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded"><input class="input" name="height-{{ card.pk }}" value="{{ card.height }}"></div>
|
||||
<div class="control"><a class="button is-static">m</a></div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="width:90px">
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded"><input class="input" name="intelligence-{{ card.pk }}" value="{{ card.intelligence }}"></div>
|
||||
<div class="control"><a class="button is-static">score</a></div>
|
||||
</div>
|
||||
</td>
|
||||
<td>ID: {{ card.pk }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -151,5 +151,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if card_count is not None %}
|
||||
<div class="level-item">
|
||||
<span class="tag is-light">{{ card_count }} card{% if card_count > 1 %}s{% endif %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="htmx-form"></div>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ urlpatterns = [
|
||||
path('preview/<int:pk>/', views.preview_card, name='preview'),
|
||||
path('backgrounds/', views.background_list, name='backgrounds'),
|
||||
path('backgrounds/create/', views.background_create, name='background_create'),
|
||||
path('backgrounds/download-all/', views.backgrounds_download_all, name='backgrounds_download_all'),
|
||||
path('backgrounds/<int:pk>/activate/', views.background_activate, name='background_activate'),
|
||||
path('backgrounds/<int:pk>/delete/', views.background_delete, name='background_delete'),
|
||||
path('overview/', views.cards_overview, name='overview'),
|
||||
|
||||
+38
-2
@@ -1,6 +1,8 @@
|
||||
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 io
|
||||
import zipfile
|
||||
import os
|
||||
from .models import Dinosaur, BackgroundImage
|
||||
from .forms import DinosaurBulkFormSet
|
||||
@@ -62,6 +64,9 @@ def card_list(request):
|
||||
'current_order': order,
|
||||
}
|
||||
|
||||
# Total (or filtered) card count for UI summary
|
||||
context['card_count'] = qs.count()
|
||||
|
||||
if is_htmx(request):
|
||||
return render(request, 'cards/_cards_list.html', context)
|
||||
return render(request, 'cards/list.html', context)
|
||||
@@ -158,6 +163,35 @@ def background_list(request):
|
||||
return render(request, 'cards/backgrounds_list.html', {'backgrounds': backgrounds, 'form': form})
|
||||
|
||||
|
||||
def backgrounds_download_all(request):
|
||||
"""Provide a ZIP archive download containing all uploaded background images.
|
||||
|
||||
Creates the ZIP in-memory and returns it as an attachment. Skips missing
|
||||
files gracefully.
|
||||
"""
|
||||
backgrounds = BackgroundImage.objects.all()
|
||||
buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(buffer, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
|
||||
for bg in backgrounds:
|
||||
if not getattr(bg, 'image', None):
|
||||
continue
|
||||
try:
|
||||
filepath = bg.image.path
|
||||
except Exception:
|
||||
# If storage backend doesn't expose a local path, skip.
|
||||
continue
|
||||
if not os.path.exists(filepath):
|
||||
continue
|
||||
arcname = os.path.basename(bg.image.name) if getattr(bg.image, 'name', None) else f'background-{bg.pk}'
|
||||
# Prefix with id to avoid duplicate names
|
||||
arcname = f"{bg.pk}_{arcname}"
|
||||
zf.write(filepath, arcname)
|
||||
buffer.seek(0)
|
||||
resp = HttpResponse(buffer.getvalue(), content_type='application/zip')
|
||||
resp['Content-Disposition'] = 'attachment; filename=backgrounds.zip'
|
||||
return resp
|
||||
|
||||
|
||||
def background_create(request):
|
||||
if request.method != 'POST':
|
||||
return HttpResponseBadRequest('Only POST allowed')
|
||||
@@ -222,7 +256,8 @@ def cards_overview(request):
|
||||
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})
|
||||
card_count = qs.count()
|
||||
return render(request, 'cards/bulk_edit.html', {'formset': formset, 'backgrounds': backgrounds, 'card_count': card_count})
|
||||
|
||||
|
||||
def cards_bulk_update(request):
|
||||
@@ -243,7 +278,8 @@ def cards_bulk_update(request):
|
||||
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})
|
||||
card_count = qs.count()
|
||||
return render(request, 'cards/bulk_edit.html', {'formset': formset, 'backgrounds': backgrounds, 'card_count': card_count})
|
||||
|
||||
|
||||
def cards_bulk_create_images(request):
|
||||
|
||||
BIN
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 6.9 MiB |
Reference in New Issue
Block a user