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 models dichotomous keys; each row represents a single couplet option in a key.

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:



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
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
# File 'app/models/lead.rb', line 44

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

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

  belongs_to :parent, class_name: 'Lead'

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

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

  before_save :check_is_public

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

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

  def go_id
    (redirect_id.presence || id)
  end

  def dupe
    return false if parent_id
    begin
      Lead.transaction do
        dupe_in_transaction(self, parent_id)
      end
    rescue ActiveRecord::RecordInvalid
      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.reload.order(:position).each do |c|
      draw(c, indent + 1)
    end
    nil
  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

      left_text = (p == :left || p == :root) ? t2 : t1
      right_text = (p == :right) ? t2 : t1
      # !! middle handling

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

      new_parent = (p == :left || p == :root) ? c : d
      last_sibling = nil

      # !! The more obvious version using add_child is actually more error
      # prone than using add_sibling.
      o.each_with_index do |c, i|
        if i == 0
          new_parent.add_child c
        else
          last_sibling.append_sibling c
        end
        last_sibling = c
      end
    else
      c = Lead.create!(text: t1, parent: self)
      d = Lead.create!(text: t1, parent: self)
    end

    [c.id, d.id]
  end

  # Destroy the children of this Lead, re-appending the grand-children to self
  #  !! Do not destroy couplet if children on > 1 side exist
  def destroy_couplet
    k = children.order(:position).reload.to_a
    return true if k.empty?

    # TODO: handle multiple facets
    # not first/last because that gives us a==b scenarious
    a = k[0]
    b = k[1]

    c = a.children.to_a
    d = b&.children&.to_a || []

    if (c.size == 0) or (d.size == 0) # At least one side, but not two have children
      if (c.size > 0) || (d.size > 0) # One side has children

        has_kids = c.size > 0 ? a : b
        no_kids = c.size == 0 ? a : b

        i = has_kids.children

        # Reparent the children of has_kids to self.
        # !! The obvious solution using add_child is actually more error-prone
        # than using add_sibling.
        last_sibling = children[-1]
        i.each do |z|
          last_sibling.append_sibling z
          last_sibling = z
        end

        has_kids.destroy!
        no_kids.destroy!
      else # Neither side has children
        Lead.transaction do
          a.destroy!
          b&.destroy!
        end
      end
      true
    else #
      false
    end
  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.second, node.children.first].compact # intentionally reversed
      c.all_children(c, result, depth + 1)
      a = {}
      a[:depth] = depth
      a[:cpl] = c
      a[:cplLabel] = node.origin_label
      result.push(a)
    end
    result
  end

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

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

  # @return [ActiveRecord::Relation] ordered by text.
  # Returns all root nodes, with new properties:
  #  * couplet_count (number of couplets in the key)
  #  * 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).
    # TODO: couplet_count will be wrong if any couplets don't have exactly two
    # children.
    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,
        (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

  protected

  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!

    for c in node.children
      dupe_in_transaction(c, a.id)
    end
  end

  def check_is_public
    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)


