[birds] Add much better bird importing
This commit is contained in:
@ -46,6 +46,21 @@ def parse_timestamp(date_str, time_str):
|
||||
return None
|
||||
|
||||
|
||||
def parse_bool(value):
|
||||
if not value:
|
||||
return None
|
||||
return value.strip().lower() in ("true", "yes", "1")
|
||||
|
||||
|
||||
def parse_int(value):
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
return int(value.strip())
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def import_birding_csv(file_path, user_id):
|
||||
user = User.objects.get(id=user_id)
|
||||
new_scrobbles = []
|
||||
@ -72,7 +87,9 @@ def import_birding_csv(file_path, user_id):
|
||||
if not timestamp:
|
||||
continue
|
||||
|
||||
location_title = LOCATION_COORDS_RE.sub("", location_str).strip().rstrip(",").strip()
|
||||
location_title = (
|
||||
LOCATION_COORDS_RE.sub("", location_str).strip().rstrip(",").strip()
|
||||
)
|
||||
if not location_title:
|
||||
location_title = location_str
|
||||
|
||||
@ -90,19 +107,13 @@ def import_birding_csv(file_path, user_id):
|
||||
location.save(update_fields=["geo_location"])
|
||||
|
||||
first_row = sighting_rows[0]
|
||||
duration_minutes = parse_duration(first_row.get("Duration", ""))
|
||||
party_size = first_row.get("Party Size", "").strip()
|
||||
|
||||
birds_data = []
|
||||
for row in sighting_rows:
|
||||
species = row.get("Species", "").strip()
|
||||
if not species:
|
||||
continue
|
||||
count_str = row.get("Count", "1").strip()
|
||||
try:
|
||||
count = int(count_str) if count_str else 1
|
||||
except ValueError:
|
||||
count = 1
|
||||
count = parse_int(row.get("Count")) or 1
|
||||
details = row.get("Details", "").strip()
|
||||
|
||||
bird = Bird.find_or_create(species)
|
||||
@ -113,19 +124,15 @@ def import_birding_csv(file_path, user_id):
|
||||
|
||||
logdata = BirdSightingLogData(
|
||||
birds=birds_data,
|
||||
duration_minutes=duration_minutes,
|
||||
duration_minutes=parse_duration(first_row.get("Duration", "")),
|
||||
observation_type=first_row.get("Observation Type", "").strip() or None,
|
||||
distance=first_row.get("Distance", "").strip() or None,
|
||||
area=first_row.get("Area", "").strip() or None,
|
||||
party_size=parse_int(first_row.get("Party Size")),
|
||||
complete_checklist=parse_bool(first_row.get("Complete Checklist")),
|
||||
)
|
||||
|
||||
notes = []
|
||||
if party_size:
|
||||
notes.append(f"Party size: {party_size}")
|
||||
checklist_flag = first_row.get("Complete Checklist", "").strip()
|
||||
if checklist_flag:
|
||||
notes.append(f"Complete checklist: {checklist_flag}")
|
||||
|
||||
log_dict = logdata.asdict
|
||||
if notes:
|
||||
log_dict["notes"] = notes
|
||||
|
||||
scrobble = Scrobble(
|
||||
user=user,
|
||||
|
||||
@ -47,6 +47,11 @@ class BirdSightingEntry(BaseLogData):
|
||||
class BirdSightingLogData(BaseLogData, WithPeopleLogData):
|
||||
birds: Optional[list[BirdSightingEntry]] = None
|
||||
duration_minutes: Optional[int] = None
|
||||
observation_type: Optional[str] = None
|
||||
distance: Optional[str] = None
|
||||
area: Optional[str] = None
|
||||
party_size: Optional[int] = None
|
||||
complete_checklist: Optional[bool] = None
|
||||
weather: Optional[str] = None
|
||||
temperature: Optional[int] = None
|
||||
guide: Optional[str] = None
|
||||
@ -64,6 +69,31 @@ class BirdSightingLogData(BaseLogData, WithPeopleLogData):
|
||||
def as_html(self) -> str:
|
||||
html_parts = []
|
||||
|
||||
if self.observation_type:
|
||||
html_parts.append(
|
||||
f'<div class="birding-obs-type">Type: {self.observation_type}</div>'
|
||||
)
|
||||
|
||||
if self.distance:
|
||||
html_parts.append(
|
||||
f'<div class="birding-distance">Distance: {self.distance}</div>'
|
||||
)
|
||||
|
||||
if self.area:
|
||||
html_parts.append(
|
||||
f'<div class="birding-area">Area: {self.area}</div>'
|
||||
)
|
||||
|
||||
if self.party_size:
|
||||
html_parts.append(
|
||||
f'<div class="birding-party">Party size: {self.party_size}</div>'
|
||||
)
|
||||
|
||||
if self.complete_checklist is not None:
|
||||
html_parts.append(
|
||||
f'<div class="birding-checklist">Complete checklist: {self.complete_checklist}</div>'
|
||||
)
|
||||
|
||||
if self.weather:
|
||||
html_parts.append(
|
||||
f'<div class="birding-weather">Weather: {self.weather}</div>'
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
<div class="bird-sightings-widget">
|
||||
<div class="bird-sightings-list">
|
||||
{% for sighting in widget.sightings %}
|
||||
<div class="bird-sighting-row row mb-2">
|
||||
<div class="col-md-6">
|
||||
<select name="{{widget.name}}_bird_id" class="form-control">
|
||||
<option value="">Select bird...</option>
|
||||
{% for bird in widget.birds %}
|
||||
<option value="{{bird.id}}" {% if sighting.bird_id == bird.id %}selected{% endif %}>{{bird.common_name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="number" name="{{widget.name}}_quantity" class="form-control" value="{{sighting.quantity|default:1}}" min="1" placeholder="Qty">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<input type="text" name="{{widget.name}}_sighting_notes" class="form-control" value="{{sighting.sighting_notes|default:''}}" placeholder="Notes">
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger remove-sighting">×</button>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="bird-sighting-row row mb-2">
|
||||
<div class="col-md-6">
|
||||
<select name="{{widget.name}}_bird_id" class="form-control">
|
||||
<option value="">Select bird...</option>
|
||||
{% for bird in widget.birds %}
|
||||
<option value="{{bird.id}}">{{bird.common_name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="number" name="{{widget.name}}_quantity" class="form-control" value="1" min="1" placeholder="Qty">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<input type="text" name="{{widget.name}}_sighting_notes" class="form-control" value="" placeholder="Notes">
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger remove-sighting">×</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary add-sighting-row mt-2">Add bird</button>
|
||||
</div>
|
||||
@ -45,7 +45,7 @@ class BirdingCSVImportCreateView(
|
||||
return HttpResponseRedirect(self.request.META.get("HTTP_REFERER"))
|
||||
|
||||
|
||||
class BirdingCSVImportDetailView(generic.DetailView):
|
||||
class BirdingCSVImportDetailView(LoginRequiredMixin, generic.DetailView):
|
||||
model = BirdingCSVImport
|
||||
slug_field = "uuid"
|
||||
template_name = "scrobbles/import_detail.html"
|
||||
|
||||
Reference in New Issue
Block a user