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 +45 -4
- data/VERSION +1 -1
- data/init.rb +1 -1
- data/lib/mongoid/taggable_with_context.rb +162 -0
- data/lib/mongoid/taggable_with_context/aggregation_strategy/map_reduce.rb +96 -0
- data/lib/mongoid/taggable_with_context/aggregation_strategy/real_time.rb +92 -0
- data/lib/mongoid_taggable_with_context.rb +4 -248
- data/mongoid_taggable_with_context.gemspec +4 -2
- data/spec/mongoid_taggable_with_context_spec.rb +94 -47
- metadata +6 -4
- data/Gemfile.lock +0 -67
data/README.rdoc
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
== Mongoid Taggable With Context
|
2
2
|
|
3
|
-
A tagging
|
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
|
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
|
-
==
|
83
|
+
== Aggregation Strategies
|
84
84
|
|
85
|
-
|
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.
|
1
|
+
0.6.0
|
data/init.rb
CHANGED
@@ -1 +1 @@
|
|
1
|
-
require '
|
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
|
-
|
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
|
-
|
16
|
-
|
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.
|
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 "
|
82
|
-
|
83
|
-
MyModel.
|
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
|
87
|
-
MyModel.
|
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
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
100
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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 =
|
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 =
|
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
|
-
|
146
|
-
|
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
|
-
|
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
|
-
|
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 =
|
175
|
-
m2 =
|
176
|
-
m3 =
|
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
|
-
|
187
|
-
|
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
|
-
|
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
|
-
|
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 =
|
218
|
-
m2 =
|
219
|
-
m3 =
|
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
|
-
|
232
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
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:
|
4
|
+
hash: 7
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
8
|
+
- 6
|
9
9
|
- 0
|
10
|
-
version: 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)
|