acts-as-taggable-on 2.0.3 → 2.1.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/.gitignore +7 -0
- data/.travis.yml +10 -0
- data/Gemfile +2 -5
- data/Guardfile +5 -0
- data/README.rdoc +19 -16
- data/Rakefile +9 -55
- data/VERSION +1 -1
- data/acts-as-taggable-on.gemspec +27 -0
- data/lib/acts-as-taggable-on/version.rb +4 -0
- data/lib/acts-as-taggable-on.rb +8 -2
- 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 +40 -31
- data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +67 -32
- data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +16 -12
- data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +17 -9
- data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +16 -6
- data/lib/acts_as_taggable_on/acts_as_tagger.rb +2 -2
- data/lib/acts_as_taggable_on/compatibility/Gemfile +3 -1
- data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +5 -1
- data/lib/acts_as_taggable_on/tag.rb +73 -57
- data/lib/acts_as_taggable_on/tag_list.rb +79 -78
- data/lib/acts_as_taggable_on/tagging.rb +19 -18
- data/lib/acts_as_taggable_on/tags_helper.rb +12 -12
- data/lib/acts_as_taggable_on/utils.rb +31 -0
- data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +3 -2
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +25 -2
- data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +3 -3
- data/spec/acts_as_taggable_on/tag_list_spec.rb +3 -3
- data/spec/acts_as_taggable_on/tag_spec.rb +41 -21
- data/spec/acts_as_taggable_on/taggable_spec.rb +54 -12
- data/spec/acts_as_taggable_on/tagger_spec.rb +5 -5
- data/spec/acts_as_taggable_on/tagging_spec.rb +7 -7
- data/spec/acts_as_taggable_on/tags_helper_spec.rb +3 -3
- data/spec/acts_as_taggable_on/utils_spec.rb +22 -0
- data/spec/database.yml.sample +19 -0
- data/spec/models.rb +4 -0
- data/spec/schema.rb +6 -0
- data/spec/spec_helper.rb +60 -33
- data/uninstall.rb +1 -0
- metadata +130 -15
- /data/{spec/spec.opts → .rspec} +0 -0
|
@@ -30,9 +30,13 @@ module ActsAsTaggableOn::Taggable
|
|
|
30
30
|
|
|
31
31
|
module InstanceMethods
|
|
32
32
|
def owner_tags_on(owner, context)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
if owner.nil?
|
|
34
|
+
base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s]).all
|
|
35
|
+
else
|
|
36
|
+
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.to_s]).all
|
|
39
|
+
end
|
|
36
40
|
end
|
|
37
41
|
|
|
38
42
|
def cached_owned_tag_list_on(context)
|
|
@@ -46,7 +50,7 @@ module ActsAsTaggableOn::Taggable
|
|
|
46
50
|
cache = cached_owned_tag_list_on(context)
|
|
47
51
|
cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
|
|
48
52
|
|
|
49
|
-
cache[owner] ||= TagList.new(*owner_tags_on(owner, context).map(&:name))
|
|
53
|
+
cache[owner] ||= ActsAsTaggableOn::TagList.new(*owner_tags_on(owner, context).map(&:name))
|
|
50
54
|
end
|
|
51
55
|
|
|
52
56
|
def set_owner_tag_list_on(owner, context, new_list)
|
|
@@ -55,22 +59,22 @@ module ActsAsTaggableOn::Taggable
|
|
|
55
59
|
cache = cached_owned_tag_list_on(context)
|
|
56
60
|
cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
|
|
57
61
|
|
|
58
|
-
cache[owner] = TagList.from(new_list)
|
|
62
|
+
cache[owner] = ActsAsTaggableOn::TagList.from(new_list)
|
|
59
63
|
end
|
|
60
64
|
|
|
61
|
-
def reload
|
|
65
|
+
def reload(*args)
|
|
62
66
|
self.class.tag_types.each do |context|
|
|
63
67
|
instance_variable_set("@owned_#{context}_list", nil)
|
|
64
68
|
end
|
|
65
69
|
|
|
66
|
-
super
|
|
70
|
+
super(*args)
|
|
67
71
|
end
|
|
68
72
|
|
|
69
73
|
def save_owned_tags
|
|
70
74
|
tagging_contexts.each do |context|
|
|
71
75
|
cached_owned_tag_list_on(context).each do |owner, tag_list|
|
|
72
76
|
# Find existing tags or create non-existing tags:
|
|
73
|
-
tag_list = Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
|
|
77
|
+
tag_list = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
|
|
74
78
|
|
|
75
79
|
owned_tags = owner_tags_on(owner, context)
|
|
76
80
|
old_tags = owned_tags - tag_list
|
|
@@ -78,13 +82,13 @@ module ActsAsTaggableOn::Taggable
|
|
|
78
82
|
|
|
79
83
|
# Find all taggings that belong to the taggable (self), are owned by the owner,
|
|
80
84
|
# have the correct context, and are removed from the list.
|
|
81
|
-
old_taggings = Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
old_taggings = ActsAsTaggableOn::Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
|
|
86
|
+
:tagger_type => owner.class.to_s, :tagger_id => owner.id,
|
|
87
|
+
:tag_id => old_tags, :context => context).all
|
|
84
88
|
|
|
85
89
|
if old_taggings.present?
|
|
86
90
|
# Destroy old taggings:
|
|
87
|
-
Tagging.destroy_all(:id => old_taggings.map(&:id))
|
|
91
|
+
ActsAsTaggableOn::Tagging.destroy_all(:id => old_taggings.map(&:id))
|
|
88
92
|
end
|
|
89
93
|
|
|
90
94
|
# Create new taggings:
|
|
@@ -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
|
-
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
unless tag_types.empty?
|
|
25
|
+
class_eval %(
|
|
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
|
|
@@ -42,10 +46,12 @@ module ActsAsTaggableOn::Taggable
|
|
|
42
46
|
|
|
43
47
|
exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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}.id) AS count",
|
|
52
|
+
:from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
|
|
53
|
+
: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],
|
|
54
|
+
:group => group_columns,
|
|
49
55
|
:order => "count DESC" }.update(options))
|
|
50
56
|
end
|
|
51
57
|
|
|
@@ -54,10 +60,12 @@ module ActsAsTaggableOn::Taggable
|
|
|
54
60
|
|
|
55
61
|
exclude_self = "#{klass.table_name}.id != #{id} AND" if self.class == klass
|
|
56
62
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
:
|
|
63
|
+
group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(klass) : "#{klass.table_name}.#{klass.primary_key}"
|
|
64
|
+
|
|
65
|
+
klass.scoped({ :select => "#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.id) AS count",
|
|
66
|
+
:from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
|
|
67
|
+
: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],
|
|
68
|
+
:group => group_columns,
|
|
61
69
|
:order => "count DESC" }.update(options))
|
|
62
70
|
end
|
|
63
71
|
end
|
|
@@ -28,19 +28,29 @@ module ActsAsTaggableOn
|
|
|
28
28
|
tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)
|
|
29
29
|
|
|
30
30
|
if taggable?
|
|
31
|
-
|
|
31
|
+
if RAILS_3
|
|
32
|
+
self.tag_types = (self.tag_types + tag_types).uniq
|
|
33
|
+
else
|
|
34
|
+
write_inheritable_attribute(:tag_types, (self.tag_types + tag_types).uniq)
|
|
35
|
+
end
|
|
32
36
|
else
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
if RAILS_3
|
|
38
|
+
class_attribute :tag_types
|
|
39
|
+
self.tag_types = tag_types
|
|
40
|
+
else
|
|
41
|
+
write_inheritable_attribute(:tag_types, tag_types)
|
|
42
|
+
class_inheritable_reader(:tag_types)
|
|
43
|
+
end
|
|
35
44
|
|
|
36
45
|
class_eval do
|
|
37
|
-
has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag
|
|
38
|
-
has_many :base_tags, :
|
|
46
|
+
has_many :taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "ActsAsTaggableOn::Tagging"
|
|
47
|
+
has_many :base_tags, :through => :taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
|
|
39
48
|
|
|
40
49
|
def self.taggable?
|
|
41
50
|
true
|
|
42
51
|
end
|
|
43
|
-
|
|
52
|
+
|
|
53
|
+
include ActsAsTaggableOn::Utils
|
|
44
54
|
include ActsAsTaggableOn::Taggable::Core
|
|
45
55
|
include ActsAsTaggableOn::Taggable::Collection
|
|
46
56
|
include ActsAsTaggableOn::Taggable::Cache
|
|
@@ -16,8 +16,8 @@ module ActsAsTaggableOn
|
|
|
16
16
|
def acts_as_tagger(opts={})
|
|
17
17
|
class_eval do
|
|
18
18
|
has_many :owned_taggings, opts.merge(:as => :tagger, :dependent => :destroy,
|
|
19
|
-
:include => :tag, :class_name => "Tagging")
|
|
20
|
-
has_many :owned_tags, :through => :owned_taggings, :source => :tag, :uniq => true
|
|
19
|
+
:include => :tag, :class_name => "ActsAsTaggableOn::Tagging")
|
|
20
|
+
has_many :owned_tags, :through => :owned_taggings, :source => :tag, :uniq => true, :class_name => "ActsAsTaggableOn::Tag"
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
include ActsAsTaggableOn::Tagger::InstanceMethods
|
|
@@ -9,7 +9,11 @@ module ActsAsTaggableOn
|
|
|
9
9
|
named_scope :order, lambda { |order| { :order => order } }
|
|
10
10
|
named_scope :select, lambda { |select| { :select => select } }
|
|
11
11
|
named_scope :limit, lambda { |limit| { :limit => limit } }
|
|
12
|
-
named_scope :readonly, lambda { |readonly| { :readonly => readonly } }
|
|
12
|
+
named_scope :readonly, lambda { |readonly| { :readonly => readonly } }
|
|
13
|
+
|
|
14
|
+
def self.to_sql
|
|
15
|
+
construct_finder_sql({})
|
|
16
|
+
end
|
|
13
17
|
end
|
|
14
18
|
end
|
|
15
19
|
end
|
|
@@ -1,65 +1,81 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
module ActsAsTaggableOn
|
|
2
|
+
class Tag < ::ActiveRecord::Base
|
|
3
|
+
include ActsAsTaggableOn::ActiveRecord::Backports if ::ActiveRecord::VERSION::MAJOR < 3
|
|
4
|
+
include ActsAsTaggableOn::Utils
|
|
5
|
+
|
|
6
|
+
attr_accessible :name
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
### ASSOCIATIONS:
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
has_many :taggings, :dependent => :destroy, :class_name => 'ActsAsTaggableOn::Tagging'
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
validates_uniqueness_of :name
|
|
12
|
+
### VALIDATIONS:
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
validates_presence_of :name
|
|
15
|
+
validates_uniqueness_of :name
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
### SCOPES:
|
|
18
|
+
|
|
19
|
+
def self.named(name)
|
|
20
|
+
where(["name #{like_operator} ?", escape_like(name)])
|
|
21
|
+
end
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
def self.named_any(list)
|
|
24
|
+
where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", escape_like(tag.to_s)]) }.join(" OR "))
|
|
25
|
+
end
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
27
|
+
def self.named_like(name)
|
|
28
|
+
where(["name #{like_operator} ?", "%#{escape_like(name)}%"])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.named_like_any(list)
|
|
32
|
+
where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", "%#{escape_like(tag.to_s)}%"]) }.join(" OR "))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
### CLASS METHODS:
|
|
36
|
+
|
|
37
|
+
def self.find_or_create_with_like_by_name(name)
|
|
38
|
+
named_like(name).first || create(:name => name)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.find_or_create_all_with_like_by_name(*list)
|
|
42
|
+
list = [list].flatten
|
|
43
|
+
|
|
44
|
+
return [] if list.empty?
|
|
45
|
+
|
|
46
|
+
existing_tags = Tag.named_any(list).all
|
|
47
|
+
new_tag_names = list.reject do |name|
|
|
48
|
+
name = comparable_name(name)
|
|
49
|
+
existing_tags.any? { |tag| comparable_name(tag.name) == name }
|
|
50
|
+
end
|
|
51
|
+
created_tags = new_tag_names.map { |name| Tag.create(:name => name) }
|
|
52
|
+
|
|
53
|
+
existing_tags + created_tags
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
### INSTANCE METHODS:
|
|
57
|
+
|
|
58
|
+
def ==(object)
|
|
59
|
+
super || (object.is_a?(Tag) && name == object.name)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def to_s
|
|
63
|
+
name
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def count
|
|
67
|
+
read_attribute(:count).to_i
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def safe_name
|
|
71
|
+
name.gsub(/[^a-zA-Z0-9]/, '')
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class << self
|
|
75
|
+
private
|
|
76
|
+
def comparable_name(str)
|
|
77
|
+
RUBY_VERSION >= "1.9" ? str.downcase : str.mb_chars.downcase
|
|
78
|
+
end
|
|
79
|
+
end
|
|
55
80
|
end
|
|
56
|
-
|
|
57
|
-
def to_s
|
|
58
|
-
name
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def count
|
|
62
|
-
read_attribute(:count).to_i
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
end
|
|
81
|
+
end
|
|
@@ -1,95 +1,96 @@
|
|
|
1
|
-
|
|
1
|
+
module ActsAsTaggableOn
|
|
2
|
+
class TagList < Array
|
|
3
|
+
cattr_accessor :delimiter
|
|
4
|
+
self.delimiter = ','
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
self.delimiter = ','
|
|
6
|
+
attr_accessor :owner
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
add(*args)
|
|
10
|
-
end
|
|
8
|
+
def initialize(*args)
|
|
9
|
+
add(*args)
|
|
10
|
+
end
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
##
|
|
13
|
+
# Returns a new TagList using the given tag string.
|
|
14
|
+
#
|
|
15
|
+
# Example:
|
|
16
|
+
# tag_list = TagList.from("One , Two, Three")
|
|
17
|
+
# tag_list # ["One", "Two", "Three"]
|
|
18
|
+
def self.from(string)
|
|
19
|
+
glue = delimiter.ends_with?(" ") ? delimiter : "#{delimiter} "
|
|
20
|
+
string = string.join(glue) if string.respond_to?(:join)
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
new.tap do |tag_list|
|
|
23
|
+
string = string.to_s.dup
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
# 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 }
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
tag_list.add(string.split(delimiter))
|
|
30
|
+
end
|
|
29
31
|
end
|
|
30
|
-
end
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
##
|
|
34
|
+
# Add tags to the tag_list. Duplicate or blank tags will be ignored.
|
|
35
|
+
# Use the <tt>:parse</tt> option to add an unparsed tag string.
|
|
36
|
+
#
|
|
37
|
+
# Example:
|
|
38
|
+
# tag_list.add("Fun", "Happy")
|
|
39
|
+
# tag_list.add("Fun, Happy", :parse => true)
|
|
40
|
+
def add(*names)
|
|
41
|
+
extract_and_apply_options!(names)
|
|
42
|
+
concat(names)
|
|
43
|
+
clean!
|
|
44
|
+
self
|
|
45
|
+
end
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
##
|
|
48
|
+
# Remove specific tags from the tag_list.
|
|
49
|
+
# Use the <tt>:parse</tt> option to add an unparsed tag string.
|
|
50
|
+
#
|
|
51
|
+
# Example:
|
|
52
|
+
# tag_list.remove("Sad", "Lonely")
|
|
53
|
+
# tag_list.remove("Sad, Lonely", :parse => true)
|
|
54
|
+
def remove(*names)
|
|
55
|
+
extract_and_apply_options!(names)
|
|
56
|
+
delete_if { |name| names.include?(name) }
|
|
57
|
+
self
|
|
58
|
+
end
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
##
|
|
61
|
+
# Transform the tag_list into a tag string suitable for edting in a form.
|
|
62
|
+
# The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
|
|
63
|
+
#
|
|
64
|
+
# Example:
|
|
65
|
+
# tag_list = TagList.new("Round", "Square,Cube")
|
|
66
|
+
# tag_list.to_s # 'Round, "Square,Cube"'
|
|
67
|
+
def to_s
|
|
68
|
+
tags = frozen? ? self.dup : self
|
|
69
|
+
tags.send(:clean!)
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
tags.map do |name|
|
|
72
|
+
name.include?(delimiter) ? "\"#{name}\"" : name
|
|
73
|
+
end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
|
|
74
|
+
end
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
private
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
# Remove whitespace, duplicates, and blanks.
|
|
79
|
+
def clean!
|
|
80
|
+
reject!(&:blank?)
|
|
81
|
+
map!(&:strip)
|
|
82
|
+
uniq!
|
|
83
|
+
end
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
def extract_and_apply_options!(args)
|
|
86
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
87
|
+
options.assert_valid_keys :parse
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
if options[:parse]
|
|
90
|
+
args.map! { |a| self.class.from(a) }
|
|
91
|
+
end
|
|
91
92
|
|
|
92
|
-
|
|
93
|
+
args.flatten!
|
|
94
|
+
end
|
|
93
95
|
end
|
|
94
|
-
|
|
95
|
-
end
|
|
96
|
+
end
|
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
module ActsAsTaggableOn
|
|
2
|
+
class Tagging < ::ActiveRecord::Base #:nodoc:
|
|
3
|
+
include ActsAsTaggableOn::ActiveRecord::Backports if ::ActiveRecord::VERSION::MAJOR < 3
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
attr_accessible :tag,
|
|
6
|
+
:tag_id,
|
|
7
|
+
:context,
|
|
8
|
+
:taggable,
|
|
9
|
+
:taggable_type,
|
|
10
|
+
:taggable_id,
|
|
11
|
+
:tagger,
|
|
12
|
+
:tagger_type,
|
|
13
|
+
:tagger_id
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
belongs_to :tag, :class_name => 'ActsAsTaggableOn::Tag'
|
|
16
|
+
belongs_to :taggable, :polymorphic => true
|
|
17
|
+
belongs_to :tagger, :polymorphic => true
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
validates_uniqueness_of :tag_id, :scope => [ :taggable_type, :taggable_id, :context, :tagger_id, :tagger_type ]
|
|
19
|
+
validates_presence_of :context
|
|
20
|
+
validates_presence_of :tag_id
|
|
22
21
|
|
|
22
|
+
validates_uniqueness_of :tag_id, :scope => [ :taggable_type, :taggable_id, :context, :tagger_id, :tagger_type ]
|
|
23
|
+
end
|
|
23
24
|
end
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
module
|
|
1
|
+
module ActsAsTaggableOn
|
|
2
|
+
module TagsHelper
|
|
3
|
+
# See the README for an example using tag_cloud.
|
|
4
|
+
def tag_cloud(tags, classes)
|
|
5
|
+
tags = tags.all if tags.respond_to?(:all)
|
|
2
6
|
|
|
3
|
-
|
|
4
|
-
def tag_cloud(tags, classes)
|
|
5
|
-
tags = tags.all if tags.respond_to?(:all)
|
|
7
|
+
return [] if tags.empty?
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
max_count = tags.sort_by(&:count).last.count.to_f
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
yield tag, classes[index]
|
|
11
|
+
tags.each do |tag|
|
|
12
|
+
index = ((tag.count / max_count) * (classes.size - 1)).round
|
|
13
|
+
yield tag, classes[index]
|
|
14
|
+
end
|
|
14
15
|
end
|
|
15
16
|
end
|
|
16
|
-
|
|
17
|
-
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
private
|
|
19
|
+
def like_operator
|
|
20
|
+
using_postgresql? ? 'ILIKE' : 'LIKE'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# escape _ and % characters in strings, since these are wildcards in SQL.
|
|
24
|
+
def escape_like(str)
|
|
25
|
+
return str if using_sqlite? # skip escaping for SQLite
|
|
26
|
+
str.to_s.gsub("_", "\\\_").gsub("%", "\\\%")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -11,7 +11,7 @@ module ActsAsTaggableOn
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def self.source_root
|
|
14
|
-
File.join(File.dirname(__FILE__), 'templates', orm)
|
|
14
|
+
File.join(File.dirname(__FILE__), 'templates', (orm.to_s unless orm.class.eql?(String)) )
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def self.orm_has_migration?
|
|
@@ -19,7 +19,7 @@ module ActsAsTaggableOn
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def self.next_migration_number(path)
|
|
22
|
-
|
|
22
|
+
ActiveRecord::Generators::Base.next_migration_number(path)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def create_migration_file
|
|
@@ -29,3 +29,4 @@ module ActsAsTaggableOn
|
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
|
+
|