mongoid_ext 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.document +5 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +50 -0
- data/LICENSE +20 -0
- data/README.rdoc +17 -0
- data/Rakefile +44 -0
- data/VERSION +1 -0
- data/bin/mongoid_console +85 -0
- data/lib/mongoid_ext.rb +71 -0
- data/lib/mongoid_ext/criteria_ext.rb +15 -0
- data/lib/mongoid_ext/document_ext.rb +29 -0
- data/lib/mongoid_ext/file.rb +86 -0
- data/lib/mongoid_ext/file_list.rb +74 -0
- data/lib/mongoid_ext/file_server.rb +69 -0
- data/lib/mongoid_ext/filter.rb +266 -0
- data/lib/mongoid_ext/filter/parser.rb +71 -0
- data/lib/mongoid_ext/filter/result_set.rb +75 -0
- data/lib/mongoid_ext/js/filter.js +41 -0
- data/lib/mongoid_ext/js/find_tags.js +26 -0
- data/lib/mongoid_ext/js/tag_cloud.js +28 -0
- data/lib/mongoid_ext/modifiers.rb +93 -0
- data/lib/mongoid_ext/mongo_mapper.rb +63 -0
- data/lib/mongoid_ext/paranoia.rb +100 -0
- data/lib/mongoid_ext/patches.rb +17 -0
- data/lib/mongoid_ext/random.rb +23 -0
- data/lib/mongoid_ext/slugizer.rb +84 -0
- data/lib/mongoid_ext/storage.rb +110 -0
- data/lib/mongoid_ext/tags.rb +26 -0
- data/lib/mongoid_ext/types/embedded_hash.rb +25 -0
- data/lib/mongoid_ext/types/open_struct.rb +15 -0
- data/lib/mongoid_ext/types/timestamp.rb +15 -0
- data/lib/mongoid_ext/types/translation.rb +51 -0
- data/lib/mongoid_ext/update.rb +11 -0
- data/lib/mongoid_ext/versioning.rb +189 -0
- data/lib/mongoid_ext/voteable.rb +104 -0
- data/mongoid_ext.gemspec +129 -0
- data/test/helper.rb +30 -0
- data/test/models.rb +80 -0
- data/test/support/custom_matchers.rb +55 -0
- data/test/test_filter.rb +51 -0
- data/test/test_modifiers.rb +65 -0
- data/test/test_paranoia.rb +40 -0
- data/test/test_random.rb +57 -0
- data/test/test_slugizer.rb +66 -0
- data/test/test_storage.rb +110 -0
- data/test/test_tags.rb +47 -0
- data/test/test_update.rb +16 -0
- data/test/test_versioning.rb +55 -0
- data/test/test_voteable.rb +77 -0
- data/test/types/test_open_struct.rb +22 -0
- data/test/types/test_set.rb +26 -0
- data/test/types/test_timestamp.rb +40 -0
- metadata +301 -0
| @@ -0,0 +1,23 @@ | |
| 1 | 
            +
            module MongoidExt
         | 
| 2 | 
            +
              module Random
         | 
| 3 | 
            +
                extend ActiveSupport::Concern
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                included do
         | 
| 6 | 
            +
                  field :_random, :type => Float, :default => lambda{rand}
         | 
| 7 | 
            +
                  field :_random_times, :type => Float, :default => 0.0
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  index :_random
         | 
| 10 | 
            +
                  index :_random_times
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                module ClassMethods
         | 
| 14 | 
            +
                  def random(conditions = {})
         | 
| 15 | 
            +
                    r = rand()
         | 
| 16 | 
            +
                    doc = self.where(conditions.merge(:_random.gte => r)).order_by(:_random_times.asc, :_random.asc).first ||
         | 
| 17 | 
            +
                          self.where(conditions.merge(:_random.lte => r)).order_by(:_random_times.asc, :_random.asc).first
         | 
| 18 | 
            +
                    doc.inc(:_random_times, 1.0) if doc
         | 
| 19 | 
            +
                    doc
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
            end
         | 
| @@ -0,0 +1,84 @@ | |
| 1 | 
            +
            # @author David Cuadrado
         | 
| 2 | 
            +
            module MongoidExt
         | 
| 3 | 
            +
              # Slugizes a given key
         | 
