acts-as-taggable-on 2.0.6 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +9 -0
  5. data/CHANGELOG.md +35 -0
  6. data/Gemfile +2 -9
  7. data/Guardfile +5 -0
  8. data/{MIT-LICENSE → MIT-LICENSE.md} +1 -1
  9. data/README.md +297 -0
  10. data/Rakefile +9 -55
  11. data/UPGRADING +14 -0
  12. data/acts-as-taggable-on.gemspec +32 -0
  13. data/lib/acts-as-taggable-on/version.rb +4 -0
  14. data/lib/acts-as-taggable-on.rb +37 -4
  15. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +6 -6
  16. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +99 -45
  17. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +162 -45
  18. data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +37 -0
  19. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +40 -15
  20. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +28 -18
  21. data/lib/acts_as_taggable_on/tag.rb +41 -16
  22. data/lib/acts_as_taggable_on/tag_list.rb +19 -14
  23. data/lib/acts_as_taggable_on/taggable.rb +102 -0
  24. data/lib/acts_as_taggable_on/{acts_as_tagger.rb → tagger.rb} +3 -3
  25. data/lib/acts_as_taggable_on/tagging.rb +12 -2
  26. data/lib/acts_as_taggable_on/tags_helper.rb +2 -2
  27. data/lib/acts_as_taggable_on/utils.rb +34 -0
  28. data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +9 -2
  29. data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +3 -1
  30. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +333 -54
  31. data/spec/acts_as_taggable_on/tag_list_spec.rb +117 -61
  32. data/spec/acts_as_taggable_on/tag_spec.rb +111 -14
  33. data/spec/acts_as_taggable_on/taggable_spec.rb +330 -34
  34. data/spec/acts_as_taggable_on/tagger_spec.rb +62 -15
  35. data/spec/acts_as_taggable_on/tagging_spec.rb +2 -5
  36. data/spec/acts_as_taggable_on/tags_helper_spec.rb +16 -0
  37. data/spec/acts_as_taggable_on/utils_spec.rb +21 -0
  38. data/spec/database.yml.sample +4 -2
  39. data/spec/generators/acts_as_taggable_on/migration/migration_generator_spec.rb +22 -0
  40. data/spec/models.rb +28 -1
  41. data/spec/schema.rb +18 -0
  42. data/spec/spec_helper.rb +30 -7
  43. data/uninstall.rb +1 -0
  44. metadata +174 -57
  45. data/CHANGELOG +0 -25
  46. data/README.rdoc +0 -221
  47. data/VERSION +0 -1
  48. data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +0 -7
  49. data/generators/acts_as_taggable_on_migration/templates/migration.rb +0 -29
  50. data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +0 -53
  51. data/lib/acts_as_taggable_on/compatibility/Gemfile +0 -8
  52. data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +0 -17
  53. data/lib/acts_as_taggable_on/compatibility/postgresql.rb +0 -44
  54. data/spec/database.yml +0 -17
@@ -19,25 +19,33 @@ module ActsAsTaggableOn::Taggable
19
19
 
20
20
  def initialize_acts_as_taggable_on_ownership
21
21
  tag_types.map(&:to_s).each do |tag_type|
22
- class_eval %(
22
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
23
23
  def #{tag_type}_from(owner)
24
24
  owner_tag_list_on(owner, '#{tag_type}')
25
25
  end
26
- )
26
+ RUBY
27
27
  end
28
28
  end
29
29
  end
30
30
 
31
31
  module InstanceMethods
32
32
  def owner_tags_on(owner, context)
33
- base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
34
- #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
35
- #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.to_s]).all
33
+ if owner.nil?
34
+ scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s])
35
+ else
36
+ scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
37
+ #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
38
+ #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.base_class.to_s])
39
+ end
40
+ # when preserving tag order, return tags in created order
41
+ # if we added the order to the association this would always apply
42
+ scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order?
43
+ scope.all
36
44
  end
37
45
 
38
46
  def cached_owned_tag_list_on(context)
39
47
  variable_name = "@owned_#{context}_list"
40
- cache = instance_variable_get(variable_name) || instance_variable_set(variable_name, {})
48
+ cache = (instance_variable_defined?(variable_name) && instance_variable_get(variable_name)) || instance_variable_set(variable_name, {})
41
49
  end
42
50
 
43
51
  def owner_tag_list_on(owner, context)
@@ -69,21 +77,38 @@ module ActsAsTaggableOn::Taggable
69
77
  def save_owned_tags
