make_taggable 0.7.4 → 1.0.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +79 -0
  3. data/CONTRIBUTING.md +65 -22
  4. data/LICENSE.md +18 -17
  5. data/README.md +73 -444
  6. data/UPGRADING.md +28 -2
  7. data/docs/caching.md +93 -0
  8. data/docs/configuration.md +108 -0
  9. data/docs/contexts.md +147 -0
  10. data/docs/database.md +106 -0
  11. data/docs/getting-started.md +114 -0
  12. data/docs/migrating-from-aato.md +67 -0
  13. data/docs/ownership.md +111 -0
  14. data/docs/parsers.md +109 -0
  15. data/docs/querying.md +163 -0
  16. data/docs/tag-clouds.md +74 -0
  17. data/lib/make_taggable/default_parser.rb +45 -32
  18. data/lib/make_taggable/engine.rb +6 -0
  19. data/lib/make_taggable/generic_parser.rb +30 -4
  20. data/lib/make_taggable/tag.rb +121 -19
  21. data/lib/make_taggable/tag_list.rb +77 -24
  22. data/lib/make_taggable/taggable/cache.rb +61 -11
  23. data/lib/make_taggable/taggable/collection.rb +102 -30
  24. data/lib/make_taggable/taggable/core.rb +154 -34
  25. data/lib/make_taggable/taggable/ownership.rb +82 -4
  26. data/lib/make_taggable/taggable/related.rb +66 -3
  27. data/lib/make_taggable/taggable/tag_list_type.rb +8 -0
  28. data/lib/make_taggable/taggable/tagged_with_query/all_tags_query.rb +10 -0
  29. data/lib/make_taggable/taggable/tagged_with_query/any_tags_query.rb +10 -0
  30. data/lib/make_taggable/taggable/tagged_with_query/exclude_tags_query.rb +10 -0
  31. data/lib/make_taggable/taggable/tagged_with_query/query_base.rb +15 -0
  32. data/lib/make_taggable/taggable/tagged_with_query.rb +17 -0
  33. data/lib/make_taggable/taggable.rb +34 -33
  34. data/lib/make_taggable/tagger.rb +73 -12
  35. data/lib/make_taggable/tagging.rb +30 -3
  36. data/lib/make_taggable/tags_helper.rb +22 -1
  37. data/lib/make_taggable/utils.rb +40 -5
  38. data/lib/make_taggable/version.rb +8 -1
  39. data/lib/make_taggable.rb +153 -8
  40. data/make_taggable.gemspec +13 -20
  41. metadata +50 -165
  42. data/.dummyrc +0 -17
  43. data/.github/workflows/ci.yml +0 -140
  44. data/.github/workflows/standard-ci.yml +0 -27
  45. data/.gitignore +0 -17
  46. data/.rspec +0 -3
  47. data/Appraisals +0 -15
  48. data/Gemfile +0 -16
  49. data/Rakefile +0 -13
  50. data/gemfiles/rails_5.gemfile +0 -9
  51. data/gemfiles/rails_6.gemfile +0 -9
  52. data/gemfiles/rails_6_1.gemfile +0 -9
  53. data/gemfiles/rails_master.gemfile +0 -9
  54. data/lib/tasks/setup_test_db.rake +0 -8
@@ -1,11 +1,32 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MakeTaggable::Taggable
4
+ ##
5
+ # Counting tags, for tag clouds and "most used" lists.
6
+ #
7
+ # Each context gets `<context>_counts` and `top_<context>` on both the class and its instances.
8
+ #
2
9
  module Collection
10
+ ##
11
+ # @param base [Class] the model being made taggable
12
+ # @return [void]
13
+ #
14
+ # @api private
15
+ #
3
16
  def self.included(base)
4
17
  base.extend MakeTaggable::Taggable::Collection::ClassMethods
5
18
  base.initialize_make_taggable_collection
6
19
  end
7
20
 
21
+ ##
22
+ # Added to every taggable model.
23
+ #
8
24
  module ClassMethods
25
+ ##
26
+ # Defines the counting methods for each context.
27
+ #
28
+ # @return [void]
29
+ #
9
30
  def initialize_make_taggable_collection
