Module: Zena::Use::Workflow

Includes:
RubyLess
Included in:
Node
Defined in:
lib/zena/use/workflow.rb

Overview

The workflow module manages the different versions’ status and transitions. This module depends on MultiVersion and VersionHash and it should be included before these two modules.

Defined Under Namespace

Modules: ClassMethods, VersionMethods

Constant Summary collapse

WORKFLOW_ATTRIBUTES =
%w{status publish_from}

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(base) ⇒ Object

ClassMethods



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
# File 'lib/zena/use/workflow.rb', line 122

def self.included(base)
  base.has_many :editions,  :class_name => 'Version',
           :conditions=>"publish_from <= #{Zena::Db::NOW} AND status = #{Zena::Status::Pub}", :order=>'lang' #, :inverse_of => :node


  base.before_validation :set_workflow_defaults
  
  base.validate      :workflow_validation
  base.before_create :workflow_before_create

  base.after_save    :update_status_after_save

  base.class_eval do
    extend  Zena::Use::Workflow::ClassMethods

    # List of allowed *version* transitions with their validation rules. This list
    # concerns the life and death of *a single version*, not the corresponding Node.

    # FIXME: we should not use the same name for 'edit'
    # We could use 'edit', 'create', 'update'
    add_transition(:edit, :from => Zena::Status::Red, 
                          :to   => Zena::Status::Red) do |node, version|
      node.can_write?
    end

    add_transition(:edit, :from => -1,
                          :to   => Zena::Status::Red) do |node, version|
      # create a new node
      node.can_write?
    end

    add_transition(:edit, :from => Zena::Status::Pub,
                          :to   => Zena::Status::Red) do |node, version|
      node.can_write? && version.edited?
    end

    add_transition(:auto_publish, :from => Zena::Status::Pub,
                                  :to   => Zena::Status::Pub) do |node, version|
      node.full_drive?
    end

    add_transition(:publish, :from => [-1, Zena::Status::Red],
                             :to   => Zena::Status::Pub) do |node, version|
      node.full_drive?
    end

    add_transition(:publish, :from => [Zena::Status::Prop, Zena::Status::PropWith],
                             :to   => Zena::Status::Pub) do |node, version|
      # editing content when publishing a proposition is not allowed
      if node.full_drive? && !version.edited?
        true
      elsif node.full_drive?
        [false, "You do not have the rights to change a proposition's attributes."]
      else
        false
      end
    end

    add_transition(:publish, :from => (Zena::Status::Del..(Zena::Status::Pub-1)),
                             :to   => Zena::Status::Pub) do |node, version|
      node.full_drive?
    end

    add_transition(:propose, :from => Zena::Status::Red,
                             :to   => Zena::Status::Prop) do |node, version|
      node.can_write?
    end

    add_transition(:propose, :from => Zena::Status::Red,
                             :to   => Zena::Status::PropWith) do |node, version|
      node.can_write?
    end

    add_transition(:refuse,  :from => [Zena::Status::Prop, Zena::Status::PropWith],
                             :to   => Zena::Status::Red) do |node, version|
      # refuse and change attributes not allowed
      if node.full_drive? && !version.edited?
        true
      elsif version.edited?
        [false, 'You cannot edit while a proposition is beeing reviewed.']
      else
        false
      end
    end

    add_transition(:unpublish,  :from => Zena::Status::Pub,
                                :to   => Zena::Status::Rem) do |node, version|
      if node.can_drive? && !version.edited?
        true
      elsif version.edited?
        [false, "You cannot unpublish and edit at the same time."]
      else
        false
      end
    end

    add_transition(:remove,  :from => (((Zena::Status::Rep+1)..(Zena::Status::Pub-1)).to_a + [Zena::Status::Red]),
                             :to   => Zena::Status::Rem) do |node, version|
      node.can_drive? && !version.edited?
    end

    # This is when destroy goes through version removal.
    # When destroy is triggered directly, node_before_destroy is used for validation.
    add_transition(:destroy_version,  :from => (-1..Zena::Status::Rep),
                                      :to   => -1) do |node, version|
      if node.can_drive? && !visitor.is_anon?
        if node.versions.count > 1
          true
        elsif !node.empty?
          [false, "Cannot destroy last version: node is not empty."]
        elsif !visitor.is_manager? && node.auth_users
          [false, "Cannot destroy last version: node is a user."]
        else
          true
        end
      elsif visitor.is_anon?
        [false, "Anonymous users are not allowed to destroy versions."]
      else
        false
      end
    end

    add_transition(:redit, :from => (Zena::Status::Rem..(Zena::Status::Pub-1)),
                           :to   => Zena::Status::Red) do |node, version|
      node.can_drive? && !version.edited?
    end
  end
end

Instance Method Details

#after_allObject



453
454
455
# File 'lib/zena/use/workflow.rb', line 453

def after_all
  true
end

#apply(method, *args) ⇒ Object

Gateway to all modifications of the node or it’s versions.



391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
# File 'lib/zena/use/workflow.rb', line 391

