├── .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!@#$%^&*•¶§∞¢££¡¿()> '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 | --------------------------------------------------------------------------------