70
78
  tagging_contexts.each do |context|
71
79
  cached_owned_tag_list_on(context).each do |owner, tag_list|
80
+
72
81
  # Find existing tags or create non-existing tags:
73
- tag_list = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
82
+ tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
74
83
 
75
- owned_tags = owner_tags_on(owner, context)
76
- old_tags = owned_tags - tag_list
77
- new_tags = tag_list - owned_tags
84
+ # Tag objects for owned tags
85
+ owned_tags = owner_tags_on(owner, context)
86
+
87
+ # Tag maintenance based on whether preserving the created order of tags
88
+ if self.class.preserve_tag_order?
89
+ # First off order the array of tag objects to match the tag list
90
+ # rather than existing tags followed by new tags
91
+ tags = tag_list.uniq.map{|s| tags.detect{|t| t.name.downcase == s.downcase}}
92
+ # To preserve tags in the order in which they were added
93
+ # delete all owned tags and create new tags if the content or order has changed
94
+ old_tags = (tags == owned_tags ? [] : owned_tags)
95
+ new_tags = (tags == owned_tags ? [] : tags)
96
+ else
97
+ # Delete discarded tags and create new tags
98
+ old_tags = owned_tags - tags
99
+ new_tags = tags - owned_tags
100
+ end
78
101
 
79
102
  # Find all taggings that belong to the taggable (self), are owned by the owner,
80
103
  # have the correct context, and are removed from the list.
81
- old_taggings = ActsAsTaggableOn::Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
82
- :tagger_type => owner.class.to_s, :tagger_id => owner.id,
83
- :tag_id => old_tags, :context => context).all
104
+ if old_tags.present?
105
+ old_taggings = ActsAsTaggableOn::Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
106
+ :tagger_type => owner.class.base_class.to_s, :tagger_id => owner.id,
107
+ :tag_id => old_tags, :context => context).all
108
+ end
84
109
 
110
+ # Destroy old taggings:
85
111
  if old_taggings.present?
86
- # Destroy old taggings:
87
112
  ActsAsTaggableOn::Tagging.destroy_all(:id => old_taggings.map(&:id))
88
113
  end
89
114
 
@@ -98,4 +123,4 @@ module ActsAsTaggableOn::Taggable
98
123
  end
99
124
  end
100
125
  end
101
- end
126
+ end
@@ -5,11 +5,11 @@ module ActsAsTaggableOn::Taggable
5
5
  base.extend ActsAsTaggableOn::Taggable::Related::ClassMethods
6
6
  base.initialize_acts_as_taggable_on_related
7
7
  end
8
-
8
+
9
9
  module ClassMethods
10
10
  def initialize_acts_as_taggable_on_related
11
11
  tag_types.map(&:to_s).each do |tag_type|
12
- class_eval %(
12
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
13
13
  def find_related_#{tag_type}(options = {})
14
14
  related_tags_for('#{tag_type}', self.class, options)
15
15
  end
@@ -18,7 +18,11 @@ module ActsAsTaggableOn::Taggable
18
18
  def find_related_#{tag_type}_for(klass, options = {})
19
19
  related_tags_for('#{tag_type}', klass, options)
20
20
  end
21
+ RUBY
22
+ end
21
23
 
24
+ unless tag_types.empty?
25
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
22
26
  def find_matching_contexts(search_context, result_context, options = {})
23
27
  matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
24
28
  end
@@ -26,40 +30,46 @@ module ActsAsTaggableOn::Taggable
26
30
  def find_matching_contexts_for(klass, search_context, result_context, options = {})
27
31
  matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
28
32
  end
29
- )
30
- end
33
+ RUBY
34
+ end
31
35
  end
32
-
36
+
33
37
  def acts_as_taggable_on(*args)
34
38
  super(*args)
35
39
  initialize_acts_as_taggable_on_related
36
40
  end
37
41
  end
38
-
42
+
39
43
  module InstanceMethods
40
44
  def matching_contexts_for(search_context, result_context, klass, options = {})
41
45
  tags_to_find = tags_on(search_context).collect { |t| t.name }
42
46
 
43
- exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
44
-
45
- klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.id) AS count",
47
+ exclude_self = "#{klass.table_name}.#{klass.primary_key} != #{id} AND" if [self.class.base_class, self.class].include? klass
48
+
49
+ group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(klass) : "#{klass.table_name}.#{klass.primary_key}"
50
+
51
+ klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count",
46
52
  :from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
