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.
@@ -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