Files
ShooterHub/apps/gears/management/commands/import_weapons_csv.py
2026-04-02 11:24:30 +02:00

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.'
))