acts-as-taggable-on 1.0.13 → 2.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +5 -2
- data/Gemfile +10 -0
- data/README.rdoc +56 -26
- data/Rakefile +46 -16
- data/VERSION +1 -1
- data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +7 -0
- data/generators/acts_as_taggable_on_migration/templates/migration.rb +29 -0
- data/lib/acts-as-taggable-on.rb +30 -7
- data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +53 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +132 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +241 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +101 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +65 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +43 -373
- data/lib/acts_as_taggable_on/acts_as_tagger.rb +60 -45
- data/lib/acts_as_taggable_on/compatibility/Gemfile +8 -0
- data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +17 -0
- data/lib/acts_as_taggable_on/compatibility/postgresql.rb +44 -0
- data/lib/acts_as_taggable_on/tag.rb +70 -22
- data/lib/acts_as_taggable_on/tag_list.rb +81 -80
- data/lib/acts_as_taggable_on/tagging.rb +23 -7
- data/lib/acts_as_taggable_on/tags_helper.rb +14 -8
- data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +32 -0
- data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +28 -0
- data/rails/init.rb +1 -7
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +102 -55
- data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +48 -6
- data/spec/acts_as_taggable_on/tag_list_spec.rb +21 -3
- data/spec/acts_as_taggable_on/tag_spec.rb +77 -24
- data/spec/acts_as_taggable_on/taggable_spec.rb +173 -76
- data/spec/acts_as_taggable_on/tagger_spec.rb +74 -6
- data/spec/acts_as_taggable_on/tagging_spec.rb +22 -7
- data/spec/acts_as_taggable_on/tags_helper_spec.rb +4 -6
- data/spec/bm.rb +52 -0
- data/spec/database.yml +17 -0
- data/spec/database.yml.sample +17 -0
- data/spec/models.rb +31 -0
- data/spec/schema.rb +13 -2
- data/spec/spec_helper.rb +52 -40
- metadata +31 -9
- data/lib/acts_as_taggable_on/group_helper.rb +0 -12
- data/spec/acts_as_taggable_on/group_helper_spec.rb +0 -18
- data/spec/spec.opts +0 -3
|
@@ -1,26 +1,74 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
module ActsAsTaggableOn
|
|
2
|
+
class Tag < ::ActiveRecord::Base
|
|
3
|
+
include ActsAsTaggableOn::ActiveRecord::Backports if ::ActiveRecord::VERSION::MAJOR < 3
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
attr_accessible :name
|
|
6
|
+
|
|
7
|
+
### ASSOCIATIONS:
|
|
8
|
+
|
|
9
|
+
has_many :taggings, :dependent => :destroy, :class_name => 'ActsAsTaggableOn::Tagging'
|
|
10
|
+
|
|
11
|
+
### VALIDATIONS:
|
|
12
|
+
|
|
13
|
+
validates_presence_of :name
|
|
14
|
+
validates_uniqueness_of :name
|
|
15
|
+
|
|
16
|
+
### SCOPES:
|
|
17
|
+
|
|
18
|
+
def self.named(name)
|
|
19
|
+
where(["name #{like_operator} ?", name])
|
|
20
|
+
end
|
|
6
21
|
|
|
7
|
-
|
|
8
|
-
|
|
22
|
+
def self.named_any(list)
|
|
23
|
+
where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", tag.to_s]) }.join(" OR "))
|
|
24
|
+
end
|
|
9
25
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
26
|
+
def self.named_like(name)
|
|
27
|
+
where(["name #{like_operator} ?", "%#{name}%"])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.named_like_any(list)
|
|
31
|
+
where(list.map { |tag| sanitize_sql(["name #{like_operator} ?", "%#{tag.to_s}%"]) }.join(" OR "))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
### CLASS METHODS:
|
|
35
|
+
|
|
36
|
+
def self.find_or_create_with_like_by_name(name)
|
|
37
|
+
named_like(name).first || create(:name => name)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.find_or_create_all_with_like_by_name(*list)
|
|
41
|
+
list = [list].flatten
|
|
42
|
+
|
|
43
|
+
return [] if list.empty?
|
|
44
|
+
|
|
45
|
+
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 } }
|
|
47
|
+
created_tags = new_tag_names.map { |name| Tag.create(:name => name) }
|
|
48
|
+
|
|
49
|
+
existing_tags + created_tags
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
### INSTANCE METHODS:
|
|
53
|
+
|
|
54
|
+
def ==(object)
|
|
55
|
+
super || (object.is_a?(Tag) && name == object.name)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_s
|
|
59
|
+
name
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def count
|
|
63
|
+
read_attribute(:count).to_i
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class << self
|
|
67
|
+
private
|
|
68
|
+
def like_operator
|
|
69
|
+
connection.adapter_name == 'PostgreSQL' ? 'ILIKE' : 'LIKE'
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
13
73
|
end
|
|
14
|
-
|
|
15
|
-
def ==(object)
|
|
16
|
-
super || (object.is_a?(Tag) && name == object.name)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def to_s
|
|
20
|
-
name
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def count
|
|
24
|
-
read_attribute(:count).to_i
|
|
25
|
-
end
|
|
26
|
-
end
|
|
74
|
+
end
|
|
@@ -1,95 +1,96 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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) }
|
|
1
|
+
module ActsAsTaggableOn
|
|
2
|
+
class TagList < Array
|
|
3
|
+
cattr_accessor :delimiter
|
|
4
|
+
self.delimiter = ','
|
|
5
|
+
|
|
6
|
+
attr_accessor :owner
|
|
7
|
+
|
|
8
|
+
def initialize(*args)
|
|
9
|
+
add(*args)
|
|
65
10
|
end
|
|
66
|
-
|
|
67
|
-
args.flatten!
|
|
68
|
-
end
|
|
69
11
|
|
|
70
|
-
|
|
12
|
+
##
|
|
71
13
|
# Returns a new TagList using the given tag string.
|
|
72
|
-
#
|
|
14
|
+
#
|
|
15
|
+
# Example:
|
|
73
16
|
# tag_list = TagList.from("One , Two, Three")
|
|
74
17
|
# tag_list # ["One", "Two", "Three"]
|
|
75
|
-
def from(string)
|
|
76
|
-
|
|
18
|
+
def self.from(string)
|
|
19
|
+
glue = delimiter.ends_with?(" ") ? delimiter : "#{delimiter} "
|
|
20
|
+
string = string.join(glue) if string.respond_to?(:join)
|
|
77
21
|
|
|
78
|
-
|
|
22
|
+
new.tap do |tag_list|
|
|
79
23
|
string = string.to_s.dup
|
|
80
|
-
|
|
24
|
+
|
|
81
25
|
# Parse the quoted tags
|
|
82
|
-
string.gsub!(/"(.*?)"\s
|
|
83
|
-
string.gsub!(/'(.*?)'\s
|
|
84
|
-
|
|
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 }
|
|
28
|
+
|
|
85
29
|
tag_list.add(string.split(delimiter))
|
|
86
30
|
end
|
|
87
31
|
end
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
32
|
+
|
|
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
|
|
46
|
+
|
|
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
|
|
59
|
+
|
|
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!)
|
|
70
|
+
|
|
71
|
+
tags.map do |name|
|
|
72
|
+
name.include?(delimiter) ? "\"#{name}\"" : name
|
|
73
|
+
end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# Remove whitespace, duplicates, and blanks.
|
|
79
|
+
def clean!
|
|
80
|
+
reject!(&:blank?)
|
|
81
|
+
map!(&:strip)
|
|
82
|
+
uniq!
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def extract_and_apply_options!(args)
|
|
86
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
|
87
|
+
options.assert_valid_keys :parse
|
|
88
|
+
|
|
89
|
+
if options[:parse]
|
|
90
|
+
args.map! { |a| self.class.from(a) }
|
|
92
91
|
end
|
|
92
|
+
|
|
93
|
+
args.flatten!
|
|
93
94
|
end
|
|
94
95
|
end
|
|
95
96
|
end
|
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
module ActsAsTaggableOn
|
|
2
|
+
class Tagging < ::ActiveRecord::Base #:nodoc:
|
|
3
|
+
include ActsAsTaggableOn::ActiveRecord::Backports if ::ActiveRecord::VERSION::MAJOR < 3
|
|
4
|
+
|
|
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
|
|
14
|
+
|
|
15
|
+
belongs_to :tag, :class_name => 'ActsAsTaggableOn::Tag'
|
|
16
|
+
belongs_to :taggable, :polymorphic => true
|
|
17
|
+
belongs_to :tagger, :polymorphic => true
|
|
18
|
+
|
|
19
|
+
validates_presence_of :context
|
|
20
|
+
validates_presence_of :tag_id
|
|
21
|
+
|
|
22
|
+
validates_uniqueness_of :tag_id, :scope => [ :taggable_type, :taggable_id, :context, :tagger_id, :tagger_type ]
|
|
23
|
+
end
|
|
8
24
|
end
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
module
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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)
|
|
6
|
+
|
|
7
|
+
return [] if tags.empty?
|
|
8
|
+
|
|
9
|
+
max_count = tags.sort_by(&:count).last.count.to_f
|
|
10
|
+
|
|
11
|
+
tags.each do |tag|
|
|
12
|
+
index = ((tag.count / max_count) * (classes.size - 1)).round
|
|
13
|
+
yield tag, classes[index]
|
|
14
|
+
end
|
|
9
15
|
end
|
|
10
16
|
end
|
|
11
17
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require 'rails/generators/migration'
|
|
2
|
+
|
|
3
|
+
module ActsAsTaggableOn
|
|
4
|
+
class MigrationGenerator < Rails::Generators::Base
|
|
5
|
+
include Rails::Generators::Migration
|
|
6
|
+
|
|
7
|
+
desc "Generates migration for Tag and Tagging models"
|
|
8
|
+
|
|
9
|
+
def self.orm
|
|
10
|
+
Rails::Generators.options[:rails][:orm]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.source_root
|
|
14
|
+
File.join(File.dirname(__FILE__), 'templates', (orm.to_s unless orm.class.eql?(String)) )
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.orm_has_migration?
|
|
18
|
+
[:active_record].include? orm
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.next_migration_number(path)
|
|
22
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create_migration_file
|
|
26
|
+
if self.class.orm_has_migration?
|
|
27
|
+
migration_template 'migration.rb', 'db/migrate/acts_as_taggable_on_migration'
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class ActsAsTaggableOnMigration < ActiveRecord::Migration
|
|
2
|
+
def self.up
|
|
3
|
+
create_table :tags do |t|
|
|
4
|
+
t.string :name
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
create_table :taggings do |t|
|
|
8
|
+
t.references :tag
|
|
9
|
+
|
|
10
|
+
# You should make sure that the column created is
|
|
11
|
+
# long enough to store the required class names.
|
|
12
|
+
t.references :taggable, :polymorphic => true
|
|
13
|
+
t.references :tagger, :polymorphic => true
|
|
14
|
+
|
|
15
|
+
t.string :context
|
|
16
|
+
|
|
17
|
+
t.datetime :created_at
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
add_index :taggings, :tag_id
|
|
21
|
+
add_index :taggings, [:taggable_id, :taggable_type, :context]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.down
|
|
25
|
+
drop_table :taggings
|
|
26
|
+
drop_table :tags
|
|
27
|
+
end
|
|
28
|
+
end
|
data/rails/init.rb
CHANGED
|
@@ -1,7 +1 @@
|
|
|
1
|
-
require 'acts-as-taggable-on'
|
|
2
|
-
|
|
3
|
-
ActiveRecord::Base.send :include, ActiveRecord::Acts::TaggableOn
|
|
4
|
-
ActiveRecord::Base.send :include, ActiveRecord::Acts::Tagger
|
|
5
|
-
ActionView::Base.send :include, TagsHelper if defined?(ActionView::Base)
|
|
6
|
-
|
|
7
|
-
RAILS_DEFAULT_LOGGER.info "** acts_as_taggable_on: initialized properly."
|
|
1
|
+
require 'acts-as-taggable-on'
|