gutentag 0.8.0 → 0.9.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,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a963454a3117dc737b8d4e70e8a02c22c2c2693e
4
- data.tar.gz: bc7c540a51edbabb378370b9157451a29dcbbfdb
3
+ metadata.gz: d6dfcf9cc249f5d24513e805b16a351641f06520
4
+ data.tar.gz: 819247c5802512d2de7cccaea882f6faa8caa3cb
5
5
  SHA512:
6
- metadata.gz: aaf39b46f22f2348b87dbc8d574d17d5d2b5728810980c16b1c344d894900701ff3434e43f2794c6ce3ce2515039de6fe887161110ee0aa4f0aa54db8152b1b1
7
- data.tar.gz: 835f63c380604816cd1e5014960f3e75e199f167705635f45cea36467c288b0da7e504c1fa4ad8d35763aec26714cb6a754d58821ed54a244ca906e39cac7d0d
6
+ metadata.gz: dd5025ccfab3083978980ea492d725235f550cec508d9a955c36ca8bb8337f44027aff7b56fc7d165b96d3fea0fb944369c54917d44ea050dcf243004748d84c
7
+ data.tar.gz: '0191ae96a804109c49e990f2bde2e6192d2f0ae37f8f8bea68a12a1294e3e0ce717a179162793982a889332ef80c125bfb9955c004cc9979fc86ca4787dd578a'
data/.travis.yml CHANGED
@@ -9,3 +9,7 @@ before_install:
9
9
  - gem install bundler
10
10
  before_script:
11
11
  - bundle exec appraisal install
12
+ env:
13
+ - DATABASE=postgres
14
+ - DATABASE=mysql
15
+ - DATABASE=sqlite
data/Appraisals CHANGED
@@ -2,18 +2,21 @@ appraise 'rails_3_2' do
2
2
  gem 'rails', '~> 3.2.22.5'
3
3
  gem 'rack', '~> 1.0', :platforms => [:ruby_20, :ruby_21]
4
4
  gem 'nokogiri', '~> 1.6.0', :platforms => [:ruby_20]
5
+ gem 'mysql2', '~> 0.3.10'
5
6
  end if RUBY_VERSION.to_f < 2.2
6
7
 
7
8
  appraise 'rails_4_0' do
8
9
  gem 'rails', '~> 4.0.13'
9
10
  gem 'rack', '~> 1.0', :platforms => [:ruby_20, :ruby_21]
10
11
  gem 'nokogiri', '~> 1.6.0', :platforms => [:ruby_20]
12
+ gem 'mysql2', '~> 0.3.10'
11
13
  end if RUBY_VERSION.to_f < 2.4
12
14
 
13
15
  appraise 'rails_4_1' do
14
16
  gem 'rails', '~> 4.1.16'
15
17
  gem 'rack', '~> 1.0', :platforms => [:ruby_20, :ruby_21]
16
18
  gem 'nokogiri', '~> 1.6.0', :platforms => [:ruby_20]
19
+ gem 'mysql2', '~> 0.3.13'
17
20
  end if RUBY_VERSION.to_f < 2.4
18
21
 
19
22
  appraise 'rails_4_2' do
@@ -25,3 +28,7 @@ end if RUBY_VERSION.to_f < 2.4
25
28
  appraise 'rails_5_0' do
26
29
  gem 'rails', '~> 5.0.1'
27
30
  end if RUBY_VERSION.to_f >= 2.2
31
+
32
+ appraise 'rails_5_1' do
33
+ gem 'rails', '~> 5.1.0'
34
+ end if RUBY_VERSION.to_f >= 2.2
data/Gemfile CHANGED
@@ -5,3 +5,8 @@ gemspec
5
5
  gem 'test-unit', :platform => :ruby_22
6
6
  gem 'rack', '~> 1.0' if RUBY_VERSION.to_f <= 2.1
7
7
  gem 'nokogiri', '~> 1.6.0' if RUBY_VERSION.to_f <= 2.0
8
+
9
+ gem 'combustion', '~> 0.6',
10
+ :git => 'https://github.com/pat/combustion.git',
11
+ :branch => 'master',
12
+ :ref => 'ef434634d7'
data/README.md CHANGED
@@ -8,41 +8,109 @@ A good, simple, solid tagging extension for ActiveRecord.
8
8
 
9
9
  This was built partly as a proof-of-concept, and partly to see how a tagging gem could work when it's not all stuffed within models, and partly just because I wanted a simpler tagging library. If you want to know more, read [this blog post](http://freelancing-gods.com/posts/gutentag_simple_rails_tagging).
10
10
 
