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.
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/PKG-INFO +16 -4
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/README.md +14 -1
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/pyproject.toml +3 -4
- ebird_api_data-0.5.0/src/ebird/api/data/__init__.py +1 -0
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/admin.py +2 -15
- ebird_api_data-0.5.0/src/ebird/api/data/factories.py +382 -0
- ebird_api_data-0.5.0/src/ebird/api/data/faker_providers.py +345 -0
- ebird_api_data-0.5.0/src/ebird/api/data/fixtures/README.md +354 -0
- ebird_api_data-0.5.0/src/ebird/api/data/fixtures/generate_portugal_data.py +527 -0
- ebird_api_data-0.5.0/src/ebird/api/data/fixtures/portugal_observations.json +139905 -0
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/loaders.py +198 -81
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/management/commands/add_checklists.py +36 -21
- ebird_api_data-0.5.0/src/ebird/api/data/migrations/0002_expand_filter.py +29 -0
- ebird_api_data-0.5.0/src/ebird/api/data/migrations/0003_remove_checklist_published_and_more.py +24 -0
- ebird_api_data-0.5.0/src/ebird/api/data/migrations/0004_remove_observer_multiple.py +17 -0
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/models/__init__.py +0 -2
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/models/checklist.py +0 -5
- ebird_api_data-0.5.0/src/ebird/api/data/models/models.md +192 -0
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/models/observation.py +0 -5
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/models/observer.py +0 -6
- ebird_api_data-0.3.7/src/ebird/api/data/__init__.py +0 -1
- ebird_api_data-0.3.7/src/ebird/api/data/models/filter.py +0 -79
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/.gitignore +0 -0
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/LICENSE +0 -0
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/apps.py +0 -0
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/fields.py +0 -0
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/locale/en/LC_MESSAGES/django.mo +0 -0
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/locale/en/LC_MESSAGES/django.po +0 -0
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/management/__init__.py +0 -0
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/management/commands/__init__.py +0 -0
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/migrations/0001_initial.py +0 -0
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/migrations/__init__.py +0 -0
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/models/country.py +0 -0
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/models/county.py +0 -0
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/models/location.py +0 -0
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/models/species.py +0 -0
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/models/state.py +0 -0
- {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
- {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
- {ebird_api_data-0.3.7 → ebird_api_data-0.5.0}/src/ebird/api/data/templates/data/widgets/translation_field.html +0 -0
- {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
|
+
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
|
|
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
|
+
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
|
|
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.
|
|
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", "
|
|
269
|
+
list_display = ("name", "identifier", "enabled")
|
|
270
270
|
ordering = ("name",)
|
|
271
271
|
search_fields = ("name", "identifier")
|
|
272
|
-
list_filter = ("
|
|
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
|