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
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .checklist import Checklist
|
|
2
|
+
from .country import Country
|
|
3
|
+
from .county import County
|
|
4
|
+
from .filter import Filter
|
|
5
|
+
from .location import Location
|
|
6
|
+
from .observation import Observation
|
|
7
|
+
from .observer import Observer
|
|
8
|
+
from .species import Species
|
|
9
|
+
from .state import State
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
"Checklist",
|
|
13
|
+
"Country",
|
|
14
|
+
"County",
|
|
15
|
+
"Filter",
|
|
16
|
+
"Location",
|
|
17
|
+
"Observation",
|
|
18
|
+
"Observer",
|
|
19
|
+
"Species",
|
|
20
|
+
"State",
|
|
21
|
+
)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Checklist(models.Model):
|
|
6
|
+
class Meta:
|
|
7
|
+
verbose_name = _("checklist")
|
|
8
|
+
verbose_name_plural = _("checklists")
|
|
9
|
+
|
|
10
|
+
created = models.DateTimeField(
|
|
11
|
+
blank=True,
|
|
12
|
+
null=True,
|
|
13
|
+
help_text=_("The date and time the checklist was added to eBird."),
|
|
14
|
+
verbose_name=_("created"),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
edited = models.DateTimeField(
|
|
18
|
+
blank=True,
|
|
19
|
+
null=True,
|
|
20
|
+
help_text=_("The date and time the eBird checklist was last edited."),
|
|
21
|
+
verbose_name=_("edited"),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
published = models.BooleanField(
|
|
25
|
+
help_text=_("Is the checklist published?"),
|
|
26
|
+
verbose_name=_("published"),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
identifier = models.TextField(
|
|
30
|
+
unique=True,
|
|
31
|
+
verbose_name=_("identifier"),
|
|
32
|
+
help_text=_("The unique identifier for the checklist."),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
country = models.ForeignKey(
|
|
36
|
+
"data.Country",
|
|
37
|
+
related_name="checklists",
|
|
38
|
+
on_delete=models.PROTECT,
|
|
39
|
+
verbose_name=_("country"),
|
|
40
|
+
help_text=_("The country where the checklist was made."),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
state = models.ForeignKey(
|
|
44
|
+
"data.State",
|
|
45
|
+
related_name="checklists",
|
|
46
|
+
on_delete=models.PROTECT,
|
|
47
|
+
verbose_name=_("state"),
|
|
48
|
+
help_text=_("The state where the checklist was made."),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
county = models.ForeignKey(
|
|
52
|
+
"data.County",
|
|
53
|
+
blank=True,
|
|
54
|
+
null=True,
|
|
55
|
+
related_name="checklists",
|
|
56
|
+
on_delete=models.PROTECT,
|
|
57
|
+
verbose_name=_("county"),
|
|
58
|
+
help_text=_("The county where the checklist was made."),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
location = models.ForeignKey(
|
|
62
|
+
"data.Location",
|
|
63
|
+
related_name="checklists",
|
|
64
|
+
on_delete=models.PROTECT,
|
|
65
|
+
verbose_name=_("location"),
|
|
66
|
+
help_text=_("The location where checklist was made."),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
observer = models.ForeignKey(
|
|
70
|
+
"data.Observer",
|
|
71
|
+
related_name="checklists",
|
|
72
|
+
on_delete=models.PROTECT,
|
|
73
|
+
verbose_name=_("observer"),
|
|
74
|
+
help_text=_("The person who submitted the checklist."),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
observer_count = models.IntegerField(
|
|
78
|
+
blank=True,
|
|
79
|
+
null=True,
|
|
80
|
+
verbose_name=_("observer count"),
|
|
81
|
+
help_text=_("The total number of observers."),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
species_count = models.IntegerField(
|
|
85
|
+
blank=True,
|
|
86
|
+
null=True,
|
|
87
|
+
verbose_name=_("species count"),
|
|
88
|
+
help_text=_("The number of species reported."),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
date = models.DateField(
|
|
92
|
+
db_index=True,
|
|
93
|
+
verbose_name=_("date"),
|
|
94
|
+
help_text=_("The date the checklist was started."),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
time = models.TimeField(
|
|
98
|
+
blank=True,
|
|
99
|
+
null=True,
|
|
100
|
+
verbose_name=_("time"),
|
|
101
|
+
help_text=_("The time the checklist was started."),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
started = models.DateTimeField(
|
|
105
|
+
blank=True,
|
|
106
|
+
db_index=True,
|
|
107
|
+
null=True,
|
|
108
|
+
verbose_name=_("date & time"),
|
|
109
|
+
help_text=_("The date and time the checklist was started."),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
protocol_code = models.TextField(
|
|
113
|
+
blank=True,
|
|
114
|
+
verbose_name=_("protocol code"),
|
|
115
|
+
help_text=_("The code used to identify the protocol."),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
project_code = models.TextField(
|
|
119
|
+
blank=True,
|
|
120
|
+
verbose_name=_("project code"),
|
|
121
|
+
help_text=_("The code used to identify the project (portal)."),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
duration = models.IntegerField(
|
|
125
|
+
blank=True,
|
|
126
|
+
null=True,
|
|
127
|
+
verbose_name=_("duration"),
|
|
128
|
+
help_text=_("The number of minutes spent counting."),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
distance = models.DecimalField(
|
|
132
|
+
blank=True,
|
|
133
|
+
null=True,
|
|
134
|
+
decimal_places=3,
|
|
135
|
+
max_digits=6,
|
|
136
|
+
verbose_name=_("distance"),
|
|
137
|
+
help_text=_("The distance, in metres, covered while travelling."),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
area = models.DecimalField(
|
|
141
|
+
blank=True,
|
|
142
|
+
null=True,
|
|
143
|
+
decimal_places=3,
|
|
144
|
+
max_digits=6,
|
|
145
|
+
verbose_name=_("area"),
|
|
146
|
+
help_text=_("The area covered, in hectares."),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
complete = models.BooleanField(
|
|
150
|
+
default=False,
|
|
151
|
+
verbose_name=_("complete"),
|
|
152
|
+
help_text=_("All species seen are reported."),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
comments = models.TextField(
|
|
156
|
+
blank=True,
|
|
157
|
+
verbose_name=_("comments"),
|
|
158
|
+
help_text=_("Any comments about the checklist."),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
url = models.URLField(
|
|
162
|
+
blank=True,
|
|
163
|
+
verbose_name=_("url"),
|
|
164
|
+
help_text=_("URL where the original checklist can be viewed."),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
data = models.JSONField(
|
|
168
|
+
verbose_name=_("Data"),
|
|
169
|
+
help_text=_("Data describing a Checklist."),
|
|
170
|
+
default=dict,
|
|
171
|
+
blank=True,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def __repr__(self) -> str:
|
|
175
|
+
return str(self.identifier)
|
|
176
|
+
|
|
177
|
+
def __str__(self) -> str:
|
|
178
|
+
return str(self.identifier)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Country(models.Model):
|
|
6
|
+
class Meta:
|
|
7
|
+
verbose_name = _("country")
|
|
8
|
+
verbose_name_plural = _("countries")
|
|
9
|
+
|
|
10
|
+
code = models.CharField(
|
|
11
|
+
max_length=2,
|
|
12
|
+
db_index=True,
|
|
13
|
+
verbose_name=_("code"),
|
|
14
|
+
help_text=_("The code used to identify the country."),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
name = models.TextField(
|
|
18
|
+
verbose_name=_("name"), help_text=_("The name of the country.")
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
place = models.TextField(
|
|
22
|
+
verbose_name=_("place"), help_text=_("The hierarchical name of the country.")
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def __repr__(self) -> str:
|
|
26
|
+
return str(self.code)
|
|
27
|
+
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
return str(self.name)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class County(models.Model):
|
|
6
|
+
class Meta:
|
|
7
|
+
verbose_name = _("county")
|
|
8
|
+
verbose_name_plural = _("counties")
|
|
9
|
+
|
|
10
|
+
code = models.CharField(
|
|
11
|
+
max_length=10,
|
|
12
|
+
db_index=True,
|
|
13
|
+
verbose_name=_("code"),
|
|
14
|
+
help_text=_("The code used to identify the county."),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
name = models.TextField(
|
|
18
|
+
verbose_name=_("name"), help_text=_("The name of the county.")
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
place = models.TextField(
|
|
22
|
+
verbose_name=_("place"), help_text=_("The hierarchical name of the county.")
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def __repr__(self) -> str:
|
|
26
|
+
return str(self.code)
|
|
27
|
+
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
return str(self.name)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from django.db import models
|
|
4
|
+
from django.utils.translation import gettext_lazy as _
|
|
5
|
+
|
|
6
|
+
from .observation import Observation
|
|
7
|
+
|
|
8
|
+
log = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Filter(models.Model):
|
|
12
|
+
class Meta:
|
|
13
|
+
verbose_name = _("filter")
|
|
14
|
+
verbose_name_plural = _("filters")
|
|
15
|
+
|
|
16
|
+
enabled = models.BooleanField(
|
|
17
|
+
help_text=_("Is the filter active?"),
|
|
18
|
+
verbose_name=_("enabled"),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
name = models.TextField(
|
|
22
|
+
verbose_name=_("name"), help_text=_("The name of the filter.")
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
species = models.ForeignKey(
|
|
26
|
+
"data.Species",
|
|
27
|
+
related_name="filtered_on",
|
|
28
|
+
on_delete=models.CASCADE,
|
|
29
|
+
verbose_name=_("species"),
|
|
30
|
+
help_text=_("The species used to find matching Observations."),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
location = models.ForeignKey(
|
|
34
|
+
"data.Location",
|
|
35
|
+
blank=True,
|
|
36
|
+
null=True,
|
|
37
|
+
related_name="filtered_on",
|
|
38
|
+
on_delete=models.CASCADE,
|
|
39
|
+
verbose_name=_("location"),
|
|
40
|
+
help_text=_("The location used to find matching Observations."),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
update_species = models.ForeignKey(
|
|
44
|
+
"data.Species",
|
|
45
|
+
related_name="filtered_by",
|
|
46
|
+
on_delete=models.CASCADE,
|
|
47
|
+
verbose_name=_("update species"),
|
|
48
|
+
help_text=_("Matching Observations are updated to this species."),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def __repr__(self) -> str:
|
|
52
|
+
return str(self.name)
|
|
53
|
+
|
|
54
|
+
def __str__(self) -> str:
|
|
55
|
+
return str(self.name)
|
|
56
|
+
|
|
57
|
+
def apply(self):
|
|
58
|
+
count = 0
|
|
59
|
+
filters = models.Q()
|
|
60
|
+
|
|
61
|
+
if self.species:
|
|
62
|
+
filters &= models.Q(species=self.species)
|
|
63
|
+
if self.location:
|
|
64
|
+
filters &= models.Q(location=self.location)
|
|
65
|
+
|
|
66
|
+
for observation in Observation.objects.filter(filters):
|
|
67
|
+
observation.species = self.update_species
|
|
68
|
+
observation.save()
|
|
69
|
+
count += 1
|
|
70
|
+
|
|
71
|
+
return count
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Location(models.Model):
|
|
6
|
+
"""
|
|
7
|
+
Location defines the location where the observations in a Checklist
|
|
8
|
+
were made.
|
|
9
|
+
|
|
10
|
+
Design Notes
|
|
11
|
+
------------
|
|
12
|
+
Rather than use country, subnational1 and subnational2 from the eBird API,
|
|
13
|
+
the names country, state and county from the eBird Basic Dataset were used.
|
|
14
|
+
Originally the models were designed so data from either source could be
|
|
15
|
+
loaded, but since the schemas did not overlap completely that meant quite
|
|
16
|
+
a few fields were optional. This dual-purpose role was discarded and the
|
|
17
|
+
models were changed to specifically support the eBird API. The names, state
|
|
18
|
+
and county were kept because they were shorter, and it was easier to remember
|
|
19
|
+
the order.
|
|
20
|
+
|
|
21
|
+
The hierarchy: country, state, country describes the administrative, but not
|
|
22
|
+
the geographical/ecological organisation. Being able to identify groups of
|
|
23
|
+
related locations in a national park or Important Bird Areas, or groups of
|
|
24
|
+
counties or states is useful to find out what has been seen in a given area.
|
|
25
|
+
Additional models for Area, Island and Region were considered but ultimately
|
|
26
|
+
rejected for three main reasons:
|
|
27
|
+
|
|
28
|
+
1. For hotspots it is easy to map locations to an Area or Island, however
|
|
29
|
+
about 40% of all locations (for Portugal) are personal or private, even
|
|
30
|
+
when the hotspot is well known, or should have been well known. Unless
|
|
31
|
+
these were ignored, that would mean an on-going effort to add these
|
|
32
|
+
locations to the relevant groups. For even a small country or area the
|
|
33
|
+
effort is enormous. Autocorrecting location should be possible but that
|
|
34
|
+
would mean defining boundaries for each hotspot. There was talk of eBird
|
|
35
|
+
doing that but again the effort would be enormous.
|
|
36
|
+
|
|
37
|
+
2. If the hierarchy is extended to add new levels, then the codes used by
|
|
38
|
+
eBird, US, US-NY, US-NY-109 would need to be changed to incorporate
|
|
39
|
+
the new levels, since it makes finding related locations easy. The
|
|
40
|
+
alternative is to create the hierarchy using foreign keys. Again that
|
|
41
|
+
create a considerable administrative burden.
|
|
42
|
+
|
|
43
|
+
3. Grouping locations, countries, or states is mainly for usability,
|
|
44
|
+
however for small numbers of each, adding form controls that support
|
|
45
|
+
multi-select can be used as a workaround / viable alternative, and
|
|
46
|
+
even provide more flexibility.
|
|
47
|
+
|
|
48
|
+
On the second point, it might be worth adopting an alternative hierarchy
|
|
49
|
+
such as the NUTS3 system used in the European Union, which has an extra
|
|
50
|
+
level. It also uses hierarchical codes, e.g. PT1, PT15, PT15D. Counties
|
|
51
|
+
are the smallest areas, and are shared by both systems, so it is easy to
|
|
52
|
+
map eBird codes to NUTS codes.
|
|
53
|
+
|
|
54
|
+
Optimisations
|
|
55
|
+
-------------
|
|
56
|
+
A Checklist has a Location, but the hierarchy was flattened so the County,
|
|
57
|
+
State and Country are duplicated on the Checklist model to reduce the
|
|
58
|
+
number of joins, and so speed up queries. Similarly, Location, County, State,
|
|
59
|
+
and Country are duplicated on Observation for the same reason.
|
|
60
|
+
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
class Meta:
|
|
64
|
+
verbose_name = _("location")
|
|
65
|
+
verbose_name_plural = _("locations")
|
|
66
|
+
|
|
67
|
+
identifier = models.TextField(
|
|
68
|
+
unique=True,
|
|
69
|
+
verbose_name=_("identifier"),
|
|
70
|
+
help_text=_("The unique identifier for the location."),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
original = models.TextField(
|
|
74
|
+
verbose_name=_("original name"),
|
|
75
|
+
help_text=_("The original name of the location from eBird."),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
name = models.TextField(
|
|
79
|
+
verbose_name=_("name"),
|
|
80
|
+
help_text=_("The display name of the location."),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
country = models.ForeignKey(
|
|
84
|
+
"data.Country",
|
|
85
|
+
related_name="locations",
|
|
86
|
+
on_delete=models.PROTECT,
|
|
87
|
+
verbose_name=_("country"),
|
|
88
|
+
help_text=_("The country for the location."),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
state = models.ForeignKey(
|
|
92
|
+
"data.State",
|
|
93
|
+
related_name="locations",
|
|
94
|
+
on_delete=models.PROTECT,
|
|
95
|
+
verbose_name=_("state"),
|
|
96
|
+
help_text=_("The state for the location."),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
county = models.ForeignKey(
|
|
100
|
+
"data.County",
|
|
101
|
+
blank=True,
|
|
102
|
+
null=True,
|
|
103
|
+
related_name="locations",
|
|
104
|
+
on_delete=models.PROTECT,
|
|
105
|
+
verbose_name=_("county"),
|
|
106
|
+
help_text=_("The county for the location."),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
latitude = models.DecimalField(
|
|
110
|
+
blank=True,
|
|
111
|
+
null=True,
|
|
112
|
+
decimal_places=7,
|
|
113
|
+
max_digits=9,
|
|
114
|
+
verbose_name=_("latitude"),
|
|
115
|
+
help_text=_("The decimal latitude of the location, relative to the equator."),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
longitude = models.DecimalField(
|
|
119
|
+
blank=True,
|
|
120
|
+
null=True,
|
|
121
|
+
decimal_places=7,
|
|
122
|
+
max_digits=10,
|
|
123
|
+
verbose_name=_("longitude"),
|
|
124
|
+
help_text=_(
|
|
125
|
+
"The decimal longitude of the location, relative to the prime meridian."
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
url = models.URLField(
|
|
130
|
+
blank=True,
|
|
131
|
+
verbose_name=_("url"),
|
|
132
|
+
help_text=_("URL of the location page on eBird."),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
hotspot = models.BooleanField(
|
|
136
|
+
blank=True,
|
|
137
|
+
null=True,
|
|
138
|
+
verbose_name=_("is hotspot"),
|
|
139
|
+
help_text=_("Is the location a hotspot."),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
data = models.JSONField(
|
|
143
|
+
verbose_name=_("Data"),
|
|
144
|
+
help_text=_("Data describing a Location."),
|
|
145
|
+
default=dict,
|
|
146
|
+
blank=True,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def __repr__(self) -> str:
|
|
150
|
+
return str(self.identifier)
|
|
151
|
+
|
|
152
|
+
def __str__(self) -> str:
|
|
153
|
+
return self.name
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from json import JSONDecodeError
|
|
5
|
+
|
|
6
|
+
from django.core.validators import MinValueValidator
|
|
7
|
+
from django.db import models
|
|
8
|
+
from django.utils.translation import get_language
|
|
9
|
+
from django.utils.translation import gettext_lazy as _
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Observation(models.Model):
|
|
15
|
+
class Meta:
|
|
16
|
+
verbose_name = _("observation")
|
|
17
|
+
verbose_name_plural = _("observations")
|
|
18
|
+
|
|
19
|
+
edited = models.DateTimeField(
|
|
20
|
+
blank=True,
|
|
21
|
+
null=True,
|
|
22
|
+
help_text=_("The date and time the eBird checklist was last edited."),
|
|
23
|
+
verbose_name=_("edited"),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
published = models.BooleanField(
|
|
27
|
+
help_text=_("Is the observation published?"),
|
|
28
|
+
verbose_name=_("published"),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
identifier = models.TextField(
|
|
32
|
+
unique=True,
|
|
33
|
+
verbose_name=_("identifier"),
|
|
34
|
+
help_text=_("A global unique identifier for the observation."),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
checklist = models.ForeignKey(
|
|
38
|
+
"data.Checklist",
|
|
39
|
+
related_name="observations",
|
|
40
|
+
on_delete=models.CASCADE,
|
|
41
|
+
verbose_name=_("checklist"),
|
|
42
|
+
help_text=_("The checklist this observation belongs to."),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
species = models.ForeignKey(
|
|
46
|
+
"data.Species",
|
|
47
|
+
related_name="observations",
|
|
48
|
+
on_delete=models.PROTECT,
|
|
49
|
+
verbose_name=_("species"),
|
|
50
|
+
help_text=_("The identified species."),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
observer = models.ForeignKey(
|
|
54
|
+
"data.Observer",
|
|
55
|
+
related_name="observations",
|
|
56
|
+
on_delete=models.PROTECT,
|
|
57
|
+
verbose_name=_("observer"),
|
|
58
|
+
help_text=_("The person who made the observation."),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
country = models.ForeignKey(
|
|
62
|
+
"data.Country",
|
|
63
|
+
related_name="observations",
|
|
64
|
+
on_delete=models.PROTECT,
|
|
65
|
+
verbose_name=_("country"),
|
|
66
|
+
help_text=_("The country where the observation was made."),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
state = models.ForeignKey(
|
|
70
|
+
"data.State",
|
|
71
|
+
related_name="observations",
|
|
72
|
+
on_delete=models.PROTECT,
|
|
73
|
+
verbose_name=_("state"),
|
|
74
|
+
help_text=_("The state where the observation was made."),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
county = models.ForeignKey(
|
|
78
|
+
"data.County",
|
|
79
|
+
blank=True,
|
|
80
|
+
null=True,
|
|
81
|
+
related_name="observations",
|
|
82
|
+
on_delete=models.PROTECT,
|
|
83
|
+
verbose_name=_("county"),
|
|
84
|
+
help_text=_("The county where the observation was made."),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
location = models.ForeignKey(
|
|
88
|
+
"data.Location",
|
|
89
|
+
related_name="observations",
|
|
90
|
+
on_delete=models.PROTECT,
|
|
91
|
+
verbose_name=_("location"),
|
|
92
|
+
help_text=_("The location where the observation was made."),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
date = models.DateField(
|
|
96
|
+
db_index=True,
|
|
97
|
+
verbose_name=_("date"),
|
|
98
|
+
help_text=_("The date the observation was made."),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
time = models.TimeField(
|
|
102
|
+
blank=True,
|
|
103
|
+
null=True,
|
|
104
|
+
verbose_name=_("time"),
|
|
105
|
+
help_text=_("The time the observation was made."),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
started = models.DateTimeField(
|
|
109
|
+
blank=True,
|
|
110
|
+
db_index=True,
|
|
111
|
+
null=True,
|
|
112
|
+
verbose_name=_("date & time"),
|
|
113
|
+
help_text=_("The date and time the observation was made."),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
count = models.IntegerField(
|
|
117
|
+
validators=[MinValueValidator(0)],
|
|
118
|
+
verbose_name=_("count"),
|
|
119
|
+
help_text=_("The number of birds seen."),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
audio = models.BooleanField(
|
|
123
|
+
default=False,
|
|
124
|
+
verbose_name=_("has audio"),
|
|
125
|
+
help_text=_("Have audio recordings been uploaded to the Macaulay library."),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
photo = models.BooleanField(
|
|
129
|
+
default=False,
|
|
130
|
+
verbose_name=_("has photos"),
|
|
131
|
+
help_text=_("Have photos been uploaded to the Macaulay library."),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
video = models.BooleanField(
|
|
135
|
+
default=False,
|
|
136
|
+
verbose_name=_("has video"),
|
|
137
|
+
help_text=_("Have video recordings been uploaded to the Macaulay library."),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
approved = models.BooleanField(
|
|
141
|
+
default=True,
|
|
142
|
+
verbose_name=_("Approved"),
|
|
143
|
+
help_text=_("Has the observation been accepted."),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
reason = models.TextField(
|
|
147
|
+
blank=True,
|
|
148
|
+
verbose_name=_("Reason"),
|
|
149
|
+
help_text=_(
|
|
150
|
+
"The reason given for the observation to be marked as not approved."
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
comments = models.TextField(
|
|
155
|
+
blank=True,
|
|
156
|
+
verbose_name=_("comments"),
|
|
157
|
+
help_text=_("Any comments about the observation."),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
urn = models.TextField(
|
|
161
|
+
blank=True,
|
|
162
|
+
verbose_name=_("URN"),
|
|
163
|
+
help_text=_("The globally unique identifier for the observation."),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
data = models.JSONField(
|
|
167
|
+
verbose_name=_("Data"),
|
|
168
|
+
help_text=_("Data describing an Observation."),
|
|
169
|
+
default=dict,
|
|
170
|
+
blank=True,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def __repr__(self) -> str:
|
|
174
|
+
return str(self.identifier)
|
|
175
|
+
|
|
176
|
+
def __str__(self) -> str:
|
|
177
|
+
return str(self.identifier)
|
|
178
|
+
|
|
179
|
+
def get_reason(self) -> str:
|
|
180
|
+
try:
|
|
181
|
+
data = json.loads(self.reason)
|
|
182
|
+
reason = data.get(get_language(), "")
|
|
183
|
+
except JSONDecodeError:
|
|
184
|
+
log.error("Incorrect JSON for Observation reason: %s", self.id)
|
|
185
|
+
reason = ""
|
|
186
|
+
return reason
|
|
187
|
+
|
|
188
|
+
def has_media(self):
|
|
189
|
+
return self.audio or self.photo or self.video
|