Class: Lead

Inherits:
ApplicationRecord show all
Includes:
Housekeeping, Shared::Attributions, Shared::Citations, Shared::Depictions, Shared::IsData, Shared::Tags
Defined in:
app/models/lead.rb

Overview

Leads model multifurcating keys: each lead represents one option in a key. The set of options at a given step of the key is referred to as a ‘couplet’ even when there are more than two options.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Shared::IsData

#errors_excepting, #full_error_messages_excepting, #identical, #is_community?, #is_destroyable?, #is_editable?, #is_in_use?, #is_in_users_projects?, #metamorphosize, #similar

Methods included from Shared::Tags

#reject_tags, #tag_with, #tagged?, #tagged_with?

Methods included from Shared::Attributions

#attributed?, #reject_attribution

Methods included from Shared::Depictions

#has_depictions?, #image_array=, #reject_depictions, #reject_images

Methods included from Shared::Citations

#cited?, #mark_citations_for_destruction, #nomenclature_date, #origin_citation_source_id, #reject_citations, #requires_citation?, #sources_by_topic_id

Methods included from Housekeeping

#has_polymorphic_relationship?

Methods inherited from ApplicationRecord

transaction_with_retry

Instance Attribute Details

#descriptiontext

Only used on the root node, to describe the overall key

Returns:



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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'app/models/lead.rb', line 47

