ebird-api-data 0.4.0__tar.gz → 0.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/PKG-INFO +15 -2
  2. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/README.md +13 -0
  3. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/pyproject.toml +3 -3
  4. ebird_api_data-0.5.1/src/ebird/api/data/__init__.py +1 -0
  5. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/admin.py +2 -15
  6. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/loaders.py +159 -88
  7. ebird_api_data-0.5.1/src/ebird/api/data/migrations/0003_remove_checklist_published_and_more.py +24 -0
  8. ebird_api_data-0.5.1/src/ebird/api/data/migrations/0004_remove_observer_multiple.py +17 -0
  9. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/models/__init__.py +0 -2
  10. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/models/checklist.py +0 -5
  11. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/models/observation.py +0 -5
  12. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/models/observer.py +0 -6
  13. ebird_api_data-0.4.0/src/ebird/api/data/__init__.py +0 -1
  14. ebird_api_data-0.4.0/src/ebird/api/data/factories.py +0 -432
  15. ebird_api_data-0.4.0/src/ebird/api/data/faker_providers.py +0 -345
  16. ebird_api_data-0.4.0/src/ebird/api/data/fixtures/README.md +0 -355
  17. ebird_api_data-0.4.0/src/ebird/api/data/fixtures/generate_portugal_data.py +0 -530
  18. ebird_api_data-0.4.0/src/ebird/api/data/fixtures/portugal_observations.json +0 -144813
  19. ebird_api_data-0.4.0/src/ebird/api/data/models/filter.py +0 -116
  20. ebird_api_data-0.4.0/src/ebird/api/data/models/models.md +0 -242
  21. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/.gitignore +0 -0
  22. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/LICENSE +0 -0
  23. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/apps.py +0 -0
  24. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/fields.py +0 -0
  25. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/locale/en/LC_MESSAGES/django.mo +0 -0
  26. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/locale/en/LC_MESSAGES/django.po +0 -0
  27. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/management/__init__.py +0 -0
  28. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/management/commands/__init__.py +0 -0
  29. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/management/commands/add_checklists.py +0 -0
  30. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/migrations/0001_initial.py +0 -0
  31. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/migrations/0002_expand_filter.py +0 -0
  32. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/migrations/__init__.py +0 -0
  33. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/models/country.py +0 -0
  34. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/models/county.py +0 -0
  35. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/models/location.py +0 -0
  36. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/models/species.py +0 -0
  37. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/models/state.py +0 -0
  38. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/templates/admin/data/species/change_list_object_tools.html +0 -0
  39. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/templates/admin/data/species/fetch_species.html +0 -0
  40. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/templates/data/widgets/translation_field.html +0 -0
  41. {ebird_api_data-0.4.0 → ebird_api_data-0.5.1}/src/ebird/api/data/widgets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ebird-api-data
3
- Version: 0.4.0
3
+ Version: 0.5.1
4
4
  Summary: eBird API Data is a reusable Django app for loading data from eBird into a database.
5
5
  Author-email: Stuart MacKay <smackay@flagstonesoftware.com>
6
6
  License-Expression: MIT
@@ -23,7 +23,7 @@ Classifier: Topic :: Scientific/Engineering :: Information Analysis
23
23
  Requires-Python: >=3.10
24
24
  Requires-Dist: beautifulsoup4>=4.13.3
25
25
  Requires-Dist: django>=4.2
26
- Requires-Dist: ebird-api-requests==4.0.2
26
+ Requires-Dist: ebird-api-requests>=4.1.0
27
27
  Requires-Dist: lxml>=6.0.0
28
28
  Description-Content-Type: text/markdown
29
29
 
@@ -158,6 +158,19 @@ python manage.py runserver
158
158
  Now, either visit the site, http:localhost:8000/, or log into the Django Admin,
159
159
  http:localhost:8000/admin to browse the tables.
160
160
 
161
+ IMPORTANT: There is a serious flaw in the eBird API - observers are identified
162
+ by name. That means if more than one observer shares the same name then only
163
+ one Observer record will be added to the database, and all the checklists will
164
+ be assigned to them. In practice, most people ensure their name is unique by
165
+ adding middle initial's etc. However it is a problem with "Anonymous eBirder".
166
+ The solution is to add two Observer records with the name "Anonymous eBirder",
167
+ and a fake identifier, e.g. ANON1, ANON2, etc. That way the loader will first
168
+ scrape the web page for the checklist where the identifier, e.g. USER2743448,
169
+ can be found, and so the checklist will be assigned to the correct Observer.
170
+ Use this workaround for any other observers you encounter that share the same
171
+ name. This won't work retroactively, but then you can delete the checklists,
172
+ and manually reload them.
173
+
161
174
  ## Project Information
