yoomee-acts_as_mongo_taggable 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 [name of plugin creator]
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,102 @@
1
+ ActsAsMongoTaggable
2
+ ===================
3
+
4
+ Inspired by mbleigh's "acts_as_taggable_on," this tagging plugin works with MongoDB+MongoMapper.
5
+
6
+ Intends to be super-performant by taking advantage of the benefits of document-driven db denormalization.
7
+
8
+ Requirements
9
+ ------------
10
+
11
+ - MongoDB
12
+ - MongoMapper gem
13
+ - Expects you to have a User model that includes MongoMapper::Document
14
+
15
+ Installation
16
+ ------------
17
+
18
+ Best practice -- install the gem:
19
+
20
+ gem install acts_as_mongo_taggable
21
+
22
+ … and add a line to your environment.rb:
23
+
24
+ config.gem 'acts_as_mongo_taggable'
25
+
26
+
27
+ or, if you're old-school, install as a plugin:
28
+
29
+ ./script/plugin install git://github.com/mepatterson/acts_as_mongo_taggable.git
30
+
31
+
32
+ Finally, add this line to the Rails model class that you want to make taggable:
33
+
34
+ include ActsAsMongoTaggable
35
+
36
+ Yeah, that's it.
37
+
38
+ Usage
39
+ -----
40
+
41
+ class User
42
+ include MongoMapper::Document
43
+ end
44
+
45
+ class Widget
46
+ include MongoMapper::Document
47
+ include ActsAsMongoTaggable
48
+ end
49
+
50
+ To rate it:
51
+
52
+ widget.tag(word_or_words, user)
53
+
54
+ - word_or_words can be a string, a string of comma-delimited words, or an array
55
+ - user is the User who is tagging this widget
56
+
57
+ Basic search:
58
+
59
+ Widget.find_with_tag('vampires')
60
+
61
+ ... will return the first Widget object that has been tagged with that phrase
62
+
63
+ Widget.find_all_with_tag('vampires')
64
+
65
+ ... will return an array of Widget objects, all of which have been tagged with that phrase
66
+
67
+ Widget.most_tagged_with('vampires')
68
+
69
+ ... will return the Widget object that has been tagged the most times with that phrase
70
+
71
+ Making tag clouds:
72
+
73
+ Widget.all_tags_with_counts
74
+
75
+ ... will return a nice array of arrays, a la [["rails", 8],["ruby", 12], ["php", 6], ["java", 2]]
76
+ Use this to make yourself a tag cloud for now. (maybe I'll implement a tag cloud view helper someday.)
77
+
78
+ Statistics on Tags:
79
+
80
+ Tag.top_25
81
+
82
+ ... returns the top 25 most used tags across all taggable object classes in the system
83
+
84
+ Tag.top_25("Widget")
85
+
86
+ ... returns the top 25 most used tags for Widget objects
87
+
88
+
89
+ Future
90
+ ------
91
+ - Performance improvements as I come across the need
92
+
93
+
94
+ Thanks To...
95
+ ------------
96
+ - Jon Bell for some sweet refactorings and gem-ification
97
+ - John Nunemaker and the rest of the folks on the MongoMapper Google Group
98
+ - Kyle Banker and his excellent blog posts on grouping and aggregation
99
+ - The MongoDB peoples and the MongoDB Google Group
100
+ - mbleigh for the acts_as_taggable_on plugin for ActiveRecord
101
+
102
+ Copyright (c) 2009 [M. E. Patterson], released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,58 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the acts_as_mongo_taggable_on plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.libs << 'test'
12
+ t.pattern = 'test/**/*_test.rb'
13
+ t.verbose = true
14
+ end
15
+
16
+ desc 'Generate documentation for the acts_as_mongo_taggable_on plugin.'
17
+ Rake::RDocTask.new(:rdoc) do |rdoc|
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = 'ActsAsMongoTaggableOn'
20
+ rdoc.options << '--line-numbers' << '--inline-source'
21
+ rdoc.rdoc_files.include('README')
22
+ rdoc.rdoc_files.include('lib/**/*.rb')
23
+ end
24
+
25
+ begin
26
+ GEM = "acts_as_mongo_taggable"
27
+ AUTHOR = "Matt E. Patterson"
28
+ EMAIL = "mpatterson@ngenera.com"
29
+ SUMMARY = "A ruby gem for acts_as_taggable to mongo"
30
+ HOMEPAGE = "http://github.com/mepatterson/acts_as_mongo_taggable"
31
+
32
+ gem 'jeweler', '>= 1.0.0'
33
+ require 'jeweler'
34
+
35
+ Jeweler::Tasks.new do |s|
36
+ s.name = GEM
37
+ s.summary = SUMMARY
38
+ s.email = EMAIL
39
+ s.homepage = HOMEPAGE
40
+ s.description = SUMMARY
41
+ s.author = AUTHOR
42
+
43
+ s.require_path = 'lib'
44
+ s.files = %w(MIT-LICENSE README.textile Rakefile) + Dir.glob("{rails,lib,generators,spec}/**/*")
45
+
46
+ # Runtime dependencies: When installing Formtastic these will be checked if they are installed.
47
+ # Will be offered to install these if they are not already installed.
48
+ s.add_dependency 'mongo_mapper', '>= 0.7.0'
49
+
50
+ # Development dependencies. Not installed by default.
51
+ # Install with: sudo gem install formtastic --development
52
+ #s.add_development_dependency 'rspec-rails', '>= 1.2.6'
53
+ end
54
+
55
+ Jeweler::GemcutterTasks.new
56
+ rescue LoadError
57
+ puts "[acts_as_mongo_taggable:] Jeweler - or one of its dependencies - is not available. Install it with: sudo gem install jeweler -s http://gemcutter.org"
58
+ end
@@ -0,0 +1,256 @@
1
+ module ActsAsMongoTaggable
2
+ module ClassMethods
3
+ def all_tags_with_counts
4
+ Tag.most_tagged(self).map{|tag| [tag.word, tag.count_for(self)]}
5
+ end
6
+
7
+ # returns the _first_ widget with this tag, a la ActiveRecord find()
8
+ # note: case-insensitive unless you specify otherwise with :case_sensitive=>true
9
+ def first_with_tag(phrase, opts={})
10
+ lo = opts.clone
11
+ case_sensitive = lo.delete :case_sensitive
12
+ phrase = phrase.downcase unless case_sensitive
13
+ first(lo.merge(:tag_words => phrase))
14
+ end
15
+
16
+
17
+ def all_with_tag(phrase, opts={})
18
+ lo = opts.clone
19
+ case_sensitive = lo.delete :case_sensitive
20
+ phrase = phrase.downcase unless case_sensitive
21
+ all(lo.merge(:tag_words => phrase))
22
+ end
23
+
24
+ def most_tagged_with(phrase, opts={})
25
+ lo = opts.clone
26
+ case_sensitive = lo.delete :case_sensitive
27
+ phrase = phrase.downcase unless case_sensitive
28
+
29
+ #Doesn't work :(
30
+ #first(lo.merge('model_tags.word' => phrase, :order => 'model_tags.tagging_count desc'))
31
+
32
+ all_with_tag(phrase, lo).sort do |a, b|
33
+ b.model_tag(phrase, opts).tagging_count <=> a.model_tag(phrase, opts).tagging_count
34
+ end.first
35
+ end
36
+
37
+ def top_25_tags
38
+ Tag.top_25(self)
39
+ end
40
+ end
41
+
42
+ module InstanceMethods
43
+
44
+ def delete_all_tags
45
+ Tag.find(tag_ids).each do |tag|
46
+ model_taggings = tag.taggings.select{|tagging| tagging.taggable_type == self.class.name && tagging.taggable_id == self.id}
47
+ model_taggings.each{|tagging| tag.taggings.delete tagging}
48
+ tag.save_or_destroy
49
+ end
50
+ update_attributes({ :model_tags => [], :tag_words => [] })
51
+ end
52
+
53
+ def _tags
54
+ Tag.find(:all, tag_ids)
55
+ end
56
+
57
+ # returns array of tags and counts:
58
+ # [["matt", 3], ["bob", 2], ["bill", 1], ["joe", 1], ["frank", 1]]
59
+ def tags_with_counts
60
+ counts = model_tags.map{|tag| [tag.word, tag.tagging_count]}
61
+ counts.sort{|a, b| b[1] <=> a[1]}
62
+ end
63
+
64
+ # returns an array of ids and user_ids
65
+ def tags_with_user_ids
66
+ model_tags.inject([]) do |arr, tag|
67
+ tag.user_ids.each{|user_id| arr << [tag.id, user_id]}
68
+ arr
69
+ end
70
+ end
71
+
72
+ def tag_words_by_user(user)
73
+ tags_by_user(user).map(&:word)
74
+ end
75
+
76
+ def tags_by_user(user)
77
+ model_tags.select{|tag| tag.user_ids.include? user.id}
78
+ end
79
+
80
+ # returns only the tag words, sorted by frequency; optionally can be limited
81
+ def tags(limit=nil)
82
+ array = tags_with_counts
83
+ limit ||= array.size
84
+ array[0,limit].map{|t| t[0]}
85
+ end
86
+
87
+ def model_tag(phrase, opts = {})
88
+ phrase = phrase.downcase unless opts[:case_sensitive]
89
+ model_tags.detect{|tag| tag.word == phrase}
90
+ end
91
+
92
+ def tag_ids
93
+ model_tags.map(&:tag_id)
94
+ end
95
+ end
96
+
97
+ def delete_tags_by_user(user)
98
+ return false unless user
99
+ return 0 if model_tags.blank?
100
+ user_tags = tags_by_user(user)
101
+
102
+ Tag.find(user_tags.map(&:tag_id)).each do |tag|
103
+ user_taggings = tag.taggings.select{|tagging| tagging.user_id == user.id && tagging.taggable_type == self.class.name && tagging.taggable_id == self.id}
104
+ user_taggings.each{|tagging| tag.taggings.delete tagging}
105
+ tag.save_or_destroy
106
+ end
107
+
108
+ user_tags.each do |tag|
109
+ tag.users.delete user
110
+ destroy_if_empty(tag)
111
+ end
112
+ save
113
+ reload
114
+ end
115
+
116
+ def arr_of_words(words)
117
+ raise "Passed an invalid data type to tag()" unless words.is_a?(String) || words.is_a?(Array)
118
+ if words.is_a?(String)
119
+ words.squish.split(',').map{|w| w.squish}
120
+ else
121
+ words.map{|w| w.squish}
122
+ end
123
+ end
124
+
125
+ # returns my current tag word list; with optional user, raises exception if user tries to
126
+ # multi-tag with same word when user specified or if duplicate tag
127
+ def tag!(word_or_words, options={})
128
+ user = options[:user]
129
+ arr_of_words(word_or_words).each do |word|
130
+ word = word.downcase unless options[:case_sensitive] == true
131
+ raise StandardError if (user && tag_words_by_user(user).include?(word)) || (model_tag = model_tags.detect{|tag| tag.word == word})
132
+
133
+ if user || !model_tag
134
+ #First add Tag/Tagging
135
+ t = Tag.first(:word => word) || Tag.create!(:word => word)
136
+ t.taggings << Tagging.new(:user => user, :taggable => self)
137
+ t.save
138
+ end
139
+
140
+ #Now add ModelTag/User/tag_word
141
+ unless model_tag
142
+ model_tag = ModelTag.new(:word => word, :tag => t)
143
+ self.model_tags << model_tag
144
+ self.tag_words << word
145
+ end
146
+ model_tag.users << user if user
147
+ end
148
+ save!
149
+ tags
150
+ end
151
+
152
+ # tags, with optional user, but silently ignores if user tries to multi-tag with same word
153
+ # NOTE: automatically downcases each word unless you manually specify :case_sensitive=>true
154
+ def tag(word_or_words, options={})
155
+ user = options[:user]
156
+ arr_of_words(word_or_words).each do |word|
157
+ word = word.downcase unless options[:case_sensitive] == true
158
+ if user.nil? || (user && !tag_words_by_user(user).include?(word))
159
+ model_tag = model_tags.detect{|tag| tag.word == word}
160
+
161
+ if user || !model_tag
162
+ #First add Tag/Tagging
163
+ t = Tag.first(:word => word) || Tag.create!(:word => word)
164
+ t.taggings << Tagging.new(:user => user, :taggable => self)
165
+ t.save
166
+ end
167
+
168
+ #Now add ModelTag/User/tag_word
169
+ unless model_tag
170
+ model_tag = ModelTag.new(:word => word, :tag => t)
171
+ self.model_tags << model_tag
172
+ self.tag_words << word
173
+ end
174
+ model_tag.users << user if user
175
+ end
176
+ end
177
+ save
178
+ tags
179
+ end
180
+
181
+ # tags anonymously, not associated with user. Doesn't allow duplicate tags.
182
+ # NOTE: automatically downcases each word unless you manually specify :case_sensitive=>true
183
+ # def tag(word_or_words, opts={})
184
+ # arr_of_words(word_or_words).each do |word|
185
+ # word = word.downcase unless opts[:case_sensitive] == true
186
+ # unless model_tags.any?{|tag| tag.word == word}
187
+ # #First add Tag/Tagging
188
+ # t = Tag.first(:word => word) || Tag.create!(:word => word)
189
+ # t.taggings << Tagging.new(:taggable => self)
190
+ # t.save
191
+ #
192
+ # model_tag = ModelTag.new(:word => word, :tag => t)
193
+ # self.model_tags << model_tag
194
+ # self.tag_words << word
195
+ # end
196
+ # end
197
+ # save
198
+ # tags
199
+ # end
200
+
201
+
202
+ # returns the Rating object found if user has rated this project, else returns nil
203
+ def tagged_by_user?(user)
204
+ !(model_tags.detect{|tag| tag.user_ids.include?(user.id)}.nil?)
205
+ end
206
+
207
+ # removes tag and all taggings from object
208
+ # NOTE: automatically downcases each word unless you manually specify :case_sensitive=>true
209
+ def untag(word_or_words, opts={})
210
+ arr_of_words(word_or_words).each do |word|
211
+ word = word.downcase unless opts[:case_sensitive] == true
212
+ model_tag = model_tags.detect{|tag| tag.word == word}
213
+ if model_tag
214
+ tag = model_tag.tag
215
+ taggings = tag.taggings.select{|tagging| tagging.taggable_type == self.class.name && tagging.taggable_id == self.id}
216
+ taggings.each{|tagging| tag.taggings.delete tagging}
217
+ tag.save_or_destroy
218
+ tag_words.delete model_tag.word
219
+ model_tags.delete model_tag
220
+ end
221
+ end
222
+ save
223
+ tags
224
+ end
225
+
226
+ def self.included(receiver)
227
+ receiver.class_eval do
228
+ key :tag_words, Array, :index => true
229
+ many :model_tags
230
+
231
+ ensure_index 'model_tags.word'
232
+ ensure_index 'model_tags.tagging_count'
233
+ end
234
+ receiver.extend ClassMethods
235
+ receiver.send :include, InstanceMethods
236
+
237
+ Tag.register_taggable_type receiver
238
+ end
239
+
240
+ private
241
+ def destroy_if_empty(tag)
242
+ if tag.user_ids.empty?
243
+ tag_words.delete(tag.word)
244
+ model_tags.delete tag
245
+ end
246
+ end
247
+
248
+ end
249
+
250
+ %w{ models observers }.each do |dir|
251
+ path = File.join(File.dirname(__FILE__), 'app', dir)
252
+ $LOAD_PATH << path
253
+ ActiveSupport::Dependencies.load_paths << path
254
+ ActiveSupport::Dependencies.load_once_paths.delete(path)
255
+ end
256
+
@@ -0,0 +1,84 @@
1
+ class ModelTag
2
+ include MongoMapper::EmbeddedDocument
3
+
4
+ key :word, String, :required => true
5
+ key :tagging_count, Integer
6
+ key :user_ids, Array
7
+
8
+ def users
9
+ UserProxy.new(self)
10
+ end
11
+
12
+ belongs_to :tag
13
+
14
+ before_save :set_tagging_count
15
+
16
+ def save_or_destroy
17
+ user_ids.empty? ? destroy : save
18
+ end
19
+
20
+ private
21
+ def set_tagging_count
22
+ self.tagging_count = user_ids.size
23
+ end
24
+
25
+ class UserProxy
26
+ attr_accessor :model_tag
27
+
28
+ def initialize(model_tag)
29
+ @model_tag = model_tag
30
+ end
31
+
32
+ def to_a
33
+ fetch_all.to_a
34
+ end
35
+
36
+ def count
37
+ model_tag.user_ids.size
38
+ end
39
+
40
+ def all(opts = {})
41
+ fetch_all
42
+ end
43
+
44
+ def each(&block)
45
+ fetch_all.each {|user| yield user}
46
+ end
47
+
48
+ def find(id)
49
+ return nil unless model_tag.user_ids.include?(id)
50
+ User.find(id)
51
+ end
52
+
53
+ def first(opts = {})
54
+ return @first ||= User.find(model_tag.user_ids.first) if opts.empty?
55
+ User.first(opts.merge(:_id.in => model_tag.user_ids))
56
+ end
57
+
58
+ def last(opts = {})
59
+ return @last ||= User.find(model_tag.user_ids.last) if opts.empty?
60
+ User.last(opts.merge(:_id.in => model_tag.user_ids))
61
+ end
62
+
63
+ alias :size :count
64
+
65
+ def << (user)
66
+ model_tag.user_ids << user.id
67
+ model_tag.send(:set_tagging_count)
68
+ end
69
+
70
+ def delete(user)
71
+ model_tag.user_ids.delete user.id
72
+ model_tag.send(:set_tagging_count)
73
+ end
74
+
75
+ def inspect
76
+ all.inspect
77
+ end
78
+
79
+ private
80
+ def fetch_all
81
+ @fetch ||= User.find(model_tag.user_ids)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,107 @@
1
+ class Tag
2
+ include MongoMapper::Document
3
+ key :word, String, :required => true, :index => true
4
+ key :taggings_count, Integer, :index => true
5
+
6
+ many :taggings do
7
+ def << (tagging)
8
+ super << tagging
9
+ tagging._parent_document.send(:increment_counts, tagging)
10
+ end
11
+
12
+ def delete(tagging)
13
+ target.delete tagging
14
+ tagging._parent_document.send(:decrement_counts, tagging)
15
+ end
16
+ end
17
+
18
+ ensure_index 'taggings.user_id'
19
+ ensure_index 'taggings.taggable_type'
20
+ ensure_index 'taggings.taggable_id'
21
+
22
+ before_save :set_tagging_counts
23
+
24
+ def self.register_taggable_type(type)
25
+ key taggings_count_key_for(type), Integer, :index => true
26
+ end
27
+
28
+ # == Various Class Methods
29
+
30
+ # takes a string and produces an array of words from the db that are 'like' this one
31
+ # great for those oh-so-fancy autocomplete/suggestion text fields
32
+ def self.like(string, klass = nil)
33
+ opts = {:word => /^#{string}/}
34
+ opts['taggings.taggable_type'] = klass.to_s if klass
35
+ all(opts)
36
+ end
37
+
38
+ def self.all_for_class(klass, opts = {})
39
+ all(opts.merge('taggings.taggable_type' => klass.to_s))
40
+ end
41
+
42
+ def self.most_tagged(klass = nil, opts = {})
43
+ order = klass ? "#{taggings_count_key_for(klass)} desc" : 'taggings_count desc'
44
+ lo = opts.merge(:order => order)
45
+ lo['taggings.taggable_type'] = klass.to_s if klass
46
+ all(lo)
47
+ end
48
+
49
+ def self.top_25(klass = nil)
50
+ most_tagged(klass, :limit => 25)
51
+ end
52
+
53
+ def count_for(klass = nil)
54
+ klass ? send(taggings_count_key_for(klass)) : taggings_count
55
+ end
56
+
57
+ #Called when removing taggings. If no taggings left, destroy, otherwise save
58
+ def save_or_destroy
59
+ taggings.empty? ? destroy : save
60
+ end
61
+
62
+ private
63
+ def set_tagging_counts
64
+ self.taggings_count = self.taggings.size
65
+
66
+ count_hash = self.taggings.inject({}) do |hash, tagging|
67
+ key = taggings_count_key_for(tagging.taggable_type)
68
+ hash[key] ||= 0
69
+ hash[key] += 1
70
+ hash
71
+ end
72
+ count_hash.each{|key, count| self.send("#{key}=", count)}
73
+ end
74
+
75
+ def increment_counts(tagging)
76
+ safe_increment_count(:taggings_count)
77
+ safe_increment_count(taggings_count_key_for(tagging.taggable_type))
78
+ end
79
+
80
+ def decrement_counts(tagging)
81
+ safe_decrement_count(:taggings_count)
82
+ safe_decrement_count(taggings_count_key_for(tagging.taggable_type))
83
+ end
84
+
85
+ def taggings_count_key_for(type)
86
+ Tag.taggings_count_key_for(type)
87
+ end
88
+
89
+ def safe_increment_count(key)
90
+ val = self.send(key) || 0
91
+ self.send("#{key}=", val + 1)
92
+ end
93
+
94
+ def safe_decrement_count(key)
95
+ self.send("#{key}=", self.send("#{key}") - 1) if self.send("#{key}")
96
+ end
97
+
98
+ def self.taggings_count_key_for(type)
99
+ type = type.constantize if type.is_a? String
100
+ #check for inheritance, get most superclass with include
101
+ while type.superclass && type.superclass.include?(ActsAsMongoTaggable)
102
+ type = type.superclass
103
+ end
104
+ type = type.name
105
+ :"#{type.underscore}_taggings_count"
106
+ end
107
+ end
@@ -0,0 +1,6 @@
1
+ class Tagging
2
+ include MongoMapper::EmbeddedDocument
3
+
4
+ belongs_to :user #, :required => true, :index => true
5
+ belongs_to :taggable, :polymorphic => true
6
+ end
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + '/../lib/acts_as_mongo_taggable.rb'
@@ -0,0 +1,205 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class ActsAsMongoTaggableTest < ActiveSupport::TestCase
4
+
5
+ def create_user(name)
6
+ u = User.create({:name => name})
7
+ puts u.errors unless u.valid?
8
+ u
9
+ end
10
+
11
+ def load_multiple_taggers
12
+ @m_tagger_1 = create_user "m_tagger_1"
13
+ @m_tagger_2 = create_user "m_tagger_2"
14
+ @m_tagger_3 = create_user "m_tagger_3"
15
+ end
16
+
17
+ def multi_tag(obj)
18
+ load_multiple_taggers
19
+ obj.tag('frankenstein', @m_tagger_1)
20
+ obj.tag('frankenstein', @m_tagger_2)
21
+ obj.tag('vampires', @m_tagger_1)
22
+ obj.tag('werewolves', @m_tagger_1)
23
+ obj.tag('werewolves', @m_tagger_2)
24
+ obj.tag('werewolves', @m_tagger_3)
25
+ end
26
+
27
+ def setup
28
+ @owner = create_user 'owner'
29
+ @tagger = create_user 'tagger'
30
+ @widget = @owner.widgets.create({:name => "Test Widget"})
31
+ end
32
+
33
+ test "widget tagged with the same word multiple times should not have dupes in tag_words" do
34
+ multi_tag(@widget)
35
+ assert_equal 3, @widget.tag_words.size
36
+ @widget.delete_tags_by_user(@m_tagger_1)
37
+ assert_equal 2, @widget.tag_words.size
38
+ end
39
+
40
+ test "ensure we can actually tag two different object types without collisions" do
41
+ dongle_owner = create_user "dongle_owner"
42
+ dongle = dongle_owner.dongles.create({:name => "Test Dongle"})
43
+ # tag the widget
44
+ multi_tag(@widget)
45
+ assert_equal 3, @widget.tags.size
46
+ assert_equal 0, dongle.tags.size
47
+ # tag the dongle
48
+ multi_tag(dongle)
49
+ assert_equal 3, @widget.tags.size
50
+ assert_equal 3, dongle.tags.size
51
+ # delete from the widget
52
+ @widget.delete_all_tags
53
+ assert_equal 0, @widget.tags.size
54
+ assert_equal 3, dongle.tags.size
55
+ # delete from the dongle
56
+ dongle.delete_all_tags
57
+ assert_equal 0, @widget.tags.size
58
+ assert_equal 0, dongle.tags.size
59
+ end
60
+
61
+ test "search and find all widgets containing a specified tag" do
62
+ num_found = Widget.all_with_tag('vampires').size
63
+ assert_equal 0, num_found
64
+ @widget.tag("vampires", @tagger)
65
+ num_found = Widget.all_with_tag('vampires').size
66
+ assert_equal 1, num_found
67
+ dongle_owner = create_user "dongle_owner"
68
+ dongle = dongle_owner.dongles.create({:name => "Test Dongle"})
69
+ multi_tag(dongle)
70
+ num_found = Widget.all_with_tag('vampires').size
71
+ # should only be 1 because we're only searching on Widgets!
72
+ assert_equal 1, num_found
73
+ end
74
+
75
+ test "find first widget that matches specified tag" do
76
+ @widget.tag("vampires", @tagger)
77
+ dongle_owner = create_user "dongle_owner"
78
+ dongle = dongle_owner.dongles.create({:name => "Test Dongle"})
79
+ # should only be 1 because we're only searching on Widgets!
80
+ assert_equal 1, Widget.all_with_tag('vampires').size
81
+ assert_equal @widget, Widget.first_with_tag('vampires')
82
+ end
83
+
84
+ test "all_with_tag and first_with_tag are case-insensitive" do
85
+ @widget.tag("VaMpiReS", @tagger)
86
+ @widget.tag("VAMPIRES", @tagger)
87
+ assert_equal @widget, Widget.first_with_tag("VampireS")
88
+ assert_equal 1, Widget.all_with_tag("VampireS").size
89
+ end
90
+
91
+ test "case-sensitive mode" do
92
+ @widget.tag("VaMpiReS", @tagger, {:case_sensitive => true})
93
+ assert_equal 0, Widget.all_with_tag("vampires").size
94
+ assert_equal 0, Widget.all_with_tag("VaMpiReS").size
95
+ assert_equal 0, Widget.all_with_tag("vampires", {:case_sensitive => true}).size
96
+ assert_equal 1, Widget.all_with_tag("VaMpiReS", {:case_sensitive => true}).size
97
+ end
98
+
99
+ test "we get an empty array if we ask for all tags with counts and there are none" do
100
+ assert_equal 0, Tag.count
101
+ assert_equal [], Widget.all_tags_with_counts
102
+ end
103
+
104
+ test "we can build an array of tags and counts across an entire tagged object space" do
105
+ multi_tag(@widget)
106
+ dongle_owner = create_user "dongle_owner"
107
+ dongle = dongle_owner.dongles.create({:name => "Test Dongle"})
108
+ dongle.tag("werewolves", @m_tagger_1)
109
+ dongle.tag("werewolves", @m_tagger_2)
110
+ assert_equal [["werewolves", 3], ["frankenstein", 2], ["vampires", 1]], Widget.all_tags_with_counts
111
+ assert_equal [["werewolves", 2]], Dongle.all_tags_with_counts
112
+ end
113
+
114
+ test "most_tagged_with returns the proper widget" do
115
+ widget_one = @owner.widgets.create({:name => "Test Widget One"})
116
+ widget_two = @owner.widgets.create({:name => "Test Widget Two"})
117
+ widget_three = @owner.widgets.create({:name => "Test Widget Three"})
118
+ load_multiple_taggers
119
+ #widget one -- worst
120
+ widget_one.tag("werewolves", @m_tagger_1)
121
+ #widget two -- best
122
+ multi_tag(widget_two)
123
+ #widget three -- middle child
124
+ widget_three.tag("werewolves", @m_tagger_1)
125
+ widget_three.tag("werewolves", @m_tagger_2)
126
+ # now, did it work?
127
+ assert_equal widget_two, Widget.most_tagged_with('werewolves')
128
+ end
129
+
130
+ test "tag is created when project tagged" do
131
+ assert_equal 0, @widget.tags.size
132
+ @widget.tag("vampires", @tagger)
133
+ assert_equal 1, @widget.tags.size
134
+ end
135
+
136
+ test "tagged_by_user? returns true when this object tagged by user" do
137
+ assert ! @widget.tagged_by_user?(@tagger)
138
+ @widget.tag("vampires", @tagger)
139
+ assert @widget.tagged_by_user?(@tagger)
140
+ end
141
+
142
+ test "tagged_by_user? returns false when this object not tagged but some other object is" do
143
+ dongle_owner = create_user "dongle_owner"
144
+ dongle = dongle_owner.dongles.create({:name => "Test Dongle"})
145
+ dongle.tag("vampires", @tagger)
146
+ assert ! @widget.tagged_by_user?(@tagger)
147
+ end
148
+
149
+ test "tag object can be retrieved after project tagged" do
150
+ assert_equal 0, @widget.tags.size
151
+ @widget.tag("werewolves", @tagger)
152
+ assert_equal "werewolves", @widget.tags.first
153
+ end
154
+
155
+ test "widget returns correct array with multiple tags with 1 count each" do
156
+ %w(vampires werewolves frankenstein).each {|word| @widget.tag(word, @tagger)}
157
+ assert_equal 3, @widget.tags.size
158
+ ["vampires", "werewolves", "frankenstein"].each { |t| assert @widget.tags.include?(t) }
159
+ end
160
+
161
+ test "widget returns correct array with multiple tags with varying counts" do
162
+ multi_tag(@widget)
163
+ assert_equal 3, @widget.tags.size
164
+ assert_equal ["werewolves", "frankenstein", "vampires"], @widget.tags
165
+ end
166
+
167
+ test "tags_with_counts returns the right tags and counts" do
168
+ expected = [["werewolves", 3], ["frankenstein", 2], ["vampires", 1]]
169
+ multi_tag(@widget)
170
+ assert_equal expected, @widget.tags_with_counts
171
+ end
172
+
173
+ test "same user cannot multi-tag with same word using tag!()" do
174
+ assert_raise StandardError do
175
+ 3.times { @widget.tag!('frankenstein', @tagger) }
176
+ end
177
+ end
178
+
179
+ test "delete only tags by certain users" do
180
+ multi_tag(@widget)
181
+ assert_equal 3, @widget.tags.size
182
+ assert_equal 6, @widget.model_tags.inject(0){|r, tag| r + tag.tagging_count}
183
+
184
+ @widget.delete_tags_by_user(@m_tagger_1)
185
+ assert_equal 2, @widget.tags.size
186
+ assert_equal 3, @widget.model_tags.inject(0){|r, tag| r + tag.tagging_count}
187
+
188
+ @widget.delete_tags_by_user(@m_tagger_2)
189
+ assert_equal 1, @widget.tags.size
190
+ assert_equal 1, @widget.model_tags.inject(0){|r, tag| r + tag.tagging_count}
191
+ end
192
+
193
+ test "silently ignore multi-tag by single user with same word using tag()" do
194
+ 3.times { @widget.tag('frankenstein', @tagger) }
195
+ assert_equal 1, @widget.tags.size
196
+ end
197
+
198
+ test "widget with multiple tags can be cleared" do
199
+ %w(vampires werewolves frankenstein).each {|word| @widget.tag(word, @tagger)}
200
+ assert_equal 3, @widget.tags.size
201
+ @widget.delete_all_tags
202
+ assert_equal 0, @widget.tags.size
203
+ end
204
+
205
+ end
@@ -0,0 +1,63 @@
1
+ require 'rubygems'
2
+ require 'active_support'
3
+ require 'active_support/test_case'
4
+ require 'test/unit'
5
+
6
+ ENV['RAILS_ROOT'] ||= File.dirname(__FILE__) + '/../../../..'
7
+ env_rb = File.expand_path(File.join(ENV['RAILS_ROOT'], 'config/environment.rb'))
8
+
9
+ if File.exists? env_rb
10
+ require env_rb
11
+ else
12
+ require 'mongo_mapper'
13
+ require File.dirname(__FILE__) + '/../lib/acts_as_mongo_taggable'
14
+ config = {'test' => {'database' => 'aamt-test'}}
15
+ MongoMapper.setup(config, 'test')
16
+ end
17
+
18
+ class ActiveSupport::TestCase
19
+ # Drop all columns after each test case.
20
+ def teardown
21
+ MongoMapper.database.collections.each do |coll|
22
+ coll.drop
23
+ end
24
+ end
25
+
26
+ # Make sure that each test case has a teardown
27
+ # method to clear the db after each test.
28
+ def inherited(base)
29
+ base.define_method teardown do
30
+ super
31
+ end
32
+ end
33
+ end
34
+
35
+ # kinda weird, but we have to do this so we can ignore the app's User class and use our own for testing
36
+ Object.send(:remove_const, :User) if Object.const_defined?(:User)
37
+
38
+ class User
39
+ include MongoMapper::Document
40
+ key :name, String
41
+ has_many :widgets
42
+ has_many :dongles
43
+ end
44
+
45
+ class Widget
46
+ include MongoMapper::Document
47
+ include ActsAsMongoTaggable
48
+
49
+ belongs_to :user
50
+
51
+ key :user_id, ObjectId
52
+ key :name, String
53
+ end
54
+
55
+ class Dongle
56
+ include MongoMapper::Document
57
+ include ActsAsMongoTaggable
58
+
59
+ belongs_to :user
60
+
61
+ key :user_id, ObjectId
62
+ key :name, String
63
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yoomee-acts_as_mongo_taggable
3
+ version: !ruby/object:Gem::Version
4
+ hash: 19
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 2
10
+ version: 0.2.2
11
+ platform: ruby
12
+ authors:
13
+ - Ian Mooney
14
+ - Matt Atkins
15
+ - Matt E. Patterson
16
+ autorequire:
17
+ bindir: bin
18
+ cert_chain: []
19
+
20
+ date: 2010-08-17 00:00:00 +01:00
21
+ default_executable:
22
+ dependencies:
23
+ - !ruby/object:Gem::Dependency
24
+ name: mongo_mapper
25
+ prerelease: false
26
+ requirement: &id001 !ruby/object:Gem::Requirement
27
+ none: false
28
+ requirements:
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ hash: 3
32
+ segments:
33
+ - 0
34
+ - 7
35
+ - 0
36
+ version: 0.7.0
37
+ type: :runtime
38
+ version_requirements: *id001
39
+ description: A ruby gem for acts_as_taggable to mongo
40
+ email: matt@yoomee.com
41
+ executables: []
42
+
43
+ extensions: []
44
+
45
+ extra_rdoc_files:
46
+ - README.markdown
47
+ files:
48
+ - MIT-LICENSE
49
+ - Rakefile
50
+ - lib/acts_as_mongo_taggable.rb
51
+ - lib/app/models/model_tag.rb
52
+ - lib/app/models/tag.rb
53
+ - lib/app/models/tagging.rb
54
+ - rails/init.rb
55
+ - README.markdown
56
+ - test/acts_as_mongo_taggable_test.rb
57
+ - test/test_helper.rb
58
+ has_rdoc: true
59
+ homepage: http://github.com/yoomee/acts_as_mongo_taggable
60
+ licenses: []
61
+
62
+ post_install_message:
63
+ rdoc_options:
64
+ - --charset=UTF-8
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ hash: 3
73
+ segments:
74
+ - 0
75
+ version: "0"
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ hash: 3
82
+ segments:
83
+ - 0
84
+ version: "0"
85
+ requirements: []
86
+
87
+ rubyforge_project:
88
+ rubygems_version: 1.3.7
89
+ signing_key:
90
+ specification_version: 3
91
+ summary: A ruby gem for acts_as_taggable to mongo
92
+ test_files:
93
+ - test/acts_as_mongo_taggable_test.rb
94
+ - test/test_helper.rb