sluggable 0.1.0 → 0.1.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/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
|