Skip to content

Commit 9fa17d1

Browse files
committedNov 10, 2018
Merge https://github.com/ambit-contacts/slug into ambit-connects
There are several improvements in this branch from @ambit-contacts which we incorporate here: - add a generic default option vs. throwing an exception when source column generates an empty slug (we made this optional; some might prefer to get the exception) - additional test for partial matches In addition: - reduce number of gem dependencies - dry-up some of the test setup - use explicit vs. implicit validation methods - a tiny bit of trailing whitespace cleanup
2 parents 35e9683 + 31c6773 commit 9fa17d1

File tree

7 files changed

+98
-177
lines changed

7 files changed

+98
-177
lines changed
 

‎Gemfile

+1-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ source 'https://rubygems.org'
33
group :development, :test do
44
gem 'minitest'
55
gem 'minitest-reporters'
6-
gem 'minitest-focus'
7-
gem 'minitest-spec-rails'
8-
gem 'rails'
6+
gem 'activerecord'
97
gem 'rake'
108
gem 'sqlite3'
119
end

‎Gemfile.lock

+1-86
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,6 @@
11
GEM
22
remote: https://rubygems.org/
33
specs:
4-
actioncable (5.1.4)
5-
actionpack (= 5.1.4)
6-
nio4r (~> 2.0)
7-
websocket-driver (~> 0.6.1)
8-
actionmailer (5.1.4)
9-
actionpack (= 5.1.4)
10-
actionview (= 5.1.4)
11-
activejob (= 5.1.4)
12-
mail (~> 2.5, >= 2.5.4)
13-
rails-dom-testing (~> 2.0)
14-
actionpack (5.1.4)
15-
actionview (= 5.1.4)
16-
activesupport (= 5.1.4)
17-
rack (~> 2.0)
18-
rack-test (>= 0.6.3)
19-
rails-dom-testing (~> 2.0)
20-
rails-html-sanitizer (~> 1.0, >= 1.0.2)
21-
actionview (5.1.4)
22-
activesupport (= 5.1.4)
23-
builder (~> 3.1)
24-
erubi (~> 1.4)
25-
rails-dom-testing (~> 2.0)
26-
rails-html-sanitizer (~> 1.0, >= 1.0.3)
27-
activejob (5.1.4)
28-
activesupport (= 5.1.4)
29-
globalid (>= 0.3.6)
304
activemodel (5.1.4)
315
activesupport (= 5.1.4)
326
activerecord (5.1.4)
@@ -42,87 +16,28 @@ GEM
4216
arel (8.0.0)
4317
builder (3.2.3)
4418
concurrent-ruby (1.0.5)
45-
crass (1.0.4)
46-
erubi (1.7.0)
47-
globalid (0.4.1)
48-
activesupport (>= 4.2.0)
4919
i18n (0.9.1)
5020
concurrent-ruby (~> 1.0)
51-
loofah (2.2.3)
52-
crass (~> 1.0.2)
53-
nokogiri (>= 1.5.9)
54-
mail (2.7.0)
55-
mini_mime (>= 0.1.1)
56-
method_source (0.9.0)
57-
mini_mime (1.0.0)
58-
mini_portile2 (2.3.0)
5921
minitest (5.10.3)
60-
minitest-focus (1.1.2)
61-
minitest (>= 4, < 6)
6222
minitest-reporters (1.1.19)
6323
ansi
6424
builder
6525
minitest (>= 5.0)
6626
ruby-progressbar
67-
minitest-spec-rails (5.4.0)
68-
minitest (~> 5.0)
69-
rails (>= 4.1)
70-
nio4r (2.1.0)
71-
nokogiri (1.8.5)
72-
mini_portile2 (~> 2.3.0)
73-
rack (2.0.5)
74-
rack-test (0.8.2)
75-
rack (>= 1.0, < 3)
76-
rails (5.1.4)
77-
actioncable (= 5.1.4)
78-
actionmailer (= 5.1.4)
79-
actionpack (= 5.1.4)
80-
actionview (= 5.1.4)
81-
activejob (= 5.1.4)
82-
activemodel (= 5.1.4)
83-
activerecord (= 5.1.4)
84-
activesupport (= 5.1.4)
85-
bundler (>= 1.3.0)
86-
railties (= 5.1.4)
87-
sprockets-rails (>= 2.0.0)
88-
rails-dom-testing (2.0.3)
89-
activesupport (>= 4.2.0)
90-
nokogiri (>= 1.6)
91-
rails-html-sanitizer (1.0.4)
92-
loofah (~> 2.2, >= 2.2.2)
93-
railties (5.1.4)
94-
actionpack (= 5.1.4)
95-
activesupport (= 5.1.4)
96-
method_source
97-
rake (>= 0.8.7)
98-
thor (>= 0.18.1, < 2.0)
9927
rake (12.3.0)
10028
ruby-progressbar (1.9.0)
101-
sprockets (3.7.2)
102-
concurrent-ruby (~> 1.0)
103-
rack (> 1, < 3)
104-
sprockets-rails (3.2.1)
105-
actionpack (>= 4.0)
106-
activesupport (>= 4.0)
107-
sprockets (>= 3.0.0)
10829
sqlite3 (1.3.13)
109-
thor (0.20.0)
11030
thread_safe (0.3.6)
11131
tzinfo (1.2.4)
11232
thread_safe (~> 0.1)
113-
websocket-driver (0.6.5)
114-
websocket-extensions (>= 0.1.0)
115-
websocket-extensions (0.1.3)
11633

