acts-as-taggable-on 1.1.9 → 2.0.0.pre1
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 +8 -0
- data/README.rdoc +0 -10
- data/Rakefile +6 -5
- data/VERSION +1 -1
- data/lib/acts-as-taggable-on.rb +27 -7
- data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +66 -85
- data/lib/acts_as_taggable_on/acts_as_tagger.rb +21 -13
- data/lib/acts_as_taggable_on/group_helper.rb +2 -0
- data/lib/acts_as_taggable_on/tag.rb +23 -22
- data/lib/acts_as_taggable_on/tag_list.rb +24 -19
- data/lib/acts_as_taggable_on/tagging.rb +15 -7
- data/lib/acts_as_taggable_on/tags_helper.rb +6 -2
- data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +31 -0
- data/{generators/acts_as_taggable_on_migration/templates → lib/generators/acts_as_taggable_on/migration/templates/active_record}/migration.rb +12 -13
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +5 -9
- data/spec/acts_as_taggable_on/group_helper_spec.rb +1 -1
- data/spec/acts_as_taggable_on/tag_spec.rb +20 -20
- data/spec/acts_as_taggable_on/taggable_spec.rb +26 -45
- data/spec/acts_as_taggable_on/tagger_spec.rb +0 -16
- data/spec/acts_as_taggable_on/tagging_spec.rb +5 -5
- data/spec/schema.rb +2 -4
- data/spec/spec.opts +1 -2
- data/spec/spec_helper.rb +6 -10
- metadata +20 -10
- data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +0 -7
- data/rails/init.rb +0 -5
@@ -1,52 +1,60 @@
|
|
1
1
|
module ActiveRecord
|
2
2
|
module Acts
|
3
3
|
module Tagger
|
4
|
+
|
4
5
|
def self.included(base)
|
5
6
|
base.extend ClassMethods
|
6
7
|
end
|
7
|
-
|
8
|
+
|
8
9
|
module ClassMethods
|
10
|
+
|
9
11
|
def acts_as_tagger(opts={})
|
10
|
-
has_many :owned_taggings, opts.merge(:as => :tagger, :dependent => :destroy,
|
12
|
+
has_many :owned_taggings, opts.merge(:as => :tagger, :dependent => :destroy,
|
11
13
|
:include => :tag, :class_name => "Tagging")
|
12
14
|
has_many :owned_tags, :through => :owned_taggings, :source => :tag, :uniq => true
|
15
|
+
|
13
16
|
include ActiveRecord::Acts::Tagger::InstanceMethods
|
14
|
-
extend ActiveRecord::Acts::Tagger::SingletonMethods
|
17
|
+
extend ActiveRecord::Acts::Tagger::SingletonMethods
|
15
18
|
end
|
16
|
-
|
19
|
+
|
17
20
|
def is_tagger?
|
18
21
|
false
|
19
22
|
end
|
23
|
+
|
20
24
|
end
|
21
|
-
|
25
|
+
|
22
26
|
module InstanceMethods
|
27
|
+
|
23
28
|
def self.included(base)
|
24
29
|
end
|
25
|
-
|
30
|
+
|
26
31
|
def tag(taggable, opts={})
|
27
32
|
opts.reverse_merge!(:force => true)
|
28
33
|
|
29
34
|
return false unless taggable.respond_to?(:is_taggable?) && taggable.is_taggable?
|
30
|
-
|
31
|
-
raise "You need to specify
|
32
|
-
raise "
|
33
|
-
|
35
|
+
|
36
|
+
raise "You need to specify a tag context using :on" unless opts.has_key?(:on)
|
37
|
+
raise "You need to specify some tags using :with" unless opts.has_key?(:with)
|
38
|
+
raise "No context :#{opts[:on]} defined in #{taggable.class.to_s}" unless (opts[:force] || taggable.tag_types.include?(opts[:on]))
|
34
39
|
|
35
40
|
taggable.set_tag_list_on(opts[:on].to_s, opts[:with], self)
|
36
41
|
taggable.save
|
37
42
|
end
|
38
|
-
|
43
|
+
|
39
44
|
def is_tagger?
|
40
45
|
self.class.is_tagger?
|
41
46
|
end
|
47
|
+
|
42
48
|
end
|
43
|
-
|
49
|
+
|
44
50
|
module SingletonMethods
|
51
|
+
|
45
52
|
def is_tagger?
|
46
53
|
true
|
47
54
|
end
|
55
|
+
|
48
56
|
end
|
49
|
-
|
57
|
+
|
50
58
|
end
|
51
59
|
end
|
52
60
|
end
|
@@ -2,10 +2,12 @@ module ActiveRecord
|
|
2
2
|
module Acts
|
3
3
|
module TaggableOn
|
4
4
|
module GroupHelper
|
5
|
+
|
5
6
|
# all column names are necessary for PostgreSQL group clause
|
6
7
|
def grouped_column_names_for(object)
|
7
8
|
object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
|
8
9
|
end
|
10
|
+
|
9
11
|
end
|
10
12
|
end
|
11
13
|
end
|
@@ -1,52 +1,53 @@
|
|
1
1
|
class Tag < ActiveRecord::Base
|
2
|
-
|
2
|
+
|
3
3
|
attr_accessible :name
|
4
|
-
|
4
|
+
|
5
5
|
### ASSOCIATIONS:
|
6
|
-
|
6
|
+
|
7
7
|
has_many :taggings, :dependent => :destroy
|
8
|
-
|
8
|
+
|
9
9
|
### VALIDATIONS:
|
10
|
-
|
10
|
+
|
11
11
|
validates_presence_of :name
|
12
12
|
validates_uniqueness_of :name
|
13
|
-
|
13
|
+
|
14
14
|
### NAMED SCOPES:
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
15
|
+
|
16
|
+
scope :named, lambda { |name| { :conditions => ["name LIKE ?", name] } }
|
17
|
+
scope :named_any, lambda { |list| { :conditions => list.map { |tag| sanitize_sql(["name LIKE ?", tag.to_s]) }.join(" OR ") } }
|
18
|
+
scope :named_like, lambda { |name| { :conditions => ["name LIKE ?", "%#{name}%"] } }
|
19
|
+
scope :named_like_any, lambda { |list| { :conditions => list.map { |tag| sanitize_sql(["name LIKE ?", "%#{tag.to_s}%"]) }.join(" OR ") } }
|
20
|
+
|
21
21
|
### CLASS METHODS:
|
22
|
-
|
22
|
+
|
23
23
|
def self.find_or_create_with_like_by_name(name)
|
24
24
|
named_like(name).first || create(:name => name)
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
def self.find_or_create_all_with_like_by_name(*list)
|
28
28
|
list = [list].flatten
|
29
|
-
|
29
|
+
|
30
30
|
return [] if list.empty?
|
31
31
|
|
32
32
|
existing_tags = Tag.named_any(list).all
|
33
|
-
new_tag_names = list.reject { |name| existing_tags.any? { |tag| tag.name.
|
33
|
+
new_tag_names = list.reject { |name| existing_tags.any? { |tag| tag.name.downcase == name.downcase } }
|
34
34
|
created_tags = new_tag_names.map { |name| Tag.create(:name => name) }
|
35
|
-
|
36
|
-
existing_tags + created_tags
|
35
|
+
|
36
|
+
existing_tags + created_tags
|
37
37
|
end
|
38
|
-
|
38
|
+
|
39
39
|
### INSTANCE METHODS:
|
40
|
-
|
40
|
+
|
41
41
|
def ==(object)
|
42
42
|
super || (object.is_a?(Tag) && name == object.name)
|
43
43
|
end
|
44
|
-
|
44
|
+
|
45
45
|
def to_s
|
46
46
|
name
|
47
47
|
end
|
48
|
-
|
48
|
+
|
49
49
|
def count
|
50
50
|
read_attribute(:count).to_i
|
51
51
|
end
|
52
|
+
|
52
53
|
end
|
@@ -1,17 +1,19 @@
|
|
1
1
|
class TagList < Array
|
2
|
+
|
2
3
|
cattr_accessor :delimiter
|
4
|
+
|
3
5
|
self.delimiter = ','
|
4
|
-
|
6
|
+
|
5
7
|
def initialize(*args)
|
6
8
|
add(*args)
|
7
9
|
end
|
8
|
-
|
10
|
+
|
9
11
|
attr_accessor :owner
|
10
|
-
|
12
|
+
|
11
13
|
# Add tags to the tag_list. Duplicate or blank tags will be ignored.
|
12
14
|
#
|
13
15
|
# tag_list.add("Fun", "Happy")
|
14
|
-
#
|
16
|
+
#
|
15
17
|
# Use the <tt>:parse</tt> option to add an unparsed tag string.
|
16
18
|
#
|
17
19
|
# tag_list.add("Fun, Happy", :parse => true)
|
@@ -21,20 +23,20 @@ class TagList < Array
|
|
21
23
|
clean!
|
22
24
|
self
|
23
25
|
end
|
24
|
-
|
26
|
+
|
25
27
|
# Remove specific tags from the tag_list.
|
26
|
-
#
|
28
|
+
#
|
27
29
|
# tag_list.remove("Sad", "Lonely")
|
28
30
|
#
|
29
31
|
# Like #add, the <tt>:parse</tt> option can be used to remove multiple tags in a string.
|
30
|
-
#
|
32
|
+
#
|
31
33
|
# tag_list.remove("Sad, Lonely", :parse => true)
|
32
34
|
def remove(*names)
|
33
35
|
extract_and_apply_options!(names)
|
34
36
|
delete_if { |name| names.include?(name) }
|
35
37
|
self
|
36
38
|
end
|
37
|
-
|
39
|
+
|
38
40
|
# Transform the tag_list into a tag string suitable for edting in a form.
|
39
41
|
# The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
|
40
42
|
#
|
@@ -43,12 +45,12 @@ class TagList < Array
|
|
43
45
|
def to_s
|
44
46
|
tags = frozen? ? self.dup : self
|
45
47
|
tags.send(:clean!)
|
46
|
-
|
48
|
+
|
47
49
|
tags.map do |name|
|
48
50
|
name.include?(delimiter) ? "\"#{name}\"" : name
|
49
51
|
end.join(delimiter.ends_with?(" ") ? delimiter : "#{delimiter} ")
|
50
52
|
end
|
51
|
-
|
53
|
+
|
52
54
|
private
|
53
55
|
# Remove whitespace, duplicates, and blanks.
|
54
56
|
def clean!
|
@@ -56,35 +58,38 @@ class TagList < Array
|
|
56
58
|
map!(&:strip)
|
57
59
|
uniq!
|
58
60
|
end
|
59
|
-
|
61
|
+
|
60
62
|
def extract_and_apply_options!(args)
|
61
63
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
62
64
|
options.assert_valid_keys :parse
|
63
|
-
|
65
|
+
|
64
66
|
if options[:parse]
|
65
67
|
args.map! { |a| self.class.from(a) }
|
66
68
|
end
|
67
|
-
|
69
|
+
|
68
70
|
args.flatten!
|
69
71
|
end
|
70
|
-
|
72
|
+
|
71
73
|
class << self
|
74
|
+
|
72
75
|
# Returns a new TagList using the given tag string.
|
73
|
-
#
|
76
|
+
#
|
74
77
|
# tag_list = TagList.from("One , Two, Three")
|
75
78
|
# tag_list # ["One", "Two", "Three"]
|
76
79
|
def from(string)
|
77
80
|
string = string.join(", ") if string.respond_to?(:join)
|
78
81
|
|
79
|
-
|
82
|
+
new.tap do |tag_list|
|
80
83
|
string = string.to_s.dup
|
81
|
-
|
84
|
+
|
82
85
|
# Parse the quoted tags
|
83
86
|
string.gsub!(/(\A|#{delimiter})\s*"(.*?)"\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
|
84
87
|
string.gsub!(/(\A|#{delimiter})\s*'(.*?)'\s*(#{delimiter}\s*|\z)/) { tag_list << $2; $3 }
|
85
|
-
|
88
|
+
|
86
89
|
tag_list.add(string.split(delimiter))
|
87
90
|
end
|
88
91
|
end
|
92
|
+
|
89
93
|
end
|
90
|
-
|
94
|
+
|
95
|
+
end
|
@@ -1,14 +1,22 @@
|
|
1
1
|
class Tagging < ActiveRecord::Base #:nodoc:
|
2
|
-
|
3
|
-
|
4
|
-
:
|
2
|
+
|
3
|
+
attr_accessible :tag,
|
4
|
+
:tag_id,
|
5
|
+
:context,
|
6
|
+
:taggable,
|
7
|
+
:taggable_type,
|
8
|
+
:taggable_id,
|
9
|
+
:tagger,
|
10
|
+
:tagger_type,
|
11
|
+
:tagger_id
|
5
12
|
|
6
13
|
belongs_to :tag
|
7
14
|
belongs_to :taggable, :polymorphic => true
|
8
|
-
belongs_to :tagger,
|
9
|
-
|
15
|
+
belongs_to :tagger, :polymorphic => true
|
16
|
+
|
10
17
|
validates_presence_of :context
|
11
18
|
validates_presence_of :tag_id
|
12
|
-
|
13
|
-
validates_uniqueness_of :tag_id, :scope => [:taggable_type, :taggable_id, :context, :tagger_id, :tagger_type]
|
19
|
+
|
20
|
+
validates_uniqueness_of :tag_id, :scope => [ :taggable_type, :taggable_id, :context, :tagger_id, :tagger_type ]
|
21
|
+
|
14
22
|
end
|
@@ -1,13 +1,17 @@
|
|
1
1
|
module TagsHelper
|
2
|
+
|
2
3
|
# See the README for an example using tag_cloud.
|
3
4
|
def tag_cloud(tags, classes)
|
5
|
+
tags = tags.all if tags.is_a? ActiveRecord::Relation
|
6
|
+
|
4
7
|
return [] if tags.empty?
|
5
8
|
|
6
9
|
max_count = tags.sort_by(&:count).last.count.to_f
|
7
|
-
|
10
|
+
|
8
11
|
tags.each do |tag|
|
9
12
|
index = ((tag.count / max_count) * (classes.size - 1)).round
|
10
13
|
yield tag, classes[index]
|
11
14
|
end
|
12
15
|
end
|
13
|
-
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,31 @@
|
|
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)
|
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
|
@@ -1,27 +1,26 @@
|
|
1
1
|
class ActsAsTaggableOnMigration < ActiveRecord::Migration
|
2
2
|
def self.up
|
3
3
|
create_table :tags do |t|
|
4
|
-
t.
|
4
|
+
t.string :name
|
5
5
|
end
|
6
|
-
|
6
|
+
|
7
7
|
create_table :taggings do |t|
|
8
|
-
t.
|
9
|
-
|
10
|
-
t.column :tagger_id, :integer
|
11
|
-
t.column :tagger_type, :string
|
12
|
-
|
8
|
+
t.references :tag
|
9
|
+
|
13
10
|
# You should make sure that the column created is
|
14
11
|
# long enough to store the required class names.
|
15
|
-
t.
|
16
|
-
t.
|
17
|
-
|
18
|
-
t.
|
12
|
+
t.references :taggable, :polymorphic => true
|
13
|
+
t.references :tagger, :polymorphic => true
|
14
|
+
|
15
|
+
t.string :context
|
16
|
+
|
17
|
+
t.datetime :created_at
|
19
18
|
end
|
20
|
-
|
19
|
+
|
21
20
|
add_index :taggings, :tag_id
|
22
21
|
add_index :taggings, [:taggable_id, :taggable_type, :context]
|
23
22
|
end
|
24
|
-
|
23
|
+
|
25
24
|
def self.down
|
26
25
|
drop_table :taggings
|
27
26
|
drop_table :tags
|
@@ -2,9 +2,9 @@ require File.dirname(__FILE__) + '/../spec_helper'
|
|
2
2
|
|
3
3
|
describe "Acts As Taggable On" do
|
4
4
|
before(:each) do
|
5
|
-
clean_database!
|
5
|
+
clean_database!
|
6
6
|
end
|
7
|
-
|
7
|
+
|
8
8
|
it "should provide a class method 'taggable?' that is false for untaggable models" do
|
9
9
|
UntaggableModel.should_not be_taggable
|
10
10
|
end
|
@@ -22,7 +22,7 @@ describe "Acts As Taggable On" do
|
|
22
22
|
it "should create a class attribute for tag types" do
|
23
23
|
@taggable.class.should respond_to(:tag_types)
|
24
24
|
end
|
25
|
-
|
25
|
+
|
26
26
|
it "should create an instance attribute for tag types" do
|
27
27
|
@taggable.should respond_to(:tag_types)
|
28
28
|
end
|
@@ -36,7 +36,7 @@ describe "Acts As Taggable On" do
|
|
36
36
|
end
|
37
37
|
|
38
38
|
it "should add tagged_with and tag_counts to singleton" do
|
39
|
-
TaggableModel.should respond_to(:
|
39
|
+
TaggableModel.should respond_to(:tagged_with, :tag_counts)
|
40
40
|
end
|
41
41
|
|
42
42
|
it "should add saving of tag lists and cached tag lists to the instance" do
|
@@ -48,10 +48,6 @@ describe "Acts As Taggable On" do
|
|
48
48
|
@taggable.should respond_to(:tag_list, :skill_list, :language_list)
|
49
49
|
@taggable.should respond_to(:tag_list=, :skill_list=, :language_list=)
|
50
50
|
end
|
51
|
-
|
52
|
-
it "should generate a tag_list accessor for getting all tags for each tag type" do
|
53
|
-
@taggable.should respond_to(:all_tags_list, :all_skills_list, :all_languages_list)
|
54
|
-
end
|
55
51
|
end
|
56
52
|
|
57
53
|
describe "Single Table Inheritance" do
|
@@ -212,4 +208,4 @@ describe "Acts As Taggable On" do
|
|
212
208
|
end
|
213
209
|
end
|
214
210
|
|
215
|
-
end
|
211
|
+
end
|
@@ -15,7 +15,7 @@ describe "Group Helper" do
|
|
15
15
|
end
|
16
16
|
|
17
17
|
it "should return all column names joined for TaggableModel GROUP clause" do
|
18
|
-
@taggable.grouped_column_names_for(TaggableModel).should == "taggable_models.id, taggable_models.name, taggable_models.type
|
18
|
+
@taggable.grouped_column_names_for(TaggableModel).should == "taggable_models.id, taggable_models.name, taggable_models.type"
|
19
19
|
end
|
20
20
|
end
|
21
21
|
end
|