friendly_id4 4.0.0.beta6 → 4.0.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +47 -68
- data/Rakefile +13 -108
- data/lib/friendly_id.rb +97 -117
- data/lib/friendly_id/scoped.rb +17 -116
- data/lib/friendly_id/slugged.rb +89 -183
- data/lib/friendly_id/test.rb +23 -0
- data/lib/friendly_id/test/generic.rb +84 -0
- data/lib/friendly_id/version.rb +9 -0
- data/test/core_test.rb +54 -16
- data/test/scoped_test.rb +39 -35
- data/test/slugged_test.rb +45 -64
- data/test/test_helper.rb +23 -0
- metadata +61 -125
- data/.gemtest +0 -0
- data/.gitignore +0 -11
- data/.yardopts +0 -4
- data/WhatsNew.md +0 -142
- data/bench.rb +0 -63
- data/friendly_id.gemspec +0 -31
- data/lib/friendly_id/base.rb +0 -134
- data/lib/friendly_id/configuration.rb +0 -78
- data/lib/friendly_id/finder_methods.rb +0 -20
- data/lib/friendly_id/history.rb +0 -64
- data/lib/friendly_id/migration.rb +0 -18
- data/lib/friendly_id/model.rb +0 -22
- data/lib/friendly_id/object_utils.rb +0 -40
- data/lib/friendly_id/reserved.rb +0 -46
- data/lib/friendly_id/slug.rb +0 -6
- data/lib/friendly_id/slug_sequencer.rb +0 -82
- data/lib/generators/friendly_id_generator.rb +0 -24
- data/test/base_test.rb +0 -54
- data/test/config/mysql.yml +0 -5
- data/test/config/mysql2.yml +0 -5
- data/test/config/postgres.yml +0 -6
- data/test/config/sqlite3.yml +0 -3
- data/test/configuration_test.rb +0 -27
- data/test/helper.rb +0 -90
- data/test/history_test.rb +0 -55
- data/test/object_utils_test.rb +0 -26
- data/test/reserved_test.rb +0 -26
- data/test/schema.rb +0 -56
- data/test/shared.rb +0 -118
- data/test/sti_test.rb +0 -48
data/README.md
CHANGED
@@ -1,95 +1,74 @@
|
|
1
|
-
# FriendlyId
|
1
|
+
# FriendlyId 4
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
This is an in-progress rethink of the FriendlyId plugin. It will probably be
|
4
|
+
released some time in August or September 2011, once I've had the chance to
|
5
|
+
actually use it in a real website for a while.
|
6
6
|
|
7
|
-
|
7
|
+
Please don't use this yet for anything real but feel free to try it and give
|
8
|
+
your feedback via the issues tracker.
|
8
9
|
|
9
|
-
|
10
|
+
## Back to basics
|
10
11
|
|
11
|
-
|
12
|
+
This isn't the "big rewrite," it's the "small rewrite."
|
12
13
|
|
13
|
-
|
14
|
+
Adding new features with each release is not sustainable. This release *removes*
|
15
|
+
features, but makes it possible to add them back as addons. We can also remove
|
16
|
+
some complexity by relying on the better default functionality provided by newer
|
17
|
+
versions of Active Support and Active Record.
|
14
18
|
|
19
|
+
Let's see how small we can make this!
|
15
20
|
|
16
|
-
|
21
|
+
Here's what's changed:
|
17
22
|
|
18
|
-
|
19
|
-
versioning, scoped slugs, reserved words, and custom slug generators.
|
23
|
+
## Active Record 3+ only
|
20
24
|
|
21
|
-
|
25
|
+
For 2.3 support, you can use FriendlyId 3, which will continue to be maintained
|
26
|
+
until people don't want it any more.
|
22
27
|
|
23
|
-
##
|
28
|
+
## Remove Babosa
|
24
29
|
|
25
|
-
|
30
|
+
Babosa is FriendlyId 3's slugging library.
|
26
31
|
|
27
|
-
|
32
|
+
FriendlyId 4 doesn't use it by default any more because the most important
|
33
|
+
pieces of it were already accepted into Active Support 3. You can still just
|
34
|
+
override `#normalize_friendly_id` in your model if you want to use Babosa.
|
28
35
|
|
29
|
-
|
36
|
+
## In-table slugs
|
30
37
|
|
31
|
-
|
32
|
-
|
33
|
-
|
38
|
+
FriendlyId no longer creates a separate slugs table - it just stores the
|
39
|
+
generated slug value in the model table, which is simpler, faster and what most
|
40
|
+
people seem to want. Keeping slugs in a separate table is an optional add-on for
|
41
|
+
FriendlyId 4 (not implemented yet).
|
34
42
|
|
43
|
+
## No more finder status
|
35
44
|
|
36
|
-
|
45
|
+
FriendlyId 3 offered finder statuses to help you determine when an outdated
|
46
|
+
or non-friendly id was used to find the record, so that you could decide whether
|
47
|
+
to permanently redirect to the canonical URL. However, there's a simpler way to
|
48
|
+
do that, so this feature has been removed:
|
37
49
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
rake db:migrate
|
42
|
-
|
43
|
-
# edit app/models/user.rb
|
44
|
-
class User < ActiveRecord::Base
|
45
|
-
extend FriendlyId
|
46
|
-
friendly_id :name, :use => :slugged
|
50
|
+
if request.path != person_path(@person)
|
51
|
+
return redirect_to @person, :status => :moved_permanently
|
47
52
|
end
|
48
53
|
|
49
|
-
|
50
|
-
|
51
|
-
rails server
|
52
|
-
|
53
|
-
GET http://localhost:3000/users/joe-schmoe
|
54
|
-
|
55
|
-
## Docs
|
56
|
-
|
57
|
-
The current docs can be found
|
58
|
-
[here](http://rdoc.info/github/norman/friendly_id/a4128af31d85ee29ad8f/frames).
|
59
|
-
|
60
|
-
## Future Compatibility
|
61
|
-
|
62
|
-
FriendlyId will always remain compatible with the current release of Rails, and
|
63
|
-
at least one stable release behind. That means that support for 3.0.x will not be
|
64
|
-
dropped until a stable release of 3.2 is out, or possibly longer.
|
65
|
-
|
66
|
-
|
67
|
-
## Benchmarks
|
68
|
-
|
69
|
-
The latest benchmarks for FriendlyId are maintained
|
70
|
-
[here](https://gist.github.com/1129745).
|
71
|
-
|
54
|
+
## No more multiple finds
|
72
55
|
|
73
|
-
|
56
|
+
Person.find "joe-schmoe" # Supported
|
57
|
+
Person.find ["joe-schmoe", "john-doe"] # No longer supported
|
74
58
|
|
75
|
-
|
76
|
-
tracker](http://github.com/norman/friendly_id/issues) for this project.
|
59
|
+
If you want find by more than one friendly id, build your own query:
|
77
60
|
|
78
|
-
|
61
|
+
Person.where(:slug => ["joe-schmoe", "john-doe"])
|
79
62
|
|
80
|
-
|
81
|
-
* Stack trace and error message.
|
82
|
-
* Any snippets of relevant model, view or controller code that shows how you
|
83
|
-
are using FriendlyId.
|
63
|
+
This lets us do *far* less monkeypatching in Active Record.
|
84
64
|
|
85
|
-
|
86
|
-
and add a test that reproduces the error you are experiencing.
|
65
|
+
## No more reserved words
|
87
66
|
|
88
|
-
|
67
|
+
Rather than use a custom reserved words validator, use the validations provided
|
68
|
+
by Active Record. FriendlyId still reserves "new" and "edit" by default to avoid
|
69
|
+
routing problems.
|
89
70
|
|
90
|
-
|
91
|
-
significant help early in its life by Emilio Tagua. I'm deeply gratful for the
|
92
|
-
generous contributions over the years from [many
|
93
|
-
volunteers](https://github.com/norman/friendly_id/contributors).
|
71
|
+
validates_exclusion_of :name, :in => ["bad", "word"]
|
94
72
|
|
95
|
-
|
73
|
+
You can configure the default words reserved by FriendlyId in
|
74
|
+
`FriendlyId::Configuration::DEFAULTS[:reserved_words]`.
|
data/Rakefile
CHANGED
@@ -1,119 +1,24 @@
|
|
1
|
-
require "
|
1
|
+
require "rake"
|
2
2
|
require "rake/testtask"
|
3
|
-
|
4
|
-
|
5
|
-
["ruby-1.9.2-p180", "ree-1.8.7-2011.03", "jruby-1.6.2", "rbx-2.0.0pre"].each do |ruby|
|
6
|
-
old = ENV["RB"]
|
7
|
-
ENV["RB"] = ruby
|
8
|
-
yield
|
9
|
-
ENV["RB"] = old
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
def versions(&block)
|
14
|
-
["3.1.0.rc5", "3.0.9"].each do |version|
|
15
|
-
old = ENV["AR"]
|
16
|
-
ENV["AR"] = version
|
17
|
-
yield
|
18
|
-
ENV["AR"] = old
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def adapters(&block)
|
23
|
-
["mysql", "mysql2", "postgres", "sqlite3"].each do |adapter|
|
24
|
-
old = ENV["DB"]
|
25
|
-
ENV["DB"] = adapter
|
26
|
-
ENV["DB_VERSION"] = "~> 0.3.6" if adapter == "mysql2" && ENV["AR"] && ENV["AR"][0..2] >= "3.1"
|
27
|
-
yield
|
28
|
-
ENV["DB"] = old
|
29
|
-
end
|
30
|
-
end
|
3
|
+
require "rake/gempackagetask"
|
4
|
+
require "rake/clean"
|
31
5
|
|
32
6
|
task :default => :test
|
33
7
|
|
34
|
-
|
35
|
-
t.test_files = FileList['test/*_test.rb']
|
36
|
-
t.verbose = true
|
37
|
-
end
|
38
|
-
|
39
|
-
task :clean do
|
40
|
-
%x{rm -rf *.gem doc pkg coverage}
|
41
|
-
%x{rm -f `find . -name '*.rbc'`}
|
42
|
-
end
|
43
|
-
|
44
|
-
task :gem do
|
45
|
-
%x{gem build friendly_id.gemspec}
|
46
|
-
end
|
47
|
-
|
48
|
-
task :yard do
|
49
|
-
puts %x{bundle exec yard}
|
50
|
-
end
|
51
|
-
|
52
|
-
task :bench do
|
53
|
-
require File.expand_path("../bench", __FILE__)
|
54
|
-
end
|
55
|
-
|
56
|
-
desc "Bundle for all supported Ruby/AR versions"
|
57
|
-
task :bundle do
|
58
|
-
rubies do
|
59
|
-
versions do
|
60
|
-
command = "#{ENV["RB"]} -S bundle"
|
61
|
-
puts "#{command} (with #{ENV['AR']})"
|
62
|
-
`#{command}`
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
namespace :test do
|
68
|
-
|
69
|
-
desc "Test with all configured adapters"
|
70
|
-
task :adapters do
|
71
|
-
adapters {|a| puts %x{rake test}}
|
72
|
-
end
|
73
|
-
|
74
|
-
desc "Test with all configured Rubies"
|
75
|
-
task :rubies do
|
76
|
-
rubies {|r| puts %x{rake-#{ENV["RB"]} test}}
|
77
|
-
end
|
78
|
-
|
79
|
-
desc "Test with all configured versions"
|
80
|
-
task :versions do
|
81
|
-
versions {|v| puts %x{rake test}}
|
82
|
-
end
|
8
|
+
CLEAN << "pkg" << "doc" << "coverage" << ".yardoc"
|
83
9
|
|
84
|
-
|
85
|
-
task :prerelease do
|
86
|
-
rubies do
|
87
|
-
versions do
|
88
|
-
adapters do
|
89
|
-
puts %x{rake test}
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
10
|
+
Rake::TestTask.new(:test) { |t| t.pattern = "test/**/*test.rb" }
|
94
11
|
|
95
|
-
|
96
|
-
task
|
97
|
-
|
98
|
-
|
99
|
-
puts "Running #{test}:"
|
100
|
-
puts %x{ruby #{test}}
|
101
|
-
end
|
12
|
+
begin
|
13
|
+
require 'reek/rake/task'
|
14
|
+
Reek::Rake::Task.new do |t|
|
15
|
+
t.fail_on_error = false
|
102
16
|
end
|
17
|
+
rescue LoadError
|
103
18
|
end
|
104
19
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
require File.expand_path("../test/helper", __FILE__)
|
109
|
-
FriendlyId::Test::Schema.up
|
110
|
-
end
|
111
|
-
|
112
|
-
desc "Destroy the database schema"
|
113
|
-
task :down do
|
114
|
-
require File.expand_path("../test/helper", __FILE__)
|
115
|
-
FriendlyId::Test::Schema.down
|
116
|
-
end
|
20
|
+
gemspec = File.expand_path("../friendly_id.gemspec", __FILE__)
|
21
|
+
if File.exists? gemspec
|
22
|
+
Rake::GemPackageTask.new(eval(File.read("friendly_id.gemspec"))) { |pkg| }
|
117
23
|
end
|
118
24
|
|
119
|
-
task :doc => :yard
|
data/lib/friendly_id.rb
CHANGED
@@ -1,141 +1,121 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
require "friendly_id/object_utils"
|
6
|
-
require "friendly_id/configuration"
|
7
|
-
require "friendly_id/finder_methods"
|
8
|
-
|
9
|
-
=begin
|
10
|
-
|
11
|
-
== About FriendlyId
|
12
|
-
|
13
|
-
FriendlyId is an add-on to Ruby's Active Record that allows you to replace ids
|
14
|
-
in your URLs with strings:
|
15
|
-
|
16
|
-
# without FriendlyId
|
17
|
-
http://example.com/states/4323454
|
1
|
+
# FriendlyId is a comprehensive Ruby library for ActiveRecord permalinks and
|
2
|
+
# slugs.
|
3
|
+
# @author Norman Clarke
|
4
|
+
module FriendlyId
|
18
5
|
|
19
|
-
|
20
|
-
|
6
|
+
autoload :Slugged, "friendly_id/slugged"
|
7
|
+
autoload :Scoped, "friendly_id/scoped"
|
8
|
+
|
9
|
+
# Class methods that will be added to ActiveRecord::Base.
|
10
|
+
module Base
|
11
|
+
extend self
|
12
|
+
|
13
|
+
def has_friendly_id(*args)
|
14
|
+
options = args.extract_options!
|
15
|
+
base = args.shift
|
16
|
+
friendly_id_config.set options.merge(:base => base)
|
17
|
+
include Model
|
18
|
+
# @NOTE: AR-specific code here
|
19
|
+
validates_exclusion_of base, :in => Configuration::DEFAULTS[:reserved_words]
|
20
|
+
before_save do |record|
|
21
|
+
record.instance_eval {@_current_friendly_id = friendly_id}
|
22
|
+
end
|
23
|
+
end
|
21
24
|
|
22
|
-
|
23
|
-
|
25
|
+
def friendly_id_config
|
26
|
+
@friendly_id_config ||= Configuration.new(self)
|
27
|
+
end
|
24
28
|
|
25
|
-
|
29
|
+
def uses_friendly_id?
|
30
|
+
!! @friendly_id_config
|
31
|
+
end
|
32
|
+
end
|
26
33
|
|
27
|
-
|
28
|
-
|
34
|
+
# Instance methods that will be added to all classes using FriendlyId.
|
35
|
+
module Model
|
29
36
|
|
30
|
-
|
37
|
+
# Convenience method for accessing the class method of the same name.
|
38
|
+
def friendly_id_config
|
39
|
+
self.class.friendly_id_config
|
40
|
+
end
|
31
41
|
|
32
|
-
|
33
|
-
|
34
|
-
|
42
|
+
# Get the instance's friendly_id.
|
43
|
+
def friendly_id
|
44
|
+
send friendly_id_config.query_field
|
45
|
+
end
|
35
46
|
|
36
|
-
|
37
|
-
|
38
|
-
friendly_id
|
39
|
-
validates_format_of :login, :with => /\A[a-z0-9]+\z/i
|
47
|
+
# Either the friendly_id, or the numeric id cast to a string.
|
48
|
+
def to_param
|
49
|
+
(friendly_id or id).to_s
|
40
50
|
end
|
51
|
+
end
|
41
52
|
|
42
|
-
|
43
|
-
|
44
|
-
|
53
|
+
# The configuration paramters passed to +has_friendly_id+ will be stored
|
54
|
+
# in this object.
|
55
|
+
class Configuration
|
56
|
+
attr_accessor :base
|
57
|
+
attr_reader :klass
|
45
58
|
|
46
|
-
|
47
|
-
|
48
|
-
|
59
|
+
DEFAULTS = {
|
60
|
+
:config_error_message => 'FriendlyId has no such config option "%s"',
|
61
|
+
:reserved_words => ["new", "edit"]
|
62
|
+
}
|
49
63
|
|
50
|
-
|
51
|
-
|
52
|
-
|
64
|
+
def initialize(klass, values = nil)
|
65
|
+
@klass = klass
|
66
|
+
set values
|
53
67
|
end
|
54
68
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
column.
|
69
|
+
def method_missing(symbol, *args, &block)
|
70
|
+
option = symbol.to_s.gsub(/=\z/, '')
|
71
|
+
raise ArgumentError, DEFAULTS[:config_error_message] % option
|
72
|
+
end
|
60
73
|
|
61
|
-
|
74
|
+
def set(values)
|
75
|
+
values and values.each {|name, value| self.send "#{name}=", value}
|
76
|
+
end
|
62
77
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
78
|
+
def query_field
|
79
|
+
base
|
80
|
+
end
|
81
|
+
end
|
67
82
|
|
68
|
-
|
69
|
-
|
70
|
-
|
83
|
+
# Utility methods that are in Object because it's impossible to predict what
|
84
|
+
# kinds of objects get passed into FinderMethods#find_one and
|
85
|
+
# Model#normalize_friendly_id.
|
86
|
+
module ObjectUtils
|
87
|
+
|
88
|
+
# True is the id is definitely friendly, false if definitely unfriendly,
|
89
|
+
# else nil.
|
90
|
+
def friendly_id?
|
91
|
+
if kind_of?(Integer) or kind_of?(Symbol) or self.class.respond_to? :friendly_id_config
|
92
|
+
false
|
93
|
+
elsif to_i.to_s != to_s
|
94
|
+
true
|
95
|
+
end
|
71
96
|
end
|
72
97
|
|
73
|
-
|
74
|
-
|
75
|
-
|
98
|
+
# True if the id is definitely unfriendly, false if definitely friendly,
|
99
|
+
# else nil.
|
100
|
+
def unfriendly_id?
|
101
|
+
val = friendly_id? ; !val unless val.nil?
|
102
|
+
end
|
103
|
+
end
|
76
104
|
|
77
|
-
|
105
|
+
# These methods will override the finder methods in ActiveRecord::Relation.
|
106
|
+
module FinderMethods
|
78
107
|
|
79
|
-
|
80
|
-
=end
|
81
|
-
module FriendlyId
|
108
|
+
protected
|
82
109
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
autoload :History, "friendly_id/history"
|
89
|
-
autoload :Reserved, "friendly_id/reserved"
|
90
|
-
autoload :Scoped, "friendly_id/scoped"
|
91
|
-
autoload :Slugged, "friendly_id/slugged"
|
92
|
-
|
93
|
-
# FriendlyId takes advantage of `extended` to do basic model setup, primarily
|
94
|
-
# extending {FriendlyId::Base} to add {FriendlyId::Base#friendly_id
|
95
|
-
# friendly_id} as a class method.
|
96
|
-
#
|
97
|
-
# Previous versions of FriendlyId simply patched ActiveRecord::Base, but this
|
98
|
-
# version tries to be less invasive.
|
99
|
-
#
|
100
|
-
# In addition to adding {FriendlyId::Base#friendly_id friendly_id}, the class
|
101
|
-
# instance variable +@friendly_id_config+ is added. This variable is an
|
102
|
-
# instance of an anonymous subclass of {FriendlyId::Configuration}. This
|
103
|
-
# allows subsequently loaded modules like {FriendlyId::Slugged} and
|
104
|
-
# {FriendlyId::Scoped} to add functionality to the configuration class only
|
105
|
-
# for the current class, rather than monkey patching
|
106
|
-
# {FriendlyId::Configuration} directly. This isolates other models from large
|
107
|
-
# feature changes an addon to FriendlyId could potentially introduce.
|
108
|
-
#
|
109
|
-
# The upshot of this is, you can htwo Active Record models that both have a
|
110
|
-
# @friendly_id_config, but each config object can have different methods and
|
111
|
-
# behaviors depending on what modules have been loaded, without conflicts.
|
112
|
-
# Keep this in mind if you're hacking on FriendlyId.
|
113
|
-
#
|
114
|
-
# For examples of this, see the source for {Scoped.included}.
|
115
|
-
def self.extended(base)
|
116
|
-
base.instance_eval do
|
117
|
-
extend FriendlyId::Base
|
118
|
-
@friendly_id_config = Class.new(FriendlyId::Configuration).new(base)
|
119
|
-
if defaults = FriendlyId.defaults
|
120
|
-
defaults.yield @friendly_id_config
|
121
|
-
end
|
110
|
+
# @NOTE AR-specific code here
|
111
|
+
def find_one(id)
|
112
|
+
return super if !@klass.uses_friendly_id? or id.unfriendly_id?
|
113
|
+
where(@klass.friendly_id_config.query_field => id).first or super
|
122
114
|
end
|
123
|
-
ActiveRecord::Relation.send :include, FriendlyId::FinderMethods
|
124
|
-
end
|
125
115
|
|
126
|
-
# Set global defaults for all models using FriendlyId.
|
127
|
-
#
|
128
|
-
# The default defaults are to use the +:reserved+ module and nothing else.
|
129
|
-
#
|
130
|
-
# @example
|
131
|
-
# FriendlyId.defaults do |config|
|
132
|
-
# config.base = :name
|
133
|
-
# config.use :slugged
|
134
|
-
# end
|
135
|
-
def self.defaults(&block)
|
136
|
-
@mutex.synchronize do
|
137
|
-
@defaults = block if block_given?
|
138
|
-
@defaults ||= lambda {|config| config.use :reserved}
|
139
|
-
end
|
140
116
|
end
|
141
|
-
end
|
117
|
+
end
|
118
|
+
|
119
|
+
ActiveRecord::Base.extend FriendlyId::Base
|
120
|
+
ActiveRecord::Relation.send :include, FriendlyId::FinderMethods
|
121
|
+
Object.send :include, FriendlyId::ObjectUtils
|