162
175
 
163
176
  * Documentation: https://ebird-api-data.readthedocs.io/en/latest/
@@ -129,6 +129,19 @@ python manage.py runserver
129
129
  Now, either visit the site, http:localhost:8000/, or log into the Django Admin,
130
130
  http:localhost:8000/admin to browse the tables.
131
131
 
132
+ IMPORTANT: There is a serious flaw in the eBird API - observers are identified
133
+ by name. That means if more than one observer shares the same name then only
134
+ one Observer record will be added to the database, and all the checklists will
135
+ be assigned to them. In practice, most people ensure their name is unique by
136
+ adding middle initial's etc. However it is a problem with "Anonymous eBirder".
137
+ The solution is to add two Observer records with the name "Anonymous eBirder",
138
+ and a fake identifier, e.g. ANON1, ANON2, etc. That way the loader will first
139
+ scrape the web page for the checklist where the identifier, e.g. USER2743448,
140
+ can be found, and so the checklist will be assigned to the correct Observer.
141
+ Use this workaround for any other observers you encounter that share the same
142
+ name. This won't work retroactively, but then you can delete the checklists,
143
+ and manually reload them.
144
+
132
145
  ## Project Information
133
146
 
134
147
  * Documentation: https://ebird-api-data.readthedocs.io/en/latest/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ebird-api-data"
3
- version = "0.4.0"
3
+ version = "0.5.1"
4
4
  description = "eBird API Data is a reusable Django app for loading data from eBird into a database."
5
5
  keywords = ["checklists", "observations", "species", "Django", "eBird", "models"]
6
6
  license = "MIT"
@@ -13,7 +13,7 @@ requires-python = ">=3.10"
13
13
  dependencies = [
14
14
  "beautifulsoup4>=4.13.3",
15
15
  "django>=4.2",
16
- "ebird-api-requests==4.0.2",
16
+ "ebird-api-requests>=4.1.0",
17
17
  "lxml>=6.0.0",
18
18
  ]
19
19
  classifiers = [
@@ -64,7 +64,7 @@ dev-dependencies = [
64
64
  ]
65
65
 
66
66
  [tool.bumpversion]
67
- current_version = "0.4.0"
67
+ current_version = "0.5.1"
68
68
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
69
69
  serialize = ["{major}.{minor}.{patch}"]
70
70
  ignore_missing_version = false
@@ -0,0 +1 @@
1
+ __version__ = "0.5.1"
@@ -266,10 +266,10 @@ class ObservationAdmin(admin.ModelAdmin):
266
266
 
267
267
  @admin.register(models.Observer)
268
268
  class ObserverAdmin(admin.ModelAdmin):
269
- list_display = ("name", "identifier", "multiple", "enabled")
269
+ list_display = ("name", "identifier", "enabled")
270
270
  ordering = ("name",)
271
271
  search_fields = ("name", "identifier")
272
- list_filter = ("multiple", "enabled")
272
+ list_filter = ("enabled",)
273
273
  form = ObservationForm
274
274
 
275
275
  def formfield_for_dbfield(self, db_field, request, **kwargs):
@@ -370,16 +370,3 @@ class SpeciesAdmin(admin.ModelAdmin):
370
370
  extra_context = extra_context or {}
371
371
  extra_context["fetch_species_url"] = reverse("admin:data_species_fetch")
372
372
  return super().changelist_view(request, extra_context=extra_context)
373
-
374
-
375
- @admin.register(models.Filter)
376
- class FilterAdmin(admin.ModelAdmin):
377
- list_display = ("name", "enabled")
378
- search_fields = ("name", "species__common_name")
379
- list_filter = ("enabled",)
380
- autocomplete_fields = ("species", "location", "update_species")
381
- formfield_overrides = {
382
- TextField: {
383
- "widget": TextInput(attrs={"class": "vTextField"}),
384
- }
385
- }
@@ -25,7 +25,6 @@ from .models import (
25
25
  Checklist,
26
26
  Country,
27
27
  County,
28
- Filter,
29
28
  Location,
30
29
  Observation,
31
30
  Observer,
@@ -144,17 +143,16 @@ class APILoader:
144
143
  return self.call(func, self.api_key, *args, **kwargs)
145
144
 
146
145
  @staticmethod
147
- def update(obj, values: dict) -> bool:
148
- ignored = ["added", "edited"] # Don't log timestamp changes
149
- updated = False
146
+ def update(obj, values: dict) -> dict[str, tuple]:
147
+ changed: dict[str, tuple] = {}
150
148
  for key, value in values.items():
151
149
  current = getattr(obj, key)
152
150
  if current != value:
153
151
  setattr(obj, key, value)
154
- updated = True
155
- if key not in ignored:
156
- logger.info("Attribute Changed: %s, %s -> %s", key, current, value)
157
- return updated
152
+ changed[key] = (current, value)
153
+ if changed:
154
+ obj.save()
155
+ return changed
158
156
 
159
157
  def get_country(self, data: dict) -> Country:
160
158
  code: str = data["countryCode"]
@@ -164,10 +162,11 @@ class APILoader:
164
162
  }
165
163
  country, created = Country.objects.get_or_create(code=code, defaults=values)
166
164
  if created:
167
- logger.info("Added country: %s, %s", code, values["name"])
168
- elif self.update(country, values):
169
- logger.info("Updated country: %s", code)
170
- country.save()
165
+ logger.info("Country %s: added", code)
166
+ elif changed := self.update(country, values):
167
+ if "name" in changed:
168
+ old, new = changed["name"]
169
+ logger.info("Country %s: renamed, %s -> %s", code, old, new)
171
170
  return country
172
171
 
173
172
  def get_state(self, data: dict) -> State:
@@ -178,10 +177,11 @@ class APILoader:
178
177
  }
