crowdint_acts-as-taggable-on 2.3.2

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 (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