ebird-api-data 0.1.0__py3-none-any.whl
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/__init__.py +1 -0
- ebird/api/data/admin.py +394 -0
- ebird/api/data/apps.py +8 -0
- ebird/api/data/fields.py +47 -0
- ebird/api/data/loaders.py +485 -0
- ebird/api/data/management/__init__.py +0 -0
- ebird/api/data/management/commands/__init__.py +0 -0
- ebird/api/data/management/commands/add_checklists.py +124 -0
- ebird/api/data/migrations/0001_initial.py +927 -0
- ebird/api/data/migrations/__init__.py +0 -0
- ebird/api/data/models/__init__.py +21 -0
- ebird/api/data/models/checklist.py +178 -0
- ebird/api/data/models/country.py +29 -0
- ebird/api/data/models/county.py +29 -0
- ebird/api/data/models/filter.py +71 -0
- ebird/api/data/models/location.py +153 -0
- ebird/api/data/models/observation.py +189 -0
- ebird/api/data/models/observer.py +48 -0
- ebird/api/data/models/species.py +118 -0
- ebird/api/data/models/state.py +29 -0
- ebird/api/data/widgets.py +46 -0
- ebird_api_data-0.1.0.dist-info/METADATA +206 -0
- ebird_api_data-0.1.0.dist-info/RECORD +25 -0
- ebird_api_data-0.1.0.dist-info/WHEEL +5 -0
- ebird_api_data-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
ebird/api/data/admin.py
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
from django import forms
|
|
2
|
+
from django.conf import settings
|
|
3
|
+
from django.contrib import admin, messages
|
|
4
|
+
from django.contrib.admin.widgets import AutocompleteSelect
|
|
5
|
+
from django.contrib.auth.mixins import PermissionRequiredMixin
|
|
6
|
+
from django.db.models import DecimalField, IntegerField, TextField
|
|
7
|
+
from django.forms import ModelForm, Textarea, TextInput
|
|
8
|
+
from django.urls import path, reverse, reverse_lazy
|
|
9
|
+
from django.utils.html import format_html
|
|
10
|
+
from django.utils.translation import gettext_lazy as _
|
|
11
|
+
from django.views import generic
|
|
12
|
+
|
|
13
|
+
from . import models
|
|
14
|
+
from .fields import TranslationCharField, TranslationTextField
|
|
15
|
+
from .loaders import APILoader
|
|
16
|
+
from .models import Observation, Species
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ObservationInline(admin.TabularInline):
|
|
20
|
+
model = models.Observation
|
|
21
|
+
fields = ("observation", "common_name", "scientific_name", "count", "comments")
|
|
22
|
+
ordering = ("species__order",)
|
|
23
|
+
readonly_fields = ("observation", "common_name", "scientific_name", "count", "comments")
|
|
24
|
+
extra = 0
|
|
25
|
+
|
|
26
|
+
@admin.display(description=_("Observation"))
|
|
27
|
+
def observation(self, obj):
|
|
28
|
+
url = reverse("admin:data_observation_change", kwargs={"object_id": obj.id})
|
|
29
|
+
return format_html('<a href="{}">{}</a>', url, obj.identifier)
|
|
30
|
+
|
|
31
|
+
@admin.display(description=_("Common name"))
|
|
32
|
+
def common_name(self, obj):
|
|
33
|
+
return obj.species.get_common_name()
|
|
34
|
+
|
|
35
|
+
@admin.display(description=_("Scientific name"))
|
|
36
|
+
def scientific_name(self, obj):
|
|
37
|
+
return format_html("<i>{}</i>", obj.species.scientific_name)
|
|
38
|
+
|
|
39
|
+
def get_queryset(self, request):
|
|
40
|
+
return (
|
|
41
|
+
super()
|
|
42
|
+
.get_queryset(request)
|
|
43
|
+
.select_related("species")
|
|
44
|
+
.order_by("species__taxon_order")
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@admin.register(models.Checklist)
|
|
49
|
+
class ChecklistAdmin(admin.ModelAdmin):
|
|
50
|
+
list_display = (
|
|
51
|
+
"identifier",
|
|
52
|
+
"date",
|
|
53
|
+
"time",
|
|
54
|
+
"species_count",
|
|
55
|
+
"location",
|
|
56
|
+
"observer",
|
|
57
|
+
)
|
|
58
|
+
ordering = ("-started",)
|
|
59
|
+
search_fields = ("identifier", "location__name", "observer__name")
|
|
60
|
+
autocomplete_fields = ("location", "observer")
|
|
61
|
+
inlines = [ObservationInline]
|
|
62
|
+
formfield_overrides = {
|
|
63
|
+
DecimalField: {
|
|
64
|
+
"widget": TextInput(),
|
|
65
|
+
},
|
|
66
|
+
IntegerField: {
|
|
67
|
+
"widget": TextInput(),
|
|
68
|
+
},
|
|
69
|
+
TextField: {
|
|
70
|
+
"widget": TextInput(attrs={"class": "vTextField"}),
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
readonly_fields = ("identifier", "edited")
|
|
74
|
+
fields = (
|
|
75
|
+
"date",
|
|
76
|
+
"time",
|
|
77
|
+
"location",
|
|
78
|
+
"observer",
|
|
79
|
+
"species_count",
|
|
80
|
+
"complete",
|
|
81
|
+
"observer_count",
|
|
82
|
+
"protocol_code",
|
|
83
|
+
"duration",
|
|
84
|
+
"distance",
|
|
85
|
+
"comments",
|
|
86
|
+
"data",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
|
90
|
+
field = super().formfield_for_dbfield(db_field, request, **kwargs)
|
|
91
|
+
if db_field.name == "comments":
|
|
92
|
+
field.widget = Textarea(attrs={"rows": 5, "class": "vLargeTextField"})
|
|
93
|
+
elif db_field.name == "data":
|
|
94
|
+
field.widget = Textarea(attrs={"rows": 10, "class": "vLargeTextField"})
|
|
95
|
+
|
|
96
|
+
return field
|
|
97
|
+
|
|
98
|
+
def save_model(self, request, obj, form, change):
|
|
99
|
+
if "location" in form.changed_data:
|
|
100
|
+
location = obj.location
|
|
101
|
+
obj.country = location.country
|
|
102
|
+
obj.state = location.state
|
|
103
|
+
obj.county = location.county
|
|
104
|
+
super().save_model(request, obj, form, change)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@admin.register(models.Country)
|
|
108
|
+
class CountryAdmin(admin.ModelAdmin):
|
|
109
|
+
list_display = ("code", "name")
|
|
110
|
+
ordering = ("code",)
|
|
111
|
+
readonly_fields = ("code",)
|
|
112
|
+
formfield_overrides = {
|
|
113
|
+
TextField: {
|
|
114
|
+
"widget": TextInput(attrs={"class": "vTextField"}),
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@admin.register(models.State)
|
|
120
|
+
class StateAdmin(admin.ModelAdmin):
|
|
121
|
+
list_display = ("code", "name")
|
|
122
|
+
ordering = ("code",)
|
|
123
|
+
readonly_fields = ("code",)
|
|
124
|
+
formfield_overrides = {
|
|
125
|
+
TextField: {
|
|
126
|
+
"widget": TextInput(attrs={"class": "vTextField"}),
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@admin.register(models.County)
|
|
132
|
+
class CountyAdmin(admin.ModelAdmin):
|
|
133
|
+
list_display = ("code", "name")
|
|
134
|
+
ordering = ("code",)
|
|
135
|
+
readonly_fields = ("code",)
|
|
136
|
+
formfield_overrides = {
|
|
137
|
+
TextField: {
|
|
138
|
+
"widget": TextInput(attrs={"class": "vTextField"}),
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@admin.register(models.Location)
|
|
144
|
+
class LocationAdmin(admin.ModelAdmin):
|
|
145
|
+
list_display = ("identifier", "names", "county", "state", "country")
|
|
146
|
+
list_select_related = ("country", "county", "state")
|
|
147
|
+
ordering = ("-identifier",)
|
|
148
|
+
search_fields = (
|
|
149
|
+
"identifier",
|
|
150
|
+
"name",
|
|
151
|
+
"county__name",
|
|
152
|
+
"state__name",
|
|
153
|
+
"country__name",
|
|
154
|
+
)
|
|
155
|
+
readonly_fields = ("identifier",)
|
|
156
|
+
|
|
157
|
+
@admin.display(description=_("Name / Byname"))
|
|
158
|
+
def names(self, obj):
|
|
159
|
+
return format_html("%s<br/>%s" % (obj.name, obj.byname))
|
|
160
|
+
|
|
161
|
+
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
|
162
|
+
field = super().formfield_for_dbfield(db_field, request, **kwargs)
|
|
163
|
+
if db_field.name == "byname":
|
|
164
|
+
field.widget = TextInput(attrs={"class": "vLargeTextField"})
|
|
165
|
+
elif db_field.name == "name":
|
|
166
|
+
field.widget = TextInput(attrs={"class": "vLargeTextField"})
|
|
167
|
+
elif db_field.name == "latitude":
|
|
168
|
+
field.widget = TextInput()
|
|
169
|
+
elif db_field.name == "longitude":
|
|
170
|
+
field.widget = TextInput()
|
|
171
|
+
elif db_field.name == "data":
|
|
172
|
+
field.widget = Textarea(attrs={"rows": 10, "class": "vLargeTextField"})
|
|
173
|
+
return field
|
|
174
|
+
|
|
175
|
+
def save_model(self, request, obj, form, change):
|
|
176
|
+
super().save_model(request, obj, form, change)
|
|
177
|
+
if "country" in form.changed_data:
|
|
178
|
+
models.Checklist.objects.filter(location=obj).update(country=obj.country)
|
|
179
|
+
models.Observation.objects.filter(location=obj).update(country=obj.country)
|
|
180
|
+
if "state" in form.changed_data:
|
|
181
|
+
models.Checklist.objects.filter(location=obj).update(state=obj.state)
|
|
182
|
+
models.Observation.objects.filter(location=obj).update(state=obj.state)
|
|
183
|
+
if "county" in form.changed_data:
|
|
184
|
+
models.Checklist.objects.filter(location=obj).update(county=obj.county)
|
|
185
|
+
models.Observation.objects.filter(location=obj).update(county=obj.county)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class ChangeSpeciesForm(forms.Form):
|
|
189
|
+
species = forms.ModelChoiceField(
|
|
190
|
+
queryset=Species.objects.all(),
|
|
191
|
+
widget=AutocompleteSelect(Observation._meta.get_field("species"), admin.site),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class ObservationForm(ModelForm):
|
|
196
|
+
reason = TranslationTextField(required=False)
|
|
197
|
+
|
|
198
|
+
class Meta:
|
|
199
|
+
fields = "__all__"
|
|
200
|
+
|
|
201
|
+
def __init__(self, *args, **kwargs):
|
|
202
|
+
super().__init__(*args, **kwargs)
|
|
203
|
+
self.fields["reason"].widget.attrs.update({"rows": 2})
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@admin.register(models.Observation)
|
|
207
|
+
class ObservationAdmin(admin.ModelAdmin):
|
|
208
|
+
list_display = (
|
|
209
|
+
"common_name",
|
|
210
|
+
"count",
|
|
211
|
+
"date",
|
|
212
|
+
"time",
|
|
213
|
+
"location",
|
|
214
|
+
"observer",
|
|
215
|
+
)
|
|
216
|
+
search_fields = (
|
|
217
|
+
"identifier",
|
|
218
|
+
"species__common_name",
|
|
219
|
+
"species__scientific_name",
|
|
220
|
+
"observer__name",
|
|
221
|
+
)
|
|
222
|
+
list_filter = (
|
|
223
|
+
"audio",
|
|
224
|
+
"photo",
|
|
225
|
+
"video",
|
|
226
|
+
"approved",
|
|
227
|
+
)
|
|
228
|
+
ordering = ("-checklist__started",)
|
|
229
|
+
form = ObservationForm
|
|
230
|
+
autocomplete_fields = ("checklist", "location", "observer", "species")
|
|
231
|
+
readonly_fields = ("identifier", "edited")
|
|
232
|
+
fields = (
|
|
233
|
+
"species",
|
|
234
|
+
"count",
|
|
235
|
+
"audio",
|
|
236
|
+
"photo",
|
|
237
|
+
"video",
|
|
238
|
+
"comments",
|
|
239
|
+
"checklist",
|
|
240
|
+
"location",
|
|
241
|
+
"observer",
|
|
242
|
+
"edited",
|
|
243
|
+
"approved",
|
|
244
|
+
"reason",
|
|
245
|
+
"data",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
@admin.display(description=_("Species"))
|
|
249
|
+
def common_name(self, obj):
|
|
250
|
+
return obj.species.get_common_name()
|
|
251
|
+
|
|
252
|
+
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
|
253
|
+
field = super().formfield_for_dbfield(db_field, request, **kwargs)
|
|
254
|
+
if db_field.name == "count":
|
|
255
|
+
field.widget = TextInput()
|
|
256
|
+
elif db_field.name == "comments":
|
|
257
|
+
field.widget = Textarea(attrs={"rows": 5, "class": "vLargeTextField"})
|
|
258
|
+
elif db_field.name == "data":
|
|
259
|
+
field.widget = Textarea(attrs={"rows": 5, "class": "vLargeTextField"})
|
|
260
|
+
return field
|
|
261
|
+
|
|
262
|
+
def save_model(self, request, obj, form, change):
|
|
263
|
+
if "location" in form.changed_data:
|
|
264
|
+
location = obj.location
|
|
265
|
+
obj.country = location.country
|
|
266
|
+
obj.state = location.state
|
|
267
|
+
obj.county = location.county
|
|
268
|
+
|
|
269
|
+
super().save_model(request, obj, form, change)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@admin.register(models.Observer)
|
|
273
|
+
class ObserverAdmin(admin.ModelAdmin):
|
|
274
|
+
list_display = ("names", "identifier", "multiple", "enabled")
|
|
275
|
+
ordering = ("name",)
|
|
276
|
+
search_fields = ("name", "identifier")
|
|
277
|
+
list_filter = ("multiple", "enabled")
|
|
278
|
+
form = ObservationForm
|
|
279
|
+
|
|
280
|
+
@admin.display(description=_("Name / Byname"))
|
|
281
|
+
def names(self, obj):
|
|
282
|
+
return format_html("%s<br/>%s" % (obj.name, obj.byname))
|
|
283
|
+
|
|
284
|
+
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
|
285
|
+
field = super().formfield_for_dbfield(db_field, request, **kwargs)
|
|
286
|
+
if db_field.name == "byname":
|
|
287
|
+
field.widget = TextInput(attrs={"class": "vTextField"})
|
|
288
|
+
elif db_field.name == "name":
|
|
289
|
+
field.widget = TextInput(attrs={"class": "vTextField"})
|
|
290
|
+
elif db_field.name == "data":
|
|
291
|
+
field.widget = Textarea(attrs={"rows": 5, "class": "vLargeTextField"})
|
|
292
|
+
return field
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class FetchSpeciesForm(forms.Form):
|
|
296
|
+
species_code = forms.CharField(
|
|
297
|
+
help_text=_("The eBird code for the species, e.g. cangoo, for Canada Goose.")
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class FetchSpeciesView(PermissionRequiredMixin, generic.FormView):
|
|
302
|
+
form_class = FetchSpeciesForm
|
|
303
|
+
permission_required = "data.add_species"
|
|
304
|
+
success_url = reverse_lazy("admin:data_species_changelist")
|
|
305
|
+
template_name = "admin/data/species/fetch_species.html"
|
|
306
|
+
|
|
307
|
+
def form_valid(self, form):
|
|
308
|
+
self.fetch_species(form)
|
|
309
|
+
return super().form_valid(form)
|
|
310
|
+
|
|
311
|
+
def fetch_species(self, form):
|
|
312
|
+
species_code = form.cleaned_data["species_code"]
|
|
313
|
+
key: str = getattr(settings, "EBIRD_API_KEY")
|
|
314
|
+
locales: dict = getattr(settings, "EBIRD_LOCALES")
|
|
315
|
+
loader = APILoader(key, locales)
|
|
316
|
+
species = loader.add_species(species_code)
|
|
317
|
+
messages.add_message(
|
|
318
|
+
self.request,
|
|
319
|
+
messages.INFO,
|
|
320
|
+
"%s was added to the Species list" % species.get_common_name(),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class SpeciesForm(ModelForm):
|
|
325
|
+
common_name = TranslationCharField()
|
|
326
|
+
family_common_name = TranslationCharField(required=False)
|
|
327
|
+
|
|
328
|
+
class Meta:
|
|
329
|
+
fields = "__all__"
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@admin.register(models.Species)
|
|
333
|
+
class SpeciesAdmin(admin.ModelAdmin):
|
|
334
|
+
list_display = (
|
|
335
|
+
"get_common_name",
|
|
336
|
+
"scientific_name",
|
|
337
|
+
"get_family_common_name",
|
|
338
|
+
"family_scientific_name",
|
|
339
|
+
"order",
|
|
340
|
+
)
|
|
341
|
+
ordering = ("order",)
|
|
342
|
+
search_fields = ("common_name", "scientific_name")
|
|
343
|
+
form = SpeciesForm
|
|
344
|
+
formfield_overrides = {
|
|
345
|
+
TextField: {
|
|
346
|
+
"widget": TextInput(attrs={"class": "vTextField"}),
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
readonly_fields = ("taxon_order",)
|
|
350
|
+
fields = (
|
|
351
|
+
"common_name",
|
|
352
|
+
"scientific_name",
|
|
353
|
+
"species_code",
|
|
354
|
+
"order",
|
|
355
|
+
"category",
|
|
356
|
+
"family_common_name",
|
|
357
|
+
"family_scientific_name",
|
|
358
|
+
"family_code",
|
|
359
|
+
"data",
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
def formfield_for_dbfield(self, db_field, request, **kwargs):
|
|
363
|
+
field = super().formfield_for_dbfield(db_field, request, **kwargs)
|
|
364
|
+
if db_field.name == "data":
|
|
365
|
+
field.widget = Textarea(attrs={"rows": 5, "class": "vLargeTextField"})
|
|
366
|
+
return field
|
|
367
|
+
|
|
368
|
+
def get_urls(self):
|
|
369
|
+
return [
|
|
370
|
+
path(
|
|
371
|
+
"fetch/",
|
|
372
|
+
self.admin_site.admin_view(FetchSpeciesView.as_view()),
|
|
373
|
+
name="data_species_fetch",
|
|
374
|
+
),
|
|
375
|
+
*super().get_urls(),
|
|
376
|
+
]
|
|
377
|
+
|
|
378
|
+
def changelist_view(self, request, extra_context=None):
|
|
379
|
+
extra_context = extra_context or {}
|
|
380
|
+
extra_context["fetch_species_url"] = reverse("admin:data_species_fetch")
|
|
381
|
+
return super().changelist_view(request, extra_context=extra_context)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@admin.register(models.Filter)
|
|
385
|
+
class FilterAdmin(admin.ModelAdmin):
|
|
386
|
+
list_display = ("name", "enabled")
|
|
387
|
+
search_fields = ("name", "species__common_name")
|
|
388
|
+
list_filter = ("enabled",)
|
|
389
|
+
autocomplete_fields = ("species", "location", "update_species")
|
|
390
|
+
formfield_overrides = {
|
|
391
|
+
TextField: {
|
|
392
|
+
"widget": TextInput(attrs={"class": "vTextField"}),
|
|
393
|
+
}
|
|
394
|
+
}
|
ebird/api/data/apps.py
ADDED
ebird/api/data/fields.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from django import forms
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.utils.translation import gettext_lazy as _
|
|
6
|
+
|
|
7
|
+
from .widgets import TranslationTextarea, TranslationTextInput
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TranslationField(forms.MultiValueField):
|
|
11
|
+
def __init__(self, **kwargs):
|
|
12
|
+
# Define one set of messages for all fields.
|
|
13
|
+
kwargs["error_messages"] = {
|
|
14
|
+
"required": "Enter the text for the translation",
|
|
15
|
+
"incomplete": "Enter text for every translation",
|
|
16
|
+
}
|
|
17
|
+
super().__init__(require_all_fields=True, **kwargs)
|
|
18
|
+
|
|
19
|
+
def compress(self, data_list):
|
|
20
|
+
codes = [code for code, language in settings.LANGUAGES]
|
|
21
|
+
values = zip(codes, data_list)
|
|
22
|
+
translations = {code: value for code, value in values}
|
|
23
|
+
return json.dumps(translations)
|
|
24
|
+
|
|
25
|
+
def has_changed(self, initial, data):
|
|
26
|
+
# If the value from the database is an empty string, rather than
|
|
27
|
+
# the string representation of an empty dict, set the value to None,
|
|
28
|
+
# so it decompresses correctly.
|
|
29
|
+
return super().has_changed(initial or None, data)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TranslationCharField(TranslationField):
|
|
33
|
+
def __init__(self, **kwargs):
|
|
34
|
+
fields = [
|
|
35
|
+
forms.CharField(label=_(language)) for code, language in settings.LANGUAGES
|
|
36
|
+
]
|
|
37
|
+
widget = TranslationTextInput
|
|
38
|
+
super().__init__(fields=fields, widget=widget, **kwargs)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TranslationTextField(TranslationField):
|
|
42
|
+
def __init__(self, **kwargs):
|
|
43
|
+
fields = [
|
|
44
|
+
forms.CharField(label=_(language)) for code, language in settings.LANGUAGES
|
|
45
|
+
]
|
|
46
|
+
widget = TranslationTextarea
|
|
47
|
+
super().__init__(fields=fields, widget=widget, **kwargs)
|