acts-as-taggable-on 2.0.0 → 3.2.2
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/.gitignore +11 -0
- data/.travis.yml +23 -0
- data/Appraisals +20 -0
- data/CHANGELOG.md +112 -0
- data/CONTRIBUTING.md +44 -0
- data/Gemfile +9 -5
- data/Guardfile +5 -0
- data/{MIT-LICENSE → LICENSE.md} +1 -1
- data/README.md +385 -0
- data/Rakefile +13 -53
- data/UPGRADING.md +13 -0
- data/acts-as-taggable-on.gemspec +38 -0
- data/{lib/generators/acts_as_taggable_on/migration/templates/active_record/migration.rb → db/migrate/1_acts_as_taggable_on_migration.rb} +5 -3
- data/db/migrate/2_add_missing_unique_indices.rb +19 -0
- data/db/migrate/3_add_taggings_counter_cache_to_tags.rb +14 -0
- data/gemfiles/activerecord_3.2.gemfile +15 -0
- data/gemfiles/activerecord_4.0.gemfile +15 -0
- data/gemfiles/activerecord_4.1.gemfile +15 -0
- data/gemfiles/activerecord_edge.gemfile +16 -0
- data/lib/acts-as-taggable-on.rb +65 -22
- data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +53 -24
- data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +120 -42
- data/lib/acts_as_taggable_on/acts_as_taggable_on/compatibility.rb +35 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +309 -124
- data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +36 -0
- data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +96 -72
- data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +43 -36
- data/lib/acts_as_taggable_on/engine.rb +5 -0
- data/lib/acts_as_taggable_on/tag.rb +133 -63
- data/lib/acts_as_taggable_on/tag_list.rb +162 -81
- data/lib/acts_as_taggable_on/taggable.rb +101 -0
- data/lib/acts_as_taggable_on/{acts_as_tagger.rb → tagger.rb} +34 -13
- data/lib/acts_as_taggable_on/tagging.rb +28 -18
- data/lib/acts_as_taggable_on/tags_helper.rb +12 -14
- data/lib/acts_as_taggable_on/utils.rb +62 -0
- data/lib/acts_as_taggable_on/version.rb +4 -0
- data/lib/acts_as_taggable_on.rb +6 -0
- data/spec/acts_as_taggable_on/acts_as_taggable_on_spec.rb +189 -195
- data/spec/acts_as_taggable_on/acts_as_tagger_spec.rb +78 -81
- data/spec/acts_as_taggable_on/caching_spec.rb +76 -0
- data/spec/acts_as_taggable_on/related_spec.rb +89 -0
- data/spec/acts_as_taggable_on/single_table_inheritance_spec.rb +210 -0
- data/spec/acts_as_taggable_on/tag_list_spec.rb +149 -61
- data/spec/acts_as_taggable_on/tag_spec.rb +245 -65
- data/spec/acts_as_taggable_on/taggable_spec.rb +776 -187
- data/spec/acts_as_taggable_on/tagger_spec.rb +119 -70
- data/spec/acts_as_taggable_on/tagging_spec.rb +17 -21
- data/spec/acts_as_taggable_on/tags_helper_spec.rb +38 -22
- data/spec/acts_as_taggable_on/utils_spec.rb +15 -0
- data/spec/internal/app/models/altered_inheriting_taggable_model.rb +3 -0
- data/spec/internal/app/models/cached_model.rb +3 -0
- data/spec/internal/app/models/cached_model_with_array.rb +5 -0
- data/spec/internal/app/models/company.rb +15 -0
- data/spec/internal/app/models/inheriting_taggable_model.rb +2 -0
- data/spec/internal/app/models/market.rb +2 -0
- data/spec/internal/app/models/models.rb +90 -0
- data/spec/internal/app/models/non_standard_id_taggable_model.rb +8 -0
- data/spec/internal/app/models/ordered_taggable_model.rb +4 -0
- data/spec/internal/app/models/other_cached_model.rb +3 -0
- data/spec/internal/app/models/other_taggable_model.rb +4 -0
- data/spec/internal/app/models/student.rb +2 -0
- data/spec/internal/app/models/taggable_model.rb +13 -0
- data/spec/internal/app/models/untaggable_model.rb +3 -0
- data/spec/internal/app/models/user.rb +3 -0
- data/spec/internal/config/database.yml.sample +19 -0
- data/spec/internal/db/schema.rb +97 -0
- data/spec/schema.rb +57 -18
- data/spec/spec_helper.rb +12 -47
- data/spec/support/array.rb +9 -0
- data/spec/support/database.rb +42 -0
- data/spec/support/database_cleaner.rb +21 -0
- metadata +286 -63
- data/CHANGELOG +0 -25
- data/README.rdoc +0 -221
- data/VERSION +0 -1
- data/generators/acts_as_taggable_on_migration/acts_as_taggable_on_migration_generator.rb +0 -7
- data/generators/acts_as_taggable_on_migration/templates/migration.rb +0 -29
- data/lib/acts_as_taggable_on/acts_as_taggable_on.rb +0 -53
- data/lib/acts_as_taggable_on/compatibility/Gemfile +0 -6
- data/lib/acts_as_taggable_on/compatibility/active_record_backports.rb +0 -17
- data/lib/generators/acts_as_taggable_on/migration/migration_generator.rb +0 -31
- data/rails/init.rb +0 -1
- data/spec/bm.rb +0 -52
- data/spec/models.rb +0 -30
- /data/{spec/spec.opts → .rspec} +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module ActsAsTaggableOn::Compatibility
|
|
2
|
+
def has_many_with_taggable_compatibility(name, options = {}, &extention)
|
|
3
|
+
if ActsAsTaggableOn::Utils.active_record4?
|
|
4
|
+
scope, opts = build_taggable_scope_and_options(options)
|
|
5
|
+
has_many(name, scope, opts, &extention)
|
|
6
|
+
else
|
|
7
|
+
has_many(name, options, &extention)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def build_taggable_scope_and_options(opts)
|
|
12
|
+
scope_opts, opts = parse_taggable_options(opts)
|
|
13
|
+
|
|
14
|
+
unless scope_opts.empty?
|
|
15
|
+
scope = -> {
|
|
16
|
+
scope_opts.inject(self) { |result, hash| result.send(*hash) }
|
|
17
|
+
}
|
|
18
|
+
return [scope, opts]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
[nil, opts]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def parse_taggable_options(opts)
|
|
25
|
+
scope_opts = {}
|
|
26
|
+
[:order, :having, :select, :group, :limit, :offset, :readonly].each do |o|
|
|
27
|
+
scope_opts[o] = opts.delete o if opts[o]
|
|
28
|
+
end
|
|
29
|
+
scope_opts[:where] = opts.delete :conditions if opts[:conditions]
|
|
30
|
+
scope_opts[:joins] = opts.delete :include if opts [:include]
|
|
31
|
+
scope_opts[:distinct] = opts.delete :uniq if opts[:uniq]
|
|
32
|
+
|
|
33
|
+
[scope_opts, opts]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -1,31 +1,43 @@
|
|
|
1
1
|
module ActsAsTaggableOn::Taggable
|
|
2
2
|
module Core
|
|
3
3
|
def self.included(base)
|
|
4
|
-
base.send :include, ActsAsTaggableOn::Taggable::Core::InstanceMethods
|
|
5
4
|
base.extend ActsAsTaggableOn::Taggable::Core::ClassMethods
|
|
6
5
|
|
|
7
6
|
base.class_eval do
|
|
8
7
|
attr_writer :custom_contexts
|
|
9
8
|
after_save :save_tags
|
|
10
9
|
end
|
|
11
|
-
|
|
10
|
+
|
|
12
11
|
base.initialize_acts_as_taggable_on_core
|
|
13
12
|
end
|
|
14
|
-
|
|
13
|
+
|
|
15
14
|
module ClassMethods
|
|
16
15
|
def initialize_acts_as_taggable_on_core
|
|
16
|
+
include taggable_mixin
|
|
17
17
|
tag_types.map(&:to_s).each do |tags_type|
|
|
18
|
-
tag_type
|
|
18
|
+
tag_type = tags_type.to_s.singularize
|
|
19
19
|
context_taggings = "#{tag_type}_taggings".to_sym
|
|
20
|
-
context_tags
|
|
20
|
+
context_tags = tags_type.to_sym
|
|
21
|
+
taggings_order = (preserve_tag_order? ? "#{ActsAsTaggableOn::Tagging.table_name}.id" : [])
|
|
21
22
|
|
|
22
23
|
class_eval do
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
# when preserving tag order, include order option so that for a 'tags' context
|
|
25
|
+
# the associations tag_taggings & tags are always returned in created order
|
|
26
|
+
has_many_with_taggable_compatibility context_taggings, as: :taggable,
|
|
27
|
+
dependent: :destroy,
|
|
28
|
+
class_name: 'ActsAsTaggableOn::Tagging',
|
|
29
|
+
order: taggings_order,
|
|
30
|
+
conditions: ["#{ActsAsTaggableOn::Tagging.table_name}.context = (?)", tags_type],
|
|
31
|
+
include: :tag
|
|
32
|
+
|
|
33
|
+
has_many_with_taggable_compatibility context_tags, through: context_taggings,
|
|
34
|
+
source: :tag,
|
|
35
|
+
class_name: 'ActsAsTaggableOn::Tag',
|
|
36
|
+
order: taggings_order
|
|
37
|
+
|
|
26
38
|
end
|
|
27
39
|
|
|
28
|
-
class_eval
|
|
40
|
+
taggable_mixin.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
29
41
|
def #{tag_type}_list
|
|
30
42
|
tag_list_on('#{tags_type}')
|
|
31
43
|
end
|
|
@@ -37,18 +49,18 @@ module ActsAsTaggableOn::Taggable
|
|
|
37
49
|
def all_#{tags_type}_list
|
|
38
50
|
all_tags_list_on('#{tags_type}')
|
|
39
51
|
end
|
|
40
|
-
|
|
41
|
-
end
|
|
52
|
+
RUBY
|
|
53
|
+
end
|
|
42
54
|
end
|
|
43
|
-
|
|
44
|
-
def
|
|
45
|
-
super(*
|
|
55
|
+
|
|
56
|
+
def taggable_on(preserve_tag_order, *tag_types)
|
|
57
|
+
super(preserve_tag_order, *tag_types)
|
|
46
58
|
initialize_acts_as_taggable_on_core
|
|
47
59
|
end
|
|
48
|
-
|
|
60
|
+
|
|
49
61
|
# all column names are necessary for PostgreSQL group clause
|
|
50
62
|
def grouped_column_names_for(object)
|
|
51
|
-
object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(
|
|
63
|
+
object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(', ')
|
|
52
64
|
end
|
|
53
65
|
|
|
54
66
|
##
|
|
@@ -58,180 +70,353 @@ module ActsAsTaggableOn::Taggable
|
|
|
58
70
|
# @param [Hash] options A hash of options to alter you query:
|
|
59
71
|
# * <tt>:exclude</tt> - if set to true, return objects that are *NOT* tagged with the specified tags
|
|
60
72
|
# * <tt>:any</tt> - if set to true, return objects that are tagged with *ANY* of the specified tags
|
|
73
|
+
# * <tt>:order_by_matching_tag_count</tt> - if set to true and used with :any, sort by objects matching the most tags, descending
|
|
61
74
|
# * <tt>:match_all</tt> - if set to true, return objects that are *ONLY* tagged with the specified tags
|
|
75
|
+
# * <tt>:owned_by</tt> - return objects that are *ONLY* owned by the owner
|
|
62
76
|
#
|
|
63
77
|
# Example:
|
|
64
78
|
# User.tagged_with("awesome", "cool") # Users that are tagged with awesome and cool
|
|
65
79
|
# User.tagged_with("awesome", "cool", :exclude => true) # Users that are not tagged with awesome or cool
|
|
66
80
|
# User.tagged_with("awesome", "cool", :any => true) # Users that are tagged with awesome or cool
|
|
81
|
+
# User.tagged_with("awesome", "cool", :any => true, :order_by_matching_tag_count => true) # Sort by users who match the most tags, descending
|
|
67
82
|
# User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool
|
|
83
|
+
# User.tagged_with("awesome", "cool", :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo'
|
|
68
84
|
def tagged_with(tags, options = {})
|
|
69
|
-
tag_list = TagList.from(tags)
|
|
85
|
+
tag_list = ActsAsTaggableOn::TagList.from(tags)
|
|
86
|
+
empty_result = where('1 = 0')
|
|
70
87
|
|
|
71
|
-
return
|
|
88
|
+
return empty_result if tag_list.empty?
|
|
72
89
|
|
|
73
90
|
joins = []
|
|
74
91
|
conditions = []
|
|
92
|
+
having = []
|
|
93
|
+
select_clause = []
|
|
94
|
+
order_by = []
|
|
75
95
|
|
|
76
96
|
context = options.delete(:on)
|
|
97
|
+
owned_by = options.delete(:owned_by)
|
|
98
|
+
alias_base_name = undecorated_table_name.gsub('.', '_')
|
|
99
|
+
quote = ActsAsTaggableOn::Utils.using_postgresql? ? '"' : ''
|
|
77
100
|
|
|
78
101
|
if options.delete(:exclude)
|
|
79
|
-
|
|
80
|
-
|
|
102
|
+
if options.delete(:wild)
|
|
103
|
+
tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{ActsAsTaggableOn::Utils.like_operator} ? ESCAPE '!'", "%#{ActsAsTaggableOn::Utils.escape_like(t)}%"]) }.join(' OR ')
|
|
104
|
+
else
|
|
105
|
+
tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{ActsAsTaggableOn::Utils.like_operator} ?", t]) }.join(' OR ')
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{ActsAsTaggableOn::Tagging.table_name}.taggable_id FROM #{ActsAsTaggableOn::Tagging.table_name} JOIN #{ActsAsTaggableOn::Tag.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)})"
|
|
109
|
+
|
|
110
|
+
if owned_by
|
|
111
|
+
joins << "JOIN #{ActsAsTaggableOn::Tagging.table_name}" \
|
|
112
|
+
" ON #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" \
|
|
113
|
+
" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)}" \
|
|
114
|
+
" AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = #{quote_value(owned_by.id, nil)}" \
|
|
115
|
+
" AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = #{quote_value(owned_by.class.base_class.to_s, nil)}"
|
|
116
|
+
end
|
|
81
117
|
|
|
82
118
|
elsif options.delete(:any)
|
|
83
|
-
|
|
84
|
-
|
|
119
|
+
# get tags, drop out if nothing returned (we need at least one)
|
|
120
|
+
tags = if options.delete(:wild)
|
|
121
|
+
ActsAsTaggableOn::Tag.named_like_any(tag_list)
|
|
122
|
+
else
|
|
123
|
+
ActsAsTaggableOn::Tag.named_any(tag_list)
|
|
124
|
+
end
|
|
85
125
|
|
|
86
|
-
|
|
87
|
-
tags = Tag.named_any(tag_list)
|
|
88
|
-
return where("1 = 0") unless tags.length == tag_list.length
|
|
126
|
+
return empty_result unless tags.length > 0
|
|
89
127
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
128
|
+
# setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
|
|
129
|
+
# avoid ambiguous column name
|
|
130
|
+
taggings_context = context ? "_#{context}" : ''
|
|
131
|
+
|
|
132
|
+
taggings_alias = adjust_taggings_alias(
|
|
133
|
+
"#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tags.map(&:name).join('_'))}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" \
|
|
137
|
+
" ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" \
|
|
138
|
+
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
|
|
139
|
+
tagging_join << ' AND ' + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
|
140
|
+
|
|
141
|
+
# don't need to sanitize sql, map all ids and join with OR logic
|
|
142
|
+
conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{quote_value(t.id, nil)}" }.join(' OR ')
|
|
143
|
+
select_clause = " #{table_name}.*" unless context and tag_types.one?
|
|
144
|
+
|
|
145
|
+
if owned_by
|
|
146
|
+
tagging_join << ' AND ' +
|
|
147
|
+
sanitize_sql([
|
|
148
|
+
"#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
|
|
149
|
+
owned_by.id,
|
|
150
|
+
owned_by.class.base_class.to_s
|
|
151
|
+
])
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
joins << tagging_join
|
|
155
|
+
group = "#{table_name}.#{primary_key}"
|
|
156
|
+
else
|
|
157
|
+
tags = ActsAsTaggableOn::Tag.named_any(tag_list)
|
|
93
158
|
|
|
94
|
-
|
|
159
|
+
return empty_result unless tags.length == tag_list.length
|
|
95
160
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
161
|
+
tags.each do |tag|
|
|
162
|
+
taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{ActsAsTaggableOn::Utils.sha_prefix(tag.name)}")
|
|
163
|
+
tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" \
|
|
164
|
+
" ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
|
|
165
|
+
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" +
|
|
166
|
+
" AND #{taggings_alias}.tag_id = #{quote_value(tag.id, nil)}"
|
|
167
|
+
|
|
168
|
+
tagging_join << ' AND ' + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
|
169
|
+
|
|
170
|
+
if owned_by
|
|
171
|
+
tagging_join << ' AND ' +
|
|
172
|
+
sanitize_sql([
|
|
173
|
+
"#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
|
|
174
|
+
owned_by.id,
|
|
175
|
+
owned_by.class.base_class.to_s
|
|
176
|
+
])
|
|
177
|
+
end
|
|
101
178
|
|
|
102
179
|
joins << tagging_join
|
|
103
180
|
end
|
|
104
181
|
end
|
|
105
182
|
|
|
106
|
-
|
|
183
|
+
group ||= [] # Rails interprets this as a no-op in the group() call below
|
|
184
|
+
if options.delete(:order_by_matching_tag_count)
|
|
185
|
+
select_clause = "#{table_name}.*, COUNT(#{taggings_alias}.tag_id) AS #{taggings_alias}_count"
|
|
186
|
+
group_columns = ActsAsTaggableOn::Utils.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
|
|
187
|
+
group = group_columns
|
|
188
|
+
order_by << "#{taggings_alias}_count DESC"
|
|
189
|
+
|
|
190
|
+
elsif options.delete(:match_all)
|
|
191
|
+
taggings_alias, _ = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group"
|
|
192
|
+
joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" \
|
|
193
|
+
" ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" \
|
|
194
|
+
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
|
|
107
195
|
|
|
108
|
-
|
|
109
|
-
joins << "LEFT OUTER JOIN #{Tagging.table_name} #{taggings_alias}" +
|
|
110
|
-
" ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
|
|
111
|
-
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name)}"
|
|
196
|
+
joins << ' AND ' + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
|
112
197
|
|
|
113
|
-
|
|
198
|
+
group_columns = ActsAsTaggableOn::Utils.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
|
|
199
|
+
group = group_columns
|
|
200
|
+
having = "COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
|
|
114
201
|
end
|
|
115
202
|
|
|
203
|
+
order_by << options[:order] if options[:order].present?
|
|
116
204
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
205
|
+
select(select_clause)
|
|
206
|
+
.joins(joins.join(' '))
|
|
207
|
+
.where(conditions.join(' AND '))
|
|
208
|
+
.group(group)
|
|
209
|
+
.having(having)
|
|
210
|
+
.order(order_by.join(', '))
|
|
211
|
+
.readonly(false)
|
|
121
212
|
end
|
|
122
213
|
|
|
123
214
|
def is_taggable?
|
|
124
215
|
true
|
|
125
216
|
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
module InstanceMethods
|
|
129
|
-
# all column names are necessary for PostgreSQL group clause
|
|
130
|
-
def grouped_column_names_for(object)
|
|
131
|
-
self.class.grouped_column_names_for(object)
|
|
132
|
-
end
|
|
133
217
|
|
|
134
|
-
def
|
|
135
|
-
|
|
218
|
+
def adjust_taggings_alias(taggings_alias)
|
|
219
|
+
if taggings_alias.size > 75
|
|
220
|
+
taggings_alias = 'taggings_alias_' + Digest::SHA1.hexdigest(taggings_alias)
|
|
221
|
+
end
|
|
222
|
+
taggings_alias
|
|
136
223
|
end
|
|
137
224
|
|
|
138
|
-
def
|
|
139
|
-
|
|
225
|
+
def taggable_mixin
|
|
226
|
+
@taggable_mixin ||= Module.new
|
|
140
227
|
end
|
|
228
|
+
end
|
|
141
229
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
230
|
+
# all column names are necessary for PostgreSQL group clause
|
|
231
|
+
def grouped_column_names_for(object)
|
|
232
|
+
self.class.grouped_column_names_for(object)
|
|
233
|
+
end
|
|
145
234
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
235
|
+
def custom_contexts
|
|
236
|
+
@custom_contexts ||= []
|
|
237
|
+
end
|
|
149
238
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
end
|
|
239
|
+
def is_taggable?
|
|
240
|
+
self.class.is_taggable?
|
|
241
|
+
end
|
|
154
242
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
end
|
|
243
|
+
def add_custom_context(value)
|
|
244
|
+
custom_contexts << value.to_s unless custom_contexts.include?(value.to_s) or self.class.tag_types.map(&:to_s).include?(value.to_s)
|
|
245
|
+
end
|
|
159
246
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
end
|
|
247
|
+
def cached_tag_list_on(context)
|
|
248
|
+
self["cached_#{context.to_s.singularize}_list"]
|
|
249
|
+
end
|
|
164
250
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
251
|
+
def tag_list_cache_set_on(context)
|
|
252
|
+
variable_name = "@#{context.to_s.singularize}_list"
|
|
253
|
+
instance_variable_defined?(variable_name) && instance_variable_get(variable_name)
|
|
254
|
+
end
|
|
168
255
|
|
|
169
|
-
|
|
256
|
+
def tag_list_cache_on(context)
|
|
257
|
+
variable_name = "@#{context.to_s.singularize}_list"
|
|
258
|
+
if instance_variable_get(variable_name)
|
|
259
|
+
instance_variable_get(variable_name)
|
|
260
|
+
elsif cached_tag_list_on(context) && self.class.caching_tag_list_on?(context)
|
|
261
|
+
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(cached_tag_list_on(context)))
|
|
262
|
+
else
|
|
263
|
+
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(tags_on(context).map(&:name)))
|
|
170
264
|
end
|
|
265
|
+
end
|
|
171
266
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
base_tags.where(opts).order("#{Tagging.table_name}.created_at").group("#{Tagging.table_name}.tag_id").all
|
|
177
|
-
end
|
|
267
|
+
def tag_list_on(context)
|
|
268
|
+
add_custom_context(context)
|
|
269
|
+
tag_list_cache_on(context)
|
|
270
|
+
end
|
|
178
271
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
base_tags.where(["#{Tagging.table_name}.context = ? AND #{Tagging.table_name}.tagger_id IS NULL", context.to_s]).all
|
|
183
|
-
end
|
|
272
|
+
def all_tags_list_on(context)
|
|
273
|
+
variable_name = "@all_#{context.to_s.singularize}_list"
|
|
274
|
+
return instance_variable_get(variable_name) if instance_variable_defined?(variable_name) && instance_variable_get(variable_name)
|
|
184
275
|
|
|
185
|
-
|
|
186
|
-
|
|
276
|
+
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(all_tags_on(context).map(&:name)).freeze)
|
|
277
|
+
end
|
|
187
278
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
279
|
+
##
|
|
280
|
+
# Returns all tags of a given context
|
|
281
|
+
def all_tags_on(context)
|
|
282
|
+
tagging_table_name = ActsAsTaggableOn::Tagging.table_name
|
|
283
|
+
|
|
284
|
+
opts = ["#{tagging_table_name}.context = ?", context.to_s]
|
|
285
|
+
scope = base_tags.where(opts)
|
|
191
286
|
|
|
192
|
-
|
|
193
|
-
|
|
287
|
+
if ActsAsTaggableOn::Utils.using_postgresql?
|
|
288
|
+
group_columns = grouped_column_names_for(ActsAsTaggableOn::Tag)
|
|
289
|
+
scope.order("max(#{tagging_table_name}.created_at)").group(group_columns)
|
|
290
|
+
else
|
|
291
|
+
scope.group("#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}")
|
|
292
|
+
end.to_a
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
##
|
|
296
|
+
# Returns all tags that are not owned of a given context
|
|
297
|
+
def tags_on(context)
|
|
298
|
+
scope = base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s])
|
|
299
|
+
# when preserving tag order, return tags in created order
|
|
300
|
+
# if we added the order to the association this would always apply
|
|
301
|
+
scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order?
|
|
302
|
+
scope
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def set_tag_list_on(context, new_list)
|
|
306
|
+
add_custom_context(context)
|
|
307
|
+
|
|
308
|
+
variable_name = "@#{context.to_s.singularize}_list"
|
|
309
|
+
process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)
|
|
310
|
+
|
|
311
|
+
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(new_list))
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def tagging_contexts
|
|
315
|
+
custom_contexts + self.class.tag_types.map(&:to_s)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def process_dirty_object(context, new_list)
|
|
319
|
+
value = new_list.is_a?(Array) ? ActsAsTaggableOn::TagList.new(new_list) : new_list
|
|
320
|
+
attrib = "#{context.to_s.singularize}_list"
|
|
321
|
+
|
|
322
|
+
if changed_attributes.include?(attrib)
|
|
323
|
+
# The attribute already has an unsaved change.
|
|
324
|
+
old = changed_attributes[attrib]
|
|
325
|
+
changed_attributes.delete(attrib) if old.to_s == value.to_s
|
|
326
|
+
else
|
|
327
|
+
old = tag_list_on(context).to_s
|
|
328
|
+
changed_attributes[attrib] = old if old.to_s != value.to_s
|
|
194
329
|
end
|
|
330
|
+
end
|
|
195
331
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
super
|
|
332
|
+
def reload(*args)
|
|
333
|
+
self.class.tag_types.each do |context|
|
|
334
|
+
instance_variable_set("@#{context.to_s.singularize}_list", nil)
|
|
335
|
+
instance_variable_set("@all_#{context.to_s.singularize}_list", nil)
|
|
203
336
|
end
|
|
204
337
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
next unless tag_list_cache_set_on(context)
|
|
338
|
+
super(*args)
|
|
339
|
+
end
|
|
208
340
|
|
|
209
|
-
|
|
341
|
+
##
|
|
342
|
+
# Find existing tags or create non-existing tags
|
|
343
|
+
def load_tags(tag_list)
|
|
344
|
+
ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
|
|
345
|
+
end
|
|
210
346
|
|
|
211
|
-
|
|
212
|
-
|
|
347
|
+
def save_tags
|
|
348
|
+
tagging_contexts.each do |context|
|
|
349
|
+
next unless tag_list_cache_set_on(context)
|
|
350
|
+
# List of currently assigned tag names
|
|
351
|
+
tag_list = tag_list_cache_on(context).uniq
|
|
213
352
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
new_tags = tag_list - current_tags
|
|
217
|
-
|
|
218
|
-
# Find taggings to remove:
|
|
219
|
-
old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil,
|
|
220
|
-
:context => context.to_s, :tag_id => old_tags).all
|
|
353
|
+
# Find existing tags or create non-existing tags:
|
|
354
|
+
tags = find_or_create_tags_from_list_with_context(tag_list, context)
|
|
221
355
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
356
|
+
# Tag objects for currently assigned tags
|
|
357
|
+
current_tags = tags_on(context)
|
|
358
|
+
|
|
359
|
+
# Tag maintenance based on whether preserving the created order of tags
|
|
360
|
+
if self.class.preserve_tag_order?
|
|
361
|
+
old_tags, new_tags = current_tags - tags, tags - current_tags
|
|
362
|
+
|
|
363
|
+
shared_tags = current_tags & tags
|
|
364
|
+
|
|
365
|
+
if shared_tags.any? && tags[0...shared_tags.size] != shared_tags
|
|
366
|
+
index = shared_tags.each_with_index { |_, i| break i unless shared_tags[i] == tags[i] }
|
|
226
367
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
368
|
+
# Update arrays of tag objects
|
|
369
|
+
old_tags |= current_tags[index...current_tags.size]
|
|
370
|
+
new_tags |= current_tags[index...current_tags.size] & shared_tags
|
|
371
|
+
|
|
372
|
+
# Order the array of tag objects to match the tag list
|
|
373
|
+
new_tags = tags.map do |t|
|
|
374
|
+
new_tags.find { |n| n.name.downcase == t.name.downcase }
|
|
375
|
+
end.compact
|
|
230
376
|
end
|
|
377
|
+
else
|
|
378
|
+
# Delete discarded tags and create new tags
|
|
379
|
+
old_tags = current_tags - tags
|
|
380
|
+
new_tags = tags - current_tags
|
|
231
381
|
end
|
|
232
382
|
|
|
233
|
-
|
|
383
|
+
# Destroy old taggings:
|
|
384
|
+
if old_tags.present?
|
|
385
|
+
ActsAsTaggableOn::Tagging.destroy_all(tagger_type: nil, tagger_id: nil, context: context.to_s, tag_id: old_tags)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Create new taggings:
|
|
389
|
+
new_tags.each do |tag|
|
|
390
|
+
taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self)
|
|
391
|
+
end
|
|
234
392
|
end
|
|
393
|
+
|
|
394
|
+
true
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
private
|
|
398
|
+
|
|
399
|
+
##
|
|
400
|
+
# Override this hook if you wish to subclass {ActsAsTaggableOn::Tag} --
|
|
401
|
+
# context is provided so that you may conditionally use a Tag subclass
|
|
402
|
+
# only for some contexts.
|
|
403
|
+
#
|
|
404
|
+
# @example Custom Tag class for one context
|
|
405
|
+
# class Company < ActiveRecord::Base
|
|
406
|
+
# acts_as_taggable_on :markets, :locations
|
|
407
|
+
#
|
|
408
|
+
# def find_or_create_tags_from_list_with_context(tag_list, context)
|
|
409
|
+
# if context.to_sym == :markets
|
|
410
|
+
# MarketTag.find_or_create_all_with_like_by_name(tag_list)
|
|
411
|
+
# else
|
|
412
|
+
# super
|
|
413
|
+
# end
|
|
414
|
+
# end
|
|
415
|
+
#
|
|
416
|
+
# @param [Array<String>] tag_list Tags to find or create
|
|
417
|
+
# @param [Symbol] context The tag context for the tag_list
|
|
418
|
+
def find_or_create_tags_from_list_with_context(tag_list, _context)
|
|
419
|
+
load_tags(tag_list)
|
|
235
420
|
end
|
|
236
421
|
end
|
|
237
|
-
end
|
|
422
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module ActsAsTaggableOn::Taggable
|
|
2
|
+
module Dirty
|
|
3
|
+
def self.included(base)
|
|
4
|
+
base.extend ActsAsTaggableOn::Taggable::Dirty::ClassMethods
|
|
5
|
+
|
|
6
|
+
base.initialize_acts_as_taggable_on_dirty
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
def initialize_acts_as_taggable_on_dirty
|
|
11
|
+
tag_types.map(&:to_s).each do |tags_type|
|
|
12
|
+
tag_type = tags_type.to_s.singularize
|
|
13
|
+
|
|
14
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
15
|
+
def #{tag_type}_list_changed?
|
|
16
|
+
changed_attributes.include?("#{tag_type}_list")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def #{tag_type}_list_was
|
|
20
|
+
changed_attributes.include?("#{tag_type}_list") ? changed_attributes["#{tag_type}_list"] : __send__("#{tag_type}_list")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def #{tag_type}_list_change
|
|
24
|
+
[changed_attributes['#{tag_type}_list'], __send__('#{tag_type}_list')] if changed_attributes.include?("#{tag_type}_list")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def #{tag_type}_list_changes
|
|
28
|
+
[changed_attributes['#{tag_type}_list'], __send__('#{tag_type}_list')] if changed_attributes.include?("#{tag_type}_list")
|
|
29
|
+
end
|
|
30
|
+
RUBY
|
|
31
|
+
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|