johnsbrn-acts-as-taggable-on 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 5
3
+ :major: 1
4
+ :minor: 0
@@ -0,0 +1,7 @@
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
+ end
6
+ end
7
+ end
@@ -0,0 +1,29 @@
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
+ t.column :tagger_id, :integer
11
+ t.column :tagger_type, :string
12
+
13
+ # You should make sure that the column created is
14
+ # long enough to store the required class names.
15
+ t.column :taggable_type, :string
16
+ t.column :context, :string
17
+
18
+ t.column :created_at, :datetime
19
+ end
20
+
21
+ add_index :taggings, :tag_id
22
+ add_index :taggings, [:taggable_id, :taggable_type, :context]
23
+ end
24
+
25
+ def self.down
26
+ drop_table :taggings
27
+ drop_table :tags
28
+ end
29
+ end
@@ -0,0 +1,11 @@
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'
7
+
8
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::TaggableOn
9
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::Tagger
10
+
11
+ RAILS_DEFAULT_LOGGER.info "** acts_as_taggable_on: initialized properly."
@@ -0,0 +1,316 @@
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 taggable?
10
+ false
11
+ end
12
+
13
+ def acts_as_taggable
14
+ acts_as_taggable_on :tags
15
+ end
16
+
17
+ def acts_as_taggable_on(*args)
18
+ args.flatten! if args
19
+ args.compact! if args
20
+ for tag_type in args
21
+ tag_type = tag_type.to_s
22
+ self.class_eval do
23
+ has_many "#{tag_type.singularize}_taggings".to_sym, :as => :taggable, :dependent => :destroy,
24
+ :include => :tag, :conditions => ["context = ?",tag_type], :class_name => "Tagging"
25
+ has_many "#{tag_type}".to_sym, :through => "#{tag_type.singularize}_taggings".to_sym, :source => :tag
26
+ end
27
+
28
+ self.class_eval <<-RUBY
29
+ def self.taggable?
30
+ true
31
+ end
32
+
33
+ def self.caching_#{tag_type.singularize}_list?
34
+ caching_tag_list_on?("#{tag_type}")
35
+ end
36
+
37
+ def self.#{tag_type.singularize}_counts(options={})
38
+ tag_counts_on('#{tag_type}',options)
39
+ end
40
+
41
+ def #{tag_type.singularize}_list
42
+ tag_list_on('#{tag_type}')
43
+ end
44
+
45
+ def #{tag_type.singularize}_list=(new_tags)
46
+ set_tag_list_on('#{tag_type}',new_tags)
47
+ end
48
+
49
+ def #{tag_type.singularize}_counts(options = {})
50
+ tag_counts_on('#{tag_type}',options)
51
+ end
52
+
53
+ def #{tag_type}_from(owner)
54
+ tag_list_on('#{tag_type}', owner)
55
+ end
56
+
57
+ def find_related_#{tag_type}(options = {})
58
+ related_tags_for('#{tag_type}', self.class, options)
59
+ end
60
+ alias_method :find_related_on_#{tag_type}, :find_related_#{tag_type}
61
+
62
+ def find_related_#{tag_type}_for(klass, options = {})
63
+ related_tags_for('#{tag_type}', klass, options)
64
+ end
65
+ RUBY
66
+ end
67
+
68
+ if respond_to?(:tag_types)
69
+ write_inheritable_attribute( :tag_types, (tag_types + args).uniq )
70
+ else
71
+ self.class_eval do
72
+ write_inheritable_attribute(:tag_types, args.uniq)
73
+ class_inheritable_reader :tag_types
74
+
75
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
76
+ has_many :base_tags, :class_name => "Tag", :through => :taggings, :source => :tag
77
+
78
+ attr_writer :custom_contexts
79
+
80
+ before_save :save_cached_tag_list
81
+ after_save :save_tags
82
+
83
+ if respond_to?(:named_scope)
84
+ named_scope :tagged_with, lambda{ |tags, options|
85
+ find_options_for_find_tagged_with(tags, options)
86
+ }
87
+ end
88
+ end
89
+
90
+ include ActiveRecord::Acts::TaggableOn::InstanceMethods
91
+ extend ActiveRecord::Acts::TaggableOn::SingletonMethods
92
+ alias_method_chain :reload, :tag_list
93
+ end
94
+ end
95
+
96
+ def is_taggable?
97
+ false
98
+ end
99
+ end
100
+
101
+ module SingletonMethods
102
+ # Pass either a tag string, or an array of strings or tags
103
+ #
104
+ # Options:
105
+ # :exclude - Find models that are not tagged with the given tags
106
+ # :match_all - Find models that match all of the given tags, not just one
107
+ # :conditions - A piece of SQL conditions to add to the query
108
+ # :on - scopes the find to a context
109
+ def find_tagged_with(*args)
110
+ options = find_options_for_find_tagged_with(*args)
111
+ options.blank? ? [] : find(:all,options)
112
+ end
113
+
114
+ def caching_tag_list_on?(context)
115
+ column_names.include?("cached_#{context.to_s.singularize}_list")
116
+ end
117
+
118
+ def tag_counts_on(context, options = {})
119
+ Tag.find(:all, find_options_for_tag_counts(options.merge({:on => context.to_s})))
120
+ end
121
+
122
+ def find_options_for_find_tagged_with(tags, options = {})
123
+ tags = tags.is_a?(Array) ? TagList.new(tags.map(&:to_s)) : TagList.from(tags)
124
+
125
+ return {} if tags.empty?
126
+
127
+ conditions = []
128
+ conditions << sanitize_sql(options.delete(:conditions)) if options[:conditions]
129
+
130
+ unless (on = options.delete(:on)).nil?
131
+ conditions << sanitize_sql(["context = ?",on.to_s])
132
+ end
133
+
134
+ taggings_alias, tags_alias = "#{table_name}_taggings", "#{table_name}_tags"
135
+
136
+ if options.delete(:exclude)
137
+ tags_conditions = tags.map { |t| sanitize_sql(["#{Tag.table_name}.name LIKE ?", t]) }.join(" OR ")
138
+ 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])
139
+ else
140
+ conditions << tags.map { |t| sanitize_sql(["#{tags_alias}.name LIKE ?", t]) }.join(" OR ")
141
+
142
+ if options.delete(:match_all)
143
+ group = "#{taggings_alias}.taggable_id HAVING COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
144
+ end
145
+ end
146
+
147
+ { :select => "DISTINCT #{table_name}.*",
148
+ :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)} " +
149
+ "LEFT OUTER JOIN #{Tag.table_name} #{tags_alias} ON #{tags_alias}.id = #{taggings_alias}.tag_id",
150
+ :conditions => conditions.join(" AND "),
151
+ :group => group
152
+ }.update(options)
153
+ end
154
+
155
+ # Calculate the tag counts for all tags.
156
+ #
157
+ # Options:
158
+ # :start_at - Restrict the tags to those created after a certain time
159
+ # :end_at - Restrict the tags to those created before a certain time
160
+ # :conditions - A piece of SQL conditions to add to the query
161
+ # :limit - The maximum number of tags to return
162
+ # :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
163
+ # :at_least - Exclude tags with a frequency less than the given value
164
+ # :at_most - Exclude tags with a frequency greater than the given value
165
+ # :on - Scope the find to only include a certain context
166
+ def find_options_for_tag_counts(options = {})
167
+ options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on
168
+
169
+ scope = scope(:find)
170
+ start_at = sanitize_sql(["#{Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
171
+ end_at = sanitize_sql(["#{Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
172
+
173
+ type_and_context = "#{Tagging.table_name}.taggable_type = #{quote_value(base_class.name)}"
174
+
175
+ conditions = [
176
+ type_and_context,
177
+ options[:conditions],
178
+ start_at,
179
+ end_at
180
+ ]
181
+
182
+ conditions = conditions.compact.join(' AND ')
183
+ conditions = merge_conditions(conditions, scope[:conditions]) if scope
184
+
185
+ joins = ["LEFT OUTER JOIN #{Tagging.table_name} ON #{Tag.table_name}.id = #{Tagging.table_name}.tag_id"]
186
+ joins << sanitize_sql(["AND #{Tagging.table_name}.context = ?",options.delete(:on).to_s]) unless options[:on].nil?
187
+ joins << "LEFT OUTER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{Tagging.table_name}.taggable_id"
188
+ joins << scope[:joins] if scope && scope[:joins]
189
+
190
+ at_least = sanitize_sql(['COUNT(*) >= ?', options.delete(:at_least)]) if options[:at_least]
191
+ at_most = sanitize_sql(['COUNT(*) <= ?', options.delete(:at_most)]) if options[:at_most]
192
+ having = [at_least, at_most].compact.join(' AND ')
193
+ group_by = "#{Tag.table_name}.id, #{Tag.table_name}.name HAVING COUNT(*) > 0"
194
+ group_by << " AND #{having}" unless having.blank?
195
+
196
+ { :select => "#{Tag.table_name}.id, #{Tag.table_name}.name, COUNT(*) AS count",
197
+ :joins => joins.join(" "),
198
+ :conditions => conditions,
199
+ :group => group_by
200
+ }.update(options)
201
+ end
202
+
203
+ def is_taggable?
204
+ true
205
+ end
206
+ end
207
+
208
+ module InstanceMethods
209
+
210
+ def tag_types
211
+ self.class.tag_types
212
+ end
213
+
214
+ def custom_contexts
215
+ @custom_contexts ||= []
216
+ end
217
+
218
+ def is_taggable?
219
+ self.class.is_taggable?
220
+ end
221
+
222
+ def add_custom_context(value)
223
+ custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s)
224
+ end
225
+
226
+ def tag_list_on(context, owner=nil)
227
+ var_name = context.to_s.singularize + "_list"
228
+ add_custom_context(context)
229
+ return instance_variable_get("@#{var_name}") unless instance_variable_get("@#{var_name}").nil?
230
+
231
+ if !owner && self.class.caching_tag_list_on?(context) and !(cached_value = cached_tag_list_on(context)).nil?
232
+ instance_variable_set("@#{var_name}", TagList.from(self["cached_#{var_name}"]))
233
+ else
234
+ instance_variable_set("@#{var_name}", TagList.new(*tags_on(context, owner).map(&:name)))
235
+ end
236
+ end
237
+
238
+ def tags_on(context, owner=nil)
239
+ if owner
240
+ opts = {:conditions => ["context = ? AND tagger_id = ? AND tagger_type = ?",
241
+ context.to_s, owner.id, owner.class.to_s]}
242
+ else
243
+ opts = {:conditions => ["context = ?", context.to_s]}
244
+ end
245
+ base_tags.find(:all, opts)
246
+ end
247
+
248
+ def cached_tag_list_on(context)
249
+ self["cached_#{context.to_s.singularize}_list"]
250
+ end
251
+
252
+ def set_tag_list_on(context,new_list, tagger=nil)
253
+ instance_variable_set("@#{context.to_s.singularize}_list", TagList.from_owner(tagger, new_list))
254
+ add_custom_context(context)
255
+ end
256
+
257
+ def tag_counts_on(context,options={})
258
+ self.class.tag_counts_on(context,{:conditions => ["#{Tag.table_name}.name IN (?)", tag_list_on(context)]}.reverse_merge!(options))
259
+ end
260
+
261
+ def related_tags_for(context, klass, options = {})
262
+ search_conditions = related_search_options(context, klass, options)
263
+
264
+ klass.find(:all, search_conditions)
265
+ end
266
+
267
+ def related_search_options(context, klass, options = {})
268
+ tags_to_find = self.tags_on(context).collect { |t| t.name }
269
+
270
+ { :select => "#{klass.table_name}.*, COUNT(#{Tag.table_name}.id) AS count",
271
+ :from => "#{klass.table_name}, #{Tag.table_name}, #{Tagging.table_name}",
272
+ :conditions => ["#{klass.table_name}.id = #{Tagging.table_name}.taggable_id AND #{Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{Tagging.table_name}.tag_id = #{Tag.table_name}.id AND #{Tag.table_name}.name IN (?)", tags_to_find],
273
+ :group => "#{klass.table_name}.id",
274
+ :order => "count DESC"
275
+ }.update(options)
276
+ end
277
+
278
+ def save_cached_tag_list
279
+ self.class.tag_types.map(&:to_s).each do |tag_type|
280
+ if self.class.send("caching_#{tag_type.singularize}_list?")
281
+ self["cached_#{tag_type.singularize}_list"] = send("#{tag_type.singularize}_list").to_s
282
+ end
283
+ end
284
+ end
285
+
286
+ def save_tags
287
+ (custom_contexts + self.class.tag_types.map(&:to_s)).each do |tag_type|
288
+ next unless instance_variable_get("@#{tag_type.singularize}_list")
289
+ owner = instance_variable_get("@#{tag_type.singularize}_list").owner
290
+ new_tag_names = instance_variable_get("@#{tag_type.singularize}_list") - tags_on(tag_type).map(&:name)
291
+ old_tags = tags_on(tag_type).reject { |tag| instance_variable_get("@#{tag_type.singularize}_list").include?(tag.name) }
292
+
293
+ self.class.transaction do
294
+ base_tags.delete(*old_tags) if old_tags.any?
295
+ new_tag_names.each do |new_tag_name|
296
+ new_tag = Tag.find_or_create_with_like_by_name(new_tag_name)
297
+ Tagging.create(:tag_id => new_tag.id, :context => tag_type,
298
+ :taggable => self, :tagger => owner)
299
+ end
300
+ end
301
+ end
302
+
303
+ true
304
+ end
305
+
306
+ def reload_with_tag_list(*args)
307
+ self.class.tag_types.each do |tag_type|
308
+ self.instance_variable_set("@#{tag_type.to_s.singularize}_list", nil)
309
+ end
310
+
311
+ reload_without_tag_list(*args)
312
+ end
313
+ end
314
+ end
315
+ end
316
+ end
@@ -0,0 +1,52 @@
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
+ opts.reverse_merge!(:force => true)
28
+
29
+ return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
30
+ raise "You need to specify a tag context using :on" unless opts.has_key?(:on)
31
+ raise "You need to specify some tags using :with" unless opts.has_key?(:with)
32
+ raise "No context :#{opts[:on]} defined in #{taggable.class.to_s}" unless
33
+ ( opts[:force] || taggable.tag_types.include?(opts[:on]) )
34
+
35
+ taggable.set_tag_list_on(opts[:on].to_s, opts[:with], self)
36
+ taggable.save
37
+ end
38
+
39
+ def is_tagger?
40
+ self.class.is_tagger?
41
+ end
42
+ end
43
+
44
+ module SingletonMethods
45
+ def is_tagger?
46
+ true
47
+ end
48
+ end
49
+
50
+ end
51
+ end
52
+ 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
@@ -0,0 +1,6 @@
1
+ # Need this to get picked up by autotest?
2
+ $:.push(File.join(File.dirname(__FILE__), %w[.. .. rspec]))
3
+
4
+ Autotest.add_discovery do
5
+ "rspec"
6
+ end
@@ -0,0 +1,151 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe "Acts As Taggable On" do
4
+ it "should provide a class method 'taggable?' that is false for untaggable models" do
5
+ UntaggableModel.should_not be_taggable
6
+ end
7
+
8
+ describe "Taggable Method Generation" do
9
+ before(:each) do
10
+ [TaggableModel, Tag, Tagging, TaggableUser].each(&:delete_all)
11
+ @taggable = TaggableModel.new(:name => "Bob Jones")
12
+ end
13
+
14
+ it "should respond 'true' to taggable?" do
15
+ @taggable.class.should be_taggable
16
+ end
17
+
18
+ it "should create a class attribute for tag types" do
19
+ @taggable.class.should respond_to(:tag_types)
20
+ end
21
+
22
+ it "should generate an association for each tag type" do
23
+ @taggable.should respond_to(:tags, :skills, :languages)
24
+ end
25
+
26
+ it "should generate a cached column checker for each tag type" do
27
+ TaggableModel.should respond_to(:caching_tag_list?, :caching_skill_list?, :caching_language_list?)
28
+ end
29
+
30
+ it "should add tagged_with and tag_counts to singleton" do
31
+ TaggableModel.should respond_to(:find_tagged_with, :tag_counts)
32
+ end
33
+
34
+ it "should add saving of tag lists and cached tag lists to the instance" do
35
+ @taggable.should respond_to(:save_cached_tag_list)
36
+ @taggable.should respond_to(:save_tags)
37
+ end
38
+
39
+ it "should generate a tag_list accessor/setter for each tag type" do
40
+ @taggable.should respond_to(:tag_list, :skill_list, :language_list)
41
+ @taggable.should respond_to(:tag_list=, :skill_list=, :language_list=)
42
+ end
43
+ end
44
+
45
+ describe "Single Table Inheritance" do
46
+ before do
47
+ @taggable = TaggableModel.new(:name => "taggable")
48
+ @inherited_same = InheritingTaggableModel.new(:name => "inherited same")
49
+ @inherited_different = AlteredInheritingTaggableModel.new(:name => "inherited different")
50
+ end
51
+
52
+ it "should pass on tag contexts to STI-inherited models" do
53
+ @inherited_same.should respond_to(:tag_list, :skill_list, :language_list)
54
+ @inherited_different.should respond_to(:tag_list, :skill_list, :language_list)
55
+ end
56
+
57
+ it "should have tag contexts added in altered STI models" do
58
+ @inherited_different.should respond_to(:part_list)
59
+ end
60
+ end
61
+
62
+ describe "Reloading" do
63
+ it "should save a model instantiated by Model.find" do
64
+ taggable = TaggableModel.create!(:name => "Taggable")
65
+ found_taggable = TaggableModel.find(taggable.id)
66
+ found_taggable.save
67
+ end
68
+ end
69
+
70
+ describe "Related Objects" do
71
+ it "should find related objects based on tag names on context" do
72
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
73
+ taggable2 = TaggableModel.create!(:name => "Taggable 2")
74
+ taggable3 = TaggableModel.create!(:name => "Taggable 3")
75
+
76
+ taggable1.tag_list = "one, two"
77
+ taggable1.save
78
+
79
+ taggable2.tag_list = "three, four"
80
+ taggable2.save
81
+
82
+ taggable3.tag_list = "one, four"
83
+ taggable3.save
84
+
85
+ taggable1.find_related_tags.should include(taggable3)
86
+ taggable1.find_related_tags.should_not include(taggable2)
87
+ end
88
+
89
+ it "should find other related objects based on tag names on context" do
90
+ taggable1 = TaggableModel.create!(:name => "Taggable 1")
91
+ taggable2 = OtherTaggableModel.create!(:name => "Taggable 2")
92
+ taggable3 = OtherTaggableModel.create!(:name => "Taggable 3")
93
+
94
+ taggable1.tag_list = "one, two"
95
+ taggable1.save
96
+
97
+ taggable2.tag_list = "three, four"
98
+ taggable2.save
99
+
100
+ taggable3.tag_list = "one, four"
101
+ taggable3.save
102
+
103
+ taggable1.find_related_tags_for(OtherTaggableModel).should include(taggable3)
104
+ taggable1.find_related_tags_for(OtherTaggableModel).should_not include(taggable2)
105
+ end
106
+ end
107
+
108
+ describe 'Tagging Contexts' do
109
+ before(:all) do
110
+ class Array
111
+ def freq
112
+ k=Hash.new(0)
113
+ self.each {|e| k[e]+=1}
114
+ k
115
+ end
116
+ end
117
+ end
118
+
119
+ it 'should eliminate duplicate tagging contexts ' do
120
+ TaggableModel.acts_as_taggable_on(:skills, :skills)
121
+ TaggableModel.tag_types.freq[:skills].should_not == 3
122
+ end
123
+
124
+ it "should not contain embedded/nested arrays" do
125
+ TaggableModel.acts_as_taggable_on([:array], [:array])
126
+ TaggableModel.tag_types.freq[[:array]].should == 0
127
+ end
128
+
129
+ it "should _flatten_ the content of arrays" do
130
+ TaggableModel.acts_as_taggable_on([:array], [:array])
131
+ TaggableModel.tag_types.freq[:array].should == 1
132
+ end
133
+
134
+ it "should not raise an error when passed nil" do
135
+ lambda {
136
+ TaggableModel.acts_as_taggable_on()
137
+ }.should_not raise_error
138
+ end
139
+
140
+ it "should not raise an error when passed [nil]" do
141
+ lambda {
142
+ TaggableModel.acts_as_taggable_on([nil])
143
+ }.should_not raise_error
144
+ end
145
+
146
+ after(:all) do
147
+ class Array; remove_method :freq; end
148
+ end
149
+ end
150
+
151
+ end
@@ -0,0 +1,72 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe "acts_as_tagger" do
4
+ context "Tagger Method Generation" do
5
+
6
+ before(:each) do
7
+ @tagger = TaggableUser.new()
8
+ end
9
+
10
+ it "should add #is_tagger? query method to the class-side" do
11
+ TaggableUser.should respond_to(:is_tagger?)
12
+ end
13
+
14
+ it "should return true from the class-side #is_tagger?" do
15
+ TaggableUser.is_tagger?.should be_true
16
+ end
17
+
18
+ it "should return false from the base #is_tagger?" do
19
+ ActiveRecord::Base.is_tagger?.should be_false
20
+ end
21
+
22
+ it "should add #is_tagger? query method to the singleton" do
23
+ @tagger.should respond_to(:is_tagger?)
24
+ end
25
+
26
+ it "should add #tag method on the instance-side" do
27
+ @tagger.should respond_to(:tag)
28
+ end
29
+
30
+ it "should generate an association for #owned_taggings and #owned_tags" do
31
+ @tagger.should respond_to(:owned_taggings, :owned_tags)
32
+ end
33
+ end
34
+
35
+ describe "#tag" do
36
+ context 'when called with a non-existent tag context' do
37
+ before(:each) do
38
+ @tagger = TaggableUser.new()
39
+ @taggable = TaggableModel.new(:name=>"Richard Prior")
40
+ end
41
+
42
+ it "should by default not throw an exception " do
43
+ @taggable.tag_list_on(:foo).should be_empty
44
+ lambda {
45
+ @tagger.tag(@taggable, :with=>'this, and, that', :on=>:foo)
46
+ }.should_not raise_error
47
+ end
48
+
49
+ it 'should by default create the tag context on-the-fly' do
50
+ @taggable.tag_list_on(:here_ond_now).should be_empty
51
+ @tagger.tag(@taggable, :with=>'that', :on=>:here_ond_now)
52
+ @taggable.tag_list_on(:here_ond_now).should include('that')
53
+ end
54
+
55
+ it "should throw an exception when the default is over-ridden" do
56
+ @taggable.tag_list_on(:foo_boo).should be_empty
57
+ lambda {
58
+ @tagger.tag(@taggable, :with=>'this, and, that', :on=>:foo_boo, :force=>false)
59
+ }.should raise_error
60
+ end
61
+
62
+ it "should not create the tag context on-the-fly when the default is over-ridden" do
63
+ @taggable.tag_list_on(:foo_boo).should be_empty
64
+ @tagger.tag(@taggable, :with=>'this, and, that', :on=>:foo_boo, :force=>false) rescue
65
+ @taggable.tag_list_on(:foo_boo).should be_empty
66
+ end
67
+
68
+ end
69
+
70
+ end
71
+
72
+ 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,136 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe "Taggable" do
4
+ before(:each) do
5
+ [TaggableModel, Tag, Tagging, TaggableUser].each(&:delete_all)
6
+ @taggable = TaggableModel.new(:name => "Bob Jones")
7
+ end
8
+
9
+ it "should be able to create tags" do
10
+ @taggable.skill_list = "ruby, rails, css"
11
+ @taggable.instance_variable_get("@skill_list").instance_of?(TagList).should be_true
12
+ @taggable.save
13
+
14
+ Tag.find(:all).size.should == 3
15
+ end
16
+
17
+ it "should be able to create tags through the tag list directly" do
18
+ @taggable.tag_list_on(:test).add("hello")
19
+ @taggable.save
20
+ @taggable.reload
21
+ @taggable.tag_list_on(:test).should == ["hello"]
22
+ end
23
+
24
+ it "should differentiate between contexts" do
25
+ @taggable.skill_list = "ruby, rails, css"
26
+ @taggable.tag_list = "ruby, bob, charlie"
27
+ @taggable.save
28
+ @taggable.reload
29
+ @taggable.skill_list.include?("ruby").should be_true
30
+ @taggable.skill_list.include?("bob").should be_false
31
+ end
32
+
33
+ it "should be able to remove tags through list alone" do
34
+ @taggable.skill_list = "ruby, rails, css"
35
+ @taggable.save
36
+ @taggable.reload
37
+ @taggable.should have(3).skills
38
+ @taggable.skill_list = "ruby, rails"
39
+ @taggable.save
40
+ @taggable.reload
41
+ @taggable.should have(2).skills
42
+ end
43
+
44
+ it "should be able to find by tag" do
45
+ @taggable.skill_list = "ruby, rails, css"
46
+ @taggable.save
47
+ TaggableModel.find_tagged_with("ruby").first.should == @taggable
48
+ end
49
+
50
+ it "should be able to find by tag with context" do
51
+ @taggable.skill_list = "ruby, rails, css"
52
+ @taggable.tag_list = "bob, charlie"
53
+ @taggable.save
54
+ TaggableModel.find_tagged_with("ruby").first.should == @taggable
55
+ TaggableModel.find_tagged_with("bob", :on => :skills).first.should_not == @taggable
56
+ TaggableModel.find_tagged_with("bob", :on => :tags).first.should == @taggable
57
+ end
58
+
59
+ it "should be able to use the tagged_with named scope" do
60
+ @taggable.skill_list = "ruby, rails, css"
61
+ @taggable.tag_list = "bob, charlie"
62
+ @taggable.save
63
+ TaggableModel.tagged_with("ruby", {}).first.should == @taggable
64
+ TaggableModel.tagged_with("bob", :on => :skills).first.should_not == @taggable
65
+ TaggableModel.tagged_with("bob", :on => :tags).first.should == @taggable
66
+ end
67
+
68
+ it "should not care about case" do
69
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby")
70
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "Ruby")
71
+
72
+ Tag.find(:all).size.should == 1
73
+ TaggableModel.find_tagged_with("ruby").should == TaggableModel.find_tagged_with("Ruby")
74
+ end
75
+
76
+ it "should be able to get tag counts on model as a whole" do
77
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
78
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
79
+ charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
80
+ TaggableModel.tag_counts.should_not be_empty
81
+ TaggableModel.skill_counts.should_not be_empty
82
+ end
83
+
84
+ it "should be able to get tag counts on an association" do
85
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
86
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
87
+ charlie = TaggableModel.create(:name => "Charlie", :skill_list => "ruby")
88
+ bob.tag_counts.first.count.should == 2
89
+ charlie.skill_counts.first.count.should == 1
90
+ end
91
+
92
+ it "should be able to set a custom tag context list" do
93
+ bob = TaggableModel.create(:name => "Bob")
94
+ bob.set_tag_list_on(:rotors, "spinning, jumping")
95
+ bob.tag_list_on(:rotors).should == ["spinning","jumping"]
96
+ bob.save
97
+ bob.reload
98
+ bob.tags_on(:rotors).should_not be_empty
99
+ end
100
+
101
+ it "should be able to find tagged on a custom tag context" do
102
+ bob = TaggableModel.create(:name => "Bob")
103
+ bob.set_tag_list_on(:rotors, "spinning, jumping")
104
+ bob.tag_list_on(:rotors).should == ["spinning","jumping"]
105
+ bob.save
106
+ TaggableModel.find_tagged_with("spinning", :on => :rotors).should_not be_empty
107
+ end
108
+
109
+ describe "Single Table Inheritance" do
110
+ before do
111
+ [TaggableModel, Tag, Tagging, TaggableUser].each(&:delete_all)
112
+ @taggable = TaggableModel.new(:name => "taggable")
113
+ @inherited_same = InheritingTaggableModel.new(:name => "inherited same")
114
+ @inherited_different = AlteredInheritingTaggableModel.new(:name => "inherited different")
115
+ end
116
+
117
+ it "should be able to save tags for inherited models" do
118
+ @inherited_same.tag_list = "bob, kelso"
119
+ @inherited_same.save
120
+ InheritingTaggableModel.find_tagged_with("bob").first.should == @inherited_same
121
+ end
122
+
123
+ it "should find STI tagged models on the superclass" do
124
+ @inherited_same.tag_list = "bob, kelso"
125
+ @inherited_same.save
126
+ TaggableModel.find_tagged_with("bob").first.should == @inherited_same
127
+ end
128
+
129
+ it "should be able to add on contexts only to some subclasses" do
130
+ @inherited_different.part_list = "fork, spoon"
131
+ @inherited_different.save
132
+ InheritingTaggableModel.find_tagged_with("fork", :on => :parts).should be_empty
133
+ AlteredInheritingTaggableModel.find_tagged_with("fork", :on => :parts).first.should == @inherited_different
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,23 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe "Tagger" do
4
+ before(:each) do
5
+ [TaggableModel, Tag, Tagging, TaggableUser].each(&:delete_all)
6
+ @user = TaggableUser.new
7
+ @taggable = TaggableModel.new(:name => "Bob Jones")
8
+ end
9
+
10
+ it "should have taggings" do
11
+ @user.tag(@taggable, :with=>'ruby,scheme', :on=>:tags)
12
+ @user.owned_taggings.size == 2
13
+ end
14
+
15
+ it "should have tags" do
16
+ @user.tag(@taggable, :with=>'ruby,scheme', :on=>:tags)
17
+ @user.owned_tags.size == 2
18
+ end
19
+
20
+ it "is tagger" do
21
+ @user.is_tagger?.should(be_true)
22
+ end
23
+ 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,32 @@
1
+ ActiveRecord::Schema.define :version => 0 do
2
+ create_table "taggings", :force => true do |t|
3
+ t.integer "tag_id", :limit => 11
4
+ t.integer "taggable_id", :limit => 11
5
+ t.string "taggable_type"
6
+ t.string "context"
7
+ t.datetime "created_at"
8
+ t.integer "tagger_id", :limit => 11
9
+ t.string "tagger_type"
10
+ end
11
+
12
+ add_index "taggings", ["tag_id"], :name => "index_taggings_on_tag_id"
13
+ add_index "taggings", ["taggable_id", "taggable_type", "context"], :name => "index_taggings_on_taggable_id_and_taggable_type_and_context"
14
+
15
+ create_table "tags", :force => true do |t|
16
+ t.string "name"
17
+ end
18
+
19
+ create_table :taggable_models, :force => true do |t|
20
+ t.column :name, :string
21
+ t.column :type, :string
22
+ #t.column :cached_tag_list, :string
23
+ end
24
+ create_table :taggable_users, :force => true do |t|
25
+ t.column :name, :string
26
+ end
27
+ create_table :other_taggable_models, :force => true do |t|
28
+ t.column :name, :string
29
+ t.column :type, :string
30
+ #t.column :cached_tag_list, :string
31
+ end
32
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1,7 @@
1
+ --colour
2
+ --format
3
+ specdoc
4
+ --loadby
5
+ mtime
6
+ --reverse
7
+ --backtrace
@@ -0,0 +1,33 @@
1
+ require File.dirname(__FILE__) + '/../../../../spec/spec_helper'
2
+
3
+ module Spec::Example::ExampleGroupMethods
4
+ alias :context :describe
5
+ end
6
+
7
+ plugin_spec_dir = File.dirname(__FILE__)
8
+ ActiveRecord::Base.logger = Logger.new(plugin_spec_dir + "/debug.log")
9
+
10
+ load(File.dirname(__FILE__) + '/schema.rb')
11
+
12
+ class TaggableModel < ActiveRecord::Base
13
+ acts_as_taggable_on :tags, :languages
14
+ acts_as_taggable_on :skills
15
+ end
16
+
17
+ class OtherTaggableModel < ActiveRecord::Base
18
+ acts_as_taggable_on :tags, :languages
19
+ end
20
+
21
+ class InheritingTaggableModel < TaggableModel
22
+ end
23
+
24
+ class AlteredInheritingTaggableModel < TaggableModel
25
+ acts_as_taggable_on :parts
26
+ end
27
+
28
+ class TaggableUser < ActiveRecord::Base
29
+ acts_as_tagger
30
+ end
31
+
32
+ class UntaggableModel < ActiveRecord::Base
33
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: johnsbrn-acts-as-taggable-on
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.5
5
+ platform: ruby
6
+ authors:
7
+ - Michael Bleigh
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-26 00:00:00 -08: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
+ - VERSION.yml
26
+ - generators/acts_as_taggable_on_migration
27
+ - generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb
28
+ - generators/acts_as_taggable_on_migration/templates
29
+ - generators/acts_as_taggable_on_migration/templates/migration.rb
30
+ - lib/acts-as-taggable-on.rb
31
+ - lib/acts_as_taggable_on
32
+ - lib/acts_as_taggable_on/acts_as_taggable_on.rb
33
+ - lib/acts_as_taggable_on/acts_as_tagger.rb
34
+ - lib/acts_as_taggable_on/tag.rb
35
+ - lib/acts_as_taggable_on/tag_list.rb
36
+ - lib/acts_as_taggable_on/tagging.rb
37
+ - lib/acts_as_taggable_on/tags_helper.rb
38
+ - lib/autotest
39
+ - lib/autotest/discover.rb
40
+ - spec/acts_as_taggable_on
41
+ - spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb
42
+ - spec/acts_as_taggable_on/acts_as_tagger_spec.rb
43
+ - spec/acts_as_taggable_on/tag_list_spec.rb
44
+ - spec/acts_as_taggable_on/tag_spec.rb
45
+ - spec/acts_as_taggable_on/taggable_spec.rb
46
+ - spec/acts_as_taggable_on/tagger_spec.rb
47
+ - spec/acts_as_taggable_on/tagging_spec.rb
48
+ - spec/schema.rb
49
+ - spec/spec.opts
50
+ - spec/spec_helper.rb
51
+ has_rdoc: true
52
+ homepage: http://github.com/mbleigh/acts-as-taggable-on
53
+ post_install_message:
54
+ rdoc_options:
55
+ - --inline-source
56
+ - --charset=UTF-8
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: "0"
64
+ version:
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "0"
70
+ version:
71
+ requirements: []
72
+
73
+ rubyforge_project:
74
+ rubygems_version: 1.2.0
75
+ signing_key:
76
+ specification_version: 2
77
+ summary: Tagging for ActiveRecord with custom contexts and advanced features.
78
+ test_files: []
79
+