acts-as-taggable-on 3.1.0.rc1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- NWU0YmVhNGFlMjYzYTg1NDdiNTRkODQ5ODViYjRmNGI3ZjNiMjZiZA==
4
+ MjM0ZjVlNjhmY2MyOWIwYTBkYzUwMTFiODc0YzQxMDYyNWEzNDA1Nw==
5
5
  data.tar.gz: !binary |-
6
- YTQ0MDhmYzEyMDA5Yzc5MTQ5MjgxODY1NWQ4YzQ1ZGI1YmYwYTg1Ng==
6
+ NWYzMTZhOWNiYTEyZjcwNTBlMTIyYTcxMTVjZmRjMGEzMjgyZDljNw==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- OGU1OTU3ZjFkMDc1ZDAzNGY1YzI4Yzk1ODFlYTE1YWNjZThmMDcyYTBhMDMz
10
- MzdiYWUxYjgxNDhiYzQ4ZmI5ZTgyMjIwMWZkYmI3ZTc2ZWM4YmUyNzUxZDYx
11
- ZWJhNmU1ZDRiOWU2ZGZiM2YyMTAwMTc1YmZmYWYzYzdkMTg4YzY=
9
+ NThiZDVmYTFjZGFiMTFkNTM4Y2Y1M2Y1OGI1ZWM5N2U4YzVhZWMzMjJjMTZl
10
+ Y2ZlYzkwMjFlY2JlMDdkYzk3YmNmYjNhNDAzZDFjMDhkYmQxZGQ4MGJhM2Vi
11
+ Mzg4MWMyNDNkZmVkNWMwYmI1ZDBkZDQxMzgyNWQzMTI2NTYwNmY=
12
12
  data.tar.gz: !binary |-
13
- YjU3MzdjMTIyNDM2ZDY2ZmNhZjUxZDhlMTgwYTM3NjFiY2E2Mzg5NDRmZDNi
14
- ZTU1NTQ1MjAwMzIwNDVjNzEyYmNlZTU1MmNkYTk3OGY2ZjhkNTA2M2M2ZWVj
15
- ZDQzODA0OTMxMTMxNDMzNTY5ZTk3NWE1MzgxZDM3ZjgwMDBjMTA=
13
+ OWZkOTMxM2ZiODMyYWQyOTBkN2MyMDQ5NmMzYTJlNjUyOWJkMzliZjFkZDg1
14
+ YjU5M2M4Y2NmOWZhZDcxODUzODRjNzc2YjE0NWQ2ZjVkMDhjNWY1NGE2MzQy
15
+ OWVmZTVkZWU2OTljNzMwZmVmZDY5YTkxMjY5YzRiOGZhZDgxOWM=
@@ -2,6 +2,7 @@ rvm:
2
2
  - 1.9.3
3
3
  - 2.0.0
4
4
  - 2.1.0
5
+ - 2.1.1
5
6
  env:
6
7
  - DB=sqlite3
7
8
  - DB=mysql
@@ -10,6 +11,7 @@ gemfile:
10
11
  - gemfiles/rails_3.2.gemfile
11
12
  - gemfiles/rails_4.0.gemfile
12
13
  - gemfiles/rails_4.1.gemfile
14
+ - gemfiles/rails_edge.gemfile
13
15
  cache: bundler
14
16
  script: bundle exec rake
15
17
  before_install:
data/Appraisals CHANGED
@@ -9,3 +9,7 @@ end
9
9
  appraise "rails-4.1" do
10
10
  gem "rails", "~> 4.1.0.beta1"
11
11
  end
12
+
13
+ appraise "rails-edge" do
14
+ gem "rails", github: "rails/rails"
15
+ end
@@ -4,13 +4,25 @@ 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.1...master)
7
+ ### Master [changes](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.1.0...master)
8
8
 
9
9
  * Breaking Changes
10
10
  * Features
11
11
  * Fixes
12
+ * Performance
12
13
  * Misc
13
14
 
