make_taggable 0.6.3

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 (156) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +47 -0
  3. data/.gitignore +13 -0
  4. data/.rspec +3 -0
  5. data/.standard.yml +18 -0
  6. data/.standard_todo.yml +5 -0
  7. data/.travis.yml +36 -0
  8. data/Appraisals +11 -0
  9. data/CHANGELOG.md +0 -0
  10. data/CODE_OF_CONDUCT.md +74 -0
  11. data/CONTRIBUTING.md +57 -0
  12. data/Gemfile +16 -0
  13. data/LICENSE.md +20 -0
  14. data/LICENSE.txt +21 -0
  15. data/README.md +478 -0
  16. data/Rakefile +7 -0
  17. data/bin/console +14 -0
  18. data/bin/setup +8 -0
  19. data/db/migrate/1_create_make_taggable_tags.rb +10 -0
  20. data/db/migrate/2_create_make_taggable_taggings.rb +12 -0
  21. data/db/migrate/3_add_index_to_tags.rb +5 -0
  22. data/db/migrate/4_add_index_to_taggings.rb +12 -0
  23. data/gemfiles/rails_5.gemfile +9 -0
  24. data/gemfiles/rails_6.gemfile +9 -0
  25. data/gemfiles/rails_master.gemfile +9 -0
  26. data/lib/make_taggable.rb +134 -0
  27. data/lib/make_taggable/default_parser.rb +75 -0
  28. data/lib/make_taggable/engine.rb +4 -0
  29. data/lib/make_taggable/generic_parser.rb +19 -0
  30. data/lib/make_taggable/tag.rb +131 -0
  31. data/lib/make_taggable/tag_list.rb +102 -0
  32. data/lib/make_taggable/taggable.rb +100 -0
  33. data/lib/make_taggable/taggable/cache.rb +90 -0
  34. data/lib/make_taggable/taggable/collection.rb +183 -0
  35. data/lib/make_taggable/taggable/core.rb +323 -0
  36. data/lib/make_taggable/taggable/ownership.rb +137 -0
  37. data/lib/make_taggable/taggable/related.rb +71 -0
  38. data/lib/make_taggable/taggable/tag_list_type.rb +4 -0
  39. data/lib/make_taggable/taggable/tagged_with_query.rb +16 -0
  40. data/lib/make_taggable/taggable/tagged_with_query/all_tags_query.rb +111 -0
  41. data/lib/make_taggable/taggable/tagged_with_query/any_tags_query.rb +68 -0
  42. data/lib/make_taggable/taggable/tagged_with_query/exclude_tags_query.rb +81 -0
  43. data/lib/make_taggable/taggable/tagged_with_query/query_base.rb +61 -0
  44. data/lib/make_taggable/tagger.rb +89 -0
  45. data/lib/make_taggable/tagging.rb +32 -0
  46. data/lib/make_taggable/tags_helper.rb +15 -0
  47. data/lib/make_taggable/utils.rb +34 -0
  48. data/lib/make_taggable/version.rb +4 -0
  49. data/lib/tasks/tags_collate_utf8.rake +17 -0
  50. data/make_taggable.gemspec +26 -0
  51. data/spec/dummy/README.md +24 -0
  52. data/spec/dummy/Rakefile +6 -0
  53. data/spec/dummy/app/assets/config/manifest.js +2 -0
  54. data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
  55. data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
  56. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  57. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  58. data/spec/dummy/app/jobs/application_job.rb +7 -0
  59. data/spec/dummy/app/mailers/application_mailer.rb +4 -0
  60. data/spec/dummy/app/models/altered_inheriting_taggable_model.rb +5 -0
  61. data/spec/dummy/app/models/application_record.rb +3 -0
  62. data/spec/dummy/app/models/cached_model.rb +3 -0
  63. data/spec/dummy/app/models/cached_model_with_array.rb +11 -0
  64. data/spec/dummy/app/models/columns_override_model.rb +5 -0
  65. data/spec/dummy/app/models/company.rb +15 -0
  66. data/spec/dummy/app/models/concerns/.keep +0 -0
  67. data/spec/dummy/app/models/inheriting_taggable_model.rb +4 -0
  68. data/spec/dummy/app/models/market.rb +2 -0
  69. data/spec/dummy/app/models/non_standard_id_taggable_model.rb +8 -0
  70. data/spec/dummy/app/models/ordered_taggable_model.rb +4 -0
  71. data/spec/dummy/app/models/other_cached_model.rb +3 -0
  72. data/spec/dummy/app/models/other_taggable_model.rb +4 -0
  73. data/spec/dummy/app/models/student.rb +4 -0
  74. data/spec/dummy/app/models/taggable_model.rb +14 -0
  75. data/spec/dummy/app/models/untaggable_model.rb +3 -0
  76. data/spec/dummy/app/models/user.rb +3 -0
  77. data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
  78. data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
  79. data/spec/dummy/bin/rails +4 -0
  80. data/spec/dummy/bin/rake +4 -0
  81. data/spec/dummy/bin/setup +33 -0
  82. data/spec/dummy/config.ru +5 -0
  83. data/spec/dummy/config/application.rb +19 -0
  84. data/spec/dummy/config/boot.rb +5 -0
  85. data/spec/dummy/config/cable.yml +10 -0
  86. data/spec/dummy/config/credentials.yml.enc +1 -0
  87. data/spec/dummy/config/database.yml +25 -0
  88. data/spec/dummy/config/environment.rb +5 -0
  89. data/spec/dummy/config/environments/development.rb +52 -0
  90. data/spec/dummy/config/environments/production.rb +105 -0
  91. data/spec/dummy/config/environments/test.rb +49 -0
  92. data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
  93. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  94. data/spec/dummy/config/initializers/cors.rb +16 -0
  95. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  96. data/spec/dummy/config/initializers/inflections.rb +16 -0
  97. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  98. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  99. data/spec/dummy/config/locales/en.yml +33 -0
  100. data/spec/dummy/config/master.key +1 -0
  101. data/spec/dummy/config/puma.rb +38 -0
  102. data/spec/dummy/config/routes.rb +3 -0
  103. data/spec/dummy/config/spring.rb +6 -0
  104. data/spec/dummy/config/storage.yml +34 -0
  105. data/spec/dummy/db/migrate/20201119220853_create_taggable_models.rb +8 -0
  106. data/spec/dummy/db/migrate/20201119221037_create_columns_override_models.rb +9 -0
  107. data/spec/dummy/db/migrate/20201119221121_create_non_standard_id_taggable_models.rb +8 -0
  108. data/spec/dummy/db/migrate/20201119221228_create_untaggable_models.rb +8 -0
  109. data/spec/dummy/db/migrate/20201119221247_create_cached_models.rb +9 -0
  110. data/spec/dummy/db/migrate/20201119221314_create_other_cached_models.rb +11 -0
  111. data/spec/dummy/db/migrate/20201119221343_create_companies.rb +7 -0
  112. data/spec/dummy/db/migrate/20201119221416_create_users.rb +7 -0
  113. data/spec/dummy/db/migrate/20201119221434_create_other_taggable_models.rb +8 -0
  114. data/spec/dummy/db/migrate/20201119221507_create_ordered_taggable_models.rb +8 -0
  115. data/spec/dummy/db/migrate/20201119221530_create_cache_methods_injected_models.rb +7 -0
  116. data/spec/dummy/db/migrate/20201119221629_create_other_cached_with_array_models.rb +11 -0
  117. data/spec/dummy/db/migrate/20201119221746_create_taggable_model_with_jsons.rb +9 -0
  118. data/spec/dummy/db/migrate/20201119222429_create_make_taggable_tags.make_taggable_engine.rb +11 -0
  119. data/spec/dummy/db/migrate/20201119222430_create_make_taggable_taggings.make_taggable_engine.rb +13 -0
  120. data/spec/dummy/db/migrate/20201119222431_add_index_to_tags.make_taggable_engine.rb +6 -0
  121. data/spec/dummy/db/migrate/20201119222432_add_index_to_taggings.make_taggable_engine.rb +13 -0
  122. data/spec/dummy/db/schema.rb +117 -0
  123. data/spec/dummy/db/seeds.rb +7 -0
  124. data/spec/dummy/lib/tasks/.keep +0 -0
  125. data/spec/dummy/log/.keep +0 -0
  126. data/spec/dummy/public/robots.txt +1 -0
  127. data/spec/dummy/storage/.keep +0 -0
  128. data/spec/dummy/test/channels/application_cable/connection_test.rb +11 -0
  129. data/spec/dummy/test/controllers/.keep +0 -0
  130. data/spec/dummy/test/fixtures/.keep +0 -0
  131. data/spec/dummy/test/fixtures/files/.keep +0 -0
  132. data/spec/dummy/test/integration/.keep +0 -0
  133. data/spec/dummy/test/mailers/.keep +0 -0
  134. data/spec/dummy/test/models/.keep +0 -0
  135. data/spec/dummy/test/test_helper.rb +13 -0
  136. data/spec/dummy/vendor/.keep +0 -0
  137. data/spec/make_taggable/acts_as_tagger_spec.rb +112 -0
  138. data/spec/make_taggable/caching_spec.rb +123 -0
  139. data/spec/make_taggable/default_parser_spec.rb +45 -0
  140. data/spec/make_taggable/dirty_spec.rb +140 -0
  141. data/spec/make_taggable/generic_parser_spec.rb +13 -0
  142. data/spec/make_taggable/make_taggable_spec.rb +260 -0
  143. data/spec/make_taggable/related_spec.rb +93 -0
  144. data/spec/make_taggable/single_table_inheritance_spec.rb +220 -0
  145. data/spec/make_taggable/tag_list_spec.rb +169 -0
  146. data/spec/make_taggable/tag_spec.rb +297 -0
  147. data/spec/make_taggable/taggable_spec.rb +804 -0
  148. data/spec/make_taggable/tagger_spec.rb +149 -0
  149. data/spec/make_taggable/tagging_spec.rb +115 -0
  150. data/spec/make_taggable/tags_helper_spec.rb +43 -0
  151. data/spec/make_taggable/utils_spec.rb +22 -0
  152. data/spec/make_taggable_spec.rb +5 -0
  153. data/spec/spec_helper.rb +18 -0
  154. data/spec/support/array.rb +9 -0
  155. data/spec/support/helpers.rb +31 -0
  156. metadata +391 -0
