├── .autotest
├── .bundle
└── config
├── .gitignore
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── README.md
├── Rakefile
├── lib
├── slug.rb
└── slug
│ └── slug.rb
├── slug.gemspec
└── test
├── models.rb
├── schema.rb
├── slug_test.rb
└── test_helper.rb
/.autotest:
--------------------------------------------------------------------------------
1 | begin
2 | require 'autotest/fsevent'
3 | rescue LoadError
4 | puts "NOTE: Install autotest-fsevent to prevent filesystem polling"
5 | end
6 |
7 | begin
8 | require 'autotest/growl'
9 | rescue LoadError
10 | puts "NOTE: Install autotest-growl for growl support"
11 | end
12 |
13 | require 'autotest/timestamp'
14 |
15 | Autotest.add_hook :initialize do |at|
16 | unless ARGV.empty?
17 | at.find_directories = ARGV.dup
18 | end
19 |
20 | # Ignore these files
21 | %w(
22 | .hg .git .svn stories tmtags
23 | Gemfile Rakefile Capfile README
24 | .html app/assets config .keep
25 | spec/spec.opts spec/rcov.opts vendor/gems vendor/ruby
26 | autotest svn-commit .DS_Store
27 | ).each { |exception|at.add_exception(exception) }
28 |
29 | at.add_mapping(%r{^lib\/slug\/.*\.rb$}) do |f, _|
30 | at.files_matching %r{^test\/.*_test\.rb$}
31 | end
32 |
33 | end
--------------------------------------------------------------------------------
/.bundle/config:
--------------------------------------------------------------------------------
1 | ---
2 | BUNDLE_PATH: "vendor"
3 | BUNDLE_DISABLE_SHARED_GEMS: "1"
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.sw?
2 | .DS_Store
3 | coverage
4 | vendor/ruby
5 | .bundle
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | group :development, :test do
4 | gem 'minitest'
5 | gem 'minitest-reporters'
6 | gem 'activerecord'
7 | gem 'rake'
8 | gem 'sqlite3'
9 | end
10 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | activemodel (5.1.4)
5 | activesupport (= 5.1.4)
6 | activerecord (5.1.4)
7 | activemodel (= 5.1.4)
8 | activesupport (= 5.1.4)
9 | arel (~> 8.0)
10 | activesupport (5.1.4)
11 | concurrent-ruby (~> 1.0, >= 1.0.2)
12 | i18n (~> 0.7)
13 | minitest (~> 5.1)
14 | tzinfo (~> 1.1)
15 | ansi (1.5.0)
16 | arel (8.0.0)
17 | builder (3.2.3)
18 | concurrent-ruby (1.0.5)
19 | i18n (0.9.1)
20 | concurrent-ruby (~> 1.0)
21 | minitest (5.10.3)
22 | minitest-reporters (1.1.19)
23 | ansi
24 | builder
25 | minitest (>= 5.0)
26 | ruby-progressbar
27 | rake (12.3.0)
28 | ruby-progressbar (1.9.0)
29 | sqlite3 (1.3.13)
30 | thread_safe (0.3.6)
31 | tzinfo (1.2.4)
32 | thread_safe (~> 0.1)
33 |
34 | PLATFORMS
35 | ruby
36 |
37 | DEPENDENCIES
38 | activerecord
39 | minitest
40 | minitest-reporters
41 | rake
42 | sqlite3
43 |
44 | BUNDLED WITH
45 | 1.16.5
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2008 Ben Koski
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # slug
2 |
3 | Slug provides simple, straightforward slugging for your ActiveRecord models.
4 |
5 | Slug is based on code from Norman Clarke's fantastic [friendly_id](https://github.com/norman/friendly_id) project and Nick Zadrozny's [friendly_identifier](http://code.google.com/p/friendly-identifier/).
6 |
7 | What's different:
8 |
9 | * Unlike friendly_id's more advanced modes, 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 | * Like friendly_id, diacritics (accented characters) are stripped from slug strings.
11 | * The number of options is manageable.
12 |
13 | ## Installation
14 |
15 | Add the gem to your Gemfile
16 |
17 | ```
18 | gem 'slug'
19 | ```
20 |
21 | of your rails project.
22 |
23 | This is tested with Rails 5.1.4, MRI Ruby 2.4.1
24 |
25 | ## Usage
26 |
27 | ### Creating the database column
28 |
29 | It's up to you to set up the appropriate column in your model. By default, slug saves the slug to a column called 'slug', so in most cases you'll just want to add
30 |
31 | ```ruby
32 | add_column :my_table, :slug, :string
33 | ```
34 |
35 | in a migration. You should also add a unque index on the slug field in your migration
36 |
37 | ```ruby
38 | add_index :model_name, :slug, unique: true
39 | ```
40 |
41 | Though Slug uses `validates_uniqueness_of` to ensue the uniqueness of your slug, two concurrent INSERTs could try to set the same slug.
42 |
43 | ### Model setup
44 |
45 | Once your table is migrated, just add
46 |
47 | ```ruby
48 | slug :source_field
49 | ```
50 |
51 | to your ActiveRecord model. `:source_field` is the column you'd like to base the slug on. For example, it might be `:headline`.
52 |
53 | #### Using an instance method as the source column
54 |
55 | 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.
56 |
57 | For example:
58 |
59 | ```ruby
60 | class Article < ActiveRecord::Base
61 | slug :title_for_slug
62 |
63 | def title_for_slug
64 | "#{headline}-#{publication_date.year}-#{publication_date.month}"
65 | end
66 | end
67 | ```
68 |
69 | would use `headline-pub year-pub month` as the slug source.
70 |
71 | From here, you can work with your slug as if it were a normal column. `find_by_slug` and named scopes will work as they do for any other column.
72 |
73 | ### Options
74 |
75 | There are two options:
76 |
77 | #### Column
78 |
79 | If you want to save the slug in a database column that isn't called
80 | `slug`, just pass the `:column` option. For example:
81 |
82 | ```
83 | slug :headline, column: :web_slug
84 | ```
85 |
86 | would generate a slug based on `headline` and save it to `web_slug`.
87 |
88 | #### Generic Default
89 |
90 | If the source column is empty, blank, or only contains filtered
91 | characters, you can avoid `ActiveRecord::ValidationError` exceptions
92 | by setting `generic_default: true`. For example:
93 |
94 | ```ruby
95 | slug :headline, generic_default: true
96 | ```
97 |
98 | will generate a slug based on your model name if the headline is blank.
99 |
100 | This is useful if the source column (e.g. headline) is based on user-generated
101 | input or can be blank (nil or empty).
102 |
103 | Some prefer to get the exception in this case. Others want to get a good
104 | slug and move on.
105 |
106 | ## Notes
107 |
108 | * 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. To avoid this, use the `:generic_default` option.
109 | * Slug doesn't update the slug if the source column changes. If you really need to regenerate the slug, call `@model.set_slug` before save.
110 | * 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'.
111 | * 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.
112 |
113 | ## Authors
114 |
115 | Ben Koski, ben.koski@gmail.com
116 |
117 | With generous contributions from:
118 | * [Derek Willis](http://thescoop.org/)
119 | * [Douglas Lovell](https://github.com/wbreeze)
120 | * [Paul Battley](https://github.com/threedaymonk)
121 | * [Yura Omelchuk](https://github.com/jurgens)
122 | * others listed in the
123 | [GitHub contributor list](https://github.com/bkoski/slug/graphs/contributors).
124 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rake'
2 |
3 | require 'rdoc/task'
4 | Rake::RDocTask.new do |rdoc|
5 | rdoc.rdoc_dir = 'rdoc'
6 | rdoc.title = 'slug'
7 | rdoc.options << '--line-numbers' << '--inline-source' << '--all'
8 | rdoc.rdoc_files.include('README*')
9 | rdoc.rdoc_files.include('lib/**/*.rb')
10 | end
11 |
12 | require 'rake/testtask'
13 | Rake::TestTask.new(:test) do |t|
14 | t.libs << 'lib' << 'test'
15 | t.pattern = 'test/**/*_test.rb'
16 | t.verbose = false
17 | end
18 |
19 | task :default => :test
20 |
--------------------------------------------------------------------------------
/lib/slug.rb:
--------------------------------------------------------------------------------
1 | require File.join(File.dirname(__FILE__), 'slug', 'slug')
2 |
3 | ActiveRecord::Base.instance_eval { include Slug }
4 | if defined?(Rails) && Rails.version.to_i < 4
5 | raise "This version of slug requires Rails 4 or higher"
6 | end
7 |
--------------------------------------------------------------------------------
/lib/slug/slug.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/concern'
2 |
3 | module Slug
4 | extend ActiveSupport::Concern
5 |
6 | class_methods do
7 |
8 | # Call this to set up slug handling on an ActiveRecord model.
9 | #
10 | # Params:
11 | # * :source - the column the slug will be based on (e.g. :headline)
12 | #
13 | # Options:
14 | # * :column - the column the slug will be saved to (defaults to :slug)
15 | # * :validate_uniquness_if - proc to determine whether uniqueness validation runs, same format as validates_uniquness_of :if
16 | #
17 | # Slug will take care of validating presence and uniqueness of slug.
18 |
19 | # Before create, Slug will generate and assign the slug if it wasn't explicitly set.
20 | # Note that subsequent changes to the source column will have no effect on the slug.
21 | # If you'd like to update the slug later on, call @model.set_slug
22 | def slug source, opts={}
23 | class_attribute :slug_source, :slug_column, :generic_default
24 |
25 | self.slug_source = source
26 | self.slug_column = opts.fetch(:column, :slug)
27 | self.generic_default = opts.fetch(:generic_default, false)
28 |
29 | uniqueness_opts = {}
30 | uniqueness_opts.merge!(:if => opts[:validate_uniqueness_if]) if opts[:validate_uniqueness_if].present?
31 | validates_uniqueness_of self.slug_column, uniqueness_opts
32 |
33 | validates_presence_of self.slug_column,
34 | message: "cannot be blank. Is #{self.slug_source} sluggable?"
35 | validates_format_of self.slug_column,
36 | with: /\A[a-z0-9-]+\z/,
37 | message: "contains invalid characters. Only downcase letters, numbers, and '-' are allowed."
38 | before_validation :set_slug, :on => :create
39 |
40 | include SlugInstanceMethods
41 | end
42 | end
43 |
44 | module SlugInstanceMethods
45 | # Sets the slug. Called before create.
46 | # By default, set_slug won't change slug if one already exists. Pass :force => true to overwrite.
47 | def set_slug(opts={})
48 | validate_slug_columns
49 | return if self[self.slug_column].present? && !opts[:force]
50 |
51 | self[self.slug_column] = normalize_slug(self.send(self.slug_source))
52 |
53 | # if normalize_slug returned a blank string, try the generic_default handling
54 | if generic_default && self[self.slug_column].blank?
55 | self[self.slug_column] = self.class.to_s.demodulize.underscore.dasherize
56 | end
57 |
58 | assign_slug_sequence if self.changed_attributes.include?(self.slug_column)
59 | end
60 |
61 | # Overwrite existing slug based on current contents of source column.
62 | def reset_slug
63 | set_slug(:force => true)
64 | end
65 |
66 | # Overrides to_param to return the model's slug.
67 | def to_param
68 | self[self.slug_column]
69 | end
70 |
71 | private
72 | # Validates that source and destination methods exist. Invoked at runtime to allow definition
73 | # of source/slug methods after slug setup in class.
74 | def validate_slug_columns
75 | raise ArgumentError, "Source column '#{self.slug_source}' does not exist!" if !self.respond_to?(self.slug_source)
76 | raise ArgumentError, "Slug column '#{self.slug_column}' does not exist!" if !self.respond_to?("#{self.slug_column}=")
77 | end
78 |
79 | # Takes the slug, downcases it and replaces non-word characters with a -.
80 | # Feel free to override this method if you'd like different slug formatting.
81 | def normalize_slug(str)
82 | return if str.blank?
83 | str.gsub!(/[\p{Pc}\p{Ps}\p{Pe}\p{Pi}\p{Pf}\p{Po}]/, '') # Remove punctuation
84 | str.parameterize
85 | end
86 |
87 | # If a slug of the same name already exists, this will append '-n' to the end of the slug to
88 | # make it unique. The second instance gets a '-1' suffix.
89 | def assign_slug_sequence
90 | return if self[self.slug_column].blank?
91 | assoc = self.class.base_class
92 | base_slug = self[self.slug_column]
93 | seq = 0
94 |
95 | while assoc.where(self.slug_column => self[self.slug_column]).exists? do
96 | seq += 1
97 | self[self.slug_column] = "#{base_slug}-#{seq}"
98 | end
99 | end
100 | end
101 | end
102 |
--------------------------------------------------------------------------------
/slug.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | # stub: slug 4.0.0 ruby lib
3 |
4 | Gem::Specification.new do |s|
5 | s.name = "slug"
6 | s.version = "4.1.0"
7 |
8 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
9 | s.require_paths = ["lib"]
10 | s.authors = ["Ben Koski"]
11 | s.date = "2018-11-17"
12 | s.description = "Simple, straightforward slugs for your ActiveRecord models."
13 | s.email = "ben.koski@gmail.com"
14 | s.extra_rdoc_files = [
15 | "LICENSE",
16 | "README.md"
17 | ]
18 | s.files = [
19 | "LICENSE",
20 | "README.md",
21 | "Rakefile",
22 | "lib/slug.rb",
23 | "lib/slug/slug.rb",
24 | "slug.gemspec",
25 | "test/models.rb",
26 | "test/schema.rb",
27 | "test/test_helper.rb",
28 | "test/slug_test.rb"
29 | ]
30 | s.homepage = "http://github.com/bkoski/slug"
31 | s.rubygems_version = "2.2.0"
32 | s.summary = "Simple, straightforward slugs for your ActiveRecord models."
33 |
34 | if s.respond_to? :specification_version then
35 | s.specification_version = 4
36 |
37 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
38 | s.add_runtime_dependency(%q, [">= 0"])
39 | s.add_development_dependency(%q, [">= 0"])
40 | s.add_development_dependency(%q, [">= 0"])
41 | s.add_runtime_dependency(%q, ["> 3.0.0"])
42 | s.add_runtime_dependency(%q, ["> 3.0.0"])
43 | else
44 | s.add_dependency(%q, [">= 0"])
45 | s.add_dependency(%q, [">= 0"])
46 | s.add_dependency(%q, [">= 0"])
47 | s.add_dependency(%q, ["> 3.0.0"])
48 | s.add_dependency(%q, ["> 3.0.0"])
49 | end
50 | else
51 | s.add_dependency(%q, [">= 0"])
52 | s.add_dependency(%q, [">= 0"])
53 | s.add_dependency(%q, [">= 0"])
54 | s.add_dependency(%q, ["> 3.0.0"])
55 | s.add_dependency(%q, ["> 3.0.0"])
56 | end
57 | end
58 |
59 |
--------------------------------------------------------------------------------
/test/models.rb:
--------------------------------------------------------------------------------
1 | # Used to test slug behavior in general
2 | class Article < ActiveRecord::Base
3 | slug :headline
4 | end
5 |
6 | class Storyline < Article
7 | end
8 |
9 | # Used to test alternate slug column
10 | class Person < ActiveRecord::Base
11 | slug :name, :column => :web_slug
12 | end
13 |
14 | # Used to test invalid method names
15 | class Company < ActiveRecord::Base
16 | slug :name
17 | end
18 |
19 | class Post < ActiveRecord::Base
20 | slug :headline, :validate_uniqueness_if => Proc.new { false }
21 | end
22 |
23 | # Used to test slugs based on methods rather than database attributes
24 | class Event < ActiveRecord::Base
25 | slug :title_for_slug
26 |
27 | def title_for_slug
28 | "#{title}-#{location}"
29 | end
30 | end
31 |
32 | # Test generation of generic slugs
33 | class Generation < ActiveRecord::Base
34 | slug :title, generic_default: true
35 | end
36 |
37 | # Test model with no slug column
38 | class Orphan < ActiveRecord::Base
39 | end
40 |
--------------------------------------------------------------------------------
/test/schema.rb:
--------------------------------------------------------------------------------
1 | ActiveRecord::Schema.define(:version => 1) do
2 | create_table "articles", :force => true do |t|
3 | t.column "headline", "string", null: false
4 | t.column "section", "string"
5 | t.column "slug", "string", null: false
6 | t.column "type", "string"
7 | t.index ["slug"], name: "index_articles_on_slug", unique: true
8 | end
9 |
10 | create_table "people", :force => true do |t|
11 | t.column "name", "string"
12 | t.column "web_slug", "string"
13 | end
14 |
15 | create_table "companies", :force => true do |t|
16 | t.column "name", "string"
17 | t.column "slug", "string"
18 | end
19 |
20 | create_table "posts", :force => true do |t|
21 | t.column "headline", "string"
22 | t.column "slug", "string"
23 | end
24 |
25 | create_table "events", :force => true do |t|
26 | t.column "title", "string"
27 | t.column "location", "string"
28 | t.column "slug", "string"
29 | 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 |
36 | # table with no slug column
37 | create_table "orphans", :force => true do |t|
38 | t.column "name", "string"
39 | t.column "location", "string"
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/test/slug_test.rb:
--------------------------------------------------------------------------------
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 | it "bases to_param on slug" do
21 | article = Article.create!(:headline => 'Test Headline')
22 | assert_equal(article.slug, article.to_param)
23 | end
24 |
25 | it "does not impact lookup of model with no slug column" do
26 | orphan = Orphan.create!(:name => 'Oliver')
27 | query = orphan.to_param
28 | assert_equal(orphan.id.to_s, query)
29 | end
30 |
31 | describe "slug column" do
32 | it "saves slug to 'slug' column by default" do
33 | article = Article.create!(:headline => 'Test Headline')
34 | assert_equal 'test-headline', article.slug
35 | end
36 |
37 | it "saves slug to :column specified in options" do
38 | Person.delete_all
39 | person = Person.create!(:name => 'Test Person')
40 | assert_equal 'test-person', person.web_slug
41 | end
42 | end
43 | end
44 |
45 | describe "column validations" do
46 | it "raises ArgumentError if an invalid source column is passed" do
47 | Company.slug(:invalid_source_column)
48 | assert_raises(ArgumentError) { Company.create! }
49 | end
50 |
51 | it "raises an ArgumentError if an invalid slug column is passed" do
52 | Company.slug(:name, :column => :bad_slug_column)
53 | assert_raises(ArgumentError) { Company.create! }
54 | end
55 | end
56 |
57 | describe 'generates a generic slug' do
58 | before do
59 | Generation.delete_all
60 | end
61 |
62 | it "if source column is empty" do
63 | generation = Generation.create!
64 | assert_equal 'generation', generation.slug
65 | end
66 |
67 | it "if normalization makes source value empty" do
68 | generation = Generation.create!(:title => '$$$')
69 | assert_equal 'generation', generation.slug
70 | end
71 |
72 | it "if source value contains no Latin characters" do
73 | generation = Generation.create!(:title => 'ローマ字がない')
74 | assert_equal 'generation', generation.slug
75 | end
76 | end
77 |
78 | describe 'validation' do
79 | it "sets validation error if source column is empty" do
80 | article = Article.create
81 | assert !article.valid?
82 | assert article.errors[:slug]
83 | end
84 |
85 | it "sets validation error if normalization makes source value empty" do
86 | article = Article.create(:headline => '$$$')
87 | assert !article.valid?
88 | assert article.errors[:slug]
89 | end
90 |
91 | it "validates slug format on save" do
92 | article = Article.create!(:headline => 'Test Headline')
93 | article.slug = 'A BAD $LUG.'
94 |
95 | assert !article.valid?
96 | assert article.errors[:slug].present?
97 | end
98 |
99 | it "validates uniqueness of slug by default" do
100 | Article.create!(:headline => 'Test Headline')
101 | article2 = Article.create!(:headline => 'Test Headline')
102 | article2.slug = 'test-headline'
103 |
104 | assert !article2.valid?
105 | assert article2.errors[:slug].present?
106 | end
107 |
108 | it "uses validate_uniqueness_if proc to decide whether uniqueness validation applies" do
109 | Post.create!(:headline => 'Test Headline')
110 | article2 = Post.new
111 | article2.slug = 'test-headline'
112 |
113 | assert article2.valid?
114 | end
115 | end
116 |
117 | it "doesn't overwrite slug value on create if it was already specified" do
118 | a = Article.create!(:headline => 'Test Headline', :slug => 'slug1')
119 | assert_equal 'slug1', a.slug
120 | end
121 |
122 | it "doesn't update the slug even if the source column changes" do
123 | article = Article.create!(:headline => 'Test Headline')
124 | article.update_attributes!(:headline => 'New Headline')
125 | assert_equal 'test-headline', article.slug
126 | end
127 |
128 | describe "resetting a slug" do
129 | before do
130 | @article = Article.create(:headline => 'test headline')
131 | @original_slug = @article.slug
132 | end
133 |
134 | it "maintains the same slug if slug column hasn't changed" do
135 | @article.reset_slug
136 | assert_equal @original_slug, @article.slug
137 | end
138 |
139 | it "changes slug if slug column has updated" do
140 | @article.headline = "donkey"
141 | @article.reset_slug
142 | refute_equal(@original_slug, @article.slug)
143 | end
144 |
145 | it "maintains sequence" do
146 | @existing_article = Article.create!(:headline => 'world cup')
147 | @article.headline = "world cup"
148 | @article.reset_slug
149 | assert_equal 'world-cup-1', @article.slug
150 | end
151 | end
152 |
153 | describe "slug normalization" do
154 | before do
155 | @article = Article.new
156 | end
157 |
158 | it "lowercases strings" do
159 | @article.headline = 'AbC'
160 | @article.save!
161 | assert_equal "abc", @article.slug
162 | end
163 |
164 | it "replaces whitespace with dashes" do
165 | @article.headline = 'a b'
166 | @article.save!
167 | assert_equal 'a-b', @article.slug
168 | end
169 |
170 | it "replaces 2spaces with 1dash" do
171 | @article.headline = 'a b'
172 | @article.save!
173 | assert_equal 'a-b', @article.slug
174 | end
175 |
176 | it "removes punctuation" do
177 | @article.headline = 'abc!@#$%^&*•¶§∞¢££¡¿()>""\':;][]\.,/'
178 | @article.save!
179 | assert_match 'abc', @article.slug
180 | end
181 |
182 | it "strips trailing space" do
183 | @article.headline = 'ab '
184 | @article.save!
185 | assert_equal 'ab', @article.slug
186 | end
187 |
188 | it "strips leading space" do
189 | @article.headline = ' ab'
190 | @article.save!
191 | assert_equal 'ab', @article.slug
192 | end
193 |
194 | it "strips trailing dashes" do
195 | @article.headline = 'ab-'
196 | @article.save!
197 | assert_match 'ab', @article.slug
198 | end
199 |
200 | it "strips leading dashes" do
201 | @article.headline = '-ab'
202 | @article.save!
203 | assert_match 'ab', @article.slug
204 | end
205 |
206 | it "remove double-dashes" do
207 | @article.headline = 'a--b--c'
208 | @article.save!
209 | assert_match 'a-b-c', @article.slug
210 | end
211 |
212 | it "doesn't modify valid slug strings" do
213 | @article.headline = 'a-b-c-d'
214 | @article.save!
215 | assert_match 'a-b-c-d', @article.slug
216 | end
217 |
218 | it "doesn't insert dashes for periods in acronyms, regardless of where they appear in string" do
219 | @article.headline = "N.Y.P.D. vs. N.S.A. vs. F.B.I."
220 | @article.save!
221 | assert_match 'nypd-vs-nsa-vs-fbi', @article.slug
222 | end
223 |
224 | it "doesn't insert dashes for apostrophes" do
225 | @article.headline = "Thomas Jefferson's Papers"
226 | @article.save!
227 | assert_match 'thomas-jeffersons-papers', @article.slug
228 | end
229 |
230 | it "preserves numbers in slug" do
231 | @article.headline = "2010 Election"
232 | @article.save!
233 | assert_match '2010-election', @article.slug
234 | end
235 | end
236 |
237 | describe "diacritics handling" do
238 | before do
239 | @article = Article.new
240 | end
241 |
242 | it "strips diacritics" do
243 | @article.headline = "açaí"
244 | @article.save!
245 | assert_equal "acai", @article.slug
246 | end
247 |
248 | it "strips diacritics correctly " do
249 | @article.headline = "ÀÁÂÃÄÅÆÇÈÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ"
250 | @article.save!
251 | expected = "aaaaaaaeceeeiiiidnoooooouuuuythssaaaaaaaeceeeeiiiidnoooooouuuuythy".split(//)
252 | output = @article.slug.split(//)
253 | output.each_index do |i|
254 | assert_equal expected[i], output[i]
255 | end
256 | end
257 | end
258 |
259 | describe "sequence handling" do
260 | it "doesn't add a sequence if saving first instance of slug" do
261 | article = Article.create!(:headline => 'Test Headline')
262 | assert_equal 'test-headline', article.slug
263 | end
264 |
265 | it "assigns a -1 suffix to the second instance of the slug" do
266 | Article.create!(:headline => 'Test Headline')
267 | article_2 = Article.create!(:headline => 'Test Headline')
268 | assert_equal 'test-headline-1', article_2.slug
269 | end
270 |
271 | it 'assigns a -2 suffix to the third instance of the slug containing numbers' do
272 | 2.times { |i| Article.create! :headline => '11111' }
273 | article_3 = Article.create! :headline => '11111'
274 | assert_equal '11111-2', article_3.slug
275 | end
276 |
277 | it "assigns a -12 suffix to the thirteenth instance of the slug" do
278 | 12.times { |i| Article.create!(:headline => 'Test Headline') }
279 | article_13 = Article.create!(:headline => 'Test Headline')
280 | assert_equal 'test-headline-12', article_13.slug
281 |
282 | 12.times { |i| Article.create!(:headline => 'latest from lybia') }
283 | article_13 = Article.create!(:headline => 'latest from lybia')
284 | assert_equal 'latest-from-lybia-12', article_13.slug
285 | end
286 |
287 | it "ignores partial matches when calculating sequence" do
288 | article_1 = Article.create!(:headline => 'Test Headline')
289 | assert_equal 'test-headline', article_1.slug
290 | article_2 = Article.create!(:headline => 'Test')
291 | assert_equal 'test', article_2.slug
292 | article_3 = Article.create!(:headline => 'Test')
293 | assert_equal 'test-1', article_3.slug
294 | article_4 = Article.create!(:headline => 'Test')
295 | assert_equal 'test-2', article_4.slug
296 | end
297 |
298 | it "knows about single table inheritance" do
299 | article = Article.create!(:headline => 'Test Headline')
300 | story = Storyline.create!(:headline => article.headline)
301 | assert_equal 'test-headline-1', story.slug
302 | end
303 |
304 | it "correctly slugs when a slug is a substring of another" do
305 | rap_metal = Article.create!(:headline => 'Rap Metal')
306 | assert_equal 'rap-metal', rap_metal.slug
307 |
308 | rap = Article.create!(:headline => 'Rap')
309 | assert_equal('rap', rap.slug)
310 | end
311 |
312 | it "applies sequence logic correctly when the slug is a substring of another" do
313 | rap_metal = Article.create!(:headline => 'Rap Metal')
314 | assert_equal 'rap-metal', rap_metal.slug
315 |
316 | Article.create!(:headline => 'Rap')
317 | second_rap = Article.create!(:headline => 'Rap')
318 | assert_equal('rap-1', second_rap.slug)
319 | end
320 | end
321 | end
322 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'minitest/autorun'
3 | require 'minitest/reporters'
4 |
5 | # You can use "rake test AR_VERSION=2.0.5" to test against 2.0.5, for example.
6 | # The default is to use the latest installed ActiveRecord.
7 | if ENV["AR_VERSION"]
8 | gem 'activerecord', "#{ENV["AR_VERSION"]}"
9 | end
10 | require 'active_record'
11 |
12 | # color test output
13 | Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new(:color => true)]
14 |
15 | $LOAD_PATH.unshift(File.dirname(__FILE__))
16 | require 'slug'
17 |
18 | ActiveRecord::Base.establish_connection :adapter => "sqlite3", :database => ":memory:"
19 | load(File.dirname(__FILE__) + "/schema.rb")
20 |
21 | require 'models'
22 |
--------------------------------------------------------------------------------