· 8 years ago · Jun 02, 2018, 11:38 AM
1Tons of "legacy" code in here, so go easy! ;)
2
3# == Schema Information
4#
5# Table name: otus
6#
7# id :integer(10) not null, primary key
8# taxon_name_id :integer(10)
9# is_child :boolean(1)
10# name :string(255)
11# manuscript_name :string(255)
12# matrix_name :string(64)
13# parent_otu_id :integer(10)
14# as_cited_in :integer(10)
15# revision_history :text
16# iczn_group :string(32)
17# syn_with_otu_id :integer(10)
18# sensu :string(255)
19# notes :text
20# proj_id :integer(10) not null
21# creator_id :integer(10) not null
22# updator_id :integer(10) not null
23# updated_on :timestamp not null
24# created_on :timestamp not null
25#
26
27class Otu < ActiveRecord::Base
28 has_standard_fields
29
30 has_many :association_parts, :dependent => :destroy # need to change this to a through relationship
31 has_many :associations, :through => :association_parts
32 has_many :claves, :class_name => "Clave", :dependent => :nullify
33 has_many :codings, :dependent => :destroy
34 has_many :contents, :dependent => :destroy
35 has_many :content_types, :through => :contents
36
37## This is the problem
38 has_many :distributions, :dependent => :destroy, :include => :geogs, :order => 'geogs.name'
39 has_many :geogs, :through => :distributions
40
41 has_many :image_descriptions, :dependent => :destroy
42
43 has_many :immediate_child_synonymous_otus, :class_name => "Otu", :foreign_key => "syn_with_otu_id", :dependent => :nullify
44
45 has_many :lots, :dependent => :destroy
46 has_many :public_tags, :as => :addressable, :class_name => "Tag", :include => [:keyword, :ref], :order => 'refs.display_name ASC', :conditions => 'keywords.is_public = true'
47 has_many :seqs, :dependent => :destroy
48 has_many :specimen_determinations, :dependent => :destroy
49 has_many :specimens, :through => :specimen_determinations
50 has_many :tags, :as => :addressable, :dependent => :destroy, :include => [:keyword, :ref], :order => 'refs.display_name ASC'
51
52 has_many :mxes_otus
53 has_many :mxes, :through => :mxes_otus, :order => 'mxes.name'
54
55 # has_and_belongs_to_many :statements
56
57 has_many :otu_groups_otus
58 has_many :otu_groups, :through => :otu_groups_otus, :source => :otu_group, :order => 'otu_groups.name'
59
60 belongs_to :parent_otu, :class_name => "Otu", :foreign_key => "parent_otu_id"
61 belongs_to :ref, :foreign_key => "as_cited_in"
62 belongs_to :sensu_ref, :class_name => "Ref", :foreign_key => "sensu_ref_id"
63 belongs_to :syn_otu, :class_name => "Otu", :foreign_key => "syn_with_otu_id"
64 belongs_to :taxon_name
65
66 # Careful- Otu.in_matrix_range will return Otus from different matrices, use as Mx#otus#in_mx_position
67 # starts at 1!!
68 named_scope :within_mx_range, lambda {|*args| {:include => :mxes_otus, :conditions => ["mxes_otus.position >= ? AND mxes_otus.position <= ?", (args.first || -1), (args[1] || -1)]}}
69
70 def validate
71 if matrix_name =~ /\W/
72 errors.add(:mx_name, "can not contain whitespace")
73 end
74
75 # synonymy
76 ## IMPORTANT need to add check for circularity
77 if (syn_with_otu_id == self.id) && !self.id.nil?
78 errors.add(:syn_with_otu_id, "can not be synonymous with self")
79 end
80 end
81
82 def multi_name
83 s = ""
84 s += "<span class=\"otu_taxon_name\">#{taxon_name.display_name}</span> " if taxon_name
85 s += "<span class=\"otu_name\">#{(name)}</span> " if name?
86 s += "<span class=\"otu_manuscript_name\">#{(manuscript_name)}</span> " if manuscript_name?
87 s += "<span class=\"otu_matrix_name\">#{(matrix_name)}</span> " if matrix_name?
88 s
89 end
90
91 def dual_name # taxon name and OTU name
92 s = ""
93 s += "<span class=\"otu_taxon_name\">#{taxon_name.display_name}</span> " if taxon_name
94 s += " / " if taxon_name and name?
95 s += "<span class=\"otu_name\">#{(name)}</span> " if name?
96 s
97 end
98
99 def picker_name
100 s = ""
101 s += taxon_name.display_name if taxon_name
102 s += " / " if taxon_name and name?
103 s += name if name
104 end
105
106 def display_name ## needs to change to magic
107 taxon_name ? taxon_name.display_name : name
108 end
109
110 def display_for_list
111 multi_name
112 end
113
114 def display_for_select
115 s = ""
116 s += "TN:#{taxon_name.display_for_list}" if taxon_name
117 s += " N:#{name}" if name?
118 s += " MNN:#{manuscript_name}" if manuscript_name?
119 s += " MXN:#{matrix_name}" if matrix_name?
120 s
121 end
122
123 def display_taxon_name_for_select
124 taxon_name.display_for_list if taxon_name
125 end
126
127 def display_mx_name
128 return self.matrix_name if !self.matrix_name.blank?
129 self.display_name
130 end
131
132 def display_top_syn_name # Returns the *TOP* synonym
133 if self.syn_otu
134 self.top_syn(self.syn_with_otu_id).display_name
135 else
136 display_name
137 end
138 end
139
140 def top_syn(syn)
141 o = Otu.find(syn)
142 if o.syn_otu
143 top_syn(o.syn_otu.id)
144 else
145 o
146 end
147 end
148
149 def mx_name # not to be confused with matrix_name, this is used to render legal analysis ready names
150 return matrix_name if not matrix_name.to_s.length < 1 # not matrix_name.to_s.length < 1
151 return name.gsub(/[^\w]/, "_") if not name.to_s.length < 1 ## concievably add id and truncate here to ensure uniqueness
152 return taxon_name.display_name.gsub(/[^\w^\<^\>^\/]/, "_") if taxon_name
153 return "mx_otu_id_#{self.id}"
154 end
155
156 # general display methods
157
158 def self.valid_iczn_groups
159 ['species','genus','family', 'higher', 'other']
160 end
161
162 # images
163
164 ## CLEANUP to use self.proj_id for these functions
165 # this should be :through, even when shared
166 def images
167 self.image_descriptions.collect{|o| o.image}.uniq
168 end
169
170 def mb_image_descriptions
171 ImageDescription.find_by_sql(["Select * from image_descriptions id left join images i on id.image_id = i.id WHERE i.mb_id is not null and id.otu_id = ?;", self.id])
172 end
173
174 def has_image_of_mb_id(mb_id)
175 ImageDescription.find_by_sql(["SELECT * from image_descriptions id left join images i on id.image_id = i.id WHERE id.otu_id = ? AND i.mb_id = ?;", self.id, mb_id])
176 end
177
178 # why not just a has many? changed for now, hopefully won't bork things?!
179
180 # def image_descriptions(proj_id = self.proj_id) ## MEH! use the OTUs proj_id -- can't use a :through because we need proj_id WHY?!
181 # ImageDescription.find_all_by_otu_id_and_proj_id(self.id, self.proj_id)
182 # end
183
184 def move_images_to_otu(to_otu_id)
185 o = Otu.find(to_otu_id) or return false
186 # (self.proj_id)
187 self.image_descriptions.each do |i|
188 i.otu_id = o.id
189 i.save!
190 end
191 true
192 end
193
194 # updates the otu_id of the many side to of current records to the passed Otu
195 # has_many_rel is a model name, as a string
196 def transfer_has_manys_to_otu(otu, has_many_rel)
197 return false if self.id == otu.id # can't transfer to yourself
198 self.send(has_many_rel).each do |o|
199 o.otu_id = otu.id
200 o.save
201 end
202 end
203
204 # appends all the content to the provided Otu, :del => true will delete the old conten
205 def transfer_content_to_otu(otu, delete_from_incoming = false)
206 return false if self.id == otu.id # can't transfer to yourself
207 self.contents.each do |c|
208 c.transfer_to_otu(otu)
209 end
210 true
211 end
212
213 def publish_all_content
214 self.contents.each do |c|
215 c.publish
216 end
217 end
218
219 # matrix specific methods
220
221 def self.find_coded_for(chr_id)
222 find_by_sql ["SELECT otus.* FROM otus LEFT JOIN codings on otus.id = codings.otu_id WHERE codings.chr_id = ? AND codings.id IS NOT NULL", chr_id]
223 end
224
225 def self.find_coded_for_state(chr_state_id)
226 find_by_sql ["SELECT otus.* FROM otus LEFT JOIN codings on otus.id = codings.otu_id WHERE codings.chr_state_id = ? AND codings.id IS NOT NULL", chr_state_id]
227 end
228
229 def codings_by_chr(chr_group_id = nil)
230 if chr_group_id
231 chrs = Chr.find_by_sql ["SELECT chrs.* FROM chrs LEFT JOIN chr_groups_chrs ON chrs.id = chr_groups_chrs.chr_id LEFT JOIN codings ON chrs.id = codings.chr_id WHERE codings.otu_id = ? AND codings.id IS NOT NULL AND chr_groups_chrs.chr_group_id = ?", id, chr_group_id]
232 else
233 chrs = Chr.find_by_sql ["SELECT chrs.* FROM chrs LEFT JOIN codings ON chrs.id = codings.chr_id WHERE codings.otu_id = ? AND codings.id IS NOT NULL", id]
234 end
235 foobar = Hash.new
236 # for each chr, set it as a key. then set the array of corresponding codings as the value
237 for chr in chrs
238 foobar[chr] = codings.select {|c| c.chr_id == chr.id}
239 end
240 foobar
241 end
242
243 # returns all Characters for which the Otu is uniquely coded for (across all codings) --- needs a better name
244 def unique_codings_by_chr
245 @chars = []
246 for c in self.codings
247 (@chars << [ Chr.find(c.chr_id), ChrState.find(c.chr_state_id)]) if c.similarly_coded_otus.size == 1
248 end
249 @chars
250 end
251
252 # returns all Codings that represent "diagnostic" states
253 def unique_codings
254 Coding.find_by_sql(["Select codings.*, count(chr_state_id) as cnt from codings group by chr_state_id having cnt = 1 and otu_id = ?;", self.id])
255 end
256
257 # return all char_state_ids for a given matrix
258 def chr_states_by_mx(mx_id)
259 mx = Mx.find(mx_id) or throw "can't find the matrix in Otu.chr_states_by_mx"
260 sql = mx.chrs.inject([]) {|sum, c| sum + c.chr_states.collect{|o| o.id}}.inject([]){|s, o| s << "chr_state_id = #{o}"}.join(' OR ')
261 Coding.find(:all, :conditions => "(#{sql}) AND otu_id = #{self.id}").collect{|o| o.chr_state_id}
262 end
263
264 # as unique_codings, but returns an Array of chr_state ids
265 def unique_states
266 Coding.find_by_sql(["SELECT otu_id, chr_state_id, count(chr_state_id) as cnt FROM codings GROUP BY chr_state_id HAVING cnt = 1 and otu_id = ?", self.id]).collect{|o| o.chr_state_id.to_i}
267 end
268
269 # project universal methods
270
271 def self.find_for_auto_complete(value)
272 value.downcase!
273 find_by_sql(["SELECT o.* FROM otus AS o
274 LEFT JOIN taxon_names t ON o.taxon_name_id = t.id LEFT JOIN taxon_names t2 ON t.parent_id = t2.id
275 WHERE o.proj_id = #{$proj_id} AND (
276 (t.name LIKE ?) OR
277 (t2.name LIKE ? AND t.name LIKE ?) OR
278 (o.manuscript_name LIKE ?) OR
279 (o.name LIKE ?) OR
280 (o.matrix_name LIKE ?) OR
281 (o.id = ?)
282 );",
283 "#{value}%", "#{value.split.first}%", "#{value.split.last}%", "#{value}%", "%#{value}%", "#{value}%", value.gsub(/.[\D\s]/, '') ])
284 end
285
286 def all_synonymous_otus(already_collected = [])
287 # use already_collected to prevent loops
288 return already_collected if already_collected.detect { |e| e == self }
289 syns = immediate_child_synonymous_otus
290 syns.map { |s| s.all_synonymous_otus(syns) }.flatten
291 end
292
293 # extract specific methods
294
295 def extract_summary
296 meta = {'attempted' => 0, 'genes_attempted' => '', 'quality' => '', 'available_specimens' => 0, 'chromatograms_attempted' => ''}
297
298 @specimens = Specimen.with_usable_dna.determined_as_otu(self)
299 @lots = self.lots.with_usable_dna
300 @extracts = (Extract.from_specimens_determined_as_otu(self) + Extract.from_lots_determined_as_otu(self)).uniq # shouldn't need uniq ultimately
301
302 @genes_tagged_to_seqs = Proj.find(self.proj_id).genes.used_in_seqs_from_otu(self)
303 @chromatograms = []
304
305 # this needs to be simplified somehow, like Otu.chromatograms
306 @extracts.each do |extract|
307 @chromatograms += extract.pcrs.collect{|o| o.chromatograms}
308 end
309
310 meta['available_specimens'] = @specimens.size + @lots.collect{|o| o.value_specimens}.inject(0) { |n, value| n + value }
311 meta['attempted'] = @extracts.size
312 meta['quality'] = (@extracts.size > 0) ? @extracts.collect{|o| o.quality}.join("; ") : "<i>no extracts</i>"
313 meta['genes_attempted'] = @genes_tagged_to_seqs.size > 0 ? @genes_tagged_to_seqs.collect{|o| o.name}.join("; ") : ""
314 meta['chromatograms_attempted'] = @chromatograms.size
315 meta
316 end
317
318 # content specific methods
319
320 # returns a hash with the ContentType id pointing to the Content
321 def text_content
322 Content.private.by_otu(self).inject({}){|hash, c| hash.update(c.content_type_id => c)} # note there shouldn't be 2 private contents of the same type for the same OTU, if there is "bad things"
323 end
324
325 # should make this redundant with above
326 # if you want to render an otu page for a given template, pass this a template_id.
327 def public_contents(template_id = nil)
328 if template_id
329 cns = Content.find_by_sql(["SELECT contents.* from contents
330 LEFT JOIN content_templates_content_types ctct ON ctct.content_type_id = contents.content_type_id
331 WHERE contents.otu_id = ? AND ctct.content_template_id = ?
332 ORDER BY ctct.position", self.id, template_id])
333 else
334 cns = self.contents
335 end
336
337 # note that if you used "memo.push(c.public_version) if c.public_version", you might get an empty array
338 cns.inject([]) {|memo, o| o.public_version ? memo.push(o.public_version) : memo}
339 end
340
341 def has_public_content?
342 return true if self.public_contents.size > 0
343 false
344 end
345
346 # specimen/ce methods
347
348 def specimens_most_recently_determined_as
349 self.specimens.inject([]){|ary, s| s.most_recent_determination.otu.id == self.id ? ary << s : ary}
350 end
351
352 def markers_for_currently_determined_specimens
353 self.specimens_most_recently_determined_as.inject([]){|ary, s| s.mappable ? ary << s.ce.gmap_hash.update({:specimen => s.display_identifiers}) : ary}
354 end
355
356end
357
358
359# a class which organizes Objects with a taxon_name_id into sets for use in display, does *NOT* take into account visibility at present, assumes the whole heirarchy is present
360# this is essentially the intersection of two trees
361
362# we have 1:1, 1:many, and many:many
363
364class ByTnDisplay
365 attr_reader :sections, :unplaced_items
366
367 # takes a TaxonName and an arry of objects that have an otu_id
368 def initialize(taxon_name, items = [])
369 @tn = taxon_name # the root name
370 @items = items
371
372 @unplaced_items = []
373 @sections = []
374
375 # grab the unplaced items
376 @items.each do |i|
377 if i.taxon_name_id == nil
378 @unplaced_items << i
379 end
380 end
381
382 @unplaced_items.each do |i| # have to do this outside the loop or wonkiness ensues
383 @items.delete(i)
384 end
385
386 @tn_set = @tn.full_set # grab the set so we can speedup
387 s = {} # internal tracking for building the sections, tn.id => a @section index
388
389 @tn.full_set.each do |t| # we have to outer loop for order
390 j = 0
391 @items[j..@items.size-1].each do |i|
392 if i.taxon_name_id == t.id
393 # if the section exists add it there
394 if s.keys.include?(t.id)
395 @sections[s[t.id]].items << i
396 else
397 sn = ByTnDisplay::Section.new(t) # make a new section
398 sn.items << i # add the initial object to it
399 s.update(t.id => (@sections.size - 1)) # create an internal reference by taxon name
400 @sections << sn
401 end
402 end
403 end
404 j += 1
405 end
406 @sections.reverse!
407 end
408
409 class Section
410 attr_reader :header, :items
411 # header is a taxon_name
412 def initialize(header)
413 @header = header
414 @items = []
415 end
416 end
417
418end
419
420# == Schema Information
421#
422# Table name: distributions
423#
424# id :integer(10) not null, primary key
425# geog_id :integer(10)
426# otu_id :integer(10)
427# ref_id :integer(10)
428# confidence_id :integer(10)
429# verbatim_geog :string(255)
430# introduced :boolean(1)
431# num_specimens :integer(10) not null
432# notes :text
433# proj_id :integer(10) not null
434# creator_id :integer(10) not null
435# updator_id :integer(10) not null
436# updated_on :timestamp not null
437# created_on :timestamp not null
438
439
440# these are essentially records extracted from the literature
441class Distribution < ActiveRecord::Base
442 has_standard_fields
443 belongs_to :otu
444 belongs_to :ref
445 belongs_to :confidence
446 belongs_to :geog
447
448 validates_presence_of :otu, :ref, :geog
449
450 def display_name
451 otu.display_name + " / " + ref.authors_year + " / " + geog.display_name
452 end
453
454
455end
456
457# == Schema Information
458#
459# Table name: geogs
460#
461# id :integer(10) not null, primary key
462# name :string(255) default(""), not null
463# abbreviation :string(64)
464# fips_code :integer(10)
465# sort_NS :integer(10)
466# sort_WE :integer(10)
467# center_lat :string(64)
468# center_long :string(64)
469# geog_type_id :integer(10)
470# inclusive_biogeo_region_id :integer(10)
471# country_id :integer(10)
472# state_id :integer(10)
473# county_id :integer(10)
474# continent_ocean_id :integer(10)
475# namespace_id :integer(10)
476# external_id :integer(10)
477# creator_id :integer(10) not null
478# updator_id :integer(10) not null
479# updated_on :timestamp not null
480# created_on :timestamp not null
481#
482
483class Geog < ActiveRecord::Base
484 has_standard_fields
485 belongs_to :geog_type
486 belongs_to :country, :class_name => "Geog", :foreign_key => "country_id"
487 belongs_to :state, :class_name => "Geog", :foreign_key => "state_id"
488 belongs_to :county, :class_name => "Geog",:foreign_key => "county_id"
489 belongs_to :continent_ocean, :class_name => "Geog", :foreign_key => "continent_ocean_id"
490 belongs_to :biogeo_region, :class_name => "Geog",:foreign_key => "inclusive_biogeo_region_id"
491
492 has_many :ces
493 has_many :tags, :as => :addressable, :dependent => :destroy, :include => [:keyword, :ref], :order => 'refs.display_name ASC'
494
495## This is the problem
496 has_many :distributions
497 has_many :otus, :through => :distributions
498
499 validates_presence_of :geog_type_id
500
501 def display_for_select
502 n = name
503 n << " [#{self.geog_type.name}]" if self.geog_type
504 n << " #{self.country.name}" if (self.country and not self.geog_type.name == 'country')
505 n
506 end
507
508 def display_name
509 type_name = (self.geog_type ? self.geog_type.name : 'none')
510 n = ''
511 n << "#{self.country.name}: " if (self.country and type_name != 'country')
512 n << "#{self.state.name}: " if (self.state_id and self.id != self.state_id) # may have a 'state', but type_name may be 'province'
513 n << (type_name == 'county' ? self.name + " Co." : self.name)
514 end
515
516 def display_for_list
517 display_name[0..25]
518 end
519end