nateabbott-friendly_id 2.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/History.txt +133 -0
  2. data/MIT-LICENSE +19 -0
  3. data/Manifest.txt +39 -0
  4. data/README.rdoc +343 -0
  5. data/Rakefile +49 -0
  6. data/config/website.yml +2 -0
  7. data/friendly_id.gemspec +45 -0
  8. data/generators/friendly_id/friendly_id_generator.rb +12 -0
  9. data/generators/friendly_id/templates/create_slugs.rb +18 -0
  10. data/generators/friendly_id_20_upgrade/friendly_id_20_upgrade_generator.rb +11 -0
  11. data/generators/friendly_id_20_upgrade/templates/upgrade_friendly_id_to_20.rb +19 -0
  12. data/init.rb +3 -0
  13. data/lib/friendly_id/helpers.rb +15 -0
  14. data/lib/friendly_id/non_sluggable_class_methods.rb +42 -0
  15. data/lib/friendly_id/non_sluggable_instance_methods.rb +43 -0
  16. data/lib/friendly_id/slug.rb +102 -0
  17. data/lib/friendly_id/sluggable_class_methods.rb +116 -0
  18. data/lib/friendly_id/sluggable_instance_methods.rb +116 -0
  19. data/lib/friendly_id/version.rb +10 -0
  20. data/lib/friendly_id.rb +101 -0
  21. data/lib/tasks/friendly_id.rake +50 -0
  22. data/lib/tasks/friendly_id.rb +1 -0
  23. data/test/contest.rb +94 -0
  24. data/test/custom_slug_normalizer_test.rb +35 -0
  25. data/test/models/book.rb +2 -0
  26. data/test/models/country.rb +4 -0
  27. data/test/models/event.rb +3 -0
  28. data/test/models/novel.rb +3 -0
  29. data/test/models/person.rb +6 -0
  30. data/test/models/post.rb +6 -0
  31. data/test/models/thing.rb +6 -0
  32. data/test/models/user.rb +3 -0
  33. data/test/non_slugged_test.rb +98 -0
  34. data/test/schema.rb +55 -0
  35. data/test/scoped_model_test.rb +53 -0
  36. data/test/slug_test.rb +106 -0
  37. data/test/slugged_model_test.rb +284 -0
  38. data/test/sti_test.rb +48 -0
  39. data/test/test_helper.rb +30 -0
  40. metadata +155 -0
