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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +47 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.standard.yml +18 -0
- data/.standard_todo.yml +5 -0
- data/.travis.yml +36 -0
- data/Appraisals +11 -0
- data/CHANGELOG.md +0 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +57 -0
- data/Gemfile +16 -0
- data/LICENSE.md +20 -0
- data/LICENSE.txt +21 -0
- data/README.md +478 -0
- data/Rakefile +7 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/db/migrate/1_create_make_taggable_tags.rb +10 -0
- data/db/migrate/2_create_make_taggable_taggings.rb +12 -0
- data/db/migrate/3_add_index_to_tags.rb +5 -0
- data/db/migrate/4_add_index_to_taggings.rb +12 -0
- data/gemfiles/rails_5.gemfile +9 -0
- data/gemfiles/rails_6.gemfile +9 -0
- data/gemfiles/rails_master.gemfile +9 -0
- data/lib/make_taggable.rb +134 -0
- data/lib/make_taggable/default_parser.rb +75 -0
- data/lib/make_taggable/engine.rb +4 -0
- data/lib/make_taggable/generic_parser.rb +19 -0
- data/lib/make_taggable/tag.rb +131 -0
- data/lib/make_taggable/tag_list.rb +102 -0
- data/lib/make_taggable/taggable.rb +100 -0
- data/lib/make_taggable/taggable/cache.rb +90 -0
- data/lib/make_taggable/taggable/collection.rb +183 -0
- data/lib/make_taggable/taggable/core.rb +323 -0
- data/lib/make_taggable/taggable/ownership.rb +137 -0
- data/lib/make_taggable/taggable/related.rb +71 -0
- data/lib/make_taggable/taggable/tag_list_type.rb +4 -0
- data/lib/make_taggable/taggable/tagged_with_query.rb +16 -0
- data/lib/make_taggable/taggable/tagged_with_query/all_tags_query.rb +111 -0
- data/lib/make_taggable/taggable/tagged_with_query/any_tags_query.rb +68 -0
- data/lib/make_taggable/taggable/tagged_with_query/exclude_tags_query.rb +81 -0
- data/lib/make_taggable/taggable/tagged_with_query/query_base.rb +61 -0
- data/lib/make_taggable/tagger.rb +89 -0
- data/lib/make_taggable/tagging.rb +32 -0
- data/lib/make_taggable/tags_helper.rb +15 -0
- data/lib/make_taggable/utils.rb +34 -0
- data/lib/make_taggable/version.rb +4 -0
- data/lib/tasks/tags_collate_utf8.rake +17 -0
- data/make_taggable.gemspec +26 -0
- data/spec/dummy/README.md +24 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +2 -0
- data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
- data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
- data/spec/dummy/app/controllers/application_controller.rb +2 -0
- data/spec/dummy/app/controllers/concerns/.keep +0 -0
- data/spec/dummy/app/jobs/application_job.rb +7 -0
- data/spec/dummy/app/mailers/application_mailer.rb +4 -0
- data/spec/dummy/app/models/altered_inheriting_taggable_model.rb +5 -0
- data/spec/dummy/app/models/application_record.rb +3 -0
- data/spec/dummy/app/models/cached_model.rb +3 -0
- data/spec/dummy/app/models/cached_model_with_array.rb +11 -0
- data/spec/dummy/app/models/columns_override_model.rb +5 -0
- data/spec/dummy/app/models/company.rb +15 -0
- data/spec/dummy/app/models/concerns/.keep +0 -0
- data/spec/dummy/app/models/inheriting_taggable_model.rb +4 -0
- data/spec/dummy/app/models/market.rb +2 -0
- data/spec/dummy/app/models/non_standard_id_taggable_model.rb +8 -0
- data/spec/dummy/app/models/ordered_taggable_model.rb +4 -0
- data/spec/dummy/app/models/other_cached_model.rb +3 -0
- data/spec/dummy/app/models/other_taggable_model.rb +4 -0
- data/spec/dummy/app/models/student.rb +4 -0
- data/spec/dummy/app/models/taggable_model.rb +14 -0
- data/spec/dummy/app/models/untaggable_model.rb +3 -0
- data/spec/dummy/app/models/user.rb +3 -0
- data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +33 -0
- data/spec/dummy/config.ru +5 -0
- data/spec/dummy/config/application.rb +19 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/cable.yml +10 -0
- data/spec/dummy/config/credentials.yml.enc +1 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +52 -0
- data/spec/dummy/config/environments/production.rb +105 -0
- data/spec/dummy/config/environments/test.rb +49 -0
- data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cors.rb +16 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +33 -0
- data/spec/dummy/config/master.key +1 -0
- data/spec/dummy/config/puma.rb +38 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/config/spring.rb +6 -0
- data/spec/dummy/config/storage.yml +34 -0
- data/spec/dummy/db/migrate/20201119220853_create_taggable_models.rb +8 -0
- data/spec/dummy/db/migrate/20201119221037_create_columns_override_models.rb +9 -0
- data/spec/dummy/db/migrate/20201119221121_create_non_standard_id_taggable_models.rb +8 -0
- data/spec/dummy/db/migrate/20201119221228_create_untaggable_models.rb +8 -0
- data/spec/dummy/db/migrate/20201119221247_create_cached_models.rb +9 -0
- data/spec/dummy/db/migrate/20201119221314_create_other_cached_models.rb +11 -0
- data/spec/dummy/db/migrate/20201119221343_create_companies.rb +7 -0
- data/spec/dummy/db/migrate/20201119221416_create_users.rb +7 -0
- data/spec/dummy/db/migrate/20201119221434_create_other_taggable_models.rb +8 -0
- data/spec/dummy/db/migrate/20201119221507_create_ordered_taggable_models.rb +8 -0
- data/spec/dummy/db/migrate/20201119221530_create_cache_methods_injected_models.rb +7 -0
- data/spec/dummy/db/migrate/20201119221629_create_other_cached_with_array_models.rb +11 -0
- data/spec/dummy/db/migrate/20201119221746_create_taggable_model_with_jsons.rb +9 -0
- data/spec/dummy/db/migrate/20201119222429_create_make_taggable_tags.make_taggable_engine.rb +11 -0
- data/spec/dummy/db/migrate/20201119222430_create_make_taggable_taggings.make_taggable_engine.rb +13 -0
- data/spec/dummy/db/migrate/20201119222431_add_index_to_tags.make_taggable_engine.rb +6 -0
- data/spec/dummy/db/migrate/20201119222432_add_index_to_taggings.make_taggable_engine.rb +13 -0
- data/spec/dummy/db/schema.rb +117 -0
- data/spec/dummy/db/seeds.rb +7 -0
- data/spec/dummy/lib/tasks/.keep +0 -0
- data/spec/dummy/log/.keep +0 -0
- data/spec/dummy/public/robots.txt +1 -0
- data/spec/dummy/storage/.keep +0 -0
- data/spec/dummy/test/channels/application_cable/connection_test.rb +11 -0
- data/spec/dummy/test/controllers/.keep +0 -0
- data/spec/dummy/test/fixtures/.keep +0 -0
- data/spec/dummy/test/fixtures/files/.keep +0 -0
- data/spec/dummy/test/integration/.keep +0 -0
- data/spec/dummy/test/mailers/.keep +0 -0
- data/spec/dummy/test/models/.keep +0 -0
- data/spec/dummy/test/test_helper.rb +13 -0
- data/spec/dummy/vendor/.keep +0 -0
- data/spec/make_taggable/acts_as_tagger_spec.rb +112 -0
- data/spec/make_taggable/caching_spec.rb +123 -0
- data/spec/make_taggable/default_parser_spec.rb +45 -0
- data/spec/make_taggable/dirty_spec.rb +140 -0
- data/spec/make_taggable/generic_parser_spec.rb +13 -0
- data/spec/make_taggable/make_taggable_spec.rb +260 -0
- data/spec/make_taggable/related_spec.rb +93 -0
- data/spec/make_taggable/single_table_inheritance_spec.rb +220 -0
- data/spec/make_taggable/tag_list_spec.rb +169 -0
- data/spec/make_taggable/tag_spec.rb +297 -0
- data/spec/make_taggable/taggable_spec.rb +804 -0
- data/spec/make_taggable/tagger_spec.rb +149 -0
- data/spec/make_taggable/tagging_spec.rb +115 -0
- data/spec/make_taggable/tags_helper_spec.rb +43 -0
- data/spec/make_taggable/utils_spec.rb +22 -0
- data/spec/make_taggable_spec.rb +5 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/support/array.rb +9 -0
- data/spec/support/helpers.rb +31 -0
- 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
|