mongoid_taggable_with_context 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -1,6 +1,6 @@
1
1
  == Mongoid Taggable With Context
2
2
 
3
- A tagging plugin for Mongoid that allows for custom tagging along dynamic contexts. This plugin was originally based on Mongoid Taggable by Wilker Lúcio and Ches Martin. It has evolved substantially since that point, but all credit goes to them for the initial tagging functionality.
3
+ A tagging lib for Mongoid that allows for custom tagging along dynamic contexts. This lib was originally based on Mongoid Taggable by Wilker Lúcio and Ches Martin. It has evolved substantially since that point, but all credit goes to them for the initial tagging functionality.
4
4
 
5
5
  For instance, in a social network, a user might have tags that are called skills, interests, sports, and more. There is no real way to differentiate between tags and so an implementation of this type is not possible with Mongoid Taggable.
6
6
 
@@ -8,7 +8,7 @@ Another example, aggregation such as counting tag occurrences was achieved by ma
8
8
 
9
9
  Enter Mongoid Taggable With Context. Rather than tying functionality to a specific keyword (namely "tags"), Mongoid Taggable With Context allows you to specify an arbitrary number of tag "contexts" that can be used locally or in combination in the same way Mongoid Taggable was used.
10
10
 
11
- Mongoid Taggable With Context implements tag aggregation without map-reduce, instead it quickly adjusts the aggregation collection whenever tags are inserted or removed with $inc operator. So performance won't be impacted as the number of tags and documents grow.
11
+ Mongoid Taggable With Context also provides flexibility on aggregation strategy. In addition to the map-reduce strategy, Mongoid Taggable With Context also comes with real-time strategy. By using real-time strategy, your document can quickly adjusts the aggregation collection whenever tags are inserted or removed with $inc operator. So performance won't be impacted as the number of tags and documents grow.
12
12
 
13
13
  == Installation
14
14
 
@@ -80,9 +80,45 @@ Then in your form, for example:
80
80
  <% end %>
81
81
 
82
82
 
83
- == Tag Aggregation with Counts
83
+ == Aggregation Strategies
84
84
 
85
- This lib can automatically create aggregate collections of contexts and their counts for you. Each aggregate collection is scoped to the document collection type and context.
85
+ By including an aggregation strategy in your document, tag aggregations will be automatically available to you.
86
+ This lib presents the following aggregation strategies:
87
+
88
+ * MapReduce
89
+ * RealTime
90
+
91
+ The following document will automatically aggregate counts on all tag contexts.
92
+
93
+ class Post
94
+ include Mongoid::Document
95
+ include Mongoid::TaggableWithContext
96
+
97
+ # automatically adds real time aggregations to all tag contexts
98
+ include Mongoid::TaggableWithContext::AggregationStrategy::RealTime
99
+
100
+ # alternatively for map-reduce
101
+ # include Mongoid::TaggableWithContext::AggregationStrategy::MapReduce
102
+
103
+ field :title
104
+ field :content
105
+
106
+ taggable
107
+ taggable :interests
108
+ taggable :skills, :separator => ','
109
+ end
110
+
111
+ When you include an aggregation strategy, your document also gains a few extra methods to retrieve aggregation data.
112
+ In the case of previous example the following methods are included:
113
+
114
+ Post.tags
115
+ Post.tags_with_weight
116
+ Post.interests
117
+ Post.interests_with_weight
118
+ Post.skills
119
+ Post.skills_with_weight
120
+
121
+ Here is how to use these methods in more detail:
86
122
 
87
123
  Post.create!(:tags => "food,ant,bee")
88
124
  Post.create!(:tags => "juice,food,bee,zip")
