Module: TreeNodes

Included in:
ArchivalObject, ClassificationTerm, DigitalObjectComponent
Defined in:
backend/app/model/mixins/tree_nodes.rb

Defined Under Namespace

Modules: ClassMethods

Class Method Summary (collapse)

Instance Method Summary (collapse)

Class Method Details

+ (Object) included(base)



7
8
9
# File 'backend/app/model/mixins/tree_nodes.rb', line 7

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

Instance Method Details

- (Object) absolute_position



122
123
124
125
# File 'backend/app/model/mixins/tree_nodes.rb', line 122

def absolute_position
  relative_position = self.position
  self.class.dataset.filter(:parent_name => self.parent_name).where { position < relative_position }.count
end

- (Object) children



200
201
202
# File 'backend/app/model/mixins/tree_nodes.rb', line 200

def children
  self.class.filter(:parent_id => self.id).order(:position)
end

- (Boolean) has_children?

Returns:

  • (Boolean)


205
206
207
# File 'backend/app/model/mixins/tree_nodes.rb', line 205

def has_children?
  self.class.filter(:parent_id => self.id).count > 0
end

- (Object) order_siblings

this is just a CYA method, that might be removed in the future. We need to be sure that all the positional gaps.j



40
41
42
43
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
# File 'backend/app/model/mixins/tree_nodes.rb', line 40

def order_siblings
  # add this to avoid DB constraints 
  siblings.update(:parent_name => Sequel.lit(DB.concat('CAST(id as CHAR(10))', "'_temp'")))
 
  # get set a list of ids and their order based on their position
  position_map = siblings.select(:id).order(:position).each_with_index.inject({}) { |m,( obj, index) | m[obj[:id]] = index; m }
  
  # now we do the update in batches of 200 
  position_map.each_slice(200) do |pm|
    # the slice reformat the hash...so quickly format it back 
    pm = pm.inject({}) { |m,v| m[v.first] = v.last; m } 
    # this ids that we're updating in this batch 
    sibling_ids = pm.keys 
   
    # the resulting update will look like:
    #  UPDATE "ARCHIVAL_OBJECT" SET "POSITION" = (CASE WHEN ("ID" = 10914)
    #  THEN 0 WHEN ("ID" = 10915) THEN 1 WHEN ("ID" = 10912) THEN 2 WHEN
    #  ("ID" = 10913) THEN 3 WHEN ("ID" = 10916) THEN 4 WHEN ("ID" = 10921)
    #  THEN 5 WHEN ("ID" = 10917) THEN 6 WHEN ("ID" = 10920) THEN 7 ELSE 0
    #  END) WHERE (("ROOT_RECORD_ID" = 3) AND ("PARENT_ID" = 10911) AND (NOT
    #  "POSITION" IS NULL) AND ("ID" IN (10914, 10915, 10912, 10913, 10916,
    #  10921, 10917, 10920))
    #  )
    # this should be faster than just iterating thru all the children,
    # since it does it in batches of 200 and limits the number of updates.  
    siblings.filter(:id => sibling_ids).update( :position => Sequel.case(pm, 0, :id) )
  end
 
  # now we return the parent_name back so our DB constraints are back on.:w
  siblings.update(:parent_name => self.parent_name )
end

- (Object) set_position_in_list(target_position, sequence)



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

def set_position_in_list(target_position, sequence)
  
  # Find the position of the element we'll be inserted after.  If there are no
  # elements, or if our target position is zero, then we'll get inserted at
  # position zero.
  predecessor = if target_position > 0
                  siblings.filter(~:id => self.id).order(:position).limit(target_position).select(:position).all
                else
                  []
                end

  new_position = !predecessor.empty? ? (predecessor.last[:position] + 1) : 0


  100.times do
    DB.attempt {
      # Go right to the database here to avoid bumping lock_version for tree changes.
      self.class.dataset.db[self.class.table_name].filter(:id => self.id).update(:position => new_position)

      return
    }.and_if_constraint_fails {
      # Someone's in our spot!  Move everyone out of the way and retry.

      # Bump the sequence to maintain the invariant that sequence.number >= max(position)
      # (since we're about to increment the last N positions by 1)
      Sequence.get(sequence)

      # Sigh.  Work around:
      # http://stackoverflow.com/questions/5403437/atomic-multi-row-update-with-a-unique-constraint
      siblings.
      filter { position >= new_position }.
      update(:parent_name => Sequel.lit(DB.concat('CAST(id as CHAR(10))', "'_temp'")))

      # Do the update we actually wanted
      siblings.
      filter { position >= new_position }.
      update(:position => Sequel.lit('position + 1')) 


      # Puts it back again
      siblings.
      filter { position >= new_position}.
      update(:parent_name => self.parent_name )
      # Now there's a gap at new_position ready for our element.
    }
  end

  raise "Failed to set the position for #{self}"