@@ -0,0 +1,323 @@
1
+ require_relative "tagged_with_query"
2
+ require_relative "tag_list_type"
3
+
4
+ module MakeTaggable::Taggable
5
+ module Core
6
+ def self.included(base)
7
+ base.extend MakeTaggable::Taggable::Core::ClassMethods
8
+
9
+ base.class_eval do
10
+ attr_writer :custom_contexts
11
+ after_save :save_tags
12
+ end
13
+
14
+ base.initialize_make_taggable_core
15
+ end
16
+
17
+ module ClassMethods
18
+ def initialize_make_taggable_core
19
+ include taggable_mixin
20
+ tag_types.map(&:to_s).each do |tags_type|
21
+ tag_type = tags_type.to_s.singularize
22
+ context_taggings = "#{tag_type}_taggings".to_sym
23
+ context_tags = tags_type.to_sym
24
+ taggings_order = (preserve_tag_order? ? "#{MakeTaggable::Tagging.table_name}.id" : [])
25
+
26
+ class_eval do
27
+ # when preserving tag order, include order option so that for a 'tags' context
28
+ # the associations tag_taggings & tags are always returned in created order
29
+ has_many context_taggings, -> { includes(:tag).order(taggings_order).where(context: tags_type) },
30
+ as: :taggable,
31
+ class_name: "MakeTaggable::Tagging",
32
+ dependent: :destroy,
33
+ after_add: :dirtify_tag_list,
34
+ after_remove: :dirtify_tag_list
35
+
36
+ has_many context_tags, -> { order(taggings_order) },
37
+ class_name: "MakeTaggable::Tag",
38
+ through: context_taggings,
39
+ source: :tag
40
+
41
+ attribute "#{tags_type.singularize}_list".to_sym, MakeTaggable::Taggable::TagListType.new
42
+ end
43
+
44
+ taggable_mixin.class_eval <<-RUBY, __FILE__, __LINE__ + 1
45
+ def #{tag_type}_list
46
+ tag_list_on('#{tags_type}')
47
+ end
48
+
49
+ def #{tag_type}_list=(new_tags)
50
+ parsed_new_list = MakeTaggable.default_parser.new(new_tags).parse
51
+
52
+ if self.class.preserve_tag_order? || (parsed_new_list.sort != #{tag_type}_list.sort)
53
+ if MakeTaggable::Utils.legacy_activerecord?
54
+ set_attribute_was("#{tag_type}_list", #{tag_type}_list)
55
+ else
56
+ unless #{tag_type}_list_changed?
57
+ @attributes["#{tag_type}_list"] = ActiveModel::Attribute.from_user("#{tag_type}_list", #{tag_type}_list, MakeTaggable::Taggable::TagListType.new)
58
+ end
59
+ end
60
+ write_attribute("#{tag_type}_list", parsed_new_list)
61
+ end
62
+
63
+ set_tag_list_on('#{tags_type}', new_tags)
64
+ end
65
+
66
+ def all_#{tags_type}_list
67
+ all_tags_list_on('#{tags_type}')
68
+ end
69
+
70
+ private
71
+ def dirtify_tag_list(tagging)
72
+ attribute_will_change! tagging.context.singularize+"_list"
73
+ end
74
+ RUBY
75
+ end
76
+ end
77
+
78
+ def taggable_on(preserve_tag_order, *tag_types)
79
+ super(preserve_tag_order, *tag_types)
80
+ initialize_make_taggable_core
81
+ end
82
+
83
+ # all column names are necessary for PostgreSQL group clause
84
+ def grouped_column_names_for(object)
85
+ object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
86
+ end
87
+
88
+ ##
89
+ # Return a scope of objects that are tagged with the specified tags.
90
+ #
91
+ # @param tags The tags that we want to query for
92
+ # @param [Hash] options A hash of options to alter you query:
93
+ # * <tt>:exclude</tt> - if set to true, return objects that are *NOT* tagged with the specified tags
94
+ # * <tt>:any</tt> - if set to true, return objects that are tagged with *ANY* of the specified tags
95
+ # * <tt>:order_by_matching_tag_count</tt> - if set to true and used with :any, sort by objects matching the most tags, descending
96
+ # * <tt>:match_all</tt> - if set to true, return objects that are *ONLY* tagged with the specified tags
97
+ # * <tt>:owned_by</tt> - return objects that are *ONLY* owned by the owner
98
+ # * <tt>:start_at</tt> - Restrict the tags to those created after a certain time
99
+ # * <tt>:end_at</tt> - Restrict the tags to those created before a certain time
100
+ #
101
+ # Example:
102
+ # User.tagged_with(["awesome", "cool"]) # Users that are tagged with awesome and cool
103
+ # User.tagged_with(["awesome", "cool"], :exclude => true) # Users that are not tagged with awesome or cool
104
+ # User.tagged_with(["awesome", "cool"], :any => true) # Users that are tagged with awesome or cool
105
+ # User.tagged_with(["awesome", "cool"], :any => true, :order_by_matching_tag_count => true) # Sort by users who match the most tags, descending
106
+ # User.tagged_with(["awesome", "cool"], :match_all => true) # Users that are tagged with just awesome and cool
107
+ # User.tagged_with(["awesome", "cool"], :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo'
108
+ # 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
109
+ def tagged_with(tags, options = {})
110
+ tag_list = MakeTaggable.default_parser.new(tags).parse
111
+ options = options.dup
112
+
113
+ return none if tag_list.empty?
114
+
115
+ ::MakeTaggable::Taggable::TaggedWithQuery.build(self, MakeTaggable::Tag, MakeTaggable::Tagging, tag_list, options)
116
+ end
117
+
118
+ def is_taggable?
119
+ true
120
+ end
121
+
122
+ def taggable_mixin
123
+ @taggable_mixin ||= Module.new
124
+ end
125
+ end
126
+
127
+ # all column names are necessary for PostgreSQL group clause
128
+ def grouped_column_names_for(object)
129
+ self.class.grouped_column_names_for(object)
130
+ end
131
+
132
+ def custom_contexts
133
+ @custom_contexts ||= taggings.map(&:context).uniq
134
+ end
135
+
136
+ def is_taggable?
137
+ self.class.is_taggable?
138
+ end
139
+
140
+ def add_custom_context(value)
141
+ custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) || self.class.tag_types.map(&:to_s).include?(value.to_s)
142
+ end
143
+
144
+ def cached_tag_list_on(context)
145
+ self["cached_#{context.to_s.singularize}_list"]
146
+ end
147
+
148
+ def tag_list_cache_set_on(context)
149
+ variable_name = "@#{context.to_s.singularize}_list"
150
+ instance_variable_defined?(variable_name) && instance_variable_get(variable_name)
151
+ end
152
+
153
+ def tag_list_cache_on(context)
154
+ variable_name = "@#{context.to_s.singularize}_list"
155
+ if instance_variable_get(variable_name)
156
+ instance_variable_get(variable_name)
157
+ elsif cached_tag_list_on(context) && ensure_included_cache_methods! && self.class.caching_tag_list_on?(context)
158
+ instance_variable_set(variable_name, MakeTaggable.default_parser.new(cached_tag_list_on(context)).parse)
159
+ else
160
+ instance_variable_set(variable_name, MakeTaggable::TagList.new(tags_on(context).map(&:name)))
161
+ end
162
+ end
163
+
164
+ def tag_list_on(context)
165
+ add_custom_context(context)
166
+ tag_list_cache_on(context)
167
+ end
168
+
169
+ def all_tags_list_on(context)
170
+ variable_name = "@all_#{context.to_s.singularize}_list"
171
+ return instance_variable_get(variable_name) if instance_variable_defined?(variable_name) && instance_variable_get(variable_name)
172
+
173
+ instance_variable_set(variable_name, MakeTaggable::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 = MakeTaggable::Tagging.table_name
180
+
181
+ opts = ["#{tagging_table_name}.context = ?", context.to_s]
182
+ scope = base_tags.where(opts)
183
+
184
+ if MakeTaggable::Utils.using_postgresql?
185
+ group_columns = grouped_column_names_for(MakeTaggable::Tag)
186
+ scope.order(Arel.sql("max(#{tagging_table_name}.created_at)")).group(group_columns)
187
+ else
188
+ scope.group("#{MakeTaggable::Tag.table_name}.#{MakeTaggable::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(["#{MakeTaggable::Tagging.table_name}.context = ? AND #{MakeTaggable::Tagging.table_name}.tagger_id IS NULL", context.to_s])
196
+ # when preserving tag order, return tags in created order
197
+ # if we added the order to the association this would always apply
198
+ scope = scope.order("#{MakeTaggable::Tagging.table_name}.id") if self.class.preserve_tag_order?
199
+ scope
200
+ end
201
+
202
+ def set_tag_list_on(context, new_list)
203
+ add_custom_context(context)
204
+
205
+ variable_name = "@#{context.to_s.singularize}_list"
206
+
207
+ parsed_new_list = MakeTaggable.default_parser.new(new_list).parse
208
+
209
+ instance_variable_set(variable_name, parsed_new_list)
210
+ end
211
+
212
+ def tagging_contexts
213
+ self.class.tag_types.map(&:to_s) + custom_contexts
214
+ end
215
+
216
+ def reload(*args)
217
+ self.class.tag_types.each do |context|
218
+ instance_variable_set("@#{context.to_s.singularize}_list", nil)
219
+ instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
220
+ end
221
+
222
+ super(*args)
223
+ end
224
+
225
+ ##
226
+ # Find existing tags or create non-existing tags
227
+ def load_tags(tag_list)
228
+ MakeTaggable::Tag.find_or_create_all_with_like_by_name(tag_list)
229
+ end
230
+
231
+ def save_tags
232
+ tagging_contexts.each do |context|
233
+ next unless tag_list_cache_set_on(context)
234
+
235
+ # List of currently assigned tag names
236
+ tag_list = tag_list_cache_on(context).uniq
237
+
238
+ # Find existing tags or create non-existing tags:
239
+ tags = find_or_create_tags_from_list_with_context(tag_list, context)
240
+
241
+ # Tag objects for currently assigned tags
242
+ current_tags = tags_on(context)
243
+
244
+ # Tag maintenance based on whether preserving the created order of tags
245
+ if self.class.preserve_tag_order?
246
+ old_tags, new_tags = current_tags - tags, tags - current_tags
247
+
248
+ shared_tags = current_tags & tags
249
+
250
+ if shared_tags.any? && tags[0...shared_tags.size] != shared_tags
251
+ index = shared_tags.each_with_index { |_, i| break i unless shared_tags[i] == tags[i] }
252
+
253
+ # Update arrays of tag objects
254
+ old_tags |= current_tags[index...current_tags.size]
255
+ new_tags |= current_tags[index...current_tags.size] & shared_tags
256
+
257
+ # Order the array of tag objects to match the tag list
258
+ new_tags = tags.map { |t|
259
+ new_tags.find { |n| n.name.downcase == t.name.downcase }
260
+ }.compact
261
+ end
262
+ else
263
+ # Delete discarded tags and create new tags
264
+ old_tags = current_tags - tags
265
+ new_tags = tags - current_tags
266
+ end
267
+
268
+ # Destroy old taggings:
269
+ if old_tags.present?
270
+ taggings.not_owned.by_context(context).where(tag_id: old_tags).destroy_all
271
+ end
272
+
273
+ # Create new taggings:
274
+ new_tags.each do |tag|
275
+ taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self)
276
+ end
277
+ end
278
+
279
+ true
280
+ end
281
+
282
+ private
283
+
284
+ def ensure_included_cache_methods!
285
+ self.class.columns
286
+ end
287
+
288
+ # Filters the tag lists from the attribute names.
289
+ def attributes_for_update(attribute_names)
290
+ tag_lists = tag_types.map { |tags_type| "#{tags_type.to_s.singularize}_list" }
291
+ super.delete_if { |attr| tag_lists.include? attr }
292
+ end
293
+
294
+ # Filters the tag lists from the attribute names.
295
+ def attributes_for_create(attribute_names)
296
+ tag_lists = tag_types.map { |tags_type| "#{tags_type.to_s.singularize}_list" }
297
+ super.delete_if { |attr| tag_lists.include? attr }
298
+ end
299
+
300
+ ##
301
+ # Override this hook if you wish to subclass {MakeTaggable::Tag} --
302
+ # context is provided so that you may conditionally use a Tag subclass
303
+ # only for some contexts.
304
+ #
305
+ # @example Custom Tag class for one context
306
+ # class Company < ActiveRecord::Base
307
+ # make_taggable :markets, :locations
308
+ #
309
+ # def find_or_create_tags_from_list_with_context(tag_list, context)
310
+ # if context.to_sym == :markets
311
+ # MarketTag.find_or_create_all_with_like_by_name(tag_list)
312
+ # else
313
+ # super
314
+ # end
315
+ # end
316
+ #
317
+ # @param [Array<String>] tag_list Tags to find or create
318
+ # @param [Symbol] context The tag context for the tag_list
319
+ def find_or_create_tags_from_list_with_context(tag_list, _context)
320
+ load_tags(tag_list)
321
+ end
322
+ end
323
+ end
@@ -0,0 +1,137 @@
1
+ module MakeTaggable::Taggable
2
+ module Ownership
3
+ def self.included(base)
4
+ base.extend MakeTaggable::Taggable::Ownership::ClassMethods
5
+
6
+ base.class_eval do
7
+ after_save :save_owned_tags
8
+ end
9
+
10
+ base.initialize_make_taggable_ownership
11
+ end
12
+
13
+ module ClassMethods
14
+ def make_taggable(*args)
15
+ initialize_make_taggable_ownership
16
+ super(*args)
17
+ end
18
+
19
+ def initialize_make_taggable_ownership
20
+ tag_types.map(&:to_s).each do |tag_type|
21
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
22
+ def #{tag_type}_from(owner)
23
+ owner_tag_list_on(owner, '#{tag_type}')
24
+ end
25
+ RUBY
26
+ end
27
+ end
28
+ end
29
+
30
+ def owner_tags(owner)
31
+ scope = if owner.nil?
32
+ base_tags
33
+ else
34
+ base_tags.where(
35
+ MakeTaggable::Tagging.table_name.to_s => {
36
+ tagger_id: owner.id,
37
+ tagger_type: owner.class.base_class.to_s
38
+ }
39
+ )
40
+ end
41
+
42
+ # when preserving tag order, return tags in created order
43
+ # if we added the order to the association this would always apply
44
+ if self.class.preserve_tag_order?
45
+ scope.order("#{MakeTaggable::Tagging.table_name}.id")
46
+ else
47
+ scope
48
+ end
49
+ end
50
+
51
+ def owner_tags_on(owner, context)
52
+ owner_tags(owner).where(
53
+ MakeTaggable::Tagging.table_name.to_s => {
54
+ context: context
55
+ }
56
+ )
57
+ end
58
+
59
+ def cached_owned_tag_list_on(context)
60
+ variable_name = "@owned_#{context}_list"
61
+ (instance_variable_defined?(variable_name) && instance_variable_get(variable_name)) || instance_variable_set(variable_name, {})
62
+ end
63
+
64
+ def owner_tag_list_on(owner, context)
65
+ add_custom_context(context)
66
+
67
+ cache = cached_owned_tag_list_on(context)
68
+
69
+ cache[owner] ||= MakeTaggable::TagList.new(*owner_tags_on(owner, context).map(&:name))
70
+ end
71
+
72
+ def set_owner_tag_list_on(owner, context, new_list)
73
+ add_custom_context(context)
74
+
75
+ cache = cached_owned_tag_list_on(context)
76
+
77
+ cache[owner] = MakeTaggable.default_parser.new(new_list).parse
78
+ end
79
+
80
+ def reload(*args)
81
+ self.class.tag_types.each do |context|
82
+ instance_variable_set("@owned_#{context}_list", nil)
83
+ end
84
+
85
+ super(*args)
86
+ end
87
+
88
+ def save_owned_tags
89
+ tagging_contexts.each do |context|
90
+ cached_owned_tag_list_on(context).each do |owner, tag_list|
91
+ # Find existing tags or create non-existing tags:
92
+ tags = find_or_create_tags_from_list_with_context(tag_list.uniq, context)
93
+
94
+ # Tag objects for owned tags
95
+ owned_tags = owner_tags_on(owner, context).to_a
96
+
97
+ # Tag maintenance based on whether preserving the created order of tags
98
+ if self.class.preserve_tag_order?
99
+ old_tags, new_tags = owned_tags - tags, tags - owned_tags
100
+
101
+ shared_tags = owned_tags & tags
102
+
103
+ if shared_tags.any? && tags[0...shared_tags.size] != shared_tags
104
+ index = shared_tags.each_with_index { |_, i| break i unless shared_tags[i] == tags[i] }
105
+
106
+ # Update arrays of tag objects
107
+ old_tags |= owned_tags.from(index)
108
+ new_tags |= owned_tags.from(index) & shared_tags
109
+
110
+ # Order the array of tag objects to match the tag list
111
+ new_tags = tags.map { |t| new_tags.find { |n| n.name.downcase == t.name.downcase } }.compact
112
+ end
113
+ else
114
+ # Delete discarded tags and create new tags
115
+ old_tags = owned_tags - tags
116
+ new_tags = tags - owned_tags
117
+ end
118
+
119
+ # Find all taggings that belong to the taggable (self), are owned by the owner,
120
+ # have the correct context, and are removed from the list.
121
+ if old_tags.present?
122
+ MakeTaggable::Tagging.where(taggable_id: id, taggable_type: self.class.base_class.to_s,
123
+ tagger_type: owner.class.base_class.to_s, tagger_id: owner.id,
124
+ tag_id: old_tags, context: context).destroy_all
125
+ end
126
+
127
+ # Create new taggings:
128
+ new_tags.each do |tag|
129
+ taggings.create!(tag_id: tag.id, context: context.to_s, tagger: owner, taggable: self)
130
+ end
131
+ end
132
+ end
133
+
134
+ true
135
+ end
136
+ end
137
+ end