crowdint_acts-as-taggable-on 2.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.gitignore +10 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +9 -0
  4. data/CHANGELOG +35 -0
  5. data/Gemfile +3 -0
  6. data/Guardfile +5 -0
  7. data/MIT-LICENSE +20 -0
  8. data/README.rdoc +250 -0
  9. data/Rakefile +13 -0
  10. data/acts-as-taggable-on.gemspec +28 -0
  11. data/lib/acts-as-taggable-on.rb +59 -0
  12. data/lib/acts-as-taggable-on/version.rb +4 -0
  13. data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +53 -0
  14. data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +127 -0
  15. data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +349 -0
  16. data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +37 -0
  17. data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +99 -0
  18. data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +73 -0
  19. data/lib/acts_as_taggable_on/tag.rb +77 -0
  20. data/lib/acts_as_taggable_on/tag_list.rb +97 -0
  21. data/lib/acts_as_taggable_on/taggable.rb +102 -0
  22. data/lib/acts_as_taggable_on/tagger.rb +67 -0
  23. data/lib/acts_as_taggable_on/tagging.rb +34 -0
  24. data/lib/acts_as_taggable_on/tags_helper.rb +17 -0
  25. data/lib/acts_as_taggable_on/utils.rb +34 -0
  26. data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +39 -0
  27. data/lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb +30 -0
  28. data/rails/init.rb +1 -0
  29. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +514 -0
  30. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +114 -0
  31. data/spec/acts_as_taggable_on/tag_list_spec.rb +93 -0
  32. data/spec/acts_as_taggable_on/tag_spec.rb +153 -0
  33. data/spec/acts_as_taggable_on/taggable_spec.rb +543 -0
  34. data/spec/acts_as_taggable_on/tagger_spec.rb +112 -0
  35. data/spec/acts_as_taggable_on/tagging_spec.rb +28 -0
  36. data/spec/acts_as_taggable_on/tags_helper_spec.rb +44 -0
  37. data/spec/acts_as_taggable_on/utils_spec.rb +21 -0
  38. data/spec/bm.rb +52 -0
  39. data/spec/database.yml.sample +19 -0
  40. data/spec/generators/acts_as_taggable_on/migration/migration_generator_spec.rb +22 -0
  41. data/spec/models.rb +49 -0
  42. data/spec/schema.rb +61 -0
  43. data/spec/spec_helper.rb +83 -0
  44. data/uninstall.rb +1 -0
  45. metadata +240 -0
