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:



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
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
# File 'app/models/lead.rb', line 51

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
  has_many :lead_items, inverse_of: :lead, dependent: :destroy
  has_many :lead_item_otus, through: :lead_items, source: :otu

  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 :text, uniqueness: { 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
  # dupe does not dupe lead_items
  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

  def add_children(project_id, user_id)
    if children.exists?
      children.create! # now multi-furcating, if it wasn't already
      return
    end

    # Create a new couplet.
    children.create!
    right_child = children.create!

    if lead_items.exists?
      Lead.transaction do
        # On a new couplet all otus start on the right hand side (the left hand
        # side typically being the shorter path through the key).
        # No ancestor leads ever retain their lead_items.
        LeadItem.move_items(self.lead_items, right_child)
      end
    end
  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.to_a
      end
    end

    Lead.transaction do
      if grandkids.present?
        lead_item_ids = children.map { |c|
          c.children.exists? ? [] : c.lead_items
        }.flatten(1).map(&:id)

        if lead_item_ids.present?
          placeholder = Lead.create!(
            text: 'PLACEHOLDER TO HOLD OTU OPTIONS FROM DELETED CHILDREN'
          )
          LeadItem.move_items(LeadItem.where(id: lead_item_ids), placeholder)
          grandkids = grandkids.prepend(placeholder)
        end
      else
        LeadItem.consolidate_descendant_items(self, self)
      end

      Lead.reparent_nodes(grandkids, self)

      children.slice(0, original_child_count).each do |c|
        c.destroy!
      end
    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
      # TODO: avoid the extra query (and hash value) when this isn't a
      # lead_items key - currently no way to know except by checking every lead.
      a[:leadItemsCount] = c.lead_items.count
      result.push(a)
    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

  def apportioned_lead_item_otus
    combined_otus =
      children.map { |c| c.lead_item_otus.includes(:taxon_name) }.flatten.uniq
    # TODO: Currently there's no (cheap) way to know if this is empty because
    # this isn't a lead_items key or because we're on a couplet that has no
    # lead_items (because, for example, it was an inserted couplet). It would be
    # useful to send an indication if we're in the latter case.
    return { parent: [], children: [] } if combined_otus.empty?

    {
      parent: combined_otus,
      children: children.map do |c|
        fixed = c.children.exists?
        {
          fixed:,
          otu_ids: fixed ? [] : c.lead_item_otus.map(&:id)
        }
      end
    }
  end

  def batch_populate_lead_items(otu_query, project_id, user_id)
    otus = ::Queries::Otu::Filter.new(otu_query).all
    LeadItem.batch_add_otus_for_lead(id, otus.map(&:id), project_id, user_id)
  end

  # Raises TaxonWorks::Error on error.
  def self.destroy_lead_and_descendants(lead, project_id, user_id)
    parent = lead.parent

    begin
      if lead.children.exists?
        items_lead = LeadItem.consolidate_descendant_items(lead)
        lead.prepend_sibling(items_lead) if items_lead.present?
      elsif lead.lead_items.exists? # lead_items with no children
        raise TaxonWorks::Error,
          'Delete not permitted; remove all checked otus first.'
      end

      lead.transaction_nuke
    rescue ActiveRecord::RecordInvalid
      raise TaxonWorks::Error,
        "Subtree destroy failed! '#{lead.errors.full_messages.join('; ')}'"
    rescue TaxonWorks::Error
      raise
    end
  end

  # @return Public root leads that have a leaf descendant with `otu` as its Otu,
  # i.e. keys that have otu on a leaf node.
  # !! Note it doesn't count if a key only has otu on an internal node;
  # currently that's only allowed in TW, not in TP.
  def self.public_root_leads_for_leaf_otus(otu)
    # Leaf leads that have otu as their Otu.
    leaf_otu_leads = Lead
      .with(l_h: LeadHierarchy.where('ancestor_id != descendant_id'))
      .merge(otu.leads)
      .joins('LEFT OUTER JOIN l_h ON leads.id = l_h.ancestor_id')
      .where(l_h: {ancestor_id: nil})

    # Root leads that are public and have one of leaf_otu_leads as their
    # descendant.
    Lead
      .with(l_o_l: leaf_otu_leads)
      .joins('JOIN lead_hierarchies l_h2 ON l_h2.ancestor_id = leads.id')
      .where('l_h2.descendant_id IN (SELECT id FROM l_o_l)')
      .where(parent_id: nil)
      .where(is_public: true)
  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)


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
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
# File 'app/models/lead.rb', line 51

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
  has_many :lead_items, inverse_of: :lead, dependent: :destroy
  has_many :lead_item_otus, through: :lead_items, source: :otu

  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 :text, uniqueness: { 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
  # dupe does not dupe lead_items
  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

  def add_children(project_id, user_id)
    if children.exists?
      children.create! # now multi-furcating, if it wasn't already
      return
    end

    # Create a new couplet.
    children.create!
    right_child = children.create!

    if lead_items.exists?
      Lead.transaction do
        # On a new couplet all otus start on the right hand side (the left hand
        # side typically being the shorter path through the key).
        # No ancestor leads ever retain their lead_items.
        LeadItem.move_items(self.lead_items, right_child)
      end
    end
  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.to_a
      end
    end

    Lead.transaction do
      if grandkids.present?
        lead_item_ids = children.map { |c|
          c.children.exists? ? [] : c.lead_items
        }.flatten(1).map(&:id)

        if lead_item_ids.present?
          placeholder = Lead.create!(
            text: 'PLACEHOLDER TO HOLD OTU OPTIONS FROM DELETED CHILDREN'
          )
          LeadItem.move_items(LeadItem.where(id: lead_item_ids), placeholder)
          grandkids = grandkids.prepend(placeholder)
        end
      else
        LeadItem.consolidate_descendant_items(self, self)
      end

      Lead.reparent_nodes(grandkids, self)

      children.slice(0, original_child_count).each do |c|
        c.destroy!
      end
    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
      # TODO: avoid the extra query (and hash value) when this isn't a
      # lead_items key - currently no way to know except by checking every lead.
      a[:leadItemsCount] = c.lead_items.count
      result.push(a)
    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

  def apportioned_lead_item_otus
    combined_otus =
      children.map { |c| c.lead_item_otus.includes(:taxon_name) }.flatten.uniq
    # TODO: Currently there's no (cheap) way to know if this is empty because
    # this isn't a lead_items key or because we're on a couplet that has no
    # lead_items (because, for example, it was an inserted couplet). It would be
    # useful to send an indication if we're in the latter case.
    return { parent: [], children: [] } if combined_otus.empty?

    {
      parent: combined_otus,
      children: children.map do |c|
        fixed = c.children.exists?
        {
          fixed:,
          otu_ids: fixed ? [] : c.lead_item_otus.map(&:id)
        }
      end
    }
  end

  def batch_populate_lead_items(otu_query, project_id, user_id)
    otus = ::Queries::Otu::Filter.new(otu_query).all
    LeadItem.batch_add_otus_for_lead(id, otus.map(&:id), project_id, user_id)
  end

  # Raises TaxonWorks::Error on error.
  def self.destroy_lead_and_descendants(lead, project_id, user_id)
    parent = lead.parent

    begin
      if lead.children.exists?
        items_lead = LeadItem.consolidate_descendant_items(lead)
        lead.prepend_sibling(items_lead) if items_lead.present?
      elsif lead.lead_items.exists? # lead_items with no children
        raise TaxonWorks::Error,
          'Delete not permitted; remove all checked otus first.'
      end

      lead.transaction_nuke
    rescue ActiveRecord::RecordInvalid
      raise TaxonWorks::Error,
        "Subtree destroy failed! '#{lead.errors.full_messages.join('; ')}'"
    rescue TaxonWorks::Error
      raise
    end
  end

  # @return Public root leads that have a leaf descendant with `otu` as its Otu,
  # i.e. keys that have otu on a leaf node.
  # !! Note it doesn't count if a key only has otu on an internal node;
  # currently that's only allowed in TW, not in TP.
  def self.public_root_leads_for_leaf_otus(otu)
    # Leaf leads that have otu as their Otu.
    leaf_otu_leads = Lead
      .with(l_h: LeadHierarchy.where('ancestor_id != descendant_id'))
      .merge(otu.leads)
      .joins('LEFT OUTER JOIN l_h ON leads.id = l_h.ancestor_id')
      .where(l_h: {ancestor_id: nil})

    # Root leads that are public and have one of leaf_otu_leads as their
    # descendant.
    Lead
      .with(l_o_l: leaf_otu_leads)
      .joins('JOIN lead_hierarchies l_h2 ON l_h2.ancestor_id = leads.id')
      .where('l_h2.descendant_id IN (SELECT id FROM l_o_l)')
      .where(parent_id: nil)
      .where(is_public: true)
  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:



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
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
# File 'app/models/lead.rb', line 51

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
  has_many :lead_items, inverse_of: :lead, dependent: :destroy
  has_many :lead_item_otus, through: :lead_items, source: :otu

  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 :text, uniqueness: { 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
  # dupe does not dupe lead_items
  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

  def add_children(project_id, user_id)
    if children.exists?
      children.create! # now multi-furcating, if it wasn't already
      return
    end

    # Create a new couplet.
    children.create!
    right_child = children.create!

    if lead_items.exists?
      Lead.transaction do
        # On a new couplet all otus start on the right hand side (the left hand
        # side typically being the shorter path through the key).
        # No ancestor leads ever retain their lead_items.
        LeadItem.move_items(self.lead_items, right_child)
      end
    end
  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.to_a
      end
    end

    Lead.transaction do
      if grandkids.present?
        lead_item_ids = children.map { |c|
          c.children.exists? ? [] : c.lead_items
        }.flatten(1).map(&:id)

        if lead_item_ids.present?
          placeholder = Lead.create!(
            text: 'PLACEHOLDER TO HOLD OTU OPTIONS FROM DELETED CHILDREN'
          )
          LeadItem.move_items(LeadItem.where(id: lead_item_ids), placeholder)
          grandkids = grandkids.prepend(placeholder)
        end
      else
        LeadItem.consolidate_descendant_items(self, self)
      end

      Lead.reparent_nodes(grandkids, self)

      children.slice(0, original_child_count).each do |c|
        c.destroy!
      end
    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
      # TODO: avoid the extra query (and hash value) when this isn't a
      # lead_items key - currently no way to know except by checking every lead.
      a[:leadItemsCount] = c.lead_items.count
      result.push(a)
    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

  def apportioned_lead_item_otus
    combined_otus =
      children.map { |c| c.lead_item_otus.includes(:taxon_name) }.flatten.uniq
    # TODO: Currently there's no (cheap) way to know if this is empty because
    # this isn't a lead_items key or because we're on a couplet that has no
    # lead_items (because, for example, it was an inserted couplet). It would be
    # useful to send an indication if we're in the latter case.
    return { parent: [], children: [] } if combined_otus.empty?

    {
      parent: combined_otus,
      children: children.map do |c|
        fixed = c.children.exists?
        {
          fixed:,
          otu_ids: fixed ? [] : c.lead_item_otus.map(&:id)
        }
      end
    }
  end

  def batch_populate_lead_items(otu_query, project_id, user_id)
    otus = ::Queries::Otu::Filter.new(otu_query).all
    LeadItem.batch_add_otus_for_lead(id, otus.map(&:id), project_id, user_id)
  end

  # Raises TaxonWorks::Error on error.
  def self.destroy_lead_and_descendants(lead, project_id, user_id)
    parent = lead.parent

    begin
      if lead.children.exists?
        items_lead = LeadItem.consolidate_descendant_items(lead)
        lead.prepend_sibling(items_lead) if items_lead.present?
      elsif lead.lead_items.exists? # lead_items with no children
        raise TaxonWorks::Error,
          'Delete not permitted; remove all checked otus first.'
      end

      lead.transaction_nuke
    rescue ActiveRecord::RecordInvalid
      raise TaxonWorks::Error,
        "Subtree destroy failed! '#{lead.errors.full_messages.join('; ')}'"
    rescue TaxonWorks::Error
      raise
    end
  end

  # @return Public root leads that have a leaf descendant with `otu` as its Otu,
  # i.e. keys that have otu on a leaf node.
  # !! Note it doesn't count if a key only has otu on an internal node;
  # currently that's only allowed in TW, not in TP.
  def self.public_root_leads_for_leaf_otus(otu)
    # Leaf leads that have otu as their Otu.
    leaf_otu_leads = Lead
      .with(l_h: LeadHierarchy.where('ancestor_id != descendant_id'))
      .merge(otu.leads)
      .joins('LEFT OUTER JOIN l_h ON leads.id = l_h.ancestor_id')
      .where(l_h: {ancestor_id: nil})

    # Root leads that are public and have one of leaf_otu_leads as their
    # descendant.
    Lead
      .with(l_o_l: leaf_otu_leads)
      .joins('JOIN lead_hierarchies l_h2 ON l_h2.ancestor_id = leads.id')
      .where('l_h2.descendant_id IN (SELECT id FROM l_o_l)')
      .where(parent_id: nil)
      .where(is_public: true)
  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)


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
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
# File 'app/models/lead.rb', line 51

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
  has_many :lead_items, inverse_of: :lead, dependent: :destroy
  has_many :lead_item_otus, through: :lead_items, source: :otu

  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 :text, uniqueness: { 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
  # dupe does not dupe lead_items
  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

  def add_children(project_id, user_id)
    if children.exists?
      children.create! # now multi-furcating, if it wasn't already
      return
    end

    # Create a new couplet.
    children.create!
    right_child = children.create!

    if lead_items.exists?
      Lead.transaction do
        # On a new couplet all otus start on the right hand side (the left hand
        # side typically being the shorter path through the key).
        # No ancestor leads ever retain their lead_items.
        LeadItem.move_items(self.lead_items, right_child)
      end
    end
  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.to_a
      end
    end

    Lead.transaction do
      if grandkids.present?
        lead_item_ids = children.map { |c|
          c.children.exists? ? [] : c.lead_items
        }.flatten(1).map(&:id)

        if lead_item_ids.present?
          placeholder = Lead.create!(
            text: 'PLACEHOLDER TO HOLD OTU OPTIONS FROM DELETED CHILDREN'
          )
          LeadItem.move_items(LeadItem.where(id: lead_item_ids), placeholder)
          grandkids = grandkids.prepend(placeholder)
        end
      else
        LeadItem.consolidate_descendant_items(self, self)
      end

      Lead.reparent_nodes(grandkids, self)

      children.slice(0, original_child_count).each do |c|
        c.destroy!
      end
    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
      # TODO: avoid the extra query (and hash value) when this isn't a
      # lead_items key - currently no way to know except by checking every lead.
      a[:leadItemsCount] = c.lead_items.count
      result.push(a)
    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

  def apportioned_lead_item_otus
    combined_otus =
      children.map { |c| c.lead_item_otus.includes(:taxon_name) }.flatten.uniq
    # TODO: Currently there's no (cheap) way to know if this is empty because
    # this isn't a lead_items key or because we're on a couplet that has no
    # lead_items (because, for example, it was an inserted couplet). It would be
    # useful to send an indication if we're in the latter case.
    return { parent: [], children: [] } if combined_otus.empty?

    {
      parent: combined_otus,
      children: children.map do |c|
        fixed = c.children.exists?
        {
          fixed:,
          otu_ids: fixed ? [] : c.lead_item_otus.map(&:id)
        }
      end
    }
  end

  def batch_populate_lead_items(otu_query, project_id, user_id)
    otus = ::Queries::Otu::Filter.new(otu_query).all
    LeadItem.batch_add_otus_for_lead(id, otus.map(&:id), project_id, user_id)
  end

  # Raises TaxonWorks::Error on error.
  def self.destroy_lead_and_descendants(lead, project_id, user_id)
    parent = lead.parent

    begin
      if lead.children.exists?
        items_lead = LeadItem.consolidate_descendant_items(lead)
        lead.prepend_sibling(items_lead) if items_lead.present?
      elsif lead.lead_items.exists? # lead_items with no children
        raise TaxonWorks::Error,
          'Delete not permitted; remove all checked otus first.'
      end

      lead.transaction_nuke
    rescue ActiveRecord::RecordInvalid
      raise TaxonWorks::Error,
        "Subtree destroy failed! '#{lead.errors.full_messages.join('; ')}'"
    rescue TaxonWorks::Error
      raise
    end
  end

  # @return Public root leads that have a leaf descendant with `otu` as its Otu,
  # i.e. keys that have otu on a leaf node.
  # !! Note it doesn't count if a key only has otu on an internal node;
  # currently that's only allowed in TW, not in TP.
  def self.public_root_leads_for_leaf_otus(otu)
    # Leaf leads that have otu as their Otu.
    leaf_otu_leads = Lead
      .with(l_h: LeadHierarchy.where('ancestor_id != descendant_id'))
      .merge(otu.leads)
      .joins('LEFT OUTER JOIN l_h ON leads.id = l_h.ancestor_id')
      .where(l_h: {ancestor_id: nil})

    # Root leads that are public and have one of leaf_otu_leads as their
    # descendant.
    Lead
      .with(l_o_l: leaf_otu_leads)
      .joins('JOIN lead_hierarchies l_h2 ON l_h2.ancestor_id = leads.id')
      .where('l_h2.descendant_id IN (SELECT id FROM l_o_l)')
      .where(parent_id: nil)
      .where(is_public: true)
  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)


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
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
# File 'app/models/lead.rb', line 51

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
  has_many :lead_items, inverse_of: :lead, dependent: :destroy
  has_many :lead_item_otus, through: :lead_items, source: :otu

  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 :text, uniqueness: { 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
  # dupe does not dupe lead_items
  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

  def add_children(project_id, user_id)
    if children.exists?
      children.create! # now multi-furcating, if it wasn't already
      return
    end

    # Create a new couplet.
    children.create!
    right_child = children.create!

    if lead_items.exists?
      Lead.transaction do
        # On a new couplet all otus start on the right hand side (the left hand
        # side typically being the shorter path through the key).
        # No ancestor leads ever retain their lead_items.
        LeadItem.move_items(self.lead_items, right_child)
      end
    end
  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.to_a
      end
    end

    Lead.transaction do
      if grandkids.present?
        lead_item_ids = children.map { |c|
          c.children.exists? ? [] : c.lead_items
        }.flatten(1).map(&:id)

        if lead_item_ids.present?
          placeholder = Lead.create!(
            text: 'PLACEHOLDER TO HOLD OTU OPTIONS FROM DELETED CHILDREN'
          )
          LeadItem.move_items(LeadItem.where(id: lead_item_ids), placeholder)
          grandkids = grandkids.prepend(placeholder)
        end
      else
        LeadItem.consolidate_descendant_items(self, self)
      end

      Lead.reparent_nodes(grandkids, self)

      children.slice(0, original_child_count).each do |c|
        c.destroy!
      end
    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
      # TODO: avoid the extra query (and hash value) when this isn't a
      # lead_items key - currently no way to know except by checking every lead.
      a[:leadItemsCount] = c.lead_items.count
      result.push(a)
    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

  def apportioned_lead_item_otus
    combined_otus =
      children.map { |c| c.lead_item_otus.includes(:taxon_name) }.flatten.uniq
    # TODO: Currently there's no (cheap) way to know if this is empty because
    # this isn't a lead_items key or because we're on a couplet that has no
    # lead_items (because, for example, it was an inserted couplet). It would be
    # useful to send an indication if we're in the latter case.
    return { parent: [], children: [] } if combined_otus.empty?

    {
      parent: combined_otus,
      children: children.map do |c|
        fixed = c.children.exists?
        {
          fixed:,
          otu_ids: fixed ? [] : c.lead_item_otus.map(&:id)
        }
      end
    }
  end

  def batch_populate_lead_items(otu_query, project_id, user_id)
    otus = ::Queries::Otu::Filter.new(otu_query).all
    LeadItem.batch_add_otus_for_lead(id, otus.map(&:id), project_id, user_id)
  end

  # Raises TaxonWorks::Error on error.
  def self.destroy_lead_and_descendants(lead, project_id, user_id)
    parent = lead.parent

    begin
      if lead.children.exists?
        items_lead = LeadItem.consolidate_descendant_items(lead)
        lead.prepend_sibling(items_lead) if items_lead.present?
      elsif lead.lead_items.exists? # lead_items with no children
        raise TaxonWorks::Error,
          'Delete not permitted; remove all checked otus first.'
      end

      lead.transaction_nuke
    rescue ActiveRecord::RecordInvalid
      raise TaxonWorks::Error,
        "Subtree destroy failed! '#{lead.errors.full_messages.join('; ')}'"
    rescue TaxonWorks::Error
      raise
    end
  end

  # @return Public root leads that have a leaf descendant with `otu` as its Otu,
  # i.e. keys that have otu on a leaf node.
  # !! Note it doesn't count if a key only has otu on an internal node;
  # currently that's only allowed in TW, not in TP.
  def self.public_root_leads_for_leaf_otus(otu)
    # Leaf leads that have otu as their Otu.
    leaf_otu_leads = Lead
      .with(l_h: LeadHierarchy.where('ancestor_id != descendant_id'))
      .merge(otu.leads)
      .joins('LEFT OUTER JOIN l_h ON leads.id = l_h.ancestor_id')
      .where(l_h: {ancestor_id: nil})

    # Root leads that are public and have one of leaf_otu_leads as their
    # descendant.
    Lead
      .with(l_o_l: leaf_otu_leads)
      .joins('JOIN lead_hierarchies l_h2 ON l_h2.ancestor_id = leads.id')
      .where('l_h2.descendant_id IN (SELECT id FROM l_o_l)')
      .where(parent_id: nil)
      .where(is_public: true)
  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)


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
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
# File 'app/models/lead.rb', line 51

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
  has_many :lead_items, inverse_of: :lead, dependent: :destroy
  has_many :lead_item_otus, through: :lead_items, source: :otu

  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 :text, uniqueness: { 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
  # dupe does not dupe lead_items
  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

  def add_children(project_id, user_id)
    if children.exists?
      children.create! # now multi-furcating, if it wasn't already
      return
    end

    # Create a new couplet.
    children.create!
    right_child = children.create!

    if lead_items.exists?
      Lead.transaction do
        # On a new couplet all otus start on the right hand side (the left hand
        # side typically being the shorter path through the key).
        # No ancestor leads ever retain their lead_items.
        LeadItem.move_items(self.lead_items, right_child)
      end
    end
  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.to_a
      end
    end

    Lead.transaction do
      if grandkids.present?
        lead_item_ids = children.map { |c|
          c.children.exists? ? [] : c.lead_items
        }.flatten(1).map(&:id)

        if lead_item_ids.present?
          placeholder = Lead.create!(
            text: 'PLACEHOLDER TO HOLD OTU OPTIONS FROM DELETED CHILDREN'
          )
          LeadItem.move_items(LeadItem.where(id: lead_item_ids), placeholder)
          grandkids = grandkids.prepend(placeholder)
        end
      else
        LeadItem.consolidate_descendant_items(self, self)
      end

      Lead.reparent_nodes(grandkids, self)

      children.slice(0, original_child_count).each do |c|
        c.destroy!
      end
    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
      # TODO: avoid the extra query (and hash value) when this isn't a
      # lead_items key - currently no way to know except by checking every lead.
      a[:leadItemsCount] = c.lead_items.count
      result.push(a)
    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

  def apportioned_lead_item_otus
    combined_otus =
      children.map { |c| c.lead_item_otus.includes(:taxon_name) }.flatten.uniq
    # TODO: Currently there's no (cheap) way to know if this is empty because
    # this isn't a lead_items key or because we're on a couplet that has no
    # lead_items (because, for example, it was an inserted couplet). It would be
    # useful to send an indication if we're in the latter case.
    return { parent: [], children: [] } if combined_otus.empty?

    {
      parent: combined_otus,
      children: children.map do |c|
        fixed = c.children.exists?
        {
          fixed:,
          otu_ids: fixed ? [] : c.lead_item_otus.map(&:id)
        }
      end
    }
  end

  def batch_populate_lead_items(otu_query, project_id, user_id)
    otus = ::Queries::Otu::Filter.new(otu_query).all
    LeadItem.batch_add_otus_for_lead(id, otus.map(&:id), project_id, user_id)
  end

  # Raises TaxonWorks::Error on error.
  def self.destroy_lead_and_descendants(lead, project_id, user_id)
    parent = lead.parent

    begin
      if lead.children.exists?
        items_lead = LeadItem.consolidate_descendant_items(lead)
        lead.prepend_sibling(items_lead) if items_lead.present?
      elsif lead.lead_items.exists? # lead_items with no children
        raise TaxonWorks::Error,
          'Delete not permitted; remove all checked otus first.'
      end

      lead.transaction_nuke
    rescue ActiveRecord::RecordInvalid
      raise TaxonWorks::Error,
        "Subtree destroy failed! '#{lead.errors.full_messages.join('; ')}'"
    rescue TaxonWorks::Error
      raise
    end
  end

  # @return Public root leads that have a leaf descendant with `otu` as its Otu,
  # i.e. keys that have otu on a leaf node.
  # !! Note it doesn't count if a key only has otu on an internal node;
  # currently that's only allowed in TW, not in TP.
  def self.public_root_leads_for_leaf_otus(otu)
    # Leaf leads that have otu as their Otu.
    leaf_otu_leads = Lead
      .with(l_h: LeadHierarchy.where('ancestor_id != descendant_id'))
      .merge(otu.leads)
      .joins('LEFT OUTER JOIN l_h ON leads.id = l_h.ancestor_id')
      .where(l_h: {ancestor_id: nil})

    # Root leads that are public and have one of leaf_otu_leads as their
    # descendant.
    Lead
      .with(l_o_l: leaf_otu_leads)
      .joins('JOIN lead_hierarchies l_h2 ON l_h2.ancestor_id = leads.id')
      .where('l_h2.descendant_id IN (SELECT id FROM l_o_l)')
      .where(parent_id: nil)
      .where(is_public: true)
  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)


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
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
# File 'app/models/lead.rb', line 51

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
  has_many :lead_items, inverse_of: :lead, dependent: :destroy
  has_many :lead_item_otus, through: :lead_items, source: :otu

  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 :text, uniqueness: { 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
  # dupe does not dupe lead_items
  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

  def add_children(project_id, user_id)
    if children.exists?
      children.create! # now multi-furcating, if it wasn't already
      return
    end

    # Create a new couplet.
    children.create!
    right_child = children.create!

    if lead_items.exists?
      Lead.transaction do
        # On a new couplet all otus start on the right hand side (the left hand
        # side typically being the shorter path through the key).
        # No ancestor leads ever retain their lead_items.
        LeadItem.move_items(self.lead_items, right_child)
      end
    end
  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.to_a
      end
    end

    Lead.transaction do
      if grandkids.present?
        lead_item_ids = children.map { |c|
          c.children.exists? ? [] : c.lead_items
        }.flatten(1).map(&:id)

        if lead_item_ids.present?
          placeholder = Lead.create!(
            text: 'PLACEHOLDER TO HOLD OTU OPTIONS FROM DELETED CHILDREN'
          )
          LeadItem.move_items(LeadItem.where(id: lead_item_ids), placeholder)
          grandkids = grandkids.prepend(placeholder)
        end
      else
        LeadItem.consolidate_descendant_items(self, self)
      end

      Lead.reparent_nodes(grandkids, self)

      children.slice(0, original_child_count).each do |c|
        c.destroy!
      end
    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
      # TODO: avoid the extra query (and hash value) when this isn't a
      # lead_items key - currently no way to know except by checking every lead.
      a[:leadItemsCount] = c.lead_items.count
      result.push(a)
    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

  def apportioned_lead_item_otus
    combined_otus =
      children.map { |c| c.lead_item_otus.includes(:taxon_name) }.flatten.uniq
    # TODO: Currently there's no (cheap) way to know if this is empty because
    # this isn't a lead_items key or because we're on a couplet that has no
    # lead_items (because, for example, it was an inserted couplet). It would be
    # useful to send an indication if we're in the latter case.
    return { parent: [], children: [] } if combined_otus.empty?

    {
      parent: combined_otus,
      children: children.map do |c|
        fixed = c.children.exists?
        {
          fixed:,
          otu_ids: fixed ? [] : c.lead_item_otus.map(&:id)
        }
      end
    }
  end

  def batch_populate_lead_items(otu_query, project_id, user_id)
    otus = ::Queries::Otu::Filter.new(otu_query).all
    LeadItem.batch_add_otus_for_lead(id, otus.map(&:id), project_id, user_id)
  end

  # Raises TaxonWorks::Error on error.
  def self.destroy_lead_and_descendants(lead, project_id, user_id)
    parent = lead.parent

    begin
      if lead.children.exists?
        items_lead = LeadItem.consolidate_descendant_items(lead)
        lead.prepend_sibling(items_lead) if items_lead.present?
      elsif lead.lead_items.exists? # lead_items with no children
        raise TaxonWorks::Error,
          'Delete not permitted; remove all checked otus first.'
      end

      lead.transaction_nuke
    rescue ActiveRecord::RecordInvalid
      raise TaxonWorks::Error,
        "Subtree destroy failed! '#{lead.errors.full_messages.join('; ')}'"
    rescue TaxonWorks::Error
      raise
    end
  end

  # @return Public root leads that have a leaf descendant with `otu` as its Otu,
  # i.e. keys that have otu on a leaf node.
  # !! Note it doesn't count if a key only has otu on an internal node;
  # currently that's only allowed in TW, not in TP.
  def self.public_root_leads_for_leaf_otus(otu)
    # Leaf leads that have otu as their Otu.
    leaf_otu_leads = Lead
      .with(l_h: LeadHierarchy.where('ancestor_id != descendant_id'))
      .merge(otu.leads)
      .joins('LEFT OUTER JOIN l_h ON leads.id = l_h.ancestor_id')
      .where(l_h: {ancestor_id: nil})

    # Root leads that are public and have one of leaf_otu_leads as their
    # descendant.
    Lead
      .with(l_o_l: leaf_otu_leads)
      .joins('JOIN lead_hierarchies l_h2 ON l_h2.ancestor_id = leads.id')
      .where('l_h2.descendant_id IN (SELECT id FROM l_o_l)')
      .where(parent_id: nil)
      .where(is_public: true)
  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)


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
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
# File 'app/models/lead.rb', line 51

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
  has_many :lead_items, inverse_of: :lead, dependent: :destroy
  has_many :lead_item_otus, through: :lead_items, source: :otu

  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 :text, uniqueness: { 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
  # dupe does not dupe lead_items
  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

  def add_children(project_id, user_id)
    if children.exists?
      children.create! # now multi-furcating, if it wasn't already
      return
    end

    # Create a new couplet.
    children.create!
    right_child = children.create!

    if lead_items.exists?
      Lead.transaction do
        # On a new couplet all otus start on the right hand side (the left hand
        # side typically being the shorter path through the key).
        # No ancestor leads ever retain their lead_items.
        LeadItem.move_items(self.lead_items, right_child)
      end
    end
  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.to_a
      end
    end

    Lead.transaction do
      if grandkids.present?
        lead_item_ids = children.map { |c|
          c.children.exists? ? [] : c.lead_items
        }.flatten(1).map(&:id)

        if lead_item_ids.present?
          placeholder = Lead.create!(
            text: 'PLACEHOLDER TO HOLD OTU OPTIONS FROM DELETED CHILDREN'
          )
          LeadItem.move_items(LeadItem.where(id: lead_item_ids), placeholder)
          grandkids = grandkids.prepend(placeholder)
        end
      else
        LeadItem.consolidate_descendant_items(self, self)
      end

      Lead.reparent_nodes(grandkids, self)

      children.slice(0, original_child_count).each do |c|
        c.destroy!
      end
    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
      # TODO: avoid the extra query (and hash value) when this isn't a
      # lead_items key - currently no way to know except by checking every lead.
      a[:leadItemsCount] = c.lead_items.count
      result.push(a)
    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

  def apportioned_lead_item_otus
    combined_otus =
      children.map { |c| c.lead_item_otus.includes(:taxon_name) }.flatten.uniq
    # TODO: Currently there's no (cheap) way to know if this is empty because
    # this isn't a lead_items key or because we're on a couplet that has no
    # lead_items (because, for example, it was an inserted couplet). It would be
    # useful to send an indication if we're in the latter case.
    return { parent: [], children: [] } if combined_otus.empty?

    {
      parent: combined_otus,
      children: children.map do |c|
        fixed = c.children.exists?
        {
          fixed:,
          otu_ids: fixed ? [] : c.lead_item_otus.map(&:id)
        }
      end
    }
  end

  def batch_populate_lead_items(otu_query, project_id, user_id)
    otus = ::Queries::Otu::Filter.new(otu_query).all
    LeadItem.batch_add_otus_for_lead(id, otus.map(&:id), project_id, user_id)
  end

  # Raises TaxonWorks::Error on error.
  def self.destroy_lead_and_descendants(lead, project_id, user_id)
    parent = lead.parent

    begin
      if lead.children.exists?
        items_lead = LeadItem.consolidate_descendant_items(lead)
        lead.prepend_sibling(items_lead) if items_lead.present?
      elsif lead.lead_items.exists? # lead_items with no children
        raise TaxonWorks::Error,
          'Delete not permitted; remove all checked otus first.'
      end

      lead.transaction_nuke
    rescue ActiveRecord::RecordInvalid
      raise TaxonWorks::Error,
        "Subtree destroy failed! '#{lead.errors.full_messages.join('; ')}'"
    rescue TaxonWorks::Error
      raise
    end
  end

  # @return Public root leads that have a leaf descendant with `otu` as its Otu,
  # i.e. keys that have otu on a leaf node.
  # !! Note it doesn't count if a key only has otu on an internal node;
  # currently that's only allowed in TW, not in TP.
  def self.public_root_leads_for_leaf_otus(otu)
    # Leaf leads that have otu as their Otu.
    leaf_otu_leads = Lead
      .with(l_h: LeadHierarchy.where('ancestor_id != descendant_id'))
      .merge(otu.leads)
      .joins('LEFT OUTER JOIN l_h ON leads.id = l_h.ancestor_id')
      .where(l_h: {ancestor_id: nil})

    # Root leads that are public and have one of leaf_otu_leads as their
    # descendant.
    Lead
      .with(l_o_l: leaf_otu_leads)
      .joins('JOIN lead_hierarchies l_h2 ON l_h2.ancestor_id = leads.id')
      .where('l_h2.descendant_id IN (SELECT id FROM l_o_l)')
      .where(parent_id: nil)
      .where(is_public: true)
  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

#project_idInteger

the project ID

Returns:

  • (Integer)


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
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
# File 'app/models/lead.rb', line 51

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
  has_many :lead_items, inverse_of: :lead, dependent: :destroy
  has_many :lead_item_otus, through: :lead_items, source: :otu

  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 :text, uniqueness: { 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
  # dupe does not dupe lead_items
  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

  def add_children(project_id, user_id)
    if children.exists?
      children.create! # now multi-furcating, if it wasn't already
      return
    end

    # Create a new couplet.
    children.create!
    right_child = children.create!

    if lead_items.exists?
      Lead.transaction do
        # On a new couplet all otus start on the right hand side (the left hand
        # side typically being the shorter path through the key).
        # No ancestor leads ever retain their lead_items.
        LeadItem.move_items(self.lead_items, right_child)
      end
    end
  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.to_a
      end
    end

    Lead.transaction do
      if grandkids.present?
        lead_item_ids = children.map { |c|
          c.children.exists? ? [] : c.lead_items
        }.flatten(1).map(&:id)

        if lead_item_ids.present?
          placeholder = Lead.create!(
            text: 'PLACEHOLDER TO HOLD OTU OPTIONS FROM DELETED CHILDREN'
          )
          LeadItem.move_items(LeadItem.where(id: lead_item_ids), placeholder)
          grandkids = grandkids.prepend(placeholder)
        end
      else
        LeadItem.consolidate_descendant_items(self, self)
      end

      Lead.reparent_nodes(grandkids, self)

      children.slice(0, original_child_count).each do |c|
        c.destroy!
      end
    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
      # TODO: avoid the extra query (and hash value) when this isn't a
      # lead_items key - currently no way to know except by checking every lead.
      a[:leadItemsCount] = c.lead_items.count
      result.push(a)
    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

  def apportioned_lead_item_otus
    combined_otus =
      children.map { |c| c.lead_item_otus.includes(:taxon_name) }.flatten.uniq
    # TODO: Currently there's no (cheap) way to know if this is empty because
    # this isn't a lead_items key or because we're on a couplet that has no
    # lead_items (because, for example, it was an inserted couplet). It would be
    # useful to send an indication if we're in the latter case.
    return { parent: [], children: [] } if combined_otus.empty?

    {
      parent: combined_otus,
      children: children.map do |c|
        fixed = c.children.exists?
        {
          fixed:,
          otu_ids: fixed ? [] : c.lead_item_otus.map(&:id)
        }
      end
    }
  end

  def batch_populate_lead_items(otu_query, project_id, user_id)
    otus = ::Queries::Otu::Filter.new(otu_query).all
    LeadItem.batch_add_otus_for_lead(id, otus.map(&:id), project_id, user_id)
  end

  # Raises TaxonWorks::Error on error.
  def self.destroy_lead_and_descendants(lead, project_id, user_id)
    parent = lead.parent

    begin
      if lead.children.exists?
        items_lead = LeadItem.consolidate_descendant_items(lead)
        lead.prepend_sibling(items_lead) if items_lead.present?
      elsif lead.lead_items.exists? # lead_items with no children
        raise TaxonWorks::Error,
          'Delete not permitted; remove all checked otus first.'
      end

      lead.transaction_nuke
    rescue ActiveRecord::RecordInvalid
      raise TaxonWorks::Error,
        "Subtree destroy failed! '#{lead.errors.full_messages.join('; ')}'"
    rescue TaxonWorks::Error
      raise
    end
  end

  # @return Public root leads that have a leaf descendant with `otu` as its Otu,
  # i.e. keys that have otu on a leaf node.
  # !! Note it doesn't count if a key only has otu on an internal node;
  # currently that's only allowed in TW, not in TP.
  def self.public_root_leads_for_leaf_otus(otu)
    # Leaf leads that have otu as their Otu.
    leaf_otu_leads = Lead
      .with(l_h: LeadHierarchy.where('ancestor_id != descendant_id'))
      .merge(otu.leads)
      .joins('LEFT OUTER JOIN l_h ON leads.id = l_h.ancestor_id')
      .where(l_h: {ancestor_id: nil})

    # Root leads that are public and have one of leaf_otu_leads as their
    # descendant.
    Lead
      .with(l_o_l: leaf_otu_leads)
      .joins('JOIN lead_hierarchies l_h2 ON l_h2.ancestor_id = leads.id')
      .where('l_h2.descendant_id IN (SELECT id FROM l_o_l)')
      .where(parent_id: nil)
      .where(is_public: true)
  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)


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
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
# File 'app/models/lead.rb', line 51

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
  has_many :lead_items, inverse_of: :lead, dependent: :destroy
  has_many :lead_item_otus, through: :lead_items, source: :otu

  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 :text, uniqueness: { 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
  # dupe does not dupe lead_items
  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

  def add_children(project_id, user_id)
    if children.exists?
      children.create! # now multi-furcating, if it wasn't already
      return
    end

    # Create a new couplet.
    children.create!
    right_child = children.create!

    if lead_items.exists?
      Lead.transaction do
        # On a new couplet all otus start on the right hand side (the left hand
        # side typically being the shorter path through the key).
        # No ancestor leads ever retain their lead_items.
        LeadItem.move_items(self.lead_items, right_child)
      end
    end
  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.to_a
      end
    end

    Lead.transaction do
      if grandkids.present?
        lead_item_ids = children.map { |c|
          c.children.exists? ? [] : c.lead_items
        }.flatten(1).map(&:id)

        if lead_item_ids.present?
          placeholder = Lead.create!(
            text: 'PLACEHOLDER TO HOLD OTU OPTIONS FROM DELETED CHILDREN'
          )
          LeadItem.move_items(LeadItem.where(id: lead_item_ids), placeholder)
          grandkids = grandkids.prepend(placeholder)
        end
      else
        LeadItem.consolidate_descendant_items(self, self)
      end

      Lead.reparent_nodes(grandkids, self)

      children.slice(0, original_child_count).each do |c|
        c.destroy!
      end
    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
      # TODO: avoid the extra query (and hash value) when this isn't a
      # lead_items key - currently no way to know except by checking every lead.
      a[:leadItemsCount] = c.lead_items.count
      result.push(a)
    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

  def apportioned_lead_item_otus
    combined_otus =
      children.map { |c| c.lead_item_otus.includes(:taxon_name) }.flatten.uniq
    # TODO: Currently there's no (cheap) way to know if this is empty because
    # this isn't a lead_items key or because we're on a couplet that has no
    # lead_items (because, for example, it was an inserted couplet). It would be
    # useful to send an indication if we're in the latter case.
    return { parent: [], children: [] } if combined_otus.empty?

    {
      parent: combined_otus,
      children: children.map do |c|
        fixed = c.children.exists?
        {
          fixed:,
          otu_ids: fixed ? [] : c.lead_item_otus.map(&:id)
        }
      end
    }
  end

  def batch_populate_lead_items(otu_query, project_id, user_id)
    otus = ::Queries::Otu::Filter.new(otu_query).all
    LeadItem.batch_add_otus_for_lead(id, otus.map(&:id), project_id, user_id)
  end

  # Raises TaxonWorks::Error on error.
  def self.destroy_lead_and_descendants(lead, project_id, user_id)
    parent = lead.parent

    begin
      if lead.children.exists?
        items_lead = LeadItem.consolidate_descendant_items(lead)
        lead.prepend_sibling(items_lead) if items_lead.present?
      elsif lead.lead_items.exists? # lead_items with no children
        raise TaxonWorks::Error,
          'Delete not permitted; remove all checked otus first.'
      end

      lead.transaction_nuke
    rescue ActiveRecord::RecordInvalid
      raise TaxonWorks::Error,
        "Subtree destroy failed! '#{lead.errors.full_messages.join('; ')}'"
    rescue TaxonWorks::Error
      raise
    end
  end

  # @return Public root leads that have a leaf descendant with `otu` as its Otu,
  # i.e. keys that have otu on a leaf node.
  # !! Note it doesn't count if a key only has otu on an internal node;
  # currently that's only allowed in TW, not in TP.
  def self.public_root_leads_for_leaf_otus(otu)
    # Leaf leads that have otu as their Otu.
    leaf_otu_leads = Lead
      .with(l_h: LeadHierarchy.where('ancestor_id != descendant_id'))
      .merge(otu.leads)
      .joins('LEFT OUTER JOIN l_h ON leads.id = l_h.ancestor_id')
      .where(l_h: {ancestor_id: nil})

    # Root leads that are public and have one of leaf_otu_leads as their
    # descendant.
    Lead
      .with(l_o_l: leaf_otu_leads)
      .joins('JOIN lead_hierarchies l_h2 ON l_h2.ancestor_id = leads.id')
      .where('l_h2.descendant_id IN (SELECT id FROM l_o_l)')
      .where(parent_id: nil)
      .where(is_public: true)
  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:



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
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
# File 'app/models/lead.rb', line 51

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
  has_many :lead_items, inverse_of: :lead, dependent: :destroy
  has_many :lead_item_otus, through: :lead_items, source: :otu

  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 :text, uniqueness: { 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
  # dupe does not dupe lead_items
  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

  def add_children(project_id, user_id)
    if children.exists?
      children.create! # now multi-furcating, if it wasn't already
      return
    end

    # Create a new couplet.
    children.create!
    right_child = children.create!

    if lead_items.exists?
      Lead.transaction do
        # On a new couplet all otus start on the right hand side (the left hand
        # side typically being the shorter path through the key).
        # No ancestor leads ever retain their lead_items.
        LeadItem.move_items(self.lead_items, right_child)
      end
    end
  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.to_a
      end
    end

    Lead.transaction do
      if grandkids.present?
        lead_item_ids = children.map { |c|
          c.children.exists? ? [] : c.lead_items
        }.flatten(1).map(&:id)

        if lead_item_ids.present?
          placeholder = Lead.create!(
            text: 'PLACEHOLDER TO HOLD OTU OPTIONS FROM DELETED CHILDREN'
          )
          LeadItem.move_items(LeadItem.where(id: lead_item_ids), placeholder)
          grandkids = grandkids.prepend(placeholder)
        end
      else
        LeadItem.consolidate_descendant_items(self, self)
      end

      Lead.reparent_nodes(grandkids, self)

      children.slice(0, original_child_count).each do |c|
        c.destroy!
      end
    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
      # TODO: avoid the extra query (and hash value) when this isn't a
      # lead_items key - currently no way to know except by checking every lead.
      a[:leadItemsCount] = c.lead_items.count
      result.push(a)
    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

  def apportioned_lead_item_otus
    combined_otus =
      children.map { |c| c.lead_item_otus.includes(:taxon_name) }.flatten.uniq
    # TODO: Currently there's no (cheap) way to know if this is empty because
    # this isn't a lead_items key or because we're on a couplet that has no
    # lead_items (because, for example, it was an inserted couplet). It would be
    # useful to send an indication if we're in the latter case.
    return { parent: [], children: [] } if combined_otus.empty?

    {
      parent: combined_otus,
      children: children.map do |c|
        fixed = c.children.exists?
        {
          fixed:,
          otu_ids: fixed ? [] : c.lead_item_otus.map(&:id)
        }
      end
    }
  end

  def batch_populate_lead_items(otu_query, project_id, user_id)
    otus = ::Queries::Otu::Filter.new(otu_query).all
    LeadItem.batch_add_otus_for_lead(id, otus.map(&:id), project_id, user_id)
  end

  # Raises TaxonWorks::Error on error.
  def self.destroy_lead_and_descendants(lead, project_id, user_id)
    parent = lead.parent

    begin
      if lead.children.exists?
        items_lead = LeadItem.consolidate_descendant_items(lead)
        lead.prepend_sibling(items_lead) if items_lead.present?
      elsif lead.lead_items.exists? # lead_items with no children
        raise TaxonWorks::Error,
          'Delete not permitted; remove all checked otus first.'
      end

      lead.transaction_nuke
    rescue ActiveRecord::RecordInvalid
      raise TaxonWorks::Error,
        "Subtree destroy failed! '#{lead.errors.full_messages.join('; ')}'"
    rescue TaxonWorks::Error
      raise
    end
  end

  # @return Public root leads that have a leaf descendant with `otu` as its Otu,
  # i.e. keys that have otu on a leaf node.
  # !! Note it doesn't count if a key only has otu on an internal node;
  # currently that's only allowed in TW, not in TP.
  def self.public_root_leads_for_leaf_otus(otu)
    # Leaf leads that have otu as their Otu.
    leaf_otu_leads = Lead
      .with(l_h: LeadHierarchy.where('ancestor_id != descendant_id'))
      .merge(otu.leads)
      .joins('LEFT OUTER JOIN l_h ON leads.id = l_h.ancestor_id')
      .where(l_h: {ancestor_id: nil})

    # Root leads that are public and have one of leaf_otu_leads as their
    # descendant.
    Lead
      .with(l_o_l: leaf_otu_leads)
      .joins('JOIN lead_hierarchies l_h2 ON l_h2.ancestor_id = leads.id')
      .where('l_h2.descendant_id IN (SELECT id FROM l_o_l)')
      .where(parent_id: nil)
      .where(is_public: true)
  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

.destroy_lead_and_descendants(lead, project_id, user_id) ⇒ Object

Raises TaxonWorks::Error on error.



399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
# File 'app/models/lead.rb', line 399

def self.destroy_lead_and_descendants(lead, project_id, user_id)
  parent = lead.parent

  begin
    if lead.children.exists?
      items_lead = LeadItem.consolidate_descendant_items(lead)
      lead.prepend_sibling(items_lead) if items_lead.present?
    elsif lead.lead_items.exists? # lead_items with no children
      raise TaxonWorks::Error,
        'Delete not permitted; remove all checked otus first.'
    end

    lead.transaction_nuke
  rescue ActiveRecord::RecordInvalid
    raise TaxonWorks::Error,
      "Subtree destroy failed! '#{lead.errors.full_messages.join('; ')}'"
  rescue TaxonWorks::Error
    raise
  end
end

.draw(lead, indent = 0) ⇒ Object



114
115
116
117
118
119
# File 'app/models/lead.rb', line 114

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

.public_root_leads_for_leaf_otus(otu) ⇒ Object

i.e. keys that have otu on a leaf node. !! Note it doesn’t count if a key only has otu on an internal node; currently that’s only allowed in TW, not in TP.

Returns:

  • Public root leads that have a leaf descendant with ‘otu` as its Otu,



424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
# File 'app/models/lead.rb', line 424

def self.public_root_leads_for_leaf_otus(otu)
  # Leaf leads that have otu as their Otu.
  leaf_otu_leads = Lead
    .with(l_h: LeadHierarchy.where('ancestor_id != descendant_id'))
    .merge(otu.leads)
    .joins('LEFT OUTER JOIN l_h ON leads.id = l_h.ancestor_id')
    .where(l_h: {ancestor_id: nil})

  # Root leads that are public and have one of leaf_otu_leads as their
  # descendant.
  Lead
    .with(l_o_l: leaf_otu_leads)
    .joins('JOIN lead_hierarchies l_h2 ON l_h2.ancestor_id = leads.id')
    .where('l_h2.descendant_id IN (SELECT id FROM l_o_l)')
    .where(parent_id: nil)
    .where(is_public: true)
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)



448
449
450
451
452
453
454
455
456
457
458
# File 'app/models/lead.rb', line 448

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.



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

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

#add_children(project_id, user_id) ⇒ Object



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

def add_children(project_id, user_id)
  if children.exists?
    children.create! # now multi-furcating, if it wasn't already
    return
  end

  # Create a new couplet.
  children.create!
  right_child = children.create!

  if lead_items.exists?
    Lead.transaction do
      # On a new couplet all otus start on the right hand side (the left hand
      # side typically being the shorter path through the key).
      # No ancestor leads ever retain their lead_items.
      LeadItem.move_items(self.lead_items, right_child)
    end
  end
end

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

TODO: Probably a helper method



274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'app/models/lead.rb', line 274

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
    # TODO: avoid the extra query (and hash value) when this isn't a
    # lead_items key - currently no way to know except by checking every lead.
    a[:leadItemsCount] = c.lead_items.count
    result.push(a)
  end
  result
end

#apportioned_lead_item_otusObject



372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
# File 'app/models/lead.rb', line 372

def apportioned_lead_item_otus
  combined_otus =
    children.map { |c| c.lead_item_otus.includes(:taxon_name) }.flatten.uniq
  # TODO: Currently there's no (cheap) way to know if this is empty because
  # this isn't a lead_items key or because we're on a couplet that has no
  # lead_items (because, for example, it was an inserted couplet). It would be
  # useful to send an indication if we're in the latter case.
  return { parent: [], children: [] } if combined_otus.empty?

  {
    parent: combined_otus,
    children: children.map do |c|
      fixed = c.children.exists?
      {
        fixed:,
        otu_ids: fixed ? [] : c.lead_item_otus.map(&:id)
      }
    end
  }
end

#batch_populate_lead_items(otu_query, project_id, user_id) ⇒ Object



393
394
395
396
# File 'app/models/lead.rb', line 393

def batch_populate_lead_items(otu_query, project_id, user_id)
  otus = ::Queries::Otu::Filter.new(otu_query).all
  LeadItem.batch_add_otus_for_lead(id, otus.map(&:id), project_id, user_id)
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



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

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.to_a
    end
  end

  Lead.transaction do
    if grandkids.present?
      lead_item_ids = children.map { |c|
        c.children.exists? ? [] : c.lead_items
      }.flatten(1).map(&:id)

      if lead_item_ids.present?
        placeholder = Lead.create!(
          text: 'PLACEHOLDER TO HOLD OTU OPTIONS FROM DELETED CHILDREN'
        )
        LeadItem.move_items(LeadItem.where(id: lead_item_ids), placeholder)
        grandkids = grandkids.prepend(placeholder)
      end
    else
      LeadItem.consolidate_descendant_items(self, self)
    end

    Lead.reparent_nodes(grandkids, self)

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

  true
end

#dupeBoolean

dupe does not dupe lead_items

Returns:

  • (Boolean)

    true on success, false on error



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'app/models/lead.rb', line 87

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)



493
494
495
496
497
498
499
500
# File 'app/models/lead.rb', line 493

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



81
82
83
# File 'app/models/lead.rb', line 81

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

#insert_coupletObject



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

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



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

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


473
474
475
# File 'app/models/lead.rb', line 473

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)



481
482
483
# File 'app/models/lead.rb', line 481

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



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

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



266
267
268
269
270
271
# File 'app/models/lead.rb', line 266

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

#redirect_isnt_ancestor_or_selfObject (private)



485
486
487
# File 'app/models/lead.rb', line 485

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)



477
478
479
# File 'app/models/lead.rb', line 477

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



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'app/models/lead.rb', line 354

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.



292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'app/models/lead.rb', line 292

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)



489
490
491
# File 'app/models/lead.rb', line 489

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)



469
470
471
# File 'app/models/lead.rb', line 469

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)



502
503
504
505
506
507
508
# File 'app/models/lead.rb', line 502

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



260
261
262
263
264
# File 'app/models/lead.rb', line 260

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.



461
462
463
464
465
466
467
# File 'app/models/lead.rb', line 461

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