ebird-api-data 0.3.7__tar.gz → 0.5.0__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.3.7 → ebird_api_data-0.5.0}/PKG-INFO +16 -4
  2. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/README.md +14 -1
  3. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/pyproject.toml +3 -4
  4. ebird_api_data-0.5.0/src/ebird/api/data/__init__.py +1 -0
  5. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/admin.py +2 -15
  6. ebird_api_data-0.5.0/src/ebird/api/data/factories.py +382 -0
  7. ebird_api_data-0.5.0/src/ebird/api/data/faker_providers.py +345 -0
  8. ebird_api_data-0.5.0/src/ebird/api/data/fixtures/README.md +354 -0
  9. ebird_api_data-0.5.0/src/ebird/api/data/fixtures/generate_portugal_data.py +527 -0
  10. ebird_api_data-0.5.0/src/ebird/api/data/fixtures/portugal_observations.json +139905 -0
  11. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/loaders.py +198 -81
  12. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/management/commands/add_checklists.py +36 -21
  13. ebird_api_data-0.5.0/src/ebird/api/data/migrations/0002_expand_filter.py +29 -0
  14. ebird_api_data-0.5.0/src/ebird/api/data/migrations/0003_remove_checklist_published_and_more.py +24 -0
  15. ebird_api_data-0.5.0/src/ebird/api/data/migrations/0004_remove_observer_multiple.py +17 -0
  16. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/models/__init__.py +0 -2
  17. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/models/checklist.py +0 -5
  18. ebird_api_data-0.5.0/src/ebird/api/data/models/models.md +192 -0
  19. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/models/observation.py +0 -5
  20. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/models/observer.py +0 -6
  21. ebird_api_data-0.3.7/src/ebird/api/data/__init__.py +0 -1
  22. ebird_api_data-0.3.7/src/ebird/api/data/models/filter.py +0 -79
  23. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/.gitignore +0 -0
  24. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/LICENSE +0 -0
  25. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/apps.py +0 -0
  26. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/fields.py +0 -0
  27. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/locale/en/LC_MESSAGES/django.mo +0 -0
  28. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/locale/en/LC_MESSAGES/django.po +0 -0
  29. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/management/__init__.py +0 -0
  30. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/management/commands/__init__.py +0 -0
  31. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/migrations/0001_initial.py +0 -0
  32. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/migrations/__init__.py +0 -0
  33. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/models/country.py +0 -0
  34. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/models/county.py +0 -0
  35. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/models/location.py +0 -0
  36. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/models/species.py +0 -0
  37. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/models/state.py +0 -0
  38. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/templates/admin/data/species/change_list_object_tools.html +0 -0
  39. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/templates/admin/data/species/fetch_species.html +0 -0
  40. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/templates/data/widgets/translation_field.html +0 -0
  41. {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/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.3.7
3
+ Version: 0.5.0
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,8 +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.1
27
- Requires-Dist: ebird-codes>=0.0.2
26
+ Requires-Dist: ebird-api-requests==4.0.2
28
27
  Requires-Dist: lxml>=6.0.0
29
28
  Description-Content-Type: text/markdown
30
29
 
@@ -142,7 +141,7 @@ EBIRD_API_KEY=<my api key>
142
141
  Now, download data from the API:
143
142
 
144
143
  ```shell
145
- python manage.py add_checklists --days 2 US-NY-109
144
+ python manage.py add_checklists --days -2 US-NY-109
146
145
  ```
147
146
 
148
147
  This loads all the checklists, submitted in the past two days by birders in
@@ -159,6 +158,19 @@ python manage.py runserver
159
158
  Now, either visit the site, http:localhost:8000/, or log into the Django Admin,
160
159
  http:localhost:8000/admin to browse the tables.
161
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
+
162
174
  ## Project Information
163
175
 
164
176
  * Documentation: https://ebird-api-data.readthedocs.io/en/latest/
@@ -112,7 +112,7 @@ EBIRD_API_KEY=<my api key>
112
112
  Now, download data from the API:
113
113
 
114
114
  ```shell
115
- python manage.py add_checklists --days 2 US-NY-109
115
+ python manage.py add_checklists --days -2 US-NY-109
116
116
  ```
117
117
 
118
118
  This loads all the checklists, submitted in the past two days by birders in
@@ -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.3.7"
3
+ version = "0.5.0"
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,8 +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.1",
17
- "ebird-codes>=0.0.2",
16
+ "ebird-api-requests==4.0.2",
18
17
  "lxml>=6.0.0",
19
18
  ]
