acts-as-taggable-on 2.0.6 → 2.4.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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +9 -0
- data/CHANGELOG.md +35 -0
- data/Gemfile +2 -9
- data/Guardfile +5 -0
- data/{MIT-LICENSE → MIT-LICENSE.md} +1 -1
- data/README.md +297 -0
- data/Rakefile +9 -55
- data/UPGRADING +14 -0
- data/acts-as-taggable-on.gemspec +32 -0
- data/lib/acts-as-taggable-on/version.rb +4 -0
- data/lib/acts-as-taggable-on.rb +37 -4
- data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +6 -6
- data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +99 -45
- data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +162 -45
- data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +37 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +40 -15
- data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +28 -18
- data/lib/acts_as_taggable_on/tag.rb +41 -16
- data/lib/acts_as_taggable_on/tag_list.rb +19 -14
- data/lib/acts_as_taggable_on/taggable.rb +102 -0
- data/lib/acts_as_taggable_on/{acts_as_tagger.rb → tagger.rb} +3 -3
- data/lib/acts_as_taggable_on/tagging.rb +12 -2
- data/lib/acts_as_taggable_on/tags_helper.rb +2 -2
- data/lib/acts_as_taggable_on/utils.rb +34 -0
- data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +9 -2
- data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +3 -1
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +333 -54
- data/spec/acts_as_taggable_on/tag_list_spec.rb +117 -61
- data/spec/acts_as_taggable_on/tag_spec.rb +111 -14
- data/spec/acts_as_taggable_on/taggable_spec.rb +330 -34
- data/spec/acts_as_taggable_on/tagger_spec.rb +62 -15
- data/spec/acts_as_taggable_on/tagging_spec.rb +2 -5
- data/spec/acts_as_taggable_on/tags_helper_spec.rb +16 -0
- data/spec/acts_as_taggable_on/utils_spec.rb +21 -0
- data/spec/database.yml.sample +4 -2
- data/spec/generators/acts_as_taggable_on/migration/migration_generator_spec.rb +22 -0
- data/spec/models.rb +28 -1
- data/spec/schema.rb +18 -0
- data/spec/spec_helper.rb +30 -7
- data/uninstall.rb +1 -0
- metadata +174 -57
- data/CHANGELOG +0 -25
- data/README.rdoc +0 -221
- data/VERSION +0 -1
- data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +0 -7
- data/generators/acts_as_taggable_on_migration/templates/migration.rb +0 -29
- data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +0 -53
- data/lib/acts_as_taggable_on/compatibility/Gemfile +0 -8
- data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +0 -17
- data/lib/acts_as_taggable_on/compatibility/postgresql.rb +0 -44
- 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
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
82
|
+
tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
|
74
83
|
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
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}.
|
44
|
-
|
45
|
-
|
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}.
|
48
|
-
:group =>
|
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
|
-
|
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
|
-
|
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}.
|
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}.
|
60
|
-
:group =>
|
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::
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
27
|
-
|
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(
|
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
|
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
|
-
|
73
|
-
|
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))
|
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(
|
22
|
-
|
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
|
-
|
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
|