47
- :conditions => ["#{exclude_self} #{klass.table_name}.id = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?) AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_to_find, result_context],
48
- :group => grouped_column_names_for(klass),
53
+ :conditions => ["#{exclude_self} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?) AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_to_find, result_context],
54
+ :group => group_columns,
49
55
  :order => "count DESC" }.update(options))
50
56
  end
51
-
57
+
52
58
  def related_tags_for(context, klass, options = {})
53
- tags_to_find = tags_on(context).collect { |t| t.name }
59
+ tags_to_ignore = Array.wrap(options.delete(:ignore)) || []
60
+ tags_to_ignore.map! { |t| t.to_s }
61
+ tags_to_find = tags_on(context).collect { |t| t.name }.reject { |t| tags_to_ignore.include? t }
62
+
63
+ exclude_self = "#{klass.table_name}.#{klass.primary_key} != #{id} AND" if [self.class.base_class, self.class].include? klass
54
64
 
55
- exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
65
+ group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(klass) : "#{klass.table_name}.#{klass.primary_key}"
56
66
 
57
- klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.id) AS count",
67
+ klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count",
58
68
  :from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
59
- :conditions => ["#{exclude_self} #{klass.table_name}.id = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?)", tags_to_find],
60
- :group => grouped_column_names_for(klass),
69
+ :conditions => ["#{exclude_self} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?)", tags_to_find],
70
+ :group => group_columns,
61
71
  :order => "count DESC" }.update(options))
62
72
  end
63
73
  end
64
74
  end
65
- end
75
+ end
@@ -1,7 +1,7 @@
1
1
  module ActsAsTaggableOn
2
2
  class Tag < ::ActiveRecord::Base
3
- include ActsAsTaggableOn::ActiveRecord::Backports if ::ActiveRecord::VERSION::MAJOR < 3
4
-
3
+ include ActsAsTaggableOn::Utils
4
+
5
5
  attr_accessible :name
6
6
 
7
7
  ### ASSOCIATIONS:
@@ -11,30 +11,48 @@ module ActsAsTaggableOn
11
11
  ### VALIDATIONS:
12
12
 
13
13
  validates_presence_of :name
14
- validates_uniqueness_of :name
14
+ validates_uniqueness_of :name, :if => :validates_name_uniqueness?
15
+ validates_length_of :name, :maximum => 255
16
+
17
+ # monkey patch this method if don't need name uniqueness validation
18
+ def validates_name_uniqueness?
19
+ true
20
+ end
15
21
 
16
22
  ### SCOPES:
17
23
 
18
24
  def self.named(name)
19
- where(["name #{like_operator} ?", name])
25
+ if ActsAsTaggableOn.strict_case_match
26
+ where(["name = #{binary}?", name])
27
+ else
28
+ where(["lower(name) = ?", name.downcase])
29
+ end
20
30
  end
21
-
31
+
22
32
  def self.named_any(list)
23
- where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", tag.to_s]) }.join(" OR "))
33
+ if ActsAsTaggableOn.strict_case_match
34
+ where(list.map { |tag| sanitize_sql(["name = #{binary}?", tag.to_s.mb_chars]) }.join(" OR "))
35
+ else
36
+ where(list.map { |tag| sanitize_sql(["lower(name) = ?", tag.to_s.mb_chars.downcase]) }.join(" OR "))
37
+ end
24
38
  end
25
-
39
+
26
40
  def self.named_like(name)
27
- where(["name #{like_operator} ?", "%#{name}%"])
41
+ where(["name #{like_operator} ? ESCAPE '!'", "%#{escape_like(name)}%"])
28
42
  end
29
43
 
30
44
  def self.named_like_any(list)
31
- where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", "%#{tag.to_s}%"]) }.join(" OR "))
45
+ where(list.map { |tag| sanitize_sql(["name #{like_operator} ? ESCAPE '!'", "%#{escape_like(tag.to_s)}%"]) }.join(" OR "))
32
46
  end
33
47
 
34
48
  ### CLASS METHODS:
35
49
 
36
50
  def self.find_or_create_with_like_by_name(name)
37
- named_like(name).first || create(:name => name)
51
+ if (ActsAsTaggableOn.strict_case_match)
52
+ self.find_or_create_all_with_like_by_name([name]).first
53
+ else
54
+ named_like(name).first || create(:name => name)
55
+ end
38
56
  end
39
57
 
40
58
  def self.find_or_create_all_with_like_by_name(*list)
@@ -43,7 +61,10 @@ module ActsAsTaggableOn
43
61
  return [] if list.empty?
44
62
 