@@ -131,3 +167,8 @@ To see the test coverage, you need to be on ruby 1.8.7 (since latest rcov doesn'
131
167
  and run the following command:
132
168
 
133
169
  rake rcov
170
+
171
+ == Next Up
172
+
173
+ * More documentation.
174
+ * More Strategies. ( Let me know what kind of strategies you want, or contribute your own. )
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.0
1
+ 0.6.0
data/init.rb CHANGED
@@ -1 +1 @@
1
- require 'mongoid/taggable_with_context'
1
+ require 'mongoid_taggable_with_context'
@@ -0,0 +1,162 @@
1
+ # Copyright (c) 2010 Wilker Lúcio <wilkerlucio@gmail.com>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Mongoid::TaggableWithContext
16
+ extend ActiveSupport::Concern
17
+
18
+ class AggregationStrategyMissing < Exception; end
19
+
20
+ included do
21
+ class_inheritable_reader :taggable_with_context_options
22
+ write_inheritable_attribute(:taggable_with_context_options, {})
23
+ delegate "convert_string_to_array", :to => 'self.class'
24
+ delegate "convert_array_to_string", :to => 'self.class'
25
+ delegate "get_tag_separator_for", :to => 'self.class'
26
+ delegate "tag_contexts", :to => 'self.class'
27
+ delegate "tag_options_for", :to => 'self.class'
28
+ end
29
+
30
+ module ClassMethods
31
+ # Macro to declare a document class as taggable, specify field name
32
+ # for tags, and set options for tagging behavior.
33
+ #
34
+ # @example Define a taggable document.
35
+ #
36
+ # class Article
37
+ # include Mongoid::Document
38
+ # include Mongoid::Taggable
39
+ # taggable :keywords, :separator => ' ', :aggregation => true, :default_type => "seo"
40
+ # end
41
+ #
42
+ # @param [ Symbol ] field The name of the field for tags.
43
+ # @param [ Hash ] options Options for taggable behavior.
44
+ #
45
+ # @option options [ String ] :separator The tag separator to
46
+ # convert from; defaults to ','
47
+ # @option options [ true, false ] :aggregation Whether or not to
48
+ # aggregate counts of tags within the document collection using
49
+ # map/reduce; defaults to false
50
+ # @option options [ String ] :default_type The default type of the tag.
51
+ # Each tag can optionally have a tag type. The default type is nil
52
+ def taggable(*args)
53
+ # init variables
54
+ options = args.extract_options!
55
+ tags_field = (args.blank? ? :tags : args.shift).to_sym
56
+ options.reverse_merge!(
57
+ :separator => ' ',
58
+ :array_field => "#{tags_field}_array".to_sym
59
+ )
60
+ tags_array_field = options[:array_field]
61
+
62
+ # register / update settings
63
+ class_options = taggable_with_context_options || {}
64
+ class_options[tags_field] = options
65
+ write_inheritable_attribute(:taggable_with_context_options, class_options)
66
+
67
+ # setup fields & indexes
68
+ field tags_field, :default => ""
69
+ field tags_array_field, :type => Array, :default => []
70
+ index tags_array_field
71
+
72
+ # singleton methods
73
+ class_eval <<-END
74
+ class << self
75
+ def #{tags_field}
76
+ tags_for(:"#{tags_field}")
77
+ end
78
+
79
+ def #{tags_field}_with_weight
80
+ tags_with_weight_for(:"#{tags_field}")
81
+ end
82
+
83
+ def #{tags_field}_separator
84
+ get_tag_separator_for(:"#{tags_field}")
85
+ end
86
+
87
+ def #{tags_field}_separator=(value)
88
+ set_tag_separator_for(:"#{tags_field}", value)
89
+ end
90
+
91
+ def #{tags_field}_tagged_with(tags)
92
+ tagged_with(:"#{tags_field}", tags)
93
+ end
94
+ end
95
+ END
96
+
97
+ # instance methods
98
+ class_eval <<-END
99
+ def #{tags_field}=(s)
100
+ super
101
+ write_attribute(:#{tags_array_field}, convert_string_to_array(s, get_tag_separator_for(:"#{tags_field}")))
102
+ end
103
+
104
+ def #{tags_array_field}=(a)
105
+ super
106
+ write_attribute(:#{tags_field}, convert_array_to_string(a, get_tag_separator_for(:"#{tags_field}")))
107
+ end
108
+ END
109
+ end
110
+
111
+ def tag_contexts
112
+ taggable_with_context_options.keys
113
+ end
114
+
115
+ def tag_options_for(context)
116
+ taggable_with_context_options[context]
117
+ end
118
+
119
+ def tags_for(context, conditions={})
120
+ raise AggregationStrategyMissing
121
+ end
122
+
123
+ def tags_with_weight_for(context, conditions={})
124
+ raise AggregationStrategyMissing
125
+ end
126
+
127
+ def get_tag_separator_for(context)
128
+ taggable_with_context_options[context][:separator]
129
+ end
130
+
131
+ def set_tag_separator_for(context, value)
132
+ taggable_with_context_options[context][:separator] = value.nil? ? " " : value.to_s
133
+ end
134
+
135
+ # Find documents tagged with all tags passed as a parameter, given
136
+ # as an Array or a String using the configured separator.
137
+ #
138
+ # @example Find matching all tags in an Array.
139
+ # Article.tagged_with(['ruby', 'mongodb'])
140
+ # @example Find matching all tags in a String.
141
+ # Article.tagged_with('ruby, mongodb')
142
+ #
143
+ # @param [ String ] :field The field name of the tag.
144
+ # @param [ Array<String, Symbol>, String ] :tags Tags to match.
145
+ # @return [ Criteria ] A new criteria.
146
+ def tagged_with(context, tags)
147
+ tags = convert_string_to_array(tags, get_tag_separator_for(context)) if tags.is_a? String
148
+ array_field = tag_options_for(context)[:array_field]
149
+ all_in(array_field => tags)
150
+ end
151
+
152
+ # Helper method to convert a String to an Array based on the
153
+ # configured tag separator.
154
+ def convert_string_to_array(str = "", seperator = " ")
155
+ str.split(seperator).map(&:strip).uniq.compact
156
+ end
157
+
158
+ def convert_array_to_string(ary = [], seperator = " ")
159
+ ary.uniq.compact.join(seperator)
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,96 @@
1
+ module Mongoid::TaggableWithContext::AggregationStrategy
2
+ module MapReduce
3
+ extend ActiveSupport::Concern
4
+ included do
5
+ set_callback :create, :after, :update_tags_agregation_on_create
6
+ set_callback :save, :after, :update_tags_aggregation_on_update
7
+ set_callback :destroy, :after, :update_tags_aggregation_on_destroy
8
+ delegate :aggregation_collection_for, :to => "self.class"
9
+ end
10
+
11
+ module ClassMethods
12
+ # Collection name for storing results of tag count aggregation
13
+ def aggregation_collection_for(context)
14
+ "#{collection_name}_#{context}_aggregation"
15
+ end
16
+
17
+ def tags_for(context, conditions={})
18
+ conditions = {:sort => '_id'}.merge(conditions)
19
+ db.collection(aggregation_collection_for(context)).find({:value => {"$gt" => 0 }}, conditions).to_a.map{ |t| t["_id"] }
20
+ end
21
+
22
+ # retrieve the list of tag with weight(count), this is useful for
23
+ # creating tag clouds
24
+ def tags_with_weight_for(context, conditions={})
25
+ conditions = {:sort => '_id'}.merge(conditions)
26
+ db.collection(aggregation_collection_for(context)).find({:value => {"$gt" => 0 }}, conditions).to_a.map{ |t| [t["_id"], t["value"].to_i] }
27
+ end
28
+ end
29
+
30
+ protected
31
+
32
+ def trigger_update_tags_aggregation_on_create?
33
+ previous_changes.empty?
34
+ end
35
+
36
+ def trigger_update_tags_aggregation_on_update?
37
+ !changed_contexts.empty?
38
+ end
39
+
40
+ def trigger_update_tags_aggregation_on_destroy?
41
+ true
42
+ end
43
+
44
+ def update_tags_agregation_on_create
45
+ return unless trigger_update_tags_aggregation_on_create?
46
+
47
+ tag_contexts.each do |context|
48
+ map_reduce_context_tags!(context)
49
+ end
50
+ end
51
+
52
+ def update_tags_aggregation_on_update
53
+ return unless trigger_update_tags_aggregation_on_update?
54
+
55
+ changed_contexts.each do |context|
56
+ map_reduce_context_tags!(context)
57
+ end
58
+ end
59
+
60
+ def update_tags_aggregation_on_destroy
61
+ return unless trigger_update_tags_aggregation_on_destroy?
62
+
63
+ tag_contexts.each do |context|
64
+ map_reduce_context_tags!(context)
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def changed_contexts
71
+ tag_contexts & previous_changes.keys.map(&:to_sym)
72
+ end
73
+
74
+ def map_reduce_context_tags!(context)
75
+ field = tag_options_for(context)[:array_field]
76
+
77
+ map = <<-END
78
+ function() {
79
+ if (!this.#{field})return;
80
+ for (index in this.#{field})
81
+ emit(this.#{field}[index], 1);
82
+ }
83
+ END
84
+
85
+ reduce = <<-END
86
+ function(key, values) {
87
+ var count = 0;
88
+ for (index in values) count += values[index];
89
+ return count;
90
+ }
91
+ END
92
+
93
+ collection.master.map_reduce(map, reduce, :out => aggregation_collection_for(context))
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,92 @@
1
+ module Mongoid::TaggableWithContext::AggregationStrategy
2
+ module RealTime
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ set_callback :create, :after, :increment_tags_agregation
7
+ set_callback :save, :after, :update_tags_aggregation
8
+ set_callback :destroy, :after, :decrement_tags_aggregation
9
+ end
10
+
11
+ module ClassMethods
12
+ # Collection name for storing results of tag count aggregation
13
+ def aggregation_collection_for(context)
14
+ "#{collection_name}_#{context}_aggregation"
15
+ end
16
+
17
+ def tags_for(context, conditions={})
18
+ conditions = {:sort => '_id'}.merge(conditions)
19
+ db.collection(aggregation_collection_for(context)).find({:value => {"$gt" => 0 }}, conditions).to_a.map{ |t| t["_id"] }
20
+ end
21
+
22
+ # retrieve the list of tag with weight(count), this is useful for
23
+ # creating tag clouds
24
+ def tags_with_weight_for(context, conditions={})
25
+ conditions = {:sort => '_id'}.merge(conditions)
26
+ db.collection(aggregation_collection_for(context)).find({:value => {"$gt" => 0 }}, conditions).to_a.map{ |t| [t["_id"], t["value"]] }
27
+ end
28
+ end
29
+
30
+ private
31
+ def need_update_tags_aggregation?
32
+ !changed_contexts.empty?
33
+ end
34
+
35
+ def changed_contexts
36
+ tag_contexts & previous_changes.keys.map(&:to_sym)
37
+ end
38
+
39
+ def increment_tags_agregation
40
+ # if document is created by using MyDocument.new
41
+ # and attributes are individually assigned
42
+ # #previous_changes won't be empty and aggregation
43
+ # is updated in after_save, so we simply skip it.
44
+ return unless previous_changes.empty?
45
+
46
+ # if the document is created by using MyDocument.create(:tags => "tag1 tag2")
47
+ # #previous_changes hash is empty and we have to update aggregation here
48
+ tag_contexts.each do |context|
49
+ coll = self.class.db.collection(self.class.aggregation_collection_for(context))
50
+ field_name = self.class.tag_options_for(context)[:array_field]
51
+ tags = self.send field_name || []
52
+ tags.each do |t|
53
+ coll.update({:_id => t}, {'$inc' => {:value => 1}}, :upsert => true)
54
+ end
55
+ end
56
+ end
57
+
58
+ def decrement_tags_aggregation
59
+ tag_contexts.each do |context|
60
+ coll = self.class.db.collection(self.class.aggregation_collection_for(context))
61
+ field_name = self.class.tag_options_for(context)[:array_field]
62
+ tags = self.send field_name || []
63
+ tags.each do |t|
64
+ coll.update({:_id => t}, {'$inc' => {:value => -1}}, :upsert => true)
65
+ end
66
+ end
67
+ end
68
+
69
+ def update_tags_aggregation
70
+ return unless need_update_tags_aggregation?
71
+
72
+ changed_contexts.each do |context|
73
+ coll = self.class.db.collection(self.class.aggregation_collection_for(context))
74
+ field_name = self.class.tag_options_for(context)[:array_field]
75
+ old_tags, new_tags = previous_changes["#{field_name}"]
76
+ old_tags ||= []
77
+ new_tags ||= []
78
+ unchanged_tags = old_tags & new_tags
79
+ tags_removed = old_tags - unchanged_tags
80
+ tags_added = new_tags - unchanged_tags
81
+
82
+ tags_removed.each do |t|
83
+ coll.update({:_id => t}, {'$inc' => {:value => -1}}, :upsert => true)
84
+ end
85
+
86
+ tags_added.each do |t|
87
+ coll.update({:_id => t}, {'$inc' => {:value => 1}}, :upsert => true)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -1,249 +1,5 @@
1
- # Copyright (c) 2010 Wilker Lúcio <wilkerlucio@gmail.com>
2
- #
3
- # Licensed under the Apache License, Version 2.0 (the "License");
4
- # you may not use this file except in compliance with the License.
5
- # You may obtain a copy of the License at
6
- #
7
- # http://www.apache.org/licenses/LICENSE-2.0
8
- #
9
- # Unless required by applicable law or agreed to in writing, software
10
- # distributed under the License is distributed on an "AS IS" BASIS,
11
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- # See the License for the specific language governing permissions and
13
- # limitations under the License.
1
+ require 'active_support/concern'
14
2
 
15
- module Mongoid::TaggableWithContext
16
- extend ActiveSupport::Concern
17
-
18
- included do
19
- class_inheritable_reader :taggable_with_context_options
20
- end
21
-
22
- module ClassMethods
23
- # Macro to declare a document class as taggable, specify field name
24
- # for tags, and set options for tagging behavior.
25
- #
26
- # @example Define a taggable document.
27
- #
28
- # class Article
29
- # include Mongoid::Document
30
- # include Mongoid::Taggable
31
- # taggable :keywords, :separator => ' ', :aggregation => true, :default_type => "seo"
32
- # end
33
- #
34
- # @param [ Symbol ] field The name of the field for tags.
35
- # @param [ Hash ] options Options for taggable behavior.
36
- #
37
- # @option options [ String ] :separator The tag separator to
38
- # convert from; defaults to ','
39
- # @option options [ true, false ] :aggregation Whether or not to
40
- # aggregate counts of tags within the document collection using
41
- # map/reduce; defaults to false
42
- # @option options [ String ] :default_type The default type of the tag.
43
- # Each tag can optionally have a tag type. The default type is nil
44
- def taggable(*args)
45
- # init variables
46
- options = args.extract_options!
47
- tags_field = (args.blank? ? :tags : args.shift).to_sym
48
- options.reverse_merge!(
49
- :separator => ' ',
50
- :array_field => "#{tags_field}_array".to_sym
51
- )
52
- tags_array_field = options[:array_field]
53
- first_invoke = taggable_with_context_options.nil?
54
-
55
- # register / update settings
56
- class_options = taggable_with_context_options || {}
57
- class_options[tags_field] = options
58
- write_inheritable_attribute(:taggable_with_context_options, class_options)
59
-
60
- # setup fields & indexes
61
- field tags_field, :default => ""
62
- field tags_array_field, :type => Array, :default => []
63
- index tags_array_field
64
-
65
- if first_invoke
66
- delegate "convert_string_to_array", :to => 'self.class'
67
- delegate "convert_array_to_string", :to => 'self.class'
68
- delegate "get_tag_separator_for", :to => 'self.class'
69
- delegate "tag_contexts", :to => 'self.class'
70
- delegate "aggregation_collection_for", :to => 'self.class'
71
- delegate "tag_options_for", :to => 'self.class'
72
-
73
- set_callback :create, :after, :increment_tags_agregation
74
- set_callback :save, :after, :update_tags_aggregation
75
- set_callback :destroy, :after, :decrement_tags_aggregation
76
- end
77
-
78
- extend SingletonMethods
79
- include InstanceMethods
80
-
81
- # singleton methods
82
- class_eval <<-END
83
- class << self
84
- def #{tags_field}_aggregation_collection
85
- @#{tags_field}_aggregation_collection ||= aggregation_collection_for(:"#{tags_field}")
86
- end
87
-
88
- def #{tags_field}
89
- tags_for(:"#{tags_field}")
90
- end
91
-
92
- def #{tags_field}_with_weight
93
- tags_with_weight_for(:"#{tags_field}")
94
- end
95
-
96
- def #{tags_field}_separator
97
- get_tag_separator_for(:"#{tags_field}")
98
- end
99
-
100
- def #{tags_field}_separator=(value)
101
- set_tag_separator_for(:"#{tags_field}", value)
102
- end
103
-
104
- def #{tags_field}_tagged_with(tags)
105
- tagged_with(:"#{tags_field}", tags)
106
- end
107
- end
108
- END
109
-
110
- # instance methods
111
- class_eval <<-END
112
- def #{tags_field}=(s)
113
- super
114
- write_attribute(:#{tags_array_field}, convert_string_to_array(s, get_tag_separator_for(:"#{tags_field}")))
115
- end
116
-
117
- def #{tags_array_field}=(a)
118
- super
119
- write_attribute(:#{tags_field}, convert_array_to_string(a, get_tag_separator_for(:"#{tags_field}")))
120
- end
121
- END
122
- end
123
- end
124
-
125
- module SingletonMethods
126
- def tag_contexts
127
- taggable_with_context_options.keys
128
- end
129
-
130
- def tag_options_for(context)
131
- taggable_with_context_options[context]
132
- end
133
-
134
- # Collection name for storing results of tag count aggregation
135
- def aggregation_collection_for(context)
136
- "#{collection_name}_#{context}_aggregation"
137
- end
138
-
139
- def tags_for(context, conditions={})
140
- conditions = {:sort => '_id'}.merge(conditions)
141
- db.collection(aggregation_collection_for(context)).find({:value => {"$gt" => 0 }}, conditions).to_a.map{ |t| t["_id"] }
142
- end
143
-
144
- # retrieve the list of tag with weight(count), this is useful for
145
- # creating tag clouds
146
- def tags_with_weight_for(context, conditions={})
147
- conditions = {:sort => '_id'}.merge(conditions)
148
- db.collection(aggregation_collection_for(context)).find({:value => {"$gt" => 0 }}, conditions).to_a.map{ |t| [t["_id"], t["value"]] }
149
- end
150
-
151
- def get_tag_separator_for(context)
152
- taggable_with_context_options[context][:separator]
153
- end
154
-
155
- def set_tag_separator_for(context, value)
156
- taggable_with_context_options[context][:separator] = value.nil? ? " " : value.to_s
157
- end
158
-
159
- # Find documents tagged with all tags passed as a parameter, given
160
- # as an Array or a String using the configured separator.
161
- #
162
- # @example Find matching all tags in an Array.
163
- # Article.tagged_with(['ruby', 'mongodb'])
164
- # @example Find matching all tags in a String.
165
- # Article.tagged_with('ruby, mongodb')
166
- #
167
- # @param [ String ] :field The field name of the tag.
168
- # @param [ Array<String, Symbol>, String ] :tags Tags to match.
169
- # @return [ Criteria ] A new criteria.
170
- def tagged_with(context, tags)
171
- tags = convert_string_to_array(tags, get_tag_separator_for(context)) if tags.is_a? String
172
- array_field = tag_options_for(context)[:array_field]
173
- all_in(array_field => tags)
174
- end
175
-
176
- # Helper method to convert a String to an Array based on the
177
- # configured tag separator.
178
- def convert_string_to_array(str = "", seperator = " ")
179
- str.split(seperator).map(&:strip).uniq.compact
180
- end
181
-
182
- def convert_array_to_string(ary = [], seperator = " ")
183
- ary.uniq.compact.join(seperator)
184
- end
185
- end
186
-
187
- module InstanceMethods
188
- def need_update_tags_aggregation?
189
- !changed_contexts.empty?
190
- end
191
-
192
- def changed_contexts
193
- tag_contexts & previous_changes.keys.map(&:to_sym)
194
- end
195
-
196
- def increment_tags_agregation
197
- # if document is created by using MyDocument.new
198
- # and attributes are individually assigned
199
- # #previous_changes won't be empty and aggregation
200
- # is updated in after_save, so we simply skip it.
201
- return unless previous_changes.empty?
202
-
203
- # if the document is created by using MyDocument.create(:tags => "tag1 tag2")
204
- # #previous_changes hash is empty and we have to update aggregation here
205
- tag_contexts.each do |context|
206
- coll = self.class.db.collection(self.class.aggregation_collection_for(context))
207
- field_name = self.class.tag_options_for(context)[:array_field]
208
- tags = self.send field_name || []
209
- tags.each do |t|
210
- coll.update({:_id => t}, {'$inc' => {:value => 1}}, :upsert => true)
211
- end
212
- end
213
- end
214
-
215
- def decrement_tags_aggregation
216
- tag_contexts.each do |context|
217
- coll = self.class.db.collection(self.class.aggregation_collection_for(context))
218
- field_name = self.class.tag_options_for(context)[:array_field]
219
- tags = self.send field_name || []
220
- tags.each do |t|
221
- coll.update({:_id => t}, {'$inc' => {:value => -1}}, :upsert => true)
222
- end
223
- end
224
- end
225
-
226
- def update_tags_aggregation
227
- return unless need_update_tags_aggregation?
228
-
229
- changed_contexts.each do |context|
230
- coll = self.class.db.collection(self.class.aggregation_collection_for(context))
231
- field_name = self.class.tag_options_for(context)[:array_field]
232
- old_tags, new_tags = previous_changes["#{field_name}"]
233
- old_tags ||= []
234
- new_tags ||= []
235
- unchanged_tags = old_tags & new_tags
236
- tags_removed = old_tags - unchanged_tags
237
- tags_added = new_tags - unchanged_tags
238
-
239
- tags_removed.each do |t|
240
- coll.update({:_id => t}, {'$inc' => {:value => -1}}, :upsert => true)
241
- end
242
-
243
- tags_added.each do |t|
244
- coll.update({:_id => t}, {'$inc' => {:value => 1}}, :upsert => true)
245
- end
246
- end
247
- end
248
- end
249
- end
3
+ require File.join(File.dirname(__FILE__), 'mongoid/taggable_with_context')
4
+ require File.join(File.dirname(__FILE__), 'mongoid/taggable_with_context/aggregation_strategy/map_reduce')
5
+ require File.join(File.dirname(__FILE__), 'mongoid/taggable_with_context/aggregation_strategy/real_time')
@@ -5,7 +5,7 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{mongoid_taggable_with_context}
8
- s.version = "0.5.0"
8
+ s.version = "0.6.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Aaron Qian"]
@@ -19,12 +19,14 @@ Gem::Specification.new do |s|
19
19
  s.files = [
20
20
  ".document",
21
21
  "Gemfile",
22
- "Gemfile.lock",
23
22
  "LICENSE.txt",
24
23
  "README.rdoc",
25
24
  "Rakefile",
26
25
  "VERSION",
27
26
  "init.rb",
27
+ "lib/mongoid/taggable_with_context.rb",
28
+ "lib/mongoid/taggable_with_context/aggregation_strategy/map_reduce.rb",
29
+ "lib/mongoid/taggable_with_context/aggregation_strategy/real_time.rb",
28
30
  "lib/mongoid_taggable_with_context.rb",
29
31
  "mongoid_taggable_with_context.gemspec",
30
32
  "spec/mongoid_taggable_with_context_spec.rb",
@@ -3,6 +3,25 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
3
3
  class MyModel
4
4
  include Mongoid::Document
5
5
  include Mongoid::TaggableWithContext
6
+
7
+ taggable
8
+ taggable :artists
9
+ end
10
+
11
+ class M1
12
+ include Mongoid::Document
13
+ include Mongoid::TaggableWithContext
14
+ include Mongoid::TaggableWithContext::AggregationStrategy::MapReduce
15
+
16
+ taggable
17
+ taggable :artists
18
+ end
19
+
20
+ class M2
21
+ include Mongoid::Document
22
+ include Mongoid::TaggableWithContext
23
+ include Mongoid::TaggableWithContext::AggregationStrategy::RealTime
24
+
6
25
  taggable
7
26
  taggable :artists
8
27
  end
@@ -77,31 +96,47 @@ describe Mongoid::TaggableWithContext do
77
96
  @m.tags.should == "some;other;sep"
78
97
  end
79
98
  end
80
-
81
- context "indexing tags" do
82
- it "should generate the tags aggregation collection name correctly" do
83
- MyModel.tags_aggregation_collection.should == "my_models_tags_aggregation"
99
+
100
+ context "tagged_with" do
101
+ before :each do
102
+ @m1 = MyModel.create!(:tags => "food ant bee", :artists => "jeff greg mandy aaron andy")
103
+ @m2 = MyModel.create!(:tags => "juice food bee zip", :artists => "grant andrew andy")
104
+ @m3 = MyModel.create!(:tags => "honey strip food", :artists => "mandy aaron andy")
84
105
  end
85
106
 
86
- it "should generate the artists aggregation collection name correctly" do
87
- MyModel.artists_aggregation_collection.should == "my_models_artists_aggregation"
107
+ it "should retrieve a list of documents" do
108
+ (MyModel.tags_tagged_with("food").to_a - [@m1, @m2, @m3]).should be_empty
109
+ (MyModel.artists_tagged_with("aaron").to_a - [@m1, @m3]).should be_empty
88
110
  end
89
-
111
+ end
112
+
113
+ context "no aggregation" do
114
+ it "should raise AggregationStrategyMissing exception when retreiving tags" do
115
+ lambda{ MyModel.tags }.should raise_error(Mongoid::TaggableWithContext::AggregationStrategyMissing)
116
+ end
117
+
118
+ it "should raise AggregationStrategyMissing exception when retreiving tags with weights" do
119
+ lambda{ MyModel.tags_with_weight }.should raise_error(Mongoid::TaggableWithContext::AggregationStrategyMissing)
120
+ end
121
+
122
+ end
123
+
124
+ shared_examples_for "aggregation" do
90
125
  context "retriving index" do
91
126
  context "on create directly" do
92
127
  before :each do
93
- MyModel.create!(:tags => "food ant bee", :artists => "jeff greg mandy aaron andy")
94
- MyModel.create!(:tags => "juice food bee zip", :artists => "grant andrew andy")
95
- MyModel.create!(:tags => "honey strip food", :artists => "mandy aaron andy")
128
+ klass.create!(:tags => "food ant bee", :artists => "jeff greg mandy aaron andy")
129
+ klass.create!(:tags => "juice food bee zip", :artists => "grant andrew andy")
130
+ klass.create!(:tags => "honey strip food", :artists => "mandy aaron andy")
96
131
  end
97
132
 
98
133
  it "should retrieve the list of all saved tags distinct and ordered" do
99
- MyModel.tags.should == %w[ant bee food honey juice strip zip]
100
- MyModel.artists.should == %w[aaron andrew andy grant greg jeff mandy]
134
+ klass.tags.should == %w[ant bee food honey juice strip zip]
135
+ klass.artists.should == %w[aaron andrew andy grant greg jeff mandy]
101
136
  end
102
137
 
103
138
  it "should retrieve a list of tags with weight" do
104
- MyModel.tags_with_weight.should == [
139
+ klass.tags_with_weight.should == [
105
140
  ['ant', 1],
106
141
  ['bee', 2],
107
142
  ['food', 3],
@@ -111,7 +146,7 @@ describe Mongoid::TaggableWithContext do
111
146
  ['zip', 1]
112
147
  ]
113
148
 
114
- MyModel.artists_with_weight.should == [
149
+ klass.artists_with_weight.should == [
115
150
  ['aaron', 2],
116
151
  ['andrew', 1],
117
152
  ['andy', 3],
@@ -125,29 +160,29 @@ describe Mongoid::TaggableWithContext do
125
160
 
126
161
  context "on new then change attributes directly" do
127
162
  before :each do
128
- m = MyModel.new
163
+ m = klass.new
129
164
  m.tags = "food ant bee"
130
165
  m.artists = "jeff greg mandy aaron andy"
131
166
  m.save!
132
167
 
133
- m = MyModel.new
168
+ m = klass.new
134
169
  m.tags = "juice food bee zip"
135
170
  m.artists = "grant andrew andy"
136
171
  m.save!
137
172
 
138
- m = MyModel.new
173
+ m = klass.new
139
174
  m.tags = "honey strip food"
140
175
  m.artists = "mandy aaron andy"
141
176
  m.save!
142
177
  end
143
178
 
144
179
  it "should retrieve the list of all saved tags distinct and ordered" do
145
- MyModel.tags.should == %w[ant bee food honey juice strip zip]
146
- MyModel.artists.should == %w[aaron andrew andy grant greg jeff mandy]
180
+ klass.tags.should == %w[ant bee food honey juice strip zip]
181
+ klass.artists.should == %w[aaron andrew andy grant greg jeff mandy]
147
182
  end
148
183
 
149
184
  it "should retrieve a list of tags with weight" do
150
- MyModel.tags_with_weight.should == [
185
+ klass.tags_with_weight.should == [
151
186
  ['ant', 1],
152
187
  ['bee', 2],
153
188
  ['food', 3],
@@ -157,7 +192,7 @@ describe Mongoid::TaggableWithContext do
157
192
  ['zip', 1]
158
193
  ]
159
194
 
160
- MyModel.artists_with_weight.should == [
195
+ klass.artists_with_weight.should == [
161
196
  ['aaron', 2],
162
197
  ['andrew', 1],
163
198
  ['andy', 3],
@@ -171,9 +206,9 @@ describe Mongoid::TaggableWithContext do
171
206
 
172
207
  context "on create then update" do
173
208
  before :each do
174
- m1 = MyModel.create!(:tags => "food ant bee", :artists => "jeff greg mandy aaron andy")
175
- m2 = MyModel.create!(:tags => "juice food bee zip", :artists => "grant andrew andy")
176
- m3 = MyModel.create!(:tags => "honey strip food", :artists => "mandy aaron andy")
209
+ m1 = klass.create!(:tags => "food ant bee", :artists => "jeff greg mandy aaron andy")
210
+ m2 = klass.create!(:tags => "juice food bee zip", :artists => "grant andrew andy")
211
+ m3 = klass.create!(:tags => "honey strip food", :artists => "mandy aaron andy")
177
212
 
178
213
  m1.tags_array = m1.tags_array + %w[honey strip shoe]
179
214
  m1.save!
@@ -183,12 +218,12 @@ describe Mongoid::TaggableWithContext do
183
218
  end
184
219
 
185
220
  it "should retrieve the list of all saved tags distinct and ordered" do
186
- MyModel.tags.should == %w[ant bee food honey juice shoe strip zip]
187
- MyModel.artists.should == %w[aaron andrew andy gory grant greg jeff mandy]
221
+ klass.tags.should == %w[ant bee food honey juice shoe strip zip]
222
+ klass.artists.should == %w[aaron andrew andy gory grant greg jeff mandy]
188
223
  end
189
224
 
190
225
  it "should retrieve a list of tags with weight" do
191
- MyModel.tags_with_weight.should == [
226
+ klass.tags_with_weight.should == [
192
227
  ['ant', 1],
193
228
  ['bee', 2],
194
229
  ['food', 3],
@@ -199,7 +234,7 @@ describe Mongoid::TaggableWithContext do
199
234
  ['zip', 1]
200
235
  ]
201
236
 
202
- MyModel.artists_with_weight.should == [
237
+ klass.artists_with_weight.should == [
203
238
  ['aaron', 2],
204
239
  ['andrew', 1],
205
240
  ['andy', 3],
@@ -214,9 +249,9 @@ describe Mongoid::TaggableWithContext do
214
249
 
215
250
  context "on create, update, then destroy" do
216
251
  before :each do
217
- m1 = MyModel.create!(:tags => "food ant bee", :artists => "jeff greg mandy aaron andy")
218
- m2 = MyModel.create!(:tags => "juice food bee zip", :artists => "grant andrew andy")
219
- m3 = MyModel.create!(:tags => "honey strip food", :artists => "mandy aaron andy")
252
+ m1 = klass.create!(:tags => "food ant bee", :artists => "jeff greg mandy aaron andy")
253
+ m2 = klass.create!(:tags => "juice food bee zip", :artists => "grant andrew andy")
254
+ m3 = klass.create!(:tags => "honey strip food", :artists => "mandy aaron andy")
220
255
 
221
256
  m1.tags_array = m1.tags_array + %w[honey strip shoe] - %w[food]
222
257
  m1.save!
@@ -228,12 +263,12 @@ describe Mongoid::TaggableWithContext do
228
263
  end
229
264
 
230
265
  it "should retrieve the list of all saved tags distinct and ordered" do
231
- MyModel.tags.should == %w[ant bee food honey shoe strip]
232
- MyModel.artists.should == %w[aaron andy gory grant greg jeff mandy]
266
+ klass.tags.should == %w[ant bee food honey shoe strip]
267
+ klass.artists.should == %w[aaron andy gory grant greg jeff mandy]
233
268
  end
234
269
 
235
270
  it "should retrieve a list of tags with weight" do
236
- MyModel.tags_with_weight.should == [
271
+ klass.tags_with_weight.should == [
237
272
  ['ant', 1],
238
273
  ['bee', 1],
239
274
  ['food', 1],
@@ -242,7 +277,7 @@ describe Mongoid::TaggableWithContext do
242
277
  ['strip', 2]
243
278
  ]
244
279
 
245
- MyModel.artists_with_weight.should == [
280
+ klass.artists_with_weight.should == [
246
281
  ['aaron', 2],
247
282
  ['andy', 1],
248
283
  ['gory', 1],
@@ -253,20 +288,32 @@ describe Mongoid::TaggableWithContext do
253
288
  ]
254
289
  end
255
290
  end
256
-
291
+ end
292
+ end
293
+
294
+ context "map-reduce aggregation" do
295
+ let(:klass) { M1 }
296
+ it_should_behave_like "aggregation"
297
+
298
+ it "should generate the tags aggregation collection name correctly" do
299
+ klass.aggregation_collection_for(:tags).should == "m1s_tags_aggregation"
257
300
  end
258
301
 
259
- context "tagged_with" do
260
- before :each do
261
- @m1 = MyModel.create!(:tags => "food ant bee", :artists => "jeff greg mandy aaron andy")
262
- @m2 = MyModel.create!(:tags => "juice food bee zip", :artists => "grant andrew andy")
263
- @m3 = MyModel.create!(:tags => "honey strip food", :artists => "mandy aaron andy")
264
- end
265
-
266
- it "should retrieve a list of documents" do
267
- (MyModel.tags_tagged_with("food").to_a - [@m1, @m2, @m3]).should be_empty
268
- (MyModel.artists_tagged_with("aaron").to_a - [@m1, @m3]).should be_empty
269
- end
302
+ it "should generate the artists aggregation collection name correctly" do
303
+ klass.aggregation_collection_for(:artists).should == "m1s_artists_aggregation"
304
+ end
305
+ end
306
+
307
+ context "realtime aggregation" do
308
+ let(:klass) { M2 }
309
+ it_should_behave_like "aggregation"
310
+
311
+ it "should generate the tags aggregation collection name correctly" do
312
+ klass.aggregation_collection_for(:tags).should == "m2s_tags_aggregation"
313
+ end
314
+
315
+ it "should generate the artists aggregation collection name correctly" do
316
+ klass.aggregation_collection_for(:artists).should == "m2s_artists_aggregation"
270
317
  end
271
318
  end
272
319
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongoid_taggable_with_context
3
3
  version: !ruby/object:Gem::Version
4
- hash: 11
4
+ hash: 7
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 5
8
+ - 6
9
9
  - 0
10
- version: 0.5.0
10
+ version: 0.6.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Aaron Qian
@@ -222,12 +222,14 @@ extra_rdoc_files:
222
222
  files:
223
223
  - .document
224
224
  - Gemfile
225
- - Gemfile.lock
226
225
  - LICENSE.txt
227
226
  - README.rdoc
228
227
  - Rakefile
229
228
  - VERSION
230
229
  - init.rb
230
+ - lib/mongoid/taggable_with_context.rb
231
+ - lib/mongoid/taggable_with_context/aggregation_strategy/map_reduce.rb
232
+ - lib/mongoid/taggable_with_context/aggregation_strategy/real_time.rb
231
233
  - lib/mongoid_taggable_with_context.rb
232
234
  - mongoid_taggable_with_context.gemspec
233
235
  - spec/mongoid_taggable_with_context_spec.rb
data/Gemfile.lock DELETED
@@ -1,67 +0,0 @@
1
- GEM
2
- remote: http://rubygems.org/
3
- specs:
4
- activemodel (3.0.4)
5
- activesupport (= 3.0.4)
6
- builder (~> 2.1.2)
7
- i18n (~> 0.4)
8
- activesupport (3.0.4)
9
- bson (1.2.1)
10
- bson_ext (1.2.1)
11
- builder (2.1.2)
12
- database_cleaner (0.6.0)
13
- diff-lcs (1.1.2)
14
- git (1.2.5)
15
- i18n (0.5.0)
16
- jeweler (1.5.2)
17
- bundler (~> 1.0.0)
18
- git (>= 1.2.5)
19
- rake
20
- mongo (1.2.1)
21
- bson (>= 1.2.1)
22
- mongoid (2.0.0.rc.7)
23
- activemodel (~> 3.0)
24
- mongo (~> 1.2)
25
- tzinfo (~> 0.3.22)
26
- will_paginate (~> 3.0.pre)
27
- rake (0.8.7)
28
- rcov (0.9.9)
29
- reek (1.2.8)
30
- ruby2ruby (~> 1.2)
31
- ruby_parser (~> 2.0)
32
- sexp_processor (~> 3.0)
33
- roodi (2.1.0)
34
- ruby_parser
35
- rspec (2.3.0)
36
- rspec-core (~> 2.3.0)
37
- rspec-expectations (~> 2.3.0)
38
- rspec-mocks (~> 2.3.0)
39
- rspec-core (2.3.1)
40
- rspec-expectations (2.3.0)
41
- diff-lcs (~> 1.1.2)
42
- rspec-mocks (2.3.0)
43
- ruby2ruby (1.2.5)
44
- ruby_parser (~> 2.0)
45
- sexp_processor (~> 3.0)
46
- ruby_parser (2.0.5)
47
- sexp_processor (~> 3.0)
48
- sexp_processor (3.0.5)
49
- tzinfo (0.3.24)
50
- will_paginate (3.0.pre2)
51
- yard (0.6.4)
52
-
53
- PLATFORMS
54
- ruby
55
-
56
- DEPENDENCIES
57
- bson (~> 1.2.1)
58
- bson_ext (~> 1.2.1)
59
- bundler (~> 1.0.0)
60
- database_cleaner
61
- jeweler (~> 1.5.2)
62
- mongoid (~> 2.0.0.beta.20)
63
- rcov
64
- reek (~> 1.2.8)
65
- roodi (~> 2.1.0)
66
- rspec (~> 2.3.0)
67
- yard (~> 0.6.0)