10
31
  tag_types.map(&:to_s).each do |tag_type|
11
32
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
@@ -28,30 +49,56 @@ module MakeTaggable::Taggable
28
49
  end
29
50
  end
30
51
 
52
+ ##
53
+ # Adds contexts and refreshes the counting methods.
54
+ #
55
+ # @param args [Array<Symbol, String>] the contexts to add
56
+ # @return [void]
57
+ #
31
58
  def make_taggable(*args)
32
- super(*args)
59
+ super
33
60
  initialize_make_taggable_collection
34
61
  end
35
62
 
63
+ ##
64
+ # Tags used in one context, each carrying how often it was used.
65
+ #
66
+ # @param context [Symbol, String] the tagging context
67
+ # @param options [Hash] options accepted by {#all_tag_counts}
68
+ # @return [ActiveRecord::Relation]
69
+ #
70
+ # @example
71
+ # Book.tag_counts_on(:genres)
72
+ #
36
73
  def tag_counts_on(context, options = {})
37
74
  all_tag_counts(options.merge({on: context.to_s}))
38
75
  end
39
76
 
77
+ ##
78
+ # Tags used in one context, without counting them.
79
+ #
80
+ # @param context [Symbol, String] the tagging context
81
+ # @param options [Hash] options accepted by {#all_tags}
82
+ # @return [ActiveRecord::Relation]
83
+ #
40
84
  def tags_on(context, options = {})
41
85
  all_tags(options.merge({on: context.to_s}))
42
86
  end
43
87
 
44
88
  ##
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
89
+ # Every tag applied to this model, without counting them.
90
+ #
91
+ # Cheaper than {#all_tag_counts}, which has to join the taggables.
92
+ #
93
+ # @param options [Hash] the query options
94
+ # @option options [Time, Date] :start_at only tags applied after this time
95
+ # @option options [Time, Date] :end_at only tags applied before this time
96
+ # @option options [String, Array] :conditions SQL conditions added to the tag query
97
+ # @option options [Integer] :limit the most tags to return
98
+ # @option options [String] :order SQL to order by, such as `"tags.name asc"`
99
+ # @option options [Symbol, String] :on only tags applied in this context
100
+ # @return [ActiveRecord::Relation]
101
+ #
55
102
  def all_tags(options = {})
56
103
  options = options.dup
57
104
  options.assert_valid_keys :start_at, :end_at, :conditions, :order, :limit, :on
@@ -76,17 +123,22 @@ module MakeTaggable::Taggable
76
123
  end
77
124
 
78
125
  ##
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
126
+ # Every tag applied to this model, each carrying how often it was used as a `count` attribute.
127
+ #
128
+ # @param options [Hash] the query options
129
+ # @option options [Time, Date] :start_at only tags applied after this time
130
+ # @option options [Time, Date] :end_at only tags applied before this time
131
+ # @option options [String, Array] :conditions SQL conditions added to the tag query
132
+ # @option options [Integer] :limit the most tags to return
133
+ # @option options [String] :order SQL to order by, such as `"count desc"`
134
+ # @option options [Integer] :at_least skip tags used fewer times than this
135
+ # @option options [Integer] :at_most skip tags used more times than this
136
+ # @option options [Symbol, String] :on only tags applied in this context
137
+ # @return [ActiveRecord::Relation]
138
+ #
139
+ # @example The ten most used genres
140
+ # Book.all_tag_counts(on: :genres, order: "count desc", limit: 10)
141
+ #
90
142
  def all_tag_counts(options = {})
91
143
  options = options.dup
92
144
  options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
@@ -101,7 +153,7 @@ module MakeTaggable::Taggable
101
153
  # Current model is STI descendant, so add type checking to the join condition
102
154
  unless descends_from_active_record?
103
155
  taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{MakeTaggable::Tagging.table_name}.taggable_id"
104
- taggable_join << " AND #{table_name}.#{inheritance_column} = '#{name}'"
156
+ taggable_join += sanitize_sql([" AND #{table_name}.#{inheritance_column} = ?", name])
105
157
  tagging_scope = tagging_scope.joins(taggable_join)
106
158
  end
