slug 0.8.0 → 4.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b26d7fca3befe175649b0bdbe3b1e145e775ea77191fbe023f017d907cdfd64c
4
+ data.tar.gz: '06786f698b25c0e2d061ca77cbafa382e774bb96ded86a7b246c858909e8a1b5'
5
+ SHA512:
6
+ metadata.gz: 248ca7e65bc8ccc47a9cec02c3843f4c1871aa743078627677078c4a52dbde1666226002033c0d5e4a107b27fdb32feb1f51c3d877c815bdb455cdf8dbe87bbf
7
+ data.tar.gz: dfab8ccf8d2daf51720cbc7ecfb44b9fb3ad5862f85e5e7a44b236170cbb1ac7caeaff52434764d80ed5f62bd0d11b2e104699a1011fe5ca3349e63db868cc16
@@ -8,19 +8,17 @@ What's different:
8
8
 
9
9
  * Unlike friendly_id, slugs are stored directly in your model's table. friendly_id stores its data in a separate sluggable table, which enables cool things like slug versioning--but forces yet another join when trying to do complex find_by_slugs.
10
10
  * Like friendly_id, diacritics (accented characters) are stripped from slug strings.
11
- * Most importantly, there are just two options: source column (for example, headline) and an optional slug column (if for some reason you don't want your slugs stored in a column called slug)
11
+ * The number of options is manageable
12
12
 
13
13
  == INSTALLATION
14
14
 
15
- sudo gem install slug
15
+ Add the gem to your Gemfile
16
16
 
17
- then add
17
+ gem 'slug'
18
18
 
19
- config.gem 'slug'
19
+ of your rails project.
20
20
 
21
- to your rails project.
22
-
23
- Note that this version of slug is only compatible with Rails/active_support/active_record 3.x+. Rails 2 applications should use slug 0.5.3.
21
+ This is tested with Rails 5.1.4, MRI Ruby 2.4.1
24
22
 
25
23
  == USAGE
26
24
 
@@ -42,11 +40,7 @@ Once your table is migrated, just add
42
40
 
43
41
  to your ActiveRecord model. <tt>:source_field</tt> is the column you'd like to base the slug on--for example, it might be <tt>:headline</tt>.
44
42
 
45
- If you want to save the slug in a database column that isn't called <tt>slug</tt>, just pass the <tt>:column</tt> option. For example
46
-
47
- slug :headline, :column => :web_slug
48
-
49
- would generate a slug based on <tt>headline</tt> and save it to <tt>web_slug</tt>.
43
+ === INSTANCE METHOD AS SOURCE COLUMN
50
44
 
51
45
  The source column isn't limited to just database attributes--you can specify any instance method. This is handy if you need special formatting on your source column before it's slugged, or if you want to base the slug on several attributes.
52
46
 
@@ -59,14 +53,43 @@ For example:
59
53
  "#{headline}-#{publication_date.year}-#{publication_date.month}"
60
54
  end
61
55
  end
62
- end
63
56
 
64
57
  would use headline-pub year-pub month as the slug source.
65
58
 
66
59
  From here, you can work with your slug as if it were a normal column--<tt>find_by_slug</tt> and named scopes will work as they do for any other column.
67
60
 
68
- A few notes:
69
- * Slug validates presence and uniqueness of the slug column. If you pass something that isn't sluggable as the source (for example, say you set the headline to '---'), a validation error will be set.
61
+ == OPTIONS
62
+ There are two options:
63
+ - <tt>:column</tt> is the slug_column, if for some reason you don't want
64
+ your slugs stored in a column called "slug"
65
+ - <tt>:generic_default</tt> is a boolean which, if true, will help you
66
+ avoid <tt>ActiveRecord::ValidationError</tt> exceptions on the slug column.
67
+
68
+ === COLUMN OPTION
69
+ If you want to save the slug in a database column that isn't called
70
+ <tt>slug</tt>, just pass the <tt>:column</tt> option. For example
71
+
72
+ slug :headline, :column => :web_slug
73
+
74
+ would generate a slug based on <tt>headline</tt> and save it to <tt>web_slug</tt>.
75
+
76
+ === GENERIC DEFAULT OPTION
77
+ You can avoid <tt>ActiveRecord::ValidationError</tt> exceptions that can
78
+ occur if the source column is empty, blank, or only contains filtered
79
+ characters. Simply set <tt>generic_default: true</tt>. For example
80
+
81
+ slug :headline, generic_default: true
82
+
83
+ will generate a slug based on your model name if the headline is blank.
84
+
85
+ This is useful if the source column (e.g. headline) is based on user-generated
86
+ input or can be blank (nil or empty).
87
+
88
+ Some prefer to get the exception in this case. Others want to get a good
89
+ slug and move on.
90
+
91
+ == NOTES
92
+ * Slug validates presence and uniqueness of the slug column. If you pass something that isn't sluggable as the source (for example, say you set the headline to '---'), a validation error will be set. (See the <tt>:generic_default</tt> option for avoiding this.
70
93
  * Slug doesn't update the slug if the source column changes. If you really need to regenerate the slug, call <tt>@model.set_slug</tt> before save.
71
94
  * If a slug already exists, Slug will automatically append a '-n' suffix to your slug to make it unique. The second instance of a slug is '-1'.
72
95
  * If you don't like the slug formatting or the accented character stripping doesn't work for you, it's easy to override Slug's formatting functions. Check the source for details.
@@ -76,4 +99,6 @@ A few notes:
76
99
  Ben Koski, ben.koski@gmail.com
77
100
 
78
101
  with generous contributions from:
79
- Derek Willis, @derekwillis
102
+ - Derek Willis, @derekwillis
103
+ - others listed in the commit history, which can be found in the
104
+ {GitHub contributor list}[https://github.com/wbreeze/slug/graphs/contributors]
data/Rakefile CHANGED
@@ -1,22 +1,5 @@
1
1
  require 'rake'
2
2
 
3
- begin
4
- require 'jeweler'
5
- Jeweler::Tasks.new do |s|
6
- s.name = "slug"
7
- s.summary = %Q{Simple, straightforward slugs for your ActiveRecord models.}
8
- s.email = "ben.koski@gmail.com"
9
- s.homepage = "http://github.com/bkoski/slug"
10
- s.description = "Simple, straightforward slugs for your ActiveRecord models."
11
- s.add_dependency 'activerecord', '> 3.0.0'
12
- s.add_dependency 'activesupport', '> 3.0.0'
13
- s.authors = ["Ben Koski"]
14
- end
15
- Jeweler::GemcutterTasks.new
16
- rescue LoadError
17
- puts "Jeweler not available. Install it with: sudo gem install jeweler"
18
- end
19
-
20
3
  require 'rdoc/task'
21
4
  Rake::RDocTask.new do |rdoc|
22
5
  rdoc.rdoc_dir = 'rdoc'
@@ -33,15 +16,4 @@ Rake::TestTask.new(:test) do |t|
33
16
  t.verbose = false
34
17
  end
35
18
 
36
- begin
37
- require 'rcov/rcovtask'
38
- Rcov::RcovTask.new do |t|
39
- t.libs << 'test'
40
- t.test_files = FileList['test/**/*_test.rb']
41
- t.verbose = true
42
- end
43
- rescue LoadError
44
- puts "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
45
- end
46
-
47
19
  task :default => :test
@@ -1,4 +1,4 @@
1
1
  ---
2
- :patch: 0
3
- :major: 0
4
- :minor: 8
2
+ :major: 4
3
+ :minor: 0
4
+ :patch: 1
@@ -1,6 +1,6 @@
1
1
  module Slug
2
2
  module ClassMethods
3
-
3
+
4
4
  # Call this to set up slug handling on an ActiveRecord model.
5
5
  #
6
6
  # Params:
@@ -11,34 +11,37 @@ module Slug
11
11
  # * <tt>:validate_uniquness_if</tt> - proc to determine whether uniqueness validation runs, same format as validates_uniquness_of :if
12
12
  #
13
13
  # Slug will take care of validating presence and uniqueness of slug.
14
-
14
+
15
15
  # Before create, Slug will generate and assign the slug if it wasn't explicitly set.
16
16
  # Note that subsequent changes to the source column will have no effect on the slug.
17
17
  # If you'd like to update the slug later on, call <tt>@model.set_slug</tt>
18
18
  def slug source, opts={}
19
- class_attribute :slug_source, :slug_column
19
+ class_attribute :slug_source, :slug_column, :generic_default
20
20
  include InstanceMethods
21
-
21
+
22
22
  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)
25
25
 
26
26
  uniqueness_opts = {}
27
- uniqueness_opts[:if] = opts[:validate_uniqueness_if] if opts.key?(:validate_uniqueness_if)
28
-
29
- validates self.slug_column, :presence => { :message => "cannot be blank. Is #{self.slug_source} sluggable?" }
30
- validates self.slug_column, :uniqueness => uniqueness_opts
31
- validates self.slug_column, :format => { :with => /^[a-z0-9-]+$/, :message => "contains invalid characters. Only downcase letters, numbers, and '-' are allowed." }
27
+ uniqueness_opts.merge!(:if => opts[:validate_uniqueness_if]) if opts[:validate_uniqueness_if].present?
28
+ validates_uniqueness_of self.slug_column, uniqueness_opts
29
+
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."
32
35
  before_validation :set_slug, :on => :create
33
36
  end
34
37
  end
35
-
38
+
36
39
  module InstanceMethods
37
-
40
+
38
41
  # Sets the slug. Called before create.
39
42
  # By default, set_slug won't change slug if one already exists. Pass :force => true to overwrite.
40
43
  def set_slug(opts={})
41
- validate_slug_columns
44
+ validate_slug_columns
42
45
  return unless self[self.slug_column].blank? || opts[:force] == true
43
46
 
44
47
  original_slug = self[self.slug_column]
@@ -46,23 +49,24 @@ module Slug
46
49
 
47
50
  strip_diacritics_from_slug
48
51
  normalize_slug
52
+ genericize_slug if generic_default
49
53
  assign_slug_sequence unless self[self.slug_column] == original_slug # don't try to increment seq if slug hasn't changed
50
54
  end
51
-
55
+
52
56
  # Overwrite existing slug based on current contents of source column.
53
57
  def reset_slug
54
58
  set_slug(:force => true)
55
59
  end
56
-
60
+
57
61
  # Overrides to_param to return the model's slug.
58
62
  def to_param
59
63
  self[self.slug_column]
60
64
  end
61
-
65
+
62
66
  def self.included(klass)
63
67
  klass.extend(ClassMethods)
64
68
  end
65
-
69
+
66
70
  private
67
71
  # Validates that source and destination methods exist. Invoked at runtime to allow definition
68
72
  # of source/slug methods after <tt>slug</tt> setup in class.
@@ -70,7 +74,7 @@ module Slug
70
74
  raise ArgumentError, "Source column '#{self.slug_source}' does not exist!" if !self.respond_to?(self.slug_source)
71
75
  raise ArgumentError, "Slug column '#{self.slug_column}' does not exist!" if !self.respond_to?("#{self.slug_column}=")
72
76
  end
73
-
77
+
74
78
  # Takes the slug, downcases it and replaces non-word characters with a -.
75
79
  # Feel free to override this method if you'd like different slug formatting.
76
80
  def normalize_slug
@@ -84,7 +88,13 @@ module Slug
84
88
  s.gsub!(/-+/, '-') # get rid of double-dashes
85
89
  self[self.slug_column] = s.to_s
86
90
  end
87
-
91
+
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
+
88
98
  # Converts accented characters to their ASCII equivalents and removes them if they have no equivalent.
89
99
  # Override this with a void function if you don't want accented characters to be stripped.
90
100
  def strip_diacritics_from_slug
@@ -102,25 +112,24 @@ module Slug
102
112
  s = s.pack('U*')
103
113
  self[self.slug_column] = s.to_s
104
114
  end
105
-
115
+
106
116
  # If a slug of the same name already exists, this will append '-n' to the end of the slug to
107
117
  # make it unique. The second instance gets a '-1' suffix.
108
118
  def assign_slug_sequence
109
119
  return if self[self.slug_column].blank?
110
- idx = next_slug_sequence
111
- self[self.slug_column] = "#{self[self.slug_column]}-#{idx}" if idx > 0
120
+ self[self.slug_column] = next_slug_sequence
112
121
  end
113
-
122
+
114
123
  # Returns the next unique index for a slug.
115
124
  def next_slug_sequence
116
- last_in_sequence = self.class.where("#{self.slug_column} LIKE ?", self[self.slug_column] + '%').order("CAST(REPLACE(#{self.slug_column},'#{self[self.slug_column]}-','') AS UNSIGNED) DESC").first
117
- if last_in_sequence.nil?
118
- return 0
119
- else
120
- sequence_match = last_in_sequence[self.slug_column].match(/^#{self[self.slug_column]}(-(\d+))?/)
121
- current = sequence_match.nil? ? 0 : sequence_match[2].to_i
122
- return current + 1
125
+ assoc = self.class.base_class
126
+ base_val = slug_val = self[self.slug_column]
127
+ next_id = 1
128
+ until assoc.where(self.slug_column => slug_val).first.nil? do
129
+ slug_val = base_val + "-#{next_id}"
130
+ next_id = next_id + 1
123
131
  end
132
+ slug_val
124
133
  end
125
134
  end
126
- end
135
+ end
@@ -1,17 +1,16 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
1
  # -*- encoding: utf-8 -*-
2
+ # stub: slug 4.0.0 ruby lib
5
3
 
6
4
  Gem::Specification.new do |s|
7
- s.name = %q{slug}
8
- s.version = "1.5.0"
5
+ s.name = "slug"
6
+ s.version = "4.0.1"
9
7
 
10
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
9
+ s.require_paths = ["lib"]
11
10
  s.authors = ["Ben Koski"]
12
- s.date = %q{2013-04-22}
13
- s.description = %q{Simple, straightforward slugs for your ActiveRecord models.}
14
- s.email = %q{ben.koski@gmail.com}
11
+ s.date = "2018-11-11"
12
+ s.description = "Simple, straightforward slugs for your ActiveRecord models."
13
+ s.email = "ben.koski@gmail.com"
15
14
  s.extra_rdoc_files = [
16
15
  "LICENSE",
17
16
  "README.rdoc"
@@ -28,25 +27,32 @@ Gem::Specification.new do |s|
28
27
  "test/models.rb",
29
28
  "test/schema.rb",
30
29
  "test/test_helper.rb",
31
- "test/test_slug.rb"
30
+ "test/slug_test.rb"
32
31
  ]
33
- s.homepage = %q{http://github.com/bkoski/slug}
34
- s.require_paths = ["lib"]
35
- s.rubygems_version = %q{1.3.7}
36
- s.summary = %q{Simple, straightforward slugs for your ActiveRecord models.}
32
+ s.homepage = "http://github.com/bkoski/slug"
33
+ s.rubygems_version = "2.2.0"
34
+ s.summary = "Simple, straightforward slugs for your ActiveRecord models."
37
35
 
38
36
  if s.respond_to? :specification_version then
39
- current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
40
- s.specification_version = 3
37
+ s.specification_version = 4
41
38
 
42
39
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
40
+ s.add_runtime_dependency(%q<rake>, [">= 0"])
41
+ s.add_development_dependency(%q<minitest>, [">= 0"])
42
+ s.add_development_dependency(%q<sqlite3>, [">= 0"])
43
43
  s.add_runtime_dependency(%q<activerecord>, ["> 3.0.0"])
44
44
  s.add_runtime_dependency(%q<activesupport>, ["> 3.0.0"])
45
45
  else
46
+ s.add_dependency(%q<rake>, [">= 0"])
47
+ s.add_dependency(%q<minitest>, [">= 0"])
48
+ s.add_dependency(%q<sqlite3>, [">= 0"])
46
49
  s.add_dependency(%q<activerecord>, ["> 3.0.0"])
47
50
  s.add_dependency(%q<activesupport>, ["> 3.0.0"])
48
51
  end
49
52
  else
53
+ s.add_dependency(%q<rake>, [">= 0"])
54
+ s.add_dependency(%q<minitest>, [">= 0"])
55
+ s.add_dependency(%q<sqlite3>, [">= 0"])
50
56
  s.add_dependency(%q<activerecord>, ["> 3.0.0"])
51
57
  s.add_dependency(%q<activesupport>, ["> 3.0.0"])
52
58
  end
@@ -1,6 +1,9 @@
1
1
  # Used to test slug behavior in general
2
2
  class Article < ActiveRecord::Base
3
- slug :headline
3
+ slug :headline
4
+ end
5
+
6
+ class Storyline < Article
4
7
  end
5
8
 
6
9
  # Used to test alternate slug column
@@ -20,8 +23,14 @@ end
20
23
  # Used to test slugs based on methods rather than database attributes
21
24
  class Event < ActiveRecord::Base
22
25
  slug :title_for_slug
23
-
26
+
24
27
  def title_for_slug
25
28
  "#{title}-#{location}"
26
29
  end
27
- end
30
+ end
31
+
32
+ # Test generation of generic slugs
33
+ class Generation < ActiveRecord::Base
34
+ slug :title, generic_default: true
35
+ end
36
+
@@ -1,30 +1,35 @@
1
1
  ActiveRecord::Schema.define(:version => 1) do
2
-
3
2
  create_table "articles", :force => true do |t|
4
- t.column "headline", "string"
3
+ t.column "headline", "string", null: false
5
4
  t.column "section", "string"
6
- t.column "slug", "string"
5
+ t.column "slug", "string", null: false
6
+ t.column "type", "string"
7
+ t.index ["slug"], name: "index_articles_on_slug", unique: true
7
8
  end
8
-
9
+
9
10
  create_table "people", :force => true do |t|
10
11
  t.column "name", "string"
11
12
  t.column "web_slug", "string"
12
13
  end
13
-
14
+
14
15
  create_table "companies", :force => true do |t|
15
16
  t.column "name", "string"
16
17
  t.column "slug", "string"
17
18
  end
18
-
19
+
19
20
  create_table "posts", :force => true do |t|
20
21
  t.column "headline", "string"
21
22
  t.column "slug", "string"
22
23
  end
23
-
24
+
24
25
  create_table "events", :force => true do |t|
25
26
  t.column "title", "string"
26
27
  t.column "location", "string"
27
28
  t.column "slug", "string"
28
29
  end
29
-
30
- end
30
+
31
+ create_table "generations", :force => true do |t|
32
+ t.column "title", "string"
33
+ t.column "slug", "string", null: false
34
+ end
35
+ end
@@ -0,0 +1,310 @@
1
+ # encoding: utf-8
2
+ require 'test_helper'
3
+
4
+ describe Slug do
5
+ before do
6
+ Article.delete_all
7
+ end
8
+
9
+ describe 'slug' do
10
+ it "bases slug on specified source column" do
11
+ article = Article.create!(:headline => 'Test Headline')
12
+ assert_equal 'test-headline', article.slug
13
+ end
14
+
15
+ it "bases slug on specified source column, even if it is defined as a method rather than database attribute" do
16
+ article = Event.create!(:title => 'Test Event', :location => 'Portland')
17
+ assert_equal 'test-event-portland', article.slug
18
+ end
19
+
20
+ describe "slug column" do
21
+ it "saves slug to 'slug' column by default" do
22
+ article = Article.create!(:headline => 'Test Headline')
23
+ assert_equal 'test-headline', article.slug
24
+ end
25
+
26
+ it "saves slug to :column specified in options" do
27
+ Person.delete_all
28
+ person = Person.create!(:name => 'Test Person')
29
+ assert_equal 'test-person', person.web_slug
30
+ end
31
+ end
32
+ end
33
+
34
+ describe "column validations" do
35
+ it "raises ArgumentError if an invalid source column is passed" do
36
+ Company.slug(:invalid_source_column)
37
+ assert_raises(ArgumentError) { Company.create! }
38
+ end
39
+
40
+ it "raises an ArgumentError if an invalid slug column is passed" do
41
+ Company.slug(:name, :column => :bad_slug_column)
42
+ assert_raises(ArgumentError) { Company.create! }
43
+ end
44
+ end
45
+
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
+
67
+ describe 'validation' do
68
+ it "sets validation error if source column is empty" do
69
+ article = Article.create
70
+ assert !article.valid?
71
+ assert article.errors[:slug]
72
+ end
73
+
74
+ it "sets validation error if normalization makes source value empty" do
75
+ article = Article.create(:headline => '$$$')
76
+ assert !article.valid?
77
+ assert article.errors[:slug]
78
+ end
79
+
80
+ it "validates slug format on save" do
81
+ article = Article.create!(:headline => 'Test Headline')
82
+ article.slug = 'A BAD $LUG.'
83
+
84
+ assert !article.valid?
85
+ assert article.errors[:slug].present?
86
+ end
87
+
88
+ it "validates uniqueness of slug by default" do
89
+ Article.create!(:headline => 'Test Headline')
90
+ article2 = Article.create!(:headline => 'Test Headline')
91
+ article2.slug = 'test-headline'
92
+
93
+ assert !article2.valid?
94
+ assert article2.errors[:slug].present?
95
+ end
96
+
97
+ it "uses validate_uniqueness_if proc to decide whether uniqueness validation applies" do
98
+ Post.create!(:headline => 'Test Headline')
99
+ article2 = Post.new
100
+ article2.slug = 'test-headline'
101
+
102
+ assert article2.valid?
103
+ end
104
+ end
105
+
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
115
+ end
116
+
117
+ describe "resetting a slug" do
118
+ before do
119
+ @article = Article.create(:headline => 'test headline')
120
+ @original_slug = @article.slug
121
+ end
122
+
123
+ it "maintains the same slug if slug column hasn't changed" do
124
+ @article.reset_slug
125
+ assert_equal @original_slug, @article.slug
126
+ end
127
+
128
+ it "changes slug if slug column has updated" do
129
+ @article.headline = "donkey"
130
+ @article.reset_slug
131
+ refute_equal(@original_slug, @article.slug)
132
+ end
133
+
134
+ it "maintains sequence" do
135
+ @existing_article = Article.create!(:headline => 'world cup')
136
+ @article.headline = "world cup"
137
+ @article.reset_slug
138
+ assert_equal 'world-cup-1', @article.slug
139
+ end
140
+ end
141
+
142
+ describe "slug normalization" do
143
+ before do
144
+ @article = Article.new
145
+ end
146
+
147
+ it "lowercases strings" do
148
+ @article.headline = 'AbC'
149
+ @article.save!
150
+ assert_equal "abc", @article.slug
151
+ end
152
+
153
+ it "replaces whitespace with dashes" do
154
+ @article.headline = 'a b'
155
+ @article.save!
156
+ assert_equal 'a-b', @article.slug
157
+ end
158
+
159
+ it "replaces 2spaces with 1dash" do
160
+ @article.headline = 'a b'
161
+ @article.save!
162
+ assert_equal 'a-b', @article.slug
163
+ end
164
+
165
+ it "removes punctuation" do
166
+ @article.headline = 'abc!@#$%^&*•¶§∞¢££¡¿()><?""\':;][]\.,/'
167
+ @article.save!
168
+ assert_match 'abc', @article.slug
169
+ end
170
+
171
+ it "strips trailing space" do
172
+ @article.headline = 'ab '
173
+ @article.save!
174
+ assert_equal 'ab', @article.slug
175
+ end
176
+
177
+ it "strips leading space" do
178
+ @article.headline = ' ab'
179
+ @article.save!
180
+ assert_equal 'ab', @article.slug
181
+ end
182
+
183
+ it "strips trailing dashes" do
184
+ @article.headline = 'ab-'
185
+ @article.save!
186
+ assert_match 'ab', @article.slug
187
+ end
188
+
189
+ it "strips leading dashes" do
190
+ @article.headline = '-ab'
191
+ @article.save!
192
+ assert_match 'ab', @article.slug
193
+ end
194
+
195
+ it "remove double-dashes" do
196
+ @article.headline = 'a--b--c'
197
+ @article.save!
198
+ assert_match 'a-b-c', @article.slug
199
+ end
200
+
201
+ it "doesn't modify valid slug strings" do
202
+ @article.headline = 'a-b-c-d'
203
+ @article.save!
204
+ assert_match 'a-b-c-d', @article.slug
205
+ end
206
+
207
+ it "doesn't insert dashes for periods in acronyms, regardless of where they appear in string" do
208
+ @article.headline = "N.Y.P.D. vs. N.S.A. vs. F.B.I."
209
+ @article.save!
210
+ assert_match 'nypd-vs-nsa-vs-fbi', @article.slug
211
+ end
212
+
213
+ it "doesn't insert dashes for apostrophes" do
214
+ @article.headline = "Thomas Jefferson's Papers"
215
+ @article.save!
216
+ assert_match 'thomas-jeffersons-papers', @article.slug
217
+ end
218
+
219
+ it "preserves numbers in slug" do
220
+ @article.headline = "2010 Election"
221
+ @article.save!
222
+ assert_match '2010-election', @article.slug
223
+ end
224
+ end
225
+
226
+ describe "diacritics handling" do
227
+ before do
228
+ @article = Article.new
229
+ end
230
+
231
+ it "strips diacritics" do
232
+ @article.headline = "açaí"
233
+ @article.save!
234
+ assert_equal "acai", @article.slug
235
+ end
236
+
237
+ it "strips diacritics correctly " do
238
+ @article.headline = "ÀÁÂÃÄÅÆÇÈÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ"
239
+ @article.save!
240
+ expected = "aaaaaaaeceeeiiiidnoooooouuuuythssaaaaaaaeceeeeiiiidnoooooouuuuythy".split(//)
241
+ output = @article.slug.split(//)
242
+ output.each_index do |i|
243
+ assert_equal expected[i], output[i]
244
+ end
245
+ end
246
+ end
247
+
248
+ describe "sequence handling" do
249
+ it "doesn't add a sequence if saving first instance of slug" do
250
+ article = Article.create!(:headline => 'Test Headline')
251
+ assert_equal 'test-headline', article.slug
252
+ end
253
+
254
+ it "assigns a -1 suffix to the second instance of the slug" do
255
+ Article.create!(:headline => 'Test Headline')
256
+ article_2 = Article.create!(:headline => 'Test Headline')
257
+ assert_equal 'test-headline-1', article_2.slug
258
+ end
259
+
260
+ it 'assigns a -2 suffix to the third instance of the slug containing numbers' do
261
+ 2.times { |i| Article.create! :headline => '11111' }
262
+ article_3 = Article.create! :headline => '11111'
263
+ assert_equal '11111-2', article_3.slug
264
+ end
265
+
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
+
287
+ it "knows about single table inheritance" do
288
+ article = Article.create!(:headline => 'Test Headline')
289
+ story = Storyline.create!(:headline => article.headline)
290
+ assert_equal 'test-headline-1', story.slug
291
+ end
292
+
293
+ it "correctly slugs when a slug is a substring of another" do
294
+ rap_metal = Article.create!(:headline => 'Rap Metal')
295
+ assert_equal 'rap-metal', rap_metal.slug
296
+
297
+ rap = Article.create!(:headline => 'Rap')
298
+ assert_equal('rap', rap.slug)
299
+ end
300
+
301
+ it "applies sequence logic correctly when the slug is a substring of another" do
302
+ rap_metal = Article.create!(:headline => 'Rap Metal')
303
+ assert_equal 'rap-metal', rap_metal.slug
304
+
305
+ Article.create!(:headline => 'Rap')
306
+ second_rap = Article.create!(:headline => 'Rap')
307
+ assert_equal('rap-1', second_rap.slug)
308
+ end
309
+ end
310
+ end
@@ -1,26 +1,21 @@
1
1
  require 'rubygems'
2
- require 'test/unit'
3
- require 'shoulda'
4
- require 'mocha'
5
-
6
- class Test::Unit::TestCase
7
- end
2
+ require 'minitest/autorun'
3
+ require 'minitest/reporters'
8
4
 
9
5
  # You can use "rake test AR_VERSION=2.0.5" to test against 2.0.5, for example.
10
6
  # The default is to use the latest installed ActiveRecord.
11
7
  if ENV["AR_VERSION"]
12
8
  gem 'activerecord', "#{ENV["AR_VERSION"]}"
13
- gem 'activesupport', "#{ENV["AR_VERSION"]}"
14
9
  end
15
10
  require 'active_record'
16
- require 'active_support'
11
+
12
+ # color test output
13
+ Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new(:color => true)]
17
14
 
18
15
  $LOAD_PATH.unshift(File.dirname(__FILE__))
19
16
  require 'slug'
20
17
 
21
18
  ActiveRecord::Base.establish_connection :adapter => "sqlite3", :database => ":memory:"
22
- silence_stream(STDOUT) do
23
- load(File.dirname(__FILE__) + "/schema.rb")
24
- end
19
+ load(File.dirname(__FILE__) + "/schema.rb")
25
20
 
26
21
  require 'models'
metadata CHANGED
@@ -1,65 +1,93 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: slug
3
- version: !ruby/object:Gem::Version
4
- hash: 63
5
- prerelease: false
6
- segments:
7
- - 0
8
- - 8
9
- - 0
10
- version: 0.8.0
3
+ version: !ruby/object:Gem::Version
4
+ version: 4.0.1
11
5
  platform: ruby
12
- authors:
6
+ authors:
13
7
  - Ben Koski
14
8
  autorequire:
15
9
  bindir: bin
16
10
  cert_chain: []
17
-
18
- date: 2013-04-22 00:00:00 -04:00
19
- default_executable:
20
- dependencies:
21
- - !ruby/object:Gem::Dependency
22
- name: activerecord
11
+ date: 2018-11-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
23
35
  prerelease: false
24
- requirement: &id001 !ruby/object:Gem::Requirement
25
- none: false
26
- requirements:
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sqlite3
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activerecord
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
27
59
  - - ">"
28
- - !ruby/object:Gem::Version
29
- hash: 7
30
- segments:
31
- - 3
32
- - 0
33
- - 0
60
+ - !ruby/object:Gem::Version
34
61
  version: 3.0.0
35
62
  type: :runtime
36
- version_requirements: *id001
37
- - !ruby/object:Gem::Dependency
38
- name: activesupport
39
63
  prerelease: false
40
- requirement: &id002 !ruby/object:Gem::Requirement
41
- none: false
42
- requirements:
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
43
66
  - - ">"
44
- - !ruby/object:Gem::Version
45
- hash: 7
46
- segments:
47
- - 3
48
- - 0
49
- - 0
67
+ - !ruby/object:Gem::Version
68
+ version: 3.0.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: activesupport
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">"
74
+ - !ruby/object:Gem::Version
50
75
  version: 3.0.0
51
76
  type: :runtime
52
- version_requirements: *id002
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">"
81
+ - !ruby/object:Gem::Version
82
+ version: 3.0.0
53
83
  description: Simple, straightforward slugs for your ActiveRecord models.
54
84
  email: ben.koski@gmail.com
55
85
  executables: []
56
-
57
86
  extensions: []
58
-
59
- extra_rdoc_files:
87
+ extra_rdoc_files:
60
88
  - LICENSE
61
89
  - README.rdoc
62
- files:
90
+ files:
63
91
  - LICENSE
64
92
  - README.rdoc
65
93
  - Rakefile
@@ -70,41 +98,29 @@ files:
70
98
  - slug.gemspec
71
99
  - test/models.rb
72
100
  - test/schema.rb
101
+ - test/slug_test.rb
73
102
  - test/test_helper.rb
74
- - test/test_slug.rb
75
- has_rdoc: true
76
103
  homepage: http://github.com/bkoski/slug
77
104
  licenses: []
78
-
105
+ metadata: {}
79
106
  post_install_message:
80
107
  rdoc_options: []
81
-
82
- require_paths:
108
+ require_paths:
83
109
  - lib
84
- required_ruby_version: !ruby/object:Gem::Requirement
85
- none: false
86
- requirements:
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
87
112
  - - ">="
88
- - !ruby/object:Gem::Version
89
- hash: 3
90
- segments:
91
- - 0
92
- version: "0"
93
- required_rubygems_version: !ruby/object:Gem::Requirement
94
- none: false
95
- requirements:
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
96
117
  - - ">="
97
- - !ruby/object:Gem::Version
98
- hash: 3
99
- segments:
100
- - 0
101
- version: "0"
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
102
120
  requirements: []
103
-
104
121
  rubyforge_project:
105
- rubygems_version: 1.3.7
122
+ rubygems_version: 2.7.6
106
123
  signing_key:
107
- specification_version: 3
124
+ specification_version: 4
108
125
  summary: Simple, straightforward slugs for your ActiveRecord models.
109
126
  test_files: []
110
-
@@ -1,250 +0,0 @@
1
- require File.dirname(__FILE__) + '/test_helper'
2
-
3
- class TestSlug < Test::Unit::TestCase
4
-
5
- def setup
6
- Article.delete_all
7
- Person.delete_all
8
- end
9
-
10
- should "base slug on specified source column" do
11
- article = Article.create!(:headline => 'Test Headline')
12
- assert_equal 'test-headline', article.slug
13
- end
14
-
15
- should "base slug on specified source column, even if it is defined as a method rather than database attribute" do
16
- article = Event.create!(:title => 'Test Event', :location => 'Portland')
17
- assert_equal 'test-event-portland', article.slug
18
- end
19
-
20
- context "slug column" do
21
- should "save slug to 'slug' column by default" do
22
- article = Article.create!(:headline => 'Test Headline')
23
- assert_equal 'test-headline', article.slug
24
- end
25
-
26
- should "save slug to :column specified in options" do
27
- person = Person.create!(:name => 'Test Person')
28
- assert_equal 'test-person', person.web_slug
29
- end
30
- end
31
-
32
- context "column validations" do
33
- should "raise ArgumentError if an invalid source column is passed" do
34
- Company.slug(:invalid_source_column)
35
- assert_raises(ArgumentError) { Company.create! }
36
- end
37
-
38
- should "raise an ArgumentError if an invalid slug column is passed" do
39
- Company.slug(:name, :column => :bad_slug_column)
40
- assert_raises(ArgumentError) { Company.create! }
41
- end
42
- end
43
-
44
- should "set validation error if source column is empty" do
45
- article = Article.create
46
- assert !article.valid?
47
- require 'ruby-debug'
48
- assert article.errors[:slug]
49
- end
50
-
51
- should "set validation error if normalization makes source value empty" do
52
- article = Article.create(:headline => '$$$')
53
- assert !article.valid?
54
- assert article.errors[:slug]
55
- end
56
-
57
- should "not update the slug even if the source column changes" do
58
- article = Article.create!(:headline => 'Test Headline')
59
- article.update_attributes!(:headline => 'New Headline')
60
- assert_equal 'test-headline', article.slug
61
- end
62
-
63
- should "validate slug format on save" do
64
- article = Article.create!(:headline => 'Test Headline')
65
- article.slug = 'A BAD $LUG.'
66
-
67
- assert !article.valid?
68
- assert article.errors[:slug].present?
69
- end
70
-
71
- should "validate uniqueness of slug by default" do
72
- article1 = Article.create!(:headline => 'Test Headline')
73
- article2 = Article.create!(:headline => 'Test Headline')
74
- article2.slug = 'test-headline'
75
-
76
- assert !article2.valid?
77
- assert article2.errors[:slug].present?
78
- end
79
-
80
- should "use validate_uniqueness_if proc to decide whether uniqueness validation applies" do
81
- article1 = Post.create!(:headline => 'Test Headline')
82
- article2 = Post.new
83
- article2.slug = 'test-headline'
84
-
85
- assert article2.valid?
86
- end
87
-
88
- should "not overwrite slug value on create if it was already specified" do
89
- a = Article.create!(:headline => 'Test Headline', :slug => 'slug1')
90
- assert_equal 'slug1', a.slug
91
- end
92
-
93
- context "resetting a slug" do
94
-
95
- setup do
96
- @article = Article.create(:headline => 'test headline')
97
- @original_slug = @article.slug
98
- end
99
-
100
- should "maintain the same slug if slug column hasn't changed" do
101
- @article.reset_slug
102
- assert_equal @original_slug, @article.slug
103
- end
104
-
105
- should "change slug if slug column has updated" do
106
- @article.headline = "donkey"
107
- @article.reset_slug
108
- assert_not_equal @original_slug, @article.slug
109
- end
110
-
111
- should "maintain sequence" do
112
- @existing_article = Article.create!(:headline => 'world cup')
113
- @article.headline = "world cup"
114
- @article.reset_slug
115
- assert_equal 'world-cup-1', @article.slug
116
- end
117
-
118
- end
119
-
120
-
121
- context "slug normalization" do
122
- setup do
123
- @article = Article.new
124
- end
125
-
126
- should "should lowercase strings" do
127
- @article.headline = 'AbC'
128
- @article.save!
129
- assert_equal "abc", @article.slug
130
- end
131
-
132
- should "should replace whitespace with dashes" do
133
- @article.headline = 'a b'
134
- @article.save!
135
- assert_equal 'a-b', @article.slug
136
- end
137
-
138
- should "should replace 2spaces with 1dash" do
139
- @article.headline = 'a b'
140
- @article.save!
141
- assert_equal 'a-b', @article.slug
142
- end
143
-
144
- should "should remove punctuation" do
145
- @article.headline = 'abc!@#$%^&*•¶§∞¢££¡¿()><?""\':;][]\.,/'
146
- @article.save!
147
- assert_match 'abc', @article.slug
148
- end
149
-
150
- should "should strip trailing space" do
151
- @article.headline = 'ab '
152
- @article.save!
153
- assert_equal 'ab', @article.slug
154
- end
155
-
156
- should "should strip leading space" do
157
- @article.headline = ' ab'
158
- @article.save!
159
- assert_equal 'ab', @article.slug
160
- end
161
-
162
- should "should strip trailing dashes" do
163
- @article.headline = 'ab-'
164
- @article.save!
165
- assert_match 'ab', @article.slug
166
- end
167
-
168
- should "should strip leading dashes" do
169
- @article.headline = '-ab'
170
- @article.save!
171
- assert_match 'ab', @article.slug
172
- end
173
-
174
- should "remove double-dashes" do
175
- @article.headline = 'a--b--c'
176
- @article.save!
177
- assert_match 'a-b-c', @article.slug
178
- end
179
-
180
- should "should not modify valid slug strings" do
181
- @article.headline = 'a-b-c-d'
182
- @article.save!
183
- assert_match 'a-b-c-d', @article.slug
184
- end
185
-
186
- should "not insert dashes for periods in acronyms, regardless of where they appear in string" do
187
- @article.headline = "N.Y.P.D. vs. N.S.A. vs. F.B.I."
188
- @article.save!
189
- assert_match 'nypd-vs-nsa-vs-fbi', @article.slug
190
- end
191
-
192
- should "not insert dashes for apostrophes" do
193
- @article.headline = "Thomas Jefferson's Papers"
194
- @article.save!
195
- assert_match 'thomas-jeffersons-papers', @article.slug
196
- end
197
-
198
- should "preserve numbers in slug" do
199
- @article.headline = "2010 Election"
200
- @article.save!
201
- assert_match '2010-election', @article.slug
202
- end
203
- end
204
-
205
- context "diacritics handling" do
206
- setup do
207
- @article = Article.new
208
- end
209
-
210
- should "should strip diacritics" do
211
- @article.headline = "açaí"
212
- @article.save!
213
- assert_equal "acai", @article.slug
214
- end
215
-
216
- should "strip diacritics correctly " do
217
- @article.headline = "ÀÁÂÃÄÅÆÇÈÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ"
218
- @article.save!
219
- expected = "aaaaaaaeceeeiiiidnoooooouuuuythssaaaaaaaeceeeeiiiidnoooooouuuuythy".split(//)
220
- output = @article.slug.split(//)
221
- output.each_index do |i|
222
- assert_equal expected[i], output[i]
223
- end
224
- end
225
- end
226
-
227
- context "sequence handling" do
228
- should "not add a sequence if saving first instance of slug" do
229
- article = Article.create!(:headline => 'Test Headline')
230
- assert_equal 'test-headline', article.slug
231
- end
232
-
233
- should "assign a -1 suffix to the second instance of the slug" do
234
- article_1 = Article.create!(:headline => 'Test Headline')
235
- article_2 = Article.create!(:headline => 'Test Headline')
236
- assert_equal 'test-headline-1', article_2.slug
237
- end
238
-
239
- should "assign a -12 suffix to the thirteenth instance of the slug" do
240
- 12.times { |i| Article.create!(:headline => 'Test Headline') }
241
- article_13 = Article.create!(:headline => 'Test Headline')
242
- assert_equal 'test-headline-12', article_13.slug
243
-
244
- 12.times { |i| Article.create!(:headline => 'latest from lybia') }
245
- article_13 = Article.create!(:headline => 'latest from lybia')
246
- assert_equal 'latest-from-lybia-12', article_13.slug
247
- end
248
- end
249
-
250
- end