end

- (Object) set_root(new_root)



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# File 'backend/app/model/mixins/tree_nodes.rb', line 12

def set_root(new_root)
  self.root_record_id = new_root.id
  save
  refresh

  if self.parent_id.nil?
    # Set ourselves to the end of the list
    update_position_only(nil, nil)
  else
    update_position_only(self.parent_id, self.position)
  end

  children.each do |child|
    child.set_root(new_root)
  end
end

- (Object) siblings



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

def siblings
  self.class.dataset.
     filter(:root_record_id => self.root_record_id,
            :parent_id => self.parent_id,
            ~:position => nil)
end

- (Object) transfer_to_repository(repository, transfer_group = [])



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'backend/app/model/mixins/tree_nodes.rb', line 210

def transfer_to_repository(repository, transfer_group = [])
  
 
  # All records under this one will be transferred too
  children.each_with_index do |child, i|
    child.transfer_to_repository(repository, transfer_group + [self]) 
  #  child.update_position_only( child.parent_id, i ) 
  end
  
  RequestContext.open(:repo_id => repository.id ) do
    self.update_position_only(self.parent_id, self.position) unless self.root_record_id.nil?
  end
    
  # ensure that the sequence if updated 
  
  super 
end

- (Object) trigger_index_of_child_nodes



147
148
149
150
# File 'backend/app/model/mixins/tree_nodes.rb', line 147

def trigger_index_of_child_nodes
  self.children.update(:system_mtime => Time.now)
  self.children.each(&:trigger_index_of_child_nodes)
end

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



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'backend/app/model/mixins/tree_nodes.rb', line 128

def update_from_json(json, opts = {}, apply_nested_records = true)
  sequence = self.class.sequence_for(json)

  self.class.set_root_record(json, sequence, opts)

  obj = super

  # Then lock in a position (which may involve contention with other updates
  # happening to the same tree of records)
  if json[self.class.root_record_type] && json.position
    self.set_position_in_list(json.position, sequence)
  end

  trigger_index_of_child_nodes

  obj
end

- (Object) update_position_only(parent_id, position)



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

def update_position_only(parent_id, position)
  if self[:root_record_id]
    root_uri = self.class.uri_for(self.class.root_record_type.intern, self[:root_record_id])
    parent_uri = parent_id ? self.class.uri_for(self.class.node_record_type.intern, parent_id) : root_uri
    sequence = "#{parent_uri}_children_position"

    parent_name = if parent_id
                    "#{parent_id}@#{self.class.node_record_type}"
                  else
                    "root@#{root_uri}"
                  end
    

    new_values = {
      :parent_id => parent_id,
      :parent_name => parent_name,
      :position => Sequence.get(sequence),
      :system_mtime => Time.now
    }
    
    # Run through the standard validation without actually saving
    self.set(new_values)
    self.validate

    if self.errors && !self.errors.empty?
      raise Sequel::ValidationFailed.new(self.errors)
    end
  
   
    # let's try and update the position. If it doesn't work, then we'll fix 
    # the position when we set it in the list...there can be problems when
    # transfering to another repo when there's holes in the tree...
    DB.attempt {
      self.class.dataset.filter(:id => self.id).update(new_values)
    }.and_if_constraint_fails { 
      new_values.delete(:position) 
      self.class.dataset.filter(:id => self.id).update(new_values)
    }
   
    self.refresh
    self.set_position_in_list(position, sequence) if position
  else
    raise "Root not set for record #{self.inspect}"
  end
end