| 4 | 
            +
              # Usage:
         | 
| 5 | 
            +
              #   class MyModel
         | 
| 6 | 
            +
              #     include Mongoid::Document
         | 
| 7 | 
            +
              #     include MongoidExt::Slugizer
         | 
| 8 | 
            +
              #     field :name
         | 
| 9 | 
            +
              #     slug_key :name, :callback_type => :before_save
         | 
| 10 | 
            +
              #   end
         | 
| 11 | 
            +
              #
         | 
| 12 | 
            +
              module Slugizer
         | 
| 13 | 
            +
                def self.included(klass)
         | 
| 14 | 
            +
                  klass.class_eval do
         | 
| 15 | 
            +
                    extend ClassMethods
         | 
| 16 | 
            +
                    extend Finder
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    field :slug, :type => String, :index => true
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def to_param
         | 
| 23 | 
            +
                  self.slug.blank? ? self.id.to_s : self.slug
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                protected
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def generate_slug
         | 
| 29 | 
            +
                  return false if self[self.class.slug_key].blank?
         | 
| 30 | 
            +
                  max_length = self.class.slug_options[:max_length]
         | 
| 31 | 
            +
                  min_length = self.class.slug_options[:min_length] || 0
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  slug = self[self.class.slug_key].parameterize.to_s
         | 
| 34 | 
            +
                  slug = slug[0, max_length] if max_length
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  if slug.size < min_length
         | 
| 37 | 
            +
                    slug = nil
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  if slug && self.class.slug_options[:add_prefix]
         | 
| 41 | 
            +
                    key = UUIDTools::UUID.random_create.hexdigest[0,4] #optimize
         | 
| 42 | 
            +
                    self.slug = key+"-"+slug
         | 
| 43 | 
            +
                  else
         | 
| 44 | 
            +
                    self.slug = slug
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                module ClassMethods
         | 
| 49 | 
            +
                  # marks a field as sluggable (default key is :name)
         | 
| 50 | 
            +
                  # == Parameters
         | 
| 51 | 
            +
                  # @param [Symbol] key the field to be slugized
         | 
| 52 | 
            +
                  # @param [Hash] options options to configure the process
         | 
| 53 | 
            +
                  # @return 
         | 
| 54 | 
            +
                  #   
         | 
| 55 | 
            +
                  def slug_key(key = :name, options = {})
         | 
| 56 | 
            +
                    @slug_options ||= options
         | 
| 57 | 
            +
                    @callback_type ||= begin
         | 
| 58 | 
            +
                      type = options[:callback_type] || :before_validation
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                      send(type, :generate_slug)
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                      type
         | 
| 63 | 
            +
                    end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                    @slug_key ||= key
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
                  class_eval do
         | 
| 68 | 
            +
                    attr_reader :slug_options
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                module Finder
         | 
| 73 | 
            +
                  # finds a document by slug or id
         | 
| 74 | 
            +
                  # @param [Strig] id slug or id
         | 
| 75 | 
            +
                  # @param [Hash] options additional conditions
         | 
| 76 | 
            +
                  def by_slug(id, options = {})
         | 
| 77 | 
            +
                    self.where(options.merge({:slug => id})).first || self.where(options.merge({:_id => id})).first
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
                  alias :find_by_slug_or_id :by_slug
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
              end
         | 
| 82 | 
            +
            end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
            Mongoid::Criteria.send(:include, MongoidExt::Slugizer::Finder)
         | 
| @@ -0,0 +1,110 @@ | |
| 1 | 
            +
            module MongoidExt
         | 
| 2 | 
            +
              module Storage
         | 
| 3 | 
            +
                def self.included(model)
         | 
| 4 | 
            +
                  model.class_eval do
         | 
| 5 | 
            +
                    extend ClassMethods
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                    validate :add_mm_storage_errors
         | 
| 8 | 
            +
                    file_list :file_list
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def put_file(name, io, options = {})
         | 
| 13 | 
            +
                  file_list = send(options.delete(:in) || :file_list)
         | 
| 14 | 
            +
                  file_list.put(name, io, options)
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def fetch_file(name, options = {})
         | 
| 18 | 
            +
                  file_list = send(options.delete(:in) || :file_list)
         | 
