mongoid_ext 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/.document +5 -0
  2. data/Gemfile +20 -0
  3. data/Gemfile.lock +50 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +17 -0
  6. data/Rakefile +44 -0
  7. data/VERSION +1 -0
  8. data/bin/mongoid_console +85 -0
  9. data/lib/mongoid_ext.rb +71 -0
  10. data/lib/mongoid_ext/criteria_ext.rb +15 -0
  11. data/lib/mongoid_ext/document_ext.rb +29 -0
  12. data/lib/mongoid_ext/file.rb +86 -0
  13. data/lib/mongoid_ext/file_list.rb +74 -0
  14. data/lib/mongoid_ext/file_server.rb +69 -0
  15. data/lib/mongoid_ext/filter.rb +266 -0
  16. data/lib/mongoid_ext/filter/parser.rb +71 -0
  17. data/lib/mongoid_ext/filter/result_set.rb +75 -0
  18. data/lib/mongoid_ext/js/filter.js +41 -0
  19. data/lib/mongoid_ext/js/find_tags.js +26 -0
  20. data/lib/mongoid_ext/js/tag_cloud.js +28 -0
  21. data/lib/mongoid_ext/modifiers.rb +93 -0
  22. data/lib/mongoid_ext/mongo_mapper.rb +63 -0
  23. data/lib/mongoid_ext/paranoia.rb +100 -0
  24. data/lib/mongoid_ext/patches.rb +17 -0
  25. data/lib/mongoid_ext/random.rb +23 -0
  26. data/lib/mongoid_ext/slugizer.rb +84 -0
  27. data/lib/mongoid_ext/storage.rb +110 -0
  28. data/lib/mongoid_ext/tags.rb +26 -0
  29. data/lib/mongoid_ext/types/embedded_hash.rb +25 -0
  30. data/lib/mongoid_ext/types/open_struct.rb +15 -0
  31. data/lib/mongoid_ext/types/timestamp.rb +15 -0
  32. data/lib/mongoid_ext/types/translation.rb +51 -0
  33. data/lib/mongoid_ext/update.rb +11 -0
  34. data/lib/mongoid_ext/versioning.rb +189 -0
  35. data/lib/mongoid_ext/voteable.rb +104 -0
  36. data/mongoid_ext.gemspec +129 -0
  37. data/test/helper.rb +30 -0
  38. data/test/models.rb +80 -0
  39. data/test/support/custom_matchers.rb +55 -0
  40. data/test/test_filter.rb +51 -0
  41. data/test/test_modifiers.rb +65 -0
  42. data/test/test_paranoia.rb +40 -0
  43. data/test/test_random.rb +57 -0
  44. data/test/test_slugizer.rb +66 -0
  45. data/test/test_storage.rb +110 -0
  46. data/test/test_tags.rb +47 -0
  47. data/test/test_update.rb +16 -0
  48. data/test/test_versioning.rb +55 -0
  49. data/test/test_voteable.rb +77 -0
  50. data/test/types/test_open_struct.rb +22 -0
  51. data/test/types/test_set.rb +26 -0
  52. data/test/types/test_timestamp.rb +40 -0
  53. 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,15 @@
1
+ require 'ostruct'
2
+
3
+ class OpenStruct
4
+ def self.set(value)
5
+ value.nil? ? nil : value.to_hash
6
+ end
7
+
8
+ def self.get(value)
9
+ value.nil? ? nil : OpenStruct.new(value || {})
10
+ end
11
+
12
+ def to_hash
13
+ send(:table)
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ class Timestamp
2
+ def self.get(value)
3
+ if value.nil? || value == ''
4
+ nil
5
+ else
6
+ Time.zone.at(value.to_i)
7
+ end
8
+ end
9
+
10
+ def self.set(value)
11
+ value.to_i
12
+ end
13
+ end
14
+
15
+ Time.zone ||= 'UTC'
@@ -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,11 @@
1
+ module MongoidExt
2
+ module Update
3
+ def safe_update(white_list, values)
4
+ white_list.each do |key|
5
+ send("#{key}=", values[key]) if values.has_key?(key)
6
+ end
7
+ end
8
+ end
9
+ end
10
+
11
+ Mongoid::Document.send(:include, MongoidExt::Update)
@@ -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
+