mbleigh-acts-as-taggable-on 1.0.0

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/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
+