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,183 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Collection
3
+ def self.included(base)
4
+ base.extend ActsAsTaggableOn::Taggable::Collection::ClassMethods
5
+ base.initialize_acts_as_taggable_on_collection
6
+ end
7
+
8
+ module ClassMethods
9
+ def initialize_acts_as_taggable_on_collection
10
+ tag_types.map(&:to_s).each do |tag_type|
11
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
12
+ def self.#{tag_type.singularize}_counts(options={})
13
+ tag_counts_on('#{tag_type}', options)
14
+ end
15
+
16
+ def #{tag_type.singularize}_counts(options = {})
17
+ tag_counts_on('#{tag_type}', options)
18
+ end
19
+
20
+ def top_#{tag_type}(limit = 10)
21
+ tag_counts_on('#{tag_type}', order: 'count desc', limit: limit.to_i)
22
+ end
23
+
24
+ def self.top_#{tag_type}(limit = 10)
25
+ tag_counts_on('#{tag_type}', order: 'count desc', limit: limit.to_i)
26
+ end
27
+ RUBY
28
+ end
29
+ end
30
+
31
+ def acts_as_taggable_on(*args)
32
+ super(*args)
33
+ initialize_acts_as_taggable_on_collection
34
+ end
35
+
36
+ def tag_counts_on(context, options = {})
37
+ all_tag_counts(options.merge({on: context.to_s}))
38
+ end
39
+
40
+ def tags_on(context, options = {})
41
+ all_tags(options.merge({on: context.to_s}))
42
+ end
43
+
44
+ ##
45
+ # Calculate the tag names.
46
+ # To be used when you don't need tag counts and want to avoid the taggable joins.
47
+ #
48
+ # @param [Hash] options Options:
49
+ # * :start_at - Restrict the tags to those created after a certain time
50
+ # * :end_at - Restrict the tags to those created before a certain time
51
+ # * :conditions - A piece of SQL conditions to add to the query. Note we don't join the taggable objects for performance reasons.
52
+ # * :limit - The maximum number of tags to return
53
+ # * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
54
+ # * :on - Scope the find to only include a certain context
55
+ def all_tags(options = {})
56
+ options = options.dup
57
+ options.assert_valid_keys :start_at, :end_at, :conditions, :order, :limit, :on
58
+
59
+ ## Generate conditions:
60
+ options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
61
+
62
+ ## Generate scope:
63
+ tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id")
64
+ tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*").order(options[:order]).limit(options[:limit])
65
+
66
+ # Joins and conditions
67
+ tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) }
68
+ tag_scope = tag_scope.where(options[:conditions])
69
+
70
+ group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
71
+
72
+ # Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
73
+ tagging_scope = generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key).group(group_columns)
74
+
75
+ tag_scope_joins(tag_scope, tagging_scope)
76
+ end
77
+
78
+ ##
79
+ # Calculate the tag counts for all tags.
80
+ #
81
+ # @param [Hash] options Options:
82
+ # * :start_at - Restrict the tags to those created after a certain time
83
+ # * :end_at - Restrict the tags to those created before a certain time
84
+ # * :conditions - A piece of SQL conditions to add to the query
85
+ # * :limit - The maximum number of tags to return
86
+ # * :order - A piece of SQL to order by. Eg 'tags.count desc' or 'taggings.created_at desc'
87
+ # * :at_least - Exclude tags with a frequency less than the given value
88
+ # * :at_most - Exclude tags with a frequency greater than the given value
89
+ # * :on - Scope the find to only include a certain context
90
+ def all_tag_counts(options = {})
91
+ options = options.dup
92
+ options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
93
+
94
+ ## Generate conditions:
95
+ options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
96
+
97
+ ## Generate scope:
98
+ tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id, COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) AS tags_count")
99
+ tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*, #{ActsAsTaggableOn::Tagging.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit])
100
+
101
+ # Current model is STI descendant, so add type checking to the join condition
102
+ unless descends_from_active_record?
103
+ taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id"
104
+ taggable_join << " AND #{table_name}.#{inheritance_column} = '#{name}'"
105
+ tagging_scope = tagging_scope.joins(taggable_join)
106
+ end
107
+
108
+ # Conditions
109
+ tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) }
110
+ tag_scope = tag_scope.where(options[:conditions])
111
+
112
+ # GROUP BY and HAVING clauses:
113
+ having = ["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) > 0"]
114
+ having.push sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) >= ?", options.delete(:at_least)]) if options[:at_least]
115
+ having.push sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) <= ?", options.delete(:at_most)]) if options[:at_most]
116
+ having = having.compact.join(' AND ')
117
+
118
+ group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
119
+
120
+ unless options[:id]
121
+ # Append the current scope to the scope, because we can't use scope(:find) in RoR 3.0 anymore:
122
+ tagging_scope = generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key)
123
+ end
124
+
125
+ tagging_scope = tagging_scope.group(group_columns).having(having)
126
+
127
+ tag_scope_joins(tag_scope, tagging_scope)
128
+ end
129
+
130
+ def safe_to_sql(relation)
131
+ connection.respond_to?(:unprepared_statement) ? connection.unprepared_statement { relation.to_sql } : relation.to_sql
132
+ end
133
+
134
+ private
135
+
136
+ def generate_tagging_scope_in_clause(tagging_scope, table_name, primary_key)
137
+ table_name_pkey = "#{table_name}.#{primary_key}"
138
+ if ActsAsTaggableOn::Utils.using_mysql?
139
+ # See https://github.com/mbleigh/acts-as-taggable-on/pull/457 for details
140
+ scoped_ids = pluck(table_name_pkey)
141
+ tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN (?)", scoped_ids)
142
+ else
143
+ tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{safe_to_sql(except(:select).select(table_name_pkey))})")
144
+ end
145
+
146
+ tagging_scope
147
+ end
148
+
149
+ def tagging_conditions(options)
150
+ tagging_conditions = []
151
+ tagging_conditions.push sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
152
+ tagging_conditions.push sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
153
+
154
+ taggable_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.taggable_type = ?", base_class.name])
155
+ taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", options.delete(:on).to_s]) if options[:on]
156
+ taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = ?", options[:id]]) if options[:id]
157
+
158
+ tagging_conditions.push taggable_conditions
159
+
160
+ tagging_conditions
161
+ end
162
+
163
+ def tag_scope_joins(tag_scope, tagging_scope)
164
+ tag_scope = tag_scope.joins("JOIN (#{safe_to_sql(tagging_scope)}) AS #{ActsAsTaggableOn::Tagging.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id")
165
+ tag_scope.extending(CalculationMethods)
166
+ end
167
+ end
168
+
169
+ def tag_counts_on(context, options={})
170
+ self.class.tag_counts_on(context, options.merge(id: id))
171
+ end
172
+
173
+ module CalculationMethods
174
+ # Rails 5 TODO: Remove options argument as soon we remove support to
175
+ # activerecord-deprecated_finders.
176
+ # See https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/calculations.rb#L38
177
+ def count(column_name = :all, options = {})
178
+ # https://github.com/rails/rails/commit/da9b5d4a8435b744fcf278fffd6d7f1e36d4a4f2
179
+ super(column_name)
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,322 @@
1
+ require_relative 'tagged_with_query'
2
+ require_relative 'tag_list_type'
3
+
4
+ module ActsAsTaggableOn::Taggable
5
+ module Core
6
+
7
+ def self.included(base)
8
+ base.extend ActsAsTaggableOn::Taggable::Core::ClassMethods
9
+
10
+ base.class_eval do
11
+ attr_writer :custom_contexts
12
+ after_save :save_tags
13
+ end
14
+
15
+ base.initialize_acts_as_taggable_on_core
16
+ end
17
+
18
+ module ClassMethods
19
+ def initialize_acts_as_taggable_on_core
20
+ include taggable_mixin
21
+ tag_types.map(&:to_s).each do |tags_type|
22
+ tag_type = tags_type.to_s.singularize
23
+ context_taggings = "#{tag_type}_taggings".to_sym
24
+ context_tags = tags_type.to_sym
25
+ taggings_order = (preserve_tag_order? ? "#{ActsAsTaggableOn::Tagging.table_name}.id" : [])
26
+
27
+ class_eval do
28
+ # when preserving tag order, include order option so that for a 'tags' context
29
+ # the associations tag_taggings & tags are always returned in created order
30
+ has_many context_taggings, -> { includes(:tag).order(taggings_order).where(context: tags_type) },
31
+ as: :taggable,
32
+ class_name: 'ActsAsTaggableOn::Tagging',
33
+ dependent: :destroy,
34
+ after_add: :dirtify_tag_list,
35
+ after_remove: :dirtify_tag_list
36
+
37
+ has_many context_tags, -> { order(taggings_order) },
38
+ class_name: 'ActsAsTaggableOn::Tag',
39
+ through: context_taggings,
40
+ source: :tag
41
+
42
+ attribute "#{tags_type.singularize}_list".to_sym, ActsAsTaggableOn::Taggable::TagListType.new
43
+ end
44
+
45
+ taggable_mixin.class_eval <<-RUBY, __FILE__, __LINE__ + 1
46
+ def #{tag_type}_list
47
+ tag_list_on('#{tags_type}')
48
+ end
49
+
50
+ def #{tag_type}_list=(new_tags)
51
+ parsed_new_list = ActsAsTaggableOn.default_parser.new(new_tags).parse
52
+
53
+ if self.class.preserve_tag_order? || (parsed_new_list.sort != #{tag_type}_list.sort)
54
+ if ActsAsTaggableOn::Utils.legacy_activerecord?
55
+ set_attribute_was("#{tag_type}_list", #{tag_type}_list)
56
+ else
57
+ attribute_change("#{tag_type}_list")
58
+ end
59
+ write_attribute("#{tag_type}_list", parsed_new_list)
60
+ end
61
+
62
+ set_tag_list_on('#{tags_type}', new_tags)
63
+ end
64
+
65
+ def all_#{tags_type}_list
66
+ all_tags_list_on('#{tags_type}')
67
+ end
68
+
69
+ private
70
+ def dirtify_tag_list(tagging)
71
+ attribute_will_change! tagging.context.singularize+"_list"
72
+ end
73
+ RUBY
74
+ end
75
+ end
76
+
77
+ def taggable_on(preserve_tag_order, *tag_types)
78
+ super(preserve_tag_order, *tag_types)
79
+ initialize_acts_as_taggable_on_core
80
+ end
81
+
82
+ # all column names are necessary for PostgreSQL group clause
83
+ def grouped_column_names_for(object)
84
+ object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(', ')
85
+ end
86
+
87
+ ##
88
+ # Return a scope of objects that are tagged with the specified tags.
89
+ #
90
+ # @param tags The tags that we want to query for
91
+ # @param [Hash] options A hash of options to alter you query:
92
+ # * <tt>:exclude</tt> - if set to true, return objects that are *NOT* tagged with the specified tags
93
+ # * <tt>:any</tt> - if set to true, return objects that are tagged with *ANY* of the specified tags
94
+ # * <tt>:order_by_matching_tag_count</tt> - if set to true and used with :any, sort by objects matching the most tags, descending
95
+ # * <tt>:match_all</tt> - if set to true, return objects that are *ONLY* tagged with the specified tags
96
+ # * <tt>:owned_by</tt> - return objects that are *ONLY* owned by the owner
97
+ # * <tt>:start_at</tt> - Restrict the tags to those created after a certain time
98
+ # * <tt>:end_at</tt> - Restrict the tags to those created before a certain time
99
+ #
100
+ # Example:
101
+ # User.tagged_with(["awesome", "cool"]) # Users that are tagged with awesome and cool
102
+ # User.tagged_with(["awesome", "cool"], :exclude => true) # Users that are not tagged with awesome or cool
103
+ # User.tagged_with(["awesome", "cool"], :any => true) # Users that are tagged with awesome or cool
104
+ # User.tagged_with(["awesome", "cool"], :any => true, :order_by_matching_tag_count => true) # Sort by users who match the most tags, descending
105
+ # User.tagged_with(["awesome", "cool"], :match_all => true) # Users that are tagged with just awesome and cool
106
+ # User.tagged_with(["awesome", "cool"], :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo'
107
+ # User.tagged_with(["awesome", "cool"], :owned_by => foo, :start_at => Date.today ) # Users that are tagged with just awesome, cool by 'foo' and starting today
108
+ def tagged_with(tags, options = {})
109
+ tag_list = ActsAsTaggableOn.default_parser.new(tags).parse
110
+ options = options.dup
111
+
112
+ return none if tag_list.empty?
113
+
114
+ ::ActsAsTaggableOn::Taggable::TaggedWithQuery.build(self, ActsAsTaggableOn::Tag, ActsAsTaggableOn::Tagging, tag_list, options)
115
+ end
116
+
117
+ def is_taggable?
118
+ true
119
+ end
120
+
121
+ def taggable_mixin
122
+ @taggable_mixin ||= Module.new
123
+ end
124
+ end
125
+
126
+ # all column names are necessary for PostgreSQL group clause
127
+ def grouped_column_names_for(object)
128
+ self.class.grouped_column_names_for(object)
129
+ end
130
+
131
+ def custom_contexts
132
+ @custom_contexts ||= taggings.map(&:context).uniq
133
+ end
134
+
135
+ def is_taggable?
136
+ self.class.is_taggable?
137
+ end
138
+
139
+ def add_custom_context(value)
140
+ custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s)
141
+ end
142
+
143
+ def cached_tag_list_on(context)
144
+ self["cached_#{context.to_s.singularize}_list"]
145
+ end
146
+
147
+ def tag_list_cache_set_on(context)
148
+ variable_name = "@#{context.to_s.singularize}_list"
149
+ instance_variable_defined?(variable_name) && instance_variable_get(variable_name)
150
+ end
151
+
152
+ def tag_list_cache_on(context)
153
+ variable_name = "@#{context.to_s.singularize}_list"
154
+ if instance_variable_get(variable_name)
155
+ instance_variable_get(variable_name)
156
+ elsif cached_tag_list_on(context) && ensure_included_cache_methods! && self.class.caching_tag_list_on?(context)
157
+ instance_variable_set(variable_name, ActsAsTaggableOn.default_parser.new(cached_tag_list_on(context)).parse)
158
+ else
159
+ instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
160
+ end
161
+ end
162
+
163
+ def tag_list_on(context)
164
+ add_custom_context(context)
165
+ tag_list_cache_on(context)
166
+ end
167
+
168
+ def all_tags_list_on(context)
169
+ variable_name = "@all_#{context.to_s.singularize}_list"
170
+ return instance_variable_get(variable_name) if instance_variable_defined?(variable_name) && instance_variable_get(variable_name)
171
+
172
+ instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(all_tags_on(context).map(&:name)).freeze)
173
+ end
174
+
175
+ ##
176
+ # Returns all tags of a given context
177
+ def all_tags_on(context)
178
+ tagging_table_name = ActsAsTaggableOn::Tagging.table_name
179
+
180
+ opts = ["#{tagging_table_name}.context = ?", context.to_s]
181
+ scope = base_tags.where(opts)
182
+
183
+ if ActsAsTaggableOn::Utils.using_postgresql?
184
+ group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
185
+ scope.order(Arel.sql("max(#{tagging_table_name}.created_at)")).group(group_columns)
186
+ else
187
+ scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
188
+ end.to_a
189
+ end
190
+
191
+ ##
192
+ # Returns all tags that are not owned of a given context
193
+ def tags_on(context)
194
+ scope = base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s])
195
+ # when preserving tag order, return tags in created order
196
+ # if we added the order to the association this would always apply
197
+ scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order?
198
+ scope
199
+ end
200
+
201
+ def set_tag_list_on(context, new_list)
202
+ add_custom_context(context)
203
+
204
+ variable_name = "@#{context.to_s.singularize}_list"
205
+
206
+ parsed_new_list = ActsAsTaggableOn.default_parser.new(new_list).parse
207
+
208
+ instance_variable_set(variable_name, parsed_new_list)
209
+ end
210
+
211
+ def tagging_contexts
212
+ self.class.tag_types.map(&:to_s) + custom_contexts
213
+ end
214
+
215
+ def reload(*args)
216
+ self.class.tag_types.each do |context|
217
+ instance_variable_set("@#{context.to_s.singularize}_list", nil)
218
+ instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
219
+ end
220
+
221
+ super(*args)
222
+ end
223
+
224
+ ##
225
+ # Find existing tags or create non-existing tags
226
+ def load_tags(tag_list)
227
+ ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
228
+ end
229
+
230
+ def save_tags
231
+ tagging_contexts.each do |context|
232
+ next unless tag_list_cache_set_on(context)
233
+ # List of currently assigned tag names
234
+ tag_list = tag_list_cache_on(context).uniq
235
+
236
+ # Find existing tags or create non-existing tags:
237
+ tags = find_or_create_tags_from_list_with_context(tag_list, context)
238
+
239
+ # Tag objects for currently assigned tags
240
+ current_tags = tags_on(context)
241
+
242
+ # Tag maintenance based on whether preserving the created order of tags
243
+ if self.class.preserve_tag_order?
244
+ old_tags, new_tags = current_tags - tags, tags - current_tags
245
+
246
+ shared_tags = current_tags & tags
247
+
248
+ if shared_tags.any? && tags[0...shared_tags.size] != shared_tags
249
+ index = shared_tags.each_with_index { |_, i| break i unless shared_tags[i] == tags[i] }
250
+
251
+ # Update arrays of tag objects
252
+ old_tags |= current_tags[index...current_tags.size]
253
+ new_tags |= current_tags[index...current_tags.size] & shared_tags
254
+
255
+ # Order the array of tag objects to match the tag list
256
+ new_tags = tags.map do |t|
257
+ new_tags.find { |n| n.name.downcase == t.name.downcase }
258
+ end.compact
259
+ end
260
+ else
261
+ # Delete discarded tags and create new tags
262
+ old_tags = current_tags - tags
263
+ new_tags = tags - current_tags
264
+ end
265
+
266
+ # Destroy old taggings:
267
+ if old_tags.present?
268
+ taggings.not_owned.by_context(context).where(tag_id: old_tags).destroy_all
269
+ end
270
+
271
+ # Create new taggings:
272
+ new_tags.each do |tag|
273
+ taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self)
274
+ end
275
+ end
276
+
277
+ true
278
+ end
279
+
280
+ private
281
+
282
+ def ensure_included_cache_methods!
283
+ self.class.columns
284
+ end
285
+
286
+ # Filters the tag lists from the attribute names.
287
+ def attributes_for_update(attribute_names)
288
+ tag_lists = tag_types.map {|tags_type| "#{tags_type.to_s.singularize}_list"}
289
+ super.delete_if {|attr| tag_lists.include? attr }
290
+ end
291
+
292
+ # Filters the tag lists from the attribute names.
293
+ def attributes_for_create(attribute_names)
294
+ tag_lists = tag_types.map {|tags_type| "#{tags_type.to_s.singularize}_list"}
295
+ super.delete_if {|attr| tag_lists.include? attr }
296
+ end
297
+
298
+ ##
299
+ # Override this hook if you wish to subclass {ActsAsTaggableOn::Tag} --
300
+ # context is provided so that you may conditionally use a Tag subclass
301
+ # only for some contexts.
302
+ #
303
+ # @example Custom Tag class for one context
304
+ # class Company < ActiveRecord::Base
305
+ # acts_as_taggable_on :markets, :locations
306
+ #
307
+ # def find_or_create_tags_from_list_with_context(tag_list, context)
308
+ # if context.to_sym == :markets
309
+ # MarketTag.find_or_create_all_with_like_by_name(tag_list)
310
+ # else
311
+ # super
312
+ # end
313
+ # end
314
+ #
315
+ # @param [Array<String>] tag_list Tags to find or create
316
+ # @param [Symbol] context The tag context for the tag_list
317
+ def find_or_create_tags_from_list_with_context(tag_list, _context)
318
+ load_tags(tag_list)
319
+ end
320
+ end
321
+ end
322
+