.
This commit is contained in:
@@ -1,30 +1,100 @@
|
||||
{% comment %} Preview fragment for pasted bulk import data. {% endcomment %}
|
||||
<div class="box">
|
||||
<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">
|
||||
{% csrf_token %}
|
||||
<textarea name="data" style="display:none">{{ suggestions_json }}</textarea>
|
||||
<table class="table is-fullwidth is-striped">
|
||||
<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>
|
||||
<tbody>
|
||||
{% for s in suggestions %}
|
||||
<tr>
|
||||
<td>{% if s.pk %}pk:{{ s.pk }}{% elif s.name %}name{% else %}—{% endif %}</td>
|
||||
<td>{{ s.name|default:"(no name)" }}{% if s.pk %} (pk: {{ s.pk }}){% endif %}</td>
|
||||
<td>{{ s.speed_kmh }}</td>
|
||||
<td>{{ s.weight_kg }}</td>
|
||||
<td>{{ s.height_m }}</td>
|
||||
<td>{{ s.intelligence_score }}</td>
|
||||
<td>{% if s.facts %}{% if s.facts|length > 0 %}{{ s.facts|join:"; " }}{% else %}{{ s.facts }}{% endif %}{% endif %}</td>
|
||||
</tr>
|
||||
{% for item in preview_items %}
|
||||
{% with s=item.suggestion i=item.index %}
|
||||
{% if item.diffs %}
|
||||
{% for field in item.diffs %}
|
||||
<tr>
|
||||
<td>{% if forloop.first %}#{{ forloop.parentloop.counter }}{% else %} {% endif %}</td>
|
||||
<td>{% if item.matched %}pk:{{ item.matched.pk }}{% elif s.name %}name{% else %}—{% endif %}</td>
|
||||
<td>{{ s.name|default:"(no name)" }}{% if s.pk %} (pk: {{ s.pk }}){% endif %}</td>
|
||||
<td>{{ field }}</td>
|
||||
<td>
|
||||
{% if item.existing %}
|
||||
{% if field == 'facts' %}
|
||||
{% 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>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td>#{{ forloop.counter }}</td>
|
||||
<td>{% if item.matched %}pk:{{ item.matched.pk }}{% else %}—{% 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>
|
||||
</table>
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -43,6 +43,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<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 == 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>
|
||||
|
||||
@@ -42,6 +42,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<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 == 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>
|
||||
|
||||
@@ -48,6 +48,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<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 == 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>
|
||||
|
||||
@@ -51,6 +51,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<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 == 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>
|
||||
|
||||
@@ -51,6 +51,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<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 == 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>
|
||||
|
||||
+184
-19
@@ -4,6 +4,9 @@ from django.template.loader import render_to_string
|
||||
import io
|
||||
import zipfile
|
||||
import os
|
||||
import json
|
||||
import csv
|
||||
from io import StringIO
|
||||
from .models import Dinosaur, BackgroundImage
|
||||
from .forms import DinosaurBulkFormSet
|
||||
from .forms import DinosaurForm
|
||||
@@ -288,7 +291,21 @@ def preview_card(request, pk):
|
||||
design = 1
|
||||
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):
|
||||
return render(request, template_name, context)
|
||||
# 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)}
|
||||
s = {
|
||||
'pk': entry.get('pk') or entry.get('id'),
|
||||
'era': entry.get('era'),
|
||||
'diet': entry.get('diet'),
|
||||
'name': entry.get('name'),
|
||||
'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'),
|
||||
@@ -454,9 +473,106 @@ def import_from_paste(request):
|
||||
s['facts'] = parts
|
||||
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)
|
||||
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):
|
||||
@@ -473,7 +589,13 @@ def apply_bulk_import(request):
|
||||
return HttpResponseBadRequest('Failed to parse JSON data')
|
||||
|
||||
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
|
||||
pk = item.get('pk') if isinstance(item, dict) else None
|
||||
d = None
|
||||
@@ -487,34 +609,77 @@ def apply_bulk_import(request):
|
||||
d = Dinosaur.objects.get(name__iexact=name)
|
||||
except Dinosaur.DoesNotExist:
|
||||
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:
|
||||
continue
|
||||
# apply numeric fields if present
|
||||
def _to_float(v):
|
||||
try:
|
||||
return float(v)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# determine whether the user checked to apply this field for this item
|
||||
def _should_apply(field_name):
|
||||
return bool(request.POST.get(f'apply-{idx}-{field_name}'))
|
||||
|
||||
speed = _to_float(item.get('speed_kmh'))
|
||||
weight = _to_float(item.get('weight_kg'))
|
||||
height = _to_float(item.get('height_m'))
|
||||
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))
|
||||
if weight is not None:
|
||||
if weight is not None and _should_apply('weight_kg'):
|
||||
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)))
|
||||
if intelligence is not None:
|
||||
if intelligence is not None and _should_apply('intelligence_score'):
|
||||
if 0 <= intelligence <= 5:
|
||||
d.intelligence = int(round((intelligence / 5.0) * 100))
|
||||
else:
|
||||
d.intelligence = int(round(max(0, min(100, intelligence))))
|
||||
facts = item.get('facts')
|
||||
if isinstance(facts, list):
|
||||
d.facts = '\n'.join([str(x).strip() for x in facts if str(x).strip()])
|
||||
elif isinstance(facts, str):
|
||||
d.facts = '\n'.join([l.strip() for l in facts.splitlines() if l.strip()])
|
||||
|
||||
# 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')
|
||||
if isinstance(facts, list):
|
||||
d.facts = '\n'.join([str(x).strip() for x in facts if str(x).strip()])
|
||||
elif isinstance(facts, str):
|
||||
d.facts = '\n'.join([l.strip() for l in facts.splitlines() if l.strip()])
|
||||
|
||||
d.save()
|
||||
applied += 1
|
||||
|
||||
|
||||
BIN
Binary file not shown.
Reference in New Issue
Block a user