sb-acts-as-taggable-on 6.5.0

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 (87) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +39 -0
  5. data/Appraisals +15 -0
  6. data/CHANGELOG.md +330 -0
  7. data/CONTRIBUTING.md +57 -0
  8. data/Gemfile +11 -0
  9. data/Guardfile +5 -0
  10. data/LICENSE.md +20 -0
  11. data/README.md +555 -0
  12. data/Rakefile +21 -0
  13. data/UPGRADING.md +8 -0
  14. data/acts-as-taggable-on.gemspec +32 -0
  15. data/db/migrate/1_acts_as_taggable_on_migration.rb +36 -0
  16. data/db/migrate/2_add_missing_unique_indices.rb +25 -0
  17. data/db/migrate/3_add_taggings_counter_cache_to_tags.rb +19 -0
  18. data/db/migrate/4_add_missing_taggable_index.rb +14 -0
  19. data/db/migrate/5_change_collation_for_tag_names.rb +14 -0
  20. data/db/migrate/6_add_missing_indexes_on_taggings.rb +22 -0
  21. data/gemfiles/activerecord_5.0.gemfile +21 -0
  22. data/gemfiles/activerecord_5.1.gemfile +21 -0
  23. data/gemfiles/activerecord_5.2.gemfile +21 -0
  24. data/gemfiles/activerecord_6.0.gemfile +21 -0
  25. data/lib/acts-as-taggable-on.rb +133 -0
  26. data/lib/acts_as_taggable_on.rb +6 -0
  27. data/lib/acts_as_taggable_on/default_parser.rb +79 -0
  28. data/lib/acts_as_taggable_on/engine.rb +4 -0
  29. data/lib/acts_as_taggable_on/generic_parser.rb +19 -0
  30. data/lib/acts_as_taggable_on/tag.rb +139 -0
  31. data/lib/acts_as_taggable_on/tag_list.rb +106 -0
  32. data/lib/acts_as_taggable_on/taggable.rb +101 -0
  33. data/lib/acts_as_taggable_on/taggable/cache.rb +90 -0
  34. data/lib/acts_as_taggable_on/taggable/collection.rb +183 -0
  35. data/lib/acts_as_taggable_on/taggable/core.rb +322 -0
  36. data/lib/acts_as_taggable_on/taggable/ownership.rb +136 -0
  37. data/lib/acts_as_taggable_on/taggable/related.rb +71 -0
  38. data/lib/acts_as_taggable_on/taggable/tag_list_type.rb +4 -0
  39. data/lib/acts_as_taggable_on/taggable/tagged_with_query.rb +16 -0
  40. data/lib/acts_as_taggable_on/taggable/tagged_with_query/all_tags_query.rb +111 -0
  41. data/lib/acts_as_taggable_on/taggable/tagged_with_query/any_tags_query.rb +70 -0
  42. data/lib/acts_as_taggable_on/taggable/tagged_with_query/exclude_tags_query.rb +82 -0
  43. data/lib/acts_as_taggable_on/taggable/tagged_with_query/query_base.rb +61 -0
  44. data/lib/acts_as_taggable_on/tagger.rb +89 -0
  45. data/lib/acts_as_taggable_on/tagging.rb +36 -0
  46. data/lib/acts_as_taggable_on/tags_helper.rb +15 -0
  47. data/lib/acts_as_taggable_on/utils.rb +37 -0
  48. data/lib/acts_as_taggable_on/version.rb +3 -0
  49. data/lib/tasks/tags_collate_utf8.rake +21 -0
  50. data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +285 -0
  51. data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +112 -0
  52. data/spec/acts_as_taggable_on/caching_spec.rb +129 -0
  53. data/spec/acts_as_taggable_on/default_parser_spec.rb +47 -0
  54. data/spec/acts_as_taggable_on/dirty_spec.rb +142 -0
  55. data/spec/acts_as_taggable_on/generic_parser_spec.rb +14 -0
  56. data/spec/acts_as_taggable_on/related_spec.rb +99 -0
  57. data/spec/acts_as_taggable_on/single_table_inheritance_spec.rb +231 -0
  58. data/spec/acts_as_taggable_on/tag_list_spec.rb +176 -0
  59. data/spec/acts_as_taggable_on/tag_spec.rb +340 -0
  60. data/spec/acts_as_taggable_on/taggable_spec.rb +817 -0
  61. data/spec/acts_as_taggable_on/tagger_spec.rb +153 -0
  62. data/spec/acts_as_taggable_on/tagging_spec.rb +117 -0
  63. data/spec/acts_as_taggable_on/tags_helper_spec.rb +45 -0
  64. data/spec/acts_as_taggable_on/utils_spec.rb +23 -0
  65. data/spec/internal/app/models/altered_inheriting_taggable_model.rb +5 -0
  66. data/spec/internal/app/models/cached_model.rb +3 -0
  67. data/spec/internal/app/models/cached_model_with_array.rb +11 -0
  68. data/spec/internal/app/models/columns_override_model.rb +5 -0
  69. data/spec/internal/app/models/company.rb +15 -0
  70. data/spec/internal/app/models/inheriting_taggable_model.rb +4 -0
  71. data/spec/internal/app/models/market.rb +2 -0
  72. data/spec/internal/app/models/non_standard_id_taggable_model.rb +8 -0
  73. data/spec/internal/app/models/ordered_taggable_model.rb +4 -0
  74. data/spec/internal/app/models/other_cached_model.rb +3 -0
  75. data/spec/internal/app/models/other_taggable_model.rb +4 -0
  76. data/spec/internal/app/models/student.rb +4 -0
  77. data/spec/internal/app/models/taggable_model.rb +14 -0
  78. data/spec/internal/app/models/untaggable_model.rb +3 -0
  79. data/spec/internal/app/models/user.rb +3 -0
  80. data/spec/internal/config/database.yml.sample +19 -0
  81. data/spec/internal/db/schema.rb +110 -0
  82. data/spec/spec_helper.rb +20 -0
  83. data/spec/support/0-helpers.rb +32 -0
  84. data/spec/support/array.rb +9 -0
  85. data/spec/support/database.rb +36 -0
  86. data/spec/support/database_cleaner.rb +21 -0
  87. metadata +269 -0
