friendly_id 1.9.9
Sign up to get free protection for your applications and to get access to all the features.
- data.tar.gz.sig +0 -0
- data/History.txt +81 -0
- data/MIT-LICENSE +19 -0
- data/Manifest.txt +53 -0
- data/README.rdoc +313 -0
- data/Rakefile +43 -0
- data/coverage/index.html +409 -0
- data/coverage/lib-friendly_id-non_sluggable_class_methods_rb.html +646 -0
- data/coverage/lib-friendly_id-non_sluggable_instance_methods_rb.html +638 -0
- data/coverage/lib-friendly_id-shoulda_macros_rb.html +641 -0
- data/coverage/lib-friendly_id-sluggable_class_methods_rb.html +714 -0
- data/coverage/lib-friendly_id-sluggable_instance_methods_rb.html +710 -0
- data/coverage/lib-friendly_id-string_helpers_rb.html +685 -0
- data/coverage/lib-friendly_id_rb.html +665 -0
- data/coverage/lib-slug_rb.html +695 -0
- data/coverage/rails-init_rb.html +606 -0
- data/friendly_id.gemspec +38 -0
- data/generators/friendly_id/friendly_id_generator.rb +12 -0
- data/generators/friendly_id/templates/create_slugs.rb +18 -0
- data/generators/friendly_id_20_upgrade/friendly_id_20_upgrade_generator.rb +11 -0
- data/generators/friendly_id_20_upgrade/templates/upgrade_friendly_id_to_20.rb +19 -0
- data/init.rb +1 -0
- data/lib/friendly_id.rb +61 -0
- data/lib/friendly_id/non_sluggable_class_methods.rb +41 -0
- data/lib/friendly_id/non_sluggable_instance_methods.rb +33 -0
- data/lib/friendly_id/shoulda_macros.rb +36 -0
- data/lib/friendly_id/slug.rb +90 -0
- data/lib/friendly_id/sluggable_class_methods.rb +109 -0
- data/lib/friendly_id/sluggable_instance_methods.rb +105 -0
- data/lib/friendly_id/version.rb +8 -0
- data/lib/tasks/friendly_id.rake +48 -0
- data/lib/tasks/friendly_id.rb +1 -0
- data/test/database.yml +3 -0
- data/test/fixtures/countries.yml +4 -0
- data/test/fixtures/country.rb +4 -0
- data/test/fixtures/people.yml +7 -0
- data/test/fixtures/person.rb +6 -0
- data/test/fixtures/post.rb +3 -0
- data/test/fixtures/posts.yml +19 -0
- data/test/fixtures/slugs.yml +45 -0
- data/test/fixtures/user.rb +3 -0
- data/test/fixtures/users.yml +7 -0
- data/test/non_slugged_test.rb +63 -0
- data/test/rails/2.x/app/controllers/application.rb +0 -0
- data/test/rails/2.x/config/boot.rb +109 -0
- data/test/rails/2.x/config/database.yml +3 -0
- data/test/rails/2.x/config/environment.rb +7 -0
- data/test/rails/2.x/config/environments/test.rb +6 -0
- data/test/rails/2.x/config/routes.rb +0 -0
- data/test/schema.rb +38 -0
- data/test/scoped_model_test.rb +21 -0
- data/test/slug_test.rb +87 -0
- data/test/sluggable_test.rb +181 -0
- data/test/test_helper.rb +35 -0
- metadata +155 -0
- metadata.gz.sig +1 -0
@@ -0,0 +1,105 @@
|
|
1
|
+
module FriendlyId::SluggableInstanceMethods
|
2
|
+
|
3
|
+
NUM_CHARS_RESERVED_FOR_FRIENDLY_ID_EXTENSION = 2
|
4
|
+
|
5
|
+
attr :finder_slug
|
6
|
+
attr_accessor :finder_slug_name
|
7
|
+
|
8
|
+
def finder_slug
|
9
|
+
@finder_slug ||= init_finder_slug
|
10
|
+
end
|
11
|
+
|
12
|
+
# Was the record found using one of its friendly ids?
|
13
|
+
def found_using_friendly_id?
|
14
|
+
finder_slug
|
15
|
+
end
|
16
|
+
|
17
|
+
# Was the record found using its numeric id?
|
18
|
+
def found_using_numeric_id?
|
19
|
+
!found_using_friendly_id?
|
20
|
+
end
|
21
|
+
|
22
|
+
# Was the record found using an old friendly id?
|
23
|
+
def found_using_outdated_friendly_id?
|
24
|
+
finder_slug.id != slug.id
|
25
|
+
end
|
26
|
+
|
27
|
+
# Was the record found using an old friendly id, or its numeric id?
|
28
|
+
def has_better_id?
|
29
|
+
slug and found_using_numeric_id? || found_using_outdated_friendly_id?
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the friendly id.
|
33
|
+
def friendly_id
|
34
|
+
slug(true).to_friendly_id
|
35
|
+
end
|
36
|
+
alias best_id friendly_id
|
37
|
+
|
38
|
+
# Has the basis of our friendly id changed, requiring the generation of a
|
39
|
+
# new slug?
|
40
|
+
def new_slug_needed?
|
41
|
+
!slug || slug_text != slug.name
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns the most recent slug, which is used to determine the friendly
|
45
|
+
# id.
|
46
|
+
def slug(reload = false)
|
47
|
+
@most_recent_slug = nil if reload
|
48
|
+
@most_recent_slug ||= slugs.first
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns the friendly id, or if none is available, the numeric id.
|
52
|
+
def to_param
|
53
|
+
slug ? slug.to_friendly_id : id.to_s
|
54
|
+
end
|
55
|
+
|
56
|
+
# Set the slug using the generated friendly id.
|
57
|
+
def set_slug
|
58
|
+
if self.class.friendly_id_options[:use_slug] && new_slug_needed?
|
59
|
+
@most_recent_slug = nil
|
60
|
+
slug_attributes = {:name => slug_text}
|
61
|
+
if friendly_id_options[:scope]
|
62
|
+
scope = send(friendly_id_options[:scope])
|
63
|
+
slug_attributes[:scope] = scope.respond_to?(:to_param) ? scope.to_param : scope.to_s
|
64
|
+
end
|
65
|
+
# If we're renaming back to a previously used friendly_id, delete the
|
66
|
+
# slug so that we can recycle the name without having to use a sequence.
|
67
|
+
slugs.find(:all, :conditions => {:name => slug_text, :scope => scope}).each { |s| s.destroy }
|
68
|
+
s = slugs.build slug_attributes
|
69
|
+
s.send(:set_sequence)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Get the processed string used as the basis of the friendly id.
|
74
|
+
def slug_text
|
75
|
+
base = send friendly_id_options[:column]
|
76
|
+
if self.friendly_id_options[:strip_diacritics]
|
77
|
+
base = Slug::normalize(Slug::strip_diacritics(base))
|
78
|
+
else
|
79
|
+
base = Slug::normalize(base)
|
80
|
+
end
|
81
|
+
if base.length > friendly_id_options[:max_length]
|
82
|
+
base = base[0...friendly_id_options[:max_length]]
|
83
|
+
end
|
84
|
+
if friendly_id_options[:reserved].include?(base)
|
85
|
+
raise FriendlyId::SlugGenerationError.new("The slug text is a reserved value")
|
86
|
+
end
|
87
|
+
return base
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def finder_slug=(finder_slug)
|
93
|
+
@finder_slug_name = finder_slug.name
|
94
|
+
slug = finder_slug
|
95
|
+
slug.sluggable = self
|
96
|
+
slug
|
97
|
+
end
|
98
|
+
|
99
|
+
def init_finder_slug
|
100
|
+
return false if !@finder_slug_name
|
101
|
+
slug = Slug.find(:first, :conditions => {:sluggable_id => id, :name => @finder_slug_name})
|
102
|
+
finder_slug = slug
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
namespace :friendly_id do
|
2
|
+
desc "Make slugs for a model."
|
3
|
+
task :make_slugs => :environment do
|
4
|
+
raise 'USAGE: rake friendly_id:make_slugs MODEL=MyModelName' if ENV["MODEL"].nil?
|
5
|
+
if !sluggable_class.friendly_id_options[:use_slug]
|
6
|
+
raise "Class \"#{sluggable_class.to_s}\" doesn't appear to be using slugs"
|
7
|
+
end
|
8
|
+
while records = sluggable_class.find(:all, :include => :slugs, :conditions => "slugs.id IS NULL", :limit => 1000) do
|
9
|
+
break if records.size == 0
|
10
|
+
records.each do |r|
|
11
|
+
r.set_slug
|
12
|
+
r.save!
|
13
|
+
puts "#{sluggable_class.to_s}(#{r.id}) friendly_id set to \"#{r.slug.name}\""
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Regenereate slugs for a model."
|
19
|
+
task :redo_slugs => :environment do
|
20
|
+
raise 'USAGE: rake friendly_id:redo_slugs MODEL=MyModelName' if ENV["MODEL"].nil?
|
21
|
+
if !sluggable_class.friendly_id_options[:use_slug]
|
22
|
+
raise "Class \"#{sluggable_class.to_s}\" doesn't appear to be using slugs"
|
23
|
+
end
|
24
|
+
Slug.destroy_all(["sluggable_type = ?", sluggable_class.to_s])
|
25
|
+
Rake::Task["friendly_id:make_slugs"].invoke
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "Kill obsolete slugs older than 45 days."
|
29
|
+
task :remove_old_slugs => :environment do
|
30
|
+
if ENV["DAYS"].nil?
|
31
|
+
@days = 45
|
32
|
+
else
|
33
|
+
@days = ENV["DAYS"].to_i
|
34
|
+
end
|
35
|
+
slugs = Slug.find(:all, :conditions => ["created_at < ?", DateTime.now - @days.days])
|
36
|
+
slugs.each do |s|
|
37
|
+
s.destroy if !s.is_most_recent?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def sluggable_class
|
43
|
+
if (ENV["MODEL"].split('::').size > 1)
|
44
|
+
ENV["MODEL"].split('::').inject(Kernel) {|scope, const_name| scope.const_get(const_name)}
|
45
|
+
else
|
46
|
+
Object.const_get(ENV["MODEL"])
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
load 'tasks/friendly_id.rake'
|
data/test/database.yml
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
without_slug:
|
2
|
+
name: Without a slug
|
3
|
+
content: Content without a slug
|
4
|
+
|
5
|
+
with_one_slug:
|
6
|
+
name: With one slug
|
7
|
+
content: Content with one slug
|
8
|
+
|
9
|
+
with_two_slugs:
|
10
|
+
name: With two slugs
|
11
|
+
content: Content with two slugs
|
12
|
+
|
13
|
+
common_title:
|
14
|
+
name: Common Title
|
15
|
+
content: A post with a very common title
|
16
|
+
|
17
|
+
common_title2:
|
18
|
+
name: Common Title
|
19
|
+
content: A second post with a very common title
|
@@ -0,0 +1,45 @@
|
|
1
|
+
one:
|
2
|
+
name: with-one-slug
|
3
|
+
sluggable: with_one_slug
|
4
|
+
sluggable_type: Post
|
5
|
+
sequence: 1
|
6
|
+
|
7
|
+
two_old:
|
8
|
+
name: with-two-slugs
|
9
|
+
sluggable: with_two_slugs (Post)
|
10
|
+
sequence: 1
|
11
|
+
|
12
|
+
two_new:
|
13
|
+
name: with-two-slugs-new
|
14
|
+
sluggable: with_two_slugs (Post)
|
15
|
+
sequence: 1
|
16
|
+
|
17
|
+
common_title:
|
18
|
+
name: common-title
|
19
|
+
sluggable: common_title (Post)
|
20
|
+
sequence: 1
|
21
|
+
|
22
|
+
common_title2:
|
23
|
+
name: common-title
|
24
|
+
sluggable: common_title2 (Post)
|
25
|
+
sequence: 2
|
26
|
+
|
27
|
+
john_smith:
|
28
|
+
name: john-smith
|
29
|
+
sluggable: john_smith (Person)
|
30
|
+
sequence: 1
|
31
|
+
scope: argentina
|
32
|
+
|
33
|
+
john_smith2:
|
34
|
+
name: john-smith
|
35
|
+
sluggable: john_smith2 (Person)
|
36
|
+
sequence: 1
|
37
|
+
scope: usa
|
38
|
+
|
39
|
+
argentina:
|
40
|
+
name: argentina
|
41
|
+
sluggable: argentina (Country)
|
42
|
+
|
43
|
+
usa:
|
44
|
+
name: usa
|
45
|
+
sluggable: usa (Country)
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class NonSluggedTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
fixtures :users
|
6
|
+
|
7
|
+
def setup
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_should_find_user_using_friendly_id
|
11
|
+
assert User.find(users(:joe).friendly_id)
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_to_param_should_return_a_string
|
15
|
+
assert_equal String, users(:joe).to_param.class
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_should_find_users_using_friendly_ids
|
19
|
+
assert_equal 2, User.find([users(:joe).friendly_id, users(:jane).friendly_id]).length
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_should_not_find_users_using_non_existent_friendly_ids
|
23
|
+
assert_raises ActiveRecord::RecordNotFound do
|
24
|
+
User.find(['non-existent-slug', 'yet-another-non-existent-slug'])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_finder_options_are_not_ignored
|
29
|
+
assert_raises ActiveRecord::RecordNotFound do
|
30
|
+
User.find(users(:joe).friendly_id, :conditions => "1 = 2")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_user_should_have_friendly_id_options
|
35
|
+
assert_not_nil User.friendly_id_options
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_user_should_not_be_found_using_friendly_id_unless_it_really_was
|
39
|
+
@user = User.new
|
40
|
+
assert !@user.found_using_friendly_id?
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_user_should_be_considered_found_by_numeric_id_as_default
|
44
|
+
@user = User.new
|
45
|
+
assert @user.found_using_numeric_id?
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_user_should_indicate_if_it_was_found_using_numeric_id
|
49
|
+
@user = User.find(users(:joe).id)
|
50
|
+
assert @user.found_using_numeric_id?
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_user_should_indicate_if_it_was_found_using_friendly_id
|
54
|
+
@user = User.find(users(:joe).friendly_id)
|
55
|
+
assert @user.found_using_friendly_id?
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_should_indicate_there_is_a_better_id_if_found_by_numeric_id
|
59
|
+
@user = User.find(users(:joe).id)
|
60
|
+
assert @user.has_better_id?
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
File without changes
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# Don't change this file!
|
2
|
+
# Configure your app in config/environment.rb and config/environments/*.rb
|
3
|
+
|
4
|
+
RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT)
|
5
|
+
|
6
|
+
module Rails
|
7
|
+
class << self
|
8
|
+
def boot!
|
9
|
+
unless booted?
|
10
|
+
preinitialize
|
11
|
+
pick_boot.run
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def booted?
|
16
|
+
defined? Rails::Initializer
|
17
|
+
end
|
18
|
+
|
19
|
+
def pick_boot
|
20
|
+
(vendor_rails? ? VendorBoot : GemBoot).new
|
21
|
+
end
|
22
|
+
|
23
|
+
def vendor_rails?
|
24
|
+
File.exist?("#{RAILS_ROOT}/vendor/rails")
|
25
|
+
end
|
26
|
+
|
27
|
+
def preinitialize
|
28
|
+
load(preinitializer_path) if File.exist?(preinitializer_path)
|
29
|
+
end
|
30
|
+
|
31
|
+
def preinitializer_path
|
32
|
+
"#{RAILS_ROOT}/config/preinitializer.rb"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Boot
|
37
|
+
def run
|
38
|
+
load_initializer
|
39
|
+
Rails::Initializer.run(:set_load_path)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class VendorBoot < Boot
|
44
|
+
def load_initializer
|
45
|
+
require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
|
46
|
+
Rails::Initializer.run(:install_gem_spec_stubs)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class GemBoot < Boot
|
51
|
+
def load_initializer
|
52
|
+
self.class.load_rubygems
|
53
|
+
load_rails_gem
|
54
|
+
require 'initializer'
|
55
|
+
end
|
56
|
+
|
57
|
+
def load_rails_gem
|
58
|
+
if version = self.class.gem_version
|
59
|
+
gem 'rails', version
|
60
|
+
else
|
61
|
+
gem 'rails'
|
62
|
+
end
|
63
|
+
rescue Gem::LoadError => load_error
|
64
|
+
$stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.)
|
65
|
+
exit 1
|
66
|
+
end
|
67
|
+
|
68
|
+
class << self
|
69
|
+
def rubygems_version
|
70
|
+
Gem::RubyGemsVersion rescue nil
|
71
|
+
end
|
72
|
+
|
73
|
+
def gem_version
|
74
|
+
if defined? RAILS_GEM_VERSION
|
75
|
+
RAILS_GEM_VERSION
|
76
|
+
elsif ENV.include?('RAILS_GEM_VERSION')
|
77
|
+
ENV['RAILS_GEM_VERSION']
|
78
|
+
else
|
79
|
+
parse_gem_version(read_environment_rb)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def load_rubygems
|
84
|
+
require 'rubygems'
|
85
|
+
min_version = '1.3.1'
|
86
|
+
unless rubygems_version >= min_version
|
87
|
+
$stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.)
|
88
|
+
exit 1
|
89
|
+
end
|
90
|
+
|
91
|
+
rescue LoadError
|
92
|
+
$stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org)
|
93
|
+
exit 1
|
94
|
+
end
|
95
|
+
|
96
|
+
def parse_gem_version(text)
|
97
|
+
$1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
def read_environment_rb
|
102
|
+
File.read("#{RAILS_ROOT}/config/environment.rb")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# All that for this:
|
109
|
+
Rails.boot!
|