make_taggable 0.6.3

Sign up to get free protection for your applications and to get access to all the features.
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