107
159
 
@@ -127,8 +179,16 @@ module MakeTaggable::Taggable
127
179
  tag_scope_joins(tag_scope, tagging_scope)
128
180
  end
129
181
 
182
+ ##
183
+ # A relation's SQL with its bind parameters inlined, so it can be embedded in another query.
184
+ #
185
+ # @param relation [ActiveRecord::Relation] the relation to render
186
+ # @return [String]
187
+ #
188
+ # @api private
189
+ #
130
190
  def safe_to_sql(relation)
131
- connection.respond_to?(:unprepared_statement) ? connection.unprepared_statement { relation.to_sql } : relation.to_sql
191
+ connection.unprepared_statement { relation.to_sql }
132
192
  end
133
193
 
134
194
  private
@@ -166,17 +226,29 @@ module MakeTaggable::Taggable
166
226
  end
167
227
  end
168
228
 
229
+ ##
230
+ # Tags used on this record in one context, each carrying how often it was used.
231
+ #
232
+ # @param context [Symbol, String] the tagging context
233
+ # @param options [Hash] options accepted by {ClassMethods#all_tag_counts}
234
+ # @return [ActiveRecord::Relation]
235
+ #
169
236
  def tag_counts_on(context, options = {})
170
237
  self.class.tag_counts_on(context, options.merge(id: id))
171
238
  end
172
239
 
240
+ # These relations carry a custom SELECT -- the tag columns plus an aliased
241
+ # count -- which Active Record would otherwise fold into the COUNT(). Count
242
+ # rows instead, whatever the relation selects.
173
243
  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)
244
+ ##
245
+ # Counts rows rather than the relation's selected columns.
246
+ #
247
+ # @param column_name [Symbol, String] the column to count
248
+ # @return [Integer]
249
+ #
250
+ def count(column_name = :all)
251
+ super
180
252
  end
181
253
  end
182
254
  end
@@ -1,8 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "tagged_with_query"
2
4
  require_relative "tag_list_type"
3
5
 
4
6
  module MakeTaggable::Taggable
7
+ ##
8
+ # The heart of tagging: the associations, the generated `<context>_list` readers and writers, and
9
+ # the `tagged_with` query.
10
+ #
5
11
  module Core
12
+ ##
13
+ # @param base [Class] the model being made taggable
14
+ # @return [void]
15
+ #
16
+ # @api private
17
+ #
6
18
  def self.included(base)
7
19
  base.extend MakeTaggable::Taggable::Core::ClassMethods
8
20
 
@@ -14,12 +26,21 @@ module MakeTaggable::Taggable
14
26
  base.initialize_make_taggable_core
15
27
  end
16
28
 
29
+ ##
30
+ # Added to every taggable model.
31
+ #
17
32
  module ClassMethods
33
+ ##
34
+ # Builds the associations and the `<context>_list` methods for each context.
35
+ #
36
+ # @return [void]
37
+ #
18
38
  def initialize_make_taggable_core
19
39
  include taggable_mixin
40
+
20
41
  tag_types.map(&:to_s).each do |tags_type|
21
42
  tag_type = tags_type.to_s.singularize
22
- context_taggings = "#{tag_type}_taggings".to_sym
43
+ context_taggings = :"#{tag_type}_taggings"
23
44
  context_tags = tags_type.to_sym
24
45
  taggings_order = (preserve_tag_order? ? "#{MakeTaggable::Tagging.table_name}.id" : [])
25
46
 
@@ -38,7 +59,7 @@ module MakeTaggable::Taggable
38
59
  through: context_taggings,
39
60
  source: :tag
40
61
 
41
- attribute "#{tags_type.singularize}_list".to_sym, MakeTaggable::Taggable::TagListType.new
62
+ attribute :"#{tags_type.singularize}_list", MakeTaggable::Taggable::TagListType.new
42
63
  end
43
64
 
44
65
  taggable_mixin.class_eval <<-RUBY, __FILE__, __LINE__ + 1
@@ -50,12 +71,8 @@ module MakeTaggable::Taggable
50
71
  parsed_new_list = MakeTaggable.default_parser.new(new_tags).parse
51
72
 