11734
PLATFORMS
11835
ruby
11936

12037
DEPENDENCIES
38+
activerecord
12139
minitest
122-
minitest-focus
12340
minitest-reporters
124-
minitest-spec-rails
125-
rails
12641
rake
12742
sqlite3
12843

‎lib/slug/slug.rb

+16-6
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,22 @@ module ClassMethods
1616
# Note that subsequent changes to the source column will have no effect on the slug.
1717
# If you'd like to update the slug later on, call <tt>@model.set_slug</tt>
1818
def slug source, opts={}
19-
class_attribute :slug_source, :slug_column
19+
class_attribute :slug_source, :slug_column, :generic_default
2020
include InstanceMethods
2121

2222
self.slug_source = source
23-
24-
self.slug_column = opts.has_key?(:column) ? opts[:column] : :slug
23+
self.slug_column = opts.fetch(:column, :slug)
24+
self.generic_default = opts.fetch(:generic_default, false)
2525

2626
uniqueness_opts = {}
2727
uniqueness_opts.merge!(:if => opts[:validate_uniqueness_if]) if opts[:validate_uniqueness_if].present?
28+
validates_uniqueness_of self.slug_column, uniqueness_opts
2829

29-
validates self.slug_column, :presence => :true, :message => "cannot be blank. Is #{self.slug_source} sluggable?"
30-
validates self.slug_column, :uniqueness => :true, uniqueness_opts
31-
validates self.slug_column, :format => :with => /\A[a-z0-9-]+\z/, :message => "contains invalid characters. Only downcase letters, numbers, and '-' are allowed."
30+
validates_presence_of self.slug_column,
31+
message: "cannot be blank. Is #{self.slug_source} sluggable?"
32+
validates_format_of self.slug_column,
33+
with: /\A[a-z0-9-]+\z/,
34+
message: "contains invalid characters. Only downcase letters, numbers, and '-' are allowed."
3235
before_validation :set_slug, :on => :create
3336
end
3437
end
@@ -46,6 +49,7 @@ def set_slug(opts={})
4649

4750
strip_diacritics_from_slug
4851
normalize_slug
52+
genericize_slug if generic_default
4953
assign_slug_sequence unless self[self.slug_column] == original_slug # don't try to increment seq if slug hasn't changed
5054
end
5155

@@ -85,6 +89,12 @@ def normalize_slug
8589
self[self.slug_column] = s.to_s
8690
end
8791

92+
def genericize_slug
93+
if self[self.slug_column].blank?
94+
self[self.slug_column] = self.class.to_s.demodulize.underscore.dasherize
95+
end
96+
end
97+
8898
# Converts accented characters to their ASCII equivalents and removes them if they have no equivalent.
8999
# Override this with a void function if you don't want accented characters to be stripped.
90100
def strip_diacritics_from_slug

‎test/models.rb

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Used to test slug behavior in general
22
class Article < ActiveRecord::Base
3-
slug :headline
3+
slug :headline
44
end
55

