mbleigh-acts-as-taggable-on 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,9 @@
1
+ == 2008-06-09
2
+
3
+ * Added support for Single Table Inheritance
4
+ * Adding gemspec and rails/init.rb for gemified plugin
5
+
6
+ == 2007-12-12
7
+
8
+ * Added ability to use dynamic tag contexts
9
+ * Fixed missing migration generator
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 Michael Bleigh and Intridea Inc.
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 ADDED
@@ -0,0 +1,154 @@
1
+ ActsAsTaggableOn
2
+ ================
3
+
4
+ This plugin was originally based on Acts as Taggable on Steroids by Jonathan Viney.
5
+ It has evolved substantially since that point, but all credit goes to him for the
6
+ initial tagging functionality that so many people have used.
7
+
8
+ For instance, in a social network, a user might have tags that are called skills,
9
+ interests, sports, and more. There is no real way to differentiate between tags and
10
+ so an implementation of this type is not possible with acts as taggable on steroids.
11
+
12
+ Enter Acts as Taggable On. Rather than tying functionality to a specific keyword
13
+ (namely "tags"), acts as taggable on allows you to specify an arbitrary number of
14
+ tag "contexts" that can be used locally or in combination in the same way steroids
15
+ was used.
16
+
17
+ Installation
18
+ ============
19
+
20
+ Plugin
21
+ ------
22
+
23
+ Acts As Taggable On is available both as a gem and as a traditional plugin. For the
24
+ traditional plugin you can install like so (Rails 2.1 or later):
25
+
26
+ script/plugin install git://github.com/mbleigh/acts-as-taggable-on.git
27
+
28
+ For earlier versions:
29
+
30
+ git clone git://github.com/mbleigh/acts-as-taggable-on.git vendor/plugins/acts-as-taggable-on
31
+
32
+ GemPlugin
33
+ ---------
34
+
35
+ Acts As Taggable On is also available as a gem plugin using Rails 2.1's gem dependencies.
36
+ To install the gem, add this to your config/environment.rb:
37
+
38
+ config.gem "mbleigh-acts-as-taggable-on", :source => "http://gems.github.com"
39
+
40
+ After that, you can run "rake gems:install" to install the gem if you don't already have it.
41
+ See http://ryandaigle.com/articles/2008/4/1/what-s-new-in-edge-rails-gem-dependencies for
42
+ additional details about gem dependencies in Rails.
43
+
44
+ Testing
45
+ =======
46
+
47
+ Acts As Taggable On uses RSpec for its test coverage. If you already have RSpec on your
48
+ application, the specs will run while using:
49
+
50
+ rake spec:plugins
51
+
52
+ Example
53
+ =======
54
+
55
+ class User < ActiveRecord::Base
56
+ acts_as_taggable_on :tags, :skills, :interests
57
+ end
58
+
59
+ @user = User.new(:name => "Bobby")
60
+ @user.tag_list = "awesome, slick, hefty" # this should be familiar
61
+ @user.skill_list = "joking, clowning, boxing" # but you can do it for any context!
62
+ @user.skill_list # => ["joking","clowning","boxing"] as TagList
63
+ @user.save
64
+
65
+ @user.tags # => [<Tag name:"awesome">,<Tag name:"slick">,<Tag name:"hefty">]
66
+ @user.skills # => [<Tag name:"joking">,<Tag name:"clowning">,<Tag name:"boxing">]
67
+
68
+ User.find_tagged_with("awesome", :on => :tags) # => [@user]
69
+ User.find_tagged_with("awesome", :on => :skills) # => []
70
+
71
+ @frankie = User.create(:name => "Frankie", :skill_list => "joking, flying, eating")
72
+ User.skill_counts # => [<Tag name="joking" count=2>,<Tag name="clowning" count=1>...]
73
+ @frankie.skill_counts
74
+
75
+ Relationships
76
+ ====================
77
+
78
+ You can find objects of the same type based on similar tags on certain contexts.
79
+ Also, objects will be returned in descending order based on the total number of
80
+ matched tags.
81
+
82
+ @bobby = User.find_by_name("Bobby")
83
+ @bobby.skill_list # => ["jogging", "diving"]
84
+
85
+ @frankie = User.find_by_name("Frankie")
86
+ @frankie.skill_list # => ["hacking"]
87
+
88
+ @tom = User.find_by_name("Tom")
89
+ @tom.skill_list # => ["hacking", "jogging", "diving"]
90
+
91
+ @tom.find_related_skills # => [<User name="Bobby">,<User name="Frankie">]
92
+ @bobby.find_related_skills # => [<User name="Tom">]
93
+ @frankie.find_related_skills # => [<User name="Tom">]
94
+
95
+
96
+ Dynamic Tag Contexts
97
+ ====================
98
+
99
+ In addition to the generated tag contexts in the definition, it is also possible
100
+ to allow for dynamic tag contexts (this could be user generated tag contexts!)
101
+
102
+ @user = User.new(:name => "Bobby")
103
+ @user.set_tag_list_on(:customs, "same, as, tag, list")
104
+ @user.tag_list_on(:customs) # => ["same","as","tag","list"]
105
+ @user.save
106
+ @user.tags_on(:customs) # => [<Tag name='same'>,...]
107
+ @user.tag_counts_on(:customs)
108
+ User.find_tagged_with("same", :on => :customs) # => [@user]
109
+
110
+ Tag Ownership
111
+ =============
112
+
113
+ Tags can have owners:
114
+
115
+ class User < ActiveRecord::Base
116
+ acts_as_tagger
117
+ end
118
+
119
+ class Photo < ActiveRecord::Base
120
+ acts_as_taggable_on :locations
121
+ end
122
+
123
+ @some_user.tag(@some_photo, :with => "paris, normandy", :on => :locations)
124
+ @some_user.owned_taggings
125
+ @some_user.owned_tags
126
+ @some_photo.locations_from(@some_user)
127
+
128
+ Caveats, Uncharted Waters
129
+ =========================
130
+
131
+ This plugin is still under active development. Tag caching has not
132
+ been thoroughly (or even casually) tested and may not work as expected.
133
+
134
+ Contributors
135
+ ============
136
+
137
+ * Michael Bleigh - Original Author
138
+ * Brendan Lim - Related objects
139
+ * Pradeep Elankumaran - Taggers
140
+
141
+ Patch Contributors
142
+ ------------------
143
+
144
+ * Peter Cooper - named_scope fix
145
+ * slainer68 - STI fix
146
+
147
+ Resources
148
+ =========
149
+
150
+ * Acts As Community - http://www.actsascommunity.com/projects/acts-as-taggable-on
151
+ * GitHub - http://github.com/mbleigh/acts-as-taggable-on
152
+ * Lighthouse - http://mbleigh.lighthouseapp.com/projects/10116-acts-as-taggable-on
153
+
154
+ Copyright (c) 2007 Michael Bleigh (http://mbleigh.com/) and Intridea Inc. (http://intridea.com/), released under the MIT license
@@ -0,0 +1,8 @@
1
+ class ActsAsTaggableOnMigrationGenerator < Rails::Generator::Base
2
+ def manifest
3
+ record do |m|
4
+ m.migration_template 'migration.rb', 'db/migrate', :migration_file_name => "acts_as_taggable_on_migration"
5
+ m.migration_template 'add_users_migration.rb', 'db/migrate', :migration_file_name => "add_users_to_acts_as_taggable_on_migration"
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ class AddUsersToActsAsTaggableOnMigration < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :taggings, :tagger_id, :integer
4
+ add_column :taggings, :tagger_type, :string
5
+ end
6
+
7
+ def self.down
8
+ remove_column :taggings, :tagger_type
9
+ remove_column :taggings, :tagger_id
10
+ end
11
+ end
@@ -0,0 +1,27 @@
1
+ class ActsAsTaggableOnMigration < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :tags do |t|
4
+ t.column :name, :string
5
+ end
6
+
7
+ create_table :taggings do |t|
8
+ t.column :tag_id, :integer
9
+ t.column :taggable_id, :integer
10
+
11
+ # You should make sure that the column created is
12
+ # long enough to store the required class names.
13
+ t.column :taggable_type, :string
14
+ t.column :context, :string
15
+
16
+ t.column :created_at, :datetime
17
+ end
18
+
19
+ add_index :taggings, :tag_id
20
+ add_index :taggings, [:taggable_id, :taggable_type, :context]
21
+ end
22
+
23
+ def self.down
24
+ drop_table :taggings
25
+ drop_table :tags
26
+ end
27
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/rails/init"
@@ -0,0 +1,6 @@
1
+ require 'acts_as_taggable_on/acts_as_taggable_on'
2
+ require 'acts_as_taggable_on/acts_as_tagger'
3
+ require 'acts_as_taggable_on/tag'
4
+ require 'acts_as_taggable_on/tag_list'
5
+ require 'acts_as_taggable_on/tags_helper'
6
+ require 'acts_as_taggable_on/tagging'
@@ -0,0 +1,293 @@
1
+ module ActiveRecord
2
+ module Acts
3
+ module TaggableOn
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ module ClassMethods
9
+ def acts_as_taggable
10
+ acts_as_taggable_on :tags
11
+ end
12
+
13
+ def acts_as_taggable_on(*args)
14
+ puts "Registering #{args.inspect} with #{self.inspect}"
15
+ for tag_type in args
16
+ tag_type = tag_type.to_s
17
+ self.class_eval do
18
+ has_many "#{tag_type.singularize}_taggings".to_sym, :as => :taggable, :dependent => :destroy,
19
+ :include => :tag, :conditions => ["context = ?",tag_type], :class_name => "Tagging"
20
+ has_many "#{tag_type}".to_sym, :through => "#{tag_type.singularize}_taggings".to_sym, :source => :tag
21
+ end
22
+
23
+ self.class_eval <<-RUBY
24
+ def self.caching_#{tag_type.singularize}_list?
25
+ caching_tag_list_on?("#{tag_type}")
26
+ end
27
+
28
+ def self.#{tag_type.singularize}_counts(options={})
29
+ tag_counts_on('#{tag_type}',options)
30
+ end
31
+
32
+ def #{tag_type.singularize}_list
33
+ tag_list_on('#{tag_type}')
34
+ end
35
+
36
+ def #{tag_type.singularize}_list=(new_tags)
37
+ set_tag_list_on('#{tag_type}',new_tags)
38
+ end
39
+
40
+ def #{tag_type.singularize}_counts(options = {})
41
+ tag_counts_on('#{tag_type}',options)
42
+ end
43
+
44
+ def #{tag_type}_from(owner)
45
+ tag_list_on('#{tag_type}', owner)
46
+ end
47
+
48
+ def find_related_#{tag_type}(options = {})
49
+ related_tags_on('#{tag_type}',options)
50
+ end
51
+ alias_method :find_related_on_#{tag_type}, :find_related_#{tag_type}
52
+ RUBY
53
+ end
54
+
55
+ if respond_to?(:tag_types)
56
+ puts "Appending #{args.inspect} onto #{tag_types.inspect}"
57
+ write_inheritable_attribute(:tag_types, tag_types + args)
58
+ else
59
+ self.class_eval do
60
+ write_inheritable_attribute(:tag_types, args)
61
+ class_inheritable_reader :tag_types
62
+
63
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
64
+ has_many :base_tags, :class_name => "Tag", :through => :taggings, :source => :tag
65
+
66
+ attr_writer :custom_contexts
67
+
68
+ before_save :save_cached_tag_list
69
+ after_save :save_tags
70
+ end
71
+
72
+ include ActiveRecord::Acts::TaggableOn::InstanceMethods
73
+ extend ActiveRecord::Acts::TaggableOn::SingletonMethods
74
+ alias_method_chain :reload, :tag_list
75
+ end
76
+ end
77
+
78
+ def is_taggable?
79
+ false
80
+ end
81
+ end
82
+
83
+ module SingletonMethods
84
+ # Pass either a tag string, or an array of strings or tags
85
+ #
86
+ # Options:
87
+ # :exclude - Find models that are not tagged with the given tags
88
+ # :match_all - Find models that match all of the given tags, not just one
89
+ # :conditions - A piece of SQL conditions to add to the query
90
+ # :on - scopes the find to a context
91
+ def find_tagged_with(*args)
92
+ options = find_options_for_find_tagged_with(*args)
93
+ options.blank? ? [] : find(:all,options)
94
+ end
95
+
96
+ def caching_tag_list_on?(context)
97
+ column_names.include?("cached_#{context.to_s.singularize}_list")
98
+ end
99
+
100
+ def tag_counts_on(context, options = {})
101
+ Tag.find(:all, find_options_for_tag_counts(options.merge({:on => context.to_s})))
102
+ end
103
+
104
+ def find_options_for_find_tagged_with(tags, options = {})
105
+ tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
106
+
107
+ return {} if tags.empty?
108
+
109
+ conditions = []
110
+ conditions << sanitize_sql(options.delete(:conditions)) if options[:conditions]
111
+
112
+ unless (on = options.delete(:on)).nil?
113
+ conditions << sanitize_sql(["context = ?",on.to_s])
114
+ end
115
+
116
+ taggings_alias, tags_alias = "#{table_name}_taggings", "#{table_name}_tags"
117
+
118
+ if options.delete(:exclude)
119
+ tags_conditions = tags.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
120
+ conditions << sanitize_sql(["#{table_name}.id NOT IN (SELECT #{Tagging.table_name}.taggable_id FROM #{Tagging.table_name} LEFT OUTER JOIN #{Tag.table_name} ON #{Tagging.table_name}.tag_id = #{Tag.table_name}.id WHERE (#{tags_conditions}) AND #{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})", tags])
121
+ else
122
+ conditions << tags.map { |t| sanitize_sql(["#{tags_alias}.name LIKE ?", t]) }.join(" OR ")
123
+
124
+ if options.delete(:match_all)
125
+ group = "#{taggings_alias}.taggable_id HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
126
+ end
127
+ end
128
+
129
+ { :select => "DISTINCT #{table_name}.*",
130
+ :joins => "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias} ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key} AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)} " +
131
+ "LEFT OUTER JOIN #{Tag.table_name} #{tags_alias} ON #{tags_alias}.id = #{taggings_alias}.tag_id",
132
+ :conditions => conditions.join(" AND "),
133
+ :group => group
134
+ }.update(options)
135
+ end
136
+
137
+ # Calculate the tag counts for all tags.
138
+ #
139
+ # Options:
140
+ # :start_at - Restrict the tags to those created after a certain time
141
+ # :end_at - Restrict the tags to those created before a certain time
142
+ # :conditions - A piece of SQL conditions to add to the query
143
+ # :limit - The maximum number of tags to return
144
+ # :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
145
+ # :at_least - Exclude tags with a frequency less than the given value
146
+ # :at_most - Exclude tags with a frequency greater than the given value
147
+ # :on - Scope the find to only include a certain context
148
+ def find_options_for_tag_counts(options = {})
149
+ options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on
150
+
151
+ scope = scope(:find)
152
+ start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
153
+ end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
154
+
155
+ type_and_context = "#{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)}"
156
+
157
+ conditions = [
158
+ type_and_context,
159
+ options[:conditions],
160
+ start_at,
161
+ end_at
162
+ ]
163
+
164
+ conditions = conditions.compact.join(' AND ')
165
+ conditions = merge_conditions(conditions, scope[:conditions]) if scope
166
+
167
+ joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
168
+ joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
169
+ joins << "LEFT OUTER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
170
+ joins << scope[:joins] if scope && scope[:joins]
171
+
172
+ at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
173
+ at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
174
+ having = [at_least, at_most].compact.join(' AND ')
175
+ group_by = "#{Tag.table_name}.id, #{Tag.table_name}.name HAVING COUNT(*) > 0"
176
+ group_by << " AND #{having}" unless having.blank?
177
+
178
+ { :select => "#{Tag.table_name}.id, #{Tag.table_name}.name, COUNT(*) AS count",
179
+ :joins => joins.join(" "),
180
+ :conditions => conditions,
181
+ :group => group_by
182
+ }.update(options)
183
+ end
184
+
185
+ def is_taggable?
186
+ true
187
+ end
188
+ end
189
+
190
+ module InstanceMethods
191
+
192
+ def tag_types
193
+ self.class.tag_types
194
+ end
195
+
196
+ def custom_contexts
197
+ @custom_contexts ||= []
198
+ end
199
+
200
+ def is_taggable?
201
+ self.class.is_taggable?
202
+ end
203
+
204
+ def add_custom_context(value)
205
+ custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s)
206
+ end
207
+
208
+ def tag_list_on(context, owner=nil)
209
+ var_name = context.to_s.singularize + "_list"
210
+ return instance_variable_get("@#{var_name}") unless instance_variable_get("@#{var_name}").nil?
211
+
212
+ if !owner && self.class.caching_tag_list_on?(context) and !(cached_value = cached_tag_list_on(context, owner)).nil?
213
+ instance_variable_set("@#{var_name}", TagList.from(self["cached_#{var_name}"]))
214
+ else
215
+ instance_variable_set("@#{var_name}", TagList.new(*tags_on(context, owner).map(&:name)))
216
+ end
217
+ end
218
+
219
+ def tags_on(context, owner=nil)
220
+ if owner
221
+ opts = {:conditions => ["context = ? AND tagger_id = ? AND tagger_type = ?",
222
+ context.to_s, owner.id, owner.class.to_s]}
223
+ else
224
+ opts = {:conditions => ["context = ?", context.to_s]}
225
+ end
226
+ base_tags.find(:all, opts)
227
+ end
228
+
229
+ def cached_tag_list_on(context)
230
+ self["cached_#{context.to_s.singularize}_list"]
231
+ end
232
+
233
+ def set_tag_list_on(context,new_list, tagger=nil)
234
+ instance_variable_set("@#{context.to_s.singularize}_list", TagList.from_owner(tagger, new_list))
235
+ add_custom_context(context)
236
+ end
237
+
238
+ def tag_counts_on(context,options={})
239
+ self.class.tag_counts_on(context,{:conditions => ["#{Tag.table_name}.name IN (?)", tag_list_on(context)]}.reverse_merge!(options))
240
+ end
241
+
242
+ def related_tags_on(context, options={})
243
+ tags_to_find = self.tags_on(context).collect {|t| t.name}
244
+ search_conditions = {
245
+ :select => "#{self.class.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
246
+ :from => "#{self.class.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
247
+ :conditions => ["#{self.class.table_name}.id != #{self.id} AND #{self.class.table_name}.id = #{Tagging.table_name}.taggable_id AND #{Tagging.table_name}.taggable_type = '#{self.class.to_s}' AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND #{Tag.table_name}.name IN (?)",tags_to_find],
248
+ :group => "#{self.class.table_name}.id",
249
+ :order => "count DESC"
250
+ }.update(options)
251
+
252
+ self.class.find(:all, search_conditions)
253
+ end
254
+
255
+ def save_cached_tag_list
256
+ self.class.tag_types.map(&:to_s).each do |tag_type|
257
+ if self.class.send("caching_#{tag_type.singularize}_list?")
258
+ self["cached_#{tag_type.singularize}_list"] = send("#{tag_type.singularize}_list").to_s
259
+ end
260
+ end
261
+ end
262
+
263
+ def save_tags
264
+ (custom_contexts + self.class.tag_types.map(&:to_s)).each do |tag_type|
265
+ next unless instance_variable_get("@#{tag_type.singularize}_list")
266
+ owner = instance_variable_get("@#{tag_type.singularize}_list").owner
267
+ new_tag_names = instance_variable_get("@#{tag_type.singularize}_list") - tags_on(tag_type).map(&:name)
268
+ old_tags = tags_on(tag_type).reject { |tag| instance_variable_get("@#{tag_type.singularize}_list").include?(tag.name) }
269
+
270
+ self.class.transaction do
271
+ base_tags.delete(*old_tags) if old_tags.any?
272
+ new_tag_names.each do |new_tag_name|
273
+ new_tag = Tag.find_or_create_with_like_by_name(new_tag_name)
274
+ Tagging.create(:tag_id => new_tag.id, :context => tag_type,
275
+ :taggable => self, :tagger => owner)
276
+ end
277
+ end
278
+ end
279
+
280
+ true
281
+ end
282
+
283
+ def reload_with_tag_list(*args)
284
+ self.class.tag_types.each do |tag_type|
285
+ self.instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
286
+ end
287
+
288
+ reload_without_tag_list(*args)
289
+ end
290
+ end
291
+ end
292
+ end
293
+ end
@@ -0,0 +1,48 @@
1
+ module ActiveRecord
2
+ module Acts
3
+ module Tagger
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ def acts_as_tagger(opts={})
10
+ has_many :owned_taggings, opts.merge(:as => :tagger, :dependent => :destroy,
11
+ :include => :tag, :class_name => "Tagging")
12
+ has_many :owned_tags, :through => :owned_taggings, :source => :tag
13
+ include ActiveRecord::Acts::Tagger::InstanceMethods
14
+ extend ActiveRecord::Acts::Tagger::SingletonMethods
15
+ end
16
+
17
+ def is_tagger?
18
+ false
19
+ end
20
+ end
21
+
22
+ module InstanceMethods
23
+ def self.included(base)
24
+ end
25
+
26
+ def tag(taggable, opts={})
27
+ return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
28
+ raise "You need to specify a tag context using :on" unless opts.has_key?(:on)
29
+ raise "You need to specify some tags using :with" unless opts.has_key?(:with)
30
+ raise "No context :#{opts[:on]} defined in #{taggable.class.to_s}" unless taggable.tag_types.include?(opts[:on])
31
+ taggable.set_tag_list_on(opts[:on].to_s, opts[:with], self)
32
+ taggable.save
33
+ end
34
+
35
+ def is_tagger?
36
+ self.class.is_tagger?
37
+ end
38
+ end
39
+
40
+ module SingletonMethods
41
+ def is_tagger?
42
+ true
43
+ end
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,23 @@
1
+ class Tag < ActiveRecord::Base
2
+ has_many :taggings
3
+
4
+ validates_presence_of :name
5
+ validates_uniqueness_of :name
6
+
7
+ # LIKE is used for cross-database case-insensitivity
8
+ def self.find_or_create_with_like_by_name(name)
9
+ find(:first, :conditions => ["name LIKE ?", name]) || create(:name => name)
10
+ end
11
+
12
+ def ==(object)
13
+ super || (object.is_a?(Tag) && name == object.name)
14
+ end
15
+
16
+ def to_s
17
+ name
18
+ end
19
+
20
+ def count
21
+ read_attribute(:count).to_i
22
+ end
23
+ end
@@ -0,0 +1,93 @@
1
+ class TagList < Array
2
+ cattr_accessor :delimiter
3
+ self.delimiter = ','
4
+
5
+ def initialize(*args)
6
+ add(*args)
7
+ end
8
+
9
+ attr_accessor :owner
10
+
11
+ # Add tags to the tag_list. Duplicate or blank tags will be ignored.
12
+ #
13
+ # tag_list.add("Fun", "Happy")
14
+ #
15
+ # Use the <tt>:parse</tt> option to add an unparsed tag string.
16
+ #
17
+ # tag_list.add("Fun, Happy", :parse => true)
18
+ def add(*names)
19
+ extract_and_apply_options!(names)
20
+ concat(names)
21
+ clean!
22
+ self
23
+ end
24
+
25
+ # Remove specific tags from the tag_list.
26
+ #
27
+ # tag_list.remove("Sad", "Lonely")
28
+ #
29
+ # Like #add, the <tt>:parse</tt> option can be used to remove multiple tags in a string.
30
+ #
31
+ # tag_list.remove("Sad, Lonely", :parse => true)
32
+ def remove(*names)
33
+ extract_and_apply_options!(names)
34
+ delete_if { |name| names.include?(name) }
35
+ self
36
+ end
37
+
38
+ # Transform the tag_list into a tag string suitable for edting in a form.
39
+ # The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
40
+ #
41
+ # tag_list = TagList.new("Round", "Square,Cube")
42
+ # tag_list.to_s # 'Round, "Square,Cube"'
43
+ def to_s
44
+ clean!
45
+
46
+ map do |name|
47
+ name.include?(delimiter) ? "\"#{name}\"" : name
48
+ end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
49
+ end
50
+
51
+ private
52
+ # Remove whitespace, duplicates, and blanks.
53
+ def clean!
54
+ reject!(&:blank?)
55
+ map!(&:strip)
56
+ uniq!
57
+ end
58
+
59
+ def extract_and_apply_options!(args)
60
+ options = args.last.is_a?(Hash) ? args.pop : {}
61
+ options.assert_valid_keys :parse
62
+
63
+ if options[:parse]
64
+ args.map! { |a| self.class.from(a) }
65
+ end
66
+
67
+ args.flatten!
68
+ end
69
+
70
+ class << self
71
+ # Returns a new TagList using the given tag string.
72
+ #
73
+ # tag_list = TagList.from("One , Two, Three")
74
+ # tag_list # ["One", "Two", "Three"]
75
+ def from(string)
76
+ returning new do |tag_list|
77
+ string = string.to_s.dup
78
+
79
+ # Parse the quoted tags
80
+ string.gsub!(/"(.*?)"\s*#{delimiter}?\s*/) { tag_list << $1; "" }
81
+ string.gsub!(/'(.*?)'\s*#{delimiter}?\s*/) { tag_list << $1; "" }
82
+
83
+ tag_list.add(string.split(delimiter))
84
+ end
85
+ end
86
+
87
+ def from_owner(owner, *tags)
88
+ returning from(*tags) do |taglist|
89
+ taglist.owner = owner
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,6 @@
1
+ class Tagging < ActiveRecord::Base #:nodoc:
2
+ belongs_to :tag
3
+ belongs_to :taggable, :polymorphic => true
4
+ belongs_to :tagger, :polymorphic => true
5
+ validates_presence_of :context
6
+ end
@@ -0,0 +1,11 @@
1
+ module TagsHelper
2
+ # See the README for an example using tag_cloud.
3
+ def tag_cloud(tags, classes)
4
+ max_count = tags.sort_by(&:count).last.count.to_f
5
+
6
+ tags.each do |tag|
7
+ index = ((tag.count / max_count) * (classes.size - 1)).round
8
+ yield tag, classes[index]
9
+ end
10
+ end
11
+ end
data/rails/init.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'acts-as-taggable-on'
2
+
3
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::TaggableOn
4
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::Tagger
5
+
6
+ RAILS_DEFAULT_LOGGER.info "** acts_as_taggable_on: initialized properly."
@@ -0,0 +1,80 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe "acts_as_taggable_on" do
4
+ context "Taggable Method Generation" do
5
+ before(:each) do
6
+ @taggable = TaggableModel.new(:name => "Bob Jones")
7
+ end
8
+
9
+ it "should create a class attribute for tag types" do
10
+ @taggable.class.should respond_to(:tag_types)
11
+ end
12
+
13
+ it "should generate an association for each tag type" do
14
+ @taggable.should respond_to(:tags, :skills, :languages)
15
+ end
16
+
17
+ it "should generate a cached column checker for each tag type" do
18
+ TaggableModel.should respond_to(:caching_tag_list?, :caching_skill_list?, :caching_language_list?)
19
+ end
20
+
21
+ it "should add tagged_with and tag_counts to singleton" do
22
+ TaggableModel.should respond_to(:find_tagged_with, :tag_counts)
23
+ end
24
+
25
+ it "should add saving of tag lists and cached tag lists to the instance" do
26
+ @taggable.should respond_to(:save_cached_tag_list)
27
+ @taggable.should respond_to(:save_tags)
28
+ end
29
+
30
+ it "should generate a tag_list accessor/setter for each tag type" do
31
+ @taggable.should respond_to(:tag_list, :skill_list, :language_list)
32
+ @taggable.should respond_to(:tag_list=, :skill_list=, :language_list=)
33
+ end
34
+ end
35
+
36
+ context "inheritance" do
37
+ before do
38
+ @taggable = TaggableModel.new(:name => "taggable")
39
+ @inherited_same = InheritingTaggableModel.new(:name => "inherited same")
40
+ @inherited_different = AlteredInheritingTaggableModel.new(:name => "inherited different")
41
+ end
42
+
43
+ it "should pass on tag contexts to STI-inherited models" do
44
+ @inherited_same.should respond_to(:tag_list, :skill_list, :language_list)
45
+ @inherited_different.should respond_to(:tag_list, :skill_list, :language_list)
46
+ end
47
+
48
+ it "should have tag contexts added in altered STI models" do
49
+ @inherited_different.should respond_to(:part_list)
50
+ end
51
+ end
52
+
53
+ context "reloading" do
54
+ it "should save a model instantiated by Model.find" do
55
+ taggable = TaggableModel.create!(:name => "Taggable")
56
+ found_taggable = TaggableModel.find(taggable.id)
57
+ found_taggable.save
58
+ end
59
+ end
60
+
61
+ context "related" do
62
+ it "should find related objects based on tag names on context" do
63
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
64
+ taggable2 = TaggableModel.create!(:name => "Taggable 2")
65
+ taggable3 = TaggableModel.create!(:name => "Taggable 3")
66
+
67
+ taggable1.tag_list = "one, two"
68
+ taggable1.save
69
+
70
+ taggable2.tag_list = "three, four"
71
+ taggable2.save
72
+
73
+ taggable3.tag_list = "one, four"
74
+ taggable3.save
75
+
76
+ taggable1.find_related_tags.should include(taggable3)
77
+ taggable1.find_related_tags.should_not include(taggable2)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,41 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe TagList do
4
+ before(:each) do
5
+ @tag_list = TagList.new("awesome","radical")
6
+ end
7
+
8
+ it "should be an array" do
9
+ @tag_list.is_a?(Array).should be_true
10
+ end
11
+
12
+ it "should be able to be add a new tag word" do
13
+ @tag_list.add("cool")
14
+ @tag_list.include?("cool").should be_true
15
+ end
16
+
17
+ it "should be able to add delimited lists of words" do
18
+ @tag_list.add("cool, wicked", :parse => true)
19
+ @tag_list.include?("cool").should be_true
20
+ @tag_list.include?("wicked").should be_true
21
+ end
22
+
23
+ it "should be able to remove words" do
24
+ @tag_list.remove("awesome")
25
+ @tag_list.include?("awesome").should be_false
26
+ end
27
+
28
+ it "should be able to remove delimited lists of words" do
29
+ @tag_list.remove("awesome, radical", :parse => true)
30
+ @tag_list.should be_empty
31
+ end
32
+
33
+ it "should give a delimited list of words when converted to string" do
34
+ @tag_list.to_s.should == "awesome, radical"
35
+ end
36
+
37
+ it "should quote escape tags with commas in them" do
38
+ @tag_list.add("cool","rad,bodacious")
39
+ @tag_list.to_s.should == "awesome, radical, cool, \"rad,bodacious\""
40
+ end
41
+ end
@@ -0,0 +1,25 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Tag do
4
+ before(:each) do
5
+ @tag = Tag.new
6
+ @user = TaggableModel.create(:name => "Pablo")
7
+ end
8
+
9
+ it "should require a name" do
10
+ @tag.should have(1).errors_on(:name)
11
+ @tag.name = "something"
12
+ @tag.should have(0).errors_on(:name)
13
+ end
14
+
15
+ it "should equal a tag with the same name" do
16
+ @tag.name = "awesome"
17
+ new_tag = Tag.new(:name => "awesome")
18
+ new_tag.should == @tag
19
+ end
20
+
21
+ it "should return its name when to_s is called" do
22
+ @tag.name = "cool"
23
+ @tag.to_s.should == "cool"
24
+ end
25
+ end
@@ -0,0 +1,118 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe "Taggable" do
4
+ before(:each) do
5
+ @taggable = TaggableModel.new(:name => "Bob Jones")
6
+ end
7
+
8
+ it "should be able to create tags" do
9
+ @taggable.skill_list = "ruby, rails, css"
10
+ @taggable.instance_variable_get("@skill_list").instance_of?(TagList).should be_true
11
+ @taggable.save
12
+
13
+ Tag.find(:all).size.should == 3
14
+ end
15
+
16
+ it "should differentiate between contexts" do
17
+ @taggable.skill_list = "ruby, rails, css"
18
+ @taggable.tag_list = "ruby, bob, charlie"
19
+ @taggable.save
20
+ @taggable.reload
21
+ @taggable.skill_list.include?("ruby").should be_true
22
+ @taggable.skill_list.include?("bob").should be_false
23
+ end
24
+
25
+ it "should be able to remove tags through list alone" do
26
+ @taggable.skill_list = "ruby, rails, css"
27
+ @taggable.save
28
+ @taggable.reload
29
+ @taggable.should have(3).skills
30
+ @taggable.skill_list = "ruby, rails"
31
+ @taggable.save
32
+ @taggable.reload
33
+ @taggable.should have(2).skills
34
+ end
35
+
36
+ it "should be able to find by tag" do
37
+ @taggable.skill_list = "ruby, rails, css"
38
+ @taggable.save
39
+ TaggableModel.find_tagged_with("ruby").first.should == @taggable
40
+ end
41
+
42
+ it "should be able to find by tag with context" do
43
+ @taggable.skill_list = "ruby, rails, css"
44
+ @taggable.tag_list = "bob, charlie"
45
+ @taggable.save
46
+ TaggableModel.find_tagged_with("ruby").first.should == @taggable
47
+ TaggableModel.find_tagged_with("bob", :on => :skills).first.should_not == @taggable
48
+ TaggableModel.find_tagged_with("bob", :on => :tags).first.should == @taggable
49
+ end
50
+
51
+ it "should not care about case" do
52
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby")
53
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "Ruby")
54
+
55
+ Tag.find(:all).size.should == 1
56
+ TaggableModel.find_tagged_with("ruby").should == TaggableModel.find_tagged_with("Ruby")
57
+ end
58
+
59
+ it "should be able to get tag counts on model as a whole" do
60
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
61
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
62
+ charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
63
+ TaggableModel.tag_counts.should_not be_empty
64
+ TaggableModel.skill_counts.should_not be_empty
65
+ end
66
+
67
+ it "should be able to get tag counts on an association" do
68
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
69
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
70
+ charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
71
+ bob.tag_counts.first.count.should == 2
72
+ charlie.skill_counts.first.count.should == 1
73
+ end
74
+
75
+ it "should be able to set a custom tag context list" do
76
+ bob = TaggableModel.create(:name => "Bob")
77
+ bob.set_tag_list_on(:rotors, "spinning, jumping")
78
+ bob.tag_list_on(:rotors).should == ["spinning","jumping"]
79
+ bob.save
80
+ bob.reload
81
+ bob.tags_on(:rotors).should_not be_empty
82
+ end
83
+
84
+ it "should be able to find tagged on a custom tag context" do
85
+ bob = TaggableModel.create(:name => "Bob")
86
+ bob.set_tag_list_on(:rotors, "spinning, jumping")
87
+ bob.tag_list_on(:rotors).should == ["spinning","jumping"]
88
+ bob.save
89
+ TaggableModel.find_tagged_with("spinning", :on => :rotors).should_not be_empty
90
+ end
91
+
92
+ context "inheritance" do
93
+ before do
94
+ @taggable = TaggableModel.new(:name => "taggable")
95
+ @inherited_same = InheritingTaggableModel.new(:name => "inherited same")
96
+ @inherited_different = AlteredInheritingTaggableModel.new(:name => "inherited different")
97
+ end
98
+
99
+ it "should be able to save tags for inherited models" do
100
+ @inherited_same.tag_list = "bob, kelso"
101
+ @inherited_same.save
102
+ InheritingTaggableModel.find_tagged_with("bob").first.should == @inherited_same
103
+ end
104
+
105
+ it "should find STI tagged models on the superclass" do
106
+ @inherited_same.tag_list = "bob, kelso"
107
+ @inherited_same.save
108
+ TaggableModel.find_tagged_with("bob").first.should == @inherited_same
109
+ end
110
+
111
+ it "should be able to add on contexts only to some subclasses" do
112
+ @inherited_different.part_list = "fork, spoon"
113
+ @inherited_different.save
114
+ InheritingTaggableModel.find_tagged_with("fork", :on => :parts).should be_empty
115
+ AlteredInheritingTaggableModel.find_tagged_with("fork", :on => :parts).first.should == @inherited_different
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,22 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe "Tagger" do
4
+ before(:each) do
5
+ @user = TaggableUser.new
6
+ @taggable = TaggableModel.new(:name => "Bob Jones")
7
+ end
8
+
9
+ it "should have taggings" do
10
+ @user.tag(@taggable, :with=>'ruby,scheme', :on=>:tags)
11
+ @user.owned_taggings.size == 2
12
+ end
13
+
14
+ it "should have tags" do
15
+ @user.tag(@taggable, :with=>'ruby,scheme', :on=>:tags)
16
+ @user.owned_tags.size == 2
17
+ end
18
+
19
+ it "is tagger" do
20
+ @user.is_tagger?.should(be_true)
21
+ end
22
+ end
@@ -0,0 +1,7 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Tagging do
4
+ before(:each) do
5
+ @tagging = Tagging.new
6
+ end
7
+ end
data/spec/schema.rb ADDED
@@ -0,0 +1,24 @@
1
+ ActiveRecord::Schema.define :version => 0 do
2
+ create_table :tags, :force => true do |t|
3
+ t.column :name, :string
4
+ end
5
+
6
+ create_table :taggings, :force => true do |t|
7
+ t.column :tag_id, :integer
8
+ t.column :taggable_id, :integer
9
+ t.column :taggable_type, :string
10
+ t.column :context, :string
11
+ t.column :created_at, :datetime
12
+ t.column :tagger_id, :integer
13
+ t.column :tagger_type, :string
14
+ end
15
+
16
+ create_table :taggable_models, :force => true do |t|
17
+ t.column :name, :string
18
+ t.column :type, :string
19
+ #t.column :cached_tag_list, :string
20
+ end
21
+ create_table :taggable_users, :force => true do |t|
22
+ t.column :name, :string
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ require File.dirname(__FILE__) + '/../../../../spec/spec_helper'
2
+
3
+ plugin_spec_dir = File.dirname(__FILE__)
4
+ ActiveRecord::Base.logger = Logger.new(plugin_spec_dir + "/debug.log")
5
+
6
+ load(File.dirname(__FILE__) + '/schema.rb')
7
+
8
+ class TaggableModel < ActiveRecord::Base
9
+ acts_as_taggable_on :tags, :languages
10
+ acts_as_taggable_on :skills
11
+ end
12
+
13
+ class InheritingTaggableModel < TaggableModel
14
+ end
15
+
16
+ class AlteredInheritingTaggableModel < TaggableModel
17
+ acts_as_taggable_on :parts
18
+ end
19
+
20
+ class TaggableUser < ActiveRecord::Base
21
+ acts_as_tagger
22
+ end
data/uninstall.rb ADDED
@@ -0,0 +1 @@
1
+ # Uninstall hook code here
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mbleigh-acts-as-taggable-on
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael Bleigh
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-06-10 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Acts As Taggable On provides the ability to have multiple tag contexts on a single model in ActiveRecord. It also has support for tag clouds, related items, taggers, and more.
17
+ email: michael@intridea.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - CHANGELOG
26
+ - MIT-LICENSE
27
+ - README
28
+ - generators/acts_as_taggable_on_migration
29
+ - generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb
30
+ - generators/acts_as_taggable_on_migration/templates
31
+ - generators/acts_as_taggable_on_migration/templates/add_users_migration.rb
32
+ - generators/acts_as_taggable_on_migration/templates/migration.rb
33
+ - init.rb
34
+ - lib/acts-as-taggable-on.rb
35
+ - lib/acts_as_taggable_on/acts_as_taggable_on.rb
36
+ - lib/acts_as_taggable_on/acts_as_tagger.rb
37
+ - lib/acts_as_taggable_on/tag.rb
38
+ - lib/acts_as_taggable_on/tag_list.rb
39
+ - lib/acts_as_taggable_on/tagging.rb
40
+ - lib/acts_as_taggable_on/tags_helper.rb
41
+ - rails/init.rb
42
+ - spec/acts_as_taggable_on
43
+ - spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb
44
+ - spec/acts_as_taggable_on/tag_list_spec.rb
45
+ - spec/acts_as_taggable_on/tag_spec.rb
46
+ - spec/acts_as_taggable_on/taggable_spec.rb
47
+ - spec/acts_as_taggable_on/tagger_spec.rb
48
+ - spec/acts_as_taggable_on/tagging_spec.rb
49
+ - spec/debug.log
50
+ - spec/schema.rb
51
+ - spec/spec_helper.rb
52
+ - uninstall.rb
53
+ has_rdoc: false
54
+ homepage: http://www.actsascommunity.com/projects/acts-as-taggable-on
55
+ post_install_message:
56
+ rdoc_options: []
57
+
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ version:
72
+ requirements: []
73
+
74
+ rubyforge_project:
75
+ rubygems_version: 1.0.1
76
+ signing_key:
77
+ specification_version: 2
78
+ summary: Tagging for ActiveRecord with custom contexts and advanced features.
79
+ test_files: []
80
+