52
73
  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
74
+ unless #{tag_type}_list_changed?
75
+ @attributes["#{tag_type}_list"] = ActiveModel::Attribute.from_user("#{tag_type}_list", #{tag_type}_list, MakeTaggable::Taggable::TagListType.new)
59
76
  end
60
77
  write_attribute("#{tag_type}_list", parsed_new_list)
61
78
  end
@@ -75,8 +92,17 @@ module MakeTaggable::Taggable
75
92
  end
76
93
  end
77
94
 
95
+ ##
96
+ # Adds contexts and rebuilds the generated methods.
97
+ #
98
+ # @param preserve_tag_order [TrueClass, FalseClass] whether to keep tags in the order added
99
+ # @param tag_types [Array<Symbol, String>] the contexts to add
100
+ # @return [void]
101
+ #
102
+ # @api private
103
+ #
78
104
  def taggable_on(preserve_tag_order, *tag_types)
79
- super(preserve_tag_order, *tag_types)
105
+ super
80
106
  initialize_make_taggable_core
81
107
  end
82
108
 
@@ -86,26 +112,34 @@ module MakeTaggable::Taggable
86
112
  end
87
113
 
88
114
  ##
89
- # Return a scope of objects that are tagged with the specified tags.
115
+ # Records tagged with the given tags.
116
+ #
117
+ # By default a record must carry every tag given. `:any` relaxes that to at least one, and
118
+ # `:exclude` inverts it to none.
90
119
  #
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
120
+ # @param tags [String, Array<String>] the tags to match
121
+ # @param options [Hash] the query options
122
+ # @option options [TrueClass, FalseClass] :any match records carrying any of the tags
123
+ # @option options [TrueClass, FalseClass] :exclude match records carrying none of the tags
124
+ # @option options [TrueClass, FalseClass] :match_all match records carrying only these tags
125
+ # @option options [TrueClass, FalseClass] :wild match tags containing the given text
126
+ # @option options [TrueClass, FalseClass] :order_by_matching_tag_count with `:any`, order by
127
+ # how many tags matched, most first
128
+ # @option options [ActiveRecord::Base] :owned_by only tags applied by this tagger
129
+ # @option options [Symbol, String] :on only tags applied in this context
130
+ # @option options [Time, Date] :start_at only tags applied after this time
131
+ # @option options [Time, Date] :end_at only tags applied before this time
132
+ # @return [ActiveRecord::Relation] empty when no tags are given
133
+ #
134
+ # @example Every tag
135
+ # User.tagged_with(["awesome", "cool"])
136
+ #
137
+ # @example Any tag, most matches first
138
+ # User.tagged_with(["awesome", "cool"], any: true, order_by_matching_tag_count: true)
139
+ #
140
+ # @example Scoped to a context and an owner
141
+ # Photo.tagged_with("paris", on: :locations, owned_by: @user)
100
142
  #
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
143
  def tagged_with(tags, options = {})
110
144
  tag_list = MakeTaggable.default_parser.new(tags).parse
111
145
  options = options.dup
@@ -119,6 +153,12 @@ module MakeTaggable::Taggable
119
153
  true
120
154
  end
121
155
 
156
+ ##
157
+ # The module the generated `<context>_list` methods are defined on, so a model can override
158
+ # one and call `super`.
159
+ #
160
+ # @return [Module]
161
+ #
122
162
  def taggable_mixin
123
163
  @taggable_mixin ||= Module.new
124
164
  end
@@ -129,6 +169,11 @@ module MakeTaggable::Taggable
129
169
  self.class.grouped_column_names_for(object)
130
170
  end
131
171
 
172
+ ##
173
+ # Contexts this record has been tagged in beyond those the model declares.
174
+ #
175
+ # @return [Array<String>]
176
+ #
132
177
  def custom_contexts
133
178
  @custom_contexts ||= taggings.map(&:context).uniq
134
179
  end
@@ -137,19 +182,43 @@ module MakeTaggable::Taggable
137
182
  self.class.is_taggable?
138
183
  end
139
184
 
185
+ ##
186
+ # Records a context the model does not declare, so it takes part in saving and reloading.
187
+ #
188
+ # @param value [Symbol, String] the context
189
+ # @return [Array<String>, NilClass]
190
+ #
140
191
  def add_custom_context(value)
141
192
  custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) || self.class.tag_types.map(&:to_s).include?(value.to_s)