66
class Storyline < Article
@@ -23,8 +23,14 @@ class Post < ActiveRecord::Base
2323
# Used to test slugs based on methods rather than database attributes
2424
class Event < ActiveRecord::Base
2525
slug :title_for_slug
26-
26+
2727
def title_for_slug
2828
"#{title}-#{location}"
2929
end
30-
end
30+
end
31+
32+
# Test generation of generic slugs
33+
class Generation < ActiveRecord::Base
34+
slug :title, generic_default: true
35+
end
36+

‎test/schema.rb

+5
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,9 @@
2727
t.column "location", "string"
2828
t.column "slug", "string"
2929
end
30+
31+
create_table "generations", :force => true do |t|
32+
t.column "title", "string"
33+
t.column "slug", "string", null: false
34+
end
3035
end

‎test/slug_test.rb

+65-75
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
# encoding: utf-8
2+
require 'test_helper'
23

3-
require File.dirname(__FILE__) + '/test_helper'
4+
describe Slug do
5+
before do
6+
Article.delete_all
7+
end
48

5-
class SlugTest < ActiveSupport::TestCase
69
describe 'slug' do
710
it "bases slug on specified source column" do
8-
Article.delete_all
911
article = Article.create!(:headline => 'Test Headline')
1012
assert_equal 'test-headline', article.slug
1113
end
1214

1315
it "bases slug on specified source column, even if it is defined as a method rather than database attribute" do
14-
Article.delete_all
1516
article = Event.create!(:title => 'Test Event', :location => 'Portland')
1617
assert_equal 'test-event-portland', article.slug
1718
end
1819

1920
describe "slug column" do
2021
it "saves slug to 'slug' column by default" do
21-
Article.delete_all
2222
article = Article.create!(:headline => 'Test Headline')
2323
assert_equal 'test-headline', article.slug
2424
end
@@ -43,30 +43,41 @@ class SlugTest < ActiveSupport::TestCase
4343
end
4444
end
4545

46+
describe 'generates a generic slug' do
47+
before do
48+
Generation.delete_all
49+
end
50+
51+
it "if source column is empty" do
52+
generation = Generation.create!
53+
assert_equal 'generation', generation.slug
54+
end
55+
56+
it "if normalization makes source value empty" do
57+
generation = Generation.create!(:title => '$$$')
58+
assert_equal 'generation', generation.slug
59+
end
60+
61+
it "if source value contains no Latin characters" do
62+
generation = Generation.create!(:title => 'ローマ字がない')
63+
assert_equal 'generation', generation.slug
64+
end
65+
end
66+
4667
describe 'validation' do
4768
it "sets validation error if source column is empty" do
48-
Article.delete_all
4969
article = Article.create
5070
assert !article.valid?
5171
assert article.errors[:slug]
5272
end
5373

5474
it "sets validation error if normalization makes source value empty" do
55-
Article.delete_all
5675
article = Article.create(:headline => '$$$')
5776
assert !article.valid?
5877
assert article.errors[:slug]
5978
end
6079

61-
it "doesn't update the slug even if the source column changes" do
62-
Article.delete_all
63-
article = Article.create!(:headline => 'Test Headline')
64-
article.update_attributes!(:headline => 'New Headline')
65-
assert_equal 'test-headline', article.slug
66-
end
67-
6880
it "validates slug format on save" do
69-
Article.delete_all
7081
article = Article.create!(:headline => 'Test Headline')
7182
article.slug = 'A BAD $LUG.'
7283

@@ -75,7 +86,6 @@ class SlugTest < ActiveSupport::TestCase
7586
end
7687

7788
it "validates uniqueness of slug by default" do
78-
Article.delete_all
7989
Article.create!(:headline => 'Test Headline')
8090
article2 = Article.create!(:headline => 'Test Headline')
8191
article2.slug = 'test-headline'
@@ -91,17 +101,21 @@ class SlugTest < ActiveSupport::TestCase
91101

92102
assert article2.valid?
93103
end
104+
end
94105

95-
it "doesn't overwrite slug value on create if it was already specified" do
96-
Article.delete_all
97-
a = Article.create!(:headline => 'Test Headline', :slug => 'slug1')
98-
assert_equal 'slug1', a.slug
99-
end
106+
it "doesn't overwrite slug value on create if it was already specified" do
107+
a = Article.create!(:headline => 'Test Headline', :slug => 'slug1')
108+
assert_equal 'slug1', a.slug
109+
end
110+
111+
it "doesn't update the slug even if the source column changes" do
112+
article = Article.create!(:headline => 'Test Headline')
113+
article.update_attributes!(:headline => 'New Headline')
114+
assert_equal 'test-headline', article.slug
100115
end
101116

