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 +4 -4
- data/.travis.yml +4 -0
- data/Appraisals +7 -0
- data/Gemfile +5 -0
- data/README.md +96 -55
- data/Rakefile +7 -2
- data/db/migrate/1_gutentag_tables.rb +3 -1
- data/db/migrate/2_gutentag_cache_counter.rb +3 -1
- data/db/migrate/3_no_null_counters.rb +3 -1
- data/gemfiles/rails_3_2.gemfile +1 -0
- data/gemfiles/rails_4_0.gemfile +1 -0
- data/gemfiles/rails_4_1.gemfile +1 -0
- data/gemfiles/rails_5_1.gemfile +8 -0
- data/gutentag.gemspec +3 -1
- data/lib/gutentag.rb +7 -10
- data/lib/gutentag/active_record.rb +12 -40
- data/lib/gutentag/active_record/class_methods.rb +19 -0
- data/lib/gutentag/active_record/instance_methods.rb +21 -0
- data/lib/gutentag/engine.rb +1 -1
- data/lib/gutentag/has_many_tags.rb +10 -0
- data/lib/gutentag/tagged_with.rb +38 -0
- data/lib/gutentag/tagged_with/id_query.rb +9 -0
- data/lib/gutentag/tagged_with/name_query.rb +15 -0
- data/lib/gutentag/tagged_with/query.rb +25 -0
- data/spec/gutentag/active_record_spec.rb +146 -19
- data/spec/gutentag_spec.rb +9 -0
- data/spec/internal/app/models/article.rb +1 -1
- data/spec/internal/config/database.yml +13 -2
- data/spec/models/gutentag/tag_spec.rb +2 -2
- data/spec/spec_helper.rb +1 -1
- metadata +40 -6
- data/lib/gutentag/tag_name.rb +0 -17
- data/lib/gutentag/tag_names.rb +0 -21
- data/spec/gutentag/tag_name_spec.rb +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d6dfcf9cc249f5d24513e805b16a351641f06520
|
4
|
+
data.tar.gz: 819247c5802512d2de7cccaea882f6faa8caa3cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dd5025ccfab3083978980ea492d725235f550cec508d9a955c36ca8bb8337f44027aff7b56fc7d165b96d3fea0fb944369c54917d44ea050dcf243004748d84c
|
7
|
+
data.tar.gz: '0191ae96a804109c49e990f2bde2e6192d2f0ae37f8f8bea68a12a1294e3e0ce717a179162793982a889332ef80c125bfb9955c004cc9979fc86ca4787dd578a'
|
data/.travis.yml
CHANGED
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
|
-
##
|
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
|
-
|
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
|
-
|
20
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
43
|
-
|
102
|
+
add_index :gutentag_tags, :name, unique: true
|
103
|
+
add_index :gutentag_tags, :taggings_count
|
104
|
+
```
|
44
105
|
|
45
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
-
|
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
|
-
|
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,4 +1,6 @@
|
|
1
|
-
|
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
|
-
|
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
|
-
|
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
|
data/gemfiles/rails_3_2.gemfile
CHANGED
data/gemfiles/rails_4_0.gemfile
CHANGED
data/gemfiles/rails_4_1.gemfile
CHANGED
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.
|
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 ||=
|
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.
|
47
|
-
|
48
|
-
|
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
|
-
|
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
|
-
|
4
|
-
extend ActiveSupport::Concern
|
8
|
+
model.after_save :persist_tags
|
5
9
|
|
6
|
-
|
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
|
data/lib/gutentag/engine.rb
CHANGED
@@ -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,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!(:
|
5
|
+
let!(:melbourne_article) do
|
6
6
|
article = Article.create :title => 'Overview'
|
7
|
-
article.tag_names << '
|
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!(:
|
19
|
+
let!(:melbourne_oregon_article) do
|
20
20
|
article = Article.create
|
21
|
-
article.tag_names = %w(oregon
|
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('
|
27
|
+
subject { Article.tagged_with(:names => 'melbourne') }
|
28
28
|
|
29
29
|
it { expect(subject.count).to eq 2 }
|
30
|
-
it { is_expected.to include
|
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(:
|
35
|
+
subject { Article.tagged_with(:names => :melbourne) }
|
36
36
|
|
37
37
|
it { expect(subject.count).to eq 2 }
|
38
|
-
it { is_expected.to include
|
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('
|
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
|
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(
|
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
|
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('
|
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
|
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(
|
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
|
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(
|
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
|
76
|
-
it { is_expected.not_to include 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
|
@@ -1,3 +1,14 @@
|
|
1
|
+
<% if ENV["DATABASE"] == "mysql" %>
|
1
2
|
test:
|
2
|
-
adapter:
|
3
|
-
database:
|
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
|
42
|
+
allow(Gutentag.normaliser).to receive(:call).and_return('waffles')
|
43
43
|
end
|
44
44
|
|
45
45
|
it "normalises the provided name" do
|
46
|
-
|
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
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.
|
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-
|
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/
|
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.
|
209
|
+
rubygems_version: 2.6.11
|
176
210
|
signing_key:
|
177
211
|
specification_version: 4
|
178
212
|
summary: Good Tags
|
data/lib/gutentag/tag_name.rb
DELETED
data/lib/gutentag/tag_names.rb
DELETED
@@ -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
|