44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
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
# File 'app/models/lead.rb', line 44

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

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

  belongs_to :parent, class_name: 'Lead'

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

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

  before_save :check_is_public

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

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

  def go_id
    (redirect_id.presence || id)
  end

  def dupe
    return false if parent_id
    begin
      Lead.transaction do
        dupe_in_transaction(self, parent_id)
      end
    rescue ActiveRecord::RecordInvalid
      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.reload.order(:position).each do |c|
      draw(c, indent + 1)
    end
    nil
  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

      left_text = (p == :left || p == :root) ? t2 : t1
      right_text = (p == :right) ? t2 : t1
      # !! middle handling

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

      new_parent = (p == :left || p == :root) ? c : d
      last_sibling = nil

      # !! The more obvious version using add_child is actually more error
      # prone than using add_sibling.
      o.each_with_index do |c, i|
        if i == 0
          new_parent.add_child c
        else
          last_sibling.append_sibling c
        end
        last_sibling = c
      end
    else
      c = Lead.create!(text: t1, parent: self)
      d = Lead.create!(text: t1, parent: self)
    end

    [c.id, d.id]
  end

  # Destroy the children of this Lead, re-appending the grand-children to self
  #  !! Do not destroy couplet if children on > 1 side exist
  def destroy_couplet
    k = children.order(:position).reload.to_a
    return true if k.empty?

    # TODO: handle multiple facets
    # not first/last because that gives us a==b scenarious
    a = k[0]
    b = k[1]

    c = a.children.to_a
    d = b&.children&.to_a || []

    if (c.size == 0) or (d.size == 0) # At least one side, but not two have children
      if (c.size > 0) || (d.size > 0) # One side has children

        has_kids = c.size > 0 ? a : b
        no_kids = c.size == 0 ? a : b

        i = has_kids.children

        # Reparent the children of has_kids to self.
        # !! The obvious solution using add_child is actually more error-prone
        # than using add_sibling.
        last_sibling = children[-1]
        i.each do |z|
          last_sibling.append_sibling z
          last_sibling = z
        end

        has_kids.destroy!
        no_kids.destroy!
      else # Neither side has children
        Lead.transaction do
          a.destroy!
          b&.destroy!
        end
      end
      true
    else #
      false
    end
  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.second, node.children.first].compact # intentionally reversed
      c.all_children(c, result, depth + 1)
      a = {}
      a[:depth] = depth
      a[:cpl] = c
      a[:cplLabel] = node.origin_label
      result.push(a)
    end
    result
  end

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

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

  # @return [ActiveRecord::Relation] ordered by text.
  # Returns all root nodes, with new properties:
  #  * couplet_count (number of couplets in the key)
  #  * 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).
    # TODO: couplet_count will be wrong if any couplets don't have exactly two
    # children.
    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,
        (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

  protected

  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!

    for c in node.children
      dupe_in_transaction(c, a.id)
    end
  end

  def check_is_public
    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:



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
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
# File 'app/models/lead.rb', line 44

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

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

  belongs_to :parent, class_name: 'Lead'

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

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

  before_save :check_is_public

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

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

  def go_id
    (redirect_id.presence || id)
  end

  def dupe
    return false if parent_id
    begin
      Lead.transaction do
        dupe_in_transaction(self, parent_id)
      end
    rescue ActiveRecord::RecordInvalid
      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.reload.order(:position).each do |c|
      draw(c, indent + 1)
    end
    nil
  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

      left_text = (p == :left || p == :root) ? t2 : t1
      right_text = (p == :right) ? t2 : t1
      # !! middle handling

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

      new_parent = (p == :left || p == :root) ? c : d
      last_sibling = nil

      # !! The more obvious version using add_child is actually more error
      # prone than using add_sibling.
      o.each_with_index do |c, i|
        if i == 0
          new_parent.add_child c
        else
          last_sibling.append_sibling c
        end
        last_sibling = c
      end
    else
      c = Lead.create!(text: t1, parent: self)
      d = Lead.create!(text: t1, parent: self)
    end

    [c.id, d.id]
  end

  # Destroy the children of this Lead, re-appending the grand-children to self
  #  !! Do not destroy couplet if children on > 1 side exist
  def destroy_couplet
    k = children.order(:position).reload.to_a
    return true if k.empty?

    # TODO: handle multiple facets
    # not first/last because that gives us a==b scenarious
    a = k[0]
    b = k[1]

    c = a.children.to_a
    d = b&.children&.to_a || []

    if (c.size == 0) or (d.size == 0) # At least one side, but not two have children
      if (c.size > 0) || (d.size > 0) # One side has children

        has_kids = c.size > 0 ? a : b
        no_kids = c.size == 0 ? a : b

        i = has_kids.children

        # Reparent the children of has_kids to self.
        # !! The obvious solution using add_child is actually more error-prone
        # than using add_sibling.
        last_sibling = children[-1]
        i.each do |z|
          last_sibling.append_sibling z
          last_sibling = z
        end

        has_kids.destroy!
        no_kids.destroy!
      else # Neither side has children
        Lead.transaction do
          a.destroy!
          b&.destroy!
        end
      end
      true
    else #
      false
    end
  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.second, node.children.first].compact # intentionally reversed
      c.all_children(c, result, depth + 1)
      a = {}
      a[:depth] = depth
      a[:cpl] = c
      a[:cplLabel] = node.origin_label
      result.push(a)
    end
    result
  end

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

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

  # @return [ActiveRecord::Relation] ordered by text.
  # Returns all root nodes, with new properties:
  #  * couplet_count (number of couplets in the key)
  #  * 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).
    # TODO: couplet_count will be wrong if any couplets don't have exactly two
    # children.
    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,
        (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

  protected

  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!

    for c in node.children
      dupe_in_transaction(c, a.id)
    end
  end

  def check_is_public
    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)