142
193
  end
143
194
 
195
+ ##
196
+ # The rendered tag list held in this context's caching column, if the model has one.
197
+ #
198
+ # @param context [Symbol, String] the tagging context
199
+ # @return [String, NilClass]
200
+ #
144
201
  def cached_tag_list_on(context)
145
202
  self["cached_#{context.to_s.singularize}_list"]
146
203
  end
147
204
 
205
+ ##
206
+ # Whether a context's tag list has been loaded or assigned on this record.
207
+ #
208
+ # @param context [Symbol, String] the tagging context
209
+ # @return [TrueClass, FalseClass, MakeTaggable::TagList]
210
+ #
148
211
  def tag_list_cache_set_on(context)
149
212
  variable_name = "@#{context.to_s.singularize}_list"
150
213
  instance_variable_defined?(variable_name) && instance_variable_get(variable_name)
151
214
  end
152
215
 
216
+ ##
217
+ # A context's tag list, loading it from the caching column or the database as needed.
218
+ #
219
+ # @param context [Symbol, String] the tagging context
220
+ # @return [MakeTaggable::TagList]
221
+ #
153
222
  def tag_list_cache_on(context)
154
223
  variable_name = "@#{context.to_s.singularize}_list"
155
224
  if instance_variable_get(variable_name)
@@ -161,11 +230,28 @@ module MakeTaggable::Taggable
161
230
  end
162
231
  end
163
232
 
233
+ ##
234
+ # A context's tag list, covering contexts the model does not declare.
235
+ #
236
+ # Only unowned tags appear here. Use {#all_tags_list_on} to include tags applied by a tagger.
237
+ #
238
+ # @param context [Symbol, String] the tagging context
239
+ # @return [MakeTaggable::TagList]
240
+ #
241
+ # @example
242
+ # @user.tag_list_on(:customs) # => ["one", "two"]
243
+ #
164
244
  def tag_list_on(context)
165
245
  add_custom_context(context)
166
246
  tag_list_cache_on(context)
167
247
  end
168
248
 
249
+ ##
250
+ # A context's tag list including tags applied by a tagger.
251
+ #
252
+ # @param context [Symbol, String] the tagging context
253
+ # @return [MakeTaggable::TagList] frozen
254
+ #
169
255
  def all_tags_list_on(context)
170
256
  variable_name = "@all_#{context.to_s.singularize}_list"
171
257
  return instance_variable_get(variable_name) if instance_variable_defined?(variable_name) && instance_variable_get(variable_name)
@@ -199,6 +285,18 @@ module MakeTaggable::Taggable
199
285
  scope
200
286
  end
201
287
 
288
+ ##
289
+ # Replaces a context's tag list, covering contexts the model does not declare. Saved with the
290
+ # record.
291
+ #
292
+ # @param context [Symbol, String] the tagging context
293
+ # @param new_list [String, Array<String>] the tags to apply
294
+ # @return [MakeTaggable::TagList]
295
+ #
296
+ # @example
297
+ # @user.set_tag_list_on(:customs, "same, as, tag, list")
298
+ # @user.save
299
+ #
202
300
  def set_tag_list_on(context, new_list)
203
301
  add_custom_context(context)
204
302
 
@@ -209,17 +307,28 @@ module MakeTaggable::Taggable
209
307
  instance_variable_set(variable_name, parsed_new_list)
210
308
  end
211
309
 
310
+ ##
311
+ # Every context this record tags in, declared and dynamic alike.
312
+ #
313
+ # @return [Array<String>]
314
+ #
212
315
  def tagging_contexts
213
316
  self.class.tag_types.map(&:to_s) + custom_contexts
214
317
  end
215
318
 
319
+ ##
320
+ # Reloads the record, discarding the tag lists held in memory.
321
+ #
322
+ # @param args [Array<Object>] arguments forwarded to Active Record
323
+ # @return [ActiveRecord::Base] self
324
+ #
216
325
  def reload(*args)
