root/trunk/lib/multiversion.rb

Revision 1270, 26.8 kB (checked in by gaspard, 3 weeks ago)

commit 67e13e0a262d9116d352b7bad4395ee639ade251
Author: Gaspard Bucher <gaspard@teti.ch>

Changed behaviour of auto_publish when the user does not have publish rights. Expected behavior is a "redaction", not a "proposition" since a proposition locks the user out of edit mode as soon as he/she saves.

Line 
1 module Zena
2   module Acts
3     module Multiversioned
4       # this is called when the module is included into the 'base' module
5       def self.included(base)
6         # add all methods from the module "AddActsAsMethod" to the 'base' module
7         base.extend AddActsAsMethod
8       end
9       module AddActsAsMethod
10         def acts_as_multiversioned
11           validate          :valid_redaction
12           after_save        :save_version
13           after_save        :after_all
14           private
15           has_many :versions, :order=>"number DESC",  :dependent => :destroy
16           has_many :editions, :class_name=>"Version", :conditions=>"publish_from <= now() AND status = #{Zena::Status[:pub]}", :order=>'lang'
17           public
18           class_eval <<-END
19             include Zena::Acts::Multiversioned::InstanceMethods
20           END
21         end
22       end
23      
24      
25       module InstanceMethods
26         def self.included(aClass)
27           aClass.extend ClassMethods
28         end
29        
30         #def new_redaction?; version.new_record?; end
31         
32         # VERSION
33         def version=(v)
34           if v.kind_of?(Version)
35             @version = v
36           end
37         end
38        
39         def can_edit?
40           can_edit_lang?
41         end
42        
43         def can_edit_lang?(lang=nil)
44           return false unless can_write?
45           if lang
46             # can we create a new redaction for this lang ?
47             v = versions.find(:first, :conditions=>["status >= #{Zena::Status[:red]} AND status < #{Zena::Status[:pub]} AND lang=?", lang])
48             v == nil
49           else
50             # can we create a new redaction in the current context ?
51             # there can only be one redaction/proposition per lang per node. Only the owner of the red can edit
52             v = versions.find(:first, :conditions=>["status >= #{Zena::Status[:red]} AND status < #{Zena::Status[:pub]} AND lang=?", visitor.lang])
53             v == nil || (v.status == Zena::Status[:red] && v.user_id == visitor[:id])
54           end
55         rescue ActiveRecord::RecordNotFound
56           true
57         end
58        
59         # Try to set the node's version to a redaction. If lang is specified
60         def edit!(lang = nil, publish_after_save = false)
61           redaction(lang, publish_after_save)
62         end
63        
64         def edit_content!
65           redaction && redaction.redaction_content
66         end
67        
68         # return an array of published versions
69         def traductions(opts={})
70           if opts == {}
71             trad = editions
72           else
73             trad = editions.find(:all, opts)
74           end
75           trad.map! do |t|
76             t.node = self # make sure relation is kept and we do not reload a node that is not secured
77             t
78           end
79           trad == [] ? nil : trad
80         end
81        
82         # can propose for validation
83         def can_propose?
84           can_apply?(:propose)
85         end
86        
87         # people who can publish:
88         # * people who #can_visible? if +status+ >= prop or owner
89         # * people who #can_manage? if node is private
90         def can_publish?
91           can_apply?(:publish)
92         end
93        
94         # Can refuse a publication. Same rights as can_publish? if the current version is a redaction.
95         def can_refuse?
96           can_apply?(:refuse)
97         end
98        
99         # Can remove publication
100         def can_unpublish?(v=version)
101           can_apply?(:unpublish, v)
102         end
103        
104         # Can destroy current version ? (only logged in user can destroy)
105         def can_destroy_version?(v=version)
106           can_apply?(:destroy_version, v)
107         end
108        
109         # Return true if the node is not a reference for any other node
110         def empty?
111           return true if new_record?
112           0 == self.class.count_by_sql("SELECT COUNT(*) FROM #{self.class.table_name} WHERE #{ref_field} = #{self[:id]}")
113         end
114        
115         # Returns false is the current visitor does not have enough rights to perform the action.
116         def can_apply?(method, v=version)
117           return false if new_record?
118           return true  if visitor.is_su?
119           case method
120           when :drive
121             can_drive?
122           when :propose, :backup
123             v.user_id == visitor[:id] && v.status == Zena::Status[:red]
124           when :refuse
125             v.status > Zena::Status[:red] && can_apply?(:publish)
126           when :publish
127             v.status < Zena::Status[:pub] &&
128             ( ( can_visible? && (v.status > Zena::Status[:red] || v.status == Zena::Status[:rep] || v.user_id == visitor[:id]) ) ||
129               ( can_manage?  && private? )
130             )
131           when :unpublish
132             can_drive? && v.status == Zena::Status[:pub]
133           when :remove
134             (can_drive? || v.user_id == visitor[:id] ) && v.status <= Zena::Status[:red] && v.status > Zena::Status[:rem]
135           when :redit
136             can_edit? && v.user_id == visitor[:id]
137           when :edit
138             can_edit?
139           when :destroy_version
140             # anonymous users cannot destroy
141             can_drive? && v.status == Zena::Status[:rem] && !visitor.is_anon? && (self.versions.count > 1 || empty?)
142           when :update_attributes
143             can_write? # basic check, complete check is made for each attribute during validations
144           end
145         end
146        
147         # Gateway to all modifications of the node or it's versions.
148         def apply(method, *args)
149           unless can_apply?(method)
150             errors.add('base', 'you do not have the rights to do this')
151             return false
152           end
153           res = case method
154           when :propose
155             version.status = args[0] || Zena::Status[:prop]
156             version.save && after_propose && update_max_status
157           when :backup
158             version.status = Zena::Status[:rep]
159             @redaction = nil
160             redaction.save if version.save
161           when :refuse
162             version.status = Zena::Status[:red]
163             version.save && after_refuse && update_max_status
164           when :publish
165             pub_time = args[0]
166             old_ids = version.class.fetch_ids "node_id = '#{self[:id]}' AND lang = '#{version[:lang]}' AND status = '#{Zena::Status[:pub]}'"
167             case version.status
168             when Zena::Status[:rep]
169               new_status = Zena::Status[:rem]
170             else
171               new_status = Zena::Status[:rep]
172             end
173             pub_time = args[0]
174             version.publish_from = pub_time || version.publish_from || Time.now
175             version.status = Zena::Status[:pub]
176             if version.save
177               # only remove previous publications if save passed
178               self.class.connection.execute "UPDATE #{version.class.table_name} SET status = '#{new_status}' WHERE id IN (#{old_ids.join(', ')})" unless old_ids == []
179               res = after_publish(pub_time) && update_publish_from && update_max_status
180               if res
181                 self.class.connection.execute "UPDATE #{self.class.table_name} SET updated_at = #{self.class.connection.quote(Time.now)} WHERE id=#{self[:id].to_i}" unless new_record?
182               end
183               res
184             else
185               merge_version_errors
186               false
187             end
188           when :unpublish
189             version.status = Zena::Status[:rem]
190             if version.save
191               update_publish_from && update_max_status && after_unpublish
192             else
193               false
194             end
195           when :remove
196             version.status = Zena::Status[:rem]
197             if version.save
198               update_publish_from && update_max_status && after_remove
199             else
200               false
201             end
202           when :redit
203             version.status = Zena::Status[:red]
204             if version.save
205               update_publish_from && update_max_status && after_redit
206             else
207               false
208             end
209           when :destroy_version
210             if versions.count == 1
211               version.destroy && self.destroy
212             else
213               version.destroy
214             end
215           when :update_attributes
216             attributes = args[0].stringify_keys
217             attributes = remove_attributes_protected_from_mass_assignment(attributes)
218             attributes = remove_attributes_with_same_value(attributes)
219             return true if attributes == {} # nothing to be done.
220             do_update_attributes(attributes)
221           end
222         end
223        
224         # Propose for publication
225         def propose(prop_status=Zena::Status[:prop])
226           apply(:propose, prop_status)
227         end
228        
229         # Backup a redaction (create a new version)
230         # TODO: test
231         def backup
232           apply(:backup)
233         end
234        
235         # Refuse publication
236         def refuse
237           apply(:refuse)
238         end
239        
240         # publish if version status is : redaction, proposition, replaced or removed
241         # if version to publish is 'rem' or 'red' or 'prop' : old publication => 'replaced'
242         # if version to publish is 'rep' : old publication => 'removed'
243         def publish(pub_time=nil)
244           apply(:publish, pub_time)
245         end
246        
247         def unpublish
248           apply(:unpublish)
249         end
250        
251         # A published version can be removed by the members of the publish group
252         # A redaction can be removed by it's owner
253         def remove
254           apply(:remove)
255         end
256        
257         # Edit again a previously published/removed version.
258         def redit
259           apply(:redit)
260         end
261        
262         # Versions can be destroyed if they are in 'deleted' status.
263         # Destroying the last version completely removes the node (it must thus be empty)
264         def destroy_version
265           apply(:destroy_version)
266         end
267        
268         # Call backs
269         def after_propose
270           true
271         end
272         def after_refuse
273           true
274         end
275         def after_publish
276           true
277         end
278         def after_unpublish
279           true
280         end
281         def after_redit
282           true
283         end
284         def after_remove
285           true
286         end
287         def after_all
288           true
289         end
290        
291         # Set +publish_from+ to the minimum publication time of all editions
292         # TODO: OPTIMIZATION: "UPDATE nodes SET publish_from = (select versions.publish_from from versions WHERE nodes.id=versions.node_id and versions.status = 50 order by versions.publish_from DESC) WHERE id = #{id}"
293         def update_publish_from
294           return true if version[:status] == Zena::Status[:pub] && self[:publish_from] == version[:publish_from]
295           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]} order by publish_from DESC LIMIT 1") || {})['publish_from']
296           pub_date    = ActiveRecord::ConnectionAdapters::Column.string_to_time(pub_string)
297           if self[:publish_from] != pub_date
298             self.class.connection.execute "UPDATE #{self.class.table_name} SET publish_from = #{pub_string ? "'#{pub_string}'" : 'NULL'} WHERE id = #{id}"
299             self[:publish_from] = pub_date
300           end
301           true
302         end
303        
304         # Set +max_status+ to the maximum status of all versions
305         def update_max_status(version = self.version)
306           if version[:status] == max_status
307             after_all
308             return true
309           end
310           vers_table = version.class.table_name
311           node_table = self.class.table_name
312           new_max    = self.class.connection.select_one("select #{vers_table}.status from #{vers_table} WHERE #{vers_table}.node_id='#{self[:id]}' order by #{vers_table}.status DESC LIMIT 1")['status']
313           self.class.connection.execute "UPDATE #{node_table} SET max_status = '#{new_max}' WHERE #{node_table}.id = #{id}" if new_max != self[:max_status]
314           self[:max_status] = new_max
315           # After_save does not necesseraly trigger after_all when a
316           # redaction is created/updated : the node is not saved when modifications only alter the redaction.
317           after_all
318         end
319        
320         # Update an node's attributes or the node's version/content attributes. If the attributes contains only
321         # :v_... or :c_... keys, then only the version will be saved. If the attributes does not contain any :v_... or :c_...
322         # attributes, only the node is saved, without creating a new version.
323         def update_attributes(new_attributes)
324           apply(:update_attributes,new_attributes)
325         end
326        
327        
328         # Return only the attributes that have changed, returns all if the record is new.
329         # FIXME: handle link=>{...} correctly
330         def remove_attributes_with_same_value(new_attributes)
331           res = {}
332           new_attributes.each do |k,v|
333             if k == 'v_status'
334               # makes no sense to remove this (it's used to auto publish changes)
335               res[k] = v
336               next
337             end
338             if safe_attribute?(k)
339               current_value = self.send(k) rescue nil
340             else
341               # ignore
342               next
343             end
344            
345             case current_value.class.to_s
346             when 'NilClass'
347               res[k] = v unless v == nil || v == ''
348             when 'String'
349               res[k] = v unless current_value == v.to_s
350             when 'Float'
351               v = v.blank? ? nil : v.to_f
352               res[k] = v unless current_value == v
353             when 'Fixnum'
354               v = v.blank? ? nil : v.to_i
355               res[k] = v unless current_value == v
356             when 'Date', 'DateTime', 'Time'
357               begin
358                 res[k] = v unless current_value.strftime('%Y-%m-%d %H:%M:%S') == (v.kind_of?(String) ? DateTime.parse(v) : v).strftime('%Y-%m-%d %H:%M:%S')
359               rescue
360                 res[k] = v
361               end
362             when 'TrueClass', 'FalseClass'
363               res[k] = v unless current_value == v
364             when 'File', 'StringIO'
365               # md5 on content
366               res[k] = v unless Digest::MD5.hexdigest(current_value.read) == Digest::MD5.hexdigest(v.read)
367               v.rewind
368               errors.clear # 'c_file' does an unparse_assets which can create errors.
369             else
370               res[k] = v
371             end
372           end
373           res
374         end
375        
376        
377         # Return the current version. If @version was not set, this is a normal find or a new record. We have to find
378         # a suitable edition :
379         # * if new_record?, create a new redaction
380         # * find user redaction or proposition in the current lang
381         # * find an edition for current lang
382         # * find an edition in the reference lang for this node
383         # * find the first publication
384         # If 'key' is set to :pub, only find the published versions. If key is a number, find the version with this number.
385         def version(key=nil) #:doc:
386           return @version if @version
387          
388           if key && !key.kind_of?(Symbol) && !new_record?
389             if visitor.is_su?
390               @version = secure!(Version) { Version.find(:first, :conditions => ["node_id = ? AND number = ?", self[:id], key]) }
391             elsif can_drive?
392               @version = secure!(Version) { Version.find(:first, :conditions => ["node_id = ? AND number = ? AND (user_id = ? OR status <> ?)", self[:id], key, visitor[:id], Zena::Status[:red]]) }
393             else
394               @version = secure!(Version) { Version.find(:first, :conditions => ["node_id = ? AND number = ? AND (user_id = ? OR status >= ?)", self[:id], key, visitor[:id], Zena::Status[:pub]]) }
395             end
396           else
397             min_status = (key == :pub) ? Zena::Status[:pub] : Zena::Status[:red]
398            
399             if new_record?
400               @version = version_class.new
401               # owner and lang set in secure_scope
402               @version.status = Zena::Status[:red]
403             elsif can_drive?
404               # sees propositions
405               lang = visitor.lang.gsub(/[^\w]/,'')
406               @version =  Version.find(:first,
407                             :select=>"*, (lang = '#{lang}') as lang_ok, (lang = '#{ref_lang}') as ref_ok",
408                             :conditions=>[ "((status >= ? AND user_id = ? AND lang = ?) OR status > ?) AND node_id = ?",
409                                             min_status, visitor[:id], lang, Zena::Status[:red], self[:id] ],
410                             :order=>"lang_ok DESC, ref_ok DESC, status ASC ")
411               if !@version
412                 @version = versions.find(:first, :order=>'id DESC')
413               end
414             else
415               # only own redactions and published versions
416               lang = visitor.lang.gsub(/[^\w]/,'')
417               @version =  Version.find(:first,
418                             :select=>"*, (lang = '#{lang}') as lang_ok, (lang = '#{ref_lang}') as ref_ok",
419                             :conditions=>[ "((status >= ? AND user_id = ? AND lang = ?) OR status = ?) and node_id = ?",
420                                             min_status, visitor[:id], lang, Zena::Status[:pub], self[:id] ],
421                             :order=>"lang_ok DESC, ref_ok DESC, status ASC, publish_from ASC")
422
423             end
424            
425             if @version.nil?
426               raise Exception.new("#{self.class} #{self[:id]} does not have any version !!")
427             end
428           end
429           @version.node = self if @version # preload self as node in version
430           @version
431         end
432        
433         def attributes=(attributes)
434           super @attributes_filtering_done ? attributes : filter_attributes(attributes)
435         end
436        
437         private
438        
439         def do_update_attributes(new_attributes)
440           attributes = filter_attributes(new_attributes)
441          
442           publish_after_save = (attributes.delete('v_status').to_i == Zena::Status[:pub]) || current_site[:auto_publish]
443           redaction_attr = false
444           node_attr      = false
445          
446           attributes.each do |k,v|
447             next if k.to_s == 'id' # just ignore 'id' (cannot be set but is often around)
448             if k.to_s =~ /^(v_|c_|d_)/
449               redaction_attr = true
450             else
451               node_attr      = true
452             end
453             break if node_attr && redaction_attr
454           end
455          
456           if redaction_attr
457             return false unless edit!(nil, publish_after_save)
458           end
459          
460           if node_attr
461             # super class call (original rails update_attributes)
462             @attributes_filtering_done = true  # if anyone knows a better way to avoid filtering twice...
463             self.attributes = attributes
464             @attributes_filtering_done = false
465             ok = save
466           elsif attributes != {}
467             attributes.each do |k,v|
468               next if k.to_s == 'id' # just ignore 'id' (cannot be set but is often around)
469               self.send("#{k}=".to_sym, v)
470             end
471             valid_redaction
472             if errors.empty?
473               ok = save_version(false)
474             end
475            
476             if ok
477               # set updated at date
478               update_attribute_without_fuss(:updated_at, Time.now)
479             end
480           else
481             # nothing to update (only v_status)
482             ok = true
483           end
484          
485           if ok && publish_after_save
486             if v_status == Zena::Status[:pub]
487               ok = after_publish && after_all && update_publish_from
488             elsif can_apply?(:publish)
489               ok = apply(:publish)
490             elsif ok
491               ok = update_max_status && update_publish_from
492             end
493           elsif ok
494             ok = update_max_status && update_publish_from
495           end
496           ok
497         end
498        
499         def update_attribute_without_fuss(att, value)
500           self[att] = value
501           if value.kind_of?(Time)
502             value = value.strftime("'%Y-%m-%d %H:%M:%S'")
503           elsif value.nil?
504             value = "NULL"
505           else
506             value = "'#{value}'"
507           end
508           self.class.connection.execute "UPDATE #{self.class.table_name} SET #{att}=#{value} WHERE id=#{self[:id]}"
509         end
510
511         # Find/create a redaction. If lang is specified, use it instead of the visitor's current language. If
512         # publish_after_save is true and the current published version is not older then the site's redit_time and
513         # the visitor is the author of the publication, update the publication instead of creating a new version.
514         def redaction(lang = nil, publish_after_save = false)
515           return @redaction if @redaction && (lang.nil? || lang == @redaction.lang)
516           redit = false
517           if new_record?
518             @redaction = version
519           elsif version.lang == lang && version.status == Zena::Status[:red] && version.user_id == visitor[:id]
520             @redaction = version
521           else
522             lang ||= visitor.lang
523             # is there a current redaction ?
524             v = versions.find(:first, :conditions=>["status >= #{Zena::Status[:red]} AND status < #{Zena::Status[:prop]} AND lang=?", lang])
525             if v == nil && can_write?
526               # create new redaction or redit current publication
527               if publish_after_save && version[:status] >= Zena::Status[:prop] && version[:user_id] == visitor[:id] && version[:lang] == lang && (Time.now < version[:updated_at] + current_site[:redit_time].to_i)
528                 if can_visible? || (can_manage? && private?) || version[:status] == Zena::Status[:prop]
529                   # re-edit publication
530                   redit = true
531                   v = version
532                 end
533               end
534              
535               if v == nil
536                 # could not convert a publication into a redaction, new version
537                 v = version.clone
538                 v.status = Zena::Status[:red]
539                 v.publish_from = v.created_at = nil
540                 v.comment = v.number = ''
541                 v.user_id = visitor[:id]
542                 v.lang = lang
543                 v[:content_id] = version[:content_id] || version[:id]
544               end
545             end
546             v.node = self if v
547            
548             if v && (v.user_id == visitor[:id]) && (v.status == Zena::Status[:red] || redit)
549               @old_title = @version.title # node sync_name leaking here...
550               @redaction = @version = v
551             elsif v
552               errors.add('base', "(#{v.user.login}) is editing this node")
553               nil
554             else
555               errors.add('base', 'you do not have the rights to do this')
556               nil
557             end
558           end
559         end
560        
561         # Any attribute starting with 'v_' belongs to the 'version' or 'redaction'
562         # Any attribute starting with 'c_' belongs to the 'version' or 'redaction' content
563         # FIXME: performance: create methods on the fly so that next calls will not pass through 'method_missing'. #189.
564         def method_missing(meth, *args)
565           if meth.to_s =~ /^(v_|c_|d_)(([\w_\?]+)(=?))$/
566             target = $1
567             method = $2
568             value  = $3
569             mode   = $4
570             if mode == '='
571               begin
572                 # set
573                 unless recipient = redaction
574                   # remove trailing '='
575                   redaction_error(meth.to_s[0..-2], "could not be set (no redaction)")
576                   return
577                 end
578                
579                 case target
580                 when 'c_'
581                   if recipient.content_class && recipient = recipient.redaction_content
582                     recipient.send(method,*args)
583                   else
584                     redaction_error(meth.to_s[0..-2], "cannot be set") # remove trailing '='
585                   end
586                 when 'd_'
587                   recipient.dyn[method[0..-2]] = args[0]
588                 else
589                   recipient.send(method,*args)
590                 end
591               rescue NoMethodError
592                 # bad attribute, just ignore
593               end
594             else
595               # read
596               recipient = version
597               if target == 'd_'
598                 version.dyn[method]
599               else
600                 recipient = recipient.content if target == 'c_'
601                 return nil unless recipient
602                 begin
603                   recipient.send(method,*args)
604                 rescue NoMethodError
605                   # bad attribute
606                   return nil
607                 end
608               end
609             end
610           else
611             super
612           end
613         end
614        
615         # Errors that occur while setting attributes from the form are recorded here.
616         def redaction_error(field, message)
617           @redaction_errors ||= []
618           @redaction_errors << [field, message]
619         end
620        
621         # Make sure the redaction is valid before we save anything.
622         def valid_redaction
623           if @version && !@version.valid?
624             merge_version_errors
625           end
626           if @redaction_errors
627             @redaction_errors.each do |k,v|
628               errors.add(k,v)
629             end
630           end
631         end
632        
633         def merge_version_errors
634           unless @version.errors.empty?
635             @version.errors.each do |k,v|
636               if k.to_s =~ /^c_/
637                 key = k.to_s
638               elsif k.to_s == 'base'
639                 key = 'base'
640               else
641                 key = "v_#{k}"
642               end
643               errors.add(key, v)
644             end
645           end
646         end
647        
648         def version_class
649           self.class.version_class
650         end
651        
652         def save_version(validations = true)
653           if validations
654             @version.save if (@version && @version.new_record?) || @redaction
655           else
656             @version.save_without_validation if (@version && @version.new_record?) || @redaction
657           end
658         end
659        
660         def set_on_create
661           # set kpath
662           self[:kpath]    = self.class.kpath
663           self[:user_id]  = visitor[:id]
664           self[:ref_lang] = visitor.lang
665           version.user_id = visitor[:id]
666           version.lang    = visitor.lang if version.lang.blank?
667           version[:status]= Zena::Status[:pub] if current_site[:auto_publish]
668           version[:publish_from] ||= Time.now if version[:status] == Zena::Status[:pub]
669           true
670         end
671        
672         public
673         module ClassMethods
674           # PUT YOUR CLASS METHODS HERE
675           
676           # This is a callback from acts_as_multiversioned
677           def version_class
678             Version
679           end
680          
681           # Find a node based on a version id
682           def version(version_id)
683             version = Version.find(version_id.to_i)
684             node = self.find(version.node_id)
685             node.version = version
686             node.eval_with_visitor 'errors.add("base", "you do not have the rights to do this") unless version.status == 50 || can_drive? || version.user_id == visitor[:id]'
687           end
688         end
689       end
690     end
691   end
692 end
693
694 ActiveRecord::Base.send :include, Zena::Acts::Multiversioned
Note: See TracBrowser for help on using the browser.