Module: Trees

Included in:
Classification, DigitalObject, Resource
Defined in:
backend/app/model/mixins/trees.rb

Defined Under Namespace

Modules: ClassMethods

Constant Summary collapse

NODE_PAGE_SIZE =
2000

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(base) ⇒ Object



5
6
7
# File 'backend/app/model/mixins/trees.rb', line 5

def self.included(base)
  base.extend(ClassMethods)
end

Instance Method Details

#adopt_children(old_parent) ⇒ Object



10
11
12
13
14
15
16
17
# File 'backend/app/model/mixins/trees.rb', line 10

def adopt_children(old_parent)
  self.class.node_model.this_repo
    .filter(:root_record_id => old_parent.id,
            :parent_id => nil)
    .order(:position).each do |root_child|
    root_child.set_root(self)
  end
end

#apply_exclusions_to_descendants(excluded_rows, parent_to_child_id) ⇒ Object

Update excluded_rows to mark any descendant of an excluded record as itself excluded.

excluded_rows is a map whose keys are the IDs of records that have been marked as excluded. parent_to_child_id is a map of record IDs to their immediate children’s IDs.



262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'backend/app/model/mixins/trees.rb', line 262

def apply_exclusions_to_descendants(excluded_rows, parent_to_child_id)
  remaining = excluded_rows.keys

  while !remaining.empty?
    excluded_parent = remaining.shift
    parent_to_child_id.fetch(excluded_parent, []).each do |child_id|
      excluded_rows[child_id] = true
      remaining.push(child_id)
    end
  end

  excluded_rows
end

#assimilate(victims) ⇒ Object



20
21
22
23
24
25
26
27
28
# File 'backend/app/model/mixins/trees.rb', line 20

def assimilate(victims)
  victims.each do |victim|
    adopt_children(victim)
  end

  Event.for_archival_record_merge(self, victims)

  super
end

#build_node_queryObject



46
47
48
# File 'backend/app/model/mixins/trees.rb', line 46

def build_node_query
  self.class.node_model.this_repo.filter(:root_record_id => self.id)
end

#childrenObject



31
32
33
34
35
# File 'backend/app/model/mixins/trees.rb', line 31

def children
  self.class.node_model.
         this_repo.filter(:root_record_id => self.id,
                          :parent_id => nil)
end

#children?Boolean

Returns:

  • (Boolean)


38
39
40
41
42
43
# File 'backend/app/model/mixins/trees.rb', line 38

def children?
  self.class.node_model.
    this_repo.filter(:root_record_id => self.id,
                     :parent_id => nil)
             .count > 0
end

#load_node_properties(node, properties, ids_of_interest = :all) ⇒ Object



51
52
53
54
# File 'backend/app/model/mixins/trees.rb', line 51

def load_node_properties(node, properties, ids_of_interest = :all)
  # Does nothing by default, but classes that use this mixin add their own
  # behaviour here.
end

#load_root_properties(properties, ids_of_interest = :all) ⇒ Object



57
58
59
60
# File 'backend/app/model/mixins/trees.rb', line 57

def load_root_properties(properties, ids_of_interest = :all)
  # Does nothing by default, but classes that use this mixin add their own
  # behaviour here.
end

#ordered_recordsObject

Return a depth-first-ordered list of URIs under this tree (starting with the tree itself)



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
# File 'backend/app/model/mixins/trees.rb', line 188

def ordered_records
  if self.publish == 0 || self.suppressed == 1
    # The whole resource is excluded.
    return []
  end

  id_positions = {}
  id_display_strings = {}
  id_depths = {nil => 0}
  parent_to_child_id = {}

  # Any record that is either suppressed or unpublished will be excluded from
  # our results.  Descendants of an excluded record will also be excluded.
  excluded_rows = {}

  self.class.node_model
    .filter(:root_record_id => self.id)
    .select(:id, :position, :parent_id, :display_string, :publish, :suppressed).each do |row|
    id_positions[row[:id]] = row[:position]
    id_display_strings[row[:id]] = row[:display_string]
    parent_to_child_id[row[:parent_id]] ||= []
    parent_to_child_id[row[:parent_id]] << row[:id]

    if row[:publish] == 0 || row[:suppressed] == 1
      excluded_rows[row[:id]] = true
    end
  end

  excluded_rows = apply_exclusions_to_descendants(excluded_rows, parent_to_child_id)

  # Our ordered list of record IDs
  result = []

  # Start with top-level records
  root_set = [nil]
  id_positions[nil] = 0

  while !root_set.empty?
    next_rec = root_set.shift
    if next_rec.nil?
      # Our first iteration.  Nothing to add yet.
    else
      unless excluded_rows[next_rec]
        result << next_rec
      end
    end

    children = parent_to_child_id.fetch(next_rec, []).sort_by {|child| id_positions[child]}
    children.reverse.each do |child|
      id_depths[child] = id_depths[next_rec] + 1
      root_set.unshift(child)
    end
  end

  extra_root_properties = self.class.ordered_record_properties([self.id])
  extra_node_properties = self.class.node_model.ordered_record_properties(result)

  [{'ref' => self.uri,
    'display_string' => self.title,
    'depth' => 0}.merge(extra_root_properties.fetch(self.id, {}))] +
    result.map {|id| {
                  'ref' => self.class.node_model.uri_for(self.class.node_type, id),
                  'display_string' => id_display_strings.fetch(id),
                  'depth' => id_depths.fetch(id),
                }.merge(extra_node_properties.fetch(id, {}))}