@@ -0,0 +1,37 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Dirty
3
+ def self.included(base)
4
+ base.extend ActsAsTaggableOn::Taggable::Dirty::ClassMethods
5
+
6
+ base.initialize_acts_as_taggable_on_dirty
7
+ end
8
+
9
+ module ClassMethods
10
+ def initialize_acts_as_taggable_on_dirty
11
+ tag_types.map(&:to_s).each do |tags_type|
12
+ tag_type = tags_type.to_s.singularize
13
+ context_tags = tags_type.to_sym
14
+
15
+ class_eval %(
16
+ def #{tag_type}_list_changed?
17
+ changed_attributes.include?("#{tag_type}_list")
18
+ end
19
+
20
+ def #{tag_type}_list_was
21
+ changed_attributes.include?("#{tag_type}_list") ? changed_attributes["#{tag_type}_list"] : __send__("#{tag_type}_list")
22
+ end
23
+
24
+ def #{tag_type}_list_change
25
+ [changed_attributes['#{tag_type}_list'], __send__('#{tag_type}_list')] if changed_attributes.include?("#{tag_type}_list")
26
+ end
27
+
28
+ def #{tag_type}_list_changes
29
+ [changed_attributes['#{tag_type}_list'], __send__('#{tag_type}_list')] if changed_attributes.include?("#{tag_type}_list")
30
+ end
31
+ )
32
+
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,99 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Ownership
3
+ def self.included(base)
4
+ base.send :include, ActsAsTaggableOn::Taggable::Ownership::InstanceMethods
5
+ base.extend ActsAsTaggableOn::Taggable::Ownership::ClassMethods
6
+
7
+ base.class_eval do
8
+ after_save :save_owned_tags
9
+ end
10
+
11
+ base.initialize_acts_as_taggable_on_ownership
12
+ end
13
+
14
+ module ClassMethods
15
+ def acts_as_taggable_on(*args)
16
+ initialize_acts_as_taggable_on_ownership
17
+ super(*args)
18
+ end
19
+
20
+ def initialize_acts_as_taggable_on_ownership
21
+ tag_types.map(&:to_s).each do |tag_type|
22
+ class_eval %(
23
+ def #{tag_type}_from(owner)
24
+ owner_tag_list_on(owner, '#{tag_type}')
25
+ end
26
+ )
27
+ end
28
+ end
29
+ end
30
+
31
+ module InstanceMethods
32
+ def owner_tags_on(owner, context)
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.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
44
+ end
45
+
46
+ def cached_owned_tag_list_on(context)
47
+ variable_name = "@owned_#{context}_list"
48
+ cache = instance_variable_get(variable_name) || instance_variable_set(variable_name, {})
49
+ end
50
+
51
+ def owner_tag_list_on(owner, context)
52
+ add_custom_context(context)
53
+
54
+ cache = cached_owned_tag_list_on(context)
55
+ cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
56
+
57
+ cache[owner] ||= ActsAsTaggableOn::TagList.new(*owner_tags_on(owner, context).map(&:name))
58
+ end
59
+
60
+ def set_owner_tag_list_on(owner, context, new_list)
61
+ add_custom_context(context)
62
+
63
+ cache = cached_owned_tag_list_on(context)
64
+ cache.delete_if { |key, value| key.id == owner.id && key.class == owner.class }
65
+
66
+ cache[owner] = ActsAsTaggableOn::TagList.from(new_list)
67
+ end
68
+
69
+ def reload(*args)
70
+ self.class.tag_types.each do |context|
71
+ instance_variable_set("@owned_#{context}_list", nil)
72
+ end
73
+
74
+ super(*args)
75
+ end
76
+
77
+ def save_owned_tags
78
+ tagging_contexts.each do |context|
79
+ cached_owned_tag_list_on(context).each do |owner, tag_list|
80
+
81
+ # Find existing tags or create non-existing tags:
82
+ tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
83
+
84
+ # Tag objects for owned tags
85
+ owned_tags = owner_tags_on(owner, context)
86
+ new_tags = tags - owned_tags
87
+
88
+ # Create new taggings:
89
+ new_tags.each do |tag|
90
+ taggings.create!(:tag_id => tag.id, :context => context.to_s, :tagger => owner, :taggable => self)
91
+ end
92
+ end
93
+ end
94
+
95
+ true
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,73 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Related
3
+ def self.included(base)
4
+ base.send :include, ActsAsTaggableOn::Taggable::Related::InstanceMethods
5
+ base.extend ActsAsTaggableOn::Taggable::Related::ClassMethods
6
+ base.initialize_acts_as_taggable_on_related
7
+ end
8
+
9
+ module ClassMethods
10
+ def initialize_acts_as_taggable_on_related
11
+ tag_types.map(&:to_s).each do |tag_type|
12
+ class_eval %(
13
+ def find_related_#{tag_type}(options = {})
14
+ related_tags_for('#{tag_type}', self.class, options)
15
+ end
16
+ alias_method :find_related_on_#{tag_type}, :find_related_#{tag_type}
17
+
18
+ def find_related_#{tag_type}_for(klass, options = {})
19
+ related_tags_for('#{tag_type}', klass, options)
20
+ end
21
+ )
22
+ end
23
+
24
+ unless tag_types.empty?
25
+ class_eval %(
26
+ def find_matching_contexts(search_context, result_context, options = {})
27
+ matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
28
+ end
29
+
30
+ def find_matching_contexts_for(klass, search_context, result_context, options = {})
31
+ matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
32
+ end
33
+ )
34
+ end
35
+ end
36
+
37
+ def acts_as_taggable_on(*args)
38
+ super(*args)
39
+ initialize_acts_as_taggable_on_related
40
+ end
41
+ end
42
+
43
+ module InstanceMethods
44
+ def matching_contexts_for(search_context, result_context, klass, options = {})
45
+ tags_to_find = tags_on(search_context).collect { |t| t.name }
46
+
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",
52
+ :from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
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,
55
+ :order => "count DESC" }.update(options))
56
+ end
57
+
58
+ def related_tags_for(context, klass, options = {})
59
+ tags_to_find = tags_on(context).collect { |t| t.name }
60
+
61
+ exclude_self = "#{klass.table_name}.#{klass.primary_key} != #{id} AND" if [self.class.base_class, self.class].include? klass
62
+
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}.#{ActsAsTaggableOn::Tag.primary_key}) AS count",
66
+ :from => "#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}",
67
+ :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],
68
+ :group => group_columns,
69
+ :order => "count DESC" }.update(options))
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,77 @@
1
+ module ActsAsTaggableOn
2
+ class Tag < ::ActiveRecord::Base
3
+ include ActsAsTaggableOn::Utils
4
+
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
+ validates_length_of :name, :maximum => 255
16
+
17
+ ### SCOPES:
18
+
19
+ def self.named(name)
20
+ where(["lower(name) = ?", name.downcase])
21
+ end
22
+
23
+ def self.named_any(list)
24
+ where(list.map { |tag| sanitize_sql(["lower(name) = ?", tag.to_s.downcase]) }.join(" OR "))
25
+ end
26
+
27
+ def self.named_like(name)
28
+ where(["name #{like_operator} ? ESCAPE '!'", "%#{escape_like(name)}%"])
29
+ end
30
+
31
+ def self.named_like_any(list)
32
+ where(list.map { |tag| sanitize_sql(["name #{like_operator} ? ESCAPE '!'", "%#{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
+ class << self
71
+ private
72
+ def comparable_name(str)
73
+ str.mb_chars.downcase.to_s
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,97 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
3
+ module ActsAsTaggableOn
4
+ class TagList < Array
5
+ attr_accessor :owner
6
+
7
+ def initialize(*args)
8
+ add(*args)
9
+ end
10
+
11
+ ##
12
+ # Returns a new TagList using the given tag string.
13
+ #
14
+ # Example:
15
+ # tag_list = TagList.from("One , Two, Three")
16
+ # tag_list # ["One", "Two", "Three"]
17
+ def self.from(string)
18
+ string = string.join(ActsAsTaggableOn.glue) if string.respond_to?(:join)
19
+
20
+ new.tap do |tag_list|
21
+ string = string.to_s.dup
22
+
23
+ # Parse the quoted tags
24
+ string.gsub!(/(\A|#{ActsAsTaggableOn.delimiter})\s*"(.*?)"\s*(#{ActsAsTaggableOn.delimiter}\s*|\z)/) { tag_list << $2; $3 }
25
+ string.gsub!(/(\A|#{ActsAsTaggableOn.delimiter})\s*'(.*?)'\s*(#{ActsAsTaggableOn.delimiter}\s*|\z)/) { tag_list << $2; $3 }
26
+
27
+ tag_list.add(string.split(ActsAsTaggableOn.delimiter))
28
+ end
29
+ end
30
+
31
+ ##
32
+ # Add tags to the tag_list. Duplicate or blank tags will be ignored.
33
+ # Use the <tt>:parse</tt> option to add an unparsed tag string.
34
+ #
35
+ # Example:
36
+ # tag_list.add("Fun", "Happy")
37
+ # tag_list.add("Fun, Happy", :parse => true)
38
+ def add(*names)
39
+ extract_and_apply_options!(names)
40
+ concat(names)
41
+ clean!
42
+ self
43
+ end
44
+
45
+ ##
46
+ # Remove specific tags from the tag_list.
47
+ # Use the <tt>:parse</tt> option to add an unparsed tag string.
48
+ #
49
+ # Example:
50
+ # tag_list.remove("Sad", "Lonely")
51
+ # tag_list.remove("Sad, Lonely", :parse => true)
52
+ def remove(*names)
53
+ extract_and_apply_options!(names)
54
+ delete_if { |name| names.include?(name) }
55
+ self
56
+ end
57
+
58
+ ##
59
+ # Transform the tag_list into a tag string suitable for edting in a form.
60
+ # The tags are joined with <tt>TagList.delimiter</tt> and quoted if necessary.
61
+ #
62
+ # Example:
63
+ # tag_list = TagList.new("Round", "Square,Cube")
64
+ # tag_list.to_s # 'Round, "Square,Cube"'
65
+ def to_s
66
+ tags = frozen? ? self.dup : self
67
+ tags.send(:clean!)
68
+
69
+ tags.map do |name|
70
+ name.include?(ActsAsTaggableOn.delimiter) ? "\"#{name}\"" : name
71
+ end.join(ActsAsTaggableOn.glue)
72
+ end
73
+
74
+ private
75
+
76
+ # Remove whitespace, duplicates, and blanks.
77
+ def clean!
78
+ reject!(&:blank?)
79
+ map!(&:strip)
80
+ map!(&:downcase) if ActsAsTaggableOn.force_lowercase
81
+ map!(&:parameterize) if ActsAsTaggableOn.force_parameterize
82
+
83
+ uniq!
84
+ end
85
+
86
+ def extract_and_apply_options!(args)
87
+ options = args.last.is_a?(Hash) ? args.pop : {}
88
+ options.assert_valid_keys :parse
89
+
90
+ if options[:parse]
91
+ args.map! { |a| self.class.from(a) }
92
+ end
93
+
94
+ args.flatten!
95
+ end
96
+ end
97
+ 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