@@ -0,0 +1,4 @@
1
+ module ActsAsTaggableOn
2
+ class Engine < Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,19 @@
1
+ module ActsAsTaggableOn
2
+ ##
3
+ # Returns a new TagList using the given tag string.
4
+ #
5
+ # Example:
6
+ # tag_list = ActsAsTaggableOn::GenericParser.new.parse("One , Two, Three")
7
+ # tag_list # ["One", "Two", "Three"]
8
+ class GenericParser
9
+ def initialize(tag_list)
10
+ @tag_list = tag_list
11
+ end
12
+
13
+ def parse
14
+ TagList.new.tap do |tag_list|
15
+ tag_list.add @tag_list.split(',').map(&:strip).reject(&:empty?)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,139 @@
1
+ # encoding: utf-8
2
+ module ActsAsTaggableOn
3
+ class Tag < ::ActiveRecord::Base
4
+ self.table_name = ActsAsTaggableOn.tags_table
5
+
6
+ ### ASSOCIATIONS:
7
+
8
+ has_many :taggings, dependent: :destroy, class_name: '::ActsAsTaggableOn::Tagging'
9
+
10
+ ### VALIDATIONS:
11
+
12
+ validates_presence_of :name
13
+ validates_uniqueness_of :name, if: :validates_name_uniqueness?, case_sensitive: true
14
+ validates_length_of :name, maximum: 255
15
+
16
+ # monkey patch this method if don't need name uniqueness validation
17
+ def validates_name_uniqueness?
18
+ true
19
+ end
20
+
21
+ ### SCOPES:
22
+ scope :most_used, ->(limit = 20) { order('taggings_count desc').limit(limit) }
23
+ scope :least_used, ->(limit = 20) { order('taggings_count asc').limit(limit) }
24
+
25
+ def self.named(name)
26
+ if ActsAsTaggableOn.strict_case_match
27
+ where(["name = #{binary}?", as_8bit_ascii(name)])
28
+ else
29
+ where(['LOWER(name) = LOWER(?)', as_8bit_ascii(unicode_downcase(name))])
30
+ end
31
+ end
32
+
33
+ def self.named_any(list)
34
+ clause = list.map { |tag|
35
+ sanitize_sql_for_named_any(tag).force_encoding('BINARY')
36
+ }.join(' OR ')
37
+ where(clause)
38
+ end
39
+
40
+ def self.named_like(name)
41
+ clause = ["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'", "%#{ActsAsTaggableOn::Utils.escape_like(name)}%"]
42
+ where(clause)
43
+ end
44
+
45
+ def self.named_like_any(list)
46
+ clause = list.map { |tag|
47
+ sanitize_sql(["name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'", "%#{ActsAsTaggableOn::Utils.escape_like(tag.to_s)}%"])
48
+ }.join(' OR ')
49
+ where(clause)
50
+ end
51
+
52
+ def self.for_context(context)
53
+ joins(:taggings).
54
+ where(["#{ActsAsTaggableOn.taggings_table}.context = ?", context]).
55
+ select("DISTINCT #{ActsAsTaggableOn.tags_table}.*")
56
+ end
57
+
58
+ ### CLASS METHODS:
59
+
60
+ def self.find_or_create_with_like_by_name(name)
61
+ if ActsAsTaggableOn.strict_case_match
62
+ self.find_or_create_all_with_like_by_name([name]).first
63
+ else
64
+ named_like(name).first || create(name: name)
65
+ end
66
+ end
67
+
68
+ def self.find_or_create_all_with_like_by_name(*list)
69
+ list = Array(list).flatten
70
+
71
+ return [] if list.empty?
72
+
73
+ existing_tags = named_any(list)
74
+ list.map do |tag_name|
75
+ begin
76
+ tries ||= 3
77
+ comparable_tag_name = comparable_name(tag_name)
78
+ existing_tag = existing_tags.find { |tag| comparable_name(tag.name) == comparable_tag_name }
79
+ existing_tag || create(name: tag_name)
80
+ rescue ActiveRecord::RecordNotUnique
81
+ if (tries -= 1).positive?
82
+ ActiveRecord::Base.connection.execute 'ROLLBACK'
83
+ existing_tags = named_any(list)
84
+ retry
85
+ end
86
+
87
+ raise DuplicateTagError.new("'#{tag_name}' has already been taken")
88
+ end
89
+ end
90
+ end
91
+
92
+ ### INSTANCE METHODS:
93
+
94
+ def ==(object)
95
+ super || (object.is_a?(Tag) && name == object.name)
96
+ end
97
+
98
+ def to_s
99
+ name
100
+ end
101
+
102
+ def count
103
+ read_attribute(:count).to_i
104
+ end
105
+
106
+ class << self
107
+
108
+ private
109
+
110
+ def comparable_name(str)
111
+ if ActsAsTaggableOn.strict_case_match
112
+ str
113
+ else
114
+ unicode_downcase(str.to_s)
115
+ end
116
+ end
117
+
118
+ def binary
119
+ ActsAsTaggableOn::Utils.using_mysql? ? 'BINARY ' : nil
120
+ end
121
+
122
+ def as_8bit_ascii(string)
123
+ string.to_s.mb_chars
124
+ end
125
+
126
+ def unicode_downcase(string)
127
+ as_8bit_ascii(string).downcase
128
+ end
129
+
130
+ def sanitize_sql_for_named_any(tag)
131
+ if ActsAsTaggableOn.strict_case_match
132
+ sanitize_sql(["name = #{binary}?", as_8bit_ascii(tag)])
133
+ else
134
+ sanitize_sql(['LOWER(name) = LOWER(?)', as_8bit_ascii(unicode_downcase(tag))])
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,106 @@
1
+
2
+ require 'active_support/core_ext/module/delegation'
3
+
4
+ module ActsAsTaggableOn
5
+ class TagList < Array
6
+ attr_accessor :owner
7
+ attr_accessor :parser
8
+
9
+ def initialize(*args)
10
+ @parser = ActsAsTaggableOn.default_parser
11
+ add(*args)
12
+ end
13
+
14
+ ##
15
+ # Add tags to the tag_list. Duplicate or blank tags will be ignored.
16
+ # Use the <tt>:parse</tt> option to add an unparsed tag string.
17
+ #
18
+ # Example:
19
+ # tag_list.add("Fun", "Happy")
20
+ # tag_list.add("Fun, Happy", :parse => true)
21
+ def add(*names)
22
+ extract_and_apply_options!(names)
23
+ concat(names)
24
+ clean!
25
+ self
26
+ end
27
+
28
+ # Append---Add the tag to the tag_list. This
29
+ # expression returns the tag_list itself, so several appends
30
+ # may be chained together.
31
+ def <<(obj)
32
+ add(obj)
33
+ end
34
+
35
+ # Concatenation --- Returns a new tag list built by concatenating the
36
+ # two tag lists together to produce a third tag list.
37
+ def +(other_tag_list)
38
+ TagList.new.add(self).add(other_tag_list)
39
+ end
40
+
41
+ # Appends the elements of +other_tag_list+ to +self+.
42
+ def concat(other_tag_list)
43
+ super(other_tag_list).send(: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 editing 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
+ d = ActsAsTaggableOn.delimiter
73
+ d = Regexp.new d.join('|') if d.kind_of? Array
74
+ name.index(d) ? "\"#{name}\"" : name
75
+ end.join(ActsAsTaggableOn.glue)
76
+ end
77
+
78
+ private
79
+
80
+ # Convert everything to string, remove whitespace, duplicates, and blanks.
81
+ def clean!
82
+ reject!(&:blank?)
83
+ map!(&:to_s)
84
+ map!(&:strip)
85
+ map! { |tag| tag.mb_chars.downcase.to_s } if ActsAsTaggableOn.force_lowercase
86
+ map!(&:parameterize) if ActsAsTaggableOn.force_parameterize
87
+
88
+ ActsAsTaggableOn.strict_case_match ? uniq! : uniq!{ |tag| tag.downcase }
89
+ self
90
+ end
91
+
92
+
93
+ def extract_and_apply_options!(args)
94
+ options = args.last.is_a?(Hash) ? args.pop : {}
95
+ options.assert_valid_keys :parse, :parser
96
+
97
+ parser = options[:parser] ? options[:parser] : @parser
98
+
99
+ args.map! { |a| parser.new(a).parse } if options[:parse] || options[:parser]
100
+
101
+ args.flatten!
102
+ end
103
+
104
+ end
105
+ end
106
+
@@ -0,0 +1,101 @@
1
+ module ActsAsTaggableOn
2
+ module Taggable
3
+
4
+ def taggable?
5
+ false
6
+ end
7
+
8
+ ##
9
+ # This is an alias for calling <tt>acts_as_taggable_on :tags</tt>.
10
+ #
11
+ # Example:
12
+ # class Book < ActiveRecord::Base
13
+ # acts_as_taggable
14
+ # end
15
+ def acts_as_taggable
16
+ acts_as_taggable_on :tags
17
+ end
18
+
19
+ ##
20
+ # This is an alias for calling <tt>acts_as_ordered_taggable_on :tags</tt>.
21
+ #
22
+ # Example:
23
+ # class Book < ActiveRecord::Base
24
+ # acts_as_ordered_taggable
25
+ # end
26
+ def acts_as_ordered_taggable
27
+ acts_as_ordered_taggable_on :tags
28
+ end
29
+
30
+ ##
31
+ # Make a model taggable on specified contexts.
32
+ #
33
+ # @param [Array] tag_types An array of taggable contexts
34
+ #
35
+ # Example:
36
+ # class User < ActiveRecord::Base
37
+ # acts_as_taggable_on :languages, :skills
38
+ # end
39
+ def acts_as_taggable_on(*tag_types)
40
+ taggable_on(false, tag_types)
41
+ end
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
+ # Separate 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 argument 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, 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
+ end
90
+ end
91
+
92
+ # each of these add context-specific methods and must be
93
+ # called on each call of taggable_on
94
+ include Core
95
+ include Collection
96
+ include Cache
97
+ include Ownership
98
+ include Related
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,90 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Cache
3
+ def self.included(base)
4
+ # When included, conditionally adds tag caching methods when the model
5
+ # has any "cached_#{tag_type}_list" column
6
+ base.extend Columns
7
+ end
8
+
9
+ module Columns
10
+ # ActiveRecord::Base.columns makes a database connection and caches the
11
+ # calculated columns hash for the record as @columns. Since we don't
12
+ # want to add caching methods until we confirm the presence of a
13
+ # caching column, and we don't want to force opening a database
14
+ # connection when the class is loaded, here we intercept and cache
15
+ # the call to :columns as @acts_as_taggable_on_cache_columns
16
+ # to mimic the underlying behavior. While processing this first
17
+ # call to columns, we do the caching column check and dynamically add
18
+ # the class and instance methods
19
+ # FIXME: this method cannot compile in rubinius
20
+ def columns
21
+ @acts_as_taggable_on_cache_columns ||= begin
22
+ db_columns = super
23
+ _add_tags_caching_methods if _has_tags_cache_columns?(db_columns)
24
+ db_columns
25
+ end
26
+ end
27
+
28
+ def reset_column_information
29
+ super
30
+ @acts_as_taggable_on_cache_columns = nil
31
+ end
32
+
33
+ private
34
+
35
+ # @private
36
+ def _has_tags_cache_columns?(db_columns)
37
+ db_column_names = db_columns.map(&:name)
38
+ tag_types.any? do |context|
39
+ db_column_names.include?("cached_#{context.to_s.singularize}_list")
40
+ end
41
+ end
42
+
43
+ # @private
44
+ def _add_tags_caching_methods
45
+ send :include, ActsAsTaggableOn::Taggable::Cache::InstanceMethods
46
+ extend ActsAsTaggableOn::Taggable::Cache::ClassMethods
47
+
48
+ before_save :save_cached_tag_list
49
+
50
+ initialize_tags_cache
51
+ end
52
+ end
53
+
54
+ module ClassMethods
55
+ def initialize_tags_cache
56
+ tag_types.map(&:to_s).each do |tag_type|
57
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
58
+ def self.caching_#{tag_type.singularize}_list?
59
+ caching_tag_list_on?("#{tag_type}")
60
+ end
61
+ RUBY
62
+ end
63
+ end
64
+
65
+ def acts_as_taggable_on(*args)
66
+ super(*args)
67
+ initialize_tags_cache
68
+ end
69
+
70
+ def caching_tag_list_on?(context)
71
+ column_names.include?("cached_#{context.to_s.singularize}_list")
72
+ end
73
+ end
74
+
75
+ module InstanceMethods
76
+ def save_cached_tag_list
77
+ tag_types.map(&:to_s).each do |tag_type|
78
+ if self.class.send("caching_#{tag_type.singularize}_list?")
79
+ if tag_list_cache_set_on(tag_type)
80
+ list = tag_list_cache_on(tag_type).to_a.flatten.compact.join("#{ActsAsTaggableOn.delimiter} ")
81
+ self["cached_#{tag_type.singularize}_list"] = list
82
+ end
83
+ end
84
+ end
85
+
86
+ true
87
+ end
88
+ end
89
+ end
90
+ end