acts-as-taggable-on 3.0.2 → 3.1.0.rc1
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 +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.
|