45
63
  existing_tags = Tag.named_any(list).all
46
- new_tag_names = list.reject { |name| existing_tags.any? { |tag| tag.name.mb_chars.downcase == name.mb_chars.downcase } }
64
+ new_tag_names = list.reject do |name|
65
+ name = comparable_name(name)
66
+ existing_tags.any? { |tag| comparable_name(tag.name) == name }
67
+ end
47
68
  created_tags = new_tag_names.map { |name| Tag.create(:name => name) }
48
69
 
49
70
  existing_tags + created_tags
@@ -65,10 +86,14 @@ module ActsAsTaggableOn
65
86
 
66
87
  class << self
67
88
  private
68
- def like_operator
69
- connection.adapter_name == 'PostgreSQL' ? 'ILIKE' : 'LIKE'
70
- end
71
- end
72
89
 
90
+ def comparable_name(str)
91
+ str.mb_chars.downcase.to_s
92
+ end
93
+
94
+ def binary
95
+ /mysql/ === ActiveRecord::Base.connection_config[:adapter] ? "BINARY " : nil
96
+ end
97
+ end
73
98
  end
74
- end
99
+ end
@@ -1,14 +1,13 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
1
3
  module ActsAsTaggableOn
2
4
  class TagList < Array
3
- cattr_accessor :delimiter
4
- self.delimiter = ','
5
-
6
5
  attr_accessor :owner
7
6
 
8
7
  def initialize(*args)
9
8
  add(*args)
10
9
  end
11
-
10
+
12
11
  ##
13
12
  # Returns a new TagList using the given tag string.
14
13
  #
@@ -16,17 +15,18 @@ module ActsAsTaggableOn
16
15
  # tag_list = TagList.from("One , Two, Three")
17
16
  # tag_list # ["One", "Two", "Three"]
18
17
  def self.from(string)
19
- glue = delimiter.ends_with?(" ") ? delimiter : "#{delimiter} "
20
- string = string.join(glue) if string.respond_to?(:join)
18
+ string = string.join(ActsAsTaggableOn.glue) if string.respond_to?(:join)
21
19
 
22
20
  new.tap do |tag_list|
23
21
  string = string.to_s.dup
24
22
 
25
23
  # Parse the quoted tags
