nano-max-tool 0.0.1

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