44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
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
# File 'app/models/lead.rb', line 44

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

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

  belongs_to :parent, class_name: 'Lead'

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

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

  before_save :check_is_public

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

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

  def go_id
    (redirect_id.presence || id)
  end

  def dupe
    return false if parent_id
    begin
      Lead.transaction do
        dupe_in_transaction(self, parent_id)
      end
    rescue ActiveRecord::RecordInvalid
      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.reload.order(:position).each do |c|
      draw(c, indent + 1)
    end
    nil
  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

      left_text = (p == :left || p == :root) ? t2 : t1
      right_text = (p == :right) ? t2 : t1
      # !! middle handling

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

      new_parent = (p == :left || p == :root) ? c : d
      last_sibling = nil

      # !! The more obvious version using add_child is actually more error
      # prone than using add_sibling.
      o.each_with_index do |c, i|
        if i == 0
          new_parent.add_child c
        else
          last_sibling.append_sibling c
        end
        last_sibling = c
      end
    else
      c = Lead.create!(text: t1, parent: self)
      d = Lead.create!(text: t1, parent: self)
    end

    [c.id, d.id]
  end

  # Destroy the children of this Lead, re-appending the grand-children to self
  #  !! Do not destroy couplet if children on > 1 side exist
  def destroy_couplet
    k = children.order(:position).reload.to_a
    return true if k.empty?

    # TODO: handle multiple facets
    # not first/last because that gives us a==b scenarious
    a = k[0]
    b = k[1]

    c = a.children.to_a
    d = b&.children&.to_a || []

    if (c.size == 0) or (d.size == 0) # At least one side, but not two have children
      if (c.size > 0) || (d.size > 0) # One side has children

        has_kids = c.size > 0 ? a : b
        no_kids = c.size == 0 ? a : b

        i = has_kids.children

        # Reparent the children of has_kids to self.
        # !! The obvious solution using add_child is actually more error-prone
        # than using add_sibling.
        last_sibling = children[-1]
        i.each do |z|
          last_sibling.append_sibling z
          last_sibling = z
        end

        has_kids.destroy!
        no_kids.destroy!
      else # Neither side has children
        Lead.transaction do
          a.destroy!
          b&.destroy!
        end
      end
      true
    else #
      false
    end
  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.second, node.children.first].compact # intentionally reversed
      c.all_children(c, result, depth + 1)
      a = {}
      a[:depth] = depth
      a[:cpl] = c
      a[:cplLabel] = node.origin_label
      result.push(a)
    end
    result
  end

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

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

  # @return [ActiveRecord::Relation] ordered by text.
  # Returns all root nodes, with new properties:
  #  * couplet_count (number of couplets in the key)
  #  * 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).
    # TODO: couplet_count will be wrong if any couplets don't have exactly two
    # children.
    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,
        (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

  protected

  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!

    for c in node.children
      dupe_in_transaction(c, a.id)
    end
  end

  def check_is_public
    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)