def apply(method, *args)
  case method
  when :update_attributes
    self.attributes = args.first
  when :propose
    # TODO: replace with version.status = ...
    self.version_attributes = {'status' => args[0] || Zena::Status::Prop}
  when :publish
    self.version_attributes = {'status' => Zena::Status::Pub}
  when :refuse, :redit
    self.version_attributes = {'status' => Zena::Status::Red}
  when :unpublish, :remove
    self.version_attributes = {'status' => Zena::Status::Rem}
  when :destroy_version
    if versions.count == 1 && empty?
      return self.destroy # will destroy last version
    else
      self.version_attributes = {:__destroy => true}
    end
  else
    return
  end
  
  if @version
    # We accessed version, make sure '@node' in version is self
    @version.instance_variable_set(:@node, self)
  end
  
  # Determine if we need to lock version cloning
  if @no_clone_on_change || (prop.changed? && !changed_versioned_properties?)
    # We only lock if properties are changed (not in case of status changes: publication, etc)
    begin
      Node.record_timestamps     = false
      Version.record_timestamps  = false
      version.no_clone_on_change = true
      # Make sure indices are rebuilt
      save
    ensure
      @no_clone_on_change = nil
      Node.record_timestamps     = true
      Version.record_timestamps  = true
      version.no_clone_on_change = nil
    end
  else
    save
  end
end

#apply_with_callbacks(method, *args) ⇒ Object



439
440
441
442
443
444
445
446
447
448
449
# File 'lib/zena/use/workflow.rb', line 439

def apply_with_callbacks(method, *args)
  if apply_without_callbacks(method, *args)
    transition_name = @current_transition ? @current_transition[:name] : method
    callback = :"after_#{transition_name}"
    if respond_to?(callback, true)
      send(callback)
    else
      true
    end
  end && after_all # after_all can trigger even if no save operation occured
end

#auto_publish?Boolean

Returns:

  • (Boolean)


301
302
303
# File 'lib/zena/use/workflow.rb', line 301

def auto_publish?
  current_site.auto_publish? || dgroup.auto_publish?
end

#can_apply?(method) ⇒ Boolean

Returns false is the current visitor does not have enough rights to perform the action.

Returns:

  • (Boolean)


377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/zena/use/workflow.rb', line 377

def can_apply?(method)
  case method
  when :edit
    can_edit?
  when :drive
    can_drive?
  else
    # All the other actions are version transition changes
    allowed, msg = transition_allowed?(method)
    allowed
  end
end

#can_destroy_version?Boolean

Can destroy current version ? (only logged in user can destroy)

Returns:

  • (Boolean)


347
348
349
# File 'lib/zena/use/workflow.rb', line 347

def can_destroy_version?
  can_apply? :destroy_version
end

#can_edit?(lang = nil) ⇒ Boolean

Returns:

  • (Boolean)


305
306
307
308
# File 'lib/zena/use/workflow.rb', line 305

def can_edit?(lang=nil)
  # Has the visitor write access to the node & node is not a proposition ?
  can_write? && !(Zena::Status::Prop..Zena::Status::PropWith).include?(version.status)
end

#can_propose?Boolean

can propose for validation

Returns:

  • (Boolean)


320
321
322
# File 'lib/zena/use/workflow.rb', line 320

def can_propose?
  can_apply? :propose
end

#can_publish?Boolean

people who can publish:

  • people who #can_drive? if status >= prop or owner

  • people who #can_drive? if node is private

Returns:

  • (Boolean)


327
328
329
# File 'lib/zena/use/workflow.rb', line 327

def can_publish?
  can_apply? :publish
end

#can_refuse?Boolean

Can refuse a publication. Same rights as can_publish? if the current version is a redaction.

Returns:

  • (Boolean)


332
333
334
# File 'lib/zena/use/workflow.rb', line 332

def can_refuse?
  can_apply? :refuse
end

#can_remove?Boolean

Can remove any other version

Returns:

  • (Boolean)


342
343
344
# File 'lib/zena/use/workflow.rb', line 342

def can_remove?
  can_apply? :remove
end

#can_unpublish?Boolean

Can remove publication

Returns:

  • (Boolean)


337
338
339
# File 'lib/zena/use/workflow.rb', line 337

def can_unpublish?
  can_apply? :unpublish
end

#can_update?Boolean

Returns:

  • (Boolean)


310
311
312
# File 'lib/zena/use/workflow.rb', line 310

def can_update?
  can_write? && version.status == Zena::Status::Red
end

#destroy_versionObject

Versions can be destroyed if they are in ‘deleted’ status. Destroying the last version completely removes the node (it must thus be empty)



507
508
509
# File 'lib/zena/use/workflow.rb', line 507

def destroy_version
  apply(:destroy_version)
end

#edit_content!Object

FIXME: remove !



315
316
317
# File 'lib/zena/use/workflow.rb', line 315

def edit_content!
  redaction && redaction.redaction_content
end

#get_publish_from(ignore_id = nil) ⇒ Object

Set publish_from to the minimum publication time of all editions



512
513
514
515
# File 'lib/zena/use/workflow.rb', line 512

