cirandas.net

ref: master

vendor/plugins/acts_as_versioned/lib/acts_as_versioned.rb


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 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
 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
104
105
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
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
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
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
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
# Copyright (c) 2005 Rick Olson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
require 'active_support/concern'

module ActiveRecord #:nodoc:
  module Acts #:nodoc:
    # Specify this act if you want to save a copy of the row in a versioned table.  This assumes there is a
    # versioned table ready and that your model has a version field.  This works with optimistic locking if the lock_version
    # column is present as well.
    #
    # The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart
    # your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
    #
    #   class Page < ActiveRecord::Base
    #     # assumes pages_versions table
    #     acts_as_versioned
    #   end
    #
    # Example:
    #
    #   page = Page.create(:title => 'hello world!')
    #   page.version       # => 1
    #
    #   page.title = 'hello world'
    #   page.save
    #   page.version       # => 2
    #   page.versions.size # => 2
    #
    #   page.revert_to(1)  # using version number
    #   page.title         # => 'hello world!'
    #
    #   page.revert_to(page.versions.last) # using versioned instance
    #   page.title         # => 'hello world'
    #
    #   page.versions.earliest # efficient query to find the first version
    #   page.versions.latest   # efficient query to find the most recently created version
    #
    #
    # Simple Queries to page between versions
    #
    #   page.versions.before(version)
    #   page.versions.after(version)
    #
    # Access the previous/next versions from the versioned model itself
    #
    #   version = page.versions.latest
    #   version.previous # go back one version
    #   version.next     # go forward one version
    #
    # See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
    module Versioned
      VERSION   = "0.6.0"
      CALLBACKS = [:set_new_version, :save_version, :save_version?]

      # == Configuration options
      #
      # * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
      # * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
      # * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
      # * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI.  (default: versioned_type)
      # * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
      # * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
      # * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
      # * <tt>if</tt> - symbol of method to check before saving a new version.  If this method returns false, a new version is not saved.
      #   For finer control, pass either a Proc or modify Model#version_condition_met?
      #
      #     acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
      #
      #   or...
      #
      #     class Auction
      #       def version_condition_met? # totally bypasses the <tt>:if</tt> option
      #         !expired?
      #       end
      #     end
      #
      # * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model.  This takes
      #   either a symbol or array of symbols.
      #
      # * <tt>extend</tt> - Lets you specify a module to be mixed in both the original and versioned models.  You can also just pass a block
      #   to create an anonymous mixin:
      #
      #     class Auction
      #       acts_as_versioned do
      #         def started?
      #           !started_at.nil?
      #         end
      #       end
      #     end
      #
      #   or...
      #
      #     module AuctionExtension
      #       def started?
      #         !started_at.nil?
      #       end
      #     end
      #     class Auction
      #       acts_as_versioned :extend => AuctionExtension
      #     end
      #
      #  Example code:
      #
      #    @auction = Auction.find(1)
      #    @auction.started?
      #    @auction.versions.first.started?
      #
      # == Database Schema
      #
      # The model that you're versioning needs to have a 'version' attribute. The model is versioned
      # into a table called #{model}_versions where the model name is singlular. The _versions table should
      # contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
      #
      # A lock_version field is also accepted if your model uses Optimistic Locking.  If your table uses Single Table inheritance,
      # then that field is reflected in the versioned model as 'versioned_type' by default.
      #
      # Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
      # method, perfect for a migration.  It will also create the version column if the main model does not already have it.
      #
      #   class AddVersions < ActiveRecord::Migration
      #     def self.up
      #       # create_versioned_table takes the same options hash
      #       # that create_table does
      #       Post.create_versioned_table
      #     end
      #
      #     def self.down
      #       Post.drop_versioned_table
      #     end
      #   end
      #
      # == Changing What Fields Are Versioned
      #
      # By default, acts_as_versioned will version all but these fields:
      #
      #   [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
      #
      # You can add or change those by modifying #non_versioned_columns.  Note that this takes strings and not symbols.
      #
      #   class Post < ActiveRecord::Base
      #     acts_as_versioned
      #     self.non_versioned_columns << 'comments_count'
      #   end
      #
      def acts_as_versioned(options = {}, &extension)
        # don't allow multiple calls
        return if self.included_modules.include?(ActiveRecord::Acts::Versioned::Behaviors)

        cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
                       :version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
                       :version_association_options, :version_if_changed, :version_except_columns

        self.versioned_class_name         = options[:class_name] || "Version"
        self.versioned_foreign_key        = options[:foreign_key] || self.to_s.foreign_key
        self.versioned_table_name         = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
        self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
        self.version_column               = options[:version_column] || 'version'
        self.version_sequence_name        = options[:sequence_name]
        self.max_version_limit            = options[:limit].to_i
        self.version_condition            = options[:if] || true
        self.version_except_columns       = [options[:except]].flatten.map(&:to_s)  #these columns are kept in _versioned, but changing them does not excplitly cause a version change
        self.non_versioned_columns        = [self.primary_key, inheritance_column, self.version_column, 'lock_version', versioned_inheritance_column] #these columns are excluded from _versions, and changing them does not cause a version change
        self.version_association_options  = {
                                                    :class_name  => "#{self.to_s}::#{versioned_class_name}",
                                                    :foreign_key => versioned_foreign_key,
                                                    :dependent   => :delete_all
        }.merge(options[:association_options] || {})

        if block_given?
          extension_module_name = "#{versioned_class_name}Extension"
          silence_warnings do
            self.const_set(extension_module_name, Module.new(&extension))
          end

          options[:extend] = self.const_get(extension_module_name)
        end

        unless options[:if_changed].nil?
          self.track_altered_attributes = true
          options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
          self.version_if_changed = options[:if_changed].map(&:to_s)
        end

        include options[:extend] if options[:extend].is_a?(Module)

        include ActiveRecord::Acts::Versioned::Behaviors

        #
        # Create the dynamic versioned model
        #
        const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
          def self.reloadable?;
            false;
          end

          # find first version before the given version
          def self.before(version)
            where(["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]).
                    order('version DESC').
                    first
          end

          # find first version after the given version.
          def self.after(version)
            where(["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]).
                    order('version ASC').
                    first
          end

          # finds earliest version of this record
          def self.earliest
            order("#{original_class.version_column}").first
          end

          # find latest version of this record
          def self.latest
            order("#{original_class.version_column} desc").first
          end

          def previous
            self.class.before(self)
          end

          def next
            self.class.after(self)
          end

          def versions_count
            page.version
          end
        end

        versioned_class.cattr_accessor :original_class
        versioned_class.original_class = self
        versioned_class.table_name = versioned_table_name
        versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
                                   :class_name  => "::#{self.to_s}",
                                   :foreign_key => versioned_foreign_key
        versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
        versioned_class.set_sequence_name version_sequence_name if version_sequence_name
      end

      module Behaviors
        extend ActiveSupport::Concern

        included do
          has_many :versions, self.version_association_options

          before_save :set_new_version
          after_save :save_version
          after_save :clear_old_versions
        end

        # Saves a version of the model in the versioned table.  This is called in the after_save callback by default
        def save_version
          if @saving_version
            @saving_version = nil
            rev = self.class.versioned_class.new
            clone_versioned_model(self, rev)
            rev.send("#{self.class.version_column}=", send(self.class.version_column))
            rev.send("#{self.class.versioned_foreign_key}=", id)
            rev.save
          end
        end

        # Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
        # Override this method to set your own criteria for clearing old versions.
        def clear_old_versions
          return if self.class.max_version_limit == 0
          excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
          if excess_baggage > 0
            self.class.versioned_class.delete_all ["#{self.class.version_column} <= ? and #{self.class.versioned_foreign_key} = ?", excess_baggage, id]
          end
        end

        # Reverts a model to a given version.  Takes either a version number or an instance of the versioned model
        def revert_to(version)
          if version.is_a?(self.class.versioned_class)
            return false unless version.send(self.class.versioned_foreign_key) == id and !version.new_record?
          else
            return false unless version = versions.where(self.class.version_column => version).first
          end
          self.clone_versioned_model(version, self)
          send("#{self.class.version_column}=", version.send(self.class.version_column))
          true
        end

        # Reverts a model to a given version and saves the model.
        # Takes either a version number or an instance of the versioned model
        def revert_to!(version)
          revert_to(version) ? save_without_revision : false
        end

        # Temporarily turns off Optimistic Locking while saving.  Used when reverting so that a new version is not created.
        def save_without_revision
          save_without_revision!
          true
        rescue
          false
        end

        def save_without_revision!
          without_locking do
            without_revision do
              save!
            end
          end
        end

        def altered?
          changed.map { |c| self.class.versioned_columns.map(&:name).include?(c) & !self.class.version_except_columns.include?(c) }.any?
        end

        # Clones a model.  Used when saving a new version or reverting a model's version.
        def clone_versioned_model(orig_model, new_model)
          self.class.versioned_columns.each do |col|
            next unless orig_model.has_attribute?(col.name)
            define_method(new_model, col.name.to_sym)
            new_model.send("#{col.name.to_sym}=", orig_model.send(col.name))
          end

          if orig_model.is_a?(self.class.versioned_class)
            new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
          elsif new_model.is_a?(self.class.versioned_class)
            sym = self.class.versioned_inheritance_column.to_sym
            define_method new_model, sym
            new_model.send("#{sym}=", orig_model[orig_model.class.inheritance_column]) if orig_model[orig_model.class.inheritance_column]
          end
        end

        def define_method(object, method)
          return if object.methods.include? method
          metaclass = class << object; self; end
          metaclass.send :attr_accessor, method
        end

        # Checks whether a new version shall be saved or not.  Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
        def save_version?
          version_condition_met? && altered?
        end

        # Checks condition set in the :if option to check whether a revision should be created or not.  Override this for
        # custom version condition checking.
        def version_condition_met?
          case
            when version_condition.is_a?(Symbol)
              send(version_condition)
            when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
              version_condition.call(self)
            else
              version_condition
          end
        end

        # Executes the block with the versioning callbacks disabled.
        #
        #   @foo.without_revision do
        #     @foo.save
        #   end
        #
        def without_revision(&block)
          self.class.without_revision(&block)
        end

        # Turns off optimistic locking for the duration of the block
        #
        #   @foo.without_locking do
        #     @foo.save
        #   end
        #
        def without_locking(&block)
          self.class.without_locking(&block)
        end

        def empty_callback()
        end

        #:nodoc:

        protected
        # sets the new version before saving, unless you're using optimistic locking.  In that case, let it take care of the version.
        def set_new_version
          @saving_version = new_record? || save_version?
          self.send("#{self.class.version_column}=", next_version) if new_record? || (!locking_enabled? && save_version?)
        end

        # Gets the next available version for the current record, or 1 for a new record
        def next_version
          (new_record? ? 0 : versions.calculate(:maximum, version_column).to_i) + 1
        end


        module ClassMethods
          # Returns an array of columns that are versioned.  See non_versioned_columns
          def versioned_columns
            @versioned_columns ||= columns.select { |c| !non_versioned_columns.include?(c.name) }
          end

          # Returns an instance of the dynamic versioned model
          def versioned_class
            const_get versioned_class_name
          end

          # Rake migration task to create the versioned table using options passed to acts_as_versioned
          def create_versioned_table(create_table_options = {})
            # create version column in main table if it does not exist
            if !self.content_columns.find { |c| [version_column.to_s, 'lock_version'].include? c.name }
              self.connection.add_column table_name, version_column, :integer
              self.reset_column_information
            end

            return if connection.table_exists?(versioned_table_name)

            self.connection.create_table(versioned_table_name, create_table_options) do |t|
              t.column versioned_foreign_key, :integer
              t.column version_column, :integer
            end

            self.versioned_columns.each do |col|
              self.connection.add_column versioned_table_name, col.name, col.type,
                                         :limit     => col.limit,
                                         :default   => col.default,
                                         :scale     => col.scale,
                                         :precision => col.precision
            end

            if type_col = self.columns_hash[inheritance_column]
              self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
                                         :limit     => type_col.limit,
                                         :default   => type_col.default,
                                         :scale     => type_col.scale,
                                         :precision => type_col.precision
            end

            self.connection.add_index versioned_table_name, versioned_foreign_key
          end

          # Rake migration task to drop the versioned table
          def drop_versioned_table
            self.connection.drop_table versioned_table_name
          end

          # Executes the block with the versioning callbacks disabled.
          #
          #   Foo.without_revision do
          #     @foo.save
          #   end
          #
          def without_revision(&block)
            class_eval do
              CALLBACKS.each do |attr_name|
                alias_method "orig_#{attr_name}".to_sym, attr_name
                alias_method attr_name, :empty_callback
              end
            end
            block.call
          ensure
            class_eval do
              CALLBACKS.each do |attr_name|
                alias_method attr_name, "orig_#{attr_name}".to_sym
              end
            end
          end

          # Turns off optimistic locking for the duration of the block
          #
          #   Foo.without_locking do
          #     @foo.save
          #   end
          #
          def without_locking(&block)
            current = ActiveRecord::Base.lock_optimistically
            ActiveRecord::Base.lock_optimistically = false if current
            begin
              block.call
            ensure
              ActiveRecord::Base.lock_optimistically = true if current
            end
          end
        end
      end
    end
  end
end

ActiveRecord::Base.extend ActiveRecord::Acts::Versioned