gutentag 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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