102117
describe "resetting a slug" do
103118
before do
104-
Article.delete_all
105119
@article = Article.create(:headline => 'test headline')
106120
@original_slug = @article.slug
107121
end
@@ -126,123 +140,101 @@ class SlugTest < ActiveSupport::TestCase
126140
end
127141

128142
describe "slug normalization" do
129-
it "lowercases strings" do
130-
Article.delete_all
143+
before do
131144
@article = Article.new
145+
end
146+
147+
it "lowercases strings" do
132148
@article.headline = 'AbC'
133149
@article.save!
134150
assert_equal "abc", @article.slug
135151
end
136152

137153
it "replaces whitespace with dashes" do
138-
Article.delete_all
139-
@article = Article.new
140154
@article.headline = 'a b'
141155
@article.save!
142156
assert_equal 'a-b', @article.slug
143157
end
144158

145159
it "replaces 2spaces with 1dash" do
146-
Article.delete_all
147-
@article = Article.new
148160
@article.headline = 'a b'
149161
@article.save!
150162
assert_equal 'a-b', @article.slug
151163
end
152164

153165
it "removes punctuation" do
154-
Article.delete_all
155-
@article = Article.new
156166
@article.headline = 'abc!@#$%^&*•¶§∞¢££¡¿()><?""\':;][]\.,/'
157167
@article.save!
158168
assert_match 'abc', @article.slug
159169
end
160170

161171
it "strips trailing space" do
162-
Article.delete_all
163-
@article = Article.new
164172
@article.headline = 'ab '
165173
@article.save!
166174
assert_equal 'ab', @article.slug
167175
end
168176

169177
it "strips leading space" do
170-
Article.delete_all
171-
@article = Article.new
172178
@article.headline = ' ab'
173179
@article.save!
174180
assert_equal 'ab', @article.slug
175181
end
176182

177183
it "strips trailing dashes" do
178-
Article.delete_all
179-
@article = Article.new
180184
@article.headline = 'ab-'
181185
@article.save!
182186
assert_match 'ab', @article.slug
183187
end
184188

185189
it "strips leading dashes" do
186-
Article.delete_all
187-
@article = Article.new
188190
@article.headline = '-ab'
189191
@article.save!
190192
assert_match 'ab', @article.slug
191193
end
192194

193195
it "remove double-dashes" do
194-
Article.delete_all
195-
@article = Article.new
196196
@article.headline = 'a--b--c'
197197
@article.save!
198198
assert_match 'a-b-c', @article.slug
199199
end
200200

201201
it "doesn't modify valid slug strings" do
202-
Article.delete_all
203-
@article = Article.new
204202
@article.headline = 'a-b-c-d'
205203
@article.save!
206204
assert_match 'a-b-c-d', @article.slug
207205
end
208206

209207
it "doesn't insert dashes for periods in acronyms, regardless of where they appear in string" do
210-
Article.delete_all
211-
@article = Article.new
212208
@article.headline = "N.Y.P.D. vs. N.S.A. vs. F.B.I."
213209
@article.save!
214210
assert_match 'nypd-vs-nsa-vs-fbi', @article.slug
215211
end
216212

217213
it "doesn't insert dashes for apostrophes" do
218-
Article.delete_all
219-
@article = Article.new
220214
@article.headline = "Thomas Jefferson's Papers"
221215
@article.save!
222216
assert_match 'thomas-jeffersons-papers', @article.slug
223217
end
224218

225219
it "preserves numbers in slug" do
226-
Article.delete_all
227-
@article = Article.new
228220
@article.headline = "2010 Election"
229221
@article.save!
230222
assert_match '2010-election', @article.slug
231223
end
232224
end
233225

234226
describe "diacritics handling" do
235-
it "strips diacritics" do
236-
Article.delete_all
227+
before do
237228
@article = Article.new
229+
end
230+
231+
it "strips diacritics" do
238232
@article.headline = "açaí"
239233
@article.save!
240234
assert_equal "acai", @article.slug
241235
end
242236