217
326
  self.class.tag_types.each do |context|
218
327
  instance_variable_set("@#{context.to_s.singularize}_list", nil)
219
328
  instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
220
329
  end
221
330
 
222
- super(*args)
331
+ super
223
332
  end
224
333
 
225
334
  ##
@@ -228,6 +337,11 @@ module MakeTaggable::Taggable
228
337
  MakeTaggable::Tag.find_or_create_all_with_like_by_name(tag_list)
229
338
  end
230
339
 
340
+ ##
341
+ # Writes every assigned tag list to the database. Runs after save.
342
+ #
343
+ # @return [TrueClass]
344
+ #
231
345
  def save_tags
232
346
  tagging_contexts.each do |context|
233
347
  next unless tag_list_cache_set_on(context)
@@ -298,14 +412,17 @@ module MakeTaggable::Taggable
298
412
  end
299
413
 
300
414
  ##
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.
415
+ # Finds or creates the tag records for a list, given the context they are being applied in.
304
416
  #
305
- # @example Custom Tag class for one context
417
+ # Override it to keep a separate vocabulary for one context by returning tags from a
418
+ # {MakeTaggable::Tag} subclass.
419
+ #
420
+ # @example A separate Tag subclass for one context
306
421
  # class Company < ActiveRecord::Base
307
422
  # make_taggable :markets, :locations
308
423
  #
424
+ # private
425
+ #
309
426
  # def find_or_create_tags_from_list_with_context(tag_list, context)
310
427
  # if context.to_sym == :markets
311
428
  # MarketTag.find_or_create_all_with_like_by_name(tag_list)
@@ -313,9 +430,12 @@ module MakeTaggable::Taggable
313
430
  # super
314
431
  # end
315
432
  # end
433
+ # end
434
+ #
435
+ # @param tag_list [Array<String>] the tags to find or create
436
+ # @param _context [Symbol] the context the tags are being applied in
437
+ # @return [Array<MakeTaggable::Tag>]
316
438
  #
317
- # @param [Array<String>] tag_list Tags to find or create
318
- # @param [Symbol] context The tag context for the tag_list
319
439
  def find_or_create_tags_from_list_with_context(tag_list, _context)
320
440
  load_tags(tag_list)
321
441
  end
@@ -1,5 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MakeTaggable::Taggable
4
+ ##
5
+ # Tags applied by a tagger, kept separate from the record's own tags.
6
+ #
7
+ # Owned tags do not appear in `tag_list`, which only ever returns unowned tags. Use
8
+ # `all_tags_list` to see both together.
9
+ #
10
+ # @see MakeTaggable::Tagger
11
+ #
2
12
  module Ownership
13
+ ##
14
+ # @param base [Class] the model being made taggable
15
+ # @return [void]
16
+ #
17
+ # @api private
18
+ #
3
19
  def self.included(base)
4
20
  base.extend MakeTaggable::Taggable::Ownership::ClassMethods
5
21
 
@@ -10,12 +26,26 @@ module MakeTaggable::Taggable
10
26
  base.initialize_make_taggable_ownership
11
27
  end
12
28
 
29
+ ##
30
+ # Added to every taggable model.
31
+ #
13
32
  module ClassMethods
33
+ ##
34
+ # Adds contexts and refreshes the ownership readers.
35
+ #
36
+ # @param args [Array<Symbol, String>] the contexts to add
37
+ # @return [void]
38
+ #
14
39
  def make_taggable(*args)
15
40
  initialize_make_taggable_ownership
16
- super(*args)
41
+ super
17
42
  end
18
43
 
44
+ ##
45
+ # Defines a `<context>_from(owner)` reader for each context.
46
+ #
47
+ # @return [void]
48
+ #
19
49
  def initialize_make_taggable_ownership
20
50
  tag_types.map(&:to_s).each do |tag_type|
21
51
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
@@ -27,6 +57,12 @@ module MakeTaggable::Taggable
27
57
  end
28
58
  end
29
59
 