26
- string.gsub!(/(\A|#{delimiter})\s*"(.*?)"\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
27
- string.gsub!(/(\A|#{delimiter})\s*'(.*?)'\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
24
+ d = ActsAsTaggableOn.delimiter
25
+ d = d.join("|") if d.kind_of?(Array)
26
+ string.gsub!(/(\A|#{d})\s*"(.*?)"\s*(#{d}\s*|\z)/) { tag_list << $2; $3 }
27
+ string.gsub!(/(\A|#{d})\s*'(.*?)'\s*(#{d}\s*|\z)/) { tag_list << $2; $3 }
28
28
 
29
- tag_list.add(string.split(delimiter))
29
+ tag_list.add(string.split(Regexp.new d))
30
30
  end
31
31
  end
32
32
 
@@ -58,7 +58,7 @@ module ActsAsTaggableOn
58
58
  end
59
59
 
60
60
  ##
61
- # Transform the tag_list into a tag string suitable for edting in a form.
61
+ # Transform the tag_list into a tag string suitable for editing in a form.
62
62
  # The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
63
63
  #
64
64
  # Example:
@@ -69,16 +69,21 @@ module ActsAsTaggableOn
69
69
  tags.send(:clean!)
70
70
 
71
71
  tags.map do |name|
72
- name.include?(delimiter) ? "\"#{name}\"" : name
73
- end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
72
+ d = ActsAsTaggableOn.delimiter
73
+ d = Regexp.new d.join("|") if d.kind_of? Array
74
+ name.index(d) ? "\"#{name}\"" : name
75
+ end.join(ActsAsTaggableOn.glue)
74
76
  end
75
77
 
76
78
  private
77
-
79
+
78
80
  # Remove whitespace, duplicates, and blanks.
79
81
  def clean!
80
82
  reject!(&:blank?)
81
83
  map!(&:strip)
84
+ map!{ |tag| tag.mb_chars.downcase.to_s } if ActsAsTaggableOn.force_lowercase
85
+ map!(&:parameterize) if ActsAsTaggableOn.force_parameterize
86
+
82
87
  uniq!
83
88
  end
84
89
 
@@ -93,4 +98,4 @@ module ActsAsTaggableOn
93
98
  args.flatten!
94
99
  end
95
100
  end
96
- end
101
+ end
@@ -0,0 +1,102 @@
1
+ module ActsAsTaggableOn
2
+ module Taggable
3
+ def taggable?
4
+ false
5
+ end
6
+
7
+ ##
8
+ # This is an alias for calling <tt>acts_as_taggable_on :tags</tt>.
9
+ #
10
+ # Example:
11
+ # class Book < ActiveRecord::Base
12
+ # acts_as_taggable
13
+ # end
14
+ def acts_as_taggable
15
+ acts_as_taggable_on :tags
16
+ end
17
+
18
+ ##
19
+ # This is an alias for calling <tt>acts_as_ordered_taggable_on :tags</tt>.
20
+ #
21
+ # Example:
22
+ # class Book < ActiveRecord::Base
23
+ # acts_as_ordered_taggable
24
+ # end
25
+ def acts_as_ordered_taggable
26
+ acts_as_ordered_taggable_on :tags
27
+ end
28
+
29
+ ##
30
+ # Make a model taggable on specified contexts.
31
+ #
32
+ # @param [Array] tag_types An array of taggable contexts
33
+ #
34
+ # Example:
35
+ # class User < ActiveRecord::Base
36
+ # acts_as_taggable_on :languages, :skills
37
+ # end
38
+ def acts_as_taggable_on(*tag_types)
39
+ taggable_on(false, tag_types)
40
+ end
41
+
42
+
43
+ ##
44
+ # Make a model taggable on specified contexts
45
+ # and preserves the order in which tags are created
46
+ #
47
+ # @param [Array] tag_types An array of taggable contexts
48
+ #
49
+ # Example:
50
+ # class User < ActiveRecord::Base
51
+ # acts_as_ordered_taggable_on :languages, :skills
52
+ # end
53
+ def acts_as_ordered_taggable_on(*tag_types)
54
+ taggable_on(true, tag_types)
55
+ end
56
+
57
+ private
58
+
59
+ # Make a model taggable on specified contexts
60
+ # and optionally preserves the order in which tags are created
61
+ #
62
+ # Seperate methods used above for backwards compatibility
63
+ # so that the original acts_as_taggable_on method is unaffected
64
+ # as it's not possible to add another arguement to the method
65
+ # without the tag_types being enclosed in square brackets
66
+ #
67
+ # NB: method overridden in core module in order to create tag type
68
+ # associations and methods after this logic has executed
69
+ #
70
+ def taggable_on(preserve_tag_order, *tag_types)
71
+ tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)
72
+
73
+ if taggable?
74
+ self.tag_types = (self.tag_types + tag_types).uniq
75
+ self.preserve_tag_order = preserve_tag_order
76
+ else
77
+ class_attribute :tag_types
78
+ self.tag_types = tag_types
79
+ class_attribute :preserve_tag_order
80
+ self.preserve_tag_order = preserve_tag_order
81
+
82
+ class_eval do
83
+ has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "ActsAsTaggableOn::Tagging"
84
+ has_many :base_tags, :through => :taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
85
+
86
+ def self.taggable?
87
+ true
88
+ end
89
+
90
+ include ActsAsTaggableOn::Utils
91
+ include ActsAsTaggableOn::Taggable::Core
92
+ include ActsAsTaggableOn::Taggable::Collection
93
+ include ActsAsTaggableOn::Taggable::Cache
94
+ include ActsAsTaggableOn::Taggable::Ownership
95
+ include ActsAsTaggableOn::Taggable::Related
96
+ include ActsAsTaggableOn::Taggable::Dirty
97
+ end
98
+ end
99
+ end
100
+
101
+ end
102
+ end
@@ -31,7 +31,7 @@ module ActsAsTaggableOn
31
31
 
32
32
  module InstanceMethods
33
33
  ##
34
- # Tag a taggable model with tags that are owned by the tagger.
34
+ # Tag a taggable model with tags that are owned by the tagger.
35
35
  #
36
36
  # @param taggable The object that will be tagged
37
37
  # @param [Hash] options An hash with options. Available options are:
@@ -42,7 +42,7 @@ module ActsAsTaggableOn
42
42
  # @user.tag(@photo, :with => "paris, normandy", :on => :locations)
43
43
  def tag(taggable, opts={})
44
44
  opts.reverse_merge!(:force => true)
45
-
45
+ skip_save = opts.delete(:skip_save)
46
46
  return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
47
47
 
48
48
  raise "You need to specify a tag context using :on" unless opts.has_key?(:on)
@@ -50,7 +50,7 @@ module ActsAsTaggableOn
50
50
  raise "No context :#{opts[:on]} defined in #{taggable.class.to_s}" unless (opts[:force] || taggable.tag_types.include?(opts[:on]))
51
51
 
52
52
  taggable.set_owner_tag_list_on(self, opts[:on].to_s, opts[:with])
53
- taggable.save
53
+ taggable.save unless skip_save
54
54
  end
55
55
 
56
56
  def is_tagger?
@@ -1,7 +1,5 @@
1
1
  module ActsAsTaggableOn
2
2
  class Tagging < ::ActiveRecord::Base #:nodoc:
3
- include ActsAsTaggableOn::ActiveRecord::Backports if ::ActiveRecord::VERSION::MAJOR < 3
4
-
5
3
  attr_accessible :tag,
6
4
  :tag_id,
7
5
  :context,
@@ -20,5 +18,17 @@ module ActsAsTaggableOn
20
18
  validates_presence_of :tag_id
21
19
 
22
20
  validates_uniqueness_of :tag_id, :scope => [ :taggable_type, :taggable_id, :context, :tagger_id, :tagger_type ]
21
+
22
+ after_destroy :remove_unused_tags
23
+
24
+ private
25
+
26
+ def remove_unused_tags
27
+ if ActsAsTaggableOn.remove_unused_tags
28
+ if tag.taggings.count.zero?
29
+ tag.destroy
30
+ end
31
+ end
32
+ end
23
33
  end
24
34
  end
@@ -9,8 +9,8 @@ module ActsAsTaggableOn
9
9
  max_count = tags.sort_by(&:count).last.count.to_f
10
10
 
11
11
  tags.each do |tag|
12
- index = ((tag.count / max_count) * (classes.size - 1)).round
13
- yield tag, classes[index]
12
+ index = ((tag.count / max_count) * (classes.size - 1))
13
+ yield tag, classes[index.nan? ? 0 : index.round]
14
14
  end
15
15
  end
16
16
  end
@@ -0,0 +1,34 @@
1
+ module ActsAsTaggableOn
2
+ module Utils
3
+ def self.included(base)
4
+
5
+ base.send :include, ActsAsTaggableOn::Utils::OverallMethods
6
+ base.extend ActsAsTaggableOn::Utils::OverallMethods
7
+ end
8
+
9
+ module OverallMethods
10
+ def using_postgresql?
11
+ ::ActiveRecord::Base.connection && ::ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
12
+ end
13
+
14
+ def using_sqlite?
15
+ ::ActiveRecord::Base.connection && ::ActiveRecord::Base.connection.adapter_name == 'SQLite'
16
+ end
17
+
18
+ def sha_prefix(string)
19
+ Digest::SHA1.hexdigest("#{string}#{rand}")[0..6]
20
+ end
21
+
22
+ private
23
+ def like_operator
24
+ using_postgresql? ? 'ILIKE' : 'LIKE'
25
+ end
26
+
27
+ # escape _ and % characters in strings, since these are wildcards in SQL.
28
+ def escape_like(str)
29
+ str.gsub(/[!%_]/){ |x| '!' + x }
30
+ end
31
+ end
32
+
33
+ end
34
+ end
@@ -1,3 +1,4 @@
1
+ require 'rails/generators'
1
2
  require 'rails/generators/migration'
2
3
 
3
4
  module ActsAsTaggableOn
@@ -18,8 +19,14 @@ module ActsAsTaggableOn
18
19
  [:active_record].include? orm
19
20
  end
20
21
 
21
- def self.next_migration_number(path)
22
- Time.now.utc.strftime("%Y%m%d%H%M%S")
22
+ def self.next_migration_number(dirname)
23
+ if ActiveRecord::Base.timestamped_migrations
24
+ migration_number = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
25
+ migration_number += 1
26
+ migration_number.to_s
27
+ else
28
+ "%.3d" % (current_migration_number(dirname) + 1)
29
+ end
23
30
  end
24
31
 
25
32
  def create_migration_file
@@ -12,7 +12,9 @@ class ActsAsTaggableOnMigration < ActiveRecord::Migration
12
12
  t.references :taggable, :polymorphic => true
13
13
  t.references :tagger, :polymorphic => true
14
14
 
15
- t.string :context
15
+ # Limit is created to prevent MySQL error on index
16
+ # length for MyISAM table type: http://bit.ly/vgW2Ql
17
+ t.string :context, :limit => 128
16
18
 
17
19
  t.datetime :created_at
18
20
  end