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
408
# 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
  validates_uniqueness_of :text, scope: [:otu_id, :parent_id], unless: -> { otu_id.nil? }

  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
408
# 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
  validates_uniqueness_of :text, scope: [:otu_id, :parent_id], unless: -> { otu_id.nil? }

  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
408
# 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
  validates_uniqueness_of :text, scope: [:otu_id, :parent_id], unless: -> { otu_id.nil? }

  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
408
# 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
  validates_uniqueness_of :text, scope: [:otu_id, :parent_id], unless: -> { otu_id.nil? }

  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
408
# 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
  validates_uniqueness_of :text, scope: [:otu_id, :parent_id], unless: -> { otu_id.nil? }

  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
408
# 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
  validates_uniqueness_of :text, scope: [:otu_id, :parent_id], unless: -> { otu_id.nil? }

  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
408
# 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
  validates_uniqueness_of :text, scope: [:otu_id, :parent_id], unless: -> { otu_id.nil? }

  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
408
# 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
  validates_uniqueness_of :text, scope: [:otu_id, :parent_id], unless: -> { otu_id.nil? }

  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
408
# 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
  validates_uniqueness_of :text, scope: [:otu_id, :parent_id], unless: -> { otu_id.nil? }

  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
408
# 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
  validates_uniqueness_of :text, scope: [:otu_id, :parent_id], unless: -> { otu_id.nil? }

  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



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

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)



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

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.



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

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



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

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



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

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



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

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



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

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)



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

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



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

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

#insert_coupletObject



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

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



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

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


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

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)



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

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



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

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



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

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

#redirect_isnt_ancestor_or_selfObject (private)



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

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)



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

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



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

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.



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

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)



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

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)



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

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)



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

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



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

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.



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

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