| 19 | 
            +
                  file_list.get(name)
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def delete_file(id, options = {})
         | 
| 23 | 
            +
                  file_list = send(options.delete(:in) || :file_list)
         | 
| 24 | 
            +
                  file_list.delete(id)
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def files(options = {})
         | 
| 28 | 
            +
                  file_list = send(options.delete(:in) || :file_list)
         | 
| 29 | 
            +
                  file_list.files
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                def mm_storage_errors
         | 
| 33 | 
            +
                  @mm_storage_errors ||= {}
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def add_mm_storage_errors
         | 
| 37 | 
            +
                  mm_storage_errors.each do |k, msgs|
         | 
| 38 | 
            +
                    msgs.each do |msg|
         | 
| 39 | 
            +
                      self.errors.add(k, msg)
         | 
| 40 | 
            +
                    end
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                module ClassMethods
         | 
| 45 | 
            +
                  def gridfs
         | 
| 46 | 
            +
                    @gridfs ||= Mongo::Grid.new(self.db)
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  def file_list(name)
         | 
| 50 | 
            +
                    field name, :type => MongoidExt::FileList
         | 
| 51 | 
            +
                    define_method(name) do
         | 
| 52 | 
            +
                      list = self[name]
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                      if list.nil?
         | 
| 55 | 
            +
                        list = self[name] = MongoidExt::FileList.new
         | 
| 56 | 
            +
                      elsif list.class == BSON::OrderedHash || list.class == Hash
         | 
| 57 | 
            +
                        list = self[name] = MongoidExt::FileList.new(list)
         | 
| 58 | 
            +
                      end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                      list.parent_document = self
         | 
| 61 | 
            +
                      list
         | 
| 62 | 
            +
                    end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                    set_callback(:create, :after) do |doc|
         | 
| 65 | 
            +
                      l = doc.send(name)
         | 
| 66 | 
            +
                      l.sync_files
         | 
| 67 | 
            +
                      doc.save(:validate => false)
         | 
| 68 | 
            +
                    end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                    set_callback(:destroy, :before) do |doc|
         | 
| 71 | 
            +
                      doc.send(name).destroy_files
         | 
| 72 | 
            +
                    end
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  def file_key(name, opts = {})
         | 
| 76 | 
            +
                    opts[:in] ||= :file_list
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                    define_method("#{name}=") do |file|
         | 
| 79 | 
            +
                      if opts[:max_length] && file.respond_to?(:size) && file.size > opts[:max_length]
         | 
| 80 | 
            +
                        errors.add(name, I18n.t("mongoid_ext.storage.errors.max_length", :default => "file is too long. max length is #{opts[:max_length]} bytes"))
         | 
| 81 | 
            +
                      end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                      if cb = opts[:validate]
         | 
| 84 | 
            +
                        if cb.kind_of?(Symbol)
         | 
| 85 | 
            +
                          send(opts[:validate], file)
         | 
| 86 | 
            +
                        elsif cb.kind_of?(Proc)
         | 
| 87 | 
            +
                          cb.call(file)
         | 
| 88 | 
            +
                        end
         | 
| 89 | 
            +
                      end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                      if self.errors[name].blank?
         | 
| 92 | 
            +
                        send(opts[:in]).get(name.to_s).put(name.to_s, file)
         | 
| 93 | 
            +
                      else
         | 
| 94 | 
            +
                        # we store the errors here because we want to validate before storing the file
         | 
| 95 | 
            +
                        mm_storage_errors.merge!(errors.errors)
         | 
| 96 | 
            +
                      end
         | 
| 97 | 
            +
                    end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                    define_method(name) do
         | 
| 100 | 
            +
                      send(opts[:in]).get(name.to_s)
         | 
| 101 | 
            +
                    end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                    define_method("has_#{name}?") do
         | 
| 104 | 
            +
                      send(opts[:in]).has_key?(name.to_s)
         | 
| 105 | 
            +
                    end
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
                  private
         | 
| 108 | 
            +
                end
         | 
| 109 | 
            +
              end
         | 
| 110 | 
            +
            end
         | 
| @@ -0,0 +1,26 @@ | |
| 1 | 
            +
            module MongoidExt
         | 
| 2 | 
            +
              module Tags
         | 