243237
it "strips diacritics correctly " do
244-
Article.delete_all
245-
@article = Article.new
246238
@article.headline = "ÀÁÂÃÄÅÆÇÈÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ"
247239
@article.save!
248240
expected = "aaaaaaaeceeeiiiidnoooooouuuuythssaaaaaaaeceeeeiiiidnoooooouuuuythy".split(//)
@@ -255,57 +247,55 @@ class SlugTest < ActiveSupport::TestCase
255247

256248
describe "sequence handling" do
257249
it "doesn't add a sequence if saving first instance of slug" do
258-
Article.delete_all
259250
article = Article.create!(:headline => 'Test Headline')
260251
assert_equal 'test-headline', article.slug
261252
end
262253

263254
it "assigns a -1 suffix to the second instance of the slug" do
264-
Article.delete_all
265255
Article.create!(:headline => 'Test Headline')
266256
article_2 = Article.create!(:headline => 'Test Headline')
267257
assert_equal 'test-headline-1', article_2.slug
268258
end
269259

270260
it 'assigns a -2 suffux to the third instance of the slug containing numbers' do
271-
Article.delete_all
272261
2.times { |i| Article.create! :headline => '11111' }
273262
article_3 = Article.create! :headline => '11111'
274263
assert_equal '11111-2', article_3.slug
275264
end
276265

266+
it "assigns a -12 suffix to the thirteenth instance of the slug" do
267+
12.times { |i| Article.create!(:headline => 'Test Headline') }
268+
article_13 = Article.create!(:headline => 'Test Headline')
269+
assert_equal 'test-headline-12', article_13.slug
270+
271+
12.times { |i| Article.create!(:headline => 'latest from lybia') }
272+
article_13 = Article.create!(:headline => 'latest from lybia')
273+
assert_equal 'latest-from-lybia-12', article_13.slug
274+
end
275+
276+
it "ignores partial matches when calculating sequence" do
277+
article_1 = Article.create!(:headline => 'Test Headline')
278+
assert_equal 'test-headline', article_1.slug
279+
article_2 = Article.create!(:headline => 'Test')
280+
assert_equal 'test', article_2.slug
281+
article_3 = Article.create!(:headline => 'Test')
282+
assert_equal 'test-1', article_3.slug
283+
article_4 = Article.create!(:headline => 'Test')
284+
assert_equal 'test-2', article_4.slug
285+
end
286+
277287
it "knows about single table inheritance" do
278-
Article.delete_all
279288
article = Article.create!(:headline => 'Test Headline')
280289
story = Storyline.create!(:headline => article.headline)
281290
assert_equal 'test-headline-1', story.slug
282291
end
283292

284293
it "correctly slugs for partial matches" do
285-
Article.delete_all
286294
rap_metal = Article.create!(:headline => 'Rap Metal')
287295
assert_equal 'rap-metal', rap_metal.slug
288296

289297
rap = Article.create!(:headline => 'Rap')
290298
assert_equal('rap', rap.slug)
291299
end
292-
293-
it "assigns a -12 suffix to the thirteenth instance of the slug" do
294-
Article.delete_all
295-
12.times { |i| Article.create!(:headline => 'Test Headline') }
296-
article_13 = Article.create!(:headline => 'Test Headline')
297-
assert_equal 'test-headline-12', article_13.slug
298-
299-
12.times { |i| Article.create!(:headline => 'latest from lybia') }
300-
article_13 = Article.create!(:headline => 'latest from lybia')
301-
assert_equal 'latest-from-lybia-12', article_13.slug
302-
end
303-
304-
it 'assigns a -2 suffux to the third instance of the slug containing numbers' do
305-
Article.delete_all
306-
2.times { |i| Article.create! :headline => '11111' }
307-
article_3 = Article.create! :headline => '11111'
308-
assert_equal '11111-2', article_3.slug
309-
end
310300
end
311301
end

‎test/test_helper.rb

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
require 'rubygems'
2+
require 'minitest/autorun'
23
require 'minitest/reporters'
3-
require 'minitest/focus'
4-
require 'minitest-spec-rails'
54

65
# You can use "rake test AR_VERSION=2.0.5" to test against 2.0.5, for example.
76
# The default is to use the latest installed ActiveRecord.
87
if ENV["AR_VERSION"]
98
gem 'activerecord', "#{ENV["AR_VERSION"]}"
10-
gem 'activesupport', "#{ENV["AR_VERSION"]}"
119
end
1210
require 'active_record'
13-
require 'active_support'
1411

1512
# color test output
1613
Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new(:color => true)]

0 commit comments

Comments
 (0)
Please sign in to comment.