15
+ ### [3.1.0 / 2014-03-31](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.0.1...v3.1.0)
16
+
17
+ * Fixes
18
+ * [@mikehale #487 Match_all respects context](https://github.com/mbleigh/acts-as-taggable-on/pull/487)
19
+ * Performance
20
+ * [@dgilperez #390 Add taggings counter cache](https://github.com/mbleigh/acts-as-taggable-on/pull/390)
21
+ * Misc
22
+ * [@jonseaberg Add missing indexes to schema used in specs #474](https://github.com/mbleigh/acts-as-taggable-on/pull/474)
23
+ * [@seuros Specify Ruby >= 1.9.3 required in gemspec](https://github.com/mbleigh/acts-as-taggable-on/pull/502)
24
+ * [@kiasaki Add missing quotes to code example](https://github.com/mbleigh/acts-as-taggable-on/pull/501)
25
+
14
26
  ### [3.1.0.rc1 / 2014-02-26](https://github.com/mbleigh/acts-as-taggable-on/compare/v3.0.1...v3.1.0.rc1)
15
27
 
16
28
  * Features
@@ -22,6 +34,7 @@ As such, a _Feature_ would map to either major or minor. A _bug fix_ to a patch.
22
34
  * [@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
35
  * [@developer88 #461 Move 'Distinct' out of select string and use .uniq instead](https://github.com/mbleigh/acts-as-taggable-on/pull/461)
24
36
  * [@gerard-leijdekkers #473 Fixed down migration index name](https://github.com/mbleigh/acts-as-taggable-on/pull/473)
37
+ * [@leo-souza #498 Use database's lower function for case-insensitive match](https://github.com/mbleigh/acts-as-taggable-on/pull/498)
25
38
  * Misc
26
39
  * [@billychan #463 Thread safe support](https://github.com/mbleigh/acts-as-taggable-on/pull/463)
27
40
  * [@billychan #386 Add parse:true instructions to README](https://github.com/mbleigh/acts-as-taggable-on/pull/386)
@@ -11,7 +11,7 @@
11
11
  2. Install the gem dependencies: `bundle install`
12
12
  3. Make the changes you want and back them up with tests.
13
13
  * [Run the tests](https://github.com/mbleigh/acts-as-taggable-on#testing) (`bundle exec rake spec`)
14
- 4. Update the CHAGELOG.md file with your changes and give yourself credit
14
+ 4. Update the CHANGELOG.md file with your changes and give yourself credit
15
15
  5. Commit and create a pull request with details as to what has been changed and why
16
16
  * Use well-described, small (atomic) commits.
17
17
  * Include links to any relevant github issues.
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # ActsAsTaggableOn
2
2
  [![Build Status](https://secure.travis-ci.org/mbleigh/acts-as-taggable-on.png)](http://travis-ci.org/mbleigh/acts-as-taggable-on)
3
3
  [![Code Climate](https://codeclimate.com/github/mbleigh/acts-as-taggable-on.png)](https://codeclimate.com/github/mbleigh/acts-as-taggable-on)
4
+ [![Inline docs](http://inch-pages.github.io/github/mbleigh/acts-as-taggable-on.png)](http://inch-pages.github.io/github/mbleigh/acts-as-taggable-on)
4
5
 
5
6
  This plugin was originally based on Acts as Taggable on Steroids by Jonathan Viney.
6
7
  It has evolved substantially since that point, but all credit goes to him for the
@@ -175,7 +176,7 @@ User.tagged_with(["awesome", "cool"], :any => true)
175
176
  User.tagged_with(["awesome", "cool"], :exclude => true)
176
177
 
177
178
  # Find a user with any of tags based on context:
178
- User.tagged_with(['awesome, cool'], :on => :tags, :any => true).tagged_with(['smart', 'shy'], :on => :skills, :any => true)
179
+ User.tagged_with(['awesome', 'cool'], :on => :tags, :any => true).tagged_with(['smart', 'shy'], :on => :skills, :any => true)
179
180
  ```
180
181
 
181
182
  You can also use `:wild => true` option along with `:any` or `:exclude` option. It will looking for `%awesome%` and `%cool%` in sql.
@@ -339,6 +340,8 @@ If you want to change the default delimiter (it defaults to ','). You can also p
339
340
  ActsAsTaggableOn.delimiter = ','
340
341
  ```
341
342
 
343
+ *NOTE: SQLite by default can't upcase or downcase multibyte characters, resulting in unwanted behavior. Load the SQLite ICU extension for proper handle of such characters. [See docs](http://www.sqlite.org/src/artifact?ci=trunk&filename=ext/icu/README.txt)*
344
+
342
345
  ## Contributors
343
346
 
344
347
  We have a long list of valued contributors. [Check them all](https://github.com/mbleigh/acts-as-taggable-on/contributors)
data/Rakefile CHANGED
@@ -5,6 +5,9 @@ rescue LoadError
5
5
  STDERR.puts "Bundler not loaded"
6
6
  end
7
7
 
8
+ desc 'Default: run specs'
9
+ task :default => :spec
10
+
8
11
  desc 'Copy sample spec database.yml over if not exists'
9
12
  task :copy_db_config do
10
13
  cp 'spec/database.yml.sample', 'spec/database.yml'
@@ -12,9 +15,6 @@ end
12
15
 
13
16
  task :spec => [:copy_db_config]
14
17
 
15
- desc 'Default: run specs'
16
- task :default => :spec
17
-
18
18
  begin
19
19
  require 'appraisal'
20
20
  desc 'Run tests across gemfiles specified in Appraisals'
@@ -17,6 +17,7 @@ Gem::Specification.new do |gem|
17
17
  gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
+ gem.required_ruby_version = '>= 1.9.3'
20
21
 
21
22
  if File.exists?('UPGRADING.md')
22
23
  gem.post_install_message = File.read('UPGRADING.md')
@@ -0,0 +1,13 @@
1
+ class AddTaggingsCounterCacheToTags < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :tags, :taggings_count, :integer, :default => 0
4
+
5
+ ActsAsTaggableOn::Tag.find_each do |tag|
6
+ ActsAsTaggableOn::Tag.reset_counters(tag.id, :taggings)
7
+ end
8
+ end
9
+
10
+ def self.down
11
+ remove_column :tags, :taggings_count
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", :github=>"rails/rails"
6
+
7
+ gemspec :path=>"../"
@@ -22,15 +22,17 @@ module ActsAsTaggableOn::Taggable
22
22
  initialize_tags_cache
23
23
  end
24
24
 
25
- # ActiveRecord::Base.columns makes a database connection and caches the calculated
26
- # columns hash for the record as @columns. Since we don't want to add caching
27
- # methods until we confirm the presence of a caching column, and we don't
28
- # want to force opening a database connection when the class is loaded,
29
- # here we intercept and cache the call to :columns as @acts_as_taggable_on_columns
30
- # to mimic the underlying behavior. While processing this first call to columns,
31
- # we do the caching column check and dynamically add the class and instance methods
25
+ # ActiveRecord::Base.columns makes a database connection and caches the
26
+ # calculated columns hash for the record as @columns. Since we don't
27
+ # want to add caching methods until we confirm the presence of a
28
+ # caching column, and we don't want to force opening a database
29
+ # connection when the class is loaded, here we intercept and cache
30
+ # the call to :columns as @acts_as_taggable_on_cache_columns
31
+ # to mimic the underlying behavior. While processing this first
32
+ # call to columns, we do the caching column check and dynamically add
33
+ # the class and instance methods
32
34
  def columns
33
- @acts_as_taggable_on_columns ||= begin
35
+ @acts_as_taggable_on_cache_columns ||= begin
34
36
  db_columns = super
35
37
  if _has_tags_cache_columns?(db_columns)
36
38
  _add_tags_caching_methods
@@ -58,29 +58,13 @@ module ActsAsTaggableOn::Taggable
58
58
  ## Generate conditions:
59
59
  options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
60
60
 
61
- start_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
62
- end_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
63
-
64
- taggable_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.taggable_type = ?", base_class.name])
65
- taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", options.delete(:on).to_s]) if options[:on]
66
-
67
- tagging_conditions = [
68
- taggable_conditions,
69
- start_at_conditions,
70
- end_at_conditions
71
- ].compact.reverse
72
-
73
- tag_conditions = [
74
- options[:conditions]
75
- ].compact.reverse
76
-
77
61
  ## Generate scope:
78
62
  tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id")
79
63
  tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*").order(options[:order]).limit(options[:limit])
80
64
 
81
65
  # Joins and conditions
82
- tagging_conditions.each { |condition| tagging_scope = tagging_scope.where(condition) }
83
- tag_conditions.each { |condition| tag_scope = tag_scope.where(condition) }
66
+ tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) }
67
+ tag_scope = tag_scope.where(options[:conditions])
84
68
 
85
69
  group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
86
70
 
@@ -88,10 +72,10 @@ module ActsAsTaggableOn::Taggable
88
72
  scoped_select = "#{table_name}.#{primary_key}"
89
73
  tagging_scope = tagging_scope.where("#{ActsAsTaggableOn::Tagging.table_name}.taggable_id IN(#{safe_to_sql(select(scoped_select))})").group(group_columns)
90
74
 
91
- tag_scope = tag_scope.joins("JOIN (#{safe_to_sql(tagging_scope)}) AS #{ActsAsTaggableOn::Tagging.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id")
92
- tag_scope.extending(CalculationMethods)
75
+ tag_scope_joins(tag_scope, tagging_scope)
93
76
  end
94
77
 
78
+
95
79
  ##
96
80
  # Calculate the tag counts for all tags.
97
81
  #
@@ -107,56 +91,28 @@ module ActsAsTaggableOn::Taggable
107
91
  def all_tag_counts(options = {})
108
92
  options.assert_valid_keys :start_at, :end_at, :conditions, :at_least, :at_most, :order, :limit, :on, :id
109
93
 
110
- scope = {}
111
-
112
94
  ## Generate conditions:
113
95
  options[:conditions] = sanitize_sql(options[:conditions]) if options[:conditions]
114
96
 
115
- start_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
116
- end_at_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
117
-
118
- taggable_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.taggable_type = ?", base_class.name])
119
- taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = ?", options[:id]]) if options[:id]
120
- taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", options.delete(:on).to_s]) if options[:on]
121
-
122
- tagging_conditions = [
123
- taggable_conditions,
124
- scope[:conditions],
125
- start_at_conditions,
126
- end_at_conditions
127
- ].compact.reverse
128
-
129
- tag_conditions = [
130
- options[:conditions]
131
- ].compact.reverse
132
-
133
97
  ## Generate joins:
134
98
  taggable_join = "INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{ActsAsTaggableOn::Tagging.table_name}.taggable_id"
135
99
  taggable_join << " AND #{table_name}.#{inheritance_column} = '#{name}'" unless descends_from_active_record? # Current model is STI descendant, so add type checking to the join condition
136
100
 
137
- tagging_joins = [
138
- taggable_join,
139
- scope[:joins]
140
- ].compact
141
-
142
- tag_joins = [
143
- ].compact
144
101
 
145
102
  ## Generate scope:
146
103
  tagging_scope = ActsAsTaggableOn::Tagging.select("#{ActsAsTaggableOn::Tagging.table_name}.tag_id, COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) AS tags_count")
147
104
  tag_scope = ActsAsTaggableOn::Tag.select("#{ActsAsTaggableOn::Tag.table_name}.*, #{ActsAsTaggableOn::Tagging.table_name}.tags_count AS count").order(options[:order]).limit(options[:limit])
148
105
 
149
106
  # Joins and conditions
150
- tagging_joins.each { |join| tagging_scope = tagging_scope.joins(join) }
151
- tagging_conditions.each { |condition| tagging_scope = tagging_scope.where(condition) }
152
-
153
- tag_joins.each { |join| tag_scope = tag_scope.joins(join) }
154
- tag_conditions.each { |condition| tag_scope = tag_scope.where(condition) }
107
+ tagging_scope = tagging_scope.joins(taggable_join)
108
+ tagging_conditions(options).each { |condition| tagging_scope = tagging_scope.where(condition) }
109
+ tag_scope = tag_scope.where(options[:conditions])
155
110
 
156
111
  # GROUP BY and HAVING clauses:
157
- at_least = sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) >= ?", options.delete(:at_least)]) if options[:at_least]
158
- at_most = sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) <= ?", options.delete(:at_most)]) if options[:at_most]
159
- having = ["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) > 0", at_least, at_most].compact.join(' AND ')
112
+ having = ["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) > 0"]
113
+ having.push sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) >= ?", options.delete(:at_least)]) if options[:at_least]
114
+ having.push sanitize_sql(["COUNT(#{ActsAsTaggableOn::Tagging.table_name}.tag_id) <= ?", options.delete(:at_most)]) if options[:at_most]
115
+ having = having.compact.join(' AND ')
160
116
 
161
117
  group_columns = "#{ActsAsTaggableOn::Tagging.table_name}.tag_id"
162
118
 
@@ -168,13 +124,33 @@ module ActsAsTaggableOn::Taggable
168
124
 
169
125
  tagging_scope = tagging_scope.group(group_columns).having(having)
170
126
 
171
- tag_scope = tag_scope.joins("JOIN (#{safe_to_sql(tagging_scope)}) AS #{ActsAsTaggableOn::Tagging.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id")
172
- tag_scope.extending(CalculationMethods)
127
+ tag_scope_joins(tag_scope, tagging_scope)
173
128
  end
174
129
 
175
130
  def safe_to_sql(relation)
176
131
  connection.respond_to?(:unprepared_statement) ? connection.unprepared_statement{relation.to_sql} : relation.to_sql
177
132
  end
133
+
134
+ private
135
+
136
+ def tagging_conditions(options)
137
+ tagging_conditions = []
138
+ tagging_conditions.push sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at <= ?", options.delete(:end_at)]) if options[:end_at]
139
+ tagging_conditions.push sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.created_at >= ?", options.delete(:start_at)]) if options[:start_at]
140
+
141
+ taggable_conditions = sanitize_sql(["#{ActsAsTaggableOn::Tagging.table_name}.taggable_type = ?", base_class.name])
142
+ taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.context = ?", options.delete(:on).to_s]) if options[:on]
143
+ taggable_conditions << sanitize_sql([" AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = ?", options[:id]]) if options[:id]
144
+
145
+ tagging_conditions.push taggable_conditions
146
+
147
+ tagging_conditions
148
+ end
149
+
150
+ def tag_scope_joins(tag_scope, tagging_scope)
151
+ tag_scope = tag_scope.joins("JOIN (#{safe_to_sql(tagging_scope)}) AS #{ActsAsTaggableOn::Tagging.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.id")
152
+ tag_scope.extending(CalculationMethods)
153
+ end
178
154
  end
179
155
 
180
156
  def tag_counts_on(context, options={})
@@ -193,6 +193,8 @@ module ActsAsTaggableOn::Taggable
193
193
  " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
194
194
  " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
195
195
 
196
+ joins << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
197
+
196
198
  group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
197
199
  group = group_columns
198
200
  having = "COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
@@ -37,33 +37,19 @@ module ActsAsTaggableOn::Taggable
37
37
 
38
38
  def matching_contexts_for(search_context, result_context, klass, options = {})
39
39
  tags_to_find = tags_on(search_context).collect { |t| t.name }
40
-
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")
40
+ related_where(klass, ["#{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])
46
41
  end
47
42
 
48
43
  def related_tags_for(context, klass, options = {})
49
44
  tags_to_ignore = Array.wrap(options.delete(:ignore)).map(&:to_s) || []
50
45
  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")
46
+ related_where(klass, ["#{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])
57
47
  end
58
48
 
59
49
  private
60
50
 
61
51
  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
66
- end
52
+ "#{klass.table_name}.#{klass.primary_key} != #{id} AND" if [self.class.base_class, self.class].include? klass
67
53
  end
68
54
 
69
55
  def group_columns(klass)
@@ -73,5 +59,13 @@ module ActsAsTaggableOn::Taggable
73
59
  "#{klass.table_name}.#{klass.primary_key}"
74
60
  end
75
61
  end
62
+
63
+ def related_where(klass, conditions)
64
+ klass.select("#{klass.table_name}.*, COUNT(#{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key}) AS count").
65
+ from("#{klass.table_name}, #{ActsAsTaggableOn::Tag.table_name}, #{ActsAsTaggableOn::Tagging.table_name}").
66
+ group(group_columns(klass)).
67
+ order("count DESC").
68
+ where(conditions)
69
+ end
76
70
  end
77
71
  end
@@ -24,9 +24,9 @@ module ActsAsTaggableOn
24
24
 
25
25
  def self.named(name)
26
26
  if ActsAsTaggableOn.strict_case_match
27
- where(["name = #{binary}?", name])
27
+ where(["name = #{binary}?", as_8bit_ascii(name)])
28
28
  else
29
- where(["lower(name) = ?", name.downcase])
29
+ where(["LOWER(name) = LOWER(?)", as_8bit_ascii(unicode_downcase(name))])
30
30
  end
31
31
  end
32
32
 
@@ -38,8 +38,7 @@ module ActsAsTaggableOn
38
38
  where(clause)
39
39
  else
40
40
  clause = list.map { |tag|
41
- lowercase_ascii_tag = as_8bit_ascii(tag).downcase
42
- sanitize_sql(["lower(name) = ?", lowercase_ascii_tag])
41
+ sanitize_sql(["LOWER(name) = LOWER(?)", as_8bit_ascii(unicode_downcase(tag))])
43
42
  }.join(" OR ")
44
43
  where(clause)
45
44
  end
@@ -101,14 +100,22 @@ module ActsAsTaggableOn
101
100
 
102
101
  def comparable_name(str)
103
102
  if ActsAsTaggableOn.strict_case_match
104
- as_8bit_ascii(str)
103
+ str
105
104
  else
106
- as_8bit_ascii(str).downcase
105
+ unicode_downcase(str.to_s)
107
106
  end
108
107
  end
109
108
 
110
109
  def binary
111
- /mysql/ === ActiveRecord::Base.connection_config[:adapter] ? "BINARY " : nil
110
+ using_mysql? ? "BINARY " : nil
111
+ end
112
+
113
+ def unicode_downcase(string)
114
+ if ActiveSupport::Multibyte::Unicode.respond_to?(:downcase)
115
+ ActiveSupport::Multibyte::Unicode.downcase(string)
116
+ else
117
+ ActiveSupport::Multibyte::Chars.new(string).downcase.to_s
118
+ end
112
119
  end
113
120
 
114
121
  def as_8bit_ascii(string)
@@ -9,7 +9,6 @@ module ActsAsTaggableOn
9
9
  :tagger,
10
10
  :tagger_type,
11
11
  :tagger_id if defined?(ActiveModel::MassAssignmentSecurity)
12
-
13
12
  belongs_to :tag, :class_name => 'ActsAsTaggableOn::Tag'
14
13
  belongs_to :taggable, :polymorphic => true
15
14
  belongs_to :tagger, :polymorphic => true
@@ -17,14 +16,29 @@ module ActsAsTaggableOn
17
16
  validates_presence_of :context
18
17
  validates_presence_of :tag_id
19
18
 
20
- validates_uniqueness_of :tag_id, :scope => [ :taggable_type, :taggable_id, :context, :tagger_id, :tagger_type ]
19
+ validates_uniqueness_of :tag_id, :scope => [:taggable_type, :taggable_id, :context, :tagger_id, :tagger_type]
21
20
 
22
21
  after_destroy :remove_unused_tags
23
22
 
23
+ # Conditionally adds a counter cache when cache column is present.
24
+ # We just regenerate the association. It's the easiest way.
25
+ # TODO: require the counter cache in release 4.0.0 and remove these methods
26
+ # @see :columns in ActsAsTaggableOn::Taggable::Cache
27
+ def self.columns
28
+ @acts_as_taggable_on_counter_columns ||= begin
29
+ db_columns = super
30
+ belongs_to :tag, :class_name => 'ActsAsTaggableOn::Tag', :counter_cache => ActsAsTaggableOn::Tag.column_names.include?('taggings_count')
31
+ db_columns
32
+ end
33
+ end
34
+
35
+
36
+
24
37
  private
25
38
 
26
39
  def remove_unused_tags
27
40
  if ActsAsTaggableOn.remove_unused_tags
41
+ # TODO: use taggings_count in release 4.0.0
28
42
  if tag.taggings.count.zero?
29
43
  tag.destroy
30
44
  end
@@ -1,11 +1,25 @@
1
1
  module ActsAsTaggableOn
2
2
  module Utils
3
+
4
+ def connection
5
+ ::ActiveRecord::Base.connection
6
+ end
7
+
3
8
  def using_postgresql?
4
- ::ActiveRecord::Base.connection && ::ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
9
+ connection && connection.adapter_name == 'PostgreSQL'
5
10
  end
6
11
 
7
12
  def using_sqlite?
8
- ::ActiveRecord::Base.connection && ::ActiveRecord::Base.connection.adapter_name == 'SQLite'
13
+ connection && connection.adapter_name == 'SQLite'
14
+ end
15
+
16
+ def using_mysql?
17
+ #We should probably use regex for mysql to support prehistoric adapters
18
+ connection && connection.adapter_name == 'Mysql2'
19
+ end
20
+
21
+ def using_case_insensitive_collation?
22
+ using_mysql? && ::ActiveRecord::Base.connection.collation =~ /_ci\Z/
9
23
  end
10
24
 
11
25
  def sha_prefix(string)
@@ -20,7 +34,7 @@ module ActsAsTaggableOn
20
34
 
21
35
  # escape _ and % characters in strings, since these are wildcards in SQL.
22
36
  def escape_like(str)
23
- str.gsub(/[!%_]/){ |x| '!' + x }
37
+ str.gsub(/[!%_]/) { |x| '!' + x }
24
38
  end
25
39
  end
26
40
  end
@@ -1,4 +1,4 @@
1
1
  module ActsAsTaggableOn
2
- VERSION = '3.1.0.rc1'
2
+ VERSION = '3.1.0'
3
3
  end
4
4
 
@@ -1,5 +1,11 @@
1
1
  # encoding: utf-8
2
2
  require 'spec_helper'
3
+ require 'db/migrate/2_add_missing_unique_indices.rb'
4
+
5
+ shared_examples_for 'without unique index' do
6
+ before { AddMissingUniqueIndices.down }
7
+ after { ActsAsTaggableOn::Tag.delete_all; AddMissingUniqueIndices.up }
8
+ end
3
9
 
4
10
  describe ActsAsTaggableOn::Tag do
5
11
  before(:each) do
@@ -9,14 +15,33 @@ describe ActsAsTaggableOn::Tag do
9
15
  end
10
16
 
11
17
  describe "named like any" do
12
- before(:each) do
13
- ActsAsTaggableOn::Tag.create(:name => "Awesome")
14
- ActsAsTaggableOn::Tag.create(:name => "awesome")
15
- ActsAsTaggableOn::Tag.create(:name => "epic")
18
+ context "case insensitive collation and unique index on tag name" do
19
+ if described_class.using_case_insensitive_collation?
20
+ before(:each) do
21
+ ActsAsTaggableOn::Tag.create(:name => "Awesome")
22
+ ActsAsTaggableOn::Tag.create(:name => "epic")
23
+ end
24
+
25
+ it "should find both tags" do
26
+ ActsAsTaggableOn::Tag.named_like_any(["awesome", "epic"]).should have(2).items
27
+ end
28
+ end
16
29
  end
17
30
 
18
- it "should find both tags" do
19
- ActsAsTaggableOn::Tag.named_like_any(["awesome", "epic"]).should have(3).items
31
+ context "case insensitive collation without indexes or case sensitive collation with indexes" do
32
+ if described_class.using_case_insensitive_collation?
33
+ include_context 'without unique index'
34
+ end
35
+
36
+ before(:each) do
37
+ ActsAsTaggableOn::Tag.create(:name => "Awesome")
38
+ ActsAsTaggableOn::Tag.create(:name => "awesome")
39
+ ActsAsTaggableOn::Tag.create(:name => "epic")
40
+ end
41
+
42
+ it "should find both tags" do
43
+ ActsAsTaggableOn::Tag.named_like_any(["awesome", "epic"]).should have(3).items
44
+ end
20
45
  end
21
46
  end
22
47
 
@@ -72,11 +97,17 @@ describe ActsAsTaggableOn::Tag do
72
97
  ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name("AWESOME").should == [@tag]
73
98
  end
74
99
 
75
- it "should find by name case sensitive" do
76
- ActsAsTaggableOn.strict_case_match = true
77
- expect {
78
- ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name("AWESOME")
79
- }.to change(ActsAsTaggableOn::Tag, :count).by(1)
100
+ context "case sensitive" do
101
+ if described_class.using_case_insensitive_collation?
102
+ include_context 'without unique index'
103
+ end
104
+
105
+ it "should find by name case sensitive" do
106
+ ActsAsTaggableOn.strict_case_match = true
107
+ expect {
108
+ ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name("AWESOME")
109
+ }.to change(ActsAsTaggableOn::Tag, :count).by(1)
110
+ end
80
111
  end
81
112
 
82
113
  it "should create by name" do
@@ -85,11 +116,17 @@ describe ActsAsTaggableOn::Tag do
85
116
  }.to change(ActsAsTaggableOn::Tag, :count).by(1)
86
117
  end
87
118
 
88
- it "should find or create by name case sensitive" do
89
- ActsAsTaggableOn.strict_case_match = true
90
- expect {
91
- ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name("AWESOME", 'awesome').map(&:name).should == ["AWESOME", "awesome"]
92
- }.to change(ActsAsTaggableOn::Tag, :count).by(1)
119
+ context "case sensitive" do
120
+ if described_class.using_case_insensitive_collation?
121
+ include_context 'without unique index'
122
+ end
123
+
124
+ it "should find or create by name case sensitive" do
125
+ ActsAsTaggableOn.strict_case_match = true
126
+ expect {
127
+ ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name("AWESOME", 'awesome').map(&:name).should == ["AWESOME", "awesome"]
128
+ }.to change(ActsAsTaggableOn::Tag, :count).by(1)
129
+ end
93
130
  end
94
131
 
95
132
  it "should find or create by name" do
@@ -178,21 +215,33 @@ describe ActsAsTaggableOn::Tag do
178
215
  ActsAsTaggableOn::Tag.find_or_create_with_like_by_name("awesome").should == @tag
179
216
  end
180
217
 
181
- it "should find by name case sensitively" do
182
- expect {
183
- ActsAsTaggableOn::Tag.find_or_create_with_like_by_name("AWESOME")
184
- }.to change(ActsAsTaggableOn::Tag, :count)
218
+ context "case sensitive" do
219
+ if described_class.using_case_insensitive_collation?
220
+ include_context 'without unique index'
221
+ end
185
222
 
186
- ActsAsTaggableOn::Tag.last.name.should == "AWESOME"
223
+ it "should find by name case sensitively" do
224
+ expect {
225
+ ActsAsTaggableOn::Tag.find_or_create_with_like_by_name("AWESOME")
226
+ }.to change(ActsAsTaggableOn::Tag, :count)
227
+
228
+ ActsAsTaggableOn::Tag.last.name.should == "AWESOME"
229
+ end
187
230
  end
188
231
 
189
- it "should have a named_scope named(something) that matches exactly" do
190
- uppercase_tag = ActsAsTaggableOn::Tag.create(:name => "Cool")
191
- @tag.name = "cool"
192
- @tag.save!
232
+ context "case sensitive" do
233
+ if described_class.using_case_insensitive_collation?
234
+ include_context 'without unique index'
235
+ end
193
236
 
194
- ActsAsTaggableOn::Tag.named('cool').should include(@tag)
195
- ActsAsTaggableOn::Tag.named('cool').should_not include(uppercase_tag)
237
+ it "should have a named_scope named(something) that matches exactly" do
238
+ uppercase_tag = ActsAsTaggableOn::Tag.create(:name => "Cool")
239
+ @tag.name = "cool"
240
+ @tag.save!
241
+
242
+ ActsAsTaggableOn::Tag.named('cool').should include(@tag)
243
+ ActsAsTaggableOn::Tag.named('cool').should_not include(uppercase_tag)
244
+ end
196
245
  end
197
246
 
198
247
  it "should not change enconding" do
@@ -209,16 +258,17 @@ describe ActsAsTaggableOn::Tag do
209
258
  before { ActsAsTaggableOn::Tag.create(:name => 'ror') }
210
259
 
211
260
  context "when don't need unique names" do
261
+ include_context 'without unique index'
212
262
  it "should not run uniqueness validation" do
213
263
  duplicate_tag.stub(:validates_name_uniqueness?).and_return(false)
214
264
  duplicate_tag.save
215
265
  duplicate_tag.should be_persisted
216
- end
266
+ end
217
267
  end
218
268
 
219
269
  context "when do need unique names" do
220
270
  it "should run uniqueness validation" do
221
- duplicate_tag.should_not be_valid
271
+ duplicate_tag.should_not be_valid
222
272
  end
223
273
 
224
274
  it "add error to name" do
@@ -1,3 +1,4 @@
1
+ # encoding: utf-8
1
2
  require 'spec_helper'
2
3
 
3
4
  describe "Taggable To Preserve Order" do
@@ -242,6 +243,34 @@ describe "Taggable" do
242
243
  TaggableModel.tagged_with("ruby").to_a.should == TaggableModel.tagged_with("Ruby").to_a
243
244
  end
244
245
 
246
+ unless ActsAsTaggableOn::Tag.using_sqlite?
247
+ it "should not care about case for unicode names" do
248
+ ActsAsTaggableOn.strict_case_match = false
249
+
250
+ anya = TaggableModel.create(:name => "Anya", :tag_list => "ПРИВЕТ")
251
+ igor = TaggableModel.create(:name => "Igor", :tag_list => "привет")
252
+ katia = TaggableModel.create(:name => "Katia", :tag_list => "ПРИВЕТ")
253
+
254
+ ActsAsTaggableOn::Tag.all.size.should == 1
255
+ TaggableModel.tagged_with("привет").to_a.should == TaggableModel.tagged_with("ПРИВЕТ").to_a
256
+ end
257
+ end
258
+
259
+ it "should be able to create and find tags in languages without capitalization" do
260
+ ActsAsTaggableOn.strict_case_match = false
261
+ chihiro = TaggableModel.create(:name => "Chihiro", :tag_list => "日本の")
262
+ salim = TaggableModel.create(:name => "Salim", :tag_list => "עברית")
263
+ ieie = TaggableModel.create(:name => "Ieie", :tag_list => "中国的")
264
+ yasser = TaggableModel.create(:name => "Yasser", :tag_list => "العربية")
265
+ emo = TaggableModel.create(:name => "Emo", :tag_list => "✏")
266
+
267
+ TaggableModel.tagged_with("日本の").to_a.size.should == 1
268
+ TaggableModel.tagged_with("עברית").to_a.size.should == 1
269
+ TaggableModel.tagged_with("中国的").to_a.size.should == 1
270
+ TaggableModel.tagged_with("العربية").to_a.size.should == 1
271
+ TaggableModel.tagged_with("✏").to_a.size.should == 1
272
+ end
273
+
245
274
  it "should be able to get tag counts on model as a whole" do
246
275
  bob = TaggableModel.create(:name => "Bob", :tag_list => "ruby, rails, css")
247
276
  frank = TaggableModel.create(:name => "Frank", :tag_list => "ruby, rails")
@@ -430,6 +459,14 @@ describe "Taggable" do
430
459
  TaggableModel.tagged_with("fitter, happier", :match_all => true).to_a.should == [steve]
431
460
  end
432
461
 
462
+ it "should be able to find tagged with only the matching tags for a context" do
463
+ bob = TaggableModel.create(:name => "Bob", :tag_list => "lazy, happier", :skill_list => "ruby, rails, css")
464
+ frank = TaggableModel.create(:name => "Frank", :tag_list => "fitter, happier, inefficient", :skill_list => "css")
465
+ steve = TaggableModel.create(:name => 'Steve', :tag_list => "fitter, happier", :skill_list => "ruby, rails, css")
466
+
467
+ TaggableModel.tagged_with("css", :on => :skills, :match_all => true).to_a.should == [frank]
468
+ end
469
+
433
470
  it "should be able to find tagged with some excluded tags" do
434
471
  bob = TaggableModel.create(:name => "Bob", :tag_list => "happier, lazy")
435
472
  frank = TaggableModel.create(:name => "Frank", :tag_list => "happier")
@@ -468,7 +505,7 @@ describe "Taggable" do
468
505
 
469
506
  describe "grouped_column_names_for method" do
470
507
  it "should return all column names joined for Tag GROUP clause" do
471
- @taggable.grouped_column_names_for(ActsAsTaggableOn::Tag).should == "tags.id, tags.name"
508
+ @taggable.grouped_column_names_for(ActsAsTaggableOn::Tag).should == "tags.id, tags.name, tags.taggings_count"
472
509
  end
473
510
 
474
511
  it "should return all column names joined for TaggableModel GROUP clause" do
@@ -654,5 +691,3 @@ describe "Taggable" do
654
691
  end
655
692
  end
656
693
  end
657
-
658
-
@@ -1,7 +1,9 @@
1
1
  ActiveRecord::Schema.define :version => 0 do
2
2
  create_table :tags, :force => true do |t|
3
3
  t.string :name
4
+ t.integer :taggings_count, :default => 0
4
5
  end
6
+ add_index "tags", ["name"], name: "index_tags_on_name", unique: true
5
7
 
6
8
  create_table :taggings, :force => true do |t|
7
9
  t.references :tag
@@ -17,6 +19,10 @@ ActiveRecord::Schema.define :version => 0 do
17
19
 
18
20
  t.datetime :created_at
19
21
  end
22
+ add_index "taggings",
23
+ ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"],
24
+ unique: true, name: "taggings_idx"
25
+
20
26
  # above copied from
21
27
  # generators/acts_as_taggable_on/migration/migration_generator
22
28
 
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.1.0.rc1
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Bleigh
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-02-27 00:00:00.000000000 Z
12
+ date: 2014-04-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -179,9 +179,11 @@ files:
179
179
  - acts-as-taggable-on.gemspec
180
180
  - db/migrate/1_acts_as_taggable_on_migration.rb
181
181
  - db/migrate/2_add_missing_unique_indices.rb
182
+ - db/migrate/3_add_taggings_counter_cache_to_tags.rb
182
183
  - gemfiles/rails_3.2.gemfile
183
184
  - gemfiles/rails_4.0.gemfile
184
185
  - gemfiles/rails_4.1.gemfile
186
+ - gemfiles/rails_edge.gemfile
185
187
  - lib/acts-as-taggable-on.rb
186
188
  - lib/acts_as_taggable_on.rb
187
189
  - lib/acts_as_taggable_on/acts_as_taggable_on/cache.rb
@@ -231,15 +233,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
231
233
  requirements:
232
234
  - - ! '>='
233
235
  - !ruby/object:Gem::Version
234
- version: '0'
236
+ version: 1.9.3
235
237
  required_rubygems_version: !ruby/object:Gem::Requirement
236
238
  requirements:
237
- - - ! '>'
239
+ - - ! '>='
238
240
  - !ruby/object:Gem::Version
239
- version: 1.3.1
241
+ version: '0'
240
242
  requirements: []
241
243
  rubyforge_project:
242
- rubygems_version: 2.2.1
244
+ rubygems_version: 2.2.2
243
245
  signing_key:
244
246
  specification_version: 4
245
247
  summary: Advanced tagging for Rails.