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