44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
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
# File 'app/models/lead.rb', line 44

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

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

  belongs_to :parent, class_name: 'Lead'

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

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

  before_save :check_is_public

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

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

  def go_id
    (redirect_id.presence || id)
  end

  def dupe
    return false if parent_id
    begin
      Lead.transaction do
        dupe_in_transaction(self, parent_id)
      end
    rescue ActiveRecord::RecordInvalid
      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.reload.order(:position).each do |c|
      draw(c, indent + 1)
    end
    nil
  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

      left_text = (p == :left || p == :root) ? t2 : t1
      right_text = (p == :right) ? t2 : t1
      # !! middle handling

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

      new_parent = (p == :left || p == :root) ? c : d
      last_sibling = nil

      # !! The more obvious version using add_child is actually more error
      # prone than using add_sibling.
      o.each_with_index do |c, i|
        if i == 0
          new_parent.add_child c
        else
          last_sibling.append_sibling c
        end
        last_sibling = c
      end
    else
      c = Lead.create!(text: t1, parent: self)
      d = Lead.create!(text: t1, parent: self)
    end

    [c.id, d.id]
  end

  # Destroy the children of this Lead, re-appending the grand-children to self
  #  !! Do not destroy couplet if children on > 1 side exist
  def destroy_couplet
    k = children.order(:position).reload.to_a
    return true if k.empty?

    # TODO: handle multiple facets
    # not first/last because that gives us a==b scenarious
    a = k[0]
    b = k[1]

    c = a.children.to_a
    d = b&.children&.to_a || []

    if (c.size == 0) or (d.size == 0) # At least one side, but not two have children
      if (c.size > 0) || (d.size > 0) # One side has children

        has_kids = c.size > 0 ? a : b
        no_kids = c.size == 0 ? a : b

        i = has_kids.children

        # Reparent the children of has_kids to self.
        # !! The obvious solution using add_child is actually more error-prone
        # than using add_sibling.
        last_sibling = children[-1]
        i.each do |z|
          last_sibling.append_sibling z
          last_sibling = z
        end

        has_kids.destroy!
        no_kids.destroy!
      else # Neither side has children
        Lead.transaction do
          a.destroy!
          b&.destroy!
        end
      end
      true
    else #
      false
    end
  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.second, node.children.first].compact # intentionally reversed
      c.all_children(c, result, depth + 1)
      a = {}
      a[:depth] = depth
      a[:cpl] = c
      a[:cplLabel] = node.origin_label
      result.push(a)
    end
    result
  end

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

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

  # @return [ActiveRecord::Relation] ordered by text.
  # Returns all root nodes, with new properties:
  #  * couplet_count (number of couplets in the key)
  #  * 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).
    # TODO: couplet_count will be wrong if any couplets don't have exactly two
    # children.
    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,
        (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

  protected

  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!

    for c in node.children
      dupe_in_transaction(c, a.id)
    end
  end

  def check_is_public
    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)


44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
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
# File 'app/models/lead.rb', line 44

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

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

  belongs_to :parent, class_name: 'Lead'

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

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

  before_save :check_is_public

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

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

  def go_id
    (redirect_id.presence || id)
  end

  def dupe
    return false if parent_id
    begin
      Lead.transaction do
        dupe_in_transaction(self, parent_id)
      end
    rescue ActiveRecord::RecordInvalid
      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.reload.order(:position).each do |c|
      draw(c, indent + 1)
    end
    nil
  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

      left_text = (p == :left || p == :root) ? t2 : t1
      right_text = (p == :right) ? t2 : t1
      # !! middle handling

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

      new_parent = (p == :left || p == :root) ? c : d
      last_sibling = nil

      # !! The more obvious version using add_child is actually more error
      # prone than using add_sibling.
      o.each_with_index do |c, i|
        if i == 0
          new_parent.add_child c
        else
          last_sibling.append_sibling c
        end
        last_sibling = c
      end
    else
      c = Lead.create!(text: t1, parent: self)
      d = Lead.create!(text: t1, parent: self)
    end

    [c.id, d.id]
  end

  # Destroy the children of this Lead, re-appending the grand-children to self
  #  !! Do not destroy couplet if children on > 1 side exist
  def destroy_couplet
    k = children.order(:position).reload.to_a
    return true if k.empty?

    # TODO: handle multiple facets
    # not first/last because that gives us a==b scenarious
    a = k[0]
    b = k[1]

    c = a.children.to_a
    d = b&.children&.to_a || []

    if (c.size == 0) or (d.size == 0) # At least one side, but not two have children
      if (c.size > 0) || (d.size > 0) # One side has children

        has_kids = c.size > 0 ? a : b
        no_kids = c.size == 0 ? a : b

        i = has_kids.children

        # Reparent the children of has_kids to self.
        # !! The obvious solution using add_child is actually more error-prone
        # than using add_sibling.
        last_sibling = children[-1]
        i.each do |z|
          last_sibling.append_sibling z
          last_sibling = z
        end

        has_kids.destroy!
        no_kids.destroy!
      else # Neither side has children
        Lead.transaction do
          a.destroy!
          b&.destroy!
        end
      end
      true
    else #
      false
    end
  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.second, node.children.first].compact # intentionally reversed
      c.all_children(c, result, depth + 1)
      a = {}
      a[:depth] = depth
      a[:cpl] = c
      a[:cplLabel] = node.origin_label
      result.push(a)
    end
    result
  end

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

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

  # @return [ActiveRecord::Relation] ordered by text.
  # Returns all root nodes, with new properties:
  #  * couplet_count (number of couplets in the key)
  #  * 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).
    # TODO: couplet_count will be wrong if any couplets don't have exactly two
    # children.
    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,
        (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

  protected

  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!

    for c in node.children
      dupe_in_transaction(c, a.id)
    end
  end

  def check_is_public
    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)