60
+ ##
61
+ # This record's tags belonging to one owner, across every context.
62
+ #
63
+ # @param owner [ActiveRecord::Base, NilClass] the tagger, or `nil` for every tag on the record
64
+ # @return [ActiveRecord::Relation]
65
+ #
30
66
  def owner_tags(owner)
31
67
  scope = if owner.nil?
32
68
  base_tags
@@ -48,6 +84,16 @@ module MakeTaggable::Taggable
48
84
  end
49
85
  end
50
86
 
87
+ ##
88
+ # This record's tags belonging to one owner, in one context.
89
+ #
90
+ # @param owner [ActiveRecord::Base, NilClass] the tagger, or `nil` for every tag on the record
91
+ # @param context [Symbol, String] the tagging context
92
+ # @return [ActiveRecord::Relation]
93
+ #
94
+ # @example
95
+ # @photo.owner_tags_on(@user, :locations)
96
+ #
51
97
  def owner_tags_on(owner, context)
52
98
  owner_tags(owner).where(
53
99
  MakeTaggable::Tagging.table_name.to_s => {
@@ -56,11 +102,24 @@ module MakeTaggable::Taggable
56
102
  )
57
103
  end
58
104
 
105
+ ##
106
+ # The per-owner tag lists held in memory for a context, keyed by owner.
107
+ #
108
+ # @param context [Symbol, String] the tagging context
109
+ # @return [Hash{ActiveRecord::Base => MakeTaggable::TagList}]
110
+ #
59
111
  def cached_owned_tag_list_on(context)
60
112
  variable_name = "@owned_#{context}_list"
61
113
  (instance_variable_defined?(variable_name) && instance_variable_get(variable_name)) || instance_variable_set(variable_name, {})
62
114
  end
63
115
 
116
+ ##
117
+ # One owner's tag list for a context.
118
+ #
119
+ # @param owner [ActiveRecord::Base] the tagger
120
+ # @param context [Symbol, String] the tagging context
121
+ # @return [MakeTaggable::TagList]
122
+ #
64
123
  def owner_tag_list_on(owner, context)
65
124
  add_custom_context(context)
66
125
 
@@ -69,6 +128,14 @@ module MakeTaggable::Taggable
69
128
  cache[owner] ||= MakeTaggable::TagList.new(*owner_tags_on(owner, context).map(&:name))
70
129
  end
71
130
 
131
+ ##
132
+ # Replaces one owner's tag list for a context. Saved with the record.
133
+ #
134
+ # @param owner [ActiveRecord::Base] the tagger
135
+ # @param context [Symbol, String] the tagging context
136
+ # @param new_list [String, Array<String>] the tags to apply
137
+ # @return [MakeTaggable::TagList]
138
+ #
72
139
  def set_owner_tag_list_on(owner, context, new_list)
73
140
  add_custom_context(context)
74
141
 
@@ -77,14 +144,25 @@ module MakeTaggable::Taggable
77
144
  cache[owner] = MakeTaggable.default_parser.new(new_list).parse
78
145
  end
79
146
 
147
+ ##
148
+ # Reloads the record, discarding the owned tag lists held in memory.
149
+ #
150
+ # @param args [Array<Object>] arguments forwarded to Active Record
151
+ # @return [ActiveRecord::Base] self
152
+ #
80
153
  def reload(*args)
81
154
  self.class.tag_types.each do |context|
82
155
  instance_variable_set("@owned_#{context}_list", nil)
83
156
  end
84
157
 
85
- super(*args)
158
+ super
86
159
  end
87
160
 
161
+ ##
162
+ # Writes every owner's tag lists to the database. Runs after save.
163
+ #
164
+ # @return [TrueClass]
165
+ #
88
166
  def save_owned_tags
89
167
  tagging_contexts.each do |context|
90
168
  cached_owned_tag_list_on(context).each do |owner, tag_list|
@@ -120,8 +198,8 @@ module MakeTaggable::Taggable
120
198
  # have the correct context, and are removed from the list.
121
199
  if old_tags.present?
122
200
  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
201
+ tagger_type: owner.class.base_class.to_s, tagger_id: owner.id,
202
+ tag_id: old_tags, context: context).destroy_all
125
203
  end
126
204
 
127
205
  # Create new taggings: