slugworth 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +46 -0
- data/lib/slugworth.rb +38 -4
- data/lib/slugworth/version.rb +1 -1
- data/lib/slugworth_shared_examples.rb +131 -0
- data/spec/sluggle_spec.rb +41 -1
- metadata +20 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a77159f292434b785943b273df3105db32acb74c
|
4
|
+
data.tar.gz: 9b5ba418ef641c443d65812907b537ea54c5b11b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 75f31bc79dc40409230b3350c377ff04f90430d21ec0b0161300a8c3816408f65b715cae1b6d499629a51bfa6a70c9b1f673af7ac4aec2afd91a133cfaa8573d
|
7
|
+
data.tar.gz: bbd860dbb13e3f4abe7a9584d19616334ead8a062226041633536e2090eefdfa4e71fe2161779a7ce3f63d537df0997d16128816d314575088dfccdf125fb28e
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -49,6 +49,52 @@ This provides most of the default slug functionality you would need.
|
|
49
49
|
* `#to_param` has been defined as a paramaterized version of the attribute declared to `slugged_with`.
|
50
50
|
* Validations stating that `slug` is present and unique in the database.
|
51
51
|
|
52
|
+
### Scoping uniqueness
|
53
|
+
|
54
|
+
By default the slug is unique to the entire table, but you can specify the scope of the uniqueness as the following:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
class Product < ActiveRecord::Base
|
58
|
+
include Slugworth
|
59
|
+
belongs_to :user
|
60
|
+
slugged_with :name, scope: :user_id
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
### Updating slugs
|
65
|
+
|
66
|
+
Sometimes you want to update the slug if the attribute is changed.
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
class User < ActiveRecord::Base
|
70
|
+
include Slugworth
|
71
|
+
slugged_with :name, updatable: true
|
72
|
+
end
|
73
|
+
|
74
|
+
user = User.create(name: 'Jack')
|
75
|
+
=> User(id: 1, name: 'Jack', slug: 'jack')
|
76
|
+
|
77
|
+
user.update_attribute(:name, 'John')
|
78
|
+
=> User(id: 1, name: 'John', slug: 'john')
|
79
|
+
```
|
80
|
+
|
81
|
+
### Incremental slugs
|
82
|
+
|
83
|
+
If another record already exists with the same slug it will generate an uniqueness validation error, but if incremental slugs is enabled, then it will append an incremented number to the slug:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
class User < ActiveRecord::Base
|
87
|
+
include Slugworth
|
88
|
+
slugged_with :name, incremental: true
|
89
|
+
end
|
90
|
+
|
91
|
+
User.create(name: 'Jack')
|
92
|
+
=> User(id: 1, name: 'Jack', slug: 'jack')
|
93
|
+
|
94
|
+
User.create(name: 'Jack')
|
95
|
+
=> User(id: 2, name: 'Jack', slug: 'jack-1')
|
96
|
+
```
|
97
|
+
|
52
98
|
## Test Helper
|
53
99
|
|
54
100
|
To aid in testing your models that implement the Slugworth functionality, I've added a shared example group that can be added to your test suite. Add this to your `spec_helper.rb`
|
data/lib/slugworth.rb
CHANGED
@@ -4,14 +4,17 @@ module Slugworth
|
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
6
|
included do
|
7
|
-
cattr_accessor :slug_attribute
|
7
|
+
cattr_accessor :slug_attribute, :slug_scope, :slug_incremental, :slug_updatable
|
8
8
|
before_validation(:add_slug)
|
9
|
-
validates_uniqueness_of :slug
|
10
9
|
end
|
11
10
|
|
12
11
|
module ClassMethods
|
13
|
-
def slugged_with(slug_attribute)
|
12
|
+
def slugged_with(slug_attribute, opts={})
|
14
13
|
self.slug_attribute = slug_attribute
|
14
|
+
self.slug_scope = opts.delete(:scope)
|
15
|
+
self.slug_incremental = opts.delete(:incremental)
|
16
|
+
self.slug_updatable = opts.delete(:updatable)
|
17
|
+
validates_uniqueness_of :slug, scope: slug_scope
|
15
18
|
end
|
16
19
|
|
17
20
|
def find_by_slug!(slug)
|
@@ -27,10 +30,41 @@ module Slugworth
|
|
27
30
|
|
28
31
|
private
|
29
32
|
def add_slug
|
30
|
-
self.slug = processed_slug
|
33
|
+
self.slug = processed_slug if generate_slug?
|
34
|
+
end
|
35
|
+
|
36
|
+
def generate_slug?
|
37
|
+
!slug.present? || slug_updatable && changes[slug_attribute].present?
|
31
38
|
end
|
32
39
|
|
33
40
|
def processed_slug
|
41
|
+
slug_incremental ? process_incremental_slug : parameterized_slug
|
42
|
+
end
|
43
|
+
|
44
|
+
def parameterized_slug
|
34
45
|
public_send(slug_attribute).parameterize
|
35
46
|
end
|
47
|
+
|
48
|
+
def process_incremental_slug
|
49
|
+
slugs = matching_slugs
|
50
|
+
if slugs.include?(parameterized_slug)
|
51
|
+
(1..slugs.size).each do |i|
|
52
|
+
incremented_slug = "#{parameterized_slug}-#{i}"
|
53
|
+
return incremented_slug unless slugs.include?(incremented_slug)
|
54
|
+
end
|
55
|
+
else
|
56
|
+
parameterized_slug
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def matching_slugs
|
61
|
+
table = self.class.arel_table
|
62
|
+
primary_key = self.class.primary_key
|
63
|
+
query = table[:slug].matches("#{parameterized_slug}%")
|
64
|
+
query = query.and(table[primary_key].not_eq(read_attribute(primary_key))) unless new_record?
|
65
|
+
Array.wrap(slug_scope).each do |scope|
|
66
|
+
query = query.and(table[scope].eq(read_attribute(scope)))
|
67
|
+
end
|
68
|
+
self.class.where(query).pluck(:slug)
|
69
|
+
end
|
36
70
|
end
|
data/lib/slugworth/version.rb
CHANGED
@@ -61,3 +61,134 @@ shared_examples_for :has_slug_functionality do
|
|
61
61
|
end
|
62
62
|
end
|
63
63
|
end
|
64
|
+
|
65
|
+
shared_examples_for :has_updatable_slug_functionality do
|
66
|
+
describe "#slug :updatable" do
|
67
|
+
let!(:existing) { described_class.create(described_class.slug_attribute => 'Name') }
|
68
|
+
context "when attribute is changed" do
|
69
|
+
specify "updates the slug" do
|
70
|
+
existing[described_class.slug_attribute] = 'New Name'
|
71
|
+
expect(existing).to be_valid
|
72
|
+
expect(existing.slug).to eq('new-name')
|
73
|
+
end
|
74
|
+
end
|
75
|
+
context "when attribute is not changed" do
|
76
|
+
specify "does not update the slug" do
|
77
|
+
expect(existing).to be_valid
|
78
|
+
expect(existing.slug).to eq('name')
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
shared_examples_for :has_incremental_slug_functionality do
|
85
|
+
describe "#slug :incremental" do
|
86
|
+
context "when slug is already taken" do
|
87
|
+
before do
|
88
|
+
existing = described_class.new(slug: 'taken-slug')
|
89
|
+
existing.save(validate: false)
|
90
|
+
end
|
91
|
+
|
92
|
+
specify "increments the slug" do
|
93
|
+
obj = described_class.new(described_class.slug_attribute => 'Taken Slug')
|
94
|
+
expect(obj).to be_valid
|
95
|
+
expect(obj.slug).to eq('taken-slug-1')
|
96
|
+
end
|
97
|
+
end
|
98
|
+
context "when incremented slug is already taken" do
|
99
|
+
before do
|
100
|
+
existing = described_class.new(slug: 'taken-slug')
|
101
|
+
existing.save(validate: false)
|
102
|
+
existing = described_class.new(slug: 'taken-slug-1')
|
103
|
+
existing.save(validate: false)
|
104
|
+
end
|
105
|
+
|
106
|
+
specify "increments the slug" do
|
107
|
+
obj = described_class.new(described_class.slug_attribute => 'Taken Slug')
|
108
|
+
expect(obj).to be_valid
|
109
|
+
expect(obj.slug).to eq('taken-slug-2')
|
110
|
+
end
|
111
|
+
end
|
112
|
+
context "when existing slug is reset" do
|
113
|
+
let!(:existing) { described_class.create(described_class.slug_attribute => 'New Name') }
|
114
|
+
|
115
|
+
specify "does not increment the slug" do
|
116
|
+
existing.slug = nil
|
117
|
+
expect(existing).to be_valid
|
118
|
+
expect(existing.slug).to eq('new-name')
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
shared_examples_for :has_scoped_slug_functionality do
|
125
|
+
describe "#slug :scope" do
|
126
|
+
context "when slug is already taken on same scope" do
|
127
|
+
before do
|
128
|
+
existing = described_class.new(slug: 'taken-slug', described_class.slug_scope => 1)
|
129
|
+
existing.save(validate: false)
|
130
|
+
end
|
131
|
+
|
132
|
+
specify "object is not valid" do
|
133
|
+
obj = described_class.new(slug: 'taken-slug', described_class.slug_scope => 1)
|
134
|
+
expect(obj).to_not be_valid
|
135
|
+
expect(obj.errors[:slug]).to_not be_empty
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
context "when slug is already taken on another scope" do
|
140
|
+
before do
|
141
|
+
existing = described_class.new(slug: 'taken-slug', described_class.slug_scope => 1)
|
142
|
+
existing.save(validate: false)
|
143
|
+
end
|
144
|
+
|
145
|
+
specify "object is valid" do
|
146
|
+
obj = described_class.new(slug: 'taken-slug', described_class.slug_scope => 2)
|
147
|
+
expect(obj).to be_valid
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
shared_examples_for :has_incremental_scoped_slug_functionality do
|
154
|
+
describe "#slug :incremental" do
|
155
|
+
context "when slug is already taken" do
|
156
|
+
before do
|
157
|
+
existing = described_class.new(slug: 'taken-slug', described_class.slug_scope => 1)
|
158
|
+
existing.save(validate: false)
|
159
|
+
end
|
160
|
+
|
161
|
+
specify "increments the slug" do
|
162
|
+
obj = described_class.new(described_class.slug_attribute => 'Taken Slug', described_class.slug_scope => 1)
|
163
|
+
expect(obj).to be_valid
|
164
|
+
expect(obj.slug).to eq('taken-slug-1')
|
165
|
+
end
|
166
|
+
end
|
167
|
+
context "when incremented slug is already taken" do
|
168
|
+
before do
|
169
|
+
existing = described_class.new(slug: 'taken-slug', described_class.slug_scope => 1)
|
170
|
+
existing.save(validate: false)
|
171
|
+
existing = described_class.new(slug: 'taken-slug-1', described_class.slug_scope => 1)
|
172
|
+
existing.save(validate: false)
|
173
|
+
end
|
174
|
+
|
175
|
+
specify "increments the slug" do
|
176
|
+
obj = described_class.new(described_class.slug_attribute => 'Taken Slug', described_class.slug_scope => 1)
|
177
|
+
expect(obj).to be_valid
|
178
|
+
expect(obj.slug).to eq('taken-slug-2')
|
179
|
+
end
|
180
|
+
end
|
181
|
+
context "when slug is already taken in another scope" do
|
182
|
+
before do
|
183
|
+
existing = described_class.new(slug: 'taken-slug', described_class.slug_scope => 1)
|
184
|
+
existing.save(validate: false)
|
185
|
+
end
|
186
|
+
|
187
|
+
specify "does not increment the slug" do
|
188
|
+
obj = described_class.new(described_class.slug_attribute => 'Taken Slug', described_class.slug_scope => 2)
|
189
|
+
expect(obj).to be_valid
|
190
|
+
expect(obj.slug).to eq('taken-slug')
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
data/spec/sluggle_spec.rb
CHANGED
@@ -3,7 +3,7 @@ require 'slugworth_shared_examples'
|
|
3
3
|
require 'slugworth'
|
4
4
|
|
5
5
|
ActiveRecord::Base.connection.execute(
|
6
|
-
%{CREATE TABLE users (id INTEGER PRIMARY KEY, name STRING, slug STRING);}
|
6
|
+
%{CREATE TABLE users (id INTEGER PRIMARY KEY, name STRING, slug STRING, age INTEGER);}
|
7
7
|
)
|
8
8
|
|
9
9
|
class User < ActiveRecord::Base
|
@@ -14,3 +14,43 @@ end
|
|
14
14
|
describe User do
|
15
15
|
it_behaves_like :has_slug_functionality
|
16
16
|
end
|
17
|
+
|
18
|
+
class IncrementalUser < ActiveRecord::Base
|
19
|
+
self.table_name = 'users'
|
20
|
+
include Slugworth
|
21
|
+
slugged_with :name, incremental: true
|
22
|
+
end
|
23
|
+
|
24
|
+
describe IncrementalUser do
|
25
|
+
it_behaves_like :has_incremental_slug_functionality
|
26
|
+
end
|
27
|
+
|
28
|
+
class UpdatableUser < ActiveRecord::Base
|
29
|
+
self.table_name = 'users'
|
30
|
+
include Slugworth
|
31
|
+
slugged_with :name, updatable: true
|
32
|
+
end
|
33
|
+
|
34
|
+
describe UpdatableUser do
|
35
|
+
it_behaves_like :has_updatable_slug_functionality
|
36
|
+
end
|
37
|
+
|
38
|
+
class ScopedUser < ActiveRecord::Base
|
39
|
+
self.table_name = 'users'
|
40
|
+
include Slugworth
|
41
|
+
slugged_with :name, scope: :age
|
42
|
+
end
|
43
|
+
|
44
|
+
describe ScopedUser do
|
45
|
+
it_behaves_like :has_scoped_slug_functionality
|
46
|
+
end
|
47
|
+
|
48
|
+
class IncrementalScopedUser < ActiveRecord::Base
|
49
|
+
self.table_name = 'users'
|
50
|
+
include Slugworth
|
51
|
+
slugged_with :name, scope: :age, incremental: true
|
52
|
+
end
|
53
|
+
|
54
|
+
describe IncrementalScopedUser do
|
55
|
+
it_behaves_like :has_incremental_scoped_slug_functionality
|
56
|
+
end
|
metadata
CHANGED
@@ -1,111 +1,111 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: slugworth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matt Polito
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-05-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - ~>
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '1.3'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - ~>
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.3'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - ~>
|
45
|
+
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '2.13'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - ~>
|
52
|
+
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '2.13'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: activerecord
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - ~>
|
59
|
+
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: 4.0.0
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - ~>
|
66
|
+
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: 4.0.0
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: pry
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- -
|
73
|
+
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
75
|
version: '0'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- -
|
80
|
+
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: database_cleaner
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- - ~>
|
87
|
+
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
89
|
version: 1.0.1
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- - ~>
|
94
|
+
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: 1.0.1
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
98
|
name: sqlite3
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
|
-
- -
|
101
|
+
- - ">="
|
102
102
|
- !ruby/object:Gem::Version
|
103
103
|
version: '0'
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
|
-
- -
|
108
|
+
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
111
|
description: Easy slug functionality
|
@@ -115,7 +115,7 @@ executables: []
|
|
115
115
|
extensions: []
|
116
116
|
extra_rdoc_files: []
|
117
117
|
files:
|
118
|
-
- .gitignore
|
118
|
+
- ".gitignore"
|
119
119
|
- CHANGELOG.md
|
120
120
|
- Gemfile
|
121
121
|
- LICENSE.txt
|
@@ -137,17 +137,17 @@ require_paths:
|
|
137
137
|
- lib
|
138
138
|
required_ruby_version: !ruby/object:Gem::Requirement
|
139
139
|
requirements:
|
140
|
-
- -
|
140
|
+
- - ">="
|
141
141
|
- !ruby/object:Gem::Version
|
142
142
|
version: 1.9.2
|
143
143
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
144
144
|
requirements:
|
145
|
-
- -
|
145
|
+
- - ">="
|
146
146
|
- !ruby/object:Gem::Version
|
147
147
|
version: '0'
|
148
148
|
requirements: []
|
149
149
|
rubyforge_project:
|
150
|
-
rubygems_version: 2.
|
150
|
+
rubygems_version: 2.4.5
|
151
151
|
signing_key:
|
152
152
|
specification_version: 4
|
153
153
|
summary: Easy slug functionality
|