| 3 | 
            +
                def self.included(klass)
         | 
| 4 | 
            +
                  klass.class_eval do
         | 
| 5 | 
            +
                    extend ClassMethods
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                    field :tags, :type => Array, :index => true, :default => []
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                module ClassMethods
         | 
| 12 | 
            +
                  def tag_cloud(conditions = {}, limit = 30)
         | 
| 13 | 
            +
                    self.db.nolock_eval("function(collection, q,l) { return tag_cloud(collection, q,l); }", self.collection_name, conditions, limit)
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  # Model.find_with_tags("budget", "big").limit(4)
         | 
| 17 | 
            +
                  def find_with_tags(*tags)
         | 
| 18 | 
            +
                    self.all(:conditions => {:tags.in => tags})
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  def find_tags(regex, conditions = {}, limit = 30)
         | 
| 22 | 
            +
                    self.db.nolock_eval("function(collection, a,b,c) { return find_tags(collection, a,b,c); }", self.collection_name, regex, conditions, limit)
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
            end
         | 
| @@ -0,0 +1,25 @@ | |
| 1 | 
            +
            class EmbeddedHash < Hash
         | 
| 2 | 
            +
              include ActiveModel::Validations
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              def initialize(other = {})
         | 
| 5 | 
            +
                other.each do |k,v|
         | 
| 6 | 
            +
                  self[k] = v
         | 
| 7 | 
            +
                end
         | 
| 8 | 
            +
                self["_id"] ||= BSON::ObjectId.new.to_s
         | 
| 9 | 
            +
              end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              def self.field(name, opts = {})
         | 
| 12 | 
            +
                define_method(name) do
         | 
| 13 | 
            +
                  self[name.to_s] ||= opts[:default].kind_of?(Proc) ? opts[:default].call : opts[:default]
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                define_method("#{name}=") do |v|
         | 
| 17 | 
            +
                  self[name.to_s] = v
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
              def id
         | 
| 22 | 
            +
                  self["_id"]
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
              alias :_id :id
         | 
| 25 | 
            +
            end
         | 
| @@ -0,0 +1,51 @@ | |
| 1 | 
            +
            class Translation < String
         | 
| 2 | 
            +
              attr_accessor :keys
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              def initialize(*args)
         | 
| 5 | 
            +
                super
         | 
| 6 | 
            +
                @keys = {}
         | 
| 7 | 
            +
                @keys["default"] = "en"
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              def []=(lang, text)
         | 
| 11 | 
            +
                @keys[lang.to_s] = text
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              def [](lang)
         | 
| 15 | 
            +
                @keys[lang.to_s]
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              def languages
         | 
| 19 | 
            +
                langs = @keys.keys
         | 
| 20 | 
            +
                langs.delete("default")
         | 
| 21 | 
            +
                langs
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
              def default_language=(lang)
         | 
| 25 | 
            +
                @keys["default"] = lang
         | 
| 26 | 
            +
                self.replace(@keys[lang.to_s])
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
              def self.build(keys, default = "en")
         | 
| 30 | 
            +
                tr = self.new
         | 
| 31 | 
            +
                tr.keys = keys
         | 
| 32 | 
            +
                tr.default_language = default
         | 
| 33 | 
            +
                tr
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
              def self.set(value)
         | 
| 37 | 
            +
                return value.keys if value.kind_of?(self)
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                @keys
         | 
| 40 | 
            +
              end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
              def self.get(value)
         | 
| 43 | 
            +
                return value if value.kind_of?(self)
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                result = self.new
         | 
| 46 | 
            +
                result.keys = value
         | 
| 47 | 
            +
                result.default_language = value["default"] || "en"
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                result
         | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
            end
         | 
| @@ -0,0 +1,189 @@ | |
| 1 | 
            +
            module MongoidExt
         | 
| 2 | 
            +
            module Versioning
         | 
| 3 | 
            +
              def self.included(klass)
         | 
| 4 | 
            +
                klass.class_eval do
         | 
| 5 | 
            +
                  extend ClassMethods
         | 
| 6 | 
            +
                  include InstanceMethods
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  cattr_accessor :versionable_options
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  attr_accessor :rolling_back
         | 
| 11 | 
            +
                  field :version_message
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  field :versions_count, :type => Integer, :default => 0
         | 
