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.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -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
@@ -0,0 +1,8 @@
1
+ from django.apps import AppConfig
2
+ from django.utils.translation import gettext_lazy as _
3
+
4
+
5
+ class Config(AppConfig):
6
+ label = "data"
7
+ name = "ebird.api.data"
8
+ verbose_name = _("Data")
@@ -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)