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 CHANGED
@@ -1,95 +1,74 @@
1
- # FriendlyId
1
+ # FriendlyId 4
2
2
 
3
- FriendlyId is the "Swiss Army bulldozer" of slugging and permalink plugins for
4
- Ruby on Rails. It allows you to create pretty URL's and work with human-friendly
5
- strings as if they were numeric ids for Active Record models.
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
- Using FriendlyId, it's easy to make your application use URL's like:
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
- http://example.com/states/washington
10
+ ## Back to basics
10
11
 
11
- instead of:
12
+ This isn't the "big rewrite," it's the "small rewrite."
12
13
 
13
- http://example.com/states/4323454
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
- ## FriendlyId Features
21
+ Here's what's changed:
17
22
 
18
- FriendlyId offers many advanced features, including: slug history and
19
- versioning, scoped slugs, reserved words, and custom slug generators.
23
+ ## Active Record 3+ only
20
24
 
21
- FriendlyId is compatible with Active Record **3.0** and **3.1**.
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
- ## Rails Quickstart
28
+ ## Remove Babosa
24
29
 
25
- gem install friendly_id
30
+ Babosa is FriendlyId 3's slugging library.
26
31
 
27
- rails new my_app
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
- cd my_app
36
+ ## In-table slugs
30
37
 
31
- # Add to Gemfile - this will change once version 4 is no longer
32
- # in beta, but for now do this:
33
- gem "friendly_id4", "4.0.0.beta4", :require => "friendly_id"
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
- rails generate scaffold user name:string slug:string
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
- # edit db/migrate/*_create_users.rb
39
- add_index :users, :slug, :unique => true
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
- User.create! :name => "Joe Schmoe"
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
- ## Bugs
56
+ Person.find "joe-schmoe" # Supported
57
+ Person.find ["joe-schmoe", "john-doe"] # No longer supported
74
58
 
75
- Please report them on the [Github issue
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
- If you have a bug to report, please include the following information:
61
+ Person.where(:slug => ["joe-schmoe", "john-doe"])
79
62
 
80
- * **Version information for FriendlyId, Rails and Ruby.**
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
- If you are able to, it helps even more if you can fork FriendlyId on Github,
86
- and add a test that reproduces the error you are experiencing.
65
+ ## No more reserved words
87
66
 
88
- ## Credits
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
- FriendlyId was originally created by Norman Clarke and Adrian Mugnolo, with
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
- Copyright (c) 2008-2011 Norman Clarke, released under the MIT license.
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 "rubygems"
1
+ require "rake"
2
2
  require "rake/testtask"
3
-
4
- def rubies(&block)
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
- Rake::TestTask.new do |t|
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
- desc "Test all rubies, versions and adapters"
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
- desc "Run each test class in a separate process"
96
- task :isolated do
97
- dir = File.expand_path("../test", __FILE__)
98
- Dir["#{dir}/*_test.rb"].each do |test|
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
- namespace :db do
106
- desc "Set up the database schema"
107
- task :up do
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
@@ -1,141 +1,121 @@
1
- # encoding: utf-8
2
- require "thread"
3
- require "friendly_id/base"
4
- require "friendly_id/model"
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
- # with FriendlyId
20
- http://example.com/states/washington
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
- It requires few changes to your application code and offers flexibility,
23
- performance and a well-documented codebase.
25
+ def friendly_id_config
26
+ @friendly_id_config ||= Configuration.new(self)
27
+ end
24
28
 
25
- === Concepts
29
+ def uses_friendly_id?
30
+ !! @friendly_id_config
31
+ end
32
+ end
26
33
 
27
- Although FriendlyId helps with URLs, it does all of its work inside your models,
28
- not your routes.
34
+ # Instance methods that will be added to all classes using FriendlyId.
35
+ module Model
29
36
 
30
- === Simple Models
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
- The simplest way to use FriendlyId is with a model that has a uniquely indexed
33
- column with no spaces or special characters, and that is seldom or never
34
- updated. The most common example of this is a user name:
42
+ # Get the instance's friendly_id.
43
+ def friendly_id
44
+ send friendly_id_config.query_field
45
+ end
35
46
 
36
- class User < ActiveRecord::Base
37
- extend FriendlyId
38
- friendly_id :login
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
- @user = User.find "joe" # the old User.find(1) still works, too
43
- @user.to_param # returns "joe"
44
- redirect_to @user # the URL will be /users/joe
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
- In this case, FriendlyId assumes you want to use the column as-is; it will never
47
- modify the value of the column, and your application should ensure that the
48
- value is admissible in a URL:
59
+ DEFAULTS = {
60
+ :config_error_message => 'FriendlyId has no such config option "%s"',
61
+ :reserved_words => ["new", "edit"]
62
+ }
49
63
 
50
- class City < ActiveRecord::Base
51
- extend FriendlyId
52
- friendly_id :name
64
+ def initialize(klass, values = nil)
65
+ @klass = klass
66
+ set values
53
67
  end
54
68
 
55
- @city.find "Viña del Mar"
56
- redirect_to @city # the URL will be /cities/Viña%20del%20Mar
57
-
58
- For this reason, it is often more convenient to use "slugs" rather than a single
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
- === Slugged Models
74
+ def set(values)
75
+ values and values.each {|name, value| self.send "#{name}=", value}
76
+ end
62
77
 
63
- FriendlyId can uses a separate column to store slugs for models which require
64
- some processing of the friendly_id text. The most common example is a blog
65
- post's title, which may have spaces, uppercase characters, or other attributes
66
- you wish to modify to make them more suitable for use in URL's.
78
+ def query_field
79
+ base
80
+ end
81
+ end
67
82
 
68
- class Post < ActiveRecord::Base
69
- extend FriendlyId
70
- friendly_id :title, :use => :slugged
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
- @post = Post.create(:title => "This is the first post!")
74
- @post.friendly_id # returns "this-is-the-first-post"
75
- redirect_to @post # the URL will be /posts/this-is-the-first-post
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
- In general, use slugs by default unless you know for sure you don't need them.
105
+ # These methods will override the finder methods in ActiveRecord::Relation.
106
+ module FinderMethods
78
107
 
79
- @author Norman Clarke
80
- =end
81
- module FriendlyId
108
+ protected
82
109
 
83
- # The current version.
84
- VERSION = "4.0.0.beta6"
85
-
86
- @mutex = Mutex.new
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