import json from birds.models import Bird, BirdSightingEntry from django import forms class BirdSightingsWidget(forms.Widget): template_name = "birds/bird_sightings_widget.html" class Media: js = ("birds/bird_sightings.js",) def value_from_datadict(self, data, files, name): bird_ids = data.getlist(f"{name}_bird_id") quantities = data.getlist(f"{name}_quantity") notes = data.getlist(f"{name}_sighting_notes") return { "bird_id": bird_ids, "quantity": quantities, "sighting_notes": notes, } def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) sightings = [] if value: if isinstance(value, str): try: value = json.loads(value) except (json.JSONDecodeError, TypeError): value = [] for item in (value or []): if isinstance(item, dict): sightings.append(item) elif isinstance(item, BirdSightingEntry): sightings.append(item.asdict) context["widget"]["sightings"] = sightings context["widget"]["birds"] = Bird.objects.all().order_by("common_name") return context class BirdSightingsField(forms.Field): widget = BirdSightingsWidget def clean(self, value): if not value: return None result = [] bird_ids = value.get("bird_id", []) if isinstance(value, dict) else [] quantities = value.get("quantity", []) if isinstance(value, dict) else [] notes_list = ( value.get("sighting_notes", []) if isinstance(value, dict) else [] ) if isinstance(bird_ids, list): for i, bird_id in enumerate(bird_ids): if not bird_id: continue try: bird_id = int(bird_id) quantity = int(quantities[i]) if i < len(quantities) else 1 except (ValueError, TypeError, IndexError): continue note = notes_list[i] if i < len(notes_list) else "" entry = BirdSightingEntry( bird_id=bird_id, quantity=quantity, sighting_notes=note or None, ) result.append(entry.asdict) elif bird_ids: try: bird_id = int(bird_ids) quantity = int(quantities) if quantities else 1 except (ValueError, TypeError): raise forms.ValidationError("Invalid bird sighting data") note = notes_list if notes_list else "" entry = BirdSightingEntry( bird_id=bird_id, quantity=quantity, sighting_notes=note or None, ) result.append(entry.asdict) return result if result else None