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
|
+
|