sluggable 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 Post < ActiveRecord::Base
21
+ class Blog < ActiveRecord::Base
22
22
  # Columns:
23
- # title: string
24
- # body: text
25
- # slug: string
23
+ # name: string
24
+ # slug: string
26
25
 
27
26
  include Sluggable
28
- slug_column :title
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 `title` column is used as 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
- Rake::TestTask.new do |t|
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
@@ -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 slug_column(column)
18
- define_method(:slug_column) do
19
- column
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.others_by_slug(self.id, valid_slug)
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(slug_column).sluggify)
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
@@ -3,7 +3,7 @@ module Sluggable
3
3
 
4
4
  MAJOR = 0
5
5
  MINOR = 1
6
- TINY = 0
6
+ TINY = 1
7
7
 
8
8
  def self.to_s # :nodoc:
9
9
  [MAJOR, MINOR, TINY].join('.')
@@ -0,0 +1,7 @@
1
+ class Configuration
2
+
3
+ def self.database(path)
4
+ {:adapter => 'sqlite3', :database => "#{path}/test.sqlite3", :timeout => 5000}
5
+ end
6
+
7
+ end
@@ -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
@@ -4,4 +4,12 @@ $:.reject! { |e| e.include? 'TextMate' }
4
4
  require 'rubygems'
5
5
  require 'throat_punch'
6
6
 
7
- require File.dirname(__FILE__) + '/../lib/sluggable'
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
+
@@ -1,80 +1,129 @@
1
1
  require File.dirname(__FILE__) + '/../test_helper'
2
2
 
3
- class SluggableImplementation
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
- def stub_others_by_slug(id, *slugs)
10
- slugs.each do |slug|
11
- return_val = (slug == slugs.last) ? nil : stub()
12
- SluggableImplementation.stubs(:others_by_slug).with(@id, slug).returns(return_val)
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
- end
15
-
16
- context "The SluggableImplmentation class" do
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 "be able to find others by slug when provided an ID" do
23
- id = 1
24
- SluggableImplementation.expects(:find_by_slug).with(@slug, {:conditions => ['id != ?', id]}).returns(@object)
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 "be able to find others by slug when not provided an ID" do
29
- SluggableImplementation.expects(:find_by_slug).with(@slug, {}).returns(@object)
30
- assert_equal @object, SluggableImplementation.others_by_slug(nil, @slug)
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 "be able to record the column for use when generating the slug" do
34
- SluggableImplementation.slug_column :title
35
- @sluggable = SluggableImplementation.new
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
- assert_equal :title, @sluggable.slug_column
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
- context "An instance of SluggableImplementation" do
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 "know the next available slug when the original is taken" do
53
- stub_others_by_slug(@id, 'title', 'title-2')
54
- assert_equal "title-2", @sluggable.send(:next_available_slug, 'title')
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 "incrementally suggest slugs until it finds an available one" do
58
- stub_others_by_slug(@id, 'title', 'title-2', 'title-3')
59
- assert_equal 'title-3', @sluggable.send(:next_available_slug, 'title')
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 "know not to suggest an incremental slug when the existing slug belongs to the current record" do
63
- SluggableImplementation.stubs(:id).with().returns(@id)
64
- SluggableImplementation.stubs(:others_by_slug).with(@id, @slug).returns(nil)
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
- assert_equal @slug, @sluggable.send(:next_available_slug, @slug)
109
+ p.send(:generate_slug)
110
+
111
+ p.slug.should == 'title'
67
112
  end
68
113
 
69
- should "be able to assign a valid slug to the slug property" do
70
- @sluggable.stubs(:slug_column).with().returns(:title)
71
- @sluggable.stubs(:title).with().returns(stub(:sluggify => @slug))
72
- @sluggable.stubs(:next_available_slug).with(@slug).returns(@slug)
73
- @sluggable.expects(:slug=).with(@slug)
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
- @sluggable.send(:generate_slug)
120
+ p.slug.should == 'title-2'
76
121
  end
77
122
 
78
123
  end
79
-
80
- end
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.0
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