data/test/contest.rb ADDED
@@ -0,0 +1,94 @@
1
+ # License
2
+ # -------
3
+ #
4
+ # Contest is copyright (c) 2009 Damian Janowski and Michel Martens for
5
+ # Citrusbyte
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person
8
+ # obtaining a copy of this software and associated documentation
9
+ # files (the "Software"), to deal in the Software without
10
+ # restriction, including without limitation the rights to use,
11
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ # copies of the Software, and to permit persons to whom the
13
+ # Software is furnished to do so, subject to the following
14
+ # conditions:
15
+ #
16
+ # The above copyright notice and this permission notice shall be
17
+ # included in all copies or substantial portions of the Software.
18
+ #
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26
+ # OTHER DEALINGS IN THE SOFTWARE.
27
+ require "test/unit"
28
+
29
+ # Test::Unit loads a default test if the suite is empty, whose purpose is to
30
+ # fail. Since having empty contexts is a common practice, we decided to
31
+ # overwrite TestSuite#empty? in order to allow them. Having a failure when no
32
+ # tests have been defined seems counter-intuitive.
33
+ class Test::Unit::TestSuite
34
+ def empty?
35
+ false
36
+ end
37
+ end
38
+
39
+ # Contest adds +teardown+, +test+ and +context+ as class methods, and the
40
+ # instance methods +setup+ and +teardown+ now iterate on the corresponding
41
+ # blocks. Note that all setup and teardown blocks must be defined with the
42
+ # block syntax. Adding setup or teardown instance methods defeats the purpose
43
+ # of this library.
44
+ class Test::Unit::TestCase
45
+ def self.setup(&block)
46
+ define_method :setup do
47
+ super(&block)
48
+ instance_eval(&block)
49
+ end
50
+ end
51
+
52
+ def self.teardown(&block)
53
+ define_method :teardown do
54
+ instance_eval(&block)
55
+ super(&block)
56
+ end
57
+ end
58
+
59
+ def self.context(name, &block)
60
+ subclass = Class.new(self)
61
+ remove_tests(subclass)
62
+ subclass.class_eval(&block)
63
+ const_set(context_name(name), subclass)
64
+ end
65
+
66
+ def self.test(name, &block)
67
+ define_method(test_name(name), &block)
68
+ end
69
+
70
+ class << self
71
+ alias_method :should, :test
72
+ alias_method :describe, :context
73
+ end
74
+
75
+ private
76
+
77
+ def self.context_name(name)
78
+ "Test#{sanitize_name(name).gsub(/(^| )(\w)/) { $2.upcase }}".to_sym
79
+ end
80
+
81
+ def self.test_name(name)
82
+ "test_#{sanitize_name(name).gsub(/\s+/,'_')}".to_sym
83
+ end
84
+
85
+ def self.sanitize_name(name)
86
+ name.gsub(/\W+/, ' ').strip
87
+ end
88
+
89
+ def self.remove_tests(subclass)
90
+ subclass.public_instance_methods.grep(/^test_/).each do |meth|
91
+ subclass.send(:undef_method, meth.to_sym)
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,35 @@
1
+ # encoding: utf-8
2
+
3
+ require File.dirname(__FILE__) + '/test_helper'
4
+
5
+ class CustomSlugNormalizerTest < Test::Unit::TestCase
6
+
7
+ context "A slugged model using a custom slug generator" do
8
+
9
+ setup do
10
+ Thing.friendly_id_options = FriendlyId::DEFAULT_FRIENDLY_ID_OPTIONS.merge(:column => :name, :use_slug => true)
11
+ Thing.delete_all
12
+ Slug.delete_all
13
+ end
14
+
15
+ should "invoke the block code" do
16
+ @thing = Thing.create!(:name => "test")
17
+ assert_equal "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", @thing.friendly_id
18
+ end
19
+
20
+ should "respect the max_length option" do
21
+ Thing.friendly_id_options = Thing.friendly_id_options.merge(:max_length => 10)
22
+ @thing = Thing.create!(:name => "test")
23
+ assert_equal "a94a8fe5cc", @thing.friendly_id
24
+ end
25
+
26
+ should "respect the reserved option" do
27
+ Thing.friendly_id_options = Thing.friendly_id_options.merge(:reserved => ["a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"])
28
+ assert_raises FriendlyId::SlugGenerationError do
29
+ Thing.create!(:name => "test")
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+ end
@@ -0,0 +1,2 @@
1
+ class Book < ActiveRecord::Base
2
+ end
@@ -0,0 +1,4 @@
1
+ class Country < ActiveRecord::Base
2
+ has_many :people
3
+ has_friendly_id :name, :use_slug => true
4
+ end
@@ -0,0 +1,3 @@
1
+ class Event < ActiveRecord::Base
2
+ has_friendly_id :event_date, :use_slug => true
3
+ end
@@ -0,0 +1,3 @@
1
+ class Novel < Book
2
+ has_friendly_id :title, :use_slug => true
3
+ end
@@ -0,0 +1,6 @@
1
+ class Person < ActiveRecord::Base
2
+
3
+ belongs_to :country
4
+ has_friendly_id :name, :use_slug => true, :scope => :country
5
+
6
+ end
@@ -0,0 +1,6 @@
1
+ class Post < ActiveRecord::Base
2
+ has_friendly_id :title, :use_slug => true
3
+
4
+ named_scope :published, :conditions => { :published => true }
5
+
6
+ end
@@ -0,0 +1,6 @@
1
+ require 'digest/sha1'
2
+ class Thing < ActiveRecord::Base
3
+ has_friendly_id :name, :use_slug => true do |text|
4
+ Digest::SHA1::hexdigest(text)
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ class User < ActiveRecord::Base
2
+ has_friendly_id :login
3
+ end
@@ -0,0 +1,98 @@
1
+ # encoding: utf-8
2
+
3
+ require File.dirname(__FILE__) + '/test_helper'
4
+
5
+ class NonSluggedTest < Test::Unit::TestCase
6
+
7
+ context "A non-slugged model with default FriendlyId options" do
8
+
9
+ setup do
10
+ User.delete_all
11
+ @user = User.create!(:login => "joe", :email => "joe@example.org")
12
+ end
13
+
14
+ should "have friendly_id options" do
15
+ assert_not_nil User.friendly_id_options
16
+ end
17
+
18
+ should "not have a slug" do
19
+ assert !@user.respond_to?(:slug)
20
+ end
21
+
22
+ should "be findable by its friendly_id" do
23
+ assert User.find(@user.friendly_id)
24
+ end
25
+
26
+ should "be findable by its regular id" do
27
+ assert User.find(@user.id)
28
+ end
29
+
30
+ should "respect finder conditions" do
31
+ assert_raises ActiveRecord::RecordNotFound do
32
+ User.find(@user.friendly_id, :conditions => "1 = 2")
33
+ end
34
+ end
35
+
36
+ should "indicate if it was found by its friendly id" do
37
+ @user = User.find(@user.friendly_id)
38
+ assert @user.found_using_friendly_id?
39
+ end
40
+
41
+ should "indicate if it was found by its numeric id" do
42
+ @user = User.find(@user.id)
43
+ assert @user.found_using_numeric_id?
44
+ end
45
+
46
+ should "indicate if it has a better id" do
47
+ @user = User.find(@user.id)
48
+ assert @user.has_better_id?
49
+ end
50
+
51
+ should "not validate if the friendly_id text is reserved" do
52
+ @user = User.new(:login => "new", :email => "test@example.org")
53
+ assert !@user.valid?
54
+ end
55
+
56
+ should "have always string for a friendly_id" do
57
+ assert_equal String, @user.to_param.class
58
+ end
59
+
60
+ should "return its id if the friendly_id is null" do
61
+ @user.login = nil
62
+ assert_equal @user.id.to_s, @user.to_param
63
+ end
64
+
65
+
66
+ context "when using an array as the find argument" do
67
+
68
+ setup do
69
+ @user2 = User.create(:login => "jane", :email => "jane@example.org")
70
+ end
71
+
72
+ should "return results" do
73
+ assert_equal 2, User.find([@user.friendly_id, @user2.friendly_id]).size
74
+ end
75
+
76
+ should "not allow mixed friendly and non-friendly ids for the same record" do
77
+ assert_raises ActiveRecord::RecordNotFound do
78
+ User.find([@user.id, @user.friendly_id]).size
79
+ end
80
+ end
81
+
82
+ should "raise an error when all records are not found" do
83
+ assert_raises ActiveRecord::RecordNotFound do
84
+ User.find(['bad', 'bad2'])
85
+ end
86
+ end
87
+
88
+ should "indicate if the results were found using a friendly_id" do
89
+ @users = User.find([@user.id, @user2.friendly_id], :order => "login ASC")
90
+ assert @users[0].found_using_friendly_id?
91
+ assert @users[1].found_using_numeric_id?
92
+ end
93
+
94
+ end
95
+
96
+ end
97
+
98
+ end
data/test/schema.rb ADDED
@@ -0,0 +1,55 @@
1
+ # encoding: utf-8
2
+
3
+ ActiveRecord::Schema.define(:version => 1) do
4
+
5
+ create_table "books", :force => true do |t|
6
+ t.column "title", "string"
7
+ t.column "type", "text"
8
+ end
9
+
10
+ create_table "things", :force => true do |t|
11
+ t.column "name", "string"
12
+ end
13
+
14
+ create_table "posts", :force => true do |t|
15
+ t.column "title", "string"
16
+ t.column "content", "text"
17
+ t.column "published", "boolean", :default => false
18
+ t.column "created_at", "datetime"
19
+ t.column "updated_at", "datetime"
20
+ end
21
+
22
+ create_table "users", :force => true do |t|
23
+ t.column "login", "string"
24
+ t.column "email", "string"
25
+ t.column "created_at", "datetime"
26
+ t.column "updated_at", "datetime"
27
+ end
28
+
29
+ create_table "people", :force => true do |t|
30
+ t.column "name", "string"
31
+ t.column "country_id", "integer"
32
+ end
33
+
34
+ create_table "countries", :force => true do |t|
35
+ t.column "name", "string"
36
+ end
37
+
38
+ create_table "events", :force => true do |t|
39
+ t.column "name", "string"
40
+ t.column "event_date", "datetime"
41
+ end
42
+
43
+ create_table "slugs", :force => true do |t|
44
+ t.column "name", "string"
45
+ t.column "sluggable_id", "integer"
46
+ t.column "sequence", "integer", :null => false, :default => 1
47
+ t.column "sluggable_type", "string", :limit => 40
48
+ t.column "scope", "string", :limit => 40
49
+ t.column "created_at", "datetime"
50
+ end
51
+
52
+ add_index "slugs", ["sluggable_id"], :name => "index_slugs_on_sluggable_id"
53
+ add_index "slugs", ["name", "sluggable_type", "scope", "sequence"], :name => "index_slugs_on_n_s_s_and_s", :unique => true
54
+
55
+ end
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+
3
+ require File.dirname(__FILE__) + '/test_helper'
4
+
5
+ class ScopedModelTest < Test::Unit::TestCase
6
+
7
+ context "A slugged model that uses a scope" do
8
+
9
+ setup do
10
+ Person.delete_all
11
+ Country.delete_all
12
+ Slug.delete_all
13
+ @usa = Country.create!(:name => "USA")
14
+ @canada = Country.create!(:name => "Canada")
15
+ @person = Person.create!(:name => "John Smith", :country => @usa)
16
+ @person2 = Person.create!(:name => "John Smith", :country => @canada)
17
+ end
18
+
19
+ should "find all scoped records without scope" do
20
+ assert_equal 2, Person.find(:all, @person.friendly_id).size
21
+ end
22
+
23
+ should "find a single scoped records with a scope" do
24
+ assert Person.find(@person.friendly_id, :scope => @person.country.to_param)
25
+ end
26
+
27
+ should "raise an error when finding a single scoped record with no scope" do
28
+ assert_raises ActiveRecord::RecordNotFound do
29
+ Person.find(@person.friendly_id)
30
+ end
31
+ end
32
+
33
+ should "append scope error info when missing scope causes a find to fail" do
34
+ begin
35
+ Person.find(@person.friendly_id)
36
+ fail "The find should not have succeeded"
37
+ rescue ActiveRecord::RecordNotFound => e
38
+ assert_match /expected scope/, e.message
39
+ end
40
+ end
41
+
42
+ should "append scope error info when the scope value causes a find to fail" do
43
+ begin
44
+ Person.find(@person.friendly_id, :scope => "badscope")
45
+ fail "The find should not have succeeded"
46
+ rescue ActiveRecord::RecordNotFound => e
47
+ assert_match /scope=badscope/, e.message
48
+ end
49
+ end
50
+
51
+ end
52
+
53
+ end
data/test/slug_test.rb ADDED
@@ -0,0 +1,106 @@
1
+ # encoding: utf-8
2
+
3
+ require File.dirname(__FILE__) + '/test_helper'
4
+
5
+ class SlugTest < Test::Unit::TestCase
6
+
7
+ context "a slug" do
8
+
9
+ setup do
10
+ Slug.delete_all
11
+ Post.delete_all
12
+ end
13
+
14
+ should "indicate if it is the most recent slug" do
15
+ @post = Post.create!(:title => "test title", :content => "test content")
16
+ @post.title = "a new title"
17
+ @post.save!
18
+ assert @post.slugs.last.is_most_recent?
19
+ assert !@post.slugs.first.is_most_recent?
20
+ end
21
+
22
+ end
23
+
24
+ context "the Slug class" do
25
+
26
+ should "parse the slug name and sequence" do
27
+ assert_equal ["test", "2"], Slug::parse("test--2")
28
+ end
29
+
30
+ should "parse with a default sequence of 1" do
31
+ assert_equal ["test", "1"], Slug::parse("test")
32
+ end
33
+
34
+ should "should strip diacritics" do
35
+ assert_equal "acai", Slug::strip_diacritics("açaí")
36
+ end
37
+
38
+ should "strip diacritics correctly " do
39
+ input = "ÀÁÂÃÄÅÆÇÈÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ"
40
+ output = Slug::strip_diacritics(input).split(//)
41
+ expected = "AAAAAAAECEEEIIIIDNOOOOOOUUUUYThssaaaaaaaeceeeeiiiidnoooooouuuuythy".split(//)
42
+ output.each_index do |i|
43
+ assert_equal expected[i], output[i]
44
+ end
45
+ end
46
+
47
+ end
48
+
49
+ context "the Slug class's to_friendly_id method" do
50
+
51
+ should "include the sequence if the sequence is greater than 1" do
52
+ slug = Slug.new(:name => "test", :sequence => 2)
53
+ assert_equal "test--2", slug.to_friendly_id
54
+ end
55
+
56
+ should "not include the sequence if the sequence is 1" do
57
+ slug = Slug.new(:name => "test", :sequence => 1)
58
+ assert_equal "test", slug.to_friendly_id
59
+ end
60
+
61
+ end
62
+
63
+ context "the Slug class's normalize method" do
64
+
65
+ should "should lowercase strings" do
66
+ assert_match /abc/, Slug::normalize("ABC")
67
+ end
68
+
69
+ should "should replace whitespace with dashes" do
70
+ assert_match /a-b/, Slug::normalize("a b")
71
+ end
72
+
73
+ should "should replace 2spaces with 1dash" do
74
+ assert_match /a-b/, Slug::normalize("a b")
75
+ end
76
+
77
+ should "should remove punctuation" do
78
+ assert_match /abc/, Slug::normalize('abc!@#$%^&*•¶§∞¢££¡¿()><?"":;][]\.,/')
79
+ end
80
+
81
+ should "should strip trailing space" do
82
+ assert_match /ab/, Slug::normalize("ab ")
83
+ end
84
+
85
+ should "should strip leading space" do
86
+ assert_match /ab/, Slug::normalize(" ab")
87
+ end
88
+
89
+ should "should strip trailing slashes" do
90
+ assert_match /ab/, Slug::normalize("ab-")
91
+ end
92
+
93
+ should "should strip leading slashes" do
94
+ assert_match /ab/, Slug::normalize("-ab")
95
+ end
96
+
97
+ should "should not modify valid name strings" do
98
+ assert_match /a-b-c-d/, Slug::normalize("a-b-c-d")
99
+ end
100
+
101
+ should "work with non roman chars" do
102
+ assert_equal "検-索", Slug::normalize("検 索")
103
+ end
104
+
105
+ end
106
+ end