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.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +131 -0
- data/Rakefile +22 -0
- data/init.rb +1 -0
- data/lib/has_slug.rb +71 -0
- data/lib/has_slug/not_sluggable_class_methods.rb +56 -0
- data/lib/has_slug/not_sluggable_instance_methods.rb +26 -0
- data/lib/has_slug/slug.rb +41 -0
- data/lib/has_slug/sluggable_class_methods.rb +89 -0
- data/lib/has_slug/sluggable_instance_methods.rb +53 -0
- data/tasks/has_slug_tasks.rake +4 -0
- data/test/factories/city_factory.rb +3 -0
- data/test/factories/kitchen_factory.rb +3 -0
- data/test/factories/restaurant_factory.rb +5 -0
- data/test/models/city.rb +8 -0
- data/test/models/kitchen.rb +5 -0
- data/test/models/restaurant.rb +9 -0
- data/test/schema.rb +17 -0
- data/test/test_helper.rb +27 -0
- data/test/unit/has_slug_test.rb +171 -0
- data/test/unit/slug_test.rb +52 -0
- metadata +84 -0
data/MIT-LICENSE
ADDED
@@ -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.
|
data/README.rdoc
ADDED
@@ -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)
|
data/Rakefile
ADDED
@@ -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'
|
data/lib/has_slug.rb
ADDED
@@ -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
|
data/test/models/city.rb
ADDED
data/test/schema.rb
ADDED
@@ -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
|
data/test/test_helper.rb
ADDED
@@ -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
|