| 14 | 
            +
                  field :version_ids, :type => Array, :default => []
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  before_save :save_version, :if => Proc.new { |d| !d.rolling_back }
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              module InstanceMethods
         | 
| 21 | 
            +
                def rollback!(pos = nil)
         | 
| 22 | 
            +
                  pos = self.versions_count-1 if pos.nil?
         | 
| 23 | 
            +
                  version = self.version_at(pos)
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  if version
         | 
| 26 | 
            +
                    version.data.each do |key, value|
         | 
| 27 | 
            +
                      self.send("#{key}=", value)
         | 
| 28 | 
            +
                    end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    owner_field = self.class.versionable_options[:owner_field]
         | 
| 31 | 
            +
                    self[owner_field] = version[owner_field] if !self.changes.include?(owner_field)
         | 
| 32 | 
            +
                    self.updated_at = version.date if self.respond_to?(:updated_at) && !self.updated_at_changed?
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  @rolling_back = true
         | 
| 36 | 
            +
                  save!
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                def load_version(pos = nil)
         | 
| 40 | 
            +
                  pos = self.versions_count-1 if pos.nil?
         | 
| 41 | 
            +
                  version = self.version_at(pos)
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  if version
         | 
| 44 | 
            +
                    version.data.each do |key, value|
         | 
| 45 | 
            +
                      self.send("#{key}=", value)
         | 
| 46 | 
            +
                    end
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                def diff(key, pos1, pos2, format = :html)
         | 
| 51 | 
            +
                  version1 = self.version_at(pos1)
         | 
| 52 | 
            +
                  version2 = self.version_at(pos2)
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  Differ.diff_by_word(version1.content(key), version2.content(key)).format_as(format).html_safe
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                def current_version
         | 
| 58 | 
            +
                  version_klass.new(:data => self.attributes, self.class.versionable_options[:owner_field] => (self.updated_by_id_was || self.updated_by_id), :created_at => Time.now)
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def version_at(pos)
         | 
| 62 | 
            +
                  case pos.to_s
         | 
| 63 | 
            +
                  when "current"
         | 
| 64 | 
            +
                    current_version
         | 
| 65 | 
            +
                  when "first"
         | 
| 66 | 
            +
                    version_klass.find(self.version_ids.first)
         | 
| 67 | 
            +
                  when "last"
         | 
| 68 | 
            +
                    version_klass.find(self.version_ids.last)
         | 
| 69 | 
            +
                  else
         | 
| 70 | 
            +
                    if version_id = self.version_ids[pos]
         | 
| 71 | 
            +
                      version_klass.find(self.version_ids[pos])
         | 
| 72 | 
            +
                    end
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                def versions
         | 
| 77 | 
            +
                  version_klass.where(:target_id => self.id)
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                def version_klass
         | 
| 81 | 
            +
                  self.class.version_klass
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
              end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
              module ClassMethods
         | 
| 86 | 
            +
                def version_klass
         | 
| 87 | 
            +
                  parent_klass = self
         | 
| 88 | 
            +
                  @version_klass ||= Class.new do
         | 
| 89 | 
            +
                    include Mongoid::Document
         | 
| 90 | 
            +
                    include Mongoid::Timestamps
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                    cattr_accessor :parent_class
         | 
| 93 | 
            +
                    self.parent_class = parent_klass
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                    self.collection_name = "#{self.parent_class.collection_name}.versions"
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                    identity :type => String
         | 
| 98 | 
            +
                    field :message, :type => String
         | 
| 99 | 
            +
                    field :data, :type => Hash
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                    referenced_in :owner, :class_name => parent_klass.versionable_options[:user_class]
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                    referenced_in :target, :polymorphic => true
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                    after_create :add_version
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                    validates_presence_of :target_id
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                    def content(key)
         | 
| 110 | 
            +
                      cdata = self.data[key]
         | 
| 111 | 
            +
                      if cdata.respond_to?(:join)
         | 
| 112 | 
            +
                        cdata.join(" ")
         | 
| 113 | 
            +
                      else
         | 
| 114 | 
            +
                        cdata || ""
         | 
| 115 | 
            +
                      end
         | 
| 116 | 
            +
                    end
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                    private
         | 
| 119 | 
            +
                    def add_version
         | 