44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
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
# File 'app/models/lead.rb', line 44

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

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

  belongs_to :parent, class_name: 'Lead'

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

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

  before_save :check_is_public

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

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

  def go_id
    (redirect_id.presence || id)
  end

  def dupe
    return false if parent_id
    begin
      Lead.transaction do
        dupe_in_transaction(self, parent_id)
      end
    rescue ActiveRecord::RecordInvalid
      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.reload.order(:position).each do |c|
      draw(c, indent + 1)
    end
    nil
  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

      left_text = (p == :left || p == :root) ? t2 : t1
      right_text = (p == :right) ? t2 : t1
      # !! middle handling

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

      new_parent = (p == :left || p == :root) ? c : d
      last_sibling = nil

      # !! The more obvious version using add_child is actually more error
      # prone than using add_sibling.
      o.each_with_index do |c, i|
        if i == 0
          new_parent.add_child c
        else
          last_sibling.append_sibling c
        end
        last_sibling = c
      end
    else
      c = Lead.create!(text: t1, parent: self)
      d = Lead.create!(text: t1, parent: self)
    end

    [c.id, d.id]
  end

  # Destroy the children of this Lead, re-appending the grand-children to self
  #  !! Do not destroy couplet if children on > 1 side exist
  def destroy_couplet
    k = children.order(:position).reload.to_a
    return true if k.empty?

    # TODO: handle multiple facets
    # not first/last because that gives us a==b scenarious
    a = k[0]
    b = k[1]

    c = a.children.to_a
    d = b&.children&.to_a || []

    if (c.size == 0) or (d.size == 0) # At least one side, but not two have children
      if (c.size > 0) || (d.size > 0) # One side has children

        has_kids = c.size > 0 ? a : b
        no_kids = c.size == 0 ? a : b

        i = has_kids.children

        # Reparent the children of has_kids to self.
        # !! The obvious solution using add_child is actually more error-prone
        # than using add_sibling.
        last_sibling = children[-1]
        i.each do |z|
          last_sibling.append_sibling z
          last_sibling = z
        end

        has_kids.destroy!
        no_kids.destroy!
      else # Neither side has children
        Lead.transaction do
          a.destroy!
          b&.destroy!
        end
      end
      true
    else #
      false
    end
  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.second, node.children.first].compact # intentionally reversed
      c.all_children(c, result, depth + 1)
      a = {}
      a[:depth] = depth
      a[:cpl] = c
      a[:cplLabel] = node.origin_label
      result.push(a)
    end
    result
  end

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

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

  # @return [ActiveRecord::Relation] ordered by text.
  # Returns all root nodes, with new properties:
  #  * couplet_count (number of couplets in the key)
  #  * 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).
    # TODO: couplet_count will be wrong if any couplets don't have exactly two
    # children.
    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,
        (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

  protected

  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!

    for c in node.children
      dupe_in_transaction(c, a.id)
    end
  end

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

end

#positioninteger

A sort order used by has_closure_tree

Returns:

  • (integer)


44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
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
# File 'app/models/lead.rb', line 44

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

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

  belongs_to :parent, class_name: 'Lead'

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

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

  before_save :check_is_public

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

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

  def go_id
    (redirect_id.presence || id)
  end

  def dupe
    return false if parent_id
    begin
      Lead.transaction do
        dupe_in_transaction(self, parent_id)
      end
    rescue ActiveRecord::RecordInvalid
      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.reload.order(:position).each do |c|
      draw(c, indent + 1)
    end
    nil
  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

      left_text = (p == :left || p == :root) ? t2 : t1
      right_text = (p == :right) ? t2 : t1
      # !! middle handling

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

      new_parent = (p == :left || p == :root) ? c : d
      last_sibling = nil

      # !! The more obvious version using add_child is actually more error
      # prone than using add_sibling.
      o.each_with_index do |c, i|
        if i == 0
          new_parent.add_child c
        else
          last_sibling.append_sibling c
        end
        last_sibling = c
      end
    else
      c = Lead.create!(text: t1, parent: self)
      d = Lead.create!(text: t1, parent: self)
    end

    [c.id, d.id]
  end

  # Destroy the children of this Lead, re-appending the grand-children to self
  #  !! Do not destroy couplet if children on > 1 side exist
  def destroy_couplet
    k = children.order(:position).reload.to_a
    return true if k.empty?

    # TODO: handle multiple facets
    # not first/last because that gives us a==b scenarious
    a = k[0]
    b = k[1]

    c = a.children.to_a
    d = b&.children&.to_a || []

    if (c.size == 0) or (d.size == 0) # At least one side, but not two have children
      if (c.size > 0) || (d.size > 0) # One side has children

        has_kids = c.size > 0 ? a : b
        no_kids = c.size == 0 ? a : b

        i = has_kids.children

        # Reparent the children of has_kids to self.
        # !! The obvious solution using add_child is actually more error-prone
        # than using add_sibling.
        last_sibling = children[-1]
        i.each do |z|
          last_sibling.append_sibling z
          last_sibling = z
        end

        has_kids.destroy!
        no_kids.destroy!
      else # Neither side has children
        Lead.transaction do
          a.destroy!
          b&.destroy!
        end
      end
      true
    else #
      false
    end
  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.second, node.children.first].compact # intentionally reversed
      c.all_children(c, result, depth + 1)
      a = {}
      a[:depth] = depth
      a[:cpl] = c
      a[:cplLabel] = node.origin_label
      result.push(a)
    end
    result
  end

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

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

  # @return [ActiveRecord::Relation] ordered by text.
  # Returns all root nodes, with new properties:
  #  * couplet_count (number of couplets in the key)
  #  * 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).
    # TODO: couplet_count will be wrong if any couplets don't have exactly two
    # children.
    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,
        (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

  protected

  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!

    for c in node.children
      dupe_in_transaction(c, a.id)
    end
  end

  def check_is_public
    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)


