217 lines
7.8 KiB
Python
217 lines
7.8 KiB
Python
"""
|
|
Management command: import_weapons_csv
|
|
|
|
Reads weapon.csv (French RGA export, ';'-separated) and:
|
|
1. Creates Caliber instances for all unique caliber names (status=VERIFIED).
|
|
2. Creates Firearm instances for each row where the weapon type is
|
|
mappable to a FirearmType (skips air guns, NL weapons, etc.).
|
|
|
|
CSV column layout (1-indexed, matching the header):
|
|
1 referenceRGA
|
|
2 famille (EPAULE / POING)
|
|
3 typeArme (weapon type in French)
|
|
4 marque (brand)
|
|
5 modele (model name)
|
|
6 fabricant
|
|
7 paysFabricant
|
|
8 modeFonctionnement
|
|
9 systemeAlimentation
|
|
10 longueurArme
|
|
11 capaciteHorsChambre
|
|
12 capaciteChambre
|
|
13 calibreCanonUn ← primary caliber
|
|
14 modePercussionCanonUn
|
|
15 typeCanonUn
|
|
16 longueurCanonUn (barrel length mm)
|
|
|
|
Usage:
|
|
python manage.py import_weapons_csv /path/to/weapon.csv
|
|
python manage.py import_weapons_csv /path/to/weapon.csv --dry-run
|
|
"""
|
|
|
|
import csv
|
|
from django.core.management.base import BaseCommand
|
|
from django.db import transaction
|
|
from django.utils import timezone
|
|
|
|
|
|
FIREARM_TYPE_MAP = {
|
|
'CARABINE': 'CARBINE',
|
|
'CARABINE A BARILLET': 'CARBINE',
|
|
'FUSIL': 'SHOTGUN',
|
|
"FUSIL A POMPE": 'SHOTGUN',
|
|
"FUSIL D'ASSAUT": 'RIFLE',
|
|
"FUSIL (FAP MODIFIE 1 COUP)": 'SHOTGUN',
|
|
"FUSIL SEMI-AUTOMATIQUE ET A POMPE": 'SHOTGUN',
|
|
'PISTOLET': 'PISTOL',
|
|
'REVOLVER': 'REVOLVER',
|
|
}
|
|
|
|
|
|
def _clean(val: str) -> str:
|
|
"""Strip surrounding whitespace and quotation marks."""
|
|
return val.strip().strip('"').strip("'").strip()
|
|
|
|
|
|
class Command(BaseCommand):
|
|
help = 'Import calibers and firearms from the French RGA weapon CSV.'
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument('csv_path', type=str, help='Path to weapon.csv')
|
|
parser.add_argument(
|
|
'--dry-run', action='store_true',
|
|
help='Parse and count without writing to the database.',
|
|
)
|
|
|
|
def handle(self, *args, **options):
|
|
from apps.calibers.models import Caliber, CaliberStatus
|
|
from apps.gears.models import Firearm, GearStatus
|
|
|
|
csv_path = options['csv_path']
|
|
dry_run = options['dry_run']
|
|
|
|
if dry_run:
|
|
self.stdout.write(self.style.WARNING('DRY RUN — nothing will be saved.'))
|
|
|
|
# ── Pass 1: collect unique caliber names ──────────────────────────────
|
|
caliber_names = set()
|
|
rows = []
|
|
|
|
self.stdout.write('Reading CSV…')
|
|
with open(csv_path, encoding='utf-8', errors='replace') as fh:
|
|
reader = csv.reader(fh, delimiter=';')
|
|
next(reader) # skip header
|
|
for row in reader:
|
|
if len(row) < 13:
|
|
continue
|
|
cal_raw = _clean(row[12])
|
|
type_raw = _clean(row[2])
|
|
if cal_raw and type_raw in FIREARM_TYPE_MAP:
|
|
caliber_names.add(cal_raw)
|
|
rows.append(row)
|
|
|
|
self.stdout.write(f' {len(rows)} data rows, {len(caliber_names)} unique calibers to import.')
|
|
|
|
if dry_run:
|
|
fw_count = sum(
|
|
1 for r in rows
|
|
if len(r) >= 5 and _clean(r[2]) in FIREARM_TYPE_MAP and _clean(r[4])
|
|
)
|
|
self.stdout.write(f' ~{fw_count} firearms would be created.')
|
|
return
|
|
|
|
# ── Pass 2: upsert Caliber instances ─────────────────────────────────
|
|
self.stdout.write('Upserting calibers…')
|
|
caliber_map = {} # name → Caliber pk
|
|
|
|
existing = {c.name: c for c in Caliber.objects.filter(name__in=caliber_names)}
|
|
caliber_map.update({name: cal.pk for name, cal in existing.items()})
|
|
|
|
new_calibers = []
|
|
now = timezone.now()
|
|
for name in caliber_names:
|
|
if name not in existing:
|
|
new_calibers.append(Caliber(
|
|
name=name,
|
|
status=CaliberStatus.VERIFIED,
|
|
reviewed_at=now,
|
|
))
|
|
|
|
if new_calibers:
|
|
created = Caliber.objects.bulk_create(new_calibers, batch_size=500)
|
|
for c in created:
|
|
caliber_map[c.name] = c.pk
|
|
self.stdout.write(f' Created {len(created)} new calibers ({len(existing)} already existed).')
|
|
else:
|
|
self.stdout.write(f' All {len(existing)} calibers already existed.')
|
|
|
|
# ── Pass 3: import firearms ───────────────────────────────────────────
|
|
self.stdout.write('Importing firearms…')
|
|
|
|
# Build set of existing (brand, model_name) pairs to avoid duplicates
|
|
existing_firearms = set(
|
|
Firearm.objects.values_list('brand', 'model_name')
|
|
)
|
|
|
|
to_create = []
|
|
skipped_type = 0
|
|
skipped_dup = 0
|
|
skipped_no_model = 0
|
|
|
|
for row in rows:
|
|
if len(row) < 5:
|
|
continue
|
|
|
|
type_raw = _clean(row[2])
|
|
firearm_type = FIREARM_TYPE_MAP.get(type_raw)
|
|
if not firearm_type:
|
|
skipped_type += 1
|
|
continue
|
|
|
|
brand = _clean(row[3])
|
|
model = _clean(row[4])
|
|
if not model:
|
|
skipped_no_model += 1
|
|
continue
|
|
if not brand:
|
|
brand = '(unknown)'
|
|
|
|
if (brand, model) in existing_firearms:
|
|
skipped_dup += 1
|
|
continue
|
|
|
|
cal_raw = _clean(row[12]) if len(row) > 12 else ''
|
|
cal_pk = caliber_map.get(cal_raw) if cal_raw else None
|
|
|
|
barrel_mm = None
|
|
if len(row) > 15:
|
|
try:
|
|
barrel_mm = float(_clean(row[15]).replace(',', '.'))
|
|
if barrel_mm <= 0:
|
|
barrel_mm = None
|
|
except (ValueError, AttributeError):
|
|
barrel_mm = None
|
|
|
|
cap_extra = None
|
|
if len(row) > 10:
|
|
try:
|
|
cap_extra = int(_clean(row[10]))
|
|
except (ValueError, AttributeError):
|
|
pass
|
|
|
|
to_create.append(Firearm(
|
|
brand = brand,
|
|
model_name = model,
|
|
firearm_type = firearm_type,
|
|
caliber_id = cal_pk,
|
|
barrel_length_mm = barrel_mm,
|
|
magazine_capacity = cap_extra if cap_extra else None,
|
|
status = GearStatus.VERIFIED,
|
|
reviewed_at = now,
|
|
))
|
|
# Track to prevent within-batch duplicates
|
|
existing_firearms.add((brand, model))
|
|
|
|
self.stdout.write(
|
|
f' {len(to_create)} firearms to create '
|
|
f'({skipped_dup} duplicates, {skipped_type} unsupported types, '
|
|
f'{skipped_no_model} no model name).'
|
|
)
|
|
|
|
with transaction.atomic():
|
|
# Firearm inherits from Gear (MTI) so bulk_create won't work directly.
|
|
# Use chunked individual creates instead.
|
|
chunk = 500
|
|
for i in range(0, len(to_create), chunk):
|
|
batch = to_create[i:i + chunk]
|
|
for fw in batch:
|
|
fw.save()
|
|
pct = min(i + chunk, len(to_create))
|
|
self.stdout.write(f' … {pct}/{len(to_create)}', ending='\r')
|
|
self.stdout.flush()
|
|
|
|
self.stdout.write('')
|
|
self.stdout.write(self.style.SUCCESS(
|
|
f'Done. {len(to_create)} firearms and {len(new_calibers)} calibers imported.'
|
|
))
|