| 120 | 
            +
                      self.class.parent_class.push({:_id => self.target_id}, {:version_ids => self.id})
         | 
| 121 | 
            +
                      self.class.parent_class.increment({:_id => self.target_id}, {:versions_count => 1})
         | 
| 122 | 
            +
                    end
         | 
| 123 | 
            +
                  end
         | 
| 124 | 
            +
                end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                # example:
         | 
| 127 | 
            +
                #     class Foo
         | 
| 128 | 
            +
                #       include Mongoid::Document
         | 
| 129 | 
            +
                #       include MongoidExt::Versioning
         | 
| 130 | 
            +
                #       versionable_keys :field1, :field2, :field3, :user_class => "Customer", :owner_field => "updated_by_id"
         | 
| 131 | 
            +
                #       ...
         | 
| 132 | 
            +
                #     end
         | 
| 133 | 
            +
                #
         | 
| 134 | 
            +
                def versionable_keys(*keys)
         | 
| 135 | 
            +
                  self.versionable_options = keys.extract_options!
         | 
| 136 | 
            +
                  self.versionable_options[:owner_field] ||= "user_id"
         | 
| 137 | 
            +
                  self.versionable_options[:owner_field] = self.versionable_options[:owner_field].to_s
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                  relationship = self.relations[self.versionable_options[:owner_field].sub(/_id$/, "")]
         | 
| 140 | 
            +
                  if !relationship
         | 
| 141 | 
            +
                    raise ArgumentError, "the supplied :owner_field => #{self.versionable_options[:owner_field].inspect} option is invalid"
         | 
| 142 | 
            +
                  end
         | 
| 143 | 
            +
                  self.versionable_options[:user_class] = relationship.class_name
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                  define_method(:save_version) do
         | 
| 146 | 
            +
                    data = {}
         | 
| 147 | 
            +
                    message = ""
         | 
| 148 | 
            +
                    keys.each do |key|
         | 
| 149 | 
            +
                      if change = changes[key.to_s]
         | 
| 150 | 
            +
                        data[key.to_s] = change.first
         | 
| 151 | 
            +
                      else
         | 
| 152 | 
            +
                        data[key.to_s] = self[key]
         | 
| 153 | 
            +
                      end
         | 
| 154 | 
            +
                    end
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                    if message_changes = self.changes["version_message"]
         | 
| 157 | 
            +
                      message = message_changes.first
         | 
| 158 | 
            +
                    else
         | 
| 159 | 
            +
                      version_message = ""
         | 
| 160 | 
            +
                    end
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                    uuser_id = send(self.versionable_options[:owner_field]+"_was")||send(self.versionable_options[:owner_field])
         | 
| 163 | 
            +
                    if !self.new? && !data.empty? && uuser_id
         | 
| 164 | 
            +
                      max_versions = self.versionable_options[:max_versions].to_i
         | 
| 165 | 
            +
                      if self.version_ids.size >= max_versions
         | 
| 166 | 
            +
                        old = self.version_ids.slice!(0, max_versions)
         | 
| 167 | 
            +
                        self.class.skip_callback(:save, :before, :save_version)
         | 
| 168 | 
            +
                        self.version_klass.delete_all(:_ids => old)
         | 
| 169 | 
            +
                        self.save
         | 
| 170 | 
            +
                        self.class.set_callback(:save, :before, :save_version)
         | 
| 171 | 
            +
                      end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                      self.version_klass.create({
         | 
| 174 | 
            +
                        'data' => data,
         | 
| 175 | 
            +
                        'owner_id' => uuser_id,
         | 
| 176 | 
            +
                        'target' => self,
         | 
| 177 | 
            +
                        'message' => message
         | 
| 178 | 
            +
                      })
         | 
| 179 | 
            +
                    end
         | 
| 180 | 
            +
                  end
         | 
| 181 | 
            +
             | 
| 182 | 
            +
                  define_method(:versioned_keys) do
         | 
| 183 | 
            +
                    keys
         | 
| 184 | 
            +
                  end
         | 
| 185 | 
            +
                end
         | 
| 186 | 
            +
              end
         | 
| 187 | 
            +
            end
         | 
| 188 | 
            +
            end
         | 
| 189 | 
            +
             |