44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
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
# File 'app/models/lead.rb', line 44

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

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

  belongs_to :parent, class_name: 'Lead'

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

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

  before_save :check_is_public

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

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

  def go_id
    (redirect_id.presence || id)
  end

  def dupe
    return false if parent_id
    begin
      Lead.transaction do
        dupe_in_transaction(self, parent_id)
      end
    rescue ActiveRecord::RecordInvalid
      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.reload.order(:position).each do |c|
      draw(c, indent + 1)
    end
    nil
  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

      left_text = (p == :left || p == :root) ? t2 : t1
      right_text = (p == :right) ? t2 : t1
      # !! middle handling

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

      new_parent = (p == :left || p == :root) ? c : d
      last_sibling = nil

      # !! The more obvious version using add_child is actually more error
      # prone than using add_sibling.
      o.each_with_index do |c, i|
        if i == 0
          new_parent.add_child c
        else
          last_sibling.append_sibling c
        end
        last_sibling = c
      end
    else
      c = Lead.create!(text: t1, parent: self)
      d = Lead.create!(text: t1, parent: self)
    end

    [c.id, d.id]
  end

  # Destroy the children of this Lead, re-appending the grand-children to self
  #  !! Do not destroy couplet if children on > 1 side exist
  def destroy_couplet
    k = children.order(:position).reload.to_a
    return true if k.empty?

    # TODO: handle multiple facets
    # not first/last because that gives us a==b scenarious
    a = k[0]
    b = k[1]

    c = a.children.to_a
    d = b&.children&.to_a || []

    if (c.size == 0) or (d.size == 0) # At least one side, but not two have children
      if (c.size > 0) || (d.size > 0) # One side has children

        has_kids = c.size > 0 ? a : b
        no_kids = c.size == 0 ? a : b

        i = has_kids.children

        # Reparent the children of has_kids to self.
        # !! The obvious solution using add_child is actually more error-prone
        # than using add_sibling.
        last_sibling = children[-1]
        i.each do |z|
          last_sibling.append_sibling z
          last_sibling = z
        end

        has_kids.destroy!
        no_kids.destroy!
      else # Neither side has children
        Lead.transaction do
          a.destroy!
          b&.destroy!
        end
      end
      true
    else #
      false
    end
  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.second, node.children.first].compact # intentionally reversed
      c.all_children(c, result, depth + 1)
      a = {}
      a[:depth] = depth
      a[:cpl] = c
      a[:cplLabel] = node.origin_label
      result.push(a)
    end
    result
  end

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

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

  # @return [ActiveRecord::Relation] ordered by text.
  # Returns all root nodes, with new properties:
  #  * couplet_count (number of couplets in the key)
  #  * 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).
    # TODO: couplet_count will be wrong if any couplets don't have exactly two
    # children.
    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,
        (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

  protected

  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!

    for c in node.children
      dupe_in_transaction(c, a.id)
    end
  end

  def check_is_public
    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:



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
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
# File 'app/models/lead.rb', line 44

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

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

  belongs_to :parent, class_name: 'Lead'

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

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

  before_save :check_is_public

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

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

  def go_id
    (redirect_id.presence || id)
  end

  def dupe
    return false if parent_id
    begin
      Lead.transaction do
        dupe_in_transaction(self, parent_id)
      end
    rescue ActiveRecord::RecordInvalid
      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.reload.order(:position).each do |c|
      draw(c, indent + 1)
    end
    nil
  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

      left_text = (p == :left || p == :root) ? t2 : t1
      right_text = (p == :right) ? t2 : t1
      # !! middle handling

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

      new_parent = (p == :left || p == :root) ? c : d
      last_sibling = nil

      # !! The more obvious version using add_child is actually more error
      # prone than using add_sibling.
      o.each_with_index do |c, i|
        if i == 0
          new_parent.add_child c
        else
          last_sibling.append_sibling c
        end
        last_sibling = c
      end
    else
      c = Lead.create!(text: t1, parent: self)
      d = Lead.create!(text: t1, parent: self)
    end

    [c.id, d.id]
  end

  # Destroy the children of this Lead, re-appending the grand-children to self
  #  !! Do not destroy couplet if children on > 1 side exist
  def destroy_couplet
    k = children.order(:position).reload.to_a
    return true if k.empty?

    # TODO: handle multiple facets
    # not first/last because that gives us a==b scenarious
    a = k[0]
    b = k[1]

    c = a.children.to_a
    d = b&.children&.to_a || []

    if (c.size == 0) or (d.size == 0) # At least one side, but not two have children
      if (c.size > 0) || (d.size > 0) # One side has children

        has_kids = c.size > 0 ? a : b
        no_kids = c.size == 0 ? a : b

        i = has_kids.children

        # Reparent the children of has_kids to self.
        # !! The obvious solution using add_child is actually more error-prone
        # than using add_sibling.
        last_sibling = children[-1]
        i.each do |z|
          last_sibling.append_sibling z
          last_sibling = z
        end

        has_kids.destroy!
        no_kids.destroy!
      else # Neither side has children
        Lead.transaction do
          a.destroy!
          b&.destroy!
        end
      end
      true
    else #
      false
    end
  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.second, node.children.first].compact # intentionally reversed
      c.all_children(c, result, depth + 1)
      a = {}
      a[:depth] = depth
      a[:cpl] = c
      a[:cplLabel] = node.origin_label
      result.push(a)
    end
    result
  end

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

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

  # @return [ActiveRecord::Relation] ordered by text.
  # Returns all root nodes, with new properties:
  #  * couplet_count (number of couplets in the key)
  #  * 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).
    # TODO: couplet_count will be wrong if any couplets don't have exactly two
    # children.
    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,
        (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

  protected

  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!

    for c in node.children
      dupe_in_transaction(c, a.id)
    end
  end

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

end

Class Method Details

.draw(lead, indent = 0) ⇒ Object



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

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.reload.order(:position).each do |c|
    draw(c, indent + 1)
  end
  nil
end

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

Returns all root nodes, with new properties:

* couplet_count (number of couplets in the key)
* 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.



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

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).
  # TODO: couplet_count will be wrong if any couplets don't have exactly two
  # children.
  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,
      (COUNT(otus_source.id) - 1) / 2 AS couplet_count
    ')

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

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