class Lead < ApplicationRecord
  include Housekeeping
  include Shared::Citations
  include Shared::Depictions
  include Shared::Attributions
  include Shared::Tags
  include Shared::IsData

  has_closure_tree order: 'position', numeric_order: true, dont_order_roots: true, dependent: nil

  belongs_to :parent, class_name: 'Lead'

  belongs_to :otu, inverse_of: :leads
  has_one :taxon_name, through: :otu
  belongs_to :redirect, class_name: 'Lead'

  has_many :redirecters, class_name: 'Lead', foreign_key: :redirect_id, inverse_of: :redirect, dependent: :nullify

  before_save :set_is_public_only_on_root

  validate :root_has_title
  validate :link_out_has_protocol
  validate :redirect_node_is_leaf_node
  validate :node_parent_doesnt_have_redirect
  validate :root_has_no_redirect
  validate :redirect_isnt_ancestor_or_self

  def future
    redirect_id.blank? ? all_children : redirect.all_children
  end

  # @return [Boolean] true on success, false on error
  def dupe
    if parent_id
      errors.add(:base, 'Can only call dupe on a root lead')
      return false
    end

    begin
      Lead.transaction do
        dupe_in_transaction(self, parent_id)
      end
    rescue ActiveRecord::RecordInvalid => e
      errors.add(:base, "dup failed: #{e}")
      return false
    end

    true
  end

  # left/right/middle
  def node_position
    o = self_and_siblings.order(:position).pluck(:id)
    return :root if o.size == 1
    return :left if o.index(id) == 0
    return :right if o.last == id
    return :middle
  end

  def self.draw(lead, indent = 0)
    puts Rainbow( (' ' * indent) + lead.text ).purple + ' ' + Rainbow( lead.node_position.to_s ).red + ' ' + Rainbow( lead.position ).blue + ' [.parent: ' + Rainbow(lead.parent&.text || 'none').gold + ']'
    lead.children.each do |c|
      draw(c, indent + 1)
    end
  end

  # Return [Boolean] true on success, false on failure
  def insert_key(id)
    if id.nil?
      errors.add(:base, 'Id of key to insert not provided')
      return false
    end
    begin
      Lead.transaction do
        key_to_insert = Lead.find(id)
        if key_to_insert.dupe != true
          raise TaxonWorks::Error, 'dupe failed'
        end

        dup_prefix = '(COPY OF)'
        duped_text = "#{dup_prefix} #{key_to_insert.text}"
        duped_root = Lead.where(text: duped_text).order(:id).last
        if duped_root.nil?
          raise TaxonWorks::Error,
            "failed to find the duped key with text '#{duped_text}'"
        end

        # Prepare the duped key to be reparented - note that root leads don't
        # have link_out_text set (the url link is applied to the title in that
        # case), so link_out_text in the inserted root lead will always be nil.
        duped_root.update!(
          text: duped_root.text.sub(dup_prefix, '(INSERTED KEY) '),
          description: nil,
          is_public: nil
        )

        add_child(duped_root)
      end
    rescue ActiveRecord::RecordNotFound => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    rescue TaxonWorks::Error => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    rescue ActiveRecord::RecordInvalid => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    end

    true
  end

  def insert_couplet
    c, d = nil, nil

    p = node_position

    t1 = 'Inserted node'
    t2 = 'Child nodes are attached to this node'

    if children.any?
      o = children.to_a

      # Reparent under left inserted node unless this is itself a right node.
      left_text = (p != :right) ? t2 : t1
      right_text = (p == :right) ? t2 : t1

      c = Lead.create!(text: left_text, parent: self)
      d = Lead.create!(text: right_text, parent: self)

      new_parent = (p != :right) ? c : d

      Lead.reparent_nodes(o, new_parent)
    else
      c = Lead.create!(text: t1, parent: self)
      d = Lead.create!(text: t1, parent: self)
    end

    [c.id, d.id]
  end

  # Destroy the children of this Lead, re-appending the grandchildren to self
  # !! Do not destroy children if more than one child has its own children
  def destroy_children
    k = children.to_a
    return true if k.empty?

    original_child_count = k.count
    grandkids = []
    k.each do |c|
      if c.children.present?
        # Fail if more than one child has its own children
        return false if grandkids.present?

        grandkids = c.children
      end
    end

    Lead.reparent_nodes(grandkids, self)

    children.slice(0, original_child_count).each do |c|
      c.destroy!
    end

    true
  end

  def transaction_nuke(lead = self)
    Lead.transaction do
      nuke(lead)
    end
  end

  def nuke(lead = self)
    lead.children.each do |c|
      c.nuke(c)
    end
    destroy!
  end

  # TODO: Probably a helper method
  def all_children(node = self, result = [], depth = 0)
    for c in node.children.to_a.reverse # intentionally reversed
      c.all_children(c, result, depth + 1)
      a = {}
      a[:depth] = depth
      a[:lead] = c
      a[:leadLabel] = node.origin_label
      result.push(a)
    end
    result
  end

  # TODO: Probably a helper method
  def all_children_standard_key(node = self, result = [], depth = 0) # couplets before depth
    ch = node.children
    for c in ch
      a = {}
      a[:depth] = depth
      a[:lead] = c
      result.push(a)
    end

    for c in ch
      c.all_children_standard_key(c, result, depth + 1)
    end
    result
  end

  # @param reorder_list [Array] array of 0-based positions in which to order
  #  the children of this lead.
  # Raises TaxonWorks::Error on error.
  def reorder_children(reorder_list)
    validate_reorder_list(reorder_list, children.count)

    i = 0
    Lead.transaction do
      children.each do |c|
        if (new_position = reorder_list[i]) != i
          c.update_column(:position, new_position)
        end
        i = i + 1
      end
    end
  end

  # @return [ActiveRecord::Relation] ordered by text.
  # Returns all root nodes, with new properties:
  #  * couplets_count (declared here, computed in views)
  #  * otus_count (total number of distinct otus on the key)
  #  * key_updated_at (last time the key was updated)
  #  * key_updated_by_id (id of last person to update the key)
  #  * key_updated_by (name of last person to update the key)
  #
  # !! Note, the relation is a join, check your results when changing order
  # or plucking, most of want you want is on the table joined to, which is
  # not the default table for ordering and plucking.
  def self.roots_with_data(project_id, load_root_otus = false)
    # The updated_at subquery computes key_updated_at (and others), the second
    # query uses that to compute key_updated_by (by finding which node has the
    # corresponding key_updated_at).
    updated_at = Lead
      .joins('JOIN lead_hierarchies AS lh
        ON leads.id = lh.ancestor_id')
      .joins('JOIN leads AS otus_source
        ON lh.descendant_id = otus_source.id')
      .where("
        leads.parent_id IS NULL
        AND leads.project_id = #{project_id}
      ")
      .group(:id)
      .select('
        leads.*,
        COUNT(DISTINCT otus_source.otu_id) AS otus_count,
        MAX(otus_source.updated_at) AS key_updated_at,
        0 AS couplets_count' # count is now computed in views
        #·(COUNT(otus_source.id) - 1) / 2 AS couplet_count
      )

    root_leads = Lead
      .joins("JOIN (#{updated_at.to_sql}) AS leads_updated_at
        ON leads_updated_at.key_updated_at = leads.updated_at")
      .joins('JOIN users
        ON users.id = leads.updated_by_id')
      .select('
        leads_updated_at.*,
        leads.updated_by_id AS key_updated_by_id,
        users.name AS key_updated_by
      ')
      .order('leads_updated_at.text')

    return load_root_otus ? root_leads.includes(:otu) : root_leads
  end

  def redirect_options(project_id)
    leads = Lead
      .select(:id, :text, :origin_label)
      .with_project_id(project_id)
      .order(:text)
    anc_ids = ancestor_ids()

    leads.filter_map do |o|
      if o.id != id && !anc_ids.include?(o.id)
        {
          id: o.id,
          label: o.origin_label,
          text: o.text.nil? ? '' : o.text.truncate(40)
        }
      end
    end
  end

  private

  # Appends `nodes`` to the children of `new_parent``, in their given order.
  # !! Use this instead of add_child to reparent multiple nodes (add_child
  # doesn't work the way one might naievely expect, see the discussion at
  # https://github.com/SpeciesFileGroup/taxonworks/pull/3892#issuecomment-2016043296)
  def self.reparent_nodes(nodes, new_parent)
    last_sibling = new_parent.children.last # may be nil
    nodes.each do |n|
      if last_sibling.nil?
        new_parent.add_child(n)
      else
        last_sibling.append_sibling(n)
      end
      last_sibling = n
    end
  end

  # Raises TaxonWorks::Error on error.
  def validate_reorder_list(reorder_list, expected_length)
    if reorder_list.sort.uniq != (0..expected_length - 1).to_a
      raise TaxonWorks::Error,
        "Bad reorder list: #{reorder_list}"
      return
    end
  end

  def root_has_title
    errors.add(:root_node, 'must have a title') if parent_id.nil? and text.nil?
  end

  def link_out_has_protocol
    errors.add(:link, 'must include https:// or http://') if !link_out.nil? && !(link_out.start_with?('https://') || link_out.start_with?('http://'))
  end

  def redirect_node_is_leaf_node
    errors.add(:redirect, "nodes can't have children") if redirect_id && children.size > 0
  end

  def node_parent_doesnt_have_redirect
    errors.add(:parent, "can't be a redirect node") if parent && parent.redirect_id
  end

  def redirect_isnt_ancestor_or_self
    errors.add(:redirect, "can't be an ancestor") if redirect_id && (redirect_id == id || ancestor_ids.include?(redirect_id))
  end

  def root_has_no_redirect
    errors.add(:root, "nodes can't have a redirect") if redirect_id && parent_id.nil?
  end

  def dupe_in_transaction(node, parentId)
    a = node.dup
    a.parent_id = parentId
    a.text = "(COPY OF) #{a.text}" if parentId == nil
    a.save!

    node.children.each { |c| dupe_in_transaction(c, a.id) }
  end

  def set_is_public_only_on_root
    if parent_id.nil?
      self.is_public ||= false
    else
      self.is_public = nil
    end
  end

end

#is_publicboolean

True if the key is viewable without being logged in

Returns:

  • (boolean)


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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'app/models/lead.rb', line 47

class Lead < ApplicationRecord
  include Housekeeping
  include Shared::Citations
  include Shared::Depictions
  include Shared::Attributions
  include Shared::Tags
  include Shared::IsData

  has_closure_tree order: 'position', numeric_order: true, dont_order_roots: true, dependent: nil

  belongs_to :parent, class_name: 'Lead'

  belongs_to :otu, inverse_of: :leads
  has_one :taxon_name, through: :otu
  belongs_to :redirect, class_name: 'Lead'

  has_many :redirecters, class_name: 'Lead', foreign_key: :redirect_id, inverse_of: :redirect, dependent: :nullify

  before_save :set_is_public_only_on_root

  validate :root_has_title
  validate :link_out_has_protocol
  validate :redirect_node_is_leaf_node
  validate :node_parent_doesnt_have_redirect
  validate :root_has_no_redirect
  validate :redirect_isnt_ancestor_or_self

  def future
    redirect_id.blank? ? all_children : redirect.all_children
  end

  # @return [Boolean] true on success, false on error
  def dupe
    if parent_id
      errors.add(:base, 'Can only call dupe on a root lead')
      return false
    end

    begin
      Lead.transaction do
        dupe_in_transaction(self, parent_id)
      end
    rescue ActiveRecord::RecordInvalid => e
      errors.add(:base, "dup failed: #{e}")
      return false
    end

    true
  end

  # left/right/middle
  def node_position
    o = self_and_siblings.order(:position).pluck(:id)
    return :root if o.size == 1
    return :left if o.index(id) == 0
    return :right if o.last == id
    return :middle
  end

  def self.draw(lead, indent = 0)
    puts Rainbow( (' ' * indent) + lead.text ).purple + ' ' + Rainbow( lead.node_position.to_s ).red + ' ' + Rainbow( lead.position ).blue + ' [.parent: ' + Rainbow(lead.parent&.text || 'none').gold + ']'
    lead.children.each do |c|
      draw(c, indent + 1)
    end
  end

  # Return [Boolean] true on success, false on failure
  def insert_key(id)
    if id.nil?
      errors.add(:base, 'Id of key to insert not provided')
      return false
    end
    begin
      Lead.transaction do
        key_to_insert = Lead.find(id)
        if key_to_insert.dupe != true
          raise TaxonWorks::Error, 'dupe failed'
        end

        dup_prefix = '(COPY OF)'
        duped_text = "#{dup_prefix} #{key_to_insert.text}"
        duped_root = Lead.where(text: duped_text).order(:id).last
        if duped_root.nil?
          raise TaxonWorks::Error,
            "failed to find the duped key with text '#{duped_text}'"
        end

        # Prepare the duped key to be reparented - note that root leads don't
        # have link_out_text set (the url link is applied to the title in that
        # case), so link_out_text in the inserted root lead will always be nil.
        duped_root.update!(
          text: duped_root.text.sub(dup_prefix, '(INSERTED KEY) '),
          description: nil,
          is_public: nil
        )

        add_child(duped_root)
      end
    rescue ActiveRecord::RecordNotFound => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    rescue TaxonWorks::Error => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    rescue ActiveRecord::RecordInvalid => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    end

    true
  end

  def insert_couplet
    c, d = nil, nil

    p = node_position

    t1 = 'Inserted node'
    t2 = 'Child nodes are attached to this node'

    if children.any?
      o = children.to_a

      # Reparent under left inserted node unless this is itself a right node.
      left_text = (p != :right) ? t2 : t1
      right_text = (p == :right) ? t2 : t1

      c = Lead.create!(text: left_text, parent: self)
      d = Lead.create!(text: right_text, parent: self)

      new_parent = (p != :right) ? c : d

      Lead.reparent_nodes(o, new_parent)
    else
      c = Lead.create!(text: t1, parent: self)
      d = Lead.create!(text: t1, parent: self)
    end

    [c.id, d.id]
  end

  # Destroy the children of this Lead, re-appending the grandchildren to self
  # !! Do not destroy children if more than one child has its own children
  def destroy_children
    k = children.to_a
    return true if k.empty?

    original_child_count = k.count
    grandkids = []
    k.each do |c|
      if c.children.present?
        # Fail if more than one child has its own children
        return false if grandkids.present?

        grandkids = c.children
      end
    end

    Lead.reparent_nodes(grandkids, self)

    children.slice(0, original_child_count).each do |c|
      c.destroy!
    end

    true
  end

  def transaction_nuke(lead = self)
    Lead.transaction do
      nuke(lead)
    end
  end

  def nuke(lead = self)
    lead.children.each do |c|
      c.nuke(c)
    end
    destroy!
  end

  # TODO: Probably a helper method
  def all_children(node = self, result = [], depth = 0)
    for c in node.children.to_a.reverse # intentionally reversed
      c.all_children(c, result, depth + 1)
      a = {}
      a[:depth] = depth
      a[:lead] = c
      a[:leadLabel] = node.origin_label
      result.push(a)
    end
    result
  end

  # TODO: Probably a helper method
  def all_children_standard_key(node = self, result = [], depth = 0) # couplets before depth
    ch = node.children
    for c in ch
      a = {}
      a[:depth] = depth
      a[:lead] = c
      result.push(a)
    end

    for c in ch
      c.all_children_standard_key(c, result, depth + 1)
    end
    result
  end

  # @param reorder_list [Array] array of 0-based positions in which to order
  #  the children of this lead.
  # Raises TaxonWorks::Error on error.
  def reorder_children(reorder_list)
    validate_reorder_list(reorder_list, children.count)

    i = 0
    Lead.transaction do
      children.each do |c|
        if (new_position = reorder_list[i]) != i
          c.update_column(:position, new_position)
        end
        i = i + 1
      end
    end
  end

  # @return [ActiveRecord::Relation] ordered by text.
  # Returns all root nodes, with new properties:
  #  * couplets_count (declared here, computed in views)
  #  * otus_count (total number of distinct otus on the key)
  #  * key_updated_at (last time the key was updated)
  #  * key_updated_by_id (id of last person to update the key)
  #  * key_updated_by (name of last person to update the key)
  #
  # !! Note, the relation is a join, check your results when changing order
  # or plucking, most of want you want is on the table joined to, which is
  # not the default table for ordering and plucking.
  def self.roots_with_data(project_id, load_root_otus = false)
    # The updated_at subquery computes key_updated_at (and others), the second
    # query uses that to compute key_updated_by (by finding which node has the
    # corresponding key_updated_at).
    updated_at = Lead
      .joins('JOIN lead_hierarchies AS lh
        ON leads.id = lh.ancestor_id')
      .joins('JOIN leads AS otus_source
        ON lh.descendant_id = otus_source.id')
      .where("
        leads.parent_id IS NULL
        AND leads.project_id = #{project_id}
      ")
      .group(:id)
      .select('
        leads.*,
        COUNT(DISTINCT otus_source.otu_id) AS otus_count,
        MAX(otus_source.updated_at) AS key_updated_at,
        0 AS couplets_count' # count is now computed in views
        #·(COUNT(otus_source.id) - 1) / 2 AS couplet_count
      )

    root_leads = Lead
      .joins("JOIN (#{updated_at.to_sql}) AS leads_updated_at
        ON leads_updated_at.key_updated_at = leads.updated_at")
      .joins('JOIN users
        ON users.id = leads.updated_by_id')
      .select('
        leads_updated_at.*,
        leads.updated_by_id AS key_updated_by_id,
        users.name AS key_updated_by
      ')
      .order('leads_updated_at.text')

    return load_root_otus ? root_leads.includes(:otu) : root_leads
  end

  def redirect_options(project_id)
    leads = Lead
      .select(:id, :text, :origin_label)
      .with_project_id(project_id)
      .order(:text)
    anc_ids = ancestor_ids()

    leads.filter_map do |o|
      if o.id != id && !anc_ids.include?(o.id)
        {
          id: o.id,
          label: o.origin_label,
          text: o.text.nil? ? '' : o.text.truncate(40)
        }
      end
    end
  end

  private

  # Appends `nodes`` to the children of `new_parent``, in their given order.
  # !! Use this instead of add_child to reparent multiple nodes (add_child
  # doesn't work the way one might naievely expect, see the discussion at
  # https://github.com/SpeciesFileGroup/taxonworks/pull/3892#issuecomment-2016043296)
  def self.reparent_nodes(nodes, new_parent)
    last_sibling = new_parent.children.last # may be nil
    nodes.each do |n|
      if last_sibling.nil?
        new_parent.add_child(n)
      else
        last_sibling.append_sibling(n)
      end
      last_sibling = n
    end
  end

  # Raises TaxonWorks::Error on error.
  def validate_reorder_list(reorder_list, expected_length)
    if reorder_list.sort.uniq != (0..expected_length - 1).to_a
      raise TaxonWorks::Error,
        "Bad reorder list: #{reorder_list}"
      return
    end
  end

  def root_has_title
    errors.add(:root_node, 'must have a title') if parent_id.nil? and text.nil?
  end

  def link_out_has_protocol
    errors.add(:link, 'must include https:// or http://') if !link_out.nil? && !(link_out.start_with?('https://') || link_out.start_with?('http://'))
  end

  def redirect_node_is_leaf_node
    errors.add(:redirect, "nodes can't have children") if redirect_id && children.size > 0
  end

  def node_parent_doesnt_have_redirect
    errors.add(:parent, "can't be a redirect node") if parent && parent.redirect_id
  end

  def redirect_isnt_ancestor_or_self
    errors.add(:redirect, "can't be an ancestor") if redirect_id && (redirect_id == id || ancestor_ids.include?(redirect_id))
  end

  def root_has_no_redirect
    errors.add(:root, "nodes can't have a redirect") if redirect_id && parent_id.nil?
  end

  def dupe_in_transaction(node, parentId)
    a = node.dup
    a.parent_id = parentId
    a.text = "(COPY OF) #{a.text}" if parentId == nil
    a.save!

    node.children.each { |c| dupe_in_transaction(c, a.id) }
  end

  def set_is_public_only_on_root
    if parent_id.nil?
      self.is_public ||= false
    else
      self.is_public = nil
    end
  end

end

Provides a URL to display on this node

Returns:



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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'app/models/lead.rb', line 47

class Lead < ApplicationRecord
  include Housekeeping
  include Shared::Citations
  include Shared::Depictions
  include Shared::Attributions
  include Shared::Tags
  include Shared::IsData

  has_closure_tree order: 'position', numeric_order: true, dont_order_roots: true, dependent: nil

  belongs_to :parent, class_name: 'Lead'

  belongs_to :otu, inverse_of: :leads
  has_one :taxon_name, through: :otu
  belongs_to :redirect, class_name: 'Lead'

  has_many :redirecters, class_name: 'Lead', foreign_key: :redirect_id, inverse_of: :redirect, dependent: :nullify

  before_save :set_is_public_only_on_root

  validate :root_has_title
  validate :link_out_has_protocol
  validate :redirect_node_is_leaf_node
  validate :node_parent_doesnt_have_redirect
  validate :root_has_no_redirect
  validate :redirect_isnt_ancestor_or_self

  def future
    redirect_id.blank? ? all_children : redirect.all_children
  end

  # @return [Boolean] true on success, false on error
  def dupe
    if parent_id
      errors.add(:base, 'Can only call dupe on a root lead')
      return false
    end

    begin
      Lead.transaction do
        dupe_in_transaction(self, parent_id)
      end
    rescue ActiveRecord::RecordInvalid => e
      errors.add(:base, "dup failed: #{e}")
      return false
    end

    true
  end

  # left/right/middle
  def node_position
    o = self_and_siblings.order(:position).pluck(:id)
    return :root if o.size == 1
    return :left if o.index(id) == 0
    return :right if o.last == id
    return :middle
  end

  def self.draw(lead, indent = 0)
    puts Rainbow( (' ' * indent) + lead.text ).purple + ' ' + Rainbow( lead.node_position.to_s ).red + ' ' + Rainbow( lead.position ).blue + ' [.parent: ' + Rainbow(lead.parent&.text || 'none').gold + ']'
    lead.children.each do |c|
      draw(c, indent + 1)
    end
  end

  # Return [Boolean] true on success, false on failure
  def insert_key(id)
    if id.nil?
      errors.add(:base, 'Id of key to insert not provided')
      return false
    end
    begin
      Lead.transaction do
        key_to_insert = Lead.find(id)
        if key_to_insert.dupe != true
          raise TaxonWorks::Error, 'dupe failed'
        end

        dup_prefix = '(COPY OF)'
        duped_text = "#{dup_prefix} #{key_to_insert.text}"
        duped_root = Lead.where(text: duped_text).order(:id).last
        if duped_root.nil?
          raise TaxonWorks::Error,
            "failed to find the duped key with text '#{duped_text}'"
        end

        # Prepare the duped key to be reparented - note that root leads don't
        # have link_out_text set (the url link is applied to the title in that
        # case), so link_out_text in the inserted root lead will always be nil.
        duped_root.update!(
          text: duped_root.text.sub(dup_prefix, '(INSERTED KEY) '),
          description: nil,
          is_public: nil
        )

        add_child(duped_root)
      end
    rescue ActiveRecord::RecordNotFound => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    rescue TaxonWorks::Error => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    rescue ActiveRecord::RecordInvalid => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    end

    true
  end

  def insert_couplet
    c, d = nil, nil

    p = node_position

    t1 = 'Inserted node'
    t2 = 'Child nodes are attached to this node'

    if children.any?
      o = children.to_a

      # Reparent under left inserted node unless this is itself a right node.
      left_text = (p != :right) ? t2 : t1
      right_text = (p == :right) ? t2 : t1

      c = Lead.create!(text: left_text, parent: self)
      d = Lead.create!(text: right_text, parent: self)

      new_parent = (p != :right) ? c : d

      Lead.reparent_nodes(o, new_parent)
    else
      c = Lead.create!(text: t1, parent: self)
      d = Lead.create!(text: t1, parent: self)
    end

    [c.id, d.id]
  end

  # Destroy the children of this Lead, re-appending the grandchildren to self
  # !! Do not destroy children if more than one child has its own children
  def destroy_children
    k = children.to_a
    return true if k.empty?

    original_child_count = k.count
    grandkids = []
    k.each do |c|
      if c.children.present?
        # Fail if more than one child has its own children
        return false if grandkids.present?

        grandkids = c.children
      end
    end

    Lead.reparent_nodes(grandkids, self)

    children.slice(0, original_child_count).each do |c|
      c.destroy!
    end

    true
  end

  def transaction_nuke(lead = self)
    Lead.transaction do
      nuke(lead)
    end
  end

  def nuke(lead = self)
    lead.children.each do |c|
      c.nuke(c)
    end
    destroy!
  end

  # TODO: Probably a helper method
  def all_children(node = self, result = [], depth = 0)
    for c in node.children.to_a.reverse # intentionally reversed
      c.all_children(c, result, depth + 1)
      a = {}
      a[:depth] = depth
      a[:lead] = c
      a[:leadLabel] = node.origin_label
      result.push(a)
    end
    result
  end

  # TODO: Probably a helper method
  def all_children_standard_key(node = self, result = [], depth = 0) # couplets before depth
    ch = node.children
    for c in ch
      a = {}
      a[:depth] = depth
      a[:lead] = c
      result.push(a)
    end

    for c in ch
      c.all_children_standard_key(c, result, depth + 1)
    end
    result
  end

  # @param reorder_list [Array] array of 0-based positions in which to order
  #  the children of this lead.
  # Raises TaxonWorks::Error on error.
  def reorder_children(reorder_list)
    validate_reorder_list(reorder_list, children.count)

    i = 0
    Lead.transaction do
      children.each do |c|
        if (new_position = reorder_list[i]) != i
          c.update_column(:position, new_position)
        end
        i = i + 1
      end
    end
  end

  # @return [ActiveRecord::Relation] ordered by text.
  # Returns all root nodes, with new properties:
  #  * couplets_count (declared here, computed in views)
  #  * otus_count (total number of distinct otus on the key)
  #  * key_updated_at (last time the key was updated)
  #  * key_updated_by_id (id of last person to update the key)
  #  * key_updated_by (name of last person to update the key)
  #
  # !! Note, the relation is a join, check your results when changing order
  # or plucking, most of want you want is on the table joined to, which is
  # not the default table for ordering and plucking.
  def self.roots_with_data(project_id, load_root_otus = false)
    # The updated_at subquery computes key_updated_at (and others), the second
    # query uses that to compute key_updated_by (by finding which node has the
    # corresponding key_updated_at).
    updated_at = Lead
      .joins('JOIN lead_hierarchies AS lh
        ON leads.id = lh.ancestor_id')
      .joins('JOIN leads AS otus_source
        ON lh.descendant_id = otus_source.id')
      .where("
        leads.parent_id IS NULL
        AND leads.project_id = #{project_id}
      ")
      .group(:id)
      .select('
        leads.*,
        COUNT(DISTINCT otus_source.otu_id) AS otus_count,
        MAX(otus_source.updated_at) AS key_updated_at,
        0 AS couplets_count' # count is now computed in views
        #·(COUNT(otus_source.id) - 1) / 2 AS couplet_count
      )

    root_leads = Lead
      .joins("JOIN (#{updated_at.to_sql}) AS leads_updated_at
        ON leads_updated_at.key_updated_at = leads.updated_at")
      .joins('JOIN users
        ON users.id = leads.updated_by_id')
      .select('
        leads_updated_at.*,
        leads.updated_by_id AS key_updated_by_id,
        users.name AS key_updated_by
      ')
      .order('leads_updated_at.text')

    return load_root_otus ? root_leads.includes(:otu) : root_leads
  end

  def redirect_options(project_id)
    leads = Lead
      .select(:id, :text, :origin_label)
      .with_project_id(project_id)
      .order(:text)
    anc_ids = ancestor_ids()

    leads.filter_map do |o|
      if o.id != id && !anc_ids.include?(o.id)
        {
          id: o.id,
          label: o.origin_label,
          text: o.text.nil? ? '' : o.text.truncate(40)
        }
      end
    end
  end

  private

  # Appends `nodes`` to the children of `new_parent``, in their given order.
  # !! Use this instead of add_child to reparent multiple nodes (add_child
  # doesn't work the way one might naievely expect, see the discussion at
  # https://github.com/SpeciesFileGroup/taxonworks/pull/3892#issuecomment-2016043296)
  def self.reparent_nodes(nodes, new_parent)
    last_sibling = new_parent.children.last # may be nil
    nodes.each do |n|
      if last_sibling.nil?
        new_parent.add_child(n)
      else
        last_sibling.append_sibling(n)
      end
      last_sibling = n
    end
  end

  # Raises TaxonWorks::Error on error.
  def validate_reorder_list(reorder_list, expected_length)
    if reorder_list.sort.uniq != (0..expected_length - 1).to_a
      raise TaxonWorks::Error,
        "Bad reorder list: #{reorder_list}"
      return
    end
  end

  def root_has_title
    errors.add(:root_node, 'must have a title') if parent_id.nil? and text.nil?
  end

  def link_out_has_protocol
    errors.add(:link, 'must include https:// or http://') if !link_out.nil? && !(link_out.start_with?('https://') || link_out.start_with?('http://'))
  end

  def redirect_node_is_leaf_node
    errors.add(:redirect, "nodes can't have children") if redirect_id && children.size > 0
  end

  def node_parent_doesnt_have_redirect
    errors.add(:parent, "can't be a redirect node") if parent && parent.redirect_id
  end

  def redirect_isnt_ancestor_or_self
    errors.add(:redirect, "can't be an ancestor") if redirect_id && (redirect_id == id || ancestor_ids.include?(redirect_id))
  end

  def root_has_no_redirect
    errors.add(:root, "nodes can't have a redirect") if redirect_id && parent_id.nil?
  end

  def dupe_in_transaction(node, parentId)
    a = node.dup
    a.parent_id = parentId
    a.text = "(COPY OF) #{a.text}" if parentId == nil
    a.save!

    node.children.each { |c| dupe_in_transaction(c, a.id) }
  end

  def set_is_public_only_on_root
    if parent_id.nil?
      self.is_public ||= false
    else
      self.is_public = nil
    end
  end

end

Text to display for the link_out URL

Returns:

  • (string)


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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'app/models/lead.rb', line 47

class Lead < ApplicationRecord
  include Housekeeping
  include Shared::Citations
  include Shared::Depictions
  include Shared::Attributions
  include Shared::Tags
  include Shared::IsData

  has_closure_tree order: 'position', numeric_order: true, dont_order_roots: true, dependent: nil

  belongs_to :parent, class_name: 'Lead'

  belongs_to :otu, inverse_of: :leads
  has_one :taxon_name, through: :otu
  belongs_to :redirect, class_name: 'Lead'

  has_many :redirecters, class_name: 'Lead', foreign_key: :redirect_id, inverse_of: :redirect, dependent: :nullify

  before_save :set_is_public_only_on_root

  validate :root_has_title
  validate :link_out_has_protocol
  validate :redirect_node_is_leaf_node
  validate :node_parent_doesnt_have_redirect
  validate :root_has_no_redirect
  validate :redirect_isnt_ancestor_or_self

  def future
    redirect_id.blank? ? all_children : redirect.all_children
  end

  # @return [Boolean] true on success, false on error
  def dupe
    if parent_id
      errors.add(:base, 'Can only call dupe on a root lead')
      return false
    end

    begin
      Lead.transaction do
        dupe_in_transaction(self, parent_id)
      end
    rescue ActiveRecord::RecordInvalid => e
      errors.add(:base, "dup failed: #{e}")
      return false
    end

    true
  end

  # left/right/middle
  def node_position
    o = self_and_siblings.order(:position).pluck(:id)
    return :root if o.size == 1
    return :left if o.index(id) == 0
    return :right if o.last == id
    return :middle
  end

  def self.draw(lead, indent = 0)
    puts Rainbow( (' ' * indent) + lead.text ).purple + ' ' + Rainbow( lead.node_position.to_s ).red + ' ' + Rainbow( lead.position ).blue + ' [.parent: ' + Rainbow(lead.parent&.text || 'none').gold + ']'
    lead.children.each do |c|
      draw(c, indent + 1)
    end
  end

  # Return [Boolean] true on success, false on failure
  def insert_key(id)
    if id.nil?
      errors.add(:base, 'Id of key to insert not provided')
      return false
    end
    begin
      Lead.transaction do
        key_to_insert = Lead.find(id)
        if key_to_insert.dupe != true
          raise TaxonWorks::Error, 'dupe failed'
        end

        dup_prefix = '(COPY OF)'
        duped_text = "#{dup_prefix} #{key_to_insert.text}"
        duped_root = Lead.where(text: duped_text).order(:id).last
        if duped_root.nil?
          raise TaxonWorks::Error,
            "failed to find the duped key with text '#{duped_text}'"
        end

        # Prepare the duped key to be reparented - note that root leads don't
        # have link_out_text set (the url link is applied to the title in that
        # case), so link_out_text in the inserted root lead will always be nil.
        duped_root.update!(
          text: duped_root.text.sub(dup_prefix, '(INSERTED KEY) '),
          description: nil,
          is_public: nil
        )

        add_child(duped_root)
      end
    rescue ActiveRecord::RecordNotFound => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    rescue TaxonWorks::Error => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    rescue ActiveRecord::RecordInvalid => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    end

    true
  end

  def insert_couplet
    c, d = nil, nil

    p = node_position

    t1 = 'Inserted node'
    t2 = 'Child nodes are attached to this node'

    if children.any?
      o = children.to_a

      # Reparent under left inserted node unless this is itself a right node.
      left_text = (p != :right) ? t2 : t1
      right_text = (p == :right) ? t2 : t1

      c = Lead.create!(text: left_text, parent: self)
      d = Lead.create!(text: right_text, parent: self)

      new_parent = (p != :right) ? c : d

      Lead.reparent_nodes(o, new_parent)
    else
      c = Lead.create!(text: t1, parent: self)
      d = Lead.create!(text: t1, parent: self)
    end

    [c.id, d.id]
  end

  # Destroy the children of this Lead, re-appending the grandchildren to self
  # !! Do not destroy children if more than one child has its own children
  def destroy_children
    k = children.to_a
    return true if k.empty?

    original_child_count = k.count
    grandkids = []
    k.each do |c|
      if c.children.present?
        # Fail if more than one child has its own children
        return false if grandkids.present?

        grandkids = c.children
      end
    end

    Lead.reparent_nodes(grandkids, self)

    children.slice(0, original_child_count).each do |c|
      c.destroy!
    end

    true
  end

  def transaction_nuke(lead = self)
    Lead.transaction do
      nuke(lead)
    end
  end

  def nuke(lead = self)
    lead.children.each do |c|
      c.nuke(c)
    end
    destroy!
  end

  # TODO: Probably a helper method
  def all_children(node = self, result = [], depth = 0)
    for c in node.children.to_a.reverse # intentionally reversed
      c.all_children(c, result, depth + 1)
      a = {}
      a[:depth] = depth
      a[:lead] = c
      a[:leadLabel] = node.origin_label
      result.push(a)
    end
    result
  end

  # TODO: Probably a helper method
  def all_children_standard_key(node = self, result = [], depth = 0) # couplets before depth
    ch = node.children
    for c in ch
      a = {}
      a[:depth] = depth
      a[:lead] = c
      result.push(a)
    end

    for c in ch
      c.all_children_standard_key(c, result, depth + 1)
    end
    result
  end

  # @param reorder_list [Array] array of 0-based positions in which to order
  #  the children of this lead.
  # Raises TaxonWorks::Error on error.
  def reorder_children(reorder_list)
    validate_reorder_list(reorder_list, children.count)

    i = 0
    Lead.transaction do
      children.each do |c|
        if (new_position = reorder_list[i]) != i
          c.update_column(:position, new_position)
        end
        i = i + 1
      end
    end
  end

  # @return [ActiveRecord::Relation] ordered by text.
  # Returns all root nodes, with new properties:
  #  * couplets_count (declared here, computed in views)
  #  * otus_count (total number of distinct otus on the key)
  #  * key_updated_at (last time the key was updated)
  #  * key_updated_by_id (id of last person to update the key)
  #  * key_updated_by (name of last person to update the key)
  #
  # !! Note, the relation is a join, check your results when changing order
  # or plucking, most of want you want is on the table joined to, which is
  # not the default table for ordering and plucking.
  def self.roots_with_data(project_id, load_root_otus = false)
    # The updated_at subquery computes key_updated_at (and others), the second
    # query uses that to compute key_updated_by (by finding which node has the
    # corresponding key_updated_at).
    updated_at = Lead
      .joins('JOIN lead_hierarchies AS lh
        ON leads.id = lh.ancestor_id')
      .joins('JOIN leads AS otus_source
        ON lh.descendant_id = otus_source.id')
      .where("
        leads.parent_id IS NULL
        AND leads.project_id = #{project_id}
      ")
      .group(:id)
      .select('
        leads.*,
        COUNT(DISTINCT otus_source.otu_id) AS otus_count,
        MAX(otus_source.updated_at) AS key_updated_at,
        0 AS couplets_count' # count is now computed in views
        #·(COUNT(otus_source.id) - 1) / 2 AS couplet_count
      )

    root_leads = Lead
      .joins("JOIN (#{updated_at.to_sql}) AS leads_updated_at
        ON leads_updated_at.key_updated_at = leads.updated_at")
      .joins('JOIN users
        ON users.id = leads.updated_by_id')
      .select('
        leads_updated_at.*,
        leads.updated_by_id AS key_updated_by_id,
        users.name AS key_updated_by
      ')
      .order('leads_updated_at.text')

    return load_root_otus ? root_leads.includes(:otu) : root_leads
  end

  def redirect_options(project_id)
    leads = Lead
      .select(:id, :text, :origin_label)
      .with_project_id(project_id)
      .order(:text)
    anc_ids = ancestor_ids()

    leads.filter_map do |o|
      if o.id != id && !anc_ids.include?(o.id)
        {
          id: o.id,
          label: o.origin_label,
          text: o.text.nil? ? '' : o.text.truncate(40)
        }
      end
    end
  end

  private

  # Appends `nodes`` to the children of `new_parent``, in their given order.
  # !! Use this instead of add_child to reparent multiple nodes (add_child
  # doesn't work the way one might naievely expect, see the discussion at
  # https://github.com/SpeciesFileGroup/taxonworks/pull/3892#issuecomment-2016043296)
  def self.reparent_nodes(nodes, new_parent)
    last_sibling = new_parent.children.last # may be nil
    nodes.each do |n|
      if last_sibling.nil?
        new_parent.add_child(n)
      else
        last_sibling.append_sibling(n)
      end
      last_sibling = n
    end
  end

  # Raises TaxonWorks::Error on error.
  def validate_reorder_list(reorder_list, expected_length)
    if reorder_list.sort.uniq != (0..expected_length - 1).to_a
      raise TaxonWorks::Error,
        "Bad reorder list: #{reorder_list}"
      return
    end
  end

  def root_has_title
    errors.add(:root_node, 'must have a title') if parent_id.nil? and text.nil?
  end

  def link_out_has_protocol
    errors.add(:link, 'must include https:// or http://') if !link_out.nil? && !(link_out.start_with?('https://') || link_out.start_with?('http://'))
  end

  def redirect_node_is_leaf_node
    errors.add(:redirect, "nodes can't have children") if redirect_id && children.size > 0
  end

  def node_parent_doesnt_have_redirect
    errors.add(:parent, "can't be a redirect node") if parent && parent.redirect_id
  end

  def redirect_isnt_ancestor_or_self
    errors.add(:redirect, "can't be an ancestor") if redirect_id && (redirect_id == id || ancestor_ids.include?(redirect_id))
  end

  def root_has_no_redirect
    errors.add(:root, "nodes can't have a redirect") if redirect_id && parent_id.nil?
  end

  def dupe_in_transaction(node, parentId)
    a = node.dup
    a.parent_id = parentId
    a.text = "(COPY OF) #{a.text}" if parentId == nil
    a.save!

    node.children.each { |c| dupe_in_transaction(c, a.id) }
  end

  def set_is_public_only_on_root
    if parent_id.nil?
      self.is_public ||= false
    else
      self.is_public = nil
    end
  end

end

#origin_labelstring

If the couplet was given a # in print, that number

Returns:

  • (string)


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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'app/models/lead.rb', line 47

class Lead < ApplicationRecord
  include Housekeeping
  include Shared::Citations
  include Shared::Depictions
  include Shared::Attributions
  include Shared::Tags
  include Shared::IsData

  has_closure_tree order: 'position', numeric_order: true, dont_order_roots: true, dependent: nil

  belongs_to :parent, class_name: 'Lead'

  belongs_to :otu, inverse_of: :leads
  has_one :taxon_name, through: :otu
  belongs_to :redirect, class_name: 'Lead'

  has_many :redirecters, class_name: 'Lead', foreign_key: :redirect_id, inverse_of: :redirect, dependent: :nullify

  before_save :set_is_public_only_on_root

  validate :root_has_title
  validate :link_out_has_protocol
  validate :redirect_node_is_leaf_node
  validate :node_parent_doesnt_have_redirect
  validate :root_has_no_redirect
  validate :redirect_isnt_ancestor_or_self

  def future
    redirect_id.blank? ? all_children : redirect.all_children
  end

  # @return [Boolean] true on success, false on error
  def dupe
    if parent_id
      errors.add(:base, 'Can only call dupe on a root lead')
      return false
    end

    begin
      Lead.transaction do
        dupe_in_transaction(self, parent_id)
      end
    rescue ActiveRecord::RecordInvalid => e
      errors.add(:base, "dup failed: #{e}")
      return false
    end

    true
  end

  # left/right/middle
  def node_position
    o = self_and_siblings.order(:position).pluck(:id)
    return :root if o.size == 1
    return :left if o.index(id) == 0
    return :right if o.last == id
    return :middle
  end

  def self.draw(lead, indent = 0)
    puts Rainbow( (' ' * indent) + lead.text ).purple + ' ' + Rainbow( lead.node_position.to_s ).red + ' ' + Rainbow( lead.position ).blue + ' [.parent: ' + Rainbow(lead.parent&.text || 'none').gold + ']'
    lead.children.each do |c|
      draw(c, indent + 1)
    end
  end

  # Return [Boolean] true on success, false on failure
  def insert_key(id)
    if id.nil?
      errors.add(:base, 'Id of key to insert not provided')
      return false
    end
    begin
      Lead.transaction do
        key_to_insert = Lead.find(id)
        if key_to_insert.dupe != true
          raise TaxonWorks::Error, 'dupe failed'
        end

        dup_prefix = '(COPY OF)'
        duped_text = "#{dup_prefix} #{key_to_insert.text}"
        duped_root = Lead.where(text: duped_text).order(:id).last
        if duped_root.nil?
          raise TaxonWorks::Error,
            "failed to find the duped key with text '#{duped_text}'"
        end

        # Prepare the duped key to be reparented - note that root leads don't
        # have link_out_text set (the url link is applied to the title in that
        # case), so link_out_text in the inserted root lead will always be nil.
        duped_root.update!(
          text: duped_root.text.sub(dup_prefix, '(INSERTED KEY) '),
          description: nil,
          is_public: nil
        )

        add_child(duped_root)
      end
    rescue ActiveRecord::RecordNotFound => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    rescue TaxonWorks::Error => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    rescue ActiveRecord::RecordInvalid => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    end

    true
  end

  def insert_couplet
    c, d = nil, nil

    p = node_position

    t1 = 'Inserted node'
    t2 = 'Child nodes are attached to this node'

    if children.any?
      o = children.to_a

      # Reparent under left inserted node unless this is itself a right node.
      left_text = (p != :right) ? t2 : t1
      right_text = (p == :right) ? t2 : t1

      c = Lead.create!(text: left_text, parent: self)
      d = Lead.create!(text: right_text, parent: self)

      new_parent = (p != :right) ? c : d

      Lead.reparent_nodes(o, new_parent)
    else
      c = Lead.create!(text: t1, parent: self)
      d = Lead.create!(text: t1, parent: self)
    end

    [c.id, d.id]
  end

  # Destroy the children of this Lead, re-appending the grandchildren to self
  # !! Do not destroy children if more than one child has its own children
  def destroy_children
    k = children.to_a
    return true if k.empty?

    original_child_count = k.count
    grandkids = []
    k.each do |c|
      if c.children.present?
        # Fail if more than one child has its own children
        return false if grandkids.present?

        grandkids = c.children
      end
    end

    Lead.reparent_nodes(grandkids, self)

    children.slice(0, original_child_count).each do |c|
      c.destroy!
    end

    true
  end

  def transaction_nuke(lead = self)
    Lead.transaction do
      nuke(lead)
    end
  end

  def nuke(lead = self)
    lead.children.each do |c|
      c.nuke(c)
    end
    destroy!
  end

  # TODO: Probably a helper method
  def all_children(node = self, result = [], depth = 0)
    for c in node.children.to_a.reverse # intentionally reversed
      c.all_children(c, result, depth + 1)
      a = {}
      a[:depth] = depth
      a[:lead] = c
      a[:leadLabel] = node.origin_label
      result.push(a)
    end
    result
  end

  # TODO: Probably a helper method
  def all_children_standard_key(node = self, result = [], depth = 0) # couplets before depth
    ch = node.children
    for c in ch
      a = {}
      a[:depth] = depth
      a[:lead] = c
      result.push(a)
    end

    for c in ch
      c.all_children_standard_key(c, result, depth + 1)
    end
    result
  end

  # @param reorder_list [Array] array of 0-based positions in which to order
  #  the children of this lead.
  # Raises TaxonWorks::Error on error.
  def reorder_children(reorder_list)
    validate_reorder_list(reorder_list, children.count)

    i = 0
    Lead.transaction do
      children.each do |c|
        if (new_position = reorder_list[i]) != i
          c.update_column(:position, new_position)
        end
        i = i + 1
      end
    end
  end

  # @return [ActiveRecord::Relation] ordered by text.
  # Returns all root nodes, with new properties:
  #  * couplets_count (declared here, computed in views)
  #  * otus_count (total number of distinct otus on the key)
  #  * key_updated_at (last time the key was updated)
  #  * key_updated_by_id (id of last person to update the key)
  #  * key_updated_by (name of last person to update the key)
  #
  # !! Note, the relation is a join, check your results when changing order
  # or plucking, most of want you want is on the table joined to, which is
  # not the default table for ordering and plucking.
  def self.roots_with_data(project_id, load_root_otus = false)
    # The updated_at subquery computes key_updated_at (and others), the second
    # query uses that to compute key_updated_by (by finding which node has the
    # corresponding key_updated_at).
    updated_at = Lead
      .joins('JOIN lead_hierarchies AS lh
        ON leads.id = lh.ancestor_id')
      .joins('JOIN leads AS otus_source
        ON lh.descendant_id = otus_source.id')
      .where("
        leads.parent_id IS NULL
        AND leads.project_id = #{project_id}
      ")
      .group(:id)
      .select('
        leads.*,
        COUNT(DISTINCT otus_source.otu_id) AS otus_count,
        MAX(otus_source.updated_at) AS key_updated_at,
        0 AS couplets_count' # count is now computed in views
        #·(COUNT(otus_source.id) - 1) / 2 AS couplet_count
      )

    root_leads = Lead
      .joins("JOIN (#{updated_at.to_sql}) AS leads_updated_at
        ON leads_updated_at.key_updated_at = leads.updated_at")
      .joins('JOIN users
        ON users.id = leads.updated_by_id')
      .select('
        leads_updated_at.*,
        leads.updated_by_id AS key_updated_by_id,
        users.name AS key_updated_by
      ')
      .order('leads_updated_at.text')

    return load_root_otus ? root_leads.includes(:otu) : root_leads
  end

  def redirect_options(project_id)
    leads = Lead
      .select(:id, :text, :origin_label)
      .with_project_id(project_id)
      .order(:text)
    anc_ids = ancestor_ids()

    leads.filter_map do |o|
      if o.id != id && !anc_ids.include?(o.id)
        {
          id: o.id,
          label: o.origin_label,
          text: o.text.nil? ? '' : o.text.truncate(40)
        }
      end
    end
  end

  private

  # Appends `nodes`` to the children of `new_parent``, in their given order.
  # !! Use this instead of add_child to reparent multiple nodes (add_child
  # doesn't work the way one might naievely expect, see the discussion at
  # https://github.com/SpeciesFileGroup/taxonworks/pull/3892#issuecomment-2016043296)
  def self.reparent_nodes(nodes, new_parent)
    last_sibling = new_parent.children.last # may be nil
    nodes.each do |n|
      if last_sibling.nil?
        new_parent.add_child(n)
      else
        last_sibling.append_sibling(n)
      end
      last_sibling = n
    end
  end

  # Raises TaxonWorks::Error on error.
  def validate_reorder_list(reorder_list, expected_length)
    if reorder_list.sort.uniq != (0..expected_length - 1).to_a
      raise TaxonWorks::Error,
        "Bad reorder list: #{reorder_list}"
      return
    end
  end

  def root_has_title
    errors.add(:root_node, 'must have a title') if parent_id.nil? and text.nil?
  end

  def link_out_has_protocol
    errors.add(:link, 'must include https:// or http://') if !link_out.nil? && !(link_out.start_with?('https://') || link_out.start_with?('http://'))
  end

  def redirect_node_is_leaf_node
    errors.add(:redirect, "nodes can't have children") if redirect_id && children.size > 0
  end

  def node_parent_doesnt_have_redirect
    errors.add(:parent, "can't be a redirect node") if parent && parent.redirect_id
  end

  def redirect_isnt_ancestor_or_self
    errors.add(:redirect, "can't be an ancestor") if redirect_id && (redirect_id == id || ancestor_ids.include?(redirect_id))
  end

  def root_has_no_redirect
    errors.add(:root, "nodes can't have a redirect") if redirect_id && parent_id.nil?
  end

  def dupe_in_transaction(node, parentId)
    a = node.dup
    a.parent_id = parentId
    a.text = "(COPY OF) #{a.text}" if parentId == nil
    a.save!

    node.children.each { |c| dupe_in_transaction(c, a.id) }
  end

  def set_is_public_only_on_root
    if parent_id.nil?
      self.is_public ||= false
    else
      self.is_public = nil
    end
  end

end

#otu_idinteger

Otu determined at this stage of the key, if any

Returns:

  • (integer)


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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'app/models/lead.rb', line 47

class Lead < ApplicationRecord
  include Housekeeping
  include Shared::Citations
  include Shared::Depictions
  include Shared::Attributions
  include Shared::Tags
  include Shared::IsData

  has_closure_tree order: 'position', numeric_order: true, dont_order_roots: true, dependent: nil

  belongs_to :parent, class_name: 'Lead'

  belongs_to :otu, inverse_of: :leads
  has_one :taxon_name, through: :otu
  belongs_to :redirect, class_name: 'Lead'

  has_many :redirecters, class_name: 'Lead', foreign_key: :redirect_id, inverse_of: :redirect, dependent: :nullify

  before_save :set_is_public_only_on_root

  validate :root_has_title
  validate :link_out_has_protocol
  validate :redirect_node_is_leaf_node
  validate :node_parent_doesnt_have_redirect
  validate :root_has_no_redirect
  validate :redirect_isnt_ancestor_or_self

  def future
    redirect_id.blank? ? all_children : redirect.all_children
  end

  # @return [Boolean] true on success, false on error
  def dupe
    if parent_id
      errors.add(:base, 'Can only call dupe on a root lead')
      return false
    end

    begin
      Lead.transaction do
        dupe_in_transaction(self, parent_id)
      end
    rescue ActiveRecord::RecordInvalid => e
      errors.add(:base, "dup failed: #{e}")
      return false
    end

    true
  end

  # left/right/middle
  def node_position
    o = self_and_siblings.order(:position).pluck(:id)
    return :root if o.size == 1
    return :left if o.index(id) == 0
    return :right if o.last == id
    return :middle
  end

  def self.draw(lead, indent = 0)
    puts Rainbow( (' ' * indent) + lead.text ).purple + ' ' + Rainbow( lead.node_position.to_s ).red + ' ' + Rainbow( lead.position ).blue + ' [.parent: ' + Rainbow(lead.parent&.text || 'none').gold + ']'
    lead.children.each do |c|
      draw(c, indent + 1)
    end
  end

  # Return [Boolean] true on success, false on failure
  def insert_key(id)
    if id.nil?
      errors.add(:base, 'Id of key to insert not provided')
      return false
    end
    begin
      Lead.transaction do
        key_to_insert = Lead.find(id)
        if key_to_insert.dupe != true
          raise TaxonWorks::Error, 'dupe failed'
        end

        dup_prefix = '(COPY OF)'
        duped_text = "#{dup_prefix} #{key_to_insert.text}"
        duped_root = Lead.where(text: duped_text).order(:id).last
        if duped_root.nil?
          raise TaxonWorks::Error,
            "failed to find the duped key with text '#{duped_text}'"
        end

        # Prepare the duped key to be reparented - note that root leads don't
        # have link_out_text set (the url link is applied to the title in that
        # case), so link_out_text in the inserted root lead will always be nil.
        duped_root.update!(
          text: duped_root.text.sub(dup_prefix, '(INSERTED KEY) '),
          description: nil,
          is_public: nil
        )

        add_child(duped_root)
      end
    rescue ActiveRecord::RecordNotFound => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    rescue TaxonWorks::Error => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    rescue ActiveRecord::RecordInvalid => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    end

    true
  end

  def insert_couplet
    c, d = nil, nil

    p = node_position

    t1 = 'Inserted node'
    t2 = 'Child nodes are attached to this node'

    if children.any?
      o = children.to_a

      # Reparent under left inserted node unless this is itself a right node.
      left_text = (p != :right) ? t2 : t1
      right_text = (p == :right) ? t2 : t1

      c = Lead.create!(text: left_text, parent: self)
      d = Lead.create!(text: right_text, parent: self)

      new_parent = (p != :right) ? c : d

      Lead.reparent_nodes(o, new_parent)
    else
      c = Lead.create!(text: t1, parent: self)
      d = Lead.create!(text: t1, parent: self)
    end

    [c.id, d.id]
  end

  # Destroy the children of this Lead, re-appending the grandchildren to self
  # !! Do not destroy children if more than one child has its own children
  def destroy_children
    k = children.to_a
    return true if k.empty?

    original_child_count = k.count
    grandkids = []
    k.each do |c|
      if c.children.present?
        # Fail if more than one child has its own children
        return false if grandkids.present?

        grandkids = c.children
      end
    end

    Lead.reparent_nodes(grandkids, self)

    children.slice(0, original_child_count).each do |c|
      c.destroy!
    end

    true
  end

  def transaction_nuke(lead = self)
    Lead.transaction do
      nuke(lead)
    end
  end

  def nuke(lead = self)
    lead.children.each do |c|
      c.nuke(c)
    end
    destroy!
  end

  # TODO: Probably a helper method
  def all_children(node = self, result = [], depth = 0)
    for c in node.children.to_a.reverse # intentionally reversed
      c.all_children(c, result, depth + 1)
      a = {}
      a[:depth] = depth
      a[:lead] = c
      a[:leadLabel] = node.origin_label
      result.push(a)
    end
    result
  end

  # TODO: Probably a helper method
  def all_children_standard_key(node = self, result = [], depth = 0) # couplets before depth
    ch = node.children
    for c in ch
      a = {}
      a[:depth] = depth
      a[:lead] = c
      result.push(a)
    end

    for c in ch
      c.all_children_standard_key(c, result, depth + 1)
    end
    result
  end

  # @param reorder_list [Array] array of 0-based positions in which to order
  #  the children of this lead.
  # Raises TaxonWorks::Error on error.
  def reorder_children(reorder_list)
    validate_reorder_list(reorder_list, children.count)

    i = 0
    Lead.transaction do
      children.each do |c|
        if (new_position = reorder_list[i]) != i
          c.update_column(:position, new_position)
        end
        i = i + 1
      end
    end
  end

  # @return [ActiveRecord::Relation] ordered by text.
  # Returns all root nodes, with new properties:
  #  * couplets_count (declared here, computed in views)
  #  * otus_count (total number of distinct otus on the key)
  #  * key_updated_at (last time the key was updated)
  #  * key_updated_by_id (id of last person to update the key)
  #  * key_updated_by (name of last person to update the key)
  #
  # !! Note, the relation is a join, check your results when changing order
  # or plucking, most of want you want is on the table joined to, which is
  # not the default table for ordering and plucking.
  def self.roots_with_data(project_id, load_root_otus = false)
    # The updated_at subquery computes key_updated_at (and others), the second
    # query uses that to compute key_updated_by (by finding which node has the
    # corresponding key_updated_at).
    updated_at = Lead
      .joins('JOIN lead_hierarchies AS lh
        ON leads.id = lh.ancestor_id')
      .joins('JOIN leads AS otus_source
        ON lh.descendant_id = otus_source.id')
      .where("
        leads.parent_id IS NULL
        AND leads.project_id = #{project_id}
      ")
      .group(:id)
      .select('
        leads.*,
        COUNT(DISTINCT otus_source.otu_id) AS otus_count,
        MAX(otus_source.updated_at) AS key_updated_at,
        0 AS couplets_count' # count is now computed in views
        #·(COUNT(otus_source.id) - 1) / 2 AS couplet_count
      )

    root_leads = Lead
      .joins("JOIN (#{updated_at.to_sql}) AS leads_updated_at
        ON leads_updated_at.key_updated_at = leads.updated_at")
      .joins('JOIN users
        ON users.id = leads.updated_by_id')
      .select('
        leads_updated_at.*,
        leads.updated_by_id AS key_updated_by_id,
        users.name AS key_updated_by
      ')
      .order('leads_updated_at.text')

    return load_root_otus ? root_leads.includes(:otu) : root_leads
  end

  def redirect_options(project_id)
    leads = Lead
      .select(:id, :text, :origin_label)
      .with_project_id(project_id)
      .order(:text)
    anc_ids = ancestor_ids()

    leads.filter_map do |o|
      if o.id != id && !anc_ids.include?(o.id)
        {
          id: o.id,
          label: o.origin_label,
          text: o.text.nil? ? '' : o.text.truncate(40)
        }
      end
    end
  end

  private

  # Appends `nodes`` to the children of `new_parent``, in their given order.
  # !! Use this instead of add_child to reparent multiple nodes (add_child
  # doesn't work the way one might naievely expect, see the discussion at
  # https://github.com/SpeciesFileGroup/taxonworks/pull/3892#issuecomment-2016043296)
  def self.reparent_nodes(nodes, new_parent)
    last_sibling = new_parent.children.last # may be nil
    nodes.each do |n|
      if last_sibling.nil?
        new_parent.add_child(n)
      else
        last_sibling.append_sibling(n)
      end
      last_sibling = n
    end
  end

  # Raises TaxonWorks::Error on error.
  def validate_reorder_list(reorder_list, expected_length)
    if reorder_list.sort.uniq != (0..expected_length - 1).to_a
      raise TaxonWorks::Error,
        "Bad reorder list: #{reorder_list}"
      return
    end
  end

  def root_has_title
    errors.add(:root_node, 'must have a title') if parent_id.nil? and text.nil?
  end

  def link_out_has_protocol
    errors.add(:link, 'must include https:// or http://') if !link_out.nil? && !(link_out.start_with?('https://') || link_out.start_with?('http://'))
  end

  def redirect_node_is_leaf_node
    errors.add(:redirect, "nodes can't have children") if redirect_id && children.size > 0
  end

  def node_parent_doesnt_have_redirect
    errors.add(:parent, "can't be a redirect node") if parent && parent.redirect_id
  end

  def redirect_isnt_ancestor_or_self
    errors.add(:redirect, "can't be an ancestor") if redirect_id && (redirect_id == id || ancestor_ids.include?(redirect_id))
  end

  def root_has_no_redirect
    errors.add(:root, "nodes can't have a redirect") if redirect_id && parent_id.nil?
  end

  def dupe_in_transaction(node, parentId)
    a = node.dup
    a.parent_id = parentId
    a.text = "(COPY OF) #{a.text}" if parentId == nil
    a.save!

    node.children.each { |c| dupe_in_transaction(c, a.id) }
  end

  def set_is_public_only_on_root
    if parent_id.nil?
      self.is_public ||= false
    else
      self.is_public = nil
    end
  end

end

#parent_idinteger

Id of the lead immediately prior to this one in the key

Returns:

  • (integer)


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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'app/models/lead.rb', line 47

class Lead < ApplicationRecord
  include Housekeeping
  include Shared::Citations
  include Shared::Depictions
  include Shared::Attributions
  include Shared::Tags
  include Shared::IsData

  has_closure_tree order: 'position', numeric_order: true, dont_order_roots: true, dependent: nil

  belongs_to :parent, class_name: 'Lead'

  belongs_to :otu, inverse_of: :leads
  has_one :taxon_name, through: :otu
  belongs_to :redirect, class_name: 'Lead'

  has_many :redirecters, class_name: 'Lead', foreign_key: :redirect_id, inverse_of: :redirect, dependent: :nullify

  before_save :set_is_public_only_on_root

  validate :root_has_title
  validate :link_out_has_protocol
  validate :redirect_node_is_leaf_node
  validate :node_parent_doesnt_have_redirect
  validate :root_has_no_redirect
  validate :redirect_isnt_ancestor_or_self

  def future
    redirect_id.blank? ? all_children : redirect.all_children
  end

  # @return [Boolean] true on success, false on error
  def dupe
    if parent_id
      errors.add(:base, 'Can only call dupe on a root lead')
      return false
    end

    begin
      Lead.transaction do
        dupe_in_transaction(self, parent_id)
      end
    rescue ActiveRecord::RecordInvalid => e
      errors.add(:base, "dup failed: #{e}")
      return false
    end

    true
  end

  # left/right/middle
  def node_position
    o = self_and_siblings.order(:position).pluck(:id)
    return :root if o.size == 1
    return :left if o.index(id) == 0
    return :right if o.last == id
    return :middle
  end

  def self.draw(lead, indent = 0)
    puts Rainbow( (' ' * indent) + lead.text ).purple + ' ' + Rainbow( lead.node_position.to_s ).red + ' ' + Rainbow( lead.position ).blue + ' [.parent: ' + Rainbow(lead.parent&.text || 'none').gold + ']'
    lead.children.each do |c|
      draw(c, indent + 1)
    end
  end

  # Return [Boolean] true on success, false on failure
  def insert_key(id)
    if id.nil?
      errors.add(:base, 'Id of key to insert not provided')
      return false
    end
    begin
      Lead.transaction do
        key_to_insert = Lead.find(id)
        if key_to_insert.dupe != true
          raise TaxonWorks::Error, 'dupe failed'
        end

        dup_prefix = '(COPY OF)'
        duped_text = "#{dup_prefix} #{key_to_insert.text}"
        duped_root = Lead.where(text: duped_text).order(:id).last
        if duped_root.nil?
          raise TaxonWorks::Error,
            "failed to find the duped key with text '#{duped_text}'"
        end

        # Prepare the duped key to be reparented - note that root leads don't
        # have link_out_text set (the url link is applied to the title in that
        # case), so link_out_text in the inserted root lead will always be nil.
        duped_root.update!(
          text: duped_root.text.sub(dup_prefix, '(INSERTED KEY) '),
          description: nil,
          is_public: nil
        )

        add_child(duped_root)
      end
    rescue ActiveRecord::RecordNotFound => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    rescue TaxonWorks::Error => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    rescue ActiveRecord::RecordInvalid => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    end

    true
  end

  def insert_couplet
    c, d = nil, nil

    p = node_position

    t1 = 'Inserted node'
    t2 = 'Child nodes are attached to this node'

    if children.any?
      o = children.to_a

      # Reparent under left inserted node unless this is itself a right node.
      left_text = (p != :right) ? t2 : t1
      right_text = (p == :right) ? t2 : t1

      c = Lead.create!(text: left_text, parent: self)
      d = Lead.create!(text: right_text, parent: self)

      new_parent = (p != :right) ? c : d

      Lead.reparent_nodes(o, new_parent)
    else
      c = Lead.create!(text: t1, parent: self)
      d = Lead.create!(text: t1, parent: self)
    end

    [c.id, d.id]
  end

  # Destroy the children of this Lead, re-appending the grandchildren to self
  # !! Do not destroy children if more than one child has its own children
  def destroy_children
    k = children.to_a
    return true if k.empty?

    original_child_count = k.count
    grandkids = []
    k.each do |c|
      if c.children.present?
        # Fail if more than one child has its own children
        return false if grandkids.present?

        grandkids = c.children
      end
    end

    Lead.reparent_nodes(grandkids, self)

    children.slice(0, original_child_count).each do |c|
      c.destroy!
    end

    true
  end

  def transaction_nuke(lead = self)
    Lead.transaction do
      nuke(lead)
    end
  end

  def nuke(lead = self)
    lead.children.each do |c|
      c.nuke(c)
    end
    destroy!
  end

  # TODO: Probably a helper method
  def all_children(node = self, result = [], depth = 0)
    for c in node.children.to_a.reverse # intentionally reversed
      c.all_children(c, result, depth + 1)
      a = {}
      a[:depth] = depth
      a[:lead] = c
      a[:leadLabel] = node.origin_label
      result.push(a)
    end
    result
  end

  # TODO: Probably a helper method
  def all_children_standard_key(node = self, result = [], depth = 0) # couplets before depth
    ch = node.children
    for c in ch
      a = {}
      a[:depth] = depth
      a[:lead] = c
      result.push(a)
    end

    for c in ch
      c.all_children_standard_key(c, result, depth + 1)
    end
    result
  end

  # @param reorder_list [Array] array of 0-based positions in which to order
  #  the children of this lead.
  # Raises TaxonWorks::Error on error.
  def reorder_children(reorder_list)
    validate_reorder_list(reorder_list, children.count)

    i = 0
    Lead.transaction do
      children.each do |c|
        if (new_position = reorder_list[i]) != i
          c.update_column(:position, new_position)
        end
        i = i + 1
      end
    end
  end

  # @return [ActiveRecord::Relation] ordered by text.
  # Returns all root nodes, with new properties:
  #  * couplets_count (declared here, computed in views)
  #  * otus_count (total number of distinct otus on the key)
  #  * key_updated_at (last time the key was updated)
  #  * key_updated_by_id (id of last person to update the key)
  #  * key_updated_by (name of last person to update the key)
  #
  # !! Note, the relation is a join, check your results when changing order
  # or plucking, most of want you want is on the table joined to, which is
  # not the default table for ordering and plucking.
  def self.roots_with_data(project_id, load_root_otus = false)
    # The updated_at subquery computes key_updated_at (and others), the second
    # query uses that to compute key_updated_by (by finding which node has the
    # corresponding key_updated_at).
    updated_at = Lead
      .joins('JOIN lead_hierarchies AS lh
        ON leads.id = lh.ancestor_id')
      .joins('JOIN leads AS otus_source
        ON lh.descendant_id = otus_source.id')
      .where("
        leads.parent_id IS NULL
        AND leads.project_id = #{project_id}
      ")
      .group(:id)
      .select('
        leads.*,
        COUNT(DISTINCT otus_source.otu_id) AS otus_count,
        MAX(otus_source.updated_at) AS key_updated_at,
        0 AS couplets_count' # count is now computed in views
        #·(COUNT(otus_source.id) - 1) / 2 AS couplet_count
      )

    root_leads = Lead
      .joins("JOIN (#{updated_at.to_sql}) AS leads_updated_at
        ON leads_updated_at.key_updated_at = leads.updated_at")
      .joins('JOIN users
        ON users.id = leads.updated_by_id')
      .select('
        leads_updated_at.*,
        leads.updated_by_id AS key_updated_by_id,
        users.name AS key_updated_by
      ')
      .order('leads_updated_at.text')

    return load_root_otus ? root_leads.includes(:otu) : root_leads
  end

  def redirect_options(project_id)
    leads = Lead
      .select(:id, :text, :origin_label)
      .with_project_id(project_id)
      .order(:text)
    anc_ids = ancestor_ids()

    leads.filter_map do |o|
      if o.id != id && !anc_ids.include?(o.id)
        {
          id: o.id,
          label: o.origin_label,
          text: o.text.nil? ? '' : o.text.truncate(40)
        }
      end
    end
  end

  private

  # Appends `nodes`` to the children of `new_parent``, in their given order.
  # !! Use this instead of add_child to reparent multiple nodes (add_child
  # doesn't work the way one might naievely expect, see the discussion at
  # https://github.com/SpeciesFileGroup/taxonworks/pull/3892#issuecomment-2016043296)
  def self.reparent_nodes(nodes, new_parent)
    last_sibling = new_parent.children.last # may be nil
    nodes.each do |n|
      if last_sibling.nil?
        new_parent.add_child(n)
      else
        last_sibling.append_sibling(n)
      end
      last_sibling = n
    end
  end

  # Raises TaxonWorks::Error on error.
  def validate_reorder_list(reorder_list, expected_length)
    if reorder_list.sort.uniq != (0..expected_length - 1).to_a
      raise TaxonWorks::Error,
        "Bad reorder list: #{reorder_list}"
      return
    end
  end

  def root_has_title
    errors.add(:root_node, 'must have a title') if parent_id.nil? and text.nil?
  end

  def link_out_has_protocol
    errors.add(:link, 'must include https:// or http://') if !link_out.nil? && !(link_out.start_with?('https://') || link_out.start_with?('http://'))
  end

  def redirect_node_is_leaf_node
    errors.add(:redirect, "nodes can't have children") if redirect_id && children.size > 0
  end

  def node_parent_doesnt_have_redirect
    errors.add(:parent, "can't be a redirect node") if parent && parent.redirect_id
  end

  def redirect_isnt_ancestor_or_self
    errors.add(:redirect, "can't be an ancestor") if redirect_id && (redirect_id == id || ancestor_ids.include?(redirect_id))
  end

  def root_has_no_redirect
    errors.add(:root, "nodes can't have a redirect") if redirect_id && parent_id.nil?
  end

  def dupe_in_transaction(node, parentId)
    a = node.dup
    a.parent_id = parentId
    a.text = "(COPY OF) #{a.text}" if parentId == nil
    a.save!

    node.children.each { |c| dupe_in_transaction(c, a.id) }
  end

  def set_is_public_only_on_root
    if parent_id.nil?
      self.is_public ||= false
    else
      self.is_public = nil
    end
  end

end

#positioninteger

Position of this lead amongst the key options at lead’s stage of the key; maintained by has_closure_tree

Returns:

  • (integer)


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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'app/models/lead.rb', line 47

class Lead < ApplicationRecord
  include Housekeeping
  include Shared::Citations
  include Shared::Depictions
  include Shared::Attributions
  include Shared::Tags
  include Shared::IsData

  has_closure_tree order: 'position', numeric_order: true, dont_order_roots: true, dependent: nil

  belongs_to :parent, class_name: 'Lead'

  belongs_to :otu, inverse_of: :leads
  has_one :taxon_name, through: :otu
  belongs_to :redirect, class_name: 'Lead'

  has_many :redirecters, class_name: 'Lead', foreign_key: :redirect_id, inverse_of: :redirect, dependent: :nullify

  before_save :set_is_public_only_on_root

  validate :root_has_title
  validate :link_out_has_protocol
  validate :redirect_node_is_leaf_node
  validate :node_parent_doesnt_have_redirect
  validate :root_has_no_redirect
  validate :redirect_isnt_ancestor_or_self

  def future
    redirect_id.blank? ? all_children : redirect.all_children
  end

  # @return [Boolean] true on success, false on error
  def dupe
    if parent_id
      errors.add(:base, 'Can only call dupe on a root lead')
      return false
    end

    begin
      Lead.transaction do
        dupe_in_transaction(self, parent_id)
      end
    rescue ActiveRecord::RecordInvalid => e
      errors.add(:base, "dup failed: #{e}")
      return false
    end

    true
  end

  # left/right/middle
  def node_position
    o = self_and_siblings.order(:position).pluck(:id)
    return :root if o.size == 1
    return :left if o.index(id) == 0
    return :right if o.last == id
    return :middle
  end

  def self.draw(lead, indent = 0)
    puts Rainbow( (' ' * indent) + lead.text ).purple + ' ' + Rainbow( lead.node_position.to_s ).red + ' ' + Rainbow( lead.position ).blue + ' [.parent: ' + Rainbow(lead.parent&.text || 'none').gold + ']'
    lead.children.each do |c|
      draw(c, indent + 1)
    end
  end

  # Return [Boolean] true on success, false on failure
  def insert_key(id)
    if id.nil?
      errors.add(:base, 'Id of key to insert not provided')
      return false
    end
    begin
      Lead.transaction do
        key_to_insert = Lead.find(id)
        if key_to_insert.dupe != true
          raise TaxonWorks::Error, 'dupe failed'
        end

        dup_prefix = '(COPY OF)'
        duped_text = "#{dup_prefix} #{key_to_insert.text}"
        duped_root = Lead.where(text: duped_text).order(:id).last
        if duped_root.nil?
          raise TaxonWorks::Error,
            "failed to find the duped key with text '#{duped_text}'"
        end

        # Prepare the duped key to be reparented - note that root leads don't
        # have link_out_text set (the url link is applied to the title in that
        # case), so link_out_text in the inserted root lead will always be nil.
        duped_root.update!(
          text: duped_root.text.sub(dup_prefix, '(INSERTED KEY) '),
          description: nil,
          is_public: nil
        )

        add_child(duped_root)
      end
    rescue ActiveRecord::RecordNotFound => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    rescue TaxonWorks::Error => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    rescue ActiveRecord::RecordInvalid => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    end

    true
  end

  def insert_couplet
    c, d = nil, nil

    p = node_position

    t1 = 'Inserted node'
    t2 = 'Child nodes are attached to this node'

    if children.any?
      o = children.to_a

      # Reparent under left inserted node unless this is itself a right node.
      left_text = (p != :right) ? t2 : t1
      right_text = (p == :right) ? t2 : t1

      c = Lead.create!(text: left_text, parent: self)
      d = Lead.create!(text: right_text, parent: self)

      new_parent = (p != :right) ? c : d

      Lead.reparent_nodes(o, new_parent)
    else
      c = Lead.create!(text: t1, parent: self)
      d = Lead.create!(text: t1, parent: self)
    end

    [c.id, d.id]
  end

  # Destroy the children of this Lead, re-appending the grandchildren to self
  # !! Do not destroy children if more than one child has its own children
  def destroy_children
    k = children.to_a
    return true if k.empty?

    original_child_count = k.count
    grandkids = []
    k.each do |c|
      if c.children.present?
        # Fail if more than one child has its own children
        return false if grandkids.present?

        grandkids = c.children
      end
    end

    Lead.reparent_nodes(grandkids, self)

    children.slice(0, original_child_count).each do |c|
      c.destroy!
    end

    true
  end

  def transaction_nuke(lead = self)
    Lead.transaction do
      nuke(lead)
    end
  end

  def nuke(lead = self)
    lead.children.each do |c|
      c.nuke(c)
    end
    destroy!
  end

  # TODO: Probably a helper method
  def all_children(node = self, result = [], depth = 0)
    for c in node.children.to_a.reverse # intentionally reversed
      c.all_children(c, result, depth + 1)
      a = {}
      a[:depth] = depth
      a[:lead] = c
      a[:leadLabel] = node.origin_label
      result.push(a)
    end
    result
  end

  # TODO: Probably a helper method
  def all_children_standard_key(node = self, result = [], depth = 0) # couplets before depth
    ch = node.children
    for c in ch
      a = {}
      a[:depth] = depth
      a[:lead] = c
      result.push(a)
    end

    for c in ch
      c.all_children_standard_key(c, result, depth + 1)
    end
    result
  end

  # @param reorder_list [Array] array of 0-based positions in which to order
  #  the children of this lead.
  # Raises TaxonWorks::Error on error.
  def reorder_children(reorder_list)
    validate_reorder_list(reorder_list, children.count)

    i = 0
    Lead.transaction do
      children.each do |c|
        if (new_position = reorder_list[i]) != i
          c.update_column(:position, new_position)
        end
        i = i + 1
      end
    end
  end

  # @return [ActiveRecord::Relation] ordered by text.
  # Returns all root nodes, with new properties:
  #  * couplets_count (declared here, computed in views)
  #  * otus_count (total number of distinct otus on the key)
  #  * key_updated_at (last time the key was updated)
  #  * key_updated_by_id (id of last person to update the key)
  #  * key_updated_by (name of last person to update the key)
  #
  # !! Note, the relation is a join, check your results when changing order
  # or plucking, most of want you want is on the table joined to, which is
  # not the default table for ordering and plucking.
  def self.roots_with_data(project_id, load_root_otus = false)
    # The updated_at subquery computes key_updated_at (and others), the second
    # query uses that to compute key_updated_by (by finding which node has the
    # corresponding key_updated_at).
    updated_at = Lead
      .joins('JOIN lead_hierarchies AS lh
        ON leads.id = lh.ancestor_id')
      .joins('JOIN leads AS otus_source
        ON lh.descendant_id = otus_source.id')
      .where("
        leads.parent_id IS NULL
        AND leads.project_id = #{project_id}
      ")
      .group(:id)
      .select('
        leads.*,
        COUNT(DISTINCT otus_source.otu_id) AS otus_count,
        MAX(otus_source.updated_at) AS key_updated_at,
        0 AS couplets_count' # count is now computed in views
        #·(COUNT(otus_source.id) - 1) / 2 AS couplet_count
      )

    root_leads = Lead
      .joins("JOIN (#{updated_at.to_sql}) AS leads_updated_at
        ON leads_updated_at.key_updated_at = leads.updated_at")
      .joins('JOIN users
        ON users.id = leads.updated_by_id')
      .select('
        leads_updated_at.*,
        leads.updated_by_id AS key_updated_by_id,
        users.name AS key_updated_by
      ')
      .order('leads_updated_at.text')

    return load_root_otus ? root_leads.includes(:otu) : root_leads
  end

  def redirect_options(project_id)
    leads = Lead
      .select(:id, :text, :origin_label)
      .with_project_id(project_id)
      .order(:text)
    anc_ids = ancestor_ids()

    leads.filter_map do |o|
      if o.id != id && !anc_ids.include?(o.id)
        {
          id: o.id,
          label: o.origin_label,
          text: o.text.nil? ? '' : o.text.truncate(40)
        }
      end
    end
  end

  private

  # Appends `nodes`` to the children of `new_parent``, in their given order.
  # !! Use this instead of add_child to reparent multiple nodes (add_child
  # doesn't work the way one might naievely expect, see the discussion at
  # https://github.com/SpeciesFileGroup/taxonworks/pull/3892#issuecomment-2016043296)
  def self.reparent_nodes(nodes, new_parent)
    last_sibling = new_parent.children.last # may be nil
    nodes.each do |n|
      if last_sibling.nil?
        new_parent.add_child(n)
      else
        last_sibling.append_sibling(n)
      end
      last_sibling = n
    end
  end

  # Raises TaxonWorks::Error on error.
  def validate_reorder_list(reorder_list, expected_length)
    if reorder_list.sort.uniq != (0..expected_length - 1).to_a
      raise TaxonWorks::Error,
        "Bad reorder list: #{reorder_list}"
      return
    end
  end

  def root_has_title
    errors.add(:root_node, 'must have a title') if parent_id.nil? and text.nil?
  end

  def link_out_has_protocol
    errors.add(:link, 'must include https:// or http://') if !link_out.nil? && !(link_out.start_with?('https://') || link_out.start_with?('http://'))
  end

  def redirect_node_is_leaf_node
    errors.add(:redirect, "nodes can't have children") if redirect_id && children.size > 0
  end

  def node_parent_doesnt_have_redirect
    errors.add(:parent, "can't be a redirect node") if parent && parent.redirect_id
  end

  def redirect_isnt_ancestor_or_self
    errors.add(:redirect, "can't be an ancestor") if redirect_id && (redirect_id == id || ancestor_ids.include?(redirect_id))
  end

  def root_has_no_redirect
    errors.add(:root, "nodes can't have a redirect") if redirect_id && parent_id.nil?
  end

  def dupe_in_transaction(node, parentId)
    a = node.dup
    a.parent_id = parentId
    a.text = "(COPY OF) #{a.text}" if parentId == nil
    a.save!

    node.children.each { |c| dupe_in_transaction(c, a.id) }
  end

  def set_is_public_only_on_root
    if parent_id.nil?
      self.is_public ||= false
    else
      self.is_public = nil
    end
  end

end

#redirect_idinteger

Id of the lead to redirect to if this option of the couplet is chosen

Returns:

  • (integer)


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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'app/models/lead.rb', line 47

class Lead < ApplicationRecord
  include Housekeeping
  include Shared::Citations
  include Shared::Depictions
  include Shared::Attributions
  include Shared::Tags
  include Shared::IsData

  has_closure_tree order: 'position', numeric_order: true, dont_order_roots: true, dependent: nil

  belongs_to :parent, class_name: 'Lead'

  belongs_to :otu, inverse_of: :leads
  has_one :taxon_name, through: :otu
  belongs_to :redirect, class_name: 'Lead'

  has_many :redirecters, class_name: 'Lead', foreign_key: :redirect_id, inverse_of: :redirect, dependent: :nullify

  before_save :set_is_public_only_on_root

  validate :root_has_title
  validate :link_out_has_protocol
  validate :redirect_node_is_leaf_node
  validate :node_parent_doesnt_have_redirect
  validate :root_has_no_redirect
  validate :redirect_isnt_ancestor_or_self

  def future
    redirect_id.blank? ? all_children : redirect.all_children
  end

  # @return [Boolean] true on success, false on error
  def dupe
    if parent_id
      errors.add(:base, 'Can only call dupe on a root lead')
      return false
    end

    begin
      Lead.transaction do
        dupe_in_transaction(self, parent_id)
      end
    rescue ActiveRecord::RecordInvalid => e
      errors.add(:base, "dup failed: #{e}")
      return false
    end

    true
  end

  # left/right/middle
  def node_position
    o = self_and_siblings.order(:position).pluck(:id)
    return :root if o.size == 1
    return :left if o.index(id) == 0
    return :right if o.last == id
    return :middle
  end

  def self.draw(lead, indent = 0)
    puts Rainbow( (' ' * indent) + lead.text ).purple + ' ' + Rainbow( lead.node_position.to_s ).red + ' ' + Rainbow( lead.position ).blue + ' [.parent: ' + Rainbow(lead.parent&.text || 'none').gold + ']'
    lead.children.each do |c|
      draw(c, indent + 1)
    end
  end

  # Return [Boolean] true on success, false on failure
  def insert_key(id)
    if id.nil?
      errors.add(:base, 'Id of key to insert not provided')
      return false
    end
    begin
      Lead.transaction do
        key_to_insert = Lead.find(id)
        if key_to_insert.dupe != true
          raise TaxonWorks::Error, 'dupe failed'
        end

        dup_prefix = '(COPY OF)'
        duped_text = "#{dup_prefix} #{key_to_insert.text}"
        duped_root = Lead.where(text: duped_text).order(:id).last
        if duped_root.nil?
          raise TaxonWorks::Error,
            "failed to find the duped key with text '#{duped_text}'"
        end

        # Prepare the duped key to be reparented - note that root leads don't
        # have link_out_text set (the url link is applied to the title in that
        # case), so link_out_text in the inserted root lead will always be nil.
        duped_root.update!(
          text: duped_root.text.sub(dup_prefix, '(INSERTED KEY) '),
          description: nil,
          is_public: nil
        )

        add_child(duped_root)
      end
    rescue ActiveRecord::RecordNotFound => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    rescue TaxonWorks::Error => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    rescue ActiveRecord::RecordInvalid => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    end

    true
  end

  def insert_couplet
    c, d = nil, nil

    p = node_position

    t1 = 'Inserted node'
    t2 = 'Child nodes are attached to this node'

    if children.any?
      o = children.to_a

      # Reparent under left inserted node unless this is itself a right node.
      left_text = (p != :right) ? t2 : t1
      right_text = (p == :right) ? t2 : t1

      c = Lead.create!(text: left_text, parent: self)
      d = Lead.create!(text: right_text, parent: self)

      new_parent = (p != :right) ? c : d

      Lead.reparent_nodes(o, new_parent)
    else
      c = Lead.create!(text: t1, parent: self)
      d = Lead.create!(text: t1, parent: self)
    end

    [c.id, d.id]
  end

  # Destroy the children of this Lead, re-appending the grandchildren to self
  # !! Do not destroy children if more than one child has its own children
  def destroy_children
    k = children.to_a
    return true if k.empty?

    original_child_count = k.count
    grandkids = []
    k.each do |c|
      if c.children.present?
        # Fail if more than one child has its own children
        return false if grandkids.present?

        grandkids = c.children
      end
    end

    Lead.reparent_nodes(grandkids, self)

    children.slice(0, original_child_count).each do |c|
      c.destroy!
    end

    true
  end

  def transaction_nuke(lead = self)
    Lead.transaction do
      nuke(lead)
    end
  end

  def nuke(lead = self)
    lead.children.each do |c|
      c.nuke(c)
    end
    destroy!
  end

  # TODO: Probably a helper method
  def all_children(node = self, result = [], depth = 0)
    for c in node.children.to_a.reverse # intentionally reversed
      c.all_children(c, result, depth + 1)
      a = {}
      a[:depth] = depth
      a[:lead] = c
      a[:leadLabel] = node.origin_label
      result.push(a)
    end
    result
  end

  # TODO: Probably a helper method
  def all_children_standard_key(node = self, result = [], depth = 0) # couplets before depth
    ch = node.children
    for c in ch
      a = {}
      a[:depth] = depth
      a[:lead] = c
      result.push(a)
    end

    for c in ch
      c.all_children_standard_key(c, result, depth + 1)
    end
    result
  end

  # @param reorder_list [Array] array of 0-based positions in which to order
  #  the children of this lead.
  # Raises TaxonWorks::Error on error.
  def reorder_children(reorder_list)
    validate_reorder_list(reorder_list, children.count)

    i = 0
    Lead.transaction do
      children.each do |c|
        if (new_position = reorder_list[i]) != i
          c.update_column(:position, new_position)
        end
        i = i + 1
      end
    end
  end

  # @return [ActiveRecord::Relation] ordered by text.
  # Returns all root nodes, with new properties:
  #  * couplets_count (declared here, computed in views)
  #  * otus_count (total number of distinct otus on the key)
  #  * key_updated_at (last time the key was updated)
  #  * key_updated_by_id (id of last person to update the key)
  #  * key_updated_by (name of last person to update the key)
  #
  # !! Note, the relation is a join, check your results when changing order
  # or plucking, most of want you want is on the table joined to, which is
  # not the default table for ordering and plucking.
  def self.roots_with_data(project_id, load_root_otus = false)
    # The updated_at subquery computes key_updated_at (and others), the second
    # query uses that to compute key_updated_by (by finding which node has the
    # corresponding key_updated_at).
    updated_at = Lead
      .joins('JOIN lead_hierarchies AS lh
        ON leads.id = lh.ancestor_id')
      .joins('JOIN leads AS otus_source
        ON lh.descendant_id = otus_source.id')
      .where("
        leads.parent_id IS NULL
        AND leads.project_id = #{project_id}
      ")
      .group(:id)
      .select('
        leads.*,
        COUNT(DISTINCT otus_source.otu_id) AS otus_count,
        MAX(otus_source.updated_at) AS key_updated_at,
        0 AS couplets_count' # count is now computed in views
        #·(COUNT(otus_source.id) - 1) / 2 AS couplet_count
      )

    root_leads = Lead
      .joins("JOIN (#{updated_at.to_sql}) AS leads_updated_at
        ON leads_updated_at.key_updated_at = leads.updated_at")
      .joins('JOIN users
        ON users.id = leads.updated_by_id')
      .select('
        leads_updated_at.*,
        leads.updated_by_id AS key_updated_by_id,
        users.name AS key_updated_by
      ')
      .order('leads_updated_at.text')

    return load_root_otus ? root_leads.includes(:otu) : root_leads
  end

  def redirect_options(project_id)
    leads = Lead
      .select(:id, :text, :origin_label)
      .with_project_id(project_id)
      .order(:text)
    anc_ids = ancestor_ids()

    leads.filter_map do |o|
      if o.id != id && !anc_ids.include?(o.id)
        {
          id: o.id,
          label: o.origin_label,
          text: o.text.nil? ? '' : o.text.truncate(40)
        }
      end
    end
  end

  private

  # Appends `nodes`` to the children of `new_parent``, in their given order.
  # !! Use this instead of add_child to reparent multiple nodes (add_child
  # doesn't work the way one might naievely expect, see the discussion at
  # https://github.com/SpeciesFileGroup/taxonworks/pull/3892#issuecomment-2016043296)
  def self.reparent_nodes(nodes, new_parent)
    last_sibling = new_parent.children.last # may be nil
    nodes.each do |n|
      if last_sibling.nil?
        new_parent.add_child(n)
      else
        last_sibling.append_sibling(n)
      end
      last_sibling = n
    end
  end

  # Raises TaxonWorks::Error on error.
  def validate_reorder_list(reorder_list, expected_length)
    if reorder_list.sort.uniq != (0..expected_length - 1).to_a
      raise TaxonWorks::Error,
        "Bad reorder list: #{reorder_list}"
      return
    end
  end

  def root_has_title
    errors.add(:root_node, 'must have a title') if parent_id.nil? and text.nil?
  end

  def link_out_has_protocol
    errors.add(:link, 'must include https:// or http://') if !link_out.nil? && !(link_out.start_with?('https://') || link_out.start_with?('http://'))
  end

  def redirect_node_is_leaf_node
    errors.add(:redirect, "nodes can't have children") if redirect_id && children.size > 0
  end

  def node_parent_doesnt_have_redirect
    errors.add(:parent, "can't be a redirect node") if parent && parent.redirect_id
  end

  def redirect_isnt_ancestor_or_self
    errors.add(:redirect, "can't be an ancestor") if redirect_id && (redirect_id == id || ancestor_ids.include?(redirect_id))
  end

  def root_has_no_redirect
    errors.add(:root, "nodes can't have a redirect") if redirect_id && parent_id.nil?
  end

  def dupe_in_transaction(node, parentId)
    a = node.dup
    a.parent_id = parentId
    a.text = "(COPY OF) #{a.text}" if parentId == nil
    a.save!

    node.children.each { |c| dupe_in_transaction(c, a.id) }
  end

  def set_is_public_only_on_root
    if parent_id.nil?
      self.is_public ||= false
    else
      self.is_public = nil
    end
  end

end

#texttext

Text for this option of a couplet in the key; title of the key on the root node

Returns:



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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'app/models/lead.rb', line 47

class Lead < ApplicationRecord
  include Housekeeping
  include Shared::Citations
  include Shared::Depictions
  include Shared::Attributions
  include Shared::Tags
  include Shared::IsData

  has_closure_tree order: 'position', numeric_order: true, dont_order_roots: true, dependent: nil

  belongs_to :parent, class_name: 'Lead'

  belongs_to :otu, inverse_of: :leads
  has_one :taxon_name, through: :otu
  belongs_to :redirect, class_name: 'Lead'

  has_many :redirecters, class_name: 'Lead', foreign_key: :redirect_id, inverse_of: :redirect, dependent: :nullify

  before_save :set_is_public_only_on_root

  validate :root_has_title
  validate :link_out_has_protocol
  validate :redirect_node_is_leaf_node
  validate :node_parent_doesnt_have_redirect
  validate :root_has_no_redirect
  validate :redirect_isnt_ancestor_or_self

  def future
    redirect_id.blank? ? all_children : redirect.all_children
  end

  # @return [Boolean] true on success, false on error
  def dupe
    if parent_id
      errors.add(:base, 'Can only call dupe on a root lead')
      return false
    end

    begin
      Lead.transaction do
        dupe_in_transaction(self, parent_id)
      end
    rescue ActiveRecord::RecordInvalid => e
      errors.add(:base, "dup failed: #{e}")
      return false
    end

    true
  end

  # left/right/middle
  def node_position
    o = self_and_siblings.order(:position).pluck(:id)
    return :root if o.size == 1
    return :left if o.index(id) == 0
    return :right if o.last == id
    return :middle
  end

  def self.draw(lead, indent = 0)
    puts Rainbow( (' ' * indent) + lead.text ).purple + ' ' + Rainbow( lead.node_position.to_s ).red + ' ' + Rainbow( lead.position ).blue + ' [.parent: ' + Rainbow(lead.parent&.text || 'none').gold + ']'
    lead.children.each do |c|
      draw(c, indent + 1)
    end
  end

  # Return [Boolean] true on success, false on failure
  def insert_key(id)
    if id.nil?
      errors.add(:base, 'Id of key to insert not provided')
      return false
    end
    begin
      Lead.transaction do
        key_to_insert = Lead.find(id)
        if key_to_insert.dupe != true
          raise TaxonWorks::Error, 'dupe failed'
        end

        dup_prefix = '(COPY OF)'
        duped_text = "#{dup_prefix} #{key_to_insert.text}"
        duped_root = Lead.where(text: duped_text).order(:id).last
        if duped_root.nil?
          raise TaxonWorks::Error,
            "failed to find the duped key with text '#{duped_text}'"
        end

        # Prepare the duped key to be reparented - note that root leads don't
        # have link_out_text set (the url link is applied to the title in that
        # case), so link_out_text in the inserted root lead will always be nil.
        duped_root.update!(
          text: duped_root.text.sub(dup_prefix, '(INSERTED KEY) '),
          description: nil,
          is_public: nil
        )

        add_child(duped_root)
      end
    rescue ActiveRecord::RecordNotFound => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    rescue TaxonWorks::Error => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    rescue ActiveRecord::RecordInvalid => e
      errors.add(:base, "Insert key failed: #{e}")
      return false
    end

    true
  end

  def insert_couplet
    c, d = nil, nil

    p = node_position

    t1 = 'Inserted node'
    t2 = 'Child nodes are attached to this node'

    if children.any?
      o = children.to_a

      # Reparent under left inserted node unless this is itself a right node.
      left_text = (p != :right) ? t2 : t1
      right_text = (p == :right) ? t2 : t1

      c = Lead.create!(text: left_text, parent: self)
      d = Lead.create!(text: right_text, parent: self)

      new_parent = (p != :right) ? c : d

      Lead.reparent_nodes(o, new_parent)
    else
      c = Lead.create!(text: t1, parent: self)
      d = Lead.create!(text: t1, parent: self)
    end

    [c.id, d.id]
  end

  # Destroy the children of this Lead, re-appending the grandchildren to self
  # !! Do not destroy children if more than one child has its own children
  def destroy_children
    k = children.to_a
    return true if k.empty?

    original_child_count = k.count
    grandkids = []
    k.each do |c|
      if c.children.present?
        # Fail if more than one child has its own children
        return false if grandkids.present?

        grandkids = c.children
      end
    end

    Lead.reparent_nodes(grandkids, self)

    children.slice(0, original_child_count).each do |c|
      c.destroy!
    end

    true
  end

  def transaction_nuke(lead = self)
    Lead.transaction do
      nuke(lead)
    end
  end

  def nuke(lead = self)
    lead.children.each do |c|
      c.nuke(c)
    end
    destroy!
  end

  # TODO: Probably a helper method
  def all_children(node = self, result = [], depth = 0)
    for c in node.children.to_a.reverse # intentionally reversed
      c.all_children(c, result, depth + 1)
      a = {}
      a[:depth] = depth
      a[:lead] = c
      a[:leadLabel] = node.origin_label
      result.push(a)
    end
    result
  end

  # TODO: Probably a helper method
  def all_children_standard_key(node = self, result = [], depth = 0) # couplets before depth
    ch = node.children
    for c in ch
      a = {}
      a[:depth] = depth
      a[:lead] = c
      result.push(a)
    end

    for c in ch
      c.all_children_standard_key(c, result, depth + 1)
    end
    result
  end

  # @param reorder_list [Array] array of 0-based positions in which to order
  #  the children of this lead.
  # Raises TaxonWorks::Error on error.
  def reorder_children(reorder_list)
    validate_reorder_list(reorder_list, children.count)

    i = 0
    Lead.transaction do
      children.each do |c|
        if (new_position = reorder_list[i]) != i
          c.update_column(:position, new_position)
        end
        i = i + 1
      end
    end
  end

  # @return [ActiveRecord::Relation] ordered by text.
  # Returns all root nodes, with new properties:
  #  * couplets_count (declared here, computed in views)
  #  * otus_count (total number of distinct otus on the key)
  #  * key_updated_at (last time the key was updated)
  #  * key_updated_by_id (id of last person to update the key)
  #  * key_updated_by (name of last person to update the key)
  #
  # !! Note, the relation is a join, check your results when changing order
  # or plucking, most of want you want is on the table joined to, which is
  # not the default table for ordering and plucking.
  def self.roots_with_data(project_id, load_root_otus = false)
    # The updated_at subquery computes key_updated_at (and others), the second
    # query uses that to compute key_updated_by (by finding which node has the
    # corresponding key_updated_at).
    updated_at = Lead
      .joins('JOIN lead_hierarchies AS lh
        ON leads.id = lh.ancestor_id')
      .joins('JOIN leads AS otus_source
        ON lh.descendant_id = otus_source.id')
      .where("
        leads.parent_id IS NULL
        AND leads.project_id = #{project_id}
      ")
      .group(:id)
      .select('
        leads.*,
        COUNT(DISTINCT otus_source.otu_id) AS otus_count,
        MAX(otus_source.updated_at) AS key_updated_at,
        0 AS couplets_count' # count is now computed in views
        #·(COUNT(otus_source.id) - 1) / 2 AS couplet_count
      )

    root_leads = Lead
      .joins("JOIN (#{updated_at.to_sql}) AS leads_updated_at
        ON leads_updated_at.key_updated_at = leads.updated_at")
      .joins('JOIN users
        ON users.id = leads.updated_by_id')
      .select('
        leads_updated_at.*,
        leads.updated_by_id AS key_updated_by_id,
        users.name AS key_updated_by
      ')
      .order('leads_updated_at.text')

    return load_root_otus ? root_leads.includes(:otu) : root_leads
  end

  def redirect_options(project_id)
    leads = Lead
      .select(:id, :text, :origin_label)
      .with_project_id(project_id)
      .order(:text)
    anc_ids = ancestor_ids()

    leads.filter_map do |o|
      if o.id != id && !anc_ids.include?(o.id)
        {
          id: o.id,
          label: o.origin_label,
          text: o.text.nil? ? '' : o.text.truncate(40)
        }
      end
    end
  end

  private

  # Appends `nodes`` to the children of `new_parent``, in their given order.
  # !! Use this instead of add_child to reparent multiple nodes (add_child
  # doesn't work the way one might naievely expect, see the discussion at
  # https://github.com/SpeciesFileGroup/taxonworks/pull/3892#issuecomment-2016043296)
  def self.reparent_nodes(nodes, new_parent)
    last_sibling = new_parent.children.last # may be nil
    nodes.each do |n|
      if last_sibling.nil?
        new_parent.add_child(n)
      else
        last_sibling.append_sibling(n)
      end
      last_sibling = n
    end
  end

  # Raises TaxonWorks::Error on error.
  def validate_reorder_list(reorder_list, expected_length)
    if reorder_list.sort.uniq != (0..expected_length - 1).to_a
      raise TaxonWorks::Error,
        "Bad reorder list: #{reorder_list}"
      return
    end
  end

  def root_has_title
    errors.add(:root_node, 'must have a title') if parent_id.nil? and text.nil?
  end

  def link_out_has_protocol
    errors.add(:link, 'must include https:// or http://') if !link_out.nil? && !(link_out.start_with?('https://') || link_out.start_with?('http://'))
  end

  def redirect_node_is_leaf_node
    errors.add(:redirect, "nodes can't have children") if redirect_id && children.size > 0
  end

  def node_parent_doesnt_have_redirect
    errors.add(:parent, "can't be a redirect node") if parent && parent.redirect_id
  end

  def redirect_isnt_ancestor_or_self
    errors.add(:redirect, "can't be an ancestor") if redirect_id && (redirect_id == id || ancestor_ids.include?(redirect_id))
  end

  def root_has_no_redirect
    errors.add(:root, "nodes can't have a redirect") if redirect_id && parent_id.nil?
  end

  def dupe_in_transaction(node, parentId)
    a = node.dup
    a.parent_id = parentId
    a.text = "(COPY OF) #{a.text}" if parentId == nil
    a.save!

    node.children.each { |c| dupe_in_transaction(c, a.id) }
  end

  def set_is_public_only_on_root
    if parent_id.nil?
      self.is_public ||= false
    else
      self.is_public = nil
    end
  end

end

Class Method Details

.draw(lead, indent = 0) ⇒ Object



106
107
108
109
110
111
# File 'app/models/lead.rb', line 106

def self.draw(lead, indent = 0)
  puts Rainbow( (' ' * indent) + lead.text ).purple + ' ' + Rainbow( lead.node_position.to_s ).red + ' ' + Rainbow( lead.position ).blue + ' [.parent: ' + Rainbow(lead.parent&.text || 'none').gold + ']'
  lead.children.each do |c|
    draw(c, indent + 1)
  end
end

.reparent_nodes(nodes, new_parent) ⇒ Object (private)

Appends ‘nodes“ to the children of `new_parent“, in their given order. !! Use this instead of add_child to reparent multiple nodes (add_child doesn’t work the way one might naievely expect, see the discussion at github.com/SpeciesFileGroup/taxonworks/pull/3892#issuecomment-2016043296)



345
346
347
348
349
350
351
352
353
354
355
# File 'app/models/lead.rb', line 345

def self.reparent_nodes(nodes, new_parent)
  last_sibling = new_parent.children.last # may be nil
  nodes.each do |n|
    if last_sibling.nil?
      new_parent.add_child(n)
    else
      last_sibling.append_sibling(n)
    end
    last_sibling = n
  end
end

.roots_with_data(project_id, load_root_otus = false) ⇒ ActiveRecord::Relation

Returns all root nodes, with new properties:

* couplets_count (declared here, computed in views)
* otus_count (total number of distinct otus on the key)
* key_updated_at (last time the key was updated)
* key_updated_by_id (id of last person to update the key)
* key_updated_by (name of last person to update the key)

!! Note, the relation is a join, check your results when changing order or plucking, most of want you want is on the table joined to, which is not the default table for ordering and plucking.

Returns:

  • (ActiveRecord::Relation)

    ordered by text.



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'app/models/lead.rb', line 284

def self.roots_with_data(project_id, load_root_otus = false)
  # The updated_at subquery computes key_updated_at (and others), the second
  # query uses that to compute key_updated_by (by finding which node has the
  # corresponding key_updated_at).
  updated_at = Lead
    .joins('JOIN lead_hierarchies AS lh
      ON leads.id = lh.ancestor_id')
    .joins('JOIN leads AS otus_source
      ON lh.descendant_id = otus_source.id')
    .where("
      leads.parent_id IS NULL
      AND leads.project_id = #{project_id}
    ")
    .group(:id)
    .select('
      leads.*,
      COUNT(DISTINCT otus_source.otu_id) AS otus_count,
      MAX(otus_source.updated_at) AS key_updated_at,
      0 AS couplets_count' # count is now computed in views
      #·(COUNT(otus_source.id) - 1) / 2 AS couplet_count
    )

  root_leads = Lead
    .joins("JOIN (#{updated_at.to_sql}) AS leads_updated_at
      ON leads_updated_at.key_updated_at = leads.updated_at")
    .joins('JOIN users
      ON users.id = leads.updated_by_id')
    .select('
      leads_updated_at.*,
      leads.updated_by_id AS key_updated_by_id,
      users.name AS key_updated_by
    ')
    .order('leads_updated_at.text')

  return load_root_otus ? root_leads.includes(:otu) : root_leads
end

Instance Method Details

#all_children(node = self, result = [], depth = 0) ⇒ Object

TODO: Probably a helper method



228
229
230
231
232
233
234
235
236
237
238
# File 'app/models/lead.rb', line 228

def all_children(node = self, result = [], depth = 0)
  for c in node.children.to_a.reverse # intentionally reversed
    c.all_children(c, result, depth + 1)
    a = {}
    a[:depth] = depth
    a[:lead] = c
    a[:leadLabel] = node.origin_label
    result.push(a)
  end
  result
end

#all_children_standard_key(node = self, result = [], depth = 0) ⇒ Object

TODO: Probably a helper method



241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'app/models/lead.rb', line 241

def all_children_standard_key(node = self, result = [], depth = 0) # couplets before depth
  ch = node.children
  for c in ch
    a = {}
    a[:depth] = depth
    a[:lead] = c
    result.push(a)
  end

  for c in ch
    c.all_children_standard_key(c, result, depth + 1)
  end
  result
end

#destroy_childrenObject

Destroy the children of this Lead, re-appending the grandchildren to self !! Do not destroy children if more than one child has its own children



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'app/models/lead.rb', line 190

def destroy_children
  k = children.to_a
  return true if k.empty?

  original_child_count = k.count
  grandkids = []
  k.each do |c|
    if c.children.present?
      # Fail if more than one child has its own children
      return false if grandkids.present?

      grandkids = c.children
    end
  end

  Lead.reparent_nodes(grandkids, self)

  children.slice(0, original_child_count).each do |c|
    c.destroy!
  end

  true
end

#dupeBoolean

Returns true on success, false on error.

Returns:

  • (Boolean)

    true on success, false on error



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'app/models/lead.rb', line 79

def dupe
  if parent_id
    errors.add(:base, 'Can only call dupe on a root lead')
    return false
  end

  begin
    Lead.transaction do
      dupe_in_transaction(self, parent_id)
    end
  rescue ActiveRecord::RecordInvalid => e
    errors.add(:base, "dup failed: #{e}")
    return false
  end

  true
end

#dupe_in_transaction(node, parentId) ⇒ Object (private)



390
391
392
393
394
395
396
397
# File 'app/models/lead.rb', line 390

def dupe_in_transaction(node, parentId)
  a = node.dup
  a.parent_id = parentId
  a.text = "(COPY OF) #{a.text}" if parentId == nil
  a.save!

  node.children.each { |c| dupe_in_transaction(c, a.id) }
end

#futureObject



74
75
76
# File 'app/models/lead.rb', line 74

def future
  redirect_id.blank? ? all_children : redirect.all_children
end

#insert_coupletObject



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
# File 'app/models/lead.rb', line 159

def insert_couplet
  c, d = nil, nil

  p = node_position

  t1 = 'Inserted node'
  t2 = 'Child nodes are attached to this node'

  if children.any?
    o = children.to_a

    # Reparent under left inserted node unless this is itself a right node.
    left_text = (p != :right) ? t2 : t1
    right_text = (p == :right) ? t2 : t1

    c = Lead.create!(text: left_text, parent: self)
    d = Lead.create!(text: right_text, parent: self)

    new_parent = (p != :right) ? c : d

    Lead.reparent_nodes(o, new_parent)
  else
    c = Lead.create!(text: t1, parent: self)
    d = Lead.create!(text: t1, parent: self)
  end

  [c.id, d.id]
end

#insert_key(id) ⇒ Object

Return [Boolean] true on success, false on failure



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
# File 'app/models/lead.rb', line 114

def insert_key(id)
  if id.nil?
    errors.add(:base, 'Id of key to insert not provided')
    return false
  end
  begin
    Lead.transaction do
      key_to_insert = Lead.find(id)
      if key_to_insert.dupe != true
        raise TaxonWorks::Error, 'dupe failed'
      end

      dup_prefix = '(COPY OF)'
      duped_text = "#{dup_prefix} #{key_to_insert.text}"
      duped_root = Lead.where(text: duped_text).order(:id).last
      if duped_root.nil?
        raise TaxonWorks::Error,
          "failed to find the duped key with text '#{duped_text}'"
      end

      # Prepare the duped key to be reparented - note that root leads don't
      # have link_out_text set (the url link is applied to the title in that
      # case), so link_out_text in the inserted root lead will always be nil.
      duped_root.update!(
        text: duped_root.text.sub(dup_prefix, '(INSERTED KEY) '),
        description: nil,
        is_public: nil
      )

      add_child(duped_root)
    end
  rescue ActiveRecord::RecordNotFound => e
    errors.add(:base, "Insert key failed: #{e}")
    return false
  rescue TaxonWorks::Error => e
    errors.add(:base, "Insert key failed: #{e}")
    return false
  rescue ActiveRecord::RecordInvalid => e
    errors.add(:base, "Insert key failed: #{e}")
    return false
  end

  true
end


370
371
372
# File 'app/models/lead.rb', line 370

def link_out_has_protocol
  errors.add(:link, 'must include https:// or http://') if !link_out.nil? && !(link_out.start_with?('https://') || link_out.start_with?('http://'))
end

#node_parent_doesnt_have_redirectObject (private)



378
379
380
# File 'app/models/lead.rb', line 378

def node_parent_doesnt_have_redirect
  errors.add(:parent, "can't be a redirect node") if parent && parent.redirect_id
end

#node_positionObject

left/right/middle



98
99
100
101
102
103
104
# File 'app/models/lead.rb', line 98

def node_position
  o = self_and_siblings.order(:position).pluck(:id)
  return :root if o.size == 1
  return :left if o.index(id) == 0
  return :right if o.last == id
  return :middle
end

#nuke(lead = self) ⇒ Object



220
221
222
223
224
225
# File 'app/models/lead.rb', line 220

def nuke(lead = self)
  lead.children.each do |c|
    c.nuke(c)
  end
  destroy!
end

#redirect_isnt_ancestor_or_selfObject (private)



382
383
384
# File 'app/models/lead.rb', line 382

def redirect_isnt_ancestor_or_self
  errors.add(:redirect, "can't be an ancestor") if redirect_id && (redirect_id == id || ancestor_ids.include?(redirect_id))
end

#redirect_node_is_leaf_nodeObject (private)



374
375
376
# File 'app/models/lead.rb', line 374

def redirect_node_is_leaf_node
  errors.add(:redirect, "nodes can't have children") if redirect_id && children.size > 0
end

#redirect_options(project_id) ⇒ Object



321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'app/models/lead.rb', line 321

def redirect_options(project_id)
  leads = Lead
    .select(:id, :text, :origin_label)
    .with_project_id(project_id)
    .order(:text)
  anc_ids = ancestor_ids()

  leads.filter_map do |o|
    if o.id != id && !anc_ids.include?(o.id)
      {
        id: o.id,
        label: o.origin_label,
        text: o.text.nil? ? '' : o.text.truncate(40)
      }
    end
  end
end

#reorder_children(reorder_list) ⇒ Object

Raises TaxonWorks::Error on error.

Parameters:

  • reorder_list (Array)

    array of 0-based positions in which to order the children of this lead.



259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'app/models/lead.rb', line 259

def reorder_children(reorder_list)
  validate_reorder_list(reorder_list, children.count)

  i = 0
  Lead.transaction do
    children.each do |c|
      if (new_position = reorder_list[i]) != i
        c.update_column(:position, new_position)
      end
      i = i + 1
    end
  end
end

#root_has_no_redirectObject (private)



386
387
388
# File 'app/models/lead.rb', line 386

def root_has_no_redirect
  errors.add(:root, "nodes can't have a redirect") if redirect_id && parent_id.nil?
end

#root_has_titleObject (private)



366
367
368
# File 'app/models/lead.rb', line 366

def root_has_title
  errors.add(:root_node, 'must have a title') if parent_id.nil? and text.nil?
end

#set_is_public_only_on_rootObject (private)



399
400
401
402
403
404
405
# File 'app/models/lead.rb', line 399

def set_is_public_only_on_root
  if parent_id.nil?
    self.is_public ||= false
  else
    self.is_public = nil
  end
end

#transaction_nuke(lead = self) ⇒ Object



214
215
216
217
218
# File 'app/models/lead.rb', line 214

def transaction_nuke(lead = self)
  Lead.transaction do
    nuke(lead)
  end
end

#validate_reorder_list(reorder_list, expected_length) ⇒ Object (private)

Raises TaxonWorks::Error on error.



358
359
360
361
362
363
364
# File 'app/models/lead.rb', line 358

def validate_reorder_list(reorder_list, expected_length)
  if reorder_list.sort.uniq != (0..expected_length - 1).to_a
    raise TaxonWorks::Error,
      "Bad reorder list: #{reorder_list}"
    return
  end
end