20
19
  classifiers = [
@@ -65,7 +64,7 @@ dev-dependencies = [
65
64
  ]
66
65
 
67
66
  [tool.bumpversion]
68
- current_version = "0.3.7"
67
+ current_version = "0.5.0"
69
68
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
70
69
  serialize = ["{major}.{minor}.{patch}"]
71
70
  ignore_missing_version = false
@@ -0,0 +1 @@
1
+ __version__ = "0.5.0"
@@ -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
- }
@@ -0,0 +1,382 @@
1
+ """
2
+ FactoryBoy factories for eBird API data models.
3
+
4
+ These factories provide an easy way to create test data with realistic,
5
+ contextually appropriate values using Faker providers.
6
+
7
+ Usage:
8
+ from ebird.api.data.factories import ChecklistFactory, ObservationFactory
9
+
10
+ # Create a single checklist with observations
11
+ checklist = ChecklistFactory()
12
+
13
+ # Create multiple checklists
14
+ checklists = ChecklistFactory.create_batch(10)
15
+
16
+ # Create checklist with specific location
17
+ checklist = ChecklistFactory(location__name="Ria de Aveiro")
18
+
19
+ # Create observation with specific species
20
+ observation = ObservationFactory(species__species_code="houspa")
21
+ """
22
+
23
+ import factory
24
+ from factory import fuzzy
25
+ from factory.django import DjangoModelFactory
26
+ from faker import Faker
27
+
28
+ from ebird.api.data.models import (
29
+ Country,
30
+ State,
31
+ County,
32
+ Location,
33
+ Species,
34
+ Observer,
35
+ Checklist,
36
+ Observation,
37
+ )
38
+ from ebird.api.data.faker_providers import (
39
+ BirdProvider,
40
+ LocationProvider,
41
+ ObserverProvider,
42
+ )
43
+
44
+ # Initialize Faker with custom providers
45
+ fake = Faker()
46
+ fake.add_provider(BirdProvider)
47
+ fake.add_provider(LocationProvider)
48
+ fake.add_provider(ObserverProvider)
49
+
50
+
51
+ class CountryFactory(DjangoModelFactory):
52
+ """Factory for Country model."""
53
+
54
+ class Meta:
55
+ model = Country
56
+ django_get_or_create = ("code",)
57
+
58
+ code = "PT"
59
+ name = "Portugal"
60
+ place = "PT"
61
+
62
+
63
+ class StateFactory(DjangoModelFactory):
64
+ """Factory for State model."""
65
+
66
+ class Meta:
67
+ model = State
68
+ django_get_or_create = ("code",)
69
+
70
+ code = factory.LazyFunction(lambda: fake.portuguese_state_code())
71
+ name = factory.LazyAttribute(lambda obj: fake.portuguese_state_name())
72
+ place = factory.LazyAttribute(lambda obj: f"PT, {obj.name}")
73
+
74
+
75
+ class CountyFactory(DjangoModelFactory):
76
+ """Factory for County model."""
77
+
78
+ class Meta:
79
+ model = County
80
+
81
+ code = factory.Sequence(lambda n: f"PT-{n:02d}-CTY")
82
+ name = factory.Faker("city", locale="pt_PT")
83
+ place = factory.LazyAttribute(lambda obj: f"PT, {obj.name}")
84
+
85
+
86
+ class LocationFactory(DjangoModelFactory):
87
+ """Factory for Location model."""
88
+
89
+ class Meta:
90
+ model = Location
91
+
92
+ identifier = factory.Sequence(lambda n: f"L{n:06d}")
93
+ original = factory.LazyFunction(lambda: fake.location_name())
94
+ name = factory.LazyAttribute(lambda obj: obj.original)
95
+
96
+ country = factory.SubFactory(CountryFactory)
97
+ state = factory.SubFactory(StateFactory)
98
+ county = factory.SubFactory(CountyFactory)
99
+
100
+ latitude = factory.LazyFunction(lambda: fake.portugal_coordinates()[0])
101
+ longitude = factory.LazyFunction(lambda: fake.portugal_coordinates()[1])
102
+
103
+ url = factory.LazyAttribute(
104
+ lambda obj: f"https://ebird.org/hotspot/{obj.identifier}" if obj.hotspot else ""
105
+ )
106
+ hotspot = factory.LazyFunction(lambda: fake.location_is_hotspot())
107
+
108
+ data = factory.LazyFunction(lambda: {"habitat": fake.bird_habitat()})
109
+
110
+
111
+ class SpeciesFactory(DjangoModelFactory):
112
+ """Factory for Species model."""
113
+
114
+ class Meta:
115
+ model = Species
116
+ django_get_or_create = ("species_code",)
117
+
118
+ species_code = factory.LazyFunction(lambda: fake.bird_species_code())
119
+ taxon_order = factory.Faker("random_int", min=1, max=5000)
120
+ order = "Passeriformes"
121
+ category = "species"
122
+ family_code = factory.Faker("word")
123
+
124
+ common_name = factory.LazyAttribute(
125
+ lambda obj: f'{{"en": "{fake.bird_species_name(obj.species_code)}"}}'
126
+ )
127
+ scientific_name = factory.Faker("sentence", nb_words=2)
128
+
129
+ family_common_name = factory.LazyAttribute(
130
+ lambda obj: f'{{"en": "{obj.family_code.title()}"}}'
131
+ )
132
+ family_scientific_name = ""
133
+
134
+ data = factory.Dict({"habitats": factory.List([factory.LazyFunction(lambda: fake.bird_habitat())])})
135
+
136
+
137
+ class ObserverFactory(DjangoModelFactory):
138
+ """Factory for Observer model."""
139
+
140
+ class Meta:
141
+ model = Observer
142
+
143
+ identifier = factory.LazyFunction(lambda: fake.observer_identifier())
144
+ name = factory.LazyFunction(lambda: fake.observer_name())
145
+ original = factory.LazyAttribute(lambda obj: obj.name)
146
+ enabled = True
147
+
148
+ data = factory.LazyFunction(
149
+ lambda: {"experience": fake.observer_experience_level()}
150
+ )
151
+
152
+
153
+ class ChecklistFactory(DjangoModelFactory):
154
+ """
155
+ Factory for Checklist model.
156
+
157
+ Creates realistic checklists with appropriate effort metrics based on protocol.
158
+ """
159
+
160
+ class Meta:
161
+ model = Checklist
162
+
163
+ identifier = factory.Sequence(lambda n: f"S{n:08d}")
164
+
165
+ # Timestamps
166
+ added = factory.Faker("date_time_this_year", tzinfo=None)
167
+ edited = factory.LazyAttribute(lambda obj: obj.added)
168
+
169
+ # Relationships
170
+ country = factory.SubFactory(CountryFactory)
171
+ state = factory.SubFactory(StateFactory)
172
+ county = factory.SubFactory(CountyFactory)
173
+ location = factory.SubFactory(LocationFactory)
174
+ observer = factory.SubFactory(ObserverFactory)
175
+
176
+ # Counts (will be set by post_generation for observations)
177
+ observer_count = 1
178
+ species_count = factory.Faker("random_int", min=3, max=20)
179
+
180
+ # Date/Time
181
+ date = factory.LazyAttribute(lambda obj: obj.started.date() if obj.started else fake.date_this_year())
182
+ time = factory.LazyAttribute(lambda obj: obj.started.time() if obj.started else fake.time())
183
+ started = factory.Faker("date_time_this_year", tzinfo=None)
184
+
185
+ # Protocol and effort
186
+ protocol_code = factory.LazyFunction(lambda: fake.checklist_protocol_code())
187
+ project_code = "EBIRD"
188
+
189
+ duration = factory.LazyAttribute(
190
+ lambda obj: fake.checklist_duration(obj.protocol_code)
191
+ )
192
+ distance = factory.LazyAttribute(
193
+ lambda obj: fake.checklist_distance(obj.protocol_code)
194
+ )
195
+ area = None
196
+
197
+ complete = True
198
+ comments = ""
199
+
200
+ url = factory.LazyAttribute(
201
+ lambda obj: f"https://ebird.org/checklist/{obj.identifier}"
202
+ )
203
+
204
+ data = factory.Dict({})
205
+
206
+ @factory.post_generation
207
+ def with_observations(self, create, extracted, **kwargs):
208
+ """
209
+ Post-generation hook to create observations for the checklist.
210
+
211
+ Usage:
212
+ ChecklistFactory(with_observations=5) # Creates 5 observations
213
+ ChecklistFactory(with_observations=True) # Creates 3-10 observations
214
+ """
215
+ if not create:
216
+ return
217
+
218
+ if extracted:
219
+ # If boolean True, create random number of observations
220
+ if extracted is True:
221
+ num_observations = fake.random_int(min=3, max=10)
222
+ else:
223
+ num_observations = extracted
224
+
225
+ # Get habitat from location
226
+ habitat = self.location.data.get("habitat", "urban")
227
+
228
+ # Get appropriate species for habitat
229
+ species_codes = fake.bird_species_list_for_habitat(
230
+ habitat, min_species=num_observations, max_species=num_observations
231
+ )
232
+
233
+ for species_code in species_codes:
234
+ species, _ = Species.objects.get_or_create(
235
+ species_code=species_code,
236
+ defaults={
237
+ "common_name": f'{{"en": "{fake.bird_species_name(species_code)}"}}',
238
+ "scientific_name": f"Species {species_code}",
239
+ "taxon_order": fake.random_int(min=1, max=5000),
240
+ "order": "Passeriformes",
241
+ "category": "species",
242
+ "family_code": "family1",
243
+ }
244
+ )
245
+
246
+ ObservationFactory(
247
+ checklist=self,
248
+ species=species,
249
+ observer=self.observer,
250
+ country=self.country,
251
+ state=self.state,
252
+ county=self.county,
253
+ location=self.location,
254
+ date=self.date,
255
+ time=self.time,
256
+ started=self.started,
257
+ )
258
+
259
+ # Update species count
260
+ self.species_count = num_observations
261
+ self.save()
262
+
263
+
264
+ class ObservationFactory(DjangoModelFactory):
265
+ """
266
+ Factory for Observation model.
267
+
268
+ Creates realistic observations with appropriate counts and media flags.
269
+ """
270
+
271
+ class Meta:
272
+ model = Observation
273
+
274
+ identifier = factory.Sequence(lambda n: f"OBS{n:09d}")
275
+
276
+ # Timestamps
277
+ edited = factory.Faker("date_time_this_year", tzinfo=None)
278
+
279
+ # Relationships
280
+ checklist = factory.SubFactory(ChecklistFactory)
281
+ species = factory.SubFactory(SpeciesFactory)
282
+ observer = factory.SubFactory(ObserverFactory)
283
+
284
+ country = factory.LazyAttribute(lambda obj: obj.checklist.country)
285
+ state = factory.LazyAttribute(lambda obj: obj.checklist.state)
286
+ county = factory.LazyAttribute(lambda obj: obj.checklist.county)
287
+ location = factory.LazyAttribute(lambda obj: obj.checklist.location)
288
+
289
+ # Date/Time (inherit from checklist)
290
+ date = factory.LazyAttribute(lambda obj: obj.checklist.date)
291
+ time = factory.LazyAttribute(lambda obj: obj.checklist.time)
292
+ started = factory.LazyAttribute(lambda obj: obj.checklist.started)
293
+
294
+ # Count based on species
295
+ count = factory.LazyAttribute(
296
+ lambda obj: fake.bird_count(obj.species.species_code)
297
+ )
298
+
299
+ # Media flags (realistic probabilities)
300
+ audio = factory.LazyFunction(lambda: fake.observation_has_media()["audio"])
301
+ photo = factory.LazyFunction(lambda: fake.observation_has_media()["photo"])
302
+ video = factory.LazyFunction(lambda: fake.observation_has_media()["video"])
303
+
304
+ approved = True
305
+ reason = ""
306
+ comments = ""
307
+
308
+ urn = factory.LazyAttribute(lambda obj: f"urn:observation:{obj.identifier}")
309
+
310
+ data = factory.Dict({})
311
+
312
+
313
+ # Convenience functions for common scenarios
314
+
315
+ def create_coastal_checklist(**kwargs):
316
+ """Create a checklist at a coastal location with appropriate species."""
317
+ location = LocationFactory(data={"habitat": "coastal"})
318
+ checklist = ChecklistFactory(
319
+ location=location,
320
+ with_observations=True,
321
+ **kwargs
322
+ )
323
+ return checklist
324
+
325
+
326
+ def create_wetland_checklist(**kwargs):
327
+ """Create a checklist at a wetland location with appropriate species."""
328
+ location = LocationFactory(data={"habitat": "coastal_wetland"})
329
+ checklist = ChecklistFactory(
330
+ location=location,
331
+ with_observations=True,
332
+ **kwargs
333
+ )
334
+ return checklist
335
+
336
+
337
+ def create_urban_checklist(**kwargs):
338
+ """Create a checklist at an urban location with appropriate species."""
339
+ location = LocationFactory(data={"habitat": "urban"})
340
+ checklist = ChecklistFactory(
341
+ location=location,
342
+ with_observations=True,
343
+ **kwargs
344
+ )
345
+ return checklist
346
+
347
+
348
+ def create_grassland_checklist(**kwargs):
349
+ """Create a checklist at a grassland location with steppe specialists."""
350
+ location = LocationFactory(data={"habitat": "grassland"})
351
+ checklist = ChecklistFactory(
352
+ location=location,
353
+ with_observations=True,
354
+ **kwargs
355
+ )
356
+ return checklist
357
+
358
+
359
+ def create_expert_observer_checklist(**kwargs):
360
+ """Create a checklist by an expert observer with many species."""
361
+ observer = ObserverFactory(data={"experience": "expert"})
362
+ checklist = ChecklistFactory(
363
+ observer=observer,
364
+ duration=fuzzy.FuzzyInteger(120, 300).fuzz(),
365
+ species_count=fuzzy.FuzzyInteger(15, 30).fuzz(),
366
+ with_observations=fuzzy.FuzzyInteger(15, 30).fuzz(),
367
+ **kwargs
368
+ )
369
+ return checklist
370
+
371
+
372
+ def create_beginner_observer_checklist(**kwargs):
373
+ """Create a checklist by a beginner observer with fewer species."""
374
+ observer = ObserverFactory(data={"experience": "beginner"})
375
+ checklist = ChecklistFactory(
376
+ observer=observer,
377
+ duration=fuzzy.FuzzyInteger(30, 90).fuzz(),
378
+ species_count=fuzzy.FuzzyInteger(3, 8).fuzz(),
379
+ with_observations=fuzzy.FuzzyInteger(3, 8).fuzz(),
380
+ **kwargs
381
+ )
382
+ return checklist