179
178
  state, created = State.objects.get_or_create(code=code, defaults=values)
180
179
  if created:
181
- logger.info("Added state: %s, %s", code, values["name"])
182
- elif self.update(state, values):
183
- logger.info("Updated state: %s", code)
184
- state.save()
180
+ logger.info("State: %s: added", code)
181
+ elif changed := self.update(state, values):
182
+ if "name" in changed:
183
+ old, new = changed["name"]
184
+ logger.info("State %s: renamed, %s -> %s", code, old, new)
185
185
  return state
186
186
 
187
187
  def get_county(self, data) -> County:
@@ -193,10 +193,11 @@ class APILoader:
193
193
  }
194
194
  county, created = County.objects.get_or_create(code=code, defaults=values)
195
195
  if created:
196
- logger.info("Added county: %s, %s", code, values["name"])
197
- elif self.update(county, values):
198
- logger.info("Updated county: %s", code)
199
- county.save()
196
+ logger.info("County %s: added", code)
197
+ elif changed := self.update(county, values):
198
+ if "name" in changed:
199
+ old, new = changed["name"]
200
+ logger.info("County %s: renamed, %s -> %s", code, old, new)
200
201
  return county
201
202
 
202
203
  def add_location(self, data: dict) -> Location:
@@ -223,13 +224,17 @@ class APILoader:
223
224
  )
224
225
 
225
226
  if not created:
226
- values.pop("name") # Don't overwrite the name
227
+ values.pop("name") # Don't overwrite the name
227
228
 
228
229
  if created:
229
- logger.info("Added location: %s, %s", identifier, location.name)
230
- elif self.update(location, values):
231
- logger.info("Updated location: %s", identifier)
232
- location.save()
230
+ logger.info("Location %s: added %s", identifier, values["name"])
231
+ elif changed := self.update(location, values):
232
+ included = ["name", "hotspot", "county", "state", "country"]
233
+ filtered = {key: value for key, value in changed.items() if key in included}
234
+ for key, (old, new) in filtered.items():
235
+ logger.info(
236
+ "Location %s: changed %s, %s -> %s", identifier, key, old, new
237
+ )
233
238
  return location
234
239
 
235
240
  def add_species(self, code: str) -> Species:
@@ -260,7 +265,7 @@ class APILoader:
260
265
  values["family_common_name"] = json.dumps(values["family_common_name"])
261
266
 
262
267
  species = Species.objects.create(species_code=code, **values)
263
- logger.info("Added species: %s, %s", code, species.get_common_name())
268
+ logger.info("Species added: %s, %s", code, species.get_common_name())
264
269
 
265
270
  return species
266
271
 
@@ -282,7 +287,6 @@ class APILoader:
282
287
 