Instance Method Details

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

TODO: Probably a helper method



206
207
208
209
210
211
212
213
214
215
216
# File 'app/models/lead.rb', line 206

def all_children(node = self, result = [], depth = 0)
  for c in [node.children.second, node.children.first].compact # intentionally reversed
    c.all_children(c, result, depth + 1)
    a = {}
    a[:depth] = depth
    a[:cpl] = c
    a[:cplLabel] = node.origin_label
    result.push(a)
  end
  result
end

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

TODO: Probably a helper method



219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'app/models/lead.rb', line 219

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

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

#check_is_publicObject (protected)



320
321
322
323
324
325
326
# File 'app/models/lead.rb', line 320

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

#destroy_coupletObject

Destroy the children of this Lead, re-appending the grand-children to self

!! Do not destroy couplet if children on > 1 side exist


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

def destroy_couplet
  k = children.order(:position).reload.to_a
  return true if k.empty?

  # TODO: handle multiple facets
  # not first/last because that gives us a==b scenarious
  a = k[0]
  b = k[1]

  c = a.children.to_a
  d = b&.children&.to_a || []

  if (c.size == 0) or (d.size == 0) # At least one side, but not two have children
    if (c.size > 0) || (d.size > 0) # One side has children

      has_kids = c.size > 0 ? a : b
      no_kids = c.size == 0 ? a : b

      i = has_kids.children

      # Reparent the children of has_kids to self.
      # !! The obvious solution using add_child is actually more error-prone
      # than using add_sibling.
      last_sibling = children[-1]
      i.each do |z|
        last_sibling.append_sibling z
        last_sibling = z
      end

      has_kids.destroy!
      no_kids.destroy!
    else # Neither side has children
      Lead.transaction do
        a.destroy!
        b&.destroy!
      end
    end
    true
  else #
    false
  end
