sluggable 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +28 -6
- data/Rakefile +23 -2
- data/lib/sluggable.rb +21 -13
- data/lib/sluggable/version.rb +1 -1
- data/test/support/configuration.rb +7 -0
- data/test/support/database.rb +15 -0
- data/test/support/test.sqlite3 +0 -0
- data/test/test_helper.rb +9 -1
- data/test/unit/sluggable_test.rb +103 -54
- metadata +4 -1
data/README.rdoc
CHANGED
@@ -18,26 +18,48 @@ Then, in your ActiveRecord model you just need to include the module
|
|
18
18
|
and provide the column to use when generating the slug. For example,
|
19
19
|
if we had a Post model with:
|
20
20
|
|
21
|
-
class
|
21
|
+
class Blog < ActiveRecord::Base
|
22
22
|
# Columns:
|
23
|
-
#
|
24
|
-
#
|
25
|
-
# slug: string
|
23
|
+
# name: string
|
24
|
+
# slug: string
|
26
25
|
|
27
26
|
include Sluggable
|
28
|
-
|
27
|
+
slug_from :name
|
29
28
|
|
30
29
|
before_validation :generate_slug
|
31
30
|
|
32
31
|
end
|
33
32
|
|
34
|
-
This sets it up so that the value of the `
|
33
|
+
This sets it up so that the value of the `name` column is used as the
|
35
34
|
source when generating the slug. The module also provides a private
|
36
35
|
method (`generate_slug`) to generate and store the slug for this record.
|
37
36
|
It requires that the table have a column called `slug` and is typically
|
38
37
|
called in a lifecycle hook, though it can be called from anywhere you
|
39
38
|
need it.
|
40
39
|
|
40
|
+
If you have a model that allows the same slug when scoped to another
|
41
|
+
associated model, you can provide a parameter as part of the `slug_from`
|
42
|
+
method:
|
43
|
+
|
44
|
+
class Post < ActiveRecord::Base
|
45
|
+
# Columns:
|
46
|
+
# blog_id: integer
|
47
|
+
# title: string
|
48
|
+
# body: text
|
49
|
+
# slug: string
|
50
|
+
|
51
|
+
belongs_to :blog
|
52
|
+
|
53
|
+
include Sluggable
|
54
|
+
slug_from :title, :scope => :blog_id
|
55
|
+
|
56
|
+
before_validation :generate_slug
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
This will allow the same slug to be generated for 2 posts each scoped to
|
61
|
+
a different blog.
|
62
|
+
|
41
63
|
== License
|
42
64
|
|
43
65
|
Copyright (c) 2009 Patrick Reagan (mailto:reaganpr@gmail.com)
|
data/Rakefile
CHANGED
@@ -24,7 +24,28 @@ Rake::GemPackageTask.new(spec) do |pkg|
|
|
24
24
|
pkg.gem_spec = spec
|
25
25
|
end
|
26
26
|
|
27
|
-
|
27
|
+
namespace :db do
|
28
|
+
desc 'Create the test database'
|
29
|
+
task :create do
|
30
|
+
require 'activerecord'
|
31
|
+
require 'test/support/configuration'
|
32
|
+
|
33
|
+
path = File.dirname(__FILE__) + '/test/support'
|
34
|
+
|
35
|
+
ActiveRecord::Base.establish_connection(Configuration.database(path))
|
36
|
+
ActiveRecord::Base.connection
|
37
|
+
|
38
|
+
load(File.dirname(__FILE__) + '/test/support/database.rb')
|
39
|
+
end
|
40
|
+
|
41
|
+
task :drop do
|
42
|
+
Dir[File.dirname(__FILE__) + '/test/support/*.sqlite3'].each {|f| FileUtils.rm(f) }
|
43
|
+
end
|
44
|
+
|
45
|
+
task :reset => [:drop, :create]
|
46
|
+
end
|
47
|
+
|
48
|
+
Rake::TestTask.new(:test => 'db:reset') do |t|
|
28
49
|
t.libs << 'test'
|
29
50
|
t.test_files = FileList["test/**/*_test.rb"]
|
30
51
|
t.verbose = true
|
@@ -33,7 +54,7 @@ end
|
|
33
54
|
begin
|
34
55
|
require 'rcov/rcovtask'
|
35
56
|
|
36
|
-
Rcov::RcovTask.new(:coverage) do |t|
|
57
|
+
Rcov::RcovTask.new(:coverage => 'db:reset') do |t|
|
37
58
|
t.libs = ['test']
|
38
59
|
t.test_files = FileList["test/**/*_test.rb"]
|
39
60
|
t.verbose = true
|
data/lib/sluggable.rb
CHANGED
@@ -6,18 +6,10 @@ require 'core_ext/string'
|
|
6
6
|
module Sluggable
|
7
7
|
module ClassMethods
|
8
8
|
|
9
|
-
# Find all the other slugs in the database that don't belong to the
|
10
|
-
# record specified by the +:id+ parameter.
|
11
|
-
def others_by_slug(id, slug)
|
12
|
-
conditions = id.nil? ? {} : {:conditions => ['id != ?', id]}
|
13
|
-
find_by_slug(slug, conditions)
|
14
|
-
end
|
15
|
-
|
16
9
|
# Determine the database column to use when generating the slug
|
17
|
-
def
|
18
|
-
define_method(:
|
19
|
-
|
20
|
-
end
|
10
|
+
def slug_from(column, options = {})
|
11
|
+
define_method(:slug_source) { column }
|
12
|
+
define_method(:slug_scope) { Array(options[:scope]) }
|
21
13
|
end
|
22
14
|
|
23
15
|
end
|
@@ -28,7 +20,7 @@ module Sluggable
|
|
28
20
|
valid_slug = base_slug
|
29
21
|
|
30
22
|
index = 2
|
31
|
-
while self.class.
|
23
|
+
while self.class.find_by_slug(valid_slug, slug_conditions)
|
32
24
|
valid_slug = base_slug + "-#{index}"
|
33
25
|
index+= 1
|
34
26
|
end
|
@@ -36,7 +28,23 @@ module Sluggable
|
|
36
28
|
end
|
37
29
|
|
38
30
|
def generate_slug # :nodoc:
|
39
|
-
self.slug = next_available_slug(self.send(
|
31
|
+
self.slug = next_available_slug(self.send(slug_source).sluggify)
|
32
|
+
end
|
33
|
+
|
34
|
+
def conditions_for(column, include = true)
|
35
|
+
operator = include ? '=' : '!='
|
36
|
+
["#{column} #{operator} ?", self[column]] unless self[column].blank?
|
37
|
+
end
|
38
|
+
|
39
|
+
def slug_conditions
|
40
|
+
condition_parts = [conditions_for(:id, false)]
|
41
|
+
condition_parts += slug_scope.map {|c| conditions_for(c) }
|
42
|
+
condition_parts.compact!
|
43
|
+
|
44
|
+
condition_string = condition_parts.map {|p| p[0] }.join(' AND ')
|
45
|
+
condition_parameters = condition_parts.map {|p| p[1] }
|
46
|
+
|
47
|
+
condition_parts.empty? ? {} : {:conditions => [condition_string, *condition_parameters]}
|
40
48
|
end
|
41
49
|
|
42
50
|
private :next_available_slug, :generate_slug
|
data/lib/sluggable/version.rb
CHANGED
@@ -0,0 +1,15 @@
|
|
1
|
+
ActiveRecord::Schema.define do
|
2
|
+
|
3
|
+
create_table "blogs", :force => true do |t|
|
4
|
+
t.string "name", :null => false
|
5
|
+
t.string "slug"
|
6
|
+
end
|
7
|
+
|
8
|
+
create_table "posts", :force => true do |t|
|
9
|
+
t.string "title", :null => false
|
10
|
+
t.string "slug"
|
11
|
+
t.text "body", :null => false
|
12
|
+
t.integer "blog_id", :null => false
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
Binary file
|
data/test/test_helper.rb
CHANGED
@@ -4,4 +4,12 @@ $:.reject! { |e| e.include? 'TextMate' }
|
|
4
4
|
require 'rubygems'
|
5
5
|
require 'throat_punch'
|
6
6
|
|
7
|
-
require
|
7
|
+
require 'activerecord'
|
8
|
+
|
9
|
+
require File.dirname(__FILE__) + '/../lib/sluggable'
|
10
|
+
|
11
|
+
path = File.dirname(__FILE__) + '/support'
|
12
|
+
require "#{path}/configuration"
|
13
|
+
|
14
|
+
ActiveRecord::Base.establish_connection(Configuration.database(path))
|
15
|
+
|
data/test/unit/sluggable_test.rb
CHANGED
@@ -1,80 +1,129 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/../test_helper'
|
2
2
|
|
3
|
-
class
|
3
|
+
class Blog < ActiveRecord::Base
|
4
|
+
|
5
|
+
include Sluggable
|
6
|
+
slug_from :name
|
7
|
+
|
8
|
+
end
|
9
|
+
|
10
|
+
class Post < ActiveRecord::Base
|
11
|
+
|
4
12
|
include Sluggable
|
13
|
+
slug_from :title, :scope => :blog_id
|
14
|
+
|
5
15
|
end
|
6
16
|
|
7
17
|
class SluggableImplementationTest < Test::Unit::TestCase
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
18
|
+
|
19
|
+
context "An instance of the Blog class" do
|
20
|
+
teardown { Blog.destroy_all }
|
21
|
+
|
22
|
+
should "know the column to use when generating the slug" do
|
23
|
+
Blog.new.slug_source.should == :name
|
13
24
|
end
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
setup do
|
18
|
-
@object = stub()
|
19
|
-
@slug = 'sample-slug'
|
25
|
+
|
26
|
+
should "know the scope for finding the uniqueness of the slug" do
|
27
|
+
Blog.new.slug_scope.should == []
|
20
28
|
end
|
21
|
-
|
22
|
-
should "
|
23
|
-
|
24
|
-
|
25
|
-
assert_equal @object, SluggableImplementation.others_by_slug(id, @slug)
|
29
|
+
|
30
|
+
should "know how to generate the conditions for a column when it exists" do
|
31
|
+
blog = Blog.create!(:name => 'Mine')
|
32
|
+
blog.conditions_for(:id).should == ['id = ?', blog.id]
|
26
33
|
end
|
27
34
|
|
28
|
-
should "
|
29
|
-
|
30
|
-
|
35
|
+
should "know how to generate the conditions for a column when it should not be included" do
|
36
|
+
blog = Blog.create!(:name => 'Mine')
|
37
|
+
blog.conditions_for(:id, false).should == ['id != ?', blog.id]
|
31
38
|
end
|
32
|
-
|
33
|
-
should "
|
34
|
-
|
35
|
-
|
39
|
+
|
40
|
+
should "know how to generate the conditions for a column when it doesn't exist" do
|
41
|
+
blog = Blog.new
|
42
|
+
blog.conditions_for(:id).should be_nil
|
43
|
+
end
|
44
|
+
|
45
|
+
should "generate an empty set of conditions when there is no id" do
|
46
|
+
blog = Blog.new
|
47
|
+
blog.slug_conditions.should == {}
|
48
|
+
end
|
49
|
+
|
50
|
+
should "generate a set of conditions when there is an ID" do
|
51
|
+
blog = Blog.create!(:name => 'Me')
|
52
|
+
blog.slug_conditions.should == {:conditions => ['id != ?', blog.id]}
|
53
|
+
end
|
54
|
+
|
55
|
+
should "know the next available slug when the original is taken" do
|
56
|
+
Blog.create!(:name => 'One', :slug => 'one')
|
57
|
+
|
58
|
+
blog = Blog.new
|
59
|
+
blog.send(:next_available_slug, 'one').should == 'one-2'
|
60
|
+
end
|
61
|
+
|
62
|
+
should "incrementally suggest slugs until it finds an available one" do
|
63
|
+
Blog.create!(:name => 'One', :slug => 'one')
|
64
|
+
Blog.create!(:name => 'One', :slug => 'one-2')
|
65
|
+
|
66
|
+
blog = Blog.new
|
67
|
+
|
68
|
+
blog.send(:next_available_slug, 'one').should == 'one-3'
|
69
|
+
end
|
70
|
+
|
71
|
+
should "know not to suggest an incremental slug when the existing slug belongs to the current record" do
|
72
|
+
blog = Blog.create!(:name => 'One', :slug => 'one')
|
73
|
+
blog.send(:next_available_slug, 'one').should == 'one'
|
74
|
+
end
|
75
|
+
|
76
|
+
should "be able to assign a valid slug to the slug property" do
|
77
|
+
blog = Blog.new(:name => 'One')
|
78
|
+
blog.send(:generate_slug)
|
36
79
|
|
37
|
-
|
80
|
+
blog.slug.should == 'one'
|
38
81
|
end
|
39
|
-
|
82
|
+
|
40
83
|
end
|
84
|
+
|
85
|
+
context "An instance of the Post class" do
|
86
|
+
teardown do
|
87
|
+
Post.destroy_all
|
88
|
+
Blog.destroy_all
|
89
|
+
end
|
41
90
|
|
42
|
-
|
43
|
-
|
44
|
-
setup do
|
45
|
-
@id = 1
|
46
|
-
@slug = 'title'
|
47
|
-
|
48
|
-
@sluggable = SluggableImplementation.new
|
49
|
-
@sluggable.stubs(:id).returns(@id)
|
91
|
+
should "know the scope for finding the uniqueness of the slug" do
|
92
|
+
Post.new.slug_scope.should == [:blog_id]
|
50
93
|
end
|
51
|
-
|
52
|
-
should "
|
53
|
-
|
54
|
-
|
94
|
+
|
95
|
+
should "be able to generate a set of conditions when there is no ID" do
|
96
|
+
p = Post.new(:blog_id => 1)
|
97
|
+
p.slug_conditions.should == {:conditions => ['blog_id = ?', 1]}
|
55
98
|
end
|
56
|
-
|
57
|
-
should "
|
58
|
-
|
59
|
-
|
99
|
+
|
100
|
+
should "be able to generate a set of conditions when there is an ID" do
|
101
|
+
p = Post.create!(:title => 'Title', :body => 'Text', :blog_id => 1)
|
102
|
+
p.slug_conditions.should == {:conditions => ['id != ? AND blog_id = ?', p.id, 1]}
|
60
103
|
end
|
61
104
|
|
62
|
-
should "
|
63
|
-
|
64
|
-
|
105
|
+
should "maintain the current slug when one exists scoped to another blog_id" do
|
106
|
+
Post.create!(:title => 'Title', :body => 'Text', :blog_id => 1, :slug => 'title')
|
107
|
+
p = Post.new(:title => 'Title', :body => 'Text', :blog_id => 2)
|
65
108
|
|
66
|
-
|
109
|
+
p.send(:generate_slug)
|
110
|
+
|
111
|
+
p.slug.should == 'title'
|
67
112
|
end
|
68
113
|
|
69
|
-
should "
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
114
|
+
should "increment the slug when one exists scoped to the same blog" do
|
115
|
+
Post.create!(:title => 'Title', :body => 'Text', :blog_id => 1, :slug => 'title')
|
116
|
+
p = Post.new(:title => 'Title', :body => 'Text', :blog_id => 1)
|
117
|
+
|
118
|
+
p.send(:generate_slug)
|
74
119
|
|
75
|
-
|
120
|
+
p.slug.should == 'title-2'
|
76
121
|
end
|
77
122
|
|
78
123
|
end
|
79
|
-
|
80
|
-
|
124
|
+
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sluggable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Patrick Reagan
|
@@ -47,6 +47,9 @@ files:
|
|
47
47
|
- lib/core_ext/string.rb
|
48
48
|
- lib/sluggable/version.rb
|
49
49
|
- lib/sluggable.rb
|
50
|
+
- test/support/configuration.rb
|
51
|
+
- test/support/database.rb
|
52
|
+
- test/support/test.sqlite3
|
50
53
|
- test/test_helper.rb
|
51
54
|
- test/unit/core_ext/nil_class_test.rb
|
52
55
|
- test/unit/core_ext/string_test.rb
|