283
288
  values: dict = {
284
289
  "edited": checklist.edited,
285
- "published": False,
286
290
  "checklist": checklist,
287
291
  "country": checklist.country,
288
292
  "state": checklist.state,
@@ -315,49 +319,93 @@ class APILoader:
315
319
  observation, created = Observation.objects.get_or_create(
316
320
  identifier=identifier, defaults=values
317
321
  )
318
- # Don't record when an observation is added. It's too noisy.
319
- if not created:
320
- values.pop("published")
321
- if self.update(observation, values):
322
- logger.info("Updated observation: %s", identifier)
323
- observation.save()
322
+
323
+ # Log observations that were added after the checklist was first loaded.
324
+ if created and checklist.created < checklist.edited:
325
+ logger.info(
326
+ "Checklist %s: added %s (%d)",
327
+ checklist.identifier,
328
+ species,
329
+ values["count"],
330
+ )
331
+
332
+ if changed := self.update(observation, values):
333
+ for field in ["audio", "photo", "video"]:
334
+ if field in changed:
335
+ old, new = changed[field]
336
+ logger.info(
337
+ "Checklist %s: %s, %s %s",
338
+ checklist.identifier,
339
+ species,
340
+ "added" if new else "deleted",
341
+ field,
342
+ )
343
+ if "count" in changed:
344
+ old, new = changed["count"]
345
+ logger.info(
346
+ "Checklist %s: %s, changed count, %s -> %s",
347
+ checklist.identifier,
348
+ species,
349
+ old,
350
+ new,
351
+ )
352
+ if "species" in changed:
353
+ old, new = changed["species"]
354
+ logger.info(
355
+ "Checklist %s: changed species, %s -> %s",
356
+ checklist.identifier,
357
+ old,
358
+ new,
359
+ )
324
360
  return observation
325
361
 
326
362
  def get_observer_identifier(self, data) -> str:
327
- identifier: str = data["subId"]
328
- logger.info("Scraping checklist: %s", identifier)
363
+ checklist_identifier: str = data["subId"]
364
+ logger.info("Scraping checklist: %s", checklist_identifier)
329
365
  response = self.call(
330
- requests.get, "https://ebird.org/checklist/%s" % identifier
366
+ requests.get, "https://ebird.org/checklist/%s" % checklist_identifier
331
367
  )
332
368
  content = response.content
333
369
  soup = BeautifulSoup(content, "lxml")
334
370
  attribute = "data-participant-userid"
335
371
  node = soup.find("span", attrs={attribute: True})
336
- return node[attribute] if node else ""
372
+ identifier = node[attribute] if node else ""
373
+ if not identifier:
374
+ logger.error("Observer Identifier: not found")
375
+ return identifier
337
376
 
338
377
  def get_observer(self, data: dict) -> Observer:
378
+ # In theory, there is a serious problem here, as observers can have
379
+ # the same name. There is no way to resolve this except to prime the
380
+ # database with multiple Observers sharing the same name. That will
381
+ # force the identifier to be scraped from the checklist web page and
382
+ # so the correct "John Smith" will be used. In practice, apart from
383
+ # Anonymous eBirder, this is not a problem. Most observers want their
384
+ # name to be unique (so they show up in the leader boards), and so they
385
+ # will add middle initials, etc. so the names are generally unique.
386
+
339
387
  name: str = data.get("userDisplayName", "Anonymous eBirder")
340
- is_multiple = Observer.objects.filter(original=name, multiple=True).exists()
341
- if is_multiple:
388
+ number_of_observers = Observer.objects.filter(original=name).count()
389
+
390
+ if number_of_observers == 0:
391
+ identifier = self.get_observer_identifier(data)
392
+ # The observer might have changed their name.
393
+ observer, created = Observer.objects.update_or_create(
394
+ identifier=identifier, defaults={"name": name, "original": name}
395
+ )
396
+ elif number_of_observers == 1:
397
+ observer = Observer.objects.get(original=name)
398
+ identifier = observer.identifier
399
+ created = False
400
+ else:
342
401
  identifier = self.get_observer_identifier(data)
343
402
  observer, created = Observer.objects.get_or_create(
344
403
  identifier=identifier,
345
- defaults={"name": name, "original": random_word(8)},
404
+ defaults={"name": name, "original": name},
346
405
  )
347
- else:
348
- if observer := Observer.objects.filter(original=name).first():
349
- created = False
350
- else:
351
- identifier = self.get_observer_identifier(data)
352
- observer, created = Observer.objects.get_or_create(
353
- identifier=identifier,
354
- defaults={"name": name, "original": name},
355
- )
356
406
 
357
407
  if created:
358
- logger.info("Added observer: %s", name)
359
- if Observer.objects.filter(original=name).count() > 1:
360
- logger.error("Multiple observers exist with same name: %s", name)
408
+ logger.info("Observer added: %s (%s)", name, identifier)
361
409
 
362
410
  return observer
363
411
 
@@ -392,7 +440,6 @@ class APILoader:
392
440
  values: dict = {
393
441
  "added": added,
394
442
  "edited": edited,
395
- "published": False,
396
443
  "country": location.country,
397
444
  "state": location.state,
398
445
  "county": location.county,
@@ -413,40 +460,78 @@ class APILoader:
413
460
 
414
461
  if data["obsTimeValid"]:
415
462
  values["time"] = started.time()
463
+ else:
464
+ values["time"] = None
416
465
 
417
466
  if "numObservers" in data:
418
467
  values["observer_count"] = int(data["numObservers"])
468
+ else:
469
+ values["observer_count"] = None
419
470
 
420
471
  if duration := data.get("durationHrs"):
421
472
  values["duration"] = int(duration * 60.0)
473
+ else:
474
+ values["duration"] = None
422
475
 
423
476
  if dist := data.get("effortDistanceKm"):
424
477
  values["distance"] = round(Decimal(dist), 3)
478
+ else:
479
+ values["distance"] = None
425
480
 
426
481
  if area := data.get("effortAreaHa"):
427
482
  values["area"] = round(Decimal(area), 3)
483
+ else:
484
+ values["area"] = None
428
485
 
429
486
  if "comments" in data:
430
487
  values["comments"] = data["comments"]
488
+ else:
489
+ values["comments"] = ""
431
490
 
432
491
  checklist, created = Checklist.objects.get_or_create(
433
492
  identifier=identifier, defaults=values
434
493
  )
435
- if not created:
436
- values.pop("published")
494
+
437
495
  if created:
438
- logger.info("Added checklist: %s", identifier)
439
- elif self.update(checklist, values):
440
- logger.info("Updated checklist: %s", identifier)
441
- checklist.save()
496
+ logger.info("Checklist %s: added", identifier)
497
+ elif changed := self.update(checklist, values):
498
+ ignored = ["added", "edited", "started", "url", "comments"]
499
+ filtered = {
500
+ key: value for key, value in changed.items() if key not in ignored
501
+ }
502
+ if "observer" in filtered:
503
+ old, new = filtered.pop("observer")
504
+ logger.info(
505
+ "Checklist %s: changed observer, %s (%s) -> %s (%s)",
506
+ identifier,
507
+ old.name,
508
+ old.identifier,
509
+ new.name,
510
+ new.identifier
511
+ )
512
+ for key, (old, new) in filtered.items():
513
+ logger.info(
514
+ "Checklist %s: changed %s, %s -> %s",
515
+ identifier,
516
+ key.replace("_", " "),
517
+ old,
518
+ new,
519
+ )
520
+ else:
521
+ logger.debug("Checklist %s: unchanged", identifier)
442
522
 
443
523
  for observation_data in observations:
444
524
  self.get_observation(observation_data, checklist)
445
525
 
446
- for observation in checklist.observations.all():
447
- if observation.edited != checklist.edited:
448
- logger.info("Deleted observation: %s", observation.identifier)
449
- observation.delete()
526
+ for obs in checklist.observations.all():
527
+ if obs.edited != checklist.edited:
528
+ obs.delete()
529
+ logger.info(
530
+ "Checklist %s: deleted, %s (%s)",
531
+ identifier,
532
+ obs.species,
533
+ obs.count,
534
+ )
450
535
 
451
536
  return checklist
452
537
 
@@ -472,15 +557,19 @@ class APILoader:
472
557
  )
473
558
 
474
559
  if len(results) == API_MAX_RESULTS:
475
- logger.info("API limit reached - fetching visits for subregions")
560
+ logger.info("Region %s: API limit reached - fetching subregions", region)
476
561
  if sub_regions := self.fetch_subregions(region):
477
562
  for sub_region in sub_regions:
478
- visits.extend(self.fetch_visits(sub_region, date))
563
+ sub_region_visits = self.fetch_visits(sub_region, date)
564
+ visits.extend(sub_region_visits)
565
+ logger.info(
566
+ "Region %s: %d visits", sub_region, len(sub_region_visits)
567
+ )
479
568
  else:
480
569
  # No more sub-regions, issue a warning and return the results
481
570
  visits.extend(results)
482
571
  logger.warning(
483
- "API limit reached - No subregions available: %s, %s", region, date
572
+ "Region %s: API limit reached - no subregions available", region
484
573
  )
485
574
  else:
486
575
  visits.extend(results)
@@ -502,11 +591,11 @@ class APILoader:
502
591
 
503
592
  """
504
593
 
505
- logger.info("Adding checklists: %s, %s", region, date)
594
+ logger.info("Adding checklists: %s, %s", region, date.strftime("%Y-%m-%d"))
506
595
 
507
596
  visits: list[dict] = self.fetch_visits(region, date)
508
597
 
509
- logger.info("Visits made: %d ", len(visits))
598
+ logger.info("Region %s: %s visits", region, len(visits))
510
599
 
511
600
  for visit in visits:
512
601
  data = visit["loc"]
@@ -514,26 +603,8 @@ class APILoader:
514
603
 
515
604
  for visit in visits:
516
605
  identifier = visit["subId"]
517
- if (
518
- not update
519
- and Checklist.objects.filter(identifier=identifier).exists()
520
- ):
606
+ if not update and Checklist.objects.filter(identifier=identifier).exists():
521
607
  continue
522
608
  self.add_checklist(identifier)
523
609
 
524
- self.run_filters()
525
- self.publish()
526
-
527
- logger.info("Adding checklists completed")
528
-
529
- @staticmethod
530
- def run_filters():
531
- for filter in Filter.objects.filter(enabled=True):
532
- logger.info("Applying filter: %s", filter.name)
533
- count = filter.apply()
534
- logger.info("Observations updated: %d", count)
535
-
536
- @staticmethod
537
- def publish():
538
- Checklist.objects.filter(published=False).update(published=True)
539
- Observation.objects.filter(published=False).update(published=True)
610
+ logger.info("Added checklists: %s, %s", region, date.strftime("%Y-%m-%d"))
@@ -0,0 +1,24 @@
1
+ # Generated by Django 5.2.7 on 2025-11-05 08:59
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('data', '0002_expand_filter'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RemoveField(
14
+ model_name='checklist',
15
+ name='published',
16
+ ),
17
+ migrations.RemoveField(
18
+ model_name='observation',
19
+ name='published',
20
+ ),
21
+ migrations.DeleteModel(
22
+ name='Filter',
23
+ ),
24
+ ]
@@ -0,0 +1,17 @@
1
+ # Generated by Django 5.2.7 on 2025-11-05 09:15
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('data', '0003_remove_checklist_published_and_more'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RemoveField(
14
+ model_name='observer',
15
+ name='multiple',
16
+ ),
17
+ ]
@@ -1,7 +1,6 @@
1
1
  from .checklist import Checklist
2
2
  from .country import Country
3
3
  from .county import County
4
- from .filter import Filter
5
4
  from .location import Location
6
5
  from .observation import Observation
7
6
  from .observer import Observer
@@ -12,7 +11,6 @@ __all__ = (
12
11
  "Checklist",
13
12
  "Country",
14
13
  "County",
15
- "Filter",
16
14
  "Location",
17
15
  "Observation",
18
16
  "Observer",
@@ -33,11 +33,6 @@ class Checklist(models.Model):
33
33
  verbose_name=_("edited"),
34
34
  )
35
35
 
36
- published = models.BooleanField(
37
- help_text=_("Is the checklist published?"),
38
- verbose_name=_("published"),
39
- )
40
-
41
36
  identifier = models.CharField(
42
37
  max_length=15,
43
38
  primary_key=True,
@@ -23,11 +23,6 @@ class Observation(models.Model):
23
23
  verbose_name=_("edited"),
24
24
  )
25
25
 
26
- published = models.BooleanField(
27
- help_text=_("Is the observation published?"),
28
- verbose_name=_("published"),
29
- )
30
-
31
26
  identifier = models.CharField(
32
27
  max_length=15,
33
28
  primary_key=True,
@@ -24,12 +24,6 @@ class Observer(models.Model):
24
24
  help_text=_("The original name of the observer from eBird."),
25
25
  )
26
26
 
27
- multiple = models.BooleanField(
28
- default=False,
29
- verbose_name=_("Multiple accounts"),
30
- help_text=_("Is the name used by more than one eBird account."),
31
- )
32
-
33
27
  enabled = models.BooleanField(
34
28
  default=True,
35
29
  verbose_name=_("Enabled"),
@@ -1 +0,0 @@
1
- __version__ = "0.4.0"