Class: RelationProxy

Inherits:
Relation
  • Object
show all
Includes:
RubyLess
Defined in:
app/models/relation_proxy.rb

Overview

This is a mess and would need a rewrite…

Constant Summary collapse

Zena::Use::Relations::LINK_ATTRIBUTES
LINK_ATTRIBUTES.map {|sym| connection.quote_column_name(sym)}.join(',')
"nodes.*,links.id AS link_id,#{LINK_ATTRIBUTES.map {|l| "links.#{l} AS l_#{l}"}.join(',')}"

Constants inherited from Relation

Relation::EXPORT_FIELDS

Instance Attribute Summary collapse

Attributes inherited from Relation

#link

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Relation

#export, #source_role, #target_role

Instance Attribute Details

Returns the value of attribute add_links.



7
8
9
# File 'app/models/relation_proxy.rb', line 7

def add_links
  @add_links
end

#last_targetObject

Returns the value of attribute last_target.



7
8
9
# File 'app/models/relation_proxy.rb', line 7

def last_target
  @last_target
end

Returns the value of attribute link_errors.



7
8
9
# File 'app/models/relation_proxy.rb', line 7

def link_errors
  @link_errors
end

get



72
73
74
# File 'app/models/relation_proxy.rb', line 72

def other_link
  @other_link
end

#sideObject

Returns the value of attribute side.



7
8
9
# File 'app/models/relation_proxy.rb', line 7

def side
  @side
end

#startObject

Returns the value of attribute start.



7
8
9
# File 'app/models/relation_proxy.rb', line 7

def start
  @start
end

Class Method Details

.find_by_role(role, source_kpath = nil) ⇒ Object

Find a role from a name. If a source_kpath is provided, only roles that could be reached from this class are found.



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'app/models/relation_proxy.rb', line 16

