.
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-label">Intelligence <small class="is-size-7">(score)</small></div>
|
||||||
<div class="stat-value" style="margin-left:8px;font-weight:700">
|
<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 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>
|
</div>
|
||||||
<div class="stat-row">
|
<div class="stat-row">
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
<a class="button is-light" href="{% url 'cards:list' %}">Back to cards</a>
|
<a class="button is-light" href="{% url 'cards:list' %}">Back to cards</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="level-item">
|
||||||
|
<a class="button is-link" href="{% url 'cards:backgrounds_download_all' %}">Download all backgrounds</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{% extends "cards/base.html" %}
|
{% extends "cards/base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<h2 class="title is-4">Cards overview — bulk edit</h2>
|
<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>
|
<p class="subtitle is-6">Edit multiple cards inline and save changes in one go.</p>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -39,108 +39,108 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="table is-fullwidth is-striped">
|
<table class="table is-fullwidth is-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Preview</th>
|
<th>Preview</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Background</th>
|
<th>Background</th>
|
||||||
<th>Speed</th>
|
<th>Speed</th>
|
||||||
<th>Weight</th>
|
<th>Weight</th>
|
||||||
<th>Height</th>
|
<th>Height</th>
|
||||||
<th>Intel</th>
|
<th>Intel</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% if formset %}
|
{% if formset %}
|
||||||
{{ formset.management_form }}
|
{{ formset.management_form }}
|
||||||
{% for form in formset.forms %}
|
{% for form in formset.forms %}
|
||||||
{{ form.id }}
|
{{ form.id }}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{% if form.instance.image %}
|
{% if form.instance.image %}
|
||||||
<img class="bulk-thumb" src="{{ form.instance.image.url }}" alt="{{ form.instance.name }}">
|
<img class="bulk-thumb" src="{{ form.instance.image.url }}" alt="{{ form.instance.name }}">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="bulk-thumb empty"></div>
|
<div class="bulk-thumb empty"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ form.name }}</td>
|
<td>{{ form.name }}</td>
|
||||||
<td>{{ form.background }}</td>
|
<td>{{ form.background }}</td>
|
||||||
<td style="width:90px">
|
<td style="width:90px">
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control is-expanded">{{ form.speed }}</div>
|
<div class="control is-expanded">{{ form.speed }}</div>
|
||||||
<div class="control"><a class="button is-static">km/h</a></div>
|
<div class="control"><a class="button is-static">km/h</a></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td style="width:90px">
|
<td style="width:90px">
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control is-expanded">{{ form.weight }}</div>
|
<div class="control is-expanded">{{ form.weight }}</div>
|
||||||
<div class="control"><a class="button is-static">kg</a></div>
|
<div class="control"><a class="button is-static">kg</a></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td style="width:90px">
|
<td style="width:90px">
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control is-expanded">{{ form.height }}</div>
|
<div class="control is-expanded">{{ form.height }}</div>
|
||||||
<div class="control"><a class="button is-static">m</a></div>
|
<div class="control"><a class="button is-static">m</a></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td style="width:90px">
|
<td style="width:90px">
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control is-expanded">{{ form.intelligence }}</div>
|
<div class="control is-expanded">{{ form.intelligence }}</div>
|
||||||
<div class="control"><a class="button is-static">score</a></div>
|
<div class="control"><a class="button is-static">score</a></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td style="width:90px">ID: {{ form.instance.pk }}</td>
|
<td style="width:90px">ID: {{ form.instance.pk }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% for card in cards %}
|
{% for card in cards %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{% if card.image %}
|
{% if card.image %}
|
||||||
<img class="bulk-thumb" src="{{ card.image.url }}" alt="{{ card.name }}">
|
<img class="bulk-thumb" src="{{ card.image.url }}" alt="{{ card.name }}">
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="bulk-thumb empty"></div>
|
<div class="bulk-thumb empty"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td><input class="input" name="name-{{ card.pk }}" value="{{ card.name }}"></td>
|
<td><input class="input" name="name-{{ card.pk }}" value="{{ card.name }}"></td>
|
||||||
<td>
|
<td>
|
||||||
<div class="select"><select name="background-{{ card.pk }}">
|
<div class="select"><select name="background-{{ card.pk }}">
|
||||||
<option value="">Site default</option>
|
<option value="">Site default</option>
|
||||||
{% for bg in backgrounds %}
|
{% for bg in backgrounds %}
|
||||||
<option value="{{ bg.pk }}"{% if card.background and card.background.pk == bg.pk %} selected{% endif %}>{{ bg.title|default:'Background' }}</option>
|
<option value="{{ bg.pk }}"{% if card.background and card.background.pk == bg.pk %} selected{% endif %}>{{ bg.title|default:'Background' }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select></div>
|
</select></div>
|
||||||
</td>
|
</td>
|
||||||
<td style="width:90px">
|
<td style="width:90px">
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control is-expanded"><input class="input" name="speed-{{ card.pk }}" value="{{ card.speed }}"></div>
|
<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 class="control"><a class="button is-static">km/h</a></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td style="width:90px">
|
<td style="width:90px">
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control is-expanded"><input class="input" name="weight-{{ card.pk }}" value="{{ card.weight }}"></div>
|
<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 class="control"><a class="button is-static">kg</a></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td style="width:90px">
|
<td style="width:90px">
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control is-expanded"><input class="input" name="height-{{ card.pk }}" value="{{ card.height }}"></div>
|
<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 class="control"><a class="button is-static">m</a></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td style="width:90px">
|
<td style="width:90px">
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<div class="control is-expanded"><input class="input" name="intelligence-{{ card.pk }}" value="{{ card.intelligence }}"></div>
|
<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 class="control"><a class="button is-static">score</a></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>ID: {{ card.pk }}</td>
|
<td>ID: {{ card.pk }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -151,5 +151,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -17,6 +17,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
<div id="htmx-form"></div>
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ urlpatterns = [
|
|||||||
path('preview/<int:pk>/', views.preview_card, name='preview'),
|
path('preview/<int:pk>/', views.preview_card, name='preview'),
|
||||||
path('backgrounds/', views.background_list, name='backgrounds'),
|
path('backgrounds/', views.background_list, name='backgrounds'),
|
||||||
path('backgrounds/create/', views.background_create, name='background_create'),
|
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>/activate/', views.background_activate, name='background_activate'),
|
||||||
path('backgrounds/<int:pk>/delete/', views.background_delete, name='background_delete'),
|
path('backgrounds/<int:pk>/delete/', views.background_delete, name='background_delete'),
|
||||||
path('overview/', views.cards_overview, name='overview'),
|
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.shortcuts import render, get_object_or_404, redirect
|
||||||
from django.http import HttpResponse, HttpResponseBadRequest
|
from django.http import HttpResponse, HttpResponseBadRequest
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
|
import io
|
||||||
|
import zipfile
|
||||||
import os
|
import os
|
||||||
from .models import Dinosaur, BackgroundImage
|
from .models import Dinosaur, BackgroundImage
|
||||||
from .forms import DinosaurBulkFormSet
|
from .forms import DinosaurBulkFormSet
|
||||||
@@ -62,6 +64,9 @@ def card_list(request):
|
|||||||
'current_order': order,
|
'current_order': order,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Total (or filtered) card count for UI summary
|
||||||
|
context['card_count'] = qs.count()
|
||||||
|
|
||||||
if is_htmx(request):
|
if is_htmx(request):
|
||||||
return render(request, 'cards/_cards_list.html', context)
|
return render(request, 'cards/_cards_list.html', context)
|
||||||
return render(request, '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})
|
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):
|
def background_create(request):
|
||||||
if request.method != 'POST':
|
if request.method != 'POST':
|
||||||
return HttpResponseBadRequest('Only POST allowed')
|
return HttpResponseBadRequest('Only POST allowed')
|
||||||
@@ -222,7 +256,8 @@ def cards_overview(request):
|
|||||||
qs = Dinosaur.objects.order_by('name')
|
qs = Dinosaur.objects.order_by('name')
|
||||||
# Use a modelformset so the template renders the management form
|
# Use a modelformset so the template renders the management form
|
||||||
formset = DinosaurBulkFormSet(queryset=qs)
|
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):
|
def cards_bulk_update(request):
|
||||||
@@ -243,7 +278,8 @@ def cards_bulk_update(request):
|
|||||||
return redirect('cards:overview')
|
return redirect('cards:overview')
|
||||||
# If invalid, re-render the overview with errors
|
# If invalid, re-render the overview with errors
|
||||||
backgrounds = BackgroundImage.objects.all()
|
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):
|
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