This commit is contained in:
Ross
2025-12-06 19:54:00 +00:00
parent 679af14739
commit 4a2afdc0c7
8 changed files with 317 additions and 32 deletions
+80 -10
View File
@@ -1,30 +1,100 @@
{% comment %} Preview fragment for pasted bulk import data. {% endcomment %} {% comment %} Preview fragment for pasted bulk import data. {% endcomment %}
<div class="box"> <div class="box">
<h4 class="title is-6">Parsed suggestions</h4> <h4 class="title is-6">Parsed suggestions</h4>
{% if suggestions %} {% if preview_items %}
<form hx-post="{% url 'cards:import_apply_bulk' %}" hx-target="#import-preview" hx-swap="innerHTML"> <form hx-post="{% url 'cards:import_apply_bulk' %}" hx-target="#import-preview" hx-swap="innerHTML">
{% csrf_token %} {% csrf_token %}
<textarea name="data" style="display:none">{{ suggestions_json }}</textarea> <textarea name="data" style="display:none">{{ suggestions_json }}</textarea>
<table class="table is-fullwidth is-striped"> <table class="table is-fullwidth is-striped">
<thead> <thead>
<tr><th>Match</th><th>Name / PK</th><th>Speed</th><th>Weight</th><th>Height</th><th>Intelligence</th><th>Facts</th></tr> <tr>
<th></th>
<th>Match</th>
<th>Name / PK</th>
<th>Field</th>
<th>Current</th>
<th>Proposed</th>
<th>Apply?</th>
</tr>
</thead> </thead>
<tbody> <tbody>
{% for s in suggestions %} {% for item in preview_items %}
{% with s=item.suggestion i=item.index %}
{% if item.diffs %}
{% for field in item.diffs %}
<tr> <tr>
<td>{% if s.pk %}pk:{{ s.pk }}{% elif s.name %}name{% else %}{% endif %}</td> <td>{% if forloop.first %}#{{ forloop.parentloop.counter }}{% else %}&nbsp;{% endif %}</td>
<td>{% if item.matched %}pk:{{ item.matched.pk }}{% elif s.name %}name{% else %}&mdash;{% endif %}</td>
<td>{{ s.name|default:"(no name)" }}{% if s.pk %} (pk: {{ s.pk }}){% endif %}</td> <td>{{ s.name|default:"(no name)" }}{% if s.pk %} (pk: {{ s.pk }}){% endif %}</td>
<td>{{ s.speed_kmh }}</td> <td>{{ field }}</td>
<td>{{ s.weight_kg }}</td> <td>
<td>{{ s.height_m }}</td> {% if item.existing %}
<td>{{ s.intelligence_score }}</td> {% if field == 'facts' %}
<td>{% if s.facts %}{% if s.facts|length > 0 %}{{ s.facts|join:"; " }}{% else %}{{ s.facts }}{% endif %}{% endif %}</td> {% if item.existing.facts %}{{ item.existing.facts|join:"; " }}{% endif %}
{% elif field == 'name' %}
{{ item.existing.name|default:"—" }}
{% elif field == 'era' %}
{{ item.existing.era|default:"—" }}
{% elif field == 'diet' %}
{{ item.existing.diet|default:"—" }}
{% elif field == 'speed_kmh' %}
{{ item.existing.speed_kmh|default:"—" }}
{% elif field == 'weight_kg' %}
{{ item.existing.weight_kg|default:"—" }}
{% elif field == 'height_m' %}
{{ item.existing.height_m|default:"—" }}
{% elif field == 'intelligence_score' %}
{{ item.existing.intelligence_score|default:"—" }}
{% else %}
{{ item.existing|default:"—" }}
{% endif %}
{% else %}
(new)
{% endif %}
</td>
<td>
{% if field == 'facts' %}
{% if s.facts %}{% if s.facts|length > 0 %}{{ s.facts|join:"; " }}{% else %}{{ s.facts }}{% endif %}{% endif %}
{% elif field == 'name' %}
{{ item.proposed.name|default:"" }}
{% elif field == 'era' %}
{{ item.proposed.era|default:"" }}
{% elif field == 'diet' %}
{{ item.proposed.diet|default:"" }}
{% elif field == 'speed_kmh' %}
{{ item.proposed.speed_kmh|default:"" }}
{% elif field == 'weight_kg' %}
{{ item.proposed.weight_kg|default:"" }}
{% elif field == 'height_m' %}
{{ item.proposed.height_m|default:"" }}
{% elif field == 'intelligence_score' %}
{{ item.proposed.intelligence_score|default:"" }}
{% else %}
{{ item.proposed|default:"" }}
{% endif %}
</td>
<td>
<input type="checkbox" name="apply-{{ i }}-{{ field }}" checked>
{% if not item.matched and forloop.first %}
<br><label style="font-size:0.85em"><input type="checkbox" name="apply-{{ i }}-create" checked> Create record</label>
{% endif %}
</td>
</tr> </tr>
{% endfor %} {% endfor %}
{% else %}
<tr>
<td>#{{ forloop.counter }}</td>
<td>{% if item.matched %}pk:{{ item.matched.pk }}{% else %}&mdash;{% endif %}</td>
<td>{{ s.name|default:"(no name)" }}{% if s.pk %} (pk: {{ s.pk }}){% endif %}</td>
<td colspan="4"><em>No changes detected</em></td>
</tr>
{% endif %}
{% endwith %}
{% endfor %}
</tbody> </tbody>
</table> </table>
<div style="margin-top:0.5rem"> <div style="margin-top:0.5rem">
<button class="button is-primary" type="submit">Apply all suggestions</button> <button class="button is-primary" type="submit">Apply selected changes</button>
<button class="button" type="button" onclick="document.getElementById('import-preview').innerHTML=''">Cancel</button> <button class="button" type="button" onclick="document.getElementById('import-preview').innerHTML=''">Cancel</button>
</div> </div>
</form> </form>
@@ -43,6 +43,16 @@
</div> </div>
</div> </div>
<div style="position:absolute; top:12px; right:12px; display:flex; gap:8px;"> <div style="position:absolute; top:12px; right:12px; display:flex; gap:8px;">
{% if prev_pk %}
<a class="button is-medium is-info" hx-get="{% url 'cards:preview' prev_pk %}?design={{ design }}" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Previous">← Prev</a>
{% else %}
<button class="button is-medium is-static" disabled aria-hidden="true">← Prev</button>
{% endif %}
{% if next_pk %}
<a class="button is-medium is-info" hx-get="{% url 'cards:preview' next_pk %}?design={{ design }}" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Next">Next →</a>
{% else %}
<button class="button is-medium is-static" disabled aria-hidden="true">Next →</button>
{% endif %}
<a class="button is-medium {% if design == 1 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=1" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 1">1</a> <a class="button is-medium {% if design == 1 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=1" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 1">1</a>
<a class="button is-medium {% if design == 2 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=2" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 2">2</a> <a class="button is-medium {% if design == 2 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=2" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 2">2</a>
<a class="button is-medium {% if design == 3 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=3" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 3">3</a> <a class="button is-medium {% if design == 3 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=3" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 3">3</a>
@@ -42,6 +42,16 @@
</div> </div>
</div> </div>
<div style="position:absolute; top:12px; right:12px; display:flex; gap:8px;"> <div style="position:absolute; top:12px; right:12px; display:flex; gap:8px;">
{% if prev_pk %}
<a class="button is-medium is-info" hx-get="{% url 'cards:preview' prev_pk %}?design={{ design }}" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Previous">← Prev</a>
{% else %}
<button class="button is-medium is-static" disabled aria-hidden="true">← Prev</button>
{% endif %}
{% if next_pk %}
<a class="button is-medium is-info" hx-get="{% url 'cards:preview' next_pk %}?design={{ design }}" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Next">Next →</a>
{% else %}
<button class="button is-medium is-static" disabled aria-hidden="true">Next →</button>
{% endif %}
<a class="button is-medium {% if design == 1 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=1" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 1">1</a> <a class="button is-medium {% if design == 1 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=1" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 1">1</a>
<a class="button is-medium {% if design == 2 %}is-primary is-link{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=2" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 2">2</a> <a class="button is-medium {% if design == 2 %}is-primary is-link{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=2" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 2">2</a>
<a class="button is-medium {% if design == 3 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=3" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 3">3</a> <a class="button is-medium {% if design == 3 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=3" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 3">3</a>
@@ -48,6 +48,16 @@
</div> </div>
</div> </div>
<div style="position:absolute; top:12px; right:12px; display:flex; gap:8px;"> <div style="position:absolute; top:12px; right:12px; display:flex; gap:8px;">
{% if prev_pk %}
<a class="button is-medium is-info" hx-get="{% url 'cards:preview' prev_pk %}?design={{ design }}" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Previous">← Prev</a>
{% else %}
<button class="button is-medium is-static" disabled aria-hidden="true">← Prev</button>
{% endif %}
{% if next_pk %}
<a class="button is-medium is-info" hx-get="{% url 'cards:preview' next_pk %}?design={{ design }}" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Next">Next →</a>
{% else %}
<button class="button is-medium is-static" disabled aria-hidden="true">Next →</button>
{% endif %}
<a class="button is-medium {% if design == 1 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=1" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 1">1</a> <a class="button is-medium {% if design == 1 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=1" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 1">1</a>
<a class="button is-medium {% if design == 2 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=2" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 2">2</a> <a class="button is-medium {% if design == 2 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=2" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 2">2</a>
<a class="button is-medium {% if design == 3 %}is-primary is-link{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=3" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 3">3</a> <a class="button is-medium {% if design == 3 %}is-primary is-link{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=3" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 3">3</a>
@@ -51,6 +51,16 @@
</div> </div>
</div> </div>
<div style="position:absolute; top:12px; right:12px; display:flex; gap:8px;"> <div style="position:absolute; top:12px; right:12px; display:flex; gap:8px;">
{% if prev_pk %}
<a class="button is-medium is-info" hx-get="{% url 'cards:preview' prev_pk %}?design={{ design }}" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Previous">← Prev</a>
{% else %}
<button class="button is-medium is-static" disabled aria-hidden="true">← Prev</button>
{% endif %}
{% if next_pk %}
<a class="button is-medium is-info" hx-get="{% url 'cards:preview' next_pk %}?design={{ design }}" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Next">Next →</a>
{% else %}
<button class="button is-medium is-static" disabled aria-hidden="true">Next →</button>
{% endif %}
<a class="button is-medium {% if design == 1 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=1" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 1">1</a> <a class="button is-medium {% if design == 1 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=1" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 1">1</a>
<a class="button is-medium {% if design == 2 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=2" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 2">2</a> <a class="button is-medium {% if design == 2 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=2" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 2">2</a>
<a class="button is-medium {% if design == 3 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=3" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 3">3</a> <a class="button is-medium {% if design == 3 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=3" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 3">3</a>
@@ -51,6 +51,16 @@
</div> </div>
</div> </div>
<div style="position:absolute; top:12px; right:12px; display:flex; gap:8px;"> <div style="position:absolute; top:12px; right:12px; display:flex; gap:8px;">
{% if prev_pk %}
<a class="button is-medium is-info" hx-get="{% url 'cards:preview' prev_pk %}?design={{ design }}" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Previous">← Prev</a>
{% else %}
<button class="button is-medium is-static" disabled aria-hidden="true">← Prev</button>
{% endif %}
{% if next_pk %}
<a class="button is-medium is-info" hx-get="{% url 'cards:preview' next_pk %}?design={{ design }}" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Next">Next →</a>
{% else %}
<button class="button is-medium is-static" disabled aria-hidden="true">Next →</button>
{% endif %}
<a class="button is-medium {% if design == 1 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=1" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 1">1</a> <a class="button is-medium {% if design == 1 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=1" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 1">1</a>
<a class="button is-medium {% if design == 2 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=2" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 2">2</a> <a class="button is-medium {% if design == 2 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=2" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 2">2</a>
<a class="button is-medium {% if design == 3 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=3" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 3">3</a> <a class="button is-medium {% if design == 3 %}is-primary{% else %}is-light{% endif %}" hx-get="{% url 'cards:preview' card.pk %}?design=3" hx-target="#modal-root" hx-swap="innerHTML" aria-label="Design 3">3</a>
+179 -14
View File
@@ -4,6 +4,9 @@ from django.template.loader import render_to_string
import io import io
import zipfile import zipfile
import os import os
import json
import csv
from io import StringIO
from .models import Dinosaur, BackgroundImage from .models import Dinosaur, BackgroundImage
from .forms import DinosaurBulkFormSet from .forms import DinosaurBulkFormSet
from .forms import DinosaurForm from .forms import DinosaurForm
@@ -288,7 +291,21 @@ def preview_card(request, pk):
design = 1 design = 1
template_name = f'cards/_card_preview_design{design}.html' template_name = f'cards/_card_preview_design{design}.html'
template_name = f'cards/_card_preview_design{design}.html' template_name = f'cards/_card_preview_design{design}.html'
context = {'card': card, 'background': background, 'design': design, 'fragment_template': template_name} # Compute previous/next PKs using a stable ordering (by name)
qs = list(Dinosaur.objects.order_by('name').values_list('pk', flat=True))
prev_pk = None
next_pk = None
try:
idx = qs.index(card.pk)
if idx > 0:
prev_pk = qs[idx - 1]
if idx < len(qs) - 1:
next_pk = qs[idx + 1]
except ValueError:
prev_pk = None
next_pk = None
context = {'card': card, 'background': background, 'design': design, 'fragment_template': template_name, 'prev_pk': prev_pk, 'next_pk': next_pk}
if is_htmx(request): if is_htmx(request):
return render(request, template_name, context) return render(request, template_name, context)
# For non-HTMX full page preview, render the generic preview which can accept design too # For non-HTMX full page preview, render the generic preview which can accept design too
@@ -438,6 +455,8 @@ def import_from_paste(request):
entry = {k.lower(): v for k, v in item.items()} if isinstance(item, dict) else {'raw': str(item)} entry = {k.lower(): v for k, v in item.items()} if isinstance(item, dict) else {'raw': str(item)}
s = { s = {
'pk': entry.get('pk') or entry.get('id'), 'pk': entry.get('pk') or entry.get('id'),
'era': entry.get('era'),
'diet': entry.get('diet'),
'name': entry.get('name'), 'name': entry.get('name'),
'speed_kmh': entry.get('speed_kmh') or entry.get('speed') or entry.get('speed_km/h'), 'speed_kmh': entry.get('speed_kmh') or entry.get('speed') or entry.get('speed_km/h'),
'weight_kg': entry.get('weight_kg') or entry.get('weight') or entry.get('mass_kg'), 'weight_kg': entry.get('weight_kg') or entry.get('weight') or entry.get('mass_kg'),
@@ -454,9 +473,106 @@ def import_from_paste(request):
s['facts'] = parts s['facts'] = parts
suggestions.append(s) suggestions.append(s)
# provide a JSON-serialized version of suggestions so the preview form can post it # Build contextual preview: match suggestions to existing dinosaurs and compute field-level diffs
from .models import Dinosaur
def _map_choice(val, choices):
"""Map an incoming value (code or display) to the internal choice code.
Returns the code or None if not found."""
if val is None:
return None
s = str(val).strip()
if not s:
return None
s_low = s.lower()
# Try exact code match first
for code, label in choices:
if s_low == str(code).lower() or s_low == str(label).lower():
return code
return None
preview_items = []
for idx, s in enumerate(suggestions):
pk = s.get('pk')
name = s.get('name')
matched = None
if pk:
try:
matched = Dinosaur.objects.get(pk=pk)
except Dinosaur.DoesNotExist:
matched = None
if matched is None and name:
try:
matched = Dinosaur.objects.get(name__iexact=name)
except Dinosaur.DoesNotExist:
matched = None
# normalize proposed values for comparison
def _to_float(v):
try:
return float(v)
except Exception:
return None
proposed = {
'name': s.get('name'),
'era': _map_choice(s.get('era'), Dinosaur.ERA_CHOICES),
'diet': _map_choice(s.get('diet'), Dinosaur.DIET_CHOICES),
'speed_kmh': _to_float(s.get('speed_kmh')),
'weight_kg': _to_float(s.get('weight_kg')),
'height_m': _to_float(s.get('height_m')),
'intelligence_score': _to_float(s.get('intelligence_score')),
'facts': s.get('facts')
}
existing = None
diffs = []
if matched:
existing = {
'name': matched.name,
'era': matched.era,
'diet': matched.diet,
'speed_kmh': float(matched.speed) if matched.speed is not None else None,
'weight_kg': float(matched.weight) if matched.weight is not None else None,
'height_m': float(matched.height) if matched.height is not None else None,
'intelligence_score': float(matched.intelligence) if matched.intelligence is not None else None,
'facts': matched.facts_list
}
# compare field-by-field (tolerant numeric comparison)
for field in ['name', 'era', 'diet']:
if proposed.get(field) is not None and str(proposed.get(field)) != str(existing.get(field)):
diffs.append(field)
for field in ['speed_kmh', 'weight_kg', 'height_m', 'intelligence_score']:
pv = proposed.get(field)
ev = existing.get(field)
if pv is not None and ev is not None:
try:
if abs(float(pv) - float(ev)) > 0.0001:
diffs.append(field)
except Exception:
diffs.append(field)
elif pv is not None and ev is None:
diffs.append(field)
# facts: compare list vs list/string
prop_facts = proposed.get('facts')
if isinstance(prop_facts, str):
prop_list = [l.strip() for l in prop_facts.splitlines() if l.strip()]
elif isinstance(prop_facts, list):
prop_list = [str(x).strip() for x in prop_facts if str(x).strip()]
else:
prop_list = []
if prop_list and prop_list != existing.get('facts'):
diffs.append('facts')
else:
# New record: everything proposed counts as a diff
existing = None
diffs = [k for k, v in proposed.items() if v is not None and v != ""]
preview_items.append({'index': idx, 'suggestion': s, 'matched': matched, 'existing': existing, 'proposed': proposed, 'diffs': diffs})
suggestions_json = json.dumps(suggestions, ensure_ascii=False) suggestions_json = json.dumps(suggestions, ensure_ascii=False)
return render(request, 'cards/_bulk_import_preview.html', {'suggestions': suggestions, 'suggestions_json': suggestions_json}) return render(request, 'cards/_bulk_import_preview.html', {'preview_items': preview_items, 'suggestions_json': suggestions_json})
def apply_bulk_import(request): def apply_bulk_import(request):
@@ -473,7 +589,13 @@ def apply_bulk_import(request):
return HttpResponseBadRequest('Failed to parse JSON data') return HttpResponseBadRequest('Failed to parse JSON data')
applied = 0 applied = 0
for item in parsed: def _to_float(v):
try:
return float(v)
except Exception:
return None
for idx, item in enumerate(parsed):
name = item.get('name') if isinstance(item, dict) else None name = item.get('name') if isinstance(item, dict) else None
pk = item.get('pk') if isinstance(item, dict) else None pk = item.get('pk') if isinstance(item, dict) else None
d = None d = None
@@ -487,34 +609,77 @@ def apply_bulk_import(request):
d = Dinosaur.objects.get(name__iexact=name) d = Dinosaur.objects.get(name__iexact=name)
except Dinosaur.DoesNotExist: except Dinosaur.DoesNotExist:
d = None d = None
# If no existing dinosaur matched, allow creation when user checked the create box
create_requested = bool(request.POST.get(f'apply-{idx}-create'))
if not d and create_requested:
d = Dinosaur()
is_new = True
if not d: if not d:
continue continue
# apply numeric fields if present
def _to_float(v): # determine whether the user checked to apply this field for this item
try: def _should_apply(field_name):
return float(v) return bool(request.POST.get(f'apply-{idx}-{field_name}'))
except Exception:
return None
speed = _to_float(item.get('speed_kmh')) speed = _to_float(item.get('speed_kmh'))
weight = _to_float(item.get('weight_kg')) weight = _to_float(item.get('weight_kg'))
height = _to_float(item.get('height_m')) height = _to_float(item.get('height_m'))
intelligence = _to_float(item.get('intelligence_score') or item.get('intelligence')) intelligence = _to_float(item.get('intelligence_score') or item.get('intelligence'))
if speed is not None:
if speed is not None and _should_apply('speed_kmh'):
d.speed = int(round(speed)) d.speed = int(round(speed))
if weight is not None: if weight is not None and _should_apply('weight_kg'):
d.weight = int(round(weight)) d.weight = int(round(weight))
if height is not None: if height is not None and _should_apply('height_m'):
d.height = Decimal(str(round(height, 1))) d.height = Decimal(str(round(height, 1)))
if intelligence is not None: if intelligence is not None and _should_apply('intelligence_score'):
if 0 <= intelligence <= 5: if 0 <= intelligence <= 5:
d.intelligence = int(round((intelligence / 5.0) * 100)) d.intelligence = int(round((intelligence / 5.0) * 100))
else: else:
d.intelligence = int(round(max(0, min(100, intelligence)))) d.intelligence = int(round(max(0, min(100, intelligence))))
# Name
if item.get('name') and _should_apply('name'):
d.name = item.get('name')
# Ensure new records have a name (fallback)
if getattr(d, 'name', None) in (None, ''):
d.name = item.get('name') or f'Imported Dinosaur {idx + 1}'
# Era and diet: accept code or display label
if item.get('era') and _should_apply('era'):
era_val = item.get('era')
if isinstance(era_val, str):
mapped = None
for code, label in Dinosaur.ERA_CHOICES:
if era_val.lower() == code.lower() or era_val.lower() == label.lower():
mapped = code
break
if mapped:
d.era = mapped
else:
d.era = era_val
if item.get('diet') and _should_apply('diet'):
diet_val = item.get('diet')
if isinstance(diet_val, str):
mapped = None
for code, label in Dinosaur.DIET_CHOICES:
if diet_val.lower() == code.lower() or diet_val.lower() == label.lower():
mapped = code
break
if mapped:
d.diet = mapped
else:
d.diet = diet_val
# Facts
if _should_apply('facts'):
facts = item.get('facts') facts = item.get('facts')
if isinstance(facts, list): if isinstance(facts, list):
d.facts = '\n'.join([str(x).strip() for x in facts if str(x).strip()]) d.facts = '\n'.join([str(x).strip() for x in facts if str(x).strip()])
elif isinstance(facts, str): elif isinstance(facts, str):
d.facts = '\n'.join([l.strip() for l in facts.splitlines() if l.strip()]) d.facts = '\n'.join([l.strip() for l in facts.splitlines() if l.strip()])
d.save() d.save()
applied += 1 applied += 1
BIN
View File
Binary file not shown.