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.
Files changed (43) hide show
  1. data/CHANGELOG +5 -2
  2. data/Gemfile +10 -0
  3. data/README.rdoc +56 -26
  4. data/Rakefile +46 -16
  5. data/VERSION +1 -1
  6. data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +7 -0
  7. data/generators/acts_as_taggable_on_migration/templates/migration.rb +29 -0
  8. data/lib/acts-as-taggable-on.rb +30 -7
  9. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +53 -0
  10. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +132 -0
  11. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +241 -0
  12. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +101 -0
  13. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +65 -0
  14. data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +43 -373
  15. data/lib/acts_as_taggable_on/acts_as_tagger.rb +60 -45
  16. data/lib/acts_as_taggable_on/compatibility/Gemfile +8 -0
  17. data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +17 -0
  18. data/lib/acts_as_taggable_on/compatibility/postgresql.rb +44 -0
  19. data/lib/acts_as_taggable_on/tag.rb +70 -22
  20. data/lib/acts_as_taggable_on/tag_list.rb +81 -80
  21. data/lib/acts_as_taggable_on/tagging.rb +23 -7
  22. data/lib/acts_as_taggable_on/tags_helper.rb +14 -8
  23. data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +32 -0
  24. data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +28 -0
  25. data/rails/init.rb +1 -7
  26. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +102 -55
  27. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +48 -6
  28. data/spec/acts_as_taggable_on/tag_list_spec.rb +21 -3
  29. data/spec/acts_as_taggable_on/tag_spec.rb +77 -24
  30. data/spec/acts_as_taggable_on/taggable_spec.rb +173 -76
  31. data/spec/acts_as_taggable_on/tagger_spec.rb +74 -6
  32. data/spec/acts_as_taggable_on/tagging_spec.rb +22 -7
  33. data/spec/acts_as_taggable_on/tags_helper_spec.rb +4 -6
  34. data/spec/bm.rb +52 -0
  35. data/spec/database.yml +17 -0
  36. data/spec/database.yml.sample +17 -0
  37. data/spec/models.rb +31 -0
  38. data/spec/schema.rb +13 -2
  39. data/spec/spec_helper.rb +52 -40
  40. metadata +31 -9
  41. data/lib/acts_as_taggable_on/group_helper.rb +0 -12
  42. data/spec/acts_as_taggable_on/group_helper_spec.rb +0 -18
  43. data/spec/spec.opts +0 -3
@@ -1,26 +1,74 @@
1
- class Tag < ActiveRecord::Base
2
- has_many :taggings, :dependent => :destroy
1
+ module ActsAsTaggableOn
2
+ class Tag < ::ActiveRecord::Base
3
+ include ActsAsTaggableOn::ActiveRecord::Backports if ::ActiveRecord::VERSION::MAJOR < 3
3
4
 
4
- validates_presence_of :name
5
- validates_uniqueness_of :name
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
- named_scope :named, lambda { |name| { :conditions => ["name = ?", name] } }
8
- named_scope :named_like, lambda { |name| { :conditions => ["name LIKE ?", "%#{name}%"] } }
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
- # LIKE is used for cross-database case-insensitivity
11
- def self.find_or_create_with_like_by_name(name)
12
- find(:first, :conditions => ["name LIKE ?", name]) || create(:name => name)
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
- class TagList < Array
2
- cattr_accessor :delimiter
3
- self.delimiter = ','
4
-
5
- def initialize(*args)
6
- add(*args)
7
- end
8
-
9
- attr_accessor :owner
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
- class << self
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
- string = string.join(", ") if string.respond_to?(:join)
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
- returning new do |tag_list|
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*#{delimiter}?\s*/) { tag_list << $1; "" }
83
- string.gsub!(/'(.*?)'\s*#{delimiter}?\s*/) { tag_list << $1; "" }
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
- def from_owner(owner, *tags)
90
- returning from(*tags) do |taglist|
91
- taglist.owner = owner
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
- class Tagging < ActiveRecord::Base #:nodoc:
2
- belongs_to :tag
3
- belongs_to :taggable, :polymorphic => true
4
- belongs_to :tagger, :polymorphic => true
5
-
6
- validates_presence_of :context
7
- validates_presence_of :tag_id
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 TagsHelper
2
- # See the README for an example using tag_cloud.
3
- def tag_cloud(tags, classes)
4
- max_count = tags.sort_by(&:count).last.count.to_f
5
-
6
- tags.each do |tag|
7
- index = ((tag.count / max_count) * (classes.size - 1)).round
8
- yield tag, classes[index]
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'