wxmn-acts_as_mongo_taggable 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,208 @@
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; raises exception if user tries to multi-tag with same word
126
+ def tag!(word_or_words, user)
127
+ arr_of_words(word_or_words).each do |word|
128
+ raise StandardError if tag_words_by_user(user).include?(word)
129
+
130
+ #First add Tag/Tagging
131
+ t = Tag.first(:word => word) || Tag.create!(:word => word)
132
+ t.taggings << Tagging.new(:user => user, :taggable => self)
133
+ t.save!
134
+
135
+ #Now add ModelTag/User/tag_word
136
+ model_tag = model_tags.detect{|tag| tag.word == word}
137
+ unless model_tag
138
+ model_tag = ModelTag.new(:word => word, :tag => t)
139
+ self.model_tags << model_tag
140
+ self.tag_words << word
141
+ end
142
+ model_tag.users << user
143
+ end
144
+ save!
145
+ tags
146
+ end
147
+
148
+ # tags, but silently ignores if user tries to multi-tag with same word
149
+ # NOTE: automatically downcases each word unless you manually specify :case_sensitive=>true
150
+ def tag(word_or_words, user, opts={})
151
+ arr_of_words(word_or_words).each do |word|
152
+ word = word.downcase unless opts[:case_sensitive] == true
153
+ unless tag_words_by_user(user).include?(word)
154
+ #First add Tag/Tagging
155
+ t = Tag.first(:word => word) || Tag.create!(:word => word)
156
+ t.taggings << Tagging.new(:user => user, :taggable => self)
157
+ t.save
158
+
159
+ #Now add ModelTag/User/tag_word
160
+ model_tag = model_tags.detect{|tag| tag.word == word}
161
+ unless model_tag
162
+ model_tag = ModelTag.new(:word => word, :tag => t)
163
+ self.model_tags << model_tag
164
+ self.tag_words << word
165
+ end
166
+ model_tag.users << user
167
+ end
168
+ end
169
+ save
170
+ tags
171
+ end
172
+
173
+ # returns the Rating object found if user has rated this project, else returns nil
174
+ def tagged_by_user?(user)
175
+ !(model_tags.detect{|tag| tag.user_ids.include?(user.id)}.nil?)
176
+ end
177
+
178
+ def self.included(receiver)
179
+ receiver.class_eval do
180
+ key :tag_words, Array, :index => true
181
+ many :model_tags
182
+
183
+ ensure_index 'model_tags.word'
184
+ ensure_index 'model_tags.tagging_count'
185
+ end
186
+ receiver.extend ClassMethods
187
+ receiver.send :include, InstanceMethods
188
+
189
+ Tag.register_taggable_type receiver
190
+ end
191
+
192
+ private
193
+ def destroy_if_empty(tag)
194
+ if tag.user_ids.empty?
195
+ tag_words.delete(tag.word)
196
+ model_tags.delete tag
197
+ end
198
+ end
199
+
200
+ end
201
+
202
+ %w{ models observers }.each do |dir|
203
+ path = File.join(File.dirname(__FILE__), 'app', dir)
204
+ $LOAD_PATH << path
205
+ ActiveSupport::Dependencies.load_paths << path
206
+ ActiveSupport::Dependencies.load_once_paths.delete(path)
207
+ end
208
+
@@ -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.count
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,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wxmn-acts_as_mongo_taggable
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Matt E. Patterson
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-03-22 00:00:00 -04:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: mongo_mapper
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - "="
28
+ - !ruby/object:Gem::Version
29
+ hash: 59
30
+ segments:
31
+ - 0
32
+ - 8
33
+ - 2
34
+ version: 0.8.2
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ description: A ruby gem for acts_as_taggable to mongo
38
+ email: mpatterson@ngenera.com
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ extra_rdoc_files:
44
+ - README.markdown
45
+ files:
46
+ - MIT-LICENSE
47
+ - Rakefile
48
+ - lib/acts_as_mongo_taggable.rb
49
+ - lib/app/models/model_tag.rb
50
+ - lib/app/models/tag.rb
51
+ - lib/app/models/tagging.rb
52
+ - rails/init.rb
53
+ - README.markdown
54
+ - test/acts_as_mongo_taggable_test.rb
55
+ - test/test_helper.rb
56
+ has_rdoc: true
57
+ homepage: http://github.com/wxmn/acts_as_mongo_taggable
58
+ licenses: []
59
+
60
+ post_install_message:
61
+ rdoc_options:
62
+ - --charset=UTF-8
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ hash: 3
71
+ segments:
72
+ - 0
73
+ version: "0"
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ hash: 3
80
+ segments:
81
+ - 0
82
+ version: "0"
83
+ requirements: []
84
+
85
+ rubyforge_project:
86
+ rubygems_version: 1.3.7
87
+ signing_key:
88
+ specification_version: 3
89
+ summary: A ruby gem for acts_as_taggable to mongo
90
+ test_files:
91
+ - test/acts_as_mongo_taggable_test.rb
92
+ - test/test_helper.rb