def find_by_role(role, source_kpath = nil)
  if source_kpath
    klasses = []
    source_kpath.split(//).each_index { |i| klasses << source_kpath[0..i] }
    rel = find(:first, :conditions => ["((source_role = ? AND target_kpath IN (?)) OR (target_role = ? AND source_kpath IN (?))) AND site_id = ?", role, klasses, role, klasses, current_site[:id]])
  else
    rel = find(:first, :conditions => ["(source_role = ? OR target_role = ?) AND site_id = ?", role, role, current_site[:id]])
  end

  return nil unless rel

  if rel[:target_role] == role
    rel.side = :source
  else
    rel.side = :target
  end

  rel
end

.get_proxy(node, role) ⇒ Object

Find a relation proxy for a role through a given node. The finder makes sure the class path is compatible with the node’s class/virtual_class given as parameter.



38
39
40
41
42
43
44
45
46
47
# File 'app/models/relation_proxy.rb', line 38

def get_proxy(node, role)
  rel = find_by_role(role, node.new_record? ? nil : node.kpath)
  if rel && (node.new_record? || node.kpath =~ /\A#{rel.this_kpath}/)
    rel.start = node
    rel
  else
    # invalid relation for the given class path
    nil
  end
end

Instance Method Details

#as_unique?Boolean

Returns:

  • (Boolean)


445
446
447
# File 'app/models/relation_proxy.rb', line 445

def as_unique?
  @side == :source ? source_unique : target_unique
end

#attributes_to_update_valid?Boolean

link can be changed if user can write in old and new

  1. can remove old link

  2. can write in new target

Returns:

  • (Boolean)


250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'app/models/relation_proxy.rb', line 250

def attributes_to_update_valid?
  return true unless @attributes_to_update || @links_to_delete

  @link_errors  = {}
  @add_links    = []
  @del_links    = []
  @update_links = []

  if @links_to_delete
    # only removing links
    @del_links = @links_to_delete
    @attributes_to_update = {}
  else

    # check if we have an update/create
    unless @attributes_to_update.has_key?(:id) # set during other_id=
      # try to find current id/ids
      if @other_link
        @attributes_to_update[:id] = @other_link[other_side]
      elsif link_id = @start.link_id
        @other_link = Link.find(link_id)
        @attributes_to_update[:id] = @other_link[other_side]
        @attributes_to_update[:link_id] = link_id
      elsif unique?
        if other_id
          @attributes_to_update[:id] = other_id
        elsif @attributes_to_update.keys == [:id]
          # ignore (set icon_id = nil when already == nil)
        else
          @link_errors['update'] = _('missing target')
        end
      else
        # error: cannot set other attributes (status/comment) on multiple nodes
        @link_errors['update'] = _('cannot update multiple targets')
      end
    end

    if @attributes_to_update[:id].kind_of?(Array)
      if unique?
        # TODO: translate
        @link_errors['arity'] = "Cannot set multiple targets on #{as_unique? ? 'one' : 'many'}-to-one relation '#{this_role}'."
      elsif (@attributes_to_update.keys & LINK_ATTRIBUTES) != []
        keys = @attributes_to_update.keys
        keys.delete(:id)
        # TODO: translate
        @link_errors['arity'] = "Cannot set attributes #{keys.join(', ')} on multiple targets."
      end
    end

    return false if @link_errors != {}
    @link_errors = @attributes_to_update[:errors] || {}

    # 1. find what changed
    if @attributes_to_update[:id].kind_of?(Array)
      # ..-to-many
      # define all links

      # list of link ids set
      add_link_ids = @attributes_to_update[:id]

      # find all current links
      # TODO: this could be optimzed (avoid loading all links...)
      other_links.each do |link|
        obj_id = link[other_side]
        if add_link_ids.include?(obj_id) && (@attributes_to_update[:date].nil? || @attributes_to_update[:link_id] || @attributes_to_update[:date] == link[:date])
          # ignore existing link
          add_link_ids.delete(obj_id)
        else
          # remove unused links / link to replace
          @del_links << link
        end
      end
      @add_links = add_link_ids.map {|obj_id| Hash[:id,obj_id] }
    elsif unique?
      # ..-to-one
      # define/update link
      if other_id == @attributes_to_update[:id]
        # same target: update
        @update_links << changed_link(other_link, @attributes_to_update)
      else
        # other target: replace
        @del_links = [other_link] if other_link
        @add_links << @attributes_to_update unless @attributes_to_update[:id].blank?
      end
    else
      # ..-to-many
      # add/update a link
      # TODO: optimize to avoid loading all links...
      if @attributes_to_update[:id].blank? && @attributes_to_update[:date]
        # delete
        @del_links = other_links.select {|l| @attributes_to_update[:date] == l[:date]}
      else
        links = other_links.select {|l| l[other_side] == @attributes_to_update[:id] && (@attributes_to_update[:date].nil? || @attributes_to_update[:link_id] || @attributes_to_update[:date] == l[:date])}
        if links != []
          # update
          if (@attributes_to_update.keys & LINK_ATTRIBUTES) != []
            links.each do |link|
              if link[other_side] == @attributes_to_update[:id]
                @update_links << changed_link(link, @attributes_to_update)
              end
            end
          end
        elsif @attributes_to_update[:id] == :ignore
          # bad id set, just used for error reporting
        else
          # add
          @add_links << @attributes_to_update
        end
      end
    end
  end

  id_to_zip = attributes_to_update[:id_to_zip] || {}

  # 2. can write in new target ? (and remove targets previous link)
  @add_links.each do |hash|
    # last_target is used by "linked_node" from Node to get hold of the last linked node
    if @last_target = find_target(hash[:id])
      # store remote node so that we can use in index rebuild (scope_index)
      hash[:node] = @last_target
      # make sure we can overwrite previous link if as_unique
      if as_unique?
        if previous_link = Link.find(:first, :conditions => ["relation_id = ? AND #{other_side} = ?", self[:id], @last_target[:id]])
          @del_links << previous_link
        end
      end
    else
      if zip = id_to_zip[hash[:id]]
        key = zip
      elsif node = secure(Node) { Node.find_by_id(hash[:id]) }
        key = node.zip
      else
        key = 'id'
      end

      @link_errors[key] = _('invalid target')
    end
  end

  # 1. can remove old link ?
  @del_links.each do |link|
    unless find_node(link[other_side], unique?)
      if zip = id_to_zip[link[other_side]]
        key = zip
      elsif node = secure(Node) { Node.find_by_id(hash[:id]) }
        key = node.zip
      else
        key = 'id'
      end

      @link_errors[key] = _('cannot remove link')
    end
  end

  @update_links.compact!
  return @link_errors == {}
end

Return updated link if changed or nil when nothing changed



409
410
411
412
413
414
415
416
417
418
419
# File 'app/models/relation_proxy.rb', line 409

def changed_link(link, attrs)
  changed = false
  LINK_ATTRIBUTES.each do |sym|
    next unless attrs.has_key?(sym)
    if attrs[sym] != link[sym]
      changed = true
      link[sym] = attrs[sym]
    end
  end
  changed ? link : nil
end

def source_unique

self[:source_unique] ? true : false

end

def target_unique

self[:target_unique] ? true : false

end



457
458
459
# File 'app/models/relation_proxy.rb', line 457

def link_side
  @side == :source ? 'source_id' : 'target_id'
end

#other_iconObject



118
119
120
# File 'app/models/relation_proxy.rb', line 118

def other_icon
  @side == :source ? target_icon : source_icon
end

#other_idObject



76
77
78
# File 'app/models/relation_proxy.rb', line 76

def other_id
  other_link ? other_link[other_side] : nil
end

#other_id=(v) ⇒ Object

set



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'app/models/relation_proxy.rb', line 145

def other_id=(v)
  attributes_to_update[:errors] = {}
  if !v.kind_of?(Array) && v.to_i < 0
    # removing a link
    # TODO: support Array
    if link = other_links.select { |l| l[other_side] == -v }.first
      remove_link(link)
    else
      # ignore
    end
  elsif v.kind_of?(Array)
    if v.first.kind_of?(Node)
      node_by_id = attributes_to_update[:nodes] = {}
      v.each do |r|
        node_by_id[r.id.to_i] = r
      end
      attributes_to_update[:id] = v.map{|r| r.id.to_i}
    else
      attributes_to_update[:id] = v.select {|e| !e.blank?}.map(&:to_i)
    end
  else
    attributes_to_update[:id] = v.blank? ? nil : v.to_i
  end
end

#other_idsObject



84
85
86
# File 'app/models/relation_proxy.rb', line 84

def other_ids
  (other_links || []).map { |l| l[other_side] }
end

#other_ids=(v) ⇒ Object



215
216
217
# File 'app/models/relation_proxy.rb', line 215

def other_ids=(v)
  self.other_id = v
end

#other_kpathObject



238
239
240
# File 'app/models/relation_proxy.rb', line 238

def other_kpath
  @side == :source ? target_kpath : source_kpath
end

find the links from the current context (source or target)



243
244
245
# File 'app/models/relation_proxy.rb', line 243

def other_links
  @other_links ||= Link.find(:all, :conditions => ["relation_id = ? AND #{link_side} = ?", self[:id], @start[:id]])
end

#other_roleObject



110
111
112
# File 'app/models/relation_proxy.rb', line 110

def other_role
  @side == :source ? self[:target_role] : self[:source_role]
end

#other_sideObject



461
462
463
# File 'app/models/relation_proxy.rb', line 461

def other_side
  @side == :source ? 'target_id' : 'source_id'
end

#other_vclassObject

Get class of other element (used by QueryNode to properly set resulting class).



123
124
125
# File 'app/models/relation_proxy.rb', line 123

def other_vclass
  VirtualClass.find_by_kpath(@side == :source ? self[:target_kpath] : self[:source_kpath])
end

#other_zipObject



80
81
82
# File 'app/models/relation_proxy.rb', line 80

def other_zip
  other_zips ? other_zips.first : nil
end

#other_zip=(zip_values) ⇒ Object

set



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

def other_zip=(zip_values)
  # Translate ids and then set
  errors    = attributes_to_update[:errors]    = {}
  id_to_zip = attributes_to_update[:id_to_zip] = {}

  if zip_values.kind_of?(Array)
    attributes_to_update[:id] = []
    zip_values.each do |zip|
      next if zip.blank?
      if id = secure(Node) { Node.translate_pseudo_id(zip,  :id, @start) }
        # ok
        id_to_zip[id] = zip
        attributes_to_update[:id] << id
      else
        # error
        errors[zip] = _('could not be found')
      end
    end
  elsif zip_values.blank?
    # remove all
    attributes_to_update[:id] = nil
  else
    if id = secure(Node) { Node.translate_pseudo_id(zip_values, :id, @start) }
      if id < 0
        # removing a link
        # TODO: support Array
        if link = other_links.select { |l| l[other_side] == -id }.first
          remove_link(link)
        else
          # ignore
        end
      else
        id_to_zip[id] = zip_values
        attributes_to_update[:id] = id
      end
    else
      # error
      # do not try to add
      attributes_to_update[:id] = :ignore
      errors[zip_values] = _('could not be found')
    end
  end
end

#other_zipsObject



88
89
90
91
92
# File 'app/models/relation_proxy.rb', line 88

def other_zips
  return nil unless @start[:id]
  return @other_zips if defined?(@other_zips)
  @other_zips = @records ? @records.map { |r| r.zip} : Zena::Db.fetch_ids("SELECT zip FROM nodes INNER JOIN links ON nodes.id=links.#{other_side} AND links.relation_id = #{self[:id]} AND links.#{link_side} = #{@start[:id]} WHERE #{secure_scope('nodes')} GROUP BY nodes.zip", 'zip')
end

#other_zips=(v) ⇒ Object



219
220
221
# File 'app/models/relation_proxy.rb', line 219

def other_zips=(v)
  self.other_zip = v
end

#qb=(qb) ⇒ Object

set from query builder



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'app/models/relation_proxy.rb', line 128

def qb=(qb)
  if qb.blank?
    self.other_id = []
  else
    query = @start.class.build_query(:all, qb,
      :node_name       => '@start',
      :main_class      => @start.virtual_class,
      :rubyless_helper => @start.virtual_class,
      :default         => {:order => 'id asc'}
    )
    self.other_id = secure(Node) {Node.find_by_sql(eval(query.to_s))} || []
  end
rescue ::QueryBuilder::Error => err
  attributes_to_update[:errors] = {'base' => err.message}
end

#records(opts = {}) ⇒ Object



94
95
96
97
98
99
100
101
102
# File 'app/models/relation_proxy.rb', line 94

def records(opts = {})
  return nil unless @start[:id]
  return @records if defined?(@records)
  options = {
    :select => "nodes.*, #{LINK_SELECT}",
    :joins => "INNER JOIN links ON nodes.id=links.#{other_side} AND links.relation_id = #{self[:id]} AND links.#{link_side} = #{@start[:id]}"
  }.merge(opts)
  @records = secure(Node) { Node.find(:all, options) }
end


223
224
225
226
# File 'app/models/relation_proxy.rb', line 223

def remove_link(link)
  @links_to_delete ||= []
  @links_to_delete << link
end

#source=(start) ⇒ Object

Define the caller’s side. Changes the relation into a proxy so we can add/remove links. This sets the caller on the source side of the relation.



51
52
53
54
# File 'app/models/relation_proxy.rb', line 51

def source=(start)
  @start = start
  @side  = :source
end

#target=(start) ⇒ Object

Define the caller’s side. Changes the relation into a proxy so we can add/remove links. This sets the caller on the target side of the relation.



57
58
59
60
# File 'app/models/relation_proxy.rb', line 57

def target=(start)
  @start = start
  @side  = :target
end

#this_kpathObject



234
235
236
# File 'app/models/relation_proxy.rb', line 234

def this_kpath
  @side == :source ? source_kpath : target_kpath
end

#this_roleObject



114
115
116
# File 'app/models/relation_proxy.rb', line 114

def this_role
  @side == :source ? self[:source_role] : self[:target_role]
end

#unique?Boolean

Returns:

  • (Boolean)


441
442
443
# File 'app/models/relation_proxy.rb', line 441

def unique?
  @side == :source ? target_unique : source_unique
end

#update_links!Object



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

def update_links!
  return unless @attributes_to_update
  @del_links.each    { |l| l.destroy }
  @update_links.each { |l| l.save }

  return if @add_links == []

  list = []
  @add_links.each do |hash|
    next if hash[:id].blank?
    list << ([self[:id], @start[:id], hash[:id]] + LINK_ATTRIBUTES.map{|sym| hash[sym]})
  end
  Zena::Db.insert_many('links', ['relation_id', link_side, other_side] + LINK_ATTRIBUTES, list)
  @attributes_to_update = nil
  @links_to_delete      = nil
  remove_instance_variable(:@records)     if defined?(@records)
  remove_instance_variable(:@record)      if defined?(@record)
  remove_instance_variable(:@other_links) if defined?(@other_links)
end