end

#partial_tree(node_of_interest) ⇒ Object

A tree that only contains nodes that are needed for displaying ‘node’

That is: any ancestors of ‘node’, plus the direct children of any ancestor



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
# File 'backend/app/model/mixins/trees.rb', line 66

def partial_tree(node_of_interest)
  ids_of_interest = []
  nodes_to_check = [node_of_interest]

  while !nodes_to_check.empty?
    node = nodes_to_check.pop

    # Include the node itself
    ids_of_interest << node.id if node != :root

    # Plus any of its siblings in this tree
    self.class.node_model.
         filter(:parent_id => (node == :root) ? nil : node.parent_id,
                :root_record_id => self.id).
         select(:id).all.each do |row|
      ids_of_interest << row[:id]
    end

    if node != :root && node.parent_id
      parent = self.class.node_model[node.parent_id]
      nodes_to_check << parent
    end
  end


  # Include the children of the node of interest too
  if node_of_interest != :root
    self.class.node_model.
         filter(:parent_id => node_of_interest.id,
                :root_record_id => self.id).
         select(:id).all.each do |row|
      ids_of_interest << row[:id]
    end
  end


  tree(ids_of_interest)
end

#transfer_to_repository(repository, transfer_group = []) ⇒ Object



276
277
278
279
280
281
282
283
284
285
286
# File 'backend/app/model/mixins/trees.rb', line 276

def transfer_to_repository(repository, transfer_group = [])
  obj = super

  # All records under this one will be transferred too

  children.each do |child|
    child.transfer_to_repository(repository, transfer_group + [self])
  end

  obj
end

#tree(ids_of_interest = :all, display_mode = :full) ⇒ Object



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
# File 'backend/app/model/mixins/trees.rb', line 106

def tree(ids_of_interest = :all, display_mode = :full)
  links = {}
  properties = {}

  root_type = self.class.root_type
  node_type = self.class.node_type

  top_nodes = []

  query = build_node_query

  has_children = {}
  if ids_of_interest != :all
    # Further limit our query to only the nodes we want to hear about
    query = query.filter(:id => ids_of_interest)

    # And check whether those nodes have children as cheaply as possible
    self.class.node_model.filter(:parent_id => ids_of_interest).distinct.select(:parent_id).all.each do |row|
      has_children[row[:parent_id]] = true
    end
  end

  offset = 0
  while true
    nodes = query.limit(NODE_PAGE_SIZE, offset).all

    nodes.each do |node|
      if node.parent_id
        links[node.parent_id] ||= []
        links[node.parent_id] << [node.position, node.id]
      else
        top_nodes << [node.position, node.id]
      end

      properties[node.id] = {
        :title => node[:title],
        :id => node.id,
        :record_uri => self.class.uri_for(node_type, node.id),
        :publish => node.respond_to?(:publish) ? node.publish===1 : true,
        :suppressed => node.respond_to?(:suppressed) ? node.suppressed===1 : false,
        :node_type => node_type.to_s
      }

      if ids_of_interest != :all
        properties[node.id]['has_children'] = !!has_children[node.id]
      end

      unless display_mode == :sparse
        load_node_properties(node, properties, ids_of_interest)
      end
    end

    if nodes.empty?
      break
    else
      offset += NODE_PAGE_SIZE
    end
  end


  result = {
    :title => self.title,
    :id => self.id,
    :node_type => root_type.to_s,
    :publish => self.respond_to?(:publish) ? self.publish===1 : true,
    :suppressed => self.respond_to?(:suppressed) ? self.suppressed===1 : false,
    :children => top_nodes.sort_by(&:first).map {|position, node| self.class.assemble_tree(node, links, properties)},
    :record_uri => self.class.uri_for(root_type, self.id)
  }

  unless display_mode == :sparse
    if self.respond_to?(:finding_aid_filing_title) && !self.finding_aid_filing_title.nil? && self.finding_aid_filing_title.length > 0
      result[:finding_aid_filing_title] = self.finding_aid_filing_title
    end

    load_root_properties(result, ids_of_interest)
  end

  JSONModel("#{self.class.root_type}_tree".intern).from_hash(result, true, true)
end

#trigger_index_of_entire_treeObject



298
299
300
301
302
# File 'backend/app/model/mixins/trees.rb', line 298

def trigger_index_of_entire_tree
  self.class.node_model.
              filter(:root_record_id => self.id).
              update(:system_mtime => Time.now)
end

#update_from_json(json, opts = {}, apply_nested_records = true) ⇒ Object



289
290
291
292
293
294
295
# File 'backend/app/model/mixins/trees.rb', line 289

def update_from_json(json, opts = {}, apply_nested_records = true)
  obj = super

  trigger_index_of_entire_tree

  obj
end