end

#dupeObject



79
80
81
82
83
84
85
86
87
88
89
# File 'app/models/lead.rb', line 79

def dupe
  return false if parent_id
  begin
    Lead.transaction do
      dupe_in_transaction(self, parent_id)
    end
  rescue ActiveRecord::RecordInvalid
    return false
  end
  true
end

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



309
310
311
312
313
314
315
316
317
318
# File 'app/models/lead.rb', line 309

def dupe_in_transaction(node, parentId)
  a = node.dup
  a.parent_id = parentId
  a.text = '(COPY OF) ' + a.text if parentId == nil
  a.save!

  for c in node.children
    dupe_in_transaction(c, a.id)
  end
end

#futureObject



71
72
73
# File 'app/models/lead.rb', line 71

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

#go_idObject



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

def go_id
  (redirect_id.presence || id)
end

#insert_coupletObject



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

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

    left_text = (p == :left || p == :root) ? t2 : t1
    right_text = (p == :right) ? t2 : t1
    # !! middle handling

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

    new_parent = (p == :left || p == :root) ? c : d
    last_sibling = nil

    # !! The more obvious version using add_child is actually more error
    # prone than using add_sibling.
    o.each_with_index do |c, i|
      if i == 0
        new_parent.add_child c
      else
        last_sibling.append_sibling c
      end
      last_sibling = c
    end
  else
    c = Lead.create!(text: t1, parent: self)
    d = Lead.create!(text: t1, parent: self)
  end

  [c.id, d.id]
end


289
290
291
# File 'app/models/lead.rb', line 289

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 (protected)



297
298
299
# File 'app/models/lead.rb', line 297

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



92
93
94
95
96
97
98
# File 'app/models/lead.rb', line 92

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



198
199
200
201
202
203
# File 'app/models/lead.rb', line 198

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

#redirect_isnt_ancestor_or_selfObject (protected)



301
302
303
# File 'app/models/lead.rb', line 301

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 (protected)



293
294
295
# File 'app/models/lead.rb', line 293

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

#root_has_no_redirectObject (protected)



305
306
307
# File 'app/models/lead.rb', line 305

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

#root_has_titleObject (protected)



285
286
287
# File 'app/models/lead.rb', line 285

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

#transaction_nuke(lead = self) ⇒ Object



192
193
194
195
196
# File 'app/models/lead.rb', line 192

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