acts-as-taggable-on 3.0.2 → 3.1.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/CHANGELOG.md +12 -5
- data/acts-as-taggable-on.gemspec +3 -1
- data/db/migrate/2_add_missing_unique_indices.rb +1 -1
- data/lib/acts-as-taggable-on.rb +26 -16
- data/lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb +2 -2
- data/lib/acts_as_taggable_on/acts_as_taggable_on/collection.rb +2 -5
- data/lib/acts_as_taggable_on/acts_as_taggable_on/compatibility.rb +1 -1
- data/lib/acts_as_taggable_on/acts_as_taggable_on/core.rb +155 -148
- data/lib/acts_as_taggable_on/acts_as_taggable_on/dirty.rb +0 -1
- data/lib/acts_as_taggable_on/acts_as_taggable_on/ownership.rb +97 -100
- data/lib/acts_as_taggable_on/acts_as_taggable_on/related.rb +38 -45
- data/lib/acts_as_taggable_on/tag.rb +1 -1
- data/lib/acts_as_taggable_on/taggable.rb +1 -1
- data/lib/acts_as_taggable_on/utils.rb +15 -23
- data/lib/acts_as_taggable_on/version.rb +1 -1
- data/spec/acts_as_taggable_on/taggable_spec.rb +19 -8
- metadata +46 -6
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
NWU0YmVhNGFlMjYzYTg1NDdiNTRkODQ5ODViYjRmNGI3ZjNiMjZiZA==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
YTQ0MDhmYzEyMDA5Yzc5MTQ5MjgxODY1NWQ4YzQ1ZGI1YmYwYTg1Ng==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
OGU1OTU3ZjFkMDc1ZDAzNGY1YzI4Yzk1ODFlYTE1YWNjZThmMDcyYTBhMDMz
|
10
|
+
MzdiYWUxYjgxNDhiYzQ4ZmI5ZTgyMjIwMWZkYmI3ZTc2ZWM4YmUyNzUxZDYx
|
11
|
+
ZWJhNmU1ZDRiOWU2ZGZiM2YyMTAwMTc1YmZmYWYzYzdkMTg4YzY=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
YjU3MzdjMTIyNDM2ZDY2ZmNhZjUxZDhlMTgwYTM3NjFiY2E2Mzg5NDRmZDNi
|
14
|
+
ZTU1NTQ1MjAwMzIwNDVjNzEyYmNlZTU1MmNkYTk3OGY2ZjhkNTA2M2M2ZWVj
|
15
|
+
ZDQzODA0OTMxMTMxNDMzNTY5ZTk3NWE1MzgxZDM3ZjgwMDBjMTA=
|
data/CHANGELOG.md
CHANGED
@@ -4,25 +4,32 @@ Each change should fall into categories that would affect whether the release is
|
|
4
4
|
|
5
5
|
As such, a _Feature_ would map to either major or minor. A _bug fix_ to a patch. And _misc_ is either minor or patch, the difference being kind of fuzzy for the purposes of history. Adding tests would be patch level.
|
6
6
|
|
7
|
-
### Master [changes](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.0.
|
7
|
+
### Master [changes](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.0.1...master)
|
8
8
|
|
9
9
|
* Breaking Changes
|
10
10
|
* Features
|
11
|
+
* Fixes
|
12
|
+
* Misc
|
13
|
+
|
14
|
+
### [3.1.0.rc1 / 2014-02-26](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.0.1...v3.1.0.rc1)
|
15
|
+
|
16
|
+
* Features
|
17
|
+
* [@jamesburke-examtime #467 Add :order_by_matching_tag_count option](https://github.com/mbleigh/acts-as-taggable-on/pull/469)
|
11
18
|
* Fixes
|
12
19
|
* [@rafael #406 Dirty attributes not correctly derived](https://github.com/mbleigh/acts-as-taggable-on/pull/406)
|
13
20
|
* [@bzbnhang #440 Did not respect strict_case_match](https://github.com/mbleigh/acts-as-taggable-on/pull/440)
|
14
21
|
* [@znz #456 Fix breaking encoding of tag](https://github.com/mbleigh/acts-as-taggable-on/pull/456)
|
22
|
+
* [@rgould #417 Let '.count' work when tagged_with is accompanied by a group clause](https://github.com/mbleigh/acts-as-taggable-on/pull/417)
|
23
|
+
* [@developer88 #461 Move 'Distinct' out of select string and use .uniq instead](https://github.com/mbleigh/acts-as-taggable-on/pull/461)
|
24
|
+
* [@gerard-leijdekkers #473 Fixed down migration index name](https://github.com/mbleigh/acts-as-taggable-on/pull/473)
|
15
25
|
* Misc
|
26
|
+
* [@billychan #463 Thread safe support](https://github.com/mbleigh/acts-as-taggable-on/pull/463)
|
16
27
|
* [@billychan #386 Add parse:true instructions to README](https://github.com/mbleigh/acts-as-taggable-on/pull/386)
|
17
28
|
* [@seuros #449 Improve README/UPGRADING/post install docs](https://github.com/mbleigh/acts-as-taggable-on/pull/449)
|
18
29
|
* [@seuros #452 Remove I18n deprecation warning in specs](https://github.com/mbleigh/acts-as-taggable-on/pull/452)
|
19
30
|
* [@seuros #453 Test against Ruby 2.1 on Travis CI](https://github.com/mbleigh/acts-as-taggable-on/pull/453)
|
20
31
|
* [@takashi #454 Clarify example in docs](https://github.com/mbleigh/acts-as-taggable-on/pull/454)
|
21
32
|
|
22
|
-
### [3.0.2 / 2014-03-12](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.0.1...v3.0.2)
|
23
|
-
|
24
|
-
* [@mikehale #486 Match_all respects context. Backport of #487](https://github.com/mbleigh/acts-as-taggable-on/pull/486)
|
25
|
-
|
26
33
|
### [3.0.1 / 2014-01-08](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.0.0...v3.0.1)
|
27
34
|
|
28
35
|
* Fixes
|
data/acts-as-taggable-on.gemspec
CHANGED
@@ -22,7 +22,9 @@ Gem::Specification.new do |gem|
|
|
22
22
|
gem.post_install_message = File.read('UPGRADING.md')
|
23
23
|
end
|
24
24
|
|
25
|
-
gem.add_runtime_dependency '
|
25
|
+
gem.add_runtime_dependency 'activerecord', ['>= 3', '< 5']
|
26
|
+
gem.add_runtime_dependency 'activesupport', ['>= 3', '< 5']
|
27
|
+
gem.add_runtime_dependency 'actionpack', ['>= 3', '< 5']
|
26
28
|
|
27
29
|
gem.add_development_dependency 'sqlite3'
|
28
30
|
gem.add_development_dependency 'mysql2', '~> 0.3.7'
|
@@ -13,7 +13,7 @@ class AddMissingUniqueIndices < ActiveRecord::Migration
|
|
13
13
|
def self.down
|
14
14
|
remove_index :tags, :name
|
15
15
|
|
16
|
-
remove_index :taggings, name: '
|
16
|
+
remove_index :taggings, name: 'taggings_idx'
|
17
17
|
add_index :taggings, :tag_id
|
18
18
|
add_index :taggings, [:taggable_id, :taggable_type, :context]
|
19
19
|
end
|
data/lib/acts-as-taggable-on.rb
CHANGED
@@ -2,34 +2,44 @@ require "active_record"
|
|
2
2
|
require "active_record/version"
|
3
3
|
require "active_support/core_ext/module"
|
4
4
|
require "action_view"
|
5
|
-
require 'active_support/all'
|
6
5
|
|
7
6
|
require "digest/sha1"
|
8
7
|
|
9
8
|
module ActsAsTaggableOn
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
@@force_lowercase = false
|
15
|
-
|
16
|
-
mattr_accessor :force_parameterize
|
17
|
-
@@force_parameterize = false
|
9
|
+
def self.setup
|
10
|
+
@configuration ||= Configuration.new
|
11
|
+
yield @configuration if block_given?
|
12
|
+
end
|
18
13
|
|
19
|
-
|
20
|
-
|
14
|
+
def self.method_missing(method_name, *args, &block)
|
15
|
+
@configuration.respond_to?(method_name) ?
|
16
|
+
@configuration.send(method_name, *args, &block) : super
|
17
|
+
end
|
21
18
|
|
22
|
-
|
23
|
-
|
19
|
+
def self.respond_to?(method_name, include_private=false)
|
20
|
+
@configuration.respond_to? method_name
|
21
|
+
end
|
24
22
|
|
25
23
|
def self.glue
|
26
|
-
|
24
|
+
setting = @configuration.delimiter
|
25
|
+
delimiter = setting.kind_of?(Array) ? setting[0] : setting
|
27
26
|
delimiter.ends_with?(" ") ? delimiter : "#{delimiter} "
|
28
27
|
end
|
29
28
|
|
30
|
-
|
31
|
-
|
29
|
+
class Configuration
|
30
|
+
attr_accessor :delimiter, :force_lowercase, :force_parameterize,
|
31
|
+
:strict_case_match, :remove_unused_tags
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
@delimiter = ','
|
35
|
+
@force_lowercase = false
|
36
|
+
@force_parameterize = false
|
37
|
+
@strict_case_match = false
|
38
|
+
@remove_unused_tags = false
|
39
|
+
end
|
32
40
|
end
|
41
|
+
|
42
|
+
setup
|
33
43
|
end
|
34
44
|
|
35
45
|
|
@@ -7,9 +7,9 @@ module ActsAsTaggableOn::Taggable
|
|
7
7
|
# @private
|
8
8
|
def _has_tags_cache_columns?(db_columns)
|
9
9
|
db_column_names = db_columns.map(&:name)
|
10
|
-
tag_types.any?
|
10
|
+
tag_types.any? do |context|
|
11
11
|
db_column_names.include?("cached_#{context.to_s.singularize}_list")
|
12
|
-
|
12
|
+
end
|
13
13
|
end
|
14
14
|
|
15
15
|
# @private
|
@@ -1,7 +1,6 @@
|
|
1
1
|
module ActsAsTaggableOn::Taggable
|
2
2
|
module Collection
|
3
3
|
def self.included(base)
|
4
|
-
base.send :include, ActsAsTaggableOn::Taggable::Collection::InstanceMethods
|
5
4
|
base.extend ActsAsTaggableOn::Taggable::Collection::ClassMethods
|
6
5
|
base.initialize_acts_as_taggable_on_collection
|
7
6
|
end
|
@@ -178,10 +177,8 @@ module ActsAsTaggableOn::Taggable
|
|
178
177
|
end
|
179
178
|
end
|
180
179
|
|
181
|
-
|
182
|
-
|
183
|
-
self.class.tag_counts_on(context, options.merge(:id => id))
|
184
|
-
end
|
180
|
+
def tag_counts_on(context, options={})
|
181
|
+
self.class.tag_counts_on(context, options.merge(:id => id))
|
185
182
|
end
|
186
183
|
|
187
184
|
module CalculationMethods
|
@@ -1,7 +1,6 @@
|
|
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
|
@@ -72,6 +71,7 @@ module ActsAsTaggableOn::Taggable
|
|
72
71
|
# @param [Hash] options A hash of options to alter you query:
|
73
72
|
# * <tt>:exclude</tt> - if set to true, return objects that are *NOT* tagged with the specified tags
|
74
73
|
# * <tt>:any</tt> - if set to true, return objects that are tagged with *ANY* of the specified tags
|
74
|
+
# * <tt>:order_by_matching_tag_count</tt> - if set to true and used with :any, sort by objects matching the most tags, descending
|
75
75
|
# * <tt>:match_all</tt> - if set to true, return objects that are *ONLY* tagged with the specified tags
|
76
76
|
# * <tt>:owned_by</tt> - return objects that are *ONLY* owned by the owner
|
77
77
|
#
|
@@ -79,6 +79,7 @@ module ActsAsTaggableOn::Taggable
|
|
79
79
|
# User.tagged_with("awesome", "cool") # Users that are tagged with awesome and cool
|
80
80
|
# User.tagged_with("awesome", "cool", :exclude => true) # Users that are not tagged with awesome or cool
|
81
81
|
# User.tagged_with("awesome", "cool", :any => true) # Users that are tagged with awesome or cool
|
82
|
+
# User.tagged_with("awesome", "cool", :any => true, :order_by_matching_tag_count => true) # Sort by users who match the most tags, descending
|
82
83
|
# User.tagged_with("awesome", "cool", :match_all => true) # Users that are tagged with just awesome and cool
|
83
84
|
# User.tagged_with("awesome", "cool", :owned_by => foo ) # Users that are tagged with just awesome and cool by 'foo'
|
84
85
|
def tagged_with(tags, options = {})
|
@@ -91,6 +92,7 @@ module ActsAsTaggableOn::Taggable
|
|
91
92
|
conditions = []
|
92
93
|
having = []
|
93
94
|
select_clause = []
|
95
|
+
order_by = []
|
94
96
|
|
95
97
|
context = options.delete(:on)
|
96
98
|
owned_by = options.delete(:owned_by)
|
@@ -139,7 +141,7 @@ module ActsAsTaggableOn::Taggable
|
|
139
141
|
|
140
142
|
# don't need to sanitize sql, map all ids and join with OR logic
|
141
143
|
conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{quote_value(t.id, nil)}" }.join(" OR ")
|
142
|
-
select_clause = "
|
144
|
+
select_clause = " #{table_name}.*" unless context and tag_types.one?
|
143
145
|
|
144
146
|
if owned_by
|
145
147
|
tagging_join << " AND " +
|
@@ -178,27 +180,35 @@ module ActsAsTaggableOn::Taggable
|
|
178
180
|
end
|
179
181
|
end
|
180
182
|
|
181
|
-
|
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::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
|
187
|
+
group = group_columns
|
188
|
+
order_by << "#{taggings_alias}_count DESC"
|
182
189
|
|
183
|
-
|
190
|
+
elsif options.delete(:match_all)
|
191
|
+
taggings_alias, _ = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group"
|
184
192
|
joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
|
185
193
|
" ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
|
186
194
|
" AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
|
187
195
|
|
188
|
-
joins << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
|
189
|
-
|
190
196
|
group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
|
191
197
|
group = group_columns
|
192
198
|
having = "COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
|
193
199
|
end
|
194
200
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
.
|
199
|
-
.
|
200
|
-
|
201
|
-
|
201
|
+
order_by << options[:order] if options[:order].present?
|
202
|
+
|
203
|
+
request = select(select_clause).
|
204
|
+
joins(joins.join(" ")).
|
205
|
+
where(conditions.join(" AND ")).
|
206
|
+
group(group).
|
207
|
+
having(having).
|
208
|
+
order(order_by.join(", ")).
|
209
|
+
readonly(false)
|
210
|
+
|
211
|
+
((context and tag_types.one?) && options.delete(:any)) ? request : request.uniq
|
202
212
|
end
|
203
213
|
|
204
214
|
def is_taggable?
|
@@ -217,179 +227,176 @@ module ActsAsTaggableOn::Taggable
|
|
217
227
|
end
|
218
228
|
end
|
219
229
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
end
|
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
|
225
234
|
|
226
|
-
|
227
|
-
|
228
|
-
|
235
|
+
def custom_contexts
|
236
|
+
@custom_contexts ||= []
|
237
|
+
end
|
229
238
|
|
230
|
-
|
231
|
-
|
232
|
-
|
239
|
+
def is_taggable?
|
240
|
+
self.class.is_taggable?
|
241
|
+
end
|
233
242
|
|
234
|
-
|
235
|
-
|
236
|
-
|
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
|
237
246
|
|
238
|
-
|
239
|
-
|
240
|
-
|
247
|
+
def cached_tag_list_on(context)
|
248
|
+
self["cached_#{context.to_s.singularize}_list"]
|
249
|
+
end
|
241
250
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
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).nil?
|
254
|
+
end
|
246
255
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
end
|
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)))
|
256
264
|
end
|
265
|
+
end
|
257
266
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
267
|
+
def tag_list_on(context)
|
268
|
+
add_custom_context(context)
|
269
|
+
tag_list_cache_on(context)
|
270
|
+
end
|
262
271
|
|
263
|
-
|
264
|
-
|
265
|
-
|
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)
|
266
275
|
|
267
|
-
|
268
|
-
|
276
|
+
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.new(all_tags_on(context).map(&:name)).freeze)
|
277
|
+
end
|
269
278
|
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
tagging_table_name = ActsAsTaggableOn::Tagging.table_name
|
279
|
+
##
|
280
|
+
# Returns all tags of a given context
|
281
|
+
def all_tags_on(context)
|
282
|
+
tagging_table_name = ActsAsTaggableOn::Tagging.table_name
|
275
283
|
|
276
|
-
|
277
|
-
|
284
|
+
opts = ["#{tagging_table_name}.context = ?", context.to_s]
|
285
|
+
scope = base_tags.where(opts)
|
278
286
|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
##
|
288
|
-
# Returns all tags that are not owned of a given context
|
289
|
-
def tags_on(context)
|
290
|
-
scope = base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s])
|
291
|
-
# when preserving tag order, return tags in created order
|
292
|
-
# if we added the order to the association this would always apply
|
293
|
-
scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order?
|
294
|
-
scope
|
295
|
-
end
|
287
|
+
if ActsAsTaggableOn::Tag.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
|
296
294
|
|
297
|
-
|
298
|
-
|
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
|
299
304
|
|
300
|
-
|
301
|
-
|
305
|
+
def set_tag_list_on(context, new_list)
|
306
|
+
add_custom_context(context)
|
302
307
|
|
303
|
-
|
304
|
-
|
308
|
+
variable_name = "@#{context.to_s.singularize}_list"
|
309
|
+
process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)
|
305
310
|
|
306
|
-
|
307
|
-
|
308
|
-
end
|
311
|
+
instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(new_list))
|
312
|
+
end
|
309
313
|
|
310
|
-
|
311
|
-
|
312
|
-
|
314
|
+
def tagging_contexts
|
315
|
+
custom_contexts + self.class.tag_types.map(&:to_s)
|
316
|
+
end
|
313
317
|
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
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)
|
322
329
|
end
|
330
|
+
end
|
323
331
|
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
end
|
329
|
-
|
330
|
-
super(*args)
|
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)
|
331
336
|
end
|
332
337
|
|
333
|
-
|
334
|
-
|
335
|
-
def load_tags(tag_list)
|
336
|
-
ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
|
337
|
-
end
|
338
|
+
super(*args)
|
339
|
+
end
|
338
340
|
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
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
|
344
346
|
|
345
|
-
|
346
|
-
|
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
|
347
352
|
|
348
|
-
|
349
|
-
|
353
|
+
# Find existing tags or create non-existing tags:
|
354
|
+
tags = load_tags(tag_list)
|
350
355
|
|
351
|
-
|
352
|
-
|
353
|
-
old_tags, new_tags = current_tags - tags, tags - current_tags
|
356
|
+
# Tag objects for currently assigned tags
|
357
|
+
current_tags = tags_on(context)
|
354
358
|
|
355
|
-
|
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
|
356
362
|
|
357
|
-
|
358
|
-
index = shared_tags.each_with_index { |_, i| break i unless shared_tags[i] == tags[i] }
|
363
|
+
shared_tags = current_tags & tags
|
359
364
|
|
360
|
-
|
361
|
-
|
362
|
-
new_tags |= current_tags[index...current_tags.size] & shared_tags
|
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] }
|
363
367
|
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
end.compact
|
368
|
-
end
|
369
|
-
else
|
370
|
-
# Delete discarded tags and create new tags
|
371
|
-
old_tags = current_tags - tags
|
372
|
-
new_tags = tags - current_tags
|
373
|
-
end
|
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
|
374
371
|
|
375
|
-
|
376
|
-
|
377
|
-
|
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
|
378
376
|
end
|
377
|
+
else
|
378
|
+
# Delete discarded tags and create new tags
|
379
|
+
old_tags = current_tags - tags
|
380
|
+
new_tags = tags - current_tags
|
381
|
+
end
|
379
382
|
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
383
|
+
# Find taggings to remove:
|
384
|
+
if old_tags.present?
|
385
|
+
old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil, :context => context.to_s, :tag_id => old_tags)
|
386
|
+
end
|
384
387
|
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
end
|
388
|
+
# Destroy old taggings:
|
389
|
+
if old_taggings.present?
|
390
|
+
ActsAsTaggableOn::Tagging.destroy_all "#{ActsAsTaggableOn::Tagging.primary_key}".to_sym => old_taggings.map(&:id)
|
389
391
|
end
|
390
392
|
|
391
|
-
|
393
|
+
# Create new taggings:
|
394
|
+
new_tags.each do |tag|
|
395
|
+
taggings.create!(:tag_id => tag.id, :context => context.to_s, :taggable => self)
|
396
|
+
end
|
392
397
|
end
|
398
|
+
|
399
|
+
true
|
393
400
|
end
|
394
401
|
end
|
395
402
|
end
|
@@ -10,7 +10,6 @@ module ActsAsTaggableOn::Taggable
|
|
10
10
|
def initialize_acts_as_taggable_on_dirty
|
11
11
|
tag_types.map(&:to_s).each do |tags_type|
|
12
12
|
tag_type = tags_type.to_s.singularize
|
13
|
-
context_tags = tags_type.to_sym
|
14
13
|
|
15
14
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
16
15
|
def #{tag_type}_list_changed?
|
@@ -1,135 +1,132 @@
|
|
1
1
|
module ActsAsTaggableOn::Taggable
|
2
2
|
module Ownership
|
3
3
|
def self.included(base)
|
4
|
-
base.send :include, ActsAsTaggableOn::Taggable::Ownership::InstanceMethods
|
5
4
|
base.extend ActsAsTaggableOn::Taggable::Ownership::ClassMethods
|
6
|
-
|
5
|
+
|
7
6
|
base.class_eval do
|
8
|
-
after_save :save_owned_tags
|
7
|
+
after_save :save_owned_tags
|
9
8
|
end
|
10
|
-
|
9
|
+
|
11
10
|
base.initialize_acts_as_taggable_on_ownership
|
12
11
|
end
|
13
|
-
|
12
|
+
|
14
13
|
module ClassMethods
|
15
14
|
def acts_as_taggable_on(*args)
|
16
15
|
initialize_acts_as_taggable_on_ownership
|
17
16
|
super(*args)
|
18
17
|
end
|
19
|
-
|
20
|
-
def initialize_acts_as_taggable_on_ownership
|
18
|
+
|
19
|
+
def initialize_acts_as_taggable_on_ownership
|
21
20
|
tag_types.map(&:to_s).each do |tag_type|
|
22
21
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
23
22
|
def #{tag_type}_from(owner)
|
24
23
|
owner_tag_list_on(owner, '#{tag_type}')
|
25
|
-
end
|
24
|
+
end
|
26
25
|
RUBY
|
27
|
-
end
|
26
|
+
end
|
28
27
|
end
|
29
28
|
end
|
30
|
-
|
31
|
-
module InstanceMethods
|
32
|
-
def owner_tags_on(owner, context)
|
33
|
-
if owner.nil?
|
34
|
-
scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s])
|
35
|
-
else
|
36
|
-
scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
|
37
|
-
#{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
|
38
|
-
#{ActsAsTaggableOn::Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.base_class.to_s])
|
39
|
-
end
|
40
29
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
30
|
+
def owner_tags_on(owner, context)
|
31
|
+
if owner.nil?
|
32
|
+
scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s])
|
33
|
+
else
|
34
|
+
scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
|
35
|
+
#{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
|
36
|
+
#{ActsAsTaggableOn::Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.base_class.to_s])
|
48
37
|
end
|
49
38
|
|
50
|
-
|
51
|
-
|
52
|
-
|
39
|
+
# when preserving tag order, return tags in created order
|
40
|
+
# if we added the order to the association this would always apply
|
41
|
+
if self.class.preserve_tag_order?
|
42
|
+
scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id")
|
43
|
+
else
|
44
|
+
scope
|
53
45
|
end
|
54
|
-
|
55
|
-
def owner_tag_list_on(owner, context)
|
56
|
-
add_custom_context(context)
|
46
|
+
end
|
57
47
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
def set_owner_tag_list_on(owner, context, new_list)
|
64
|
-
add_custom_context(context)
|
65
|
-
|
66
|
-
cache = cached_owned_tag_list_on(context)
|
48
|
+
def cached_owned_tag_list_on(context)
|
49
|
+
variable_name = "@owned_#{context}_list"
|
50
|
+
(instance_variable_defined?(variable_name) && instance_variable_get(variable_name)) || instance_variable_set(variable_name, {})
|
51
|
+
end
|
67
52
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
53
|
+
def owner_tag_list_on(owner, context)
|
54
|
+
add_custom_context(context)
|
55
|
+
|
56
|
+
cache = cached_owned_tag_list_on(context)
|
57
|
+
|
58
|
+
cache[owner] ||= ActsAsTaggableOn::TagList.new(*owner_tags_on(owner, context).map(&:name))
|
59
|
+
end
|
60
|
+
|
61
|
+
def set_owner_tag_list_on(owner, context, new_list)
|
62
|
+
add_custom_context(context)
|
63
|
+
|
64
|
+
cache = cached_owned_tag_list_on(context)
|
65
|
+
|
66
|
+
cache[owner] = ActsAsTaggableOn::TagList.from(new_list)
|
67
|
+
end
|
68
|
+
|
69
|
+
def reload(*args)
|
70
|
+
self.class.tag_types.each do |context|
|
71
|
+
instance_variable_set("@owned_#{context}_list", nil)
|
77
72
|
end
|
78
|
-
|
79
|
-
def save_owned_tags
|
80
|
-
tagging_contexts.each do |context|
|
81
|
-
cached_owned_tag_list_on(context).each do |owner, tag_list|
|
82
|
-
|
83
|
-
# Find existing tags or create non-existing tags:
|
84
|
-
tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
|
85
|
-
|
86
|
-
# Tag objects for owned tags
|
87
|
-
owned_tags = owner_tags_on(owner, context)
|
88
|
-
|
89
|
-
# Tag maintenance based on whether preserving the created order of tags
|
90
|
-
if self.class.preserve_tag_order?
|
91
|
-
old_tags, new_tags = owned_tags - tags, tags - owned_tags
|
92
|
-
|
93
|
-
shared_tags = owned_tags & tags
|
94
|
-
|
95
|
-
if shared_tags.any? && tags[0...shared_tags.size] != shared_tags
|
96
|
-
index = shared_tags.each_with_index { |_, i| break i unless shared_tags[i] == tags[i] }
|
97
|
-
|
98
|
-
# Update arrays of tag objects
|
99
|
-
old_tags |= owned_tags.from(index)
|
100
|
-
new_tags |= owned_tags.from(index) & shared_tags
|
101
|
-
|
102
|
-
# Order the array of tag objects to match the tag list
|
103
|
-
new_tags = tags.map { |t| new_tags.find { |n| n.name.downcase == t.name.downcase } }.compact
|
104
|
-
end
|
105
|
-
else
|
106
|
-
# Delete discarded tags and create new tags
|
107
|
-
old_tags = owned_tags - tags
|
108
|
-
new_tags = tags - owned_tags
|
109
|
-
end
|
110
|
-
|
111
|
-
# Find all taggings that belong to the taggable (self), are owned by the owner,
|
112
|
-
# have the correct context, and are removed from the list.
|
113
|
-
if old_tags.present?
|
114
|
-
old_taggings = ActsAsTaggableOn::Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
|
115
|
-
:tagger_type => owner.class.base_class.to_s, :tagger_id => owner.id,
|
116
|
-
:tag_id => old_tags, :context => context)
|
117
|
-
end
|
118
|
-
|
119
|
-
# Destroy old taggings:
|
120
|
-
if old_taggings.present?
|
121
|
-
ActsAsTaggableOn::Tagging.destroy_all(:id => old_taggings.map(&:id))
|
122
|
-
end
|
123
73
|
|
124
|
-
|
125
|
-
|
126
|
-
|
74
|
+
super(*args)
|
75
|
+
end
|
76
|
+
|
77
|
+
def save_owned_tags
|
78
|
+
tagging_contexts.each do |context|
|
79
|
+
cached_owned_tag_list_on(context).each do |owner, tag_list|
|
80
|
+
|
81
|
+
# Find existing tags or create non-existing tags:
|
82
|
+
tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
|
83
|
+
|
84
|
+
# Tag objects for owned tags
|
85
|
+
owned_tags = owner_tags_on(owner, context)
|
86
|
+
|
87
|
+
# Tag maintenance based on whether preserving the created order of tags
|
88
|
+
if self.class.preserve_tag_order?
|
89
|
+
old_tags, new_tags = owned_tags - tags, tags - owned_tags
|
90
|
+
|
91
|
+
shared_tags = owned_tags & tags
|
92
|
+
|
93
|
+
if shared_tags.any? && tags[0...shared_tags.size] != shared_tags
|
94
|
+
index = shared_tags.each_with_index { |_, i| break i unless shared_tags[i] == tags[i] }
|
95
|
+
|
96
|
+
# Update arrays of tag objects
|
97
|
+
old_tags |= owned_tags.from(index)
|
98
|
+
new_tags |= owned_tags.from(index) & shared_tags
|
99
|
+
|
100
|
+
# Order the array of tag objects to match the tag list
|
101
|
+
new_tags = tags.map { |t| new_tags.find { |n| n.name.downcase == t.name.downcase } }.compact
|
127
102
|
end
|
103
|
+
else
|
104
|
+
# Delete discarded tags and create new tags
|
105
|
+
old_tags = owned_tags - tags
|
106
|
+
new_tags = tags - owned_tags
|
107
|
+
end
|
108
|
+
|
109
|
+
# Find all taggings that belong to the taggable (self), are owned by the owner,
|
110
|
+
# have the correct context, and are removed from the list.
|
111
|
+
if old_tags.present?
|
112
|
+
old_taggings = ActsAsTaggableOn::Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
|
113
|
+
:tagger_type => owner.class.base_class.to_s, :tagger_id => owner.id,
|
114
|
+
:tag_id => old_tags, :context => context)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Destroy old taggings:
|
118
|
+
if old_taggings.present?
|
119
|
+
ActsAsTaggableOn::Tagging.destroy_all(:id => old_taggings.map(&:id))
|
120
|
+
end
|
121
|
+
|
122
|
+
# Create new taggings:
|
123
|
+
new_tags.each do |tag|
|
124
|
+
taggings.create!(:tag_id => tag.id, :context => context.to_s, :tagger => owner, :taggable => self)
|
128
125
|
end
|
129
126
|
end
|
130
|
-
|
131
|
-
true
|
132
127
|
end
|
128
|
+
|
129
|
+
true
|
133
130
|
end
|
134
131
|
end
|
135
132
|
end
|
@@ -1,7 +1,6 @@
|
|
1
1
|
module ActsAsTaggableOn::Taggable
|
2
2
|
module Related
|
3
3
|
def self.included(base)
|
4
|
-
base.send :include, ActsAsTaggableOn::Taggable::Related::InstanceMethods
|
5
4
|
base.extend ActsAsTaggableOn::Taggable::Related::ClassMethods
|
6
5
|
base.initialize_acts_as_taggable_on_related
|
7
6
|
end
|
@@ -20,18 +19,6 @@ module ActsAsTaggableOn::Taggable
|
|
20
19
|
end
|
21
20
|
RUBY
|
22
21
|
end
|
23
|
-
|
24
|
-
unless tag_types.empty?
|
25
|
-
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
26
|
-
def find_matching_contexts(search_context, result_context, options = {})
|
27
|
-
matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
|
28
|
-
end
|
29
|
-
|
30
|
-
def find_matching_contexts_for(klass, search_context, result_context, options = {})
|
31
|
-
matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
|
32
|
-
end
|
33
|
-
RUBY
|
34
|
-
end
|
35
22
|
end
|
36
23
|
|
37
24
|
def acts_as_taggable_on(*args)
|
@@ -40,44 +27,50 @@ module ActsAsTaggableOn::Taggable
|
|
40
27
|
end
|
41
28
|
end
|
42
29
|
|
43
|
-
|
44
|
-
|
45
|
-
|
30
|
+
def find_matching_contexts(search_context, result_context, options = {})
|
31
|
+
matching_contexts_for(search_context.to_s, result_context.to_s, self.class, options)
|
32
|
+
end
|
46
33
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
.group(group_columns(klass)) \
|
51
|
-
.order("count DESC")
|
52
|
-
end
|
34
|
+
def find_matching_contexts_for(klass, search_context, result_context, options = {})
|
35
|
+
matching_contexts_for(search_context.to_s, result_context.to_s, klass, options)
|
36
|
+
end
|
53
37
|
|
54
|
-
|
55
|
-
|
56
|
-
tags_to_find = tags_on(context).collect { |t| t.name }.reject { |t| tags_to_ignore.include? t }
|
38
|
+
def matching_contexts_for(search_context, result_context, klass, options = {})
|
39
|
+
tags_to_find = tags_on(search_context).collect { |t| t.name }
|
57
40
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
41
|
+
klass.select("#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count").
|
42
|
+
from("#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}").
|
43
|
+
where(["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?) AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_to_find, result_context]).
|
44
|
+
group(group_columns(klass)).
|
45
|
+
order("count DESC")
|
46
|
+
end
|
64
47
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
48
|
+
def related_tags_for(context, klass, options = {})
|
49
|
+
tags_to_ignore = Array.wrap(options.delete(:ignore)).map(&:to_s) || []
|
50
|
+
tags_to_find = tags_on(context).collect { |t| t.name }.reject { |t| tags_to_ignore.include? t }
|
51
|
+
|
52
|
+
klass.select("#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count").
|
53
|
+
from("#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}").
|
54
|
+
where(["#{exclude_self(klass, id)} #{klass.table_name}.#{klass.primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = '#{klass.base_class.to_s}' AND #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND #{ActsAsTaggableOn::Tag.table_name}.name IN (?)", tags_to_find]).
|
55
|
+
group(group_columns(klass)).
|
56
|
+
order("count DESC")
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def exclude_self(klass, id)
|
62
|
+
if [self.class.base_class, self.class].include? klass
|
63
|
+
"#{klass.table_name}.#{klass.primary_key} != #{id} AND"
|
64
|
+
else
|
65
|
+
nil
|
73
66
|
end
|
67
|
+
end
|
74
68
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
end
|
69
|
+
def group_columns(klass)
|
70
|
+
if ActsAsTaggableOn::Tag.using_postgresql?
|
71
|
+
grouped_column_names_for(klass)
|
72
|
+
else
|
73
|
+
"#{klass.table_name}.#{klass.primary_key}"
|
81
74
|
end
|
82
75
|
end
|
83
76
|
end
|
@@ -1,34 +1,26 @@
|
|
1
1
|
module ActsAsTaggableOn
|
2
2
|
module Utils
|
3
|
-
def
|
4
|
-
|
5
|
-
base.send :include, ActsAsTaggableOn::Utils::OverallMethods
|
6
|
-
base.extend ActsAsTaggableOn::Utils::OverallMethods
|
3
|
+
def using_postgresql?
|
4
|
+
::ActiveRecord::Base.connection && ::ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
|
7
5
|
end
|
8
6
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
end
|
7
|
+
def using_sqlite?
|
8
|
+
::ActiveRecord::Base.connection && ::ActiveRecord::Base.connection.adapter_name == 'SQLite'
|
9
|
+
end
|
13
10
|
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
def sha_prefix(string)
|
12
|
+
Digest::SHA1.hexdigest("#{string}#{rand}")[0..6]
|
13
|
+
end
|
17
14
|
|
18
|
-
|
19
|
-
Digest::SHA1.hexdigest("#{string}#{rand}")[0..6]
|
20
|
-
end
|
21
|
-
|
22
|
-
private
|
23
|
-
def like_operator
|
24
|
-
using_postgresql? ? 'ILIKE' : 'LIKE'
|
25
|
-
end
|
15
|
+
private
|
26
16
|
|
27
|
-
|
28
|
-
|
29
|
-
str.gsub(/[!%_]/){ |x| '!' + x }
|
30
|
-
end
|
17
|
+
def like_operator
|
18
|
+
using_postgresql? ? 'ILIKE' : 'LIKE'
|
31
19
|
end
|
32
20
|
|
21
|
+
# escape _ and % characters in strings, since these are wildcards in SQL.
|
22
|
+
def escape_like(str)
|
23
|
+
str.gsub(/[!%_]/){ |x| '!' + x }
|
24
|
+
end
|
33
25
|
end
|
34
26
|
end
|
@@ -216,6 +216,13 @@ describe "Taggable" do
|
|
216
216
|
TaggableModel.tagged_with("ruby").first.should == @taggable
|
217
217
|
end
|
218
218
|
|
219
|
+
it "should be able to get a count with find by tag when using a group by" do
|
220
|
+
@taggable.skill_list = "ruby"
|
221
|
+
@taggable.save
|
222
|
+
|
223
|
+
expect(TaggableModel.tagged_with("ruby").group(:created_at).count.count).to eq(1)
|
224
|
+
end
|
225
|
+
|
219
226
|
it "should be able to find by tag with context" do
|
220
227
|
@taggable.skill_list = "ruby, rails, css"
|
221
228
|
@taggable.tag_list = "bob, charlie"
|
@@ -369,6 +376,18 @@ describe "Taggable" do
|
|
369
376
|
TaggableModel.tagged_with(["depressed", "css"], :order => 'taggable_models.name', :any => true).to_a.should == [bob, frank]
|
370
377
|
end
|
371
378
|
|
379
|
+
it "should be able to order by number of matching tags when matching any" do
|
380
|
+
bob = TaggableModel.create(:name => "Bob", :tag_list => "fitter, happier, more productive", :skill_list => "ruby, rails, css")
|
381
|
+
frank = TaggableModel.create(:name => "Frank", :tag_list => "weaker, depressed, inefficient", :skill_list => "ruby, rails, css")
|
382
|
+
steve = TaggableModel.create(:name => 'Steve', :tag_list => 'fitter, happier, more productive', :skill_list => 'c++, java, ruby')
|
383
|
+
|
384
|
+
TaggableModel.tagged_with(["ruby", "java"], :any => true, :order_by_matching_tag_count => true, :order => 'taggable_models.name').to_a.should == [steve, bob, frank]
|
385
|
+
TaggableModel.tagged_with(["c++", "fitter"], :any => true, :order_by_matching_tag_count => true, :order => 'taggable_models.name').to_a.should == [steve, bob]
|
386
|
+
TaggableModel.tagged_with(["depressed", "css"], :any => true, :order_by_matching_tag_count => true, :order => 'taggable_models.name').to_a.should == [frank, bob]
|
387
|
+
TaggableModel.tagged_with(["fitter", "happier", "more productive", "c++", "java", "ruby"], :any => true, :order_by_matching_tag_count => true, :order => 'taggable_models.name').to_a.should == [steve, bob, frank]
|
388
|
+
TaggableModel.tagged_with(["c++", "java", "ruby", "fitter"], :any => true, :order_by_matching_tag_count => true, :order => 'taggable_models.name').to_a.should == [steve, bob, frank]
|
389
|
+
end
|
390
|
+
|
372
391
|
context "wild: true" do
|
373
392
|
it "should use params as wildcards" do
|
374
393
|
bob = TaggableModel.create(:name => "Bob", :tag_list => "bob, tricia")
|
@@ -411,14 +430,6 @@ describe "Taggable" do
|
|
411
430
|
TaggableModel.tagged_with("fitter, happier", :match_all => true).to_a.should == [steve]
|
412
431
|
end
|
413
432
|
|
414
|
-
it "should be able to find tagged with only the matching tags for a context" do
|
415
|
-
bob = TaggableModel.create(:name => "Bob", :tag_list => "lazy, happier", :skill_list => "ruby, rails, css")
|
416
|
-
frank = TaggableModel.create(:name => "Frank", :tag_list => "fitter, happier, inefficient", :skill_list => "css")
|
417
|
-
steve = TaggableModel.create(:name => 'Steve', :tag_list => "fitter, happier", :skill_list => "ruby, rails, css")
|
418
|
-
|
419
|
-
TaggableModel.tagged_with("css", :on => :skills, :match_all => true).to_a.should == [frank]
|
420
|
-
end
|
421
|
-
|
422
433
|
it "should be able to find tagged with some excluded tags" do
|
423
434
|
bob = TaggableModel.create(:name => "Bob", :tag_list => "happier, lazy")
|
424
435
|
frank = TaggableModel.create(:name => "Frank", :tag_list => "happier")
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acts-as-taggable-on
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.0.
|
4
|
+
version: 3.1.0.rc1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Bleigh
|
@@ -9,10 +9,50 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-
|
12
|
+
date: 2014-02-27 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
|
-
name:
|
15
|
+
name: activerecord
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ! '>='
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '3'
|
21
|
+
- - <
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: '5'
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '3'
|
31
|
+
- - <
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '5'
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: activesupport
|
36
|
+
requirement: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3'
|
41
|
+
- - <
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '5'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ! '>='
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '3'
|
51
|
+
- - <
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '5'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: actionpack
|
16
56
|
requirement: !ruby/object:Gem::Requirement
|
17
57
|
requirements:
|
18
58
|
- - ! '>='
|
@@ -194,12 +234,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
194
234
|
version: '0'
|
195
235
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
196
236
|
requirements:
|
197
|
-
- - ! '
|
237
|
+
- - ! '>'
|
198
238
|
- !ruby/object:Gem::Version
|
199
|
-
version:
|
239
|
+
version: 1.3.1
|
200
240
|
requirements: []
|
201
241
|
rubyforge_project:
|
202
|
-
rubygems_version: 2.2.
|
242
|
+
rubygems_version: 2.2.1
|
203
243
|
signing_key:
|
204
244
|
specification_version: 4
|
205
245
|
summary: Advanced tagging for Rails.
|