def get_publish_from(ignore_id = nil)
  pub_string  = (self.class.connection.select_one("SELECT publish_from FROM #{version.class.table_name} WHERE node_id = '#{self[:id]}' AND status = #{Zena::Status::Pub} #{ignore_id ? "AND id != '#{ignore_id}'" : ''} order by publish_from ASC LIMIT 1") || {})['publish_from']
  ActiveRecord::ConnectionAdapters::Column.string_to_time(pub_string)
end

#propose(prop_status = Zena::Status::Prop) ⇒ Object

Propose for publication



458
459
460
461
462
463
464
# File 'lib/zena/use/workflow.rb', line 458

def propose(prop_status=Zena::Status::Prop)
  if version.status == Zena::Status::Prop
    errors.add(:base, 'Already proposed.')
    return false
  end
  apply(:propose, prop_status)
end

#publish(pub_time = nil) ⇒ Object

publish if version status is : redaction, proposition, replaced or removed if version to publish is ‘rem’ or ‘red’ or ‘prop’ : old publication => ‘replaced’ if version to publish is ‘rep’ : old publication => ‘removed’



474
475
476
477
478
479
480
# File 'lib/zena/use/workflow.rb', line 474

def publish(pub_time=nil)
  if version.status == Zena::Status::Pub
    errors.add(:base, 'Already published.')
    return false
  end
  apply(:publish, pub_time)
end

#reditObject

Edit again a previously published/removed version.



501
502
503
# File 'lib/zena/use/workflow.rb', line 501

def redit
  apply(:redit)
end

#refuseObject

Refuse publication



467
468
469
# File 'lib/zena/use/workflow.rb', line 467

def refuse
  apply(:refuse)
end

#removeObject

A published version can be removed by the members of the publish group A redaction can be removed by it’s owner



492
493
494
# File 'lib/zena/use/workflow.rb', line 492

def remove
  apply(:remove)
end

#save_without_cloneObject

Used when we want to save changed properties without changing author and/or creating new versions. This is needed when we want to synchronise some properties with an external application.



533
534
535
536
# File 'lib/zena/use/workflow.rb', line 533

def save_without_clone
  @no_clone_on_change = true
  apply(:update_attributes, {})
end

#set_current_transitionObject



351
352
353
354
355
356
# File 'lib/zena/use/workflow.rb', line 351

def set_current_transition
  version = self.version
  prev = new_record? ? -1 : version.status_was
  curr = version.status
  @current_transition = transition_for(prev, curr)
end

#traductions(opts = {}) ⇒ Object

return an array of published versions



273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/zena/use/workflow.rb', line 273

def traductions(opts={})
  if opts == {}
    trad = editions
  else
    trad = editions.find(:all, opts)
  end

  if trad == []
    nil
  else
    trad.each {|t| t.node = self}
    trad
  end
end

#transition_allowed?(transition) ⇒ Boolean

Returns:

  • (Boolean)


368
369
370
371
372
373
374
# File 'lib/zena/use/workflow.rb', line 368

def transition_allowed?(transition)
  from_status = self.version.status
  if transition.kind_of?(Symbol)
    transition = self.class.transitions.detect { |t| t[:name] == transition && t[:from].include?(from_status) }
  end
  transition && (transition[:validate].nil? || transition[:validate].call(self, version))
end

#transition_for(prev, curr) ⇒ Object



358
359
360
361
362
363
364
365
366
# File 'lib/zena/use/workflow.rb', line 358

def transition_for(prev, curr)
  self.class.transitions.each do |t|
    from, to = t[:from], t[:to]
    if curr == to && from.include?(prev)
      return t
    end
  end
  return nil
end

#unpublishObject



482
483
484
# File 'lib/zena/use/workflow.rb', line 482

def unpublish
  apply(:unpublish)
end

#update_attributes(new_attributes) ⇒ Object

Update an node’s attributes or the node’s version/content attributes. If the attributes contains only properties, then only the version will be saved. If the attributes does not contain any properties only the node is saved, without creating a new version.



520
521
522
# File 'lib/zena/use/workflow.rb', line 520

def update_attributes(new_attributes)
  apply(:update_attributes, new_attributes)
end

#update_attributes_without_clone(new_attributes) ⇒ Object

Used when we want to update properties without changing author and/or creating new versions. This is needed when we want to synchronise some properties with an external application.



526
527
528
529
# File 'lib/zena/use/workflow.rb', line 526

def update_attributes_without_clone(new_attributes)
  @no_clone_on_change = true
  apply(:update_attributes, new_attributes)
end

#would_change_original?Boolean

Return true if the version is the original version and we are in redit time (will not clone)

Returns:

  • (Boolean)


289
290
291
292
293
294
295
296
297
298
299
# File 'lib/zena/use/workflow.rb', line 289

def would_change_original?
  # on original
  version.number == 1 &&
  # redaction or pub with autopublish
  (  
     version.status == Zena::Status::Red || 
    (version.status == Zena::Status::Pub && auto_publish?)
  ) &&
  # same owner, in redit time, ...
  !version.clone_on_change?
end