11
- ## Installation
11
+ ## Contents
12
+
13
+ * [Usage](#usage)
14
+ * [Installation](#installation)
15
+ * [Upgrading](#upgrading)
16
+ * [Contribution](#contribution)
17
+ * [Licence](#licence)
18
+
19
+ <h2 id="usage">Usage</h2>
20
+
21
+ The first step is easy: add the tag associations to whichever models should have tags (in these examples, the Article model):
22
+
23
+ ```Ruby
24
+ class Article < ActiveRecord::Base
25
+ # ...
26
+ Gutentag::ActiveRecord.call self
27
+ # ...
28
+ end
29
+ ```
30
+
31
+ That's all it takes to get a tags association on each article. Of course, populating tags can be a little frustrating, unless you want to manage Gutentag::Tag instances yourself? As an alternative, just use the tag_names accessor to get/set tags via string representations.
32
+
33
+ ```Ruby
34
+ article.tag_names #=> ['pancakes', 'melbourne', 'ruby']
35
+ article.tag_names << 'portland'
36
+ article.tag_names #=> ['pancakes', 'melbourne', 'ruby', 'portland']
37
+ article.tag_names -= ['ruby']
38
+ article.tag_names #=> ['pancakes', 'melbourne', 'portland']
39
+ ```
40
+
41
+ Changes to tag_names are not persisted immediately - you must save your taggable object to have the tag changes reflected in your database:
42
+
43
+ ```Ruby
44
+ article.tag_names << 'ruby'
45
+ article.save
46
+ ```
47
+
48
+ You can also query for instances with specified tags. The default `:match` mode is `:any`, and so provides OR logic, not AND - it'll match any instances that have _any_ of the tags or tag names:
49
+
50
+ ```Ruby
51
+ Article.tagged_with(:names => ['tag1', 'tag2'], :match => :any)
52
+ Article.tagged_with(
53
+ :tags => Gutentag::Tag.where(name: ['tag1', 'tag2']),
54
+ :match => :any
55
+ )
56
+ Article.tagged_with(:ids => [tag_id], :match => :any)
57
+ ```
58
+
59
+ To return records that have _all_ specified tags, use `:match => :all`:
60
+
61
+ ```ruby
62
+ # Returns all articles that have *both* tag_a and tag_b.
63
+ Article.tagged_with(:ids => [tag_a.id, tag_b.id], :match => :all)
64
+ ```
65
+
66
+ <h2 id="installation">Installation</h2>
12
67
 
13
68
  Get it into your Gemfile - and don't forget the version constraint!
14
69
 
15
- gem 'gutentag', '~> 0.8.0'
70
+ ```Ruby
71
+ gem 'gutentag', '~> 0.9.0'
72
+ ```
16
73
 
17
74
  Next: your tags get persisted to your database, so let's import and run the migrations to get the tables set up:
18
75
 
19
- rake gutentag:install:migrations
20
- rake db:migrate
76
+ ```Bash
77
+ rake gutentag:install:migrations
78
+ rake db:migrate
79
+ ```
21
80
 
22
81
  If you want to use Gutentag outside of Rails, you can. However, this means you lose the migration import rake task. As a workaround, here's the expected schema (as of 0.7.0):
23
82
 
24
- create_table :gutentag_taggings do |t|
25
- t.integer :tag_id, :null => false
26
- t.integer :taggable_id, :null => false
27
- t.string :taggable_type, :null => false
28
- t.timestamps :null => false
29
- end
83
+ ```Ruby
84
+ create_table :gutentag_taggings do |t|
85
+ t.integer :tag_id, null: false
86
+ t.integer :taggable_id, null: false
87
+ t.string :taggable_type, null: false
88
+ t.timestamps null: false
89
+ end
30
90
 
31
- add_index :gutentag_taggings, :tag_id
32
- add_index :gutentag_taggings, [:taggable_type, :taggable_id]
33
- add_index :gutentag_taggings, [:taggable_type, :taggable_id, :tag_id],
34
- :unique => true, :name => 'unique_taggings'
91
+ add_index :gutentag_taggings, :tag_id
92
+ add_index :gutentag_taggings, [:taggable_type, :taggable_id]
93
+ add_index :gutentag_taggings, [:taggable_type, :taggable_id, :tag_id],
94
+ unique: true, name: 'unique_taggings'
35
95
 
36
- create_table :gutentag_tags do |t|
37
- t.string :name, :null => false
38
- t.integer :taggings_count, :null => false, :default => 0
39
- t.timestamps :null => false
40
- end
96
+ create_table :gutentag_tags do |t|
97
+ t.string :name, null: false
98
+ t.integer :taggings_count, null: false, default: 0
99
+ t.timestamps null: false
100
+ end
41
101
 
42
- add_index :gutentag_tags, :name, :unique => true
43
- add_index :gutentag_tags, :taggings_count
102
+ add_index :gutentag_tags, :name, unique: true
103
+ add_index :gutentag_tags, :taggings_count
104
+ ```
44
105
 
45
- ## Upgrading
106
+ <h2 id="upgrading">Upgrading</h2>
107
+
108
+ ### 0.9.0
109
+
110
+ * In your models with tags, change `has_many_tags` to `Gutentag::ActiveRecord.call self`.
111
+ * Any calls to `tagged_with` should change from `Model.tagged_with('ruby', 'pancakes')` to `Model.tagged_with(:names => ['ruby', 'pancakes'])`.
112
+
113
+ In both of the above cases, the old behaviour will continue to work for 0.9.x releases, but with a deprecation warning.
46
114
 
47
115
  ### 0.8.0
48
116
 
@@ -62,42 +130,15 @@ Between 0.4.0 and 0.5.0, Gutentag switched table names from `tags` and `taggings
62
130
 
63
131
  If you were using Gutentag 0.4.0 (or older) and now want to upgrade, you'll need to create a migration manually that renames these tables:
64
132
 
65
- rename_table :tags, :gutentag_tags
66
- rename_table :taggings, :gutentag_taggings
67
-
68
- ## Usage
69
-
70
- The first step is easy: add the tag associations to whichever models should have tags (in these examples, the Article model):
71
-
72
- class Article < ActiveRecord::Base
73
- # ...
74
- has_many_tags
75
- # ...
76
- end
77
-
78
- That's all it takes to get a tags association on each article. Of course, populating tags can be a little frustrating, unless you want to manage Gutentag::Tag instances yourself? As an alternative, just use the tag_names accessor to get/set tags via string representations.
79
-
80
- article.tag_names #=> ['pancakes', 'melbourne', 'ruby']
81
- article.tag_names << 'portland'
82
- article.tag_names #=> ['pancakes', 'melbourne', 'ruby', 'portland']
83
- article.tag_names -= ['ruby']
84
- article.tag_names #=> ['pancakes', 'melbourne', 'portland']
85
-
86
- Changes to tag_names are not persisted immediately - you must save your taggable object to have the tag changes reflected in your database:
87
-
88
- article.tag_names << 'ruby'
89
- article.save
90
-
91
- You can also query for instances with specified tags. This is OR logic, not AND - it'll match any instances that have *any* of the tags or tag names.
92
-
93
- Article.tagged_with('tag1', 'tag2')
94
- Article.tagged_with(Gutentag::Tag.where(name: ['tag1', 'tag2'])
95
- # => [#<Article id: "123">]
133
+ ```Ruby
134
+ rename_table :tags, :gutentag_tags
135
+ rename_table :taggings, :gutentag_taggings
136
+ ```
96
137
 
97
- ## Contribution
138
+ <h2 id="contribution">Contribution</h2>
98
139
 
99
140
  Please note that this project now has a [Contributor Code of Conduct](http://contributor-covenant.org/version/1/0/0/). By participating in this project you agree to abide by its terms.
100
141
 
101
- ## Licence
142
+ <h2 id="licence">Licence</h2>
102
143
 
103
144
  Copyright (c) 2013-2015, Gutentag is developed and maintained by Pat Allan, and is released under the open MIT Licence.
data/Rakefile CHANGED
@@ -1,3 +1,8 @@
1
- require 'bundler'
1
+ require 'bundler/setup'
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
2
4
 
3
- Bundler::GemHelper.install_tasks
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :default => [:test]
8
+ task :test => [:spec]
@@ -1,4 +1,6 @@
1
- class GutentagTables < ActiveRecord::Migration
1
+ superclass = ActiveRecord::VERSION::MAJOR < 5 ?
2
+ ActiveRecord::Migration : ActiveRecord::Migration[4.2]
3
+ class GutentagTables < superclass
2
4
  def up
3
5
  create_table :gutentag_taggings do |t|
4
6
  t.integer :tag_id, :null => false
@@ -1,4 +1,6 @@
1
- class GutentagCacheCounter < ActiveRecord::Migration
1
+ superclass = ActiveRecord::VERSION::MAJOR < 5 ?
2
+ ActiveRecord::Migration : ActiveRecord::Migration[4.2]
3
+ class GutentagCacheCounter < superclass
2
4
  def up
3
5
  add_column :gutentag_tags, :taggings_count, :integer, :default => 0
4
6
  add_index :gutentag_tags, :taggings_count
@@ -1,4 +1,6 @@
1
- class NoNullCounters < ActiveRecord::Migration
1
+ superclass = ActiveRecord::VERSION::MAJOR < 5 ?
2
+ ActiveRecord::Migration : ActiveRecord::Migration[4.2]
3
+ class NoNullCounters < superclass
2
4
  def up
3
5
  change_column :gutentag_tags, :taggings_count, :integer, :default => 0,
4
6
  :null => false
@@ -6,5 +6,6 @@ gem "test-unit", :platform => :ruby_22
6
6
  gem "rack", "~> 1.0", :platforms => [:ruby_20, :ruby_21]
7
7
  gem "rails", "~> 3.2.22.5"
8
8
  gem "nokogiri", "~> 1.6.0", :platforms => [:ruby_20]
9
+ gem "mysql2", "~> 0.3.10"
9
10
 
10
11
  gemspec :path => "../"
@@ -6,5 +6,6 @@ gem "test-unit", :platform => :ruby_22
6
6
  gem "rails", "~> 4.0.13"
7
7
  gem "rack", "~> 1.0", :platforms => [:ruby_20, :ruby_21]
8
8
  gem "nokogiri", "~> 1.6.0", :platforms => [:ruby_20]
9
+ gem "mysql2", "~> 0.3.10"
9
10
 
10
11
  gemspec :path => "../"
@@ -6,5 +6,6 @@ gem "test-unit", :platform => :ruby_22
6
6
  gem "rails", "~> 4.1.16"
7
7
  gem "rack", "~> 1.0", :platforms => [:ruby_20, :ruby_21]
8
8
  gem "nokogiri", "~> 1.6.0", :platforms => [:ruby_20]
9
+ gem "mysql2", "~> 0.3.13"
9
10
 
10
11
  gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "test-unit", :platform => :ruby_22
6
+ gem "rails", "~> 5.1.0"
7
+
8
+ gemspec :path => "../"
data/gutentag.gemspec CHANGED
@@ -1,7 +1,7 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  Gem::Specification.new do |s|
3
3
  s.name = 'gutentag'
4
- s.version = '0.8.0'
4
+ s.version = '0.9.0'
5
5
  s.authors = ['Pat Allan']
6
6
  s.email = ['pat@freelancing-gods.com']
7
7
  s.homepage = 'https://github.com/pat/gutentag'
@@ -18,6 +18,8 @@ Gem::Specification.new do |s|
18
18
  s.add_development_dependency 'appraisal', '~> 2.1.0'
19
19
  s.add_development_dependency 'bundler', '>= 1.7.12'
20
20
  s.add_development_dependency 'combustion', '0.5.5'
21
+ s.add_development_dependency 'mysql2'
22
+ s.add_development_dependency 'pg'
21
23
  s.add_development_dependency 'rails'
22
24
  s.add_development_dependency 'rspec-rails', '~> 3.1'
23
25
  s.add_development_dependency 'sqlite3', '~> 1.3.7'
data/lib/gutentag.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'active_record/version'
2
+ require 'active_support/deprecation'
2
3
 
3
4
  module Gutentag
4
5
  def self.dirtier
@@ -10,7 +11,7 @@ module Gutentag
10
11
  end
11
12
 
12
13
  def self.normaliser
13
- @normaliser ||= Gutentag::TagName
14
+ @normaliser ||= lambda { |tag_name| tag_name.to_s.downcase }
14
15
  end
15
16
 
16
17
  def self.normaliser=(normaliser)
@@ -28,10 +29,10 @@ end
28
29
 
29
30
  require 'gutentag/active_record'
30
31
  require 'gutentag/dirty'
32
+ require 'gutentag/has_many_tags'
31
33
  require 'gutentag/persistence'
32
- require 'gutentag/tag_name'
33
- require 'gutentag/tag_names'
34
34
  require 'gutentag/tag_validations'
35
+ require 'gutentag/tagged_with'
35
36
 
36
37
  if ActiveRecord::VERSION::MAJOR == 3
37
38
  Gutentag.dirtier = Gutentag::Dirty
@@ -43,11 +44,7 @@ if defined?(Rails::Engine)
43
44
  require 'gutentag/engine'
44
45
  else
45
46
  require 'active_record'
46
- ActiveRecord::Base.send :include, Gutentag::ActiveRecord
47
- require File.expand_path(
48
- './../app/models/gutentag/tag', File.dirname(__FILE__)
49
- )
50
- require File.expand_path(
51
- './../app/models/gutentag/tagging', File.dirname(__FILE__)
52
- )
47
+ ActiveRecord::Base.extend Gutentag::HasManyTags
48
+ require_relative '../app/models/gutentag/tag'
49
+ require_relative '../app/models/gutentag/tagging'
53
50
  end
@@ -1,44 +1,16 @@
1
- require 'active_support/concern'
1
+ class Gutentag::ActiveRecord
2
+ def self.call(model)
3
+ model.has_many :taggings, :class_name => 'Gutentag::Tagging',
4
+ :as => :taggable, :dependent => :destroy
5
+ model.has_many :tags, :class_name => 'Gutentag::Tag',
6
+ :through => :taggings
2
7
 
3
- module Gutentag::ActiveRecord
4
- extend ActiveSupport::Concern
8
+ model.after_save :persist_tags
5
9
 
6
- UNIQUENESS_METHOD = ActiveRecord::VERSION::MAJOR == 3 ? :uniq : :distinct
7
-
8
- module ClassMethods
9
- def has_many_tags
10
- has_many :taggings, :class_name => 'Gutentag::Tagging', :as => :taggable,
11
- :dependent => :destroy
12
- has_many :tags, :class_name => 'Gutentag::Tag',
13
- :through => :taggings
14
-
15
- after_save :persist_tags
16
- end
17
-
18
- def tagged_with(*tags)
19
- joins(:tags).where(
20
- Gutentag::Tag.table_name => {:name => Gutentag::TagNames.call(tags)}
21
- ).public_send UNIQUENESS_METHOD
22
- end
23
- end
24
-
25
- def reset_tag_names
26
- @tag_names = nil
27
- end
28
-
29
- def tag_names
30
- @tag_names ||= tags.pluck(:name)
31
- end
32
-
33
- def tag_names=(names)
34
- Gutentag.dirtier.call self, names if Gutentag.dirtier
35
-
36
- @tag_names = names
37
- end
38
-
39
- private
40
-
41
- def persist_tags
42
- Gutentag::Persistence.new(self).persist
10
+ model.send :extend, Gutentag::ActiveRecord::ClassMethods
11
+ model.send :include, Gutentag::ActiveRecord::InstanceMethods
43
12
  end
44
13
  end
14
+
15
+ require 'gutentag/active_record/class_methods'
16
+ require 'gutentag/active_record/instance_methods'
@@ -0,0 +1,19 @@
1
+ module Gutentag::ActiveRecord::ClassMethods
2
+ def tagged_with(*arguments)
3
+ arguments.flatten!
4
+
5
+ case arguments.first
6
+ when Hash
7
+ Gutentag::TaggedWith.call(self, arguments.first)
8
+ when Integer
9
+ ActiveSupport::Deprecation.warn "Calling tagged_with with an array of integers will not be supported in Gutentag 1.0. Please use tagged_with :ids => [1, 2] instead."
10
+ Gutentag::TaggedWith.call(self, :ids => arguments)
11
+ when Gutentag::Tag
12
+ ActiveSupport::Deprecation.warn "Calling tagged_with with an array of tags will not be supported in Gutentag 1.0. Please use tagged_with :tags => [tag_a, tag_b] instead."
13
+ Gutentag::TaggedWith.call(self, :tags => arguments)
14
+ else
15
+ ActiveSupport::Deprecation.warn "Calling tagged_with with an array of strings will not be supported in Gutentag 1.0. Please use tagged_with :names => [\"melbourne\", \"ruby\"] instead."
16
+ Gutentag::TaggedWith.call(self, :names => arguments)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ module Gutentag::ActiveRecord::InstanceMethods
2
+ def reset_tag_names
3
+ @tag_names = nil
4
+ end
5
+
6
+ def tag_names
7
+ @tag_names ||= tags.pluck(:name)
8
+ end
9
+
10
+ def tag_names=(names)
11
+ Gutentag.dirtier.call self, names if Gutentag.dirtier
12
+
13
+ @tag_names = names
14
+ end
15
+
16
+ private
17
+
18
+ def persist_tags
19
+ Gutentag::Persistence.new(self).persist
20
+ end
21
+ end
@@ -2,6 +2,6 @@ class Gutentag::Engine < Rails::Engine
2
2
  engine_name :gutentag
3
3
 
4
4
  ActiveSupport.on_load :active_record do
5
- include Gutentag::ActiveRecord
5
+ extend Gutentag::HasManyTags
6
6
  end
7
7
  end
@@ -0,0 +1,10 @@
1
+ module Gutentag::HasManyTags
2
+ def has_many_tags
3
+ ActiveSupport::Deprecation.warn <<-TXT
4
+ has_many_tags is now deprecated, and will be removed in Gutentag v1.0.0.
5
+ Replace it with Gutentag::ActiveRecord.call(self), and everything will continue
6
+ to work as expected.
7
+ TXT
8
+ Gutentag::ActiveRecord.call self
9
+ end
10
+ end
@@ -0,0 +1,38 @@
1
+ class Gutentag::TaggedWith
2
+ def self.call(model, options)
3
+ new(model, options).call
4
+ end
5
+
6
+ def initialize(model, options)
7
+ @model = model
8
+ @options = options
9
+ end
10
+
11
+ def call
12
+ query_class.new(model, values, match).call
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :model, :options
18
+
19
+ def match
20
+ options[:match] || :any
21
+ end
22
+
23
+ def query_class
24
+ options[:names] ? NameQuery : IDQuery
25
+ end
26
+
27
+ def values
28
+ if options[:tags]
29
+ Array(options[:tags]).collect(&:id)
30
+ else
31
+ options[:ids] || options[:names]
32
+ end
33
+ end
34
+ end
35
+
36
+ require 'gutentag/tagged_with/query'
37
+ require 'gutentag/tagged_with/id_query'
38
+ require 'gutentag/tagged_with/name_query'
@@ -0,0 +1,9 @@
1
+ class Gutentag::TaggedWith::IDQuery < Gutentag::TaggedWith::Query
2
+ private
3
+
4
+ def taggable_ids_query
5
+ Gutentag::Tagging.select(:taggable_id).
6
+ where(:taggable_type => model.name).
7
+ where(:tag_id => values)
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ class Gutentag::TaggedWith::NameQuery < Gutentag::TaggedWith::Query
2
+ def initialize(model, values, match)
3
+ super
4
+
5
+ @values = @values.collect { |tag| Gutentag.normaliser.call(tag) }
6
+ end
7
+
8
+ private
9
+
10
+ def taggable_ids_query
11
+ Gutentag::Tagging.joins(:tag).select(:taggable_id).
12
+ where(:taggable_type => model.name).
13
+ where(Gutentag::Tag.table_name => {:name => values})
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ class Gutentag::TaggedWith::Query
2
+ def initialize(model, values, match)
3
+ @model = model
4
+ @values = Array values
5
+ @match = match
6
+ end
7
+
8
+ def call
9
+ model.where "#{model_id} IN (#{query.to_sql})"
10
+ end
11
+
12
+ private
13
+
14
+ attr_reader :model, :values, :match
15
+
16
+ def model_id
17
+ "#{model.quoted_table_name}.#{model.quoted_primary_key}"
18
+ end
19
+
20
+ def query
21
+ return taggable_ids_query if match == :any || values.length == 1
22
+
23
+ taggable_ids_query.having("COUNT(*) = #{values.length}").group(:taggable_id)
24
+ end
25
+ end
@@ -2,9 +2,9 @@ require 'spec_helper'
2
2
 
3
3
  describe Gutentag::ActiveRecord do
4
4
  describe '.tagged_with' do
5
- let!(:melborne_article) do
5
+ let!(:melbourne_article) do
6
6
  article = Article.create :title => 'Overview'
7
- article.tag_names << 'melborne'
7
+ article.tag_names << 'melbourne'
8
8
  article.save!
9
9
  article
10
10
  end
@@ -16,64 +16,191 @@ describe Gutentag::ActiveRecord do
16
16
  article
17
17
  end
18
18
 
19
- let!(:melborne_oregon_article) do
19
+ let!(:melbourne_oregon_article) do
20
20
  article = Article.create
21
- article.tag_names = %w(oregon melborne)
21
+ article.tag_names = %w(oregon melbourne)
22
22
  article.save!
23
23
  article
24
24
  end
25
25
 
26
26
  context 'given a single tag name' do
27
- subject { Article.tagged_with('melborne') }
27
+ subject { Article.tagged_with(:names => 'melbourne') }
28
28
 
29
29
  it { expect(subject.count).to eq 2 }
30
- it { is_expected.to include melborne_article, melborne_oregon_article }
30
+ it { is_expected.to include melbourne_article, melbourne_oregon_article }
31
31
  it { is_expected.not_to include oregon_article }
32
32
  end
33
33
 
34
34
  context 'given a single tag name[symbol]' do
35
- subject { Article.tagged_with(:melborne) }
35
+ subject { Article.tagged_with(:names => :melbourne) }
36
36
 
37
37
  it { expect(subject.count).to eq 2 }
38
- it { is_expected.to include melborne_article, melborne_oregon_article }
38
+ it { is_expected.to include melbourne_article, melbourne_oregon_article }
39
+ it { is_expected.not_to include oregon_article }
40
+ end
41
+
42
+ context 'given a denormalized tag name' do
43
+ subject { Article.tagged_with(:names => "MelbournE") }
44
+
45
+ it { expect(subject.count).to eq 2 }
46
+ it { is_expected.to include melbourne_article, melbourne_oregon_article }
39
47
  it { is_expected.not_to include oregon_article }
40
48
  end
41
49
 
42
50
  context 'given multiple tag names' do
43
- subject { Article.tagged_with('melborne', 'oregon') }
51
+ subject { Article.tagged_with(:names => ['melbourne', 'oregon']) }
44
52
 
45
53
  it { expect(subject.count).to eq 3 }
46
- it { is_expected.to include melborne_article, oregon_article, melborne_oregon_article }
54
+ it { is_expected.to include melbourne_article, oregon_article, melbourne_oregon_article }
47
55
  end
48
56
 
49
57
  context 'given an array of tag names' do
50
- subject { Article.tagged_with(%w(melborne oregon)) }
58
+ subject { Article.tagged_with(:names => %w(melbourne oregon)) }
51
59
 
52
60
  it { expect(subject.count).to eq 3 }
53
- it { is_expected.to include melborne_article, oregon_article, melborne_oregon_article }
61
+ it { is_expected.to include melbourne_article, oregon_article, melbourne_oregon_article }
54
62
  end
55
63
 
56
64
  context 'given a single tag instance' do
57
- subject { Article.tagged_with(Gutentag::Tag.find_by_name('melborne')) }
65
+ subject { Article.tagged_with(:tags => Gutentag::Tag.find_by_name!('melbourne')) }
66
+
67
+ it { expect(subject.count).to eq 2 }
68
+ it { is_expected.to include melbourne_article, melbourne_oregon_article }
69
+ it { is_expected.not_to include oregon_article }
70
+ it { expect(subject.to_sql).not_to include 'gutentag_tags' }
71
+ end
72
+
73
+ context 'given a single tag id' do
74
+ subject { Article.tagged_with(:ids => Gutentag::Tag.find_by_name!('melbourne').id) }
58
75
 
59
76
  it { expect(subject.count).to eq 2 }
60
- it { is_expected.to include melborne_article, melborne_oregon_article }
77
+ it { is_expected.to include melbourne_article, melbourne_oregon_article }
61
78
  it { is_expected.not_to include oregon_article }
79
+ it { expect(subject.to_sql).not_to include 'gutentag_tags' }
62
80
  end
63
81
 
64
82
  context 'given multiple tag objects' do
65
- subject { Article.tagged_with(Gutentag::Tag.where(name: %w(melborne oregon))) }
83
+ subject { Article.tagged_with(:tags => Gutentag::Tag.where(name: %w(melbourne oregon))) }
66
84
 
67
85
  it { expect(subject.count).to eq 3 }
68
- it { is_expected.to include melborne_article, oregon_article, melborne_oregon_article }
86
+ it { is_expected.to include melbourne_article, oregon_article, melbourne_oregon_article }
87
+ it { expect(subject.to_sql).not_to include 'gutentag_tags' }
69
88
  end
70
89
 
71
90
  context 'chaining where clause' do
72
- subject { Article.tagged_with(%w(melborne oregon)).where(title: 'Overview') }
91
+ subject { Article.tagged_with(:names => %w(melbourne oregon)).where(title: 'Overview') }
92
+
93
+ it { expect(subject.count).to eq 1 }
94
+ it { is_expected.to include melbourne_article }
95
+ it { is_expected.not_to include oregon_article, melbourne_oregon_article }
96
+ end
97
+
98
+ context 'appended onto a relation' do
99
+ subject { Article.where(title: 'Overview').tagged_with(:names => %w(melbourne oregon)) }
100
+
101
+ it { expect(subject.count).to eq 1 }
102
+ it { is_expected.to include melbourne_article }
103
+ it { is_expected.not_to include oregon_article, melbourne_oregon_article }
104
+ end
105
+
106
+ context 'matching against all tags' do
107
+ subject { Article.tagged_with(:names => %w(melbourne oregon), :match => :all) }
73
108
 
74
109
  it { expect(subject.count).to eq 1 }
75
- it { is_expected.to include melborne_article }
76
- it { is_expected.not_to include oregon_article, melborne_oregon_article }
110
+ it { is_expected.to include melbourne_oregon_article }
111
+ it { is_expected.not_to include oregon_article, melbourne_article }
112
+ end
113
+
114
+ context 'matching against all tag ids' do
115
+ subject { Article.tagged_with(:ids => Gutentag::Tag.where(:name => %w(melbourne oregon)).pluck(:id), :match => :all) }
116
+
117
+ it { expect(subject.count).to eq 1 }
118
+ it { is_expected.to include melbourne_oregon_article }
119
+ it { is_expected.not_to include oregon_article, melbourne_article }
120
+ end
121
+
122
+ context 'matching against all one tag is the same as any' do
123
+ subject { Article.tagged_with(:names => %w(melbourne), :match => :all) }
124
+
125
+ it { expect(subject.count).to eq 2 }
126
+ it { is_expected.to include melbourne_article, melbourne_oregon_article }
127
+ it { is_expected.not_to include oregon_article }
128
+ end
129
+
130
+ context "deprecated" do
131
+ before { expect(ActiveSupport::Deprecation).to receive(:warn) }
132
+
133
+ context 'given a single tag name' do
134
+ subject { Article.tagged_with('melbourne') }
135
+
136
+ it { expect(subject.count).to eq 2 }
137
+ it { is_expected.to include melbourne_article, melbourne_oregon_article }
138
+ it { is_expected.not_to include oregon_article }
139
+ end
140
+
141
+ context 'given a single tag name[symbol]' do
142
+ subject { Article.tagged_with(:melbourne) }
143
+
144
+ it { expect(subject.count).to eq 2 }
145
+ it { is_expected.to include melbourne_article, melbourne_oregon_article }
146
+ it { is_expected.not_to include oregon_article }
147
+ end
148
+
149
+ context 'given a denormalized tag name' do
150
+ subject { Article.tagged_with("MelbournE") }
151
+
152
+ it { expect(subject.count).to eq 2 }
153
+ it { is_expected.to include melbourne_article, melbourne_oregon_article }
154
+ it { is_expected.not_to include oregon_article }
155
+ end
156
+
157
+ context 'given multiple tag names' do
158
+ subject { Article.tagged_with('melbourne', 'oregon') }
159
+
160
+ it { expect(subject.count).to eq 3 }
161
+ it { is_expected.to include melbourne_article, oregon_article, melbourne_oregon_article }
162
+ end
163
+
164
+ context 'given an array of tag names' do
165
+ subject { Article.tagged_with(%w(melbourne oregon)) }
166
+
167
+ it { expect(subject.count).to eq 3 }
168
+ it { is_expected.to include melbourne_article, oregon_article, melbourne_oregon_article }
169
+ end
170
+
171
+ context 'given a single tag instance' do
172
+ subject { Article.tagged_with(Gutentag::Tag.find_by_name!('melbourne')) }
173
+
174
+ it { expect(subject.count).to eq 2 }
175
+ it { is_expected.to include melbourne_article, melbourne_oregon_article }
176
+ it { is_expected.not_to include oregon_article }
177
+ it { expect(subject.to_sql).not_to include 'gutentag_tags' }
178
+ end
179
+
180
+ context 'given a single tag id' do
181
+ subject { Article.tagged_with(Gutentag::Tag.find_by_name!('melbourne').id) }
182
+
183
+ it { expect(subject.count).to eq 2 }
184
+ it { is_expected.to include melbourne_article, melbourne_oregon_article }
185
+ it { is_expected.not_to include oregon_article }
186
+ it { expect(subject.to_sql).not_to include 'gutentag_tags' }
187
+ end
188
+
189
+ context 'given multiple tag objects' do
190
+ subject { Article.tagged_with(Gutentag::Tag.where(name: %w(melbourne oregon))) }
191
+
192
+ it { expect(subject.count).to eq 3 }
193
+ it { is_expected.to include melbourne_article, oregon_article, melbourne_oregon_article }
194
+ it { expect(subject.to_sql).not_to include 'gutentag_tags' }
195
+ end
196
+
197
+ context 'chaining where clause' do
198
+ subject { Article.tagged_with(%w(melbourne oregon)).where(title: 'Overview') }
199
+
200
+ it { expect(subject.count).to eq 1 }
201
+ it { is_expected.to include melbourne_article }
202
+ it { is_expected.not_to include oregon_article, melbourne_oregon_article }
203
+ end
77
204
  end
78
205
  end
79
206
  end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe Gutentag do
4
+ describe '.normalizer' do
5
+ it "downcases the provided name" do
6
+ expect(Gutentag.normaliser.call('Tasty Pancakes')).to eq('tasty pancakes')
7
+ end
8
+ end
9
+ end
@@ -1,3 +1,3 @@
1
1
  class Article < ActiveRecord::Base
2
- has_many_tags
2
+ Gutentag::ActiveRecord.call self
3
3
  end
@@ -1,3 +1,14 @@
1
+ <% if ENV["DATABASE"] == "mysql" %>
1
2
  test:
2
- adapter: sqlite3
3
- database: db/combustion_test.sqlite
3
+ adapter: mysql2
4
+ database: gutentag
5
+ username: root
6
+ <% elsif ENV["DATABASE"] == "postgres" %>
7
+ test:
8
+ adapter: postgresql
9
+ database: gutentag
10
+ <% else %>
11
+ test:
12
+ adapter: sqlite3
13
+ database: db/gutentag.sqlite
14
+ <% end %>
@@ -39,11 +39,11 @@ describe Gutentag::Tag, :type => :model do
39
39
 
40
40
  describe '#name' do
41
41
  before :each do
42
- allow(Gutentag::TagName).to receive(:call).and_return('waffles')
42
+ allow(Gutentag.normaliser).to receive(:call).and_return('waffles')
43
43
  end
44
44
 
45
45
  it "normalises the provided name" do
46
- expect(Gutentag::TagName).to receive(:call).with('Pancakes').
46
+ allow(Gutentag.normaliser).to receive(:call).with('Pancakes').
47
47
  and_return('waffles')
48
48
 
49
49
  Gutentag::Tag.create!(:name => 'Pancakes')
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'bundler'
1
+ require 'bundler/setup'
2
2
 
3
3
  Bundler.require :default, :development
4
4
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gutentag
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pat Allan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-01-19 00:00:00.000000000 Z
11
+ date: 2017-06-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -66,6 +66,34 @@ dependencies:
66
66
  - - '='
67
67
  - !ruby/object:Gem::Version
68
68
  version: 0.5.5
69
+ - !ruby/object:Gem::Dependency
70
+ name: mysql2
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pg
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
69
97
  - !ruby/object:Gem::Dependency
70
98
  name: rails
71
99
  requirement: !ruby/object:Gem::Requirement
@@ -132,19 +160,25 @@ files:
132
160
  - gemfiles/rails_4_1.gemfile
133
161
  - gemfiles/rails_4_2.gemfile
134
162
  - gemfiles/rails_5_0.gemfile
163
+ - gemfiles/rails_5_1.gemfile
135
164
  - gutentag.gemspec
136
165
  - lib/gutentag.rb
137
166
  - lib/gutentag/active_record.rb
167
+ - lib/gutentag/active_record/class_methods.rb
168
+ - lib/gutentag/active_record/instance_methods.rb
138
169
  - lib/gutentag/dirty.rb
139
170
  - lib/gutentag/engine.rb
171
+ - lib/gutentag/has_many_tags.rb
140
172
  - lib/gutentag/persistence.rb
141
- - lib/gutentag/tag_name.rb
142
- - lib/gutentag/tag_names.rb
143
173
  - lib/gutentag/tag_validations.rb
174
+ - lib/gutentag/tagged_with.rb
175
+ - lib/gutentag/tagged_with/id_query.rb
176
+ - lib/gutentag/tagged_with/name_query.rb
177
+ - lib/gutentag/tagged_with/query.rb
144
178
  - spec/acceptance/tag_names_spec.rb
145
179
  - spec/acceptance/tags_spec.rb
146
180
  - spec/gutentag/active_record_spec.rb
147
- - spec/gutentag/tag_name_spec.rb
181
+ - spec/gutentag_spec.rb
148
182
  - spec/internal/app/models/article.rb
149
183
  - spec/internal/config/database.yml
150
184
  - spec/internal/db/schema.rb
@@ -172,7 +206,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
172
206
  version: '0'
173
207
  requirements: []
174
208
  rubyforge_project:
175
- rubygems_version: 2.6.8
209
+ rubygems_version: 2.6.11
176
210
  signing_key:
177
211
  specification_version: 4
178
212
  summary: Good Tags
@@ -1,17 +0,0 @@
1
- class Gutentag::TagName
2
- def self.call(name)
3
- new(name).to_s
4
- end
5
-
6
- def initialize(name)
7
- @name = name
8
- end
9
-
10
- def to_s
11
- name.downcase
12
- end
13
-
14
- private
15
-
16
- attr_reader :name
17
- end
@@ -1,21 +0,0 @@
1
- class Gutentag::TagNames
2
- def self.call(*names)
3
- new(names).call
4
- end
5
-
6
- def initialize(names)
7
- @names = names.flatten
8
- end
9
-
10
- def call
11
- name_values.collect { |name| Gutentag::TagName.call name }
12
- end
13
-
14
- private
15
-
16
- attr_reader :names
17
-
18
- def name_values
19
- names.collect { |name| name.respond_to?(:name) ? name.name : name.to_s }
20
- end
21
- end
@@ -1,9 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe Gutentag::TagName do
4
- describe '.call' do
5
- it "downcases the provided name" do
6
- expect(Gutentag::TagName.call('Tasty Pancakes')).to eq('tasty pancakes')
7
- end
8
- end
9
- end