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 +20 -0
- data/README.markdown +102 -0
- data/Rakefile +58 -0
- data/lib/acts_as_mongo_taggable.rb +256 -0
- data/lib/app/models/model_tag.rb +84 -0
- data/lib/app/models/tag.rb +107 -0
- data/lib/app/models/tagging.rb +6 -0
- data/rails/init.rb +1 -0
- data/test/acts_as_mongo_taggable_test.rb +205 -0
- data/test/test_helper.rb +63 -0
- metadata +94 -0
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
|
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
|
data/test/test_helper.rb
ADDED
@@ -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
|