-
Notifications
You must be signed in to change notification settings - Fork 25
/
observation.rb
296 lines (265 loc) · 8.89 KB
/
observation.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# Records Qualitative, Quantitative, Statistical, free-text (Working), Media and other types of "measurements" gathered from our observations.
# Where we record the data behind concepts like traits, phenotypes, measurements, character matrices, and descriptive matrices.
#
# Subclasses of Observation define the applicable attributes for its type. Type is echoed 1:1 in each
# corresponding Descriptor, for convenience and validation purposes.
#
# @!attribute cached
# @return [string]
# !! Not used. Perhaps records a human readable short description ultimately.
#
# @!attribute cached_column_label
# @return [string]
# !! Not used. Candidate for removal.
#
# @!attribute cached_row_label
# @return [string]
# !! Not used. Candidate for removal.
#
# @!attribute descriptor_id
# @return [Descriptor#id]
# The type of observation according to it's Descriptor. See also `type`.
#
# @!attribute observation_object_id
# @return [Object#id]
# The id of the observed object
#
# @!attribute observation_object_type
# @return [Object#class.name]
# The type of the observed object
#
# @!attribute type
# @return [String]
# The type of observation. Defines the attribute set that is applicable to it.
#
# @!attribute year_made
# @return [Integer]
# 4 digit year the observation originated (not when it was recorded in TaxonWorks, though these might be the close to the same)
#
# @!attribute month_made
# @return [Integer]
# 2 digit month the observation originated
#
# @!attribute day_made
# @return [Integer]
# 2 digit day the observation originated
#
# @!attribute time_made
# @return [Integer]
# time without time zone
#
# Subclass specific attributes
#
# Observation::Qualitative attributes
#
## @!attribute character_state_id
# @return [Integer]
# The corresponding CharacterState id for "traditional" Qualitative "characters"
#
# Observation::Quantiative attributes
#
# @!attribute continuous_unit
# @return [String]
# A controlled vocabulary from Ruby::Units, like 'm". The unit of the quantitative measurement
#
# @!attribute continuous_value
# @return [String]
# The value of a quantitative measurement
#
# Observation::Working
#
# @!attribute descriptions
# @return [String]
# Free text description of an Observation
#
# Observation::??
#
# @!attribute frequency
# @return [Descriptor#id]
# ?! Candidate or removal ?!
#
# Observation::PresenceAbsence
#
# @!attribute presence
# @return [Boolean]
#
# Observation::Sample
# !! Should only apply to OTUs technically, as this
# is an aggregation of measurements seen in a typical
# statistical summary
#
# @!attribute sample_max
# @return [Boolean]
# statistical max
#
# @!attribute sample_mean
# @return [Boolean]
# statistical mean
#
# @!attribute sample_median
# @return [Boolean]
# statistical median
#
# @!attribute sample_min
# @return [Boolean]
# statistical median
#
# @!attribute sample_n
# @return [Boolean]
# statistical n
#
# @!attribute sample_standard_deviation
# @return [Boolean]
# statistical standard deviation
#
# @!attribute sample_standard_error
# @return [Boolean]
# statistical standard error
#
# @!attribute sample_standard_units
# @return [Boolean]
# A controlled vocabulary from Ruby::Units, like 'm". The unit of the sample observations
#
class Observation < ApplicationRecord
include Housekeeping
include Shared::Citations
include Shared::DataAttributes
include Shared::Identifiers
include Shared::Depictions
include Shared::Notes
include Shared::Tags
include Shared::Depictions
include Shared::Confidences
include Shared::ProtocolRelationships
include Shared::IsData
include Shared::ObservationIndex
ignore_whitespace_on(:description)
self.skip_time_zone_conversion_for_attributes = [:time_made]
# String, not GlobalId
attr_accessor :observation_object_global_id
belongs_to :character_state, inverse_of: :observations
belongs_to :descriptor, inverse_of: :observations
belongs_to :observation_object, polymorphic: true
# before_validation :convert_observation_object_global_id
before_validation :set_type_from_descriptor
validates_presence_of :descriptor_id, :type
validates_presence_of :observation_object
validate :type_matches_descriptor
validates :year_made, date_year: { min_year: 1757, max_year: -> {Time.now.year} }
validates :month_made, date_month: true
validates :day_made, date_day: {year_sym: :year_made, month_sym: :month_made}, unless: -> {year_made.nil? || month_made.nil?}
# depends on timeliness 6.0, which is breaking something
# validates_time :time_made, allow_nil: true
def qualitative?
type == 'Observation::Qualitative'
end
def presence_absence?
type == 'Observation::PresenceAbsence'
end
def continuous?
type == 'Observation::Continuous'
end
def self.in_observation_matrix(observation_matrix_id)
Observation.joins('JOIN observation_matrix_rows omr on (omr.observation_object_type = observations.observation_object_type AND omr.observation_object_id = observations.observation_object_id)')
.joins('JOIN observation_matrix_columns omc on omc.descriptor_id = observations.descriptor_id')
.where('omr.observation_matrix_id = ? AND omc.observation_matrix_id = ?', observation_matrix_id, observation_matrix_id)
end
def self.by_matrix_and_position(observation_matrix_id, options = {})
opts = {
row_start: 1,
row_end: 'all',
col_start: 1,
col_end: 'all'
}.merge!(options.symbolize_keys)
return in_observation_matrix(observation_matrix_id).order('omc.position, omr.position') if opts[:row_start] == 1 && opts[:row_end] == 'all' && opts[:col_start] == 1 && opts[:col_end] == 'all'
base = Observation.joins('JOIN observation_matrix_rows omr on (omr.observation_object_type = observations.observation_object_type AND omr.observation_object_id = observations.observation_object_id)')
.joins('JOIN observation_matrix_columns omc on omc.descriptor_id = observations.descriptor_id')
.where('omr.observation_matrix_id = ? AND omc.observation_matrix_id = ?', observation_matrix_id, observation_matrix_id)
# row scope
base = base.where('omr.position >= ?', opts[:row_start])
base = base.where('omr.position <= ?', opts[:row_end]) if !(opts[:row_end] == 'all')
# col scope
base = base.where('omc.position >= ?', opts[:col_start])
base = base.where('omc.position <= ?', opts[:col_end]) if !(opts[:col_end] == 'all')
base
end
def self.by_observation_matrix_row(observation_matrix_row_id)
Observation.joins('JOIN observation_matrix_rows omr on (omr.observation_object_type = observations.observation_object_type AND omr.observation_object_id = observations.observation_object_id)')
.joins('JOIN observation_matrix_columns omc on omc.descriptor_id = observations.descriptor_id')
.where('omr.id = ?', observation_matrix_row_id)
.order('omc.position')
end
# TODO: deprecate or remove
def self.object_scope(object)
return Observation.none if object.nil?
Observation.where(observation_object: object)
end
def self.human_name
'YAY'
end
def observation_object_global_id=(value)
self.observation_object = GlobalID::Locator.locate(value)
@observation_object_global_id = value
end
# @return [String]
# TODO: this is not memoized correctly ?!
def observation_object_global_id
if observation_object
observation_object.to_global_id.to_s
else
@observation_object_global_id
end
end
# @return [Boolean]
# @params old_global_id [String]
# global_id of collection object or Otu
#
# @params new_global_id [String]
# global_id of collection object or Otu
#
def self.copy(old_global_id, new_global_id)
begin
old = GlobalID::Locator.locate(old_global_id)
Observation.transaction do
old.observations.each do |o|
d = o.dup
d.update(observation_object_global_id: new_global_id)
# Copy depictions
o.depictions.each do |i|
j = i.dup
j.update(depiction_object: d)
end
end
end
true
rescue
return false
end
true
end
# TODO: Does this belong here?
# Remove all observations for the set of descriptors in a given row
def self.destroy_row(observation_matrix_row_id)
r = ObservationMatrixRow.find(observation_matrix_row_id)
begin
Observation.transaction do
r.observations.destroy_all
end
rescue
raise
end
true
end
protected
def set_type_from_descriptor
if type.blank? && descriptor&.type
write_attribute(:type, 'Observation::' + descriptor.type.split('::').last)
end
end
def type_matches_descriptor
a = type&.split('::')&.last
b = descriptor&.type&.split('::')&.last
errors.add(:type, 'type of Observation does not match type of Descriptor') if a && b && a != b
end
end
Dir[Rails.root.to_s + '/app/models/observation/**/*.rb'].each { |file| require_dependency file }