inat-get 0.8.0.11
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.
- checksums.yaml +7 -0
- data/LICENSE +674 -0
- data/README.md +16 -0
- data/Rakefile +4 -0
- data/bin/inat-get +59 -0
- data/docs/logo.png +0 -0
- data/inat-get.gemspec +33 -0
- data/lib/extra/enum.rb +184 -0
- data/lib/extra/period.rb +252 -0
- data/lib/extra/uuid.rb +90 -0
- data/lib/inat/app/application.rb +50 -0
- data/lib/inat/app/config/messagelevel.rb +22 -0
- data/lib/inat/app/config/shiftage.rb +24 -0
- data/lib/inat/app/config/updatemode.rb +20 -0
- data/lib/inat/app/config.rb +296 -0
- data/lib/inat/app/globals.rb +80 -0
- data/lib/inat/app/info.rb +21 -0
- data/lib/inat/app/logging.rb +35 -0
- data/lib/inat/app/preamble.rb +27 -0
- data/lib/inat/app/status.rb +74 -0
- data/lib/inat/app/task/context.rb +47 -0
- data/lib/inat/app/task/dsl.rb +24 -0
- data/lib/inat/app/task.rb +75 -0
- data/lib/inat/data/api.rb +218 -0
- data/lib/inat/data/cache.rb +9 -0
- data/lib/inat/data/db.rb +87 -0
- data/lib/inat/data/ddl.rb +18 -0
- data/lib/inat/data/entity/comment.rb +29 -0
- data/lib/inat/data/entity/flag.rb +22 -0
- data/lib/inat/data/entity/identification.rb +45 -0
- data/lib/inat/data/entity/observation.rb +172 -0
- data/lib/inat/data/entity/observationphoto.rb +25 -0
- data/lib/inat/data/entity/observationsound.rb +26 -0
- data/lib/inat/data/entity/photo.rb +31 -0
- data/lib/inat/data/entity/place.rb +57 -0
- data/lib/inat/data/entity/project.rb +94 -0
- data/lib/inat/data/entity/projectadmin.rb +21 -0
- data/lib/inat/data/entity/projectobservationrule.rb +50 -0
- data/lib/inat/data/entity/request.rb +58 -0
- data/lib/inat/data/entity/sound.rb +27 -0
- data/lib/inat/data/entity/taxon.rb +94 -0
- data/lib/inat/data/entity/user.rb +67 -0
- data/lib/inat/data/entity/vote.rb +22 -0
- data/lib/inat/data/entity.rb +291 -0
- data/lib/inat/data/enums/conservationstatus.rb +30 -0
- data/lib/inat/data/enums/geoprivacy.rb +14 -0
- data/lib/inat/data/enums/iconictaxa.rb +23 -0
- data/lib/inat/data/enums/identificationcategory.rb +13 -0
- data/lib/inat/data/enums/licensecode.rb +16 -0
- data/lib/inat/data/enums/projectadminrole.rb +11 -0
- data/lib/inat/data/enums/projecttype.rb +37 -0
- data/lib/inat/data/enums/qualitygrade.rb +12 -0
- data/lib/inat/data/enums/rank.rb +60 -0
- data/lib/inat/data/model.rb +551 -0
- data/lib/inat/data/query.rb +1145 -0
- data/lib/inat/data/sets/dataset.rb +104 -0
- data/lib/inat/data/sets/list.rb +190 -0
- data/lib/inat/data/sets/listers.rb +15 -0
- data/lib/inat/data/sets/wrappers.rb +137 -0
- data/lib/inat/data/types/extras.rb +88 -0
- data/lib/inat/data/types/location.rb +89 -0
- data/lib/inat/data/types/std.rb +293 -0
- data/lib/inat/report/table.rb +135 -0
- data/lib/inat/utils/deep.rb +30 -0
- metadata +137 -0
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'globals'
|
4
|
+
require_relative 'task/context'
|
5
|
+
|
6
|
+
class Task
|
7
|
+
|
8
|
+
CHECK_LIST = %w[ . .inat .rb ]
|
9
|
+
FILE_CHECK_LIST = %w[ .inat .iNat .INat .INAT .rb .RB ]
|
10
|
+
|
11
|
+
private_constant :CHECK_LIST, :FILE_CHECK_LIST
|
12
|
+
|
13
|
+
private def existing path
|
14
|
+
if File.exist?(path)
|
15
|
+
path
|
16
|
+
else
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private def try_extensions base, *extensions
|
22
|
+
FILE_CHECK_LIST.each do |exception|
|
23
|
+
path = base + exception
|
24
|
+
return path if File.exist?(path)
|
25
|
+
end
|
26
|
+
return nil
|
27
|
+
end
|
28
|
+
|
29
|
+
private def name_complete? source
|
30
|
+
s = source.downcase
|
31
|
+
CHECK_LIST.each do |check|
|
32
|
+
return true if s.end_with?(check)
|
33
|
+
end
|
34
|
+
return false
|
35
|
+
end
|
36
|
+
|
37
|
+
# TODO: переписать более внятно
|
38
|
+
private def get_names source
|
39
|
+
path = File.expand_path source
|
40
|
+
basename = File.basename(source, '.*')
|
41
|
+
return [ basename, existing(path) ] if name_complete?(source)
|
42
|
+
base = path.split('/')[..-2].join('/') + '/' + basename
|
43
|
+
name = try_extensions base, *FILE_CHECK_LIST
|
44
|
+
return [ basename, name ]
|
45
|
+
end
|
46
|
+
|
47
|
+
def config
|
48
|
+
@application.config
|
49
|
+
end
|
50
|
+
|
51
|
+
def logger
|
52
|
+
@application.logger
|
53
|
+
end
|
54
|
+
|
55
|
+
def name
|
56
|
+
@context&.name
|
57
|
+
end
|
58
|
+
|
59
|
+
def done?
|
60
|
+
@context&.done?
|
61
|
+
end
|
62
|
+
|
63
|
+
def initialize application, source
|
64
|
+
@application = application
|
65
|
+
@basename, @path = get_names source
|
66
|
+
raise ArgumentError, "File not found: #{source}!" if @path.nil?
|
67
|
+
@context = Task::Context::new self, @basename, @path
|
68
|
+
end
|
69
|
+
|
70
|
+
def execute
|
71
|
+
G.current_task = self
|
72
|
+
@context.execute
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'uri'
|
5
|
+
require 'net/http'
|
6
|
+
|
7
|
+
require_relative '../app/globals'
|
8
|
+
require_relative '../app/status'
|
9
|
+
|
10
|
+
module API
|
11
|
+
|
12
|
+
RECORDS_LIMIT = 200
|
13
|
+
FREQUENCY_LIMIT = 1.0
|
14
|
+
|
15
|
+
class << self
|
16
|
+
|
17
|
+
include LogDSL
|
18
|
+
|
19
|
+
def init
|
20
|
+
@mutex = Mutex::new
|
21
|
+
end
|
22
|
+
|
23
|
+
def get path, part, limit, *ids
|
24
|
+
return [] if ids.empty?
|
25
|
+
if ids.size > limit
|
26
|
+
rest = ids.dup
|
27
|
+
head = rest.shift limit
|
28
|
+
return get(path, *head) + get(path, *rest)
|
29
|
+
end
|
30
|
+
result = []
|
31
|
+
Status::status '[api]', "#{ path } ..."
|
32
|
+
@mutex.synchronize do
|
33
|
+
now = Time::new
|
34
|
+
if @last_call && now - @last_call < FREQUENCY_LIMIT
|
35
|
+
sleep FREQUENCY_LIMIT - (now - @last_call)
|
36
|
+
end
|
37
|
+
case part
|
38
|
+
when :query
|
39
|
+
url = G.config[:api][:root] + path.to_s + "?id=#{ ids.join(',') }"
|
40
|
+
url += "&per_page=#{ limit }"
|
41
|
+
locale = G.config[:api][:locale]
|
42
|
+
url += "&locale=#{ locale }" if locale
|
43
|
+
preferred_place_id = G.config[:api][:preferred_place_id]
|
44
|
+
url += "&preferred_place_id=#{ preferred_place_id }" if preferred_place_id
|
45
|
+
when :path
|
46
|
+
url = G.config[:api][:root] + path.to_s + "/#{ ids.join(',') }"
|
47
|
+
else
|
48
|
+
raise ArgumentError, "Invalid 'part' argument: #{ part.inspect }!", caller
|
49
|
+
end
|
50
|
+
uri = URI(url)
|
51
|
+
info "GET: URI = #{ uri.inspect }"
|
52
|
+
# Status::status 'GET', uri.to_s
|
53
|
+
https = uri.scheme == 'https'
|
54
|
+
open_timeout = G.config[:api][:open_timeout]
|
55
|
+
read_timeout = G.config[:api][:read_timeout]
|
56
|
+
http_options = {
|
57
|
+
use_ssl: https
|
58
|
+
}
|
59
|
+
http_options[:open_timeout] = open_timeout if open_timeout
|
60
|
+
http_options[:read_timeout] = read_timeout if read_timeout
|
61
|
+
answered = false
|
62
|
+
answer_count = 50
|
63
|
+
last_time = Time::new
|
64
|
+
until answered
|
65
|
+
begin
|
66
|
+
Net::HTTP::start uri.host, uri.port, **http_options do |http|
|
67
|
+
request = Net::HTTP::Get::new uri
|
68
|
+
request['User-Agent'] = G.config[:api][:user_agent] || "INat::Get // Unknown Instance"
|
69
|
+
response = http.request request
|
70
|
+
if Net::HTTPSuccess === response
|
71
|
+
data = JSON.parse(response.body)
|
72
|
+
result = data['results']
|
73
|
+
total = data['total_results']
|
74
|
+
paged = data['per_page']
|
75
|
+
time_diff = Time::new - last_time
|
76
|
+
debug "GET OK: total = #{ total } paged = #{ paged } time = #{ time_diff } "
|
77
|
+
# Status::status 'GET', uri.to_s + ' DONE'
|
78
|
+
else
|
79
|
+
error "Bad response om #{ uri.path }#{ uri.query && !uri.query.empty? && '?' + uri.query || '' }: #{ response.inspect }!"
|
80
|
+
result = [ { 'id' => ids.first } ]
|
81
|
+
# Status::status 'GET', uri.to_s + ' ERROR'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
answered = true
|
85
|
+
rescue Exception
|
86
|
+
if answer_count > 0
|
87
|
+
answer_count -= 1
|
88
|
+
answered = false
|
89
|
+
error "Error in HTTP request: #{ $!.inspect }, retry: #{ answer_count }."
|
90
|
+
# Status::status "Error in HTTP request: #{ $!.inspect }, retry: #{ answer_count }."
|
91
|
+
sleep 2.0
|
92
|
+
else
|
93
|
+
answered = true
|
94
|
+
error "Error in HTTP request: #{ $!.inspect }!"
|
95
|
+
# Status::status "Error in HTTP request: #{ $!.inspect }!"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
@last_call = Time::new
|
100
|
+
end
|
101
|
+
Status::status '[api]', "#{ path } DONE"
|
102
|
+
result
|
103
|
+
end
|
104
|
+
|
105
|
+
private def make_url path, **params
|
106
|
+
url = G.config[:api][:root] + path.to_s
|
107
|
+
query = []
|
108
|
+
params.each do |key, value|
|
109
|
+
query_param = "#{ key }="
|
110
|
+
if Array === value
|
111
|
+
query_param += value.map(&:to_query).join(',')
|
112
|
+
else
|
113
|
+
query_param += value.to_query
|
114
|
+
end
|
115
|
+
query << query_param
|
116
|
+
end
|
117
|
+
locale = G.config[:api][:locale]
|
118
|
+
query << "locale=#{ locale }" if locale
|
119
|
+
preferred_place_id = G.config[:api][:preferred_place_id]
|
120
|
+
query << "preferred_place_id=#{ preferred_place_id }" if preferred_place_id
|
121
|
+
if !query.empty?
|
122
|
+
url += "?" + query.join('&')
|
123
|
+
end
|
124
|
+
url
|
125
|
+
end
|
126
|
+
|
127
|
+
def query path, first_only: false, **params, &block
|
128
|
+
Status::status '[api]', "#{ path } ..."
|
129
|
+
para = params.dup
|
130
|
+
para.delete_if { |key, _| key.intern == :page }
|
131
|
+
para[:per_page] = RECORDS_LIMIT
|
132
|
+
para[:order_by] = :id
|
133
|
+
para[:order] = :asc
|
134
|
+
result = []
|
135
|
+
rest = nil
|
136
|
+
total = 0
|
137
|
+
@mutex.synchronize do
|
138
|
+
now = Time::new
|
139
|
+
if @last_call && now - @last_call < FREQUENCY_LIMIT
|
140
|
+
sleep FREQUENCY_LIMIT - (now - @last_call)
|
141
|
+
end
|
142
|
+
url = make_url path, **para
|
143
|
+
uri = URI(url)
|
144
|
+
info "QUERY: URI = #{ uri.inspect }"
|
145
|
+
# Status::status 'QUERY', uri.to_s
|
146
|
+
https = uri.scheme == 'https'
|
147
|
+
open_timeout = G.config[:api][:open_timeout]
|
148
|
+
read_timeout = G.config[:api][:read_timeout]
|
149
|
+
http_options = {
|
150
|
+
use_ssl: https
|
151
|
+
}
|
152
|
+
http_options[:open_timeout] = open_timeout if open_timeout
|
153
|
+
http_options[:read_timeout] = read_timeout if read_timeout
|
154
|
+
answered = false
|
155
|
+
answer_count = 50
|
156
|
+
last_time = Time::new
|
157
|
+
until answered
|
158
|
+
begin
|
159
|
+
Net::HTTP::start uri.host, uri.port, **http_options do |http|
|
160
|
+
request = Net::HTTP::Get::new uri
|
161
|
+
request["User-Agent"] = G.config[:api][:user_agent] || "INat::Get // Unknown Instance"
|
162
|
+
response = http.request request
|
163
|
+
if Net::HTTPSuccess === response
|
164
|
+
data = JSON.parse(response.body)
|
165
|
+
result = data["results"]
|
166
|
+
total = data["total_results"]
|
167
|
+
paged = data["per_page"]
|
168
|
+
time_diff = Time::new - last_time
|
169
|
+
debug "QUERY OK: total = #{ total } paged = #{ paged } time = #{ time_diff } "
|
170
|
+
if total > paged && !first_only
|
171
|
+
max = result.map { |o| o["id"] }.max
|
172
|
+
rest = para
|
173
|
+
rest[:id_above] = max
|
174
|
+
end
|
175
|
+
else
|
176
|
+
raise RuntimeError, "Invalid response: #{response.inspect}"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
answered = true
|
180
|
+
rescue Exception
|
181
|
+
if answer_count > 0
|
182
|
+
answer_count -= 1
|
183
|
+
answered = false
|
184
|
+
error "Error in HTTP request: #{ $!.inspect }, retry: #{ answer_count }."
|
185
|
+
# Status::status "Error in HTTP request: #{ $!.inspect }, retry: #{ answer_count }."
|
186
|
+
sleep 2.0
|
187
|
+
else
|
188
|
+
raise
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
@last_call = Time::new
|
193
|
+
end
|
194
|
+
Status::status '[api]', "#{ path } DONE"
|
195
|
+
# TODO: переделать рекурсию в итерации
|
196
|
+
if block_given?
|
197
|
+
rr = []
|
198
|
+
result.each do |js_object|
|
199
|
+
rr << yield(js_object, total)
|
200
|
+
end
|
201
|
+
rr += query(path, **rest, &block) if rest
|
202
|
+
rr
|
203
|
+
else
|
204
|
+
result += query(path, **rest) if rest
|
205
|
+
result
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def load_file filename
|
210
|
+
data = JSON.parse File.read(filename)
|
211
|
+
data['results']
|
212
|
+
end
|
213
|
+
|
214
|
+
end
|
215
|
+
|
216
|
+
end
|
217
|
+
|
218
|
+
API::init
|
data/lib/inat/data/db.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'sqlite3'
|
5
|
+
|
6
|
+
require_relative '../app/globals'
|
7
|
+
require_relative 'ddl'
|
8
|
+
|
9
|
+
class DB
|
10
|
+
|
11
|
+
include LogDSL
|
12
|
+
|
13
|
+
def self.get_finalizer *dbs
|
14
|
+
proc do
|
15
|
+
dbs.each { |db| db.close }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@mutex = Mutex::new
|
21
|
+
@config = G.config
|
22
|
+
@directory = @config[:data][:directory]
|
23
|
+
FileUtils.mkpath @directory
|
24
|
+
@data = SQLite3::Database::open "#{@directory}/inat-cache.db"
|
25
|
+
@mutex.synchronize do
|
26
|
+
@data.encoding = 'UTF-8'
|
27
|
+
@data.auto_vacuum = 1
|
28
|
+
@data.results_as_hash = true
|
29
|
+
@data.foreign_keys = true
|
30
|
+
@data.execute_batch DDL.DDL
|
31
|
+
end
|
32
|
+
ObjectSpace.define_finalizer self, self.class.get_finalizer(@data)
|
33
|
+
end
|
34
|
+
|
35
|
+
def execute query, *args
|
36
|
+
Status::status '[db]', '...'
|
37
|
+
result = []
|
38
|
+
@mutex.synchronize do
|
39
|
+
last_time = Time::new
|
40
|
+
info "DB: query = #{ query } args = #{ args.inspect }"
|
41
|
+
result = @data.execute query, args
|
42
|
+
time_diff = Time::new - last_time
|
43
|
+
debug "DB OK: count = #{ Array === result && result.size || 'none' } time = #{ (time_diff * 1000000).to_i }ns"
|
44
|
+
end
|
45
|
+
Status::status '[db]', 'DONE'
|
46
|
+
result
|
47
|
+
end
|
48
|
+
|
49
|
+
def execute_batch query
|
50
|
+
Status::status '[db]', '...'
|
51
|
+
@mutex.synchronize do
|
52
|
+
last_time = Time::new
|
53
|
+
info "DB: batch = #{ query }"
|
54
|
+
@data.execute_batch "BEGIN TRANSACTION;\n" + query + "\nCOMMIT;\n"
|
55
|
+
time_diff = Time::new - last_time
|
56
|
+
debug "DB OK: time = #{ (time_diff * 1000000).to_i }ns"
|
57
|
+
end
|
58
|
+
Status::status '[db]', 'DONE'
|
59
|
+
end
|
60
|
+
|
61
|
+
# def transaction &block
|
62
|
+
# raise ArgumentError, "Block is required?", caller unless block_given?
|
63
|
+
# @data.transaction(&block)
|
64
|
+
# end
|
65
|
+
|
66
|
+
class << self
|
67
|
+
|
68
|
+
def instance
|
69
|
+
@instance ||= DB::new
|
70
|
+
@instance
|
71
|
+
end
|
72
|
+
|
73
|
+
def execute query, *args
|
74
|
+
instance.execute query, *args
|
75
|
+
end
|
76
|
+
|
77
|
+
def execute_batch query
|
78
|
+
instance.execute_batch query
|
79
|
+
end
|
80
|
+
|
81
|
+
# def transaction &block
|
82
|
+
# instance.transaction(&block)
|
83
|
+
# end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../types/std'
|
4
|
+
require_relative '../types/extras'
|
5
|
+
require_relative '../entity'
|
6
|
+
|
7
|
+
autoload :Observation, 'inat/data/entity/observation'
|
8
|
+
autoload :User, 'inat/data/entity/user'
|
9
|
+
autoload :Flag, 'inat/data/entity/flag'
|
10
|
+
|
11
|
+
class Comment < Entity
|
12
|
+
|
13
|
+
table :comments
|
14
|
+
|
15
|
+
field :observation, type: Observation, index: true
|
16
|
+
|
17
|
+
field :uuid, type: UUID, unique: true
|
18
|
+
field :user, type: User, index: true
|
19
|
+
field :created_at, type: Time, required: true
|
20
|
+
field :body, type: String
|
21
|
+
field :hidden, type: Boolean, index: true
|
22
|
+
|
23
|
+
links :flags, item_type: Flag, owned: true
|
24
|
+
|
25
|
+
ignore :moderator_actions # TODO: разобраться
|
26
|
+
|
27
|
+
ignore :created_at_details
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../types/std'
|
4
|
+
require_relative '../types/extras'
|
5
|
+
require_relative '../entity'
|
6
|
+
|
7
|
+
autoload :Observation, 'inat/data/entity/observation'
|
8
|
+
autoload :User, 'inat/data/entity/user'
|
9
|
+
|
10
|
+
class Flag < Entity
|
11
|
+
|
12
|
+
table :flags
|
13
|
+
|
14
|
+
field :flag, type: String, required: true
|
15
|
+
field :created_at, type: Time
|
16
|
+
field :updated_at, type: Time
|
17
|
+
field :user, type: User, index: true, required: true
|
18
|
+
field :resolved, type: Boolean, index: true
|
19
|
+
field :resolver, type: User, index: true
|
20
|
+
field :comment, type: String
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'extra/uuid'
|
4
|
+
|
5
|
+
require_relative '../types/std'
|
6
|
+
require_relative '../types/extras'
|
7
|
+
require_relative '../entity'
|
8
|
+
|
9
|
+
require_relative '../enums/identificationcategory'
|
10
|
+
|
11
|
+
autoload :Observation, 'inat/data/entity/observation'
|
12
|
+
autoload :Taxon, 'inat/data/entity/taxon'
|
13
|
+
autoload :Flag, 'inat/data/entity/flag'
|
14
|
+
|
15
|
+
class Identification < Entity
|
16
|
+
|
17
|
+
api_path :identifications
|
18
|
+
api_part :query
|
19
|
+
api_limit 200
|
20
|
+
|
21
|
+
table :identifications
|
22
|
+
|
23
|
+
field :observation, type: Observation, index: true
|
24
|
+
|
25
|
+
field :uuid, type: UUID, unique: true
|
26
|
+
field :user, type: User, index: true, required: true
|
27
|
+
field :created_at, type: Time, index: true
|
28
|
+
field :body, type: String
|
29
|
+
field :category, type: IdentificationCategory, index: true
|
30
|
+
field :current, type: Boolean
|
31
|
+
field :own_observation, type: Boolean, index: true
|
32
|
+
field :vision, type: Boolean
|
33
|
+
field :disagreement, type: Boolean, index: true
|
34
|
+
field :spam, type: Boolean, index: true
|
35
|
+
field :hidden, type: Boolean, index: true
|
36
|
+
field :taxon, type: Taxon, index: true
|
37
|
+
field :previous_observation_taxon, type: Taxon, index: true
|
38
|
+
|
39
|
+
links :flags, item_type: Flag
|
40
|
+
|
41
|
+
ignore :taxon_change # TODO: разобраться
|
42
|
+
ignore :moderator_actions
|
43
|
+
ignore :created_at_details
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../types/std'
|
4
|
+
require_relative '../types/extras'
|
5
|
+
require_relative '../types/location'
|
6
|
+
require_relative '../entity'
|
7
|
+
require_relative '../enums/qualitygrade'
|
8
|
+
require_relative '../enums/licensecode'
|
9
|
+
require_relative '../enums/geoprivacy'
|
10
|
+
|
11
|
+
autoload :Taxon, 'inat/data/entity/taxon'
|
12
|
+
autoload :User, 'inat/data/entity/user'
|
13
|
+
autoload :Flag, 'inat/data/entity/flag'
|
14
|
+
autoload :ObservationSound, 'inat/data/entity/observationsound'
|
15
|
+
autoload :Sound, 'inat/data/entity/sound'
|
16
|
+
autoload :ObservationPhoto, 'inat/data/entity/observationphoto'
|
17
|
+
autoload :Photo, 'inat/data/entity/photo'
|
18
|
+
autoload :Place, 'inat/data/entity/place'
|
19
|
+
autoload :Project, 'inat/data/entity/project'
|
20
|
+
autoload :Comment, 'inat/data/entity/comment'
|
21
|
+
autoload :Identification, 'inat/data/entity/identification'
|
22
|
+
autoload :Vote, 'inat/data/entity/vote'
|
23
|
+
|
24
|
+
class Observation < Entity
|
25
|
+
|
26
|
+
api_path :observations
|
27
|
+
api_part :query
|
28
|
+
api_limit 200
|
29
|
+
|
30
|
+
table :observations
|
31
|
+
|
32
|
+
field :quality_grade, type: QualityGrade, required: true, index: true
|
33
|
+
field :uuid, type: UUID, unique: true
|
34
|
+
field :species_guess, type: String
|
35
|
+
field :license_code, type: LicenseCode, index: true
|
36
|
+
field :description, type: String
|
37
|
+
field :captive, type: Boolean, index: true
|
38
|
+
field :taxon, type: Taxon, index: true
|
39
|
+
field :community_taxon, type: Taxon, index: true
|
40
|
+
field :uri, type: URI
|
41
|
+
field :obscured, type: Boolean, index: true
|
42
|
+
field :spam, type: Boolean, index: true
|
43
|
+
field :user, type: User, index: true
|
44
|
+
field :mappable, type: Boolean, index: true
|
45
|
+
|
46
|
+
links :flags, item_type: Flag, owned: true
|
47
|
+
|
48
|
+
field :time_observed_at, type: Time, index: true
|
49
|
+
field :created_at, type: Time, index: true
|
50
|
+
field :observed_on, type: Date, index: true
|
51
|
+
field :observed_on_string, type: String
|
52
|
+
field :updated_at, type: Time, index: true
|
53
|
+
|
54
|
+
field :geoprivacy, type: GeoPrivacy, index: true
|
55
|
+
field :taxon_geoprivacy, type: GeoPrivacy, index: true
|
56
|
+
field :location, type: Location, index: true
|
57
|
+
field :positional_accuracy, type: Integer, index: true
|
58
|
+
field :public_positional_accuracy, type: Integer, index: true
|
59
|
+
field :place_guess, type: String
|
60
|
+
|
61
|
+
backs :observation_sounds, item_type: ObservationSound, owned: true
|
62
|
+
links :sounds, item_type: Sound, table_name: :observation_sounds, owned: false
|
63
|
+
backs :observation_photos, item_type: ObservationPhoto, owned: true
|
64
|
+
links :photos, item_type: Photo, table_name: :observation_photos, owned: false
|
65
|
+
|
66
|
+
links :places, item_type: Place, owned: true
|
67
|
+
links :projects, item_type: Project, owned: true # Это только ручные
|
68
|
+
links :cached_projects, item_type: Project, table_name: :project_observations, owned: false
|
69
|
+
|
70
|
+
field :owners_identification_from_vision, type: Boolean
|
71
|
+
field :identifications_most_agree, type: Boolean
|
72
|
+
field :identifications_some_agree, type: Boolean
|
73
|
+
field :num_identification_agreements, type: Integer
|
74
|
+
field :identifications_most_disagree, type: Boolean
|
75
|
+
field :num_identification_disagreements, type: Integer
|
76
|
+
backs :comments, item_type: Comment, owned: true
|
77
|
+
backs :identifications, item_type: Identification, owned: true
|
78
|
+
|
79
|
+
links :ident_taxa, item_type: Taxon, ids_field: :ident_taxon_ids, index: true, owned: true
|
80
|
+
|
81
|
+
links :votes, item_type: Vote, owned: true
|
82
|
+
links :faves, item_type: Vote, owned: true
|
83
|
+
|
84
|
+
field :day, type: Integer, index: true
|
85
|
+
field :month, type: Integer, index: true
|
86
|
+
field :year, type: Integer, index: true
|
87
|
+
|
88
|
+
block :observed_on_details, type: Object do |value|
|
89
|
+
if Hash === value
|
90
|
+
self.day = value['day']
|
91
|
+
self.month = value['month']
|
92
|
+
self.year = value['year']
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
field :endemic, type: Boolean, index: true
|
97
|
+
field :introduced, type: Boolean, index: true
|
98
|
+
field :native, type: Boolean, index: true
|
99
|
+
field :taxon_is_active, type: Boolean, index: true
|
100
|
+
field :threatened, type: Boolean, index: true
|
101
|
+
field :photo_licensed, type: Boolean, index: true
|
102
|
+
field :rank, type: Rank, index: true
|
103
|
+
|
104
|
+
def post_update
|
105
|
+
t = self.taxon
|
106
|
+
if t
|
107
|
+
self.endemic = t.endemic
|
108
|
+
self.introduced = t.introduced
|
109
|
+
self.native = t.native
|
110
|
+
self.taxon_is_active = t.is_active
|
111
|
+
self.threatened = t.threatened
|
112
|
+
self.rank = t.rank
|
113
|
+
end
|
114
|
+
if self.observation_photos != nil
|
115
|
+
self.photo_licensed = self.observation_photos.any? { |p| p.photo.license_code != nil }
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def normalized_taxon ranks
|
120
|
+
ranks = (ranks .. ranks) if Rank === ranks
|
121
|
+
raise TypeError, "Invalid type for ranks: #{ ranks.inspect }!", caller unless Range === ranks
|
122
|
+
min = ranks.begin
|
123
|
+
# max = ranks.end
|
124
|
+
taxon = self.taxon
|
125
|
+
while true do
|
126
|
+
return nil if taxon == nil
|
127
|
+
return nil if taxon.rank < min
|
128
|
+
return taxon if ranks === taxon.rank
|
129
|
+
taxon = taxon.parent
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def sort_key
|
134
|
+
time_observed_at || observed_on&.to_time || Time::at(0)
|
135
|
+
end
|
136
|
+
|
137
|
+
ignore :tags # TODO: implement
|
138
|
+
ignore :created_time_zone # TODO: подумать...
|
139
|
+
ignore :observed_time_zone
|
140
|
+
ignore :time_zone_offset
|
141
|
+
ignore :geojson
|
142
|
+
ignore :annotations
|
143
|
+
# ignore :observed_on_details
|
144
|
+
ignore :created_at_details
|
145
|
+
ignore :cached_votes_total
|
146
|
+
ignore :comments_count
|
147
|
+
ignore :site_id
|
148
|
+
ignore :quality_metrics
|
149
|
+
ignore :reviewed_by
|
150
|
+
ignore :oauth_application_id
|
151
|
+
ignore :project_ids_with_curator_id
|
152
|
+
# ignore :place_ids
|
153
|
+
ignore :outlinks # TODO
|
154
|
+
ignore :faves_count
|
155
|
+
ignore :ofvs
|
156
|
+
ignore :preferences
|
157
|
+
ignore :map_scale
|
158
|
+
ignore :identifications_count
|
159
|
+
ignore :project_ids_without_curator_id
|
160
|
+
ignore :project_observations # TODO: ???
|
161
|
+
ignore :non_owner_ids
|
162
|
+
ignore :out_of_range
|
163
|
+
ignore :id_please
|
164
|
+
ignore :context_user_geoprivacy # NEED: обязательно разобраться
|
165
|
+
ignore :context_geoprivacy
|
166
|
+
ignore :context_taxon_geoprivacy
|
167
|
+
|
168
|
+
def to_s
|
169
|
+
"<a href=\"https://www.inaturalist.org/observations/#{ id }\">\##{ id }</a>"
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|