mongoid_ext 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|