has_slug 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 Bill Eisenhauer & Andre Lewis
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.
@@ -0,0 +1,131 @@
1
+ = has_slug
2
+
3
+ == Description
4
+
5
+ has_slug is a plugin that add's slugging capabilities to ActiveRecord models.
6
+ It allows you to create SEO friendly URL's that will work the same as the
7
+ numeric defaults Rails provides you.
8
+
9
+ Using has_slug, you can make this:
10
+
11
+ http://example.com/articles/1
12
+
13
+ Look like this:
14
+
15
+ http://example.com/articles/1-first-post
16
+
17
+ Or even better:
18
+
19
+ http://example.com/articles/first-post
20
+
21
+ has_slug is inspired by friendly_id (http://github.com/norman/friendly_id/tree/master),
22
+ but instead of adding a new table for the slugs it uses a single column per model.
23
+
24
+ === Why?
25
+
26
+ * Text-based id's look better
27
+ * They make URL's easier to remember.
28
+ * They give no hint about the number of records in your database.
29
+ * They are better for search engine optimization.
30
+
31
+ === But ...
32
+
33
+ * It can be tricky to ensure they're always unique.
34
+ * They can change, breaking your URL's and your SEO.
35
+ * They can conflict with your application's namespace.
36
+
37
+ has_slug takes care of creating unique slugs by using a counter. The first time
38
+ a slug is generated for "Some Title", it will become "some-title". The second
39
+ time this is done, it will use the slug "some-title_2".
40
+
41
+ has_slug DOES NOT take care of changing slugs! If you need to care of this,
42
+ either use the friendly_id plugin, which DOES take care of this, or use a
43
+ custom solution.
44
+
45
+ has_slug also doesn't take care of the namespace conflicts (a slug named
46
+ 'new' for instance), you could use the following technique if this is
47
+ required: http://henrik.nyh.se/2008/10/validating-slugs-against-existing-routes-in-rails
48
+
49
+ == Usage
50
+
51
+ === Typical usage (with slug column)
52
+
53
+ Blog posts have a distinct, but not necessarily unique title stored in a column
54
+ in the database. If we add a slug column to the posts table, we can use has_slug
55
+ to generate slugs automatically.
56
+
57
+ class Post < ActiveRecord::Base
58
+ has_slug :title
59
+ end
60
+
61
+ We can then use the standard url helpers to generate a SEO friendly URL:
62
+
63
+ @post_1 = Post.create(:title => 'First Post',
64
+ :description => 'Description ...')
65
+
66
+ @post_2 = Post.create(:title => 'First Post',
67
+ :description => 'Description ...')
68
+
69
+ url_for(@post_1)
70
+ # => http://example.com/posts/first-post
71
+
72
+ url_for(@post_2)
73
+ # => http://example.com/posts/first-post_2
74
+
75
+ And use the standard finder to find the corresponding post:
76
+
77
+ @post = Post.find('first-post')
78
+
79
+ You can also use the <code>found_by_slug?</code> method to find out if the
80
+ record was found by the slug. You could use this to redirect to the URL with
81
+ the slug for SEO purposes.
82
+
83
+ redirect_to(@post, :status => 301) unless @post.found_by_slug?
84
+
85
+ == Typical usage (without slug column)
86
+
87
+ We can also use has_slug without adding the slug column. If we do this, the id
88
+ of the record is prepended to the slug.
89
+
90
+ @post = Post.find(1)
91
+
92
+ url_for(@post)
93
+ # => http://example.com/posts/1-first-post
94
+
95
+ == Preserving characters
96
+
97
+ Sometimes you'd like to preserve characters. For instance, if you'd like to have
98
+ slugs that look like filenames, you would want to preserve the ".". This is made
99
+ possible by the preserve option:
100
+
101
+ has_slug :filename, :preserve => "."
102
+
103
+ == Scoped usage
104
+
105
+ Restaurants belong to a city. They have a unique name, but only in the city they
106
+ are in. If we add a slug column to the restaurants table, we can use has_slug
107
+ to generate scoped slugs automatically.
108
+
109
+ class Restaurant < ActiveRecord::Base
110
+ belongs_to :city
111
+
112
+ has_slug :name, :scope => :city
113
+ end
114
+
115
+ We can then use the standard url helpers to generate a SEO friendly URL:
116
+
117
+ @restaurant_1 = Restaurant.create(:name => 'Da Marco',
118
+ :city => City.find('new-york'))
119
+
120
+ @restaurant_2 = Restaurant.create(:name => 'Da Marco',
121
+ :city => City.find('san-fransisco'))
122
+
123
+ url_for(@restaurant_1.city, @restaurant)
124
+ # => http://example.com/cities/new-york/restaurants/da-marco
125
+
126
+ url_for(@restaurant_2.city, @restaurant)
127
+ # => http://example.com/cities/san-fransisco/restaurants/da-marco
128
+
129
+ == Authors
130
+
131
+ * Tom-Eric Gerritsen (tomeric@i76.nl)
@@ -0,0 +1,22 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the has_slug plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/**/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the has_slug plugin.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'has_slug'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'has_slug'
@@ -0,0 +1,71 @@
1
+ require 'rubygems'
2
+
3
+ begin
4
+ require 'unicode'
5
+ rescue MissingSourceFile
6
+ require 'unicode_utils'
7
+ end
8
+
9
+ require 'has_slug/slug'
10
+
11
+ # has_slug is a slugging library for Ruby on Rails
12
+ module HasSlug
13
+
14
+ # Load has_slug if the gem is included
15
+ def self.enable
16
+ return if ActiveRecord::Base.methods.include? 'has_slug'
17
+
18
+ ActiveRecord::Base.class_eval { extend HasSlug::ClassMethods }
19
+ end
20
+
21
+ module ClassMethods
22
+
23
+ # Valid options for the has_slug method
24
+ VALID_HAS_SLUG_OPTIONS = [:scope, :slug_column, :preserve].freeze
25
+
26
+ # Default options for the has_slug method
27
+ DEFAULT_HAS_SLUG_OPTIONS = { :scope => nil, :slug_column => 'slug', :preserve => '' }.freeze
28
+
29
+ # Set up an ActiveRecord model to use a slug.
30
+ #
31
+ # The attribute argument can be one of your model's columns, or a method
32
+ # you use to generate the slug.
33
+ #
34
+ # Options:
35
+ # * <tt>:scope</tt> - The scope of the slug
36
+ # * <tt>:slug_column</tt> - The column that will be used to store the slug in (defaults to slug)
37
+ def has_slug(attribute, options = {})
38
+ options.assert_valid_keys(VALID_HAS_SLUG_OPTIONS)
39
+
40
+ options = DEFAULT_HAS_SLUG_OPTIONS.merge(options).merge(:attribute => attribute)
41
+
42
+ if defined?(has_slug_options)
43
+ raise Exception, "has_slug_options is already defined, you can only call has_slug once"
44
+ end
45
+
46
+ write_inheritable_attribute(:has_slug_options, options)
47
+ class_inheritable_reader(:has_slug_options)
48
+
49
+ if columns.any? { |column| column.name.to_s == options[:slug_column].to_s }
50
+ require 'has_slug/sluggable_class_methods'
51
+ require 'has_slug/sluggable_instance_methods'
52
+
53
+ extend SluggableClassMethods
54
+ include SluggableInstanceMethods
55
+
56
+ before_save :set_slug,
57
+ :if => :new_slug_needed?
58
+ else
59
+ require 'has_slug/not_sluggable_class_methods'
60
+ require 'has_slug/not_sluggable_instance_methods'
61
+
62
+ extend NotSluggableClassMethods
63
+ include NotSluggableInstanceMethods
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ if defined?(ActiveRecord)
70
+ HasSlug::enable
71
+ end
@@ -0,0 +1,56 @@
1
+ module HasSlug::NotSluggableClassMethods
2
+
3
+ def self.extended(base)
4
+ class << base
5
+ alias_method_chain :find_one, :slug
6
+ alias_method_chain :find_some, :slug
7
+ end
8
+ end
9
+
10
+ def find_one_with_slug(id_or_slug, options = {})
11
+ return find_one_without_slug(id_or_slug, options) if id_or_slug.is_a?(Fixnum)
12
+
13
+ if match = id_or_slug.match(/^([0-9]+)/)
14
+ result = find_one_without_slug(match[1].to_i, options)
15
+ result.found_by_slug! if id_or_slug == result.slug
16
+ else
17
+ result = find_one_without_slug(id_or_slug, options)
18
+ end
19
+
20
+ return result
21
+ end
22
+
23
+ def find_some_with_slug(ids_or_slugs, options = {})
24
+ return find_some_without_slug(ids_or_slugs, options) if ids_or_slugs.all? { |x| x.is_a?(Fixnum) }
25
+
26
+ ids = ids_or_slugs.map do |id_or_slug|
27
+ if match = id_or_slug.to_s.match(/^([0-9]+)/)
28
+ match[1].to_i
29
+ else
30
+ id_or_slug
31
+ end
32
+ end
33
+
34
+ find_options = options.dup
35
+
36
+ find_options[:conditions] ||= [""]
37
+ find_options[:conditions] = [find_options[:conditions] + " AND "] if find_options[:conditions].is_a?(String)
38
+
39
+ find_options[:conditions][0] << "#{quoted_table_name}.#{primary_key} IN (?)"
40
+ find_options[:conditions] << ids
41
+
42
+ found = find_every(find_options)
43
+ expected = ids_or_slugs.map(&:to_s).uniq
44
+
45
+ unless found.size == expected.size
46
+ raise ActiveRecord::RecordNotFound, "Couldn't find all #{ name.pluralize } with IDs (#{ ids_or_slugs * ', ' }) AND #{ sanitize_sql options[:conditions] } (found #{ found.size } results, but was looking for #{ expected.size })"
47
+ end
48
+
49
+ ids_or_slugs.each do |slug|
50
+ slug_record = found.detect { |record| record.slug == slug }
51
+ slug_record.found_by_slug! if slug_record
52
+ end
53
+
54
+ found
55
+ end
56
+ end
@@ -0,0 +1,26 @@
1
+ module HasSlug::NotSluggableInstanceMethods
2
+ attr :found_by_slug
3
+
4
+ def found_by_slug!
5
+ @found_by_slug = true
6
+ end
7
+
8
+ def found_by_slug?
9
+ @found_by_slug
10
+ end
11
+
12
+ def sluggable
13
+ read_attribute(self.class.has_slug_options[:attribute])
14
+ end
15
+
16
+ def slug
17
+ id = self.send(self.class.primary_key)
18
+ slug = self.sluggable.to_slug(:preserve => has_slug_options[:preserve])
19
+
20
+ "#{id}-#{slug}"
21
+ end
22
+
23
+ def to_param
24
+ self.slug
25
+ end
26
+ end
@@ -0,0 +1,41 @@
1
+ class String
2
+ # convert strings to slugs that are lowercase an only contain alphanumeric
3
+ # characters, dashes and sometimes dots
4
+ def to_slug(options = {})
5
+ slug = self
6
+
7
+ preserve = options.delete(:preserve).to_s.split
8
+ acceptable = preserve + ["-", "_"]
9
+
10
+ # Transliterate
11
+ if defined?(Unicode) && Unicode.respond_to?(:normalize_KD)
12
+ slug = Unicode.normalize_KD(slug).gsub(/[^\x00-\x7F]/n,'')
13
+ elsif defined?(UnicodeUtils) && UnicodeUtils.respond_to?(:compatibility_decomposition)
14
+ slug = UnicodeUtils.compatibility_decomposition(slug).gsub(/[^\x00-\x7F]/n,'')
15
+ end
16
+
17
+ # Convert to lowercase
18
+ slug.downcase!
19
+
20
+ # Change all characters that are not to be preserved to dashes
21
+ slug.gsub!(Regexp.new("[^a-z0-9#{Regexp.escape(preserve.join)}]"), '-')
22
+
23
+ # Preservable chars should be saved only when they have letter or number on
24
+ # both sides (this preserves file extensions in case of :preserve => '.')
25
+ preserve.each do |char|
26
+ slug.gsub!(Regexp.new("([^a-z0-9])#{Regexp.escape(char)}+"), '\\1-')
27
+ slug.gsub!(Regexp.new("#{Regexp.escape(char)}+([^a-z0-9])"), '-\\1')
28
+ slug.gsub!(Regexp.new("^#{Regexp.escape(char)}+"), '')
29
+ slug.gsub!(Regexp.new("#{Regexp.escape(char)}+$"), '')
30
+ end
31
+
32
+ # Strip dashes from begining and end
33
+ slug.gsub!(/^-(.+)+/, '\1')
34
+ slug.gsub!(/(.+)-+$/, '\1')
35
+
36
+ # Change multiple succesive dashes to single dash.
37
+ slug.gsub!(/-+/, '-')
38
+
39
+ slug
40
+ end
41
+ end
@@ -0,0 +1,89 @@
1
+ module HasSlug::SluggableClassMethods
2
+
3
+ def self.extended(base) #:nodoc:#
4
+ class << base
5
+ alias_method_chain :find_one, :slug
6
+ alias_method_chain :find_some, :slug
7
+ end
8
+ end
9
+
10
+ def slug_scope_attribute
11
+ return @@slug_scope_attribute if defined?(@@slug_scope_attribute)
12
+
13
+ if scope = has_slug_options[:scope]
14
+ if columns.any? { |c| c.name == scope }
15
+ @@slug_scope_attribute = scope.to_sym
16
+ elsif columns.any? { |c| c.name == "#{scope}_id" }
17
+ @@slug_scope_attribute = "#{scope}_id".to_sym
18
+ else
19
+ raise Exception, "has_slug's scope '#{scope}' does not exist in the model '#{self.class.to_s}'"
20
+ end
21
+ end
22
+
23
+ @@slug_scope_attribute
24
+ end
25
+
26
+ # Find a single record that has the same slug as the given record's slug
27
+ def find_one_with_same_slug(object)
28
+ slug_column = has_slug_options[:slug_column]
29
+
30
+ options = if object.new_record? then {}
31
+ else { :conditions => ["#{quoted_table_name}.#{primary_key} != ?", object] }
32
+ end
33
+
34
+ if scope = has_slug_options[:scope]
35
+ result = send("find_by_#{slug_column}_and_#{slug_scope_attribute}",
36
+ object.slug, object.send(scope), options)
37
+ else
38
+ result = send("find_by_#{slug_column}", object.slug, options)
39
+ end
40
+
41
+ result.found_by_slug! if result
42
+
43
+ result
44
+ end
45
+
46
+ # Find a single record using the record's slug or the record's id
47
+ def find_one_with_slug(id_or_slug, options = {})
48
+ return find_one_without_slug(id_or_slug, options) if id_or_slug.is_a?(Fixnum)
49
+
50
+ slug_column = has_slug_options[:slug_column]
51
+
52
+ if result = send("find_by_#{slug_column}", id_or_slug, options)
53
+ result.found_by_slug!
54
+ else
55
+ result = find_one_without_slug(id_or_slug, options)
56
+ end
57
+
58
+ result
59
+ end
60
+
61
+ # Find multiple records using the records slugs or the records id's
62
+ def find_some_with_slug(ids_or_slugs, options = {})
63
+ return find_some_without_slug(ids_or_slugs, options) if ids_or_slugs.all? { |x| x.is_a?(Fixnum) }
64
+
65
+ find_options = options.dup
66
+
67
+ find_options[:conditions] ||= [""]
68
+ find_options[:conditions] = [find_options[:conditions] + " AND "] if find_options[:conditions].is_a?(String)
69
+ find_options[:conditions][0] << "(#{quoted_table_name}.#{primary_key} IN (?)" <<
70
+ " OR #{quoted_table_name}.#{has_slug_options[:slug_column]} IN (?))"
71
+
72
+ find_options[:conditions] << ids_or_slugs
73
+ find_options[:conditions] << ids_or_slugs.map(&:to_s)
74
+
75
+ found = find_every(find_options)
76
+ expected = ids_or_slugs.map(&:to_s).uniq
77
+
78
+ unless found.size == expected.size
79
+ raise ActiveRecord::RecordNotFound, "Couldn't find all #{name.pluralize} with IDs (#{ids_or_slugs * ', '}) AND #{sanitize_sql(options[:conditions])} (found #{found.size} results, but was looking for #{expected.size})"
80
+ end
81
+
82
+ ids_or_slugs.each do |slug|
83
+ slug_record = found.detect { |record| record.send(has_slug_options[:slug_column]) == slug }
84
+ slug_record.found_by_slug! if slug_record
85
+ end
86
+
87
+ found
88
+ end
89
+ end
@@ -0,0 +1,53 @@
1
+ module HasSlug::SluggableInstanceMethods
2
+ attr :found_by_slug
3
+
4
+ def found_by_slug!
5
+ @found_by_slug = true
6
+ end
7
+
8
+ def found_by_slug?
9
+ @found_by_slug
10
+ end
11
+
12
+ def sluggable
13
+ read_attribute(self.class.has_slug_options[:attribute])
14
+ end
15
+
16
+ def slug
17
+ read_attribute(self.class.has_slug_options[:slug_column])
18
+ end
19
+
20
+ def slug=(slug)
21
+ write_attribute(self.class.has_slug_options[:slug_column], slug)
22
+ end
23
+
24
+ def new_slug_needed?
25
+ slug_changed = self.send("#{self.class.has_slug_options[:slug_column]}_changed?")
26
+ sluggable_changed = self.send("#{self.class.has_slug_options[:attribute]}_changed?")
27
+
28
+ scope_changed = if self.class.has_slug_options[:scope] then self.send("#{self.class.slug_scope_attribute}_changed?")
29
+ else false
30
+ end
31
+
32
+ (!slug_changed && (self.new_record? || sluggable_changed)) || scope_changed
33
+
34
+ end
35
+
36
+ def to_param
37
+ self.slug || self.id
38
+ end
39
+
40
+ private
41
+
42
+ def set_slug
43
+ self.slug = self.sluggable.to_slug(:preserve => has_slug_options[:preserve])
44
+
45
+ while existing = self.class.find_one_with_same_slug(self)
46
+ index ||= 2
47
+
48
+ self.slug = "#{self.sluggable.to_slug(:preserve => has_slug_options[:preserve])}_#{index}"
49
+
50
+ index += 1
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :has_slug do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,3 @@
1
+ Factory.define :city do |city|
2
+ city.name 'New York'
3
+ end
@@ -0,0 +1,3 @@
1
+ Factory.define :kitchen do |kitchen|
2
+ kitchen.name 'Italian'
3
+ end
@@ -0,0 +1,5 @@
1
+ Factory.define :restaurant do |restaurant|
2
+ restaurant.name 'Da Marco'
3
+ restaurant.city { |city| city.association(:city) }
4
+ restaurant.kitchen { |kitchen| kitchen.association(:kitchen) }
5
+ end
@@ -0,0 +1,8 @@
1
+ class City < ActiveRecord::Base
2
+ has_slug :name
3
+
4
+ has_many :restaurants
5
+
6
+ has_many :kitchens,
7
+ :through => :restaurants
8
+ end
@@ -0,0 +1,5 @@
1
+ class Kitchen < ActiveRecord::Base
2
+ has_slug :name
3
+
4
+ belongs_to :restaurant
5
+ end
@@ -0,0 +1,9 @@
1
+ class Restaurant < ActiveRecord::Base
2
+ has_slug :name,
3
+ :scope => :city,
4
+ :preserve => '.'
5
+
6
+ belongs_to :city
7
+
8
+ belongs_to :kitchen
9
+ end
@@ -0,0 +1,17 @@
1
+ ActiveRecord::Schema.define(:version => 1) do
2
+ create_table 'cities', :force => true do |t|
3
+ t.column 'name', :string
4
+ t.column 'slug', :string
5
+ end
6
+
7
+ create_table 'restaurants', :force => true do |t|
8
+ t.column 'name', :string
9
+ t.column 'slug', :string
10
+ t.column 'city_id', :integer
11
+ t.column 'kitchen_id', :integer
12
+ end
13
+
14
+ create_table 'kitchens', :force => true do |t|
15
+ t.column 'name', :string
16
+ end
17
+ end
@@ -0,0 +1,27 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'sqlite3'
4
+ require 'activerecord'
5
+ require 'shoulda'
6
+ require 'factory_girl'
7
+
8
+ HAS_SLUG_ROOT = File.dirname(__FILE__) + "/.."
9
+ $:.unshift("#{HAS_SLUG_ROOT}/lib")
10
+
11
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3",
12
+ :dbfile => "#{HAS_SLUG_ROOT}/test/test.db")
13
+ require 'has_slug'
14
+ require "#{HAS_SLUG_ROOT}/test/schema.rb"
15
+
16
+ Dir["#{HAS_SLUG_ROOT}/test/models/*"].each { |f| require f }
17
+ Dir["#{HAS_SLUG_ROOT}/test/factories/*"].each { |f| require f }
18
+
19
+ [City, Restaurant, Kitchen].each { |c| c.reset_column_information }
20
+
21
+ class Test::Unit::TestCase
22
+ def reset_database!
23
+ City.destroy_all
24
+ Restaurant.destroy_all
25
+ Kitchen.destroy_all
26
+ end
27
+ end
@@ -0,0 +1,171 @@
1
+ # encoding: utf-8
2
+ require "#{File.dirname(__FILE__)}/../test_helper"
3
+
4
+ class HasSlugTest < Test::Unit::TestCase
5
+ context 'A model' do
6
+ setup do
7
+ reset_database!
8
+ end
9
+
10
+ context 'without a slug column' do
11
+ setup do
12
+ @italian = Factory(:kitchen, :name => 'Italian')
13
+ @french = Factory(:kitchen, :name => 'French')
14
+ end
15
+
16
+ should 'set the slug' do
17
+ assert_not_nil @italian.slug
18
+ end
19
+
20
+ should 'return the slug on call to #to_param' do
21
+ assert_equal @italian.slug, @italian.to_param
22
+ end
23
+
24
+ should 'still find by id' do
25
+ kitchen = Kitchen.find(@italian.id)
26
+
27
+ assert_equal @italian, kitchen
28
+ assert !kitchen.found_by_slug?
29
+ end
30
+
31
+ should 'find one by slug' do
32
+ kitchen = Kitchen.find(@italian.slug)
33
+
34
+ assert_equal @italian, kitchen
35
+ assert kitchen.found_by_slug?
36
+ end
37
+
38
+ should 'still find some by id' do
39
+ kitchens = Kitchen.find([@italian.id, @french.id])
40
+
41
+ assert_equal 2, kitchens.length
42
+ assert !kitchens.any?(&:found_by_slug?)
43
+ end
44
+
45
+ should 'find some by slug' do
46
+ kitchens = Kitchen.find([@italian.slug, @french.slug])
47
+
48
+ assert_equal 2, kitchens.length
49
+ assert kitchens.all?(&:found_by_slug?)
50
+ end
51
+
52
+ should 'find some by id or slug' do
53
+ kitchens = Kitchen.find([@italian.id, @french.slug])
54
+
55
+ assert_equal 2, kitchens.length
56
+ assert !kitchens[0].found_by_slug?
57
+ assert kitchens[1].found_by_slug?
58
+ end
59
+ end
60
+
61
+ context 'with a slug column' do
62
+ setup do
63
+ @new_york = Factory(:city, :name => 'New York')
64
+ @san_francisco = Factory(:city, :name => 'San Francisco')
65
+ end
66
+
67
+ context 'and a custom slug' do
68
+ setup do
69
+ @custom = Factory(:city, :name => 'Las Vegas', :slug => 'lv')
70
+ end
71
+
72
+ should 'not update the slug unless sluggable_column changed' do
73
+ @custom.save
74
+ assert_equal 'lv', @custom.slug
75
+ end
76
+
77
+ should 'update the slug if sluggable_column changed' do
78
+ @custom.update_attributes(:name => 'Los Angeles')
79
+
80
+ assert_equal 'los-angeles', @custom.slug
81
+ end
82
+ end
83
+
84
+ context 'and characters to preserve' do
85
+ setup do
86
+ @restaurant_2_0 = Factory(:restaurant, :name => 'Restaurant 2.0')
87
+ end
88
+
89
+ should 'preserve the .' do
90
+ assert_equal 'restaurant-2.0', @restaurant_2_0.slug
91
+ end
92
+ end
93
+
94
+ context 'and a scope' do
95
+ setup do
96
+ @da_marco = Factory(:restaurant, :name => 'Da Marco',
97
+ :city => @new_york)
98
+ end
99
+
100
+ should 'create the same slug in a different scope' do
101
+ @da_marco_2 = Factory(:restaurant, :name => 'Da Marco',
102
+ :city => @san_francisco)
103
+
104
+ assert_equal @da_marco_2.slug, @da_marco.slug
105
+ end
106
+
107
+ should 'not create duplicate slugs' do
108
+ @da_marco_2 = Factory(:restaurant, :name => 'Da Marco',
109
+ :city => @new_york)
110
+
111
+ assert_not_equal @da_marco_2.slug, @da_marco.slug
112
+
113
+ @da_marco_2.update_attributes(:city => @san_fransisco)
114
+ assert_equal @da_marco_2.slug, @da_marco.slug
115
+ end
116
+ end
117
+
118
+ should 'set the slug' do
119
+ assert_equal 'new-york', @new_york.slug
120
+ assert_equal 'san-francisco', @san_francisco.slug
121
+ end
122
+
123
+ should 'return the slug on call to #to_param' do
124
+ assert_equal @new_york.slug, @new_york.to_param
125
+ assert_equal @san_francisco.slug, @san_francisco.to_param
126
+ end
127
+
128
+ should 'not create duplicate slugs' do
129
+ @new_york_2 = Factory(:city, :name => 'New-York')
130
+
131
+ assert_not_equal @new_york_2.slug, @new_york.slug
132
+ end
133
+
134
+ should 'still find by id' do
135
+ city = City.find(@new_york.id)
136
+
137
+ assert_equal @new_york, city
138
+ assert !city.found_by_slug?
139
+ end
140
+
141
+ should 'find one by slug' do
142
+ city = City.find(@new_york.slug)
143
+
144
+ assert_equal @new_york, city
145
+ assert city.found_by_slug?
146
+ end
147
+
148
+ should 'still find some by id' do
149
+ cities = City.find([@new_york.id, @san_francisco.id])
150
+
151
+ assert_equal 2, cities.length
152
+ assert !cities.any?(&:found_by_slug?)
153
+ end
154
+
155
+ should 'find some by slug' do
156
+ cities = City.find([@new_york.slug, @san_francisco.slug])
157
+
158
+ assert_equal 2, cities.length
159
+ assert cities.all?(&:found_by_slug?)
160
+ end
161
+
162
+ should 'find some by id or slug' do
163
+ cities = City.find([@new_york.id, @san_francisco.slug])
164
+
165
+ assert_equal 2, cities.length
166
+ assert !cities[0].found_by_slug?
167
+ assert cities[1].found_by_slug?
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,52 @@
1
+ # encoding: utf-8
2
+ require "#{File.dirname(__FILE__)}/../test_helper"
3
+
4
+ class SlugTest < Test::Unit::TestCase
5
+ context 'A Slug' do
6
+ should 'be lowercase' do
7
+ assert_equal 'abc', "ABC".to_slug
8
+ end
9
+
10
+ should 'have replaced spaces with dashes' do
11
+ assert_equal 'abc-def', "abc def".to_slug
12
+ end
13
+
14
+ should 'have replaced special characters with dashes' do
15
+ assert_equal 'l-atelier', "l'Atelier".to_slug
16
+ assert_equal 'five-stars', "Five*Stars".to_slug
17
+ assert_equal '-', "***".to_slug
18
+ end
19
+
20
+ should 'have stripped utf-8 characters' do
21
+ assert_equal 'internationalization', "Iñtërnâtiônàlizâtiôn".to_slug
22
+ end
23
+
24
+ should 'preserve specified characters if they have numbers or letters on both sides' do
25
+ assert_equal 'filename-1', "filename.1".to_slug
26
+ assert_equal 'filename.1', "filename.1".to_slug(:preserve => '.')
27
+
28
+ assert_equal 'filename-txt', "filename.txt".to_slug
29
+ assert_equal 'filename.txt', "filename.txt".to_slug(:preserve => '.')
30
+
31
+ assert_equal 'a-sentence', "A sentence.".to_slug
32
+ assert_equal 'a-sentence', "A sentence.".to_slug(:preserve => '.')
33
+
34
+ assert_equal 'a-sentence-a-new-sentence', "A sentence. A new sentence.".to_slug
35
+ assert_equal 'a-sentence-a-new-sentence', "A sentence. A new sentence.".to_slug(:preserve => '.')
36
+ end
37
+
38
+ should 'not have trailing dashes' do
39
+ assert_equal 'abc-def', "abc def ".to_slug
40
+ assert_equal 'abc-def', "abc def-".to_slug
41
+ end
42
+
43
+ should 'not have leading dashes' do
44
+ assert_equal 'abc-def', " abc def".to_slug
45
+ assert_equal 'abc-def', "-abc def".to_slug
46
+ end
47
+
48
+ should 'not have succesive dashes' do
49
+ assert_equal 'abc-def', "abc def".to_slug
50
+ end
51
+ end
52
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: has_slug
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Tom-Eric Gerritsen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-12-29 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: unicode
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description: has_slug is a plugin that provides slugging capabilities to Ruby on Rails models. Inspired by the friendly_id plugin.
26
+ email: tomeric@i76.nl
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - init.rb
35
+ - MIT-LICENSE
36
+ - Rakefile
37
+ - README.rdoc
38
+ - lib/has_slug.rb
39
+ - lib/has_slug/slug.rb
40
+ - lib/has_slug/not_sluggable_class_methods.rb
41
+ - lib/has_slug/not_sluggable_instance_methods.rb
42
+ - lib/has_slug/sluggable_class_methods.rb
43
+ - lib/has_slug/sluggable_instance_methods.rb
44
+ - tasks/has_slug_tasks.rake
45
+ has_rdoc: true
46
+ homepage: http://www.i76.nl/
47
+ licenses: []
48
+
49
+ post_install_message:
50
+ rdoc_options:
51
+ - --main
52
+ - README.rdoc
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ version:
67
+ requirements: []
68
+
69
+ rubyforge_project:
70
+ rubygems_version: 1.3.5
71
+ signing_key:
72
+ specification_version: 2
73
+ summary: A slugging plugin for Ruby on Rails
74
+ test_files:
75
+ - test/schema.rb
76
+ - test/test_helper.rb
77
+ - test/factories/city_factory.rb
78
+ - test/factories/kitchen_factory.rb
79
+ - test/factories/restaurant_factory.rb
80
+ - test/models/city.rb
81
+ - test/models/kitchen.rb
82
+ - test/models/restaurant.rb
83
+ - test/unit/has_slug_test.rb
84
+ - test/unit/slug_test.rb