friendly_id4 4.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,111 @@
1
+ require "rubygems"
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
+ ENV["RB"] = ruby
7
+ yield
8
+ ENV["RB"] = nil
9
+ end
10
+ end
11
+
12
+ def versions(&block)
13
+ ["3.1.0.rc4", "3.0.9"].each do |version|
14
+ ENV["AR"] = version
15
+ yield
16
+ ENV["AR"] = nil
17
+ end
18
+ end
19
+
20
+ def adapters(&block)
21
+ ["mysql", "postgres", "sqlite3"].each do |adapter|
22
+ ENV["DB"] = adapter
23
+ yield
24
+ ENV["DB"] = nil
25
+ end
26
+ end
27
+
28
+ task :default => :test
29
+
30
+ Rake::TestTask.new do |t|
31
+ t.test_files = FileList['test/*_test.rb']
32
+ t.verbose = true
33
+ end
34
+
35
+ task :clean do
36
+ %x{rm -rf *.gem doc pkg}
37
+ %x{rm -f `find . -name '*.rbc'`}
38
+ end
39
+
40
+ task :gem do
41
+ %x{gem build friendly_id.gemspec}
42
+ end
43
+
44
+ task :yard do
45
+ %x{bundle exec yard doc --files=*.md}
46
+ end
47
+
48
+ desc "Bundle for all supported Ruby/AR versions"
49
+ task :bundle do
50
+ rubies do
51
+ versions do
52
+ command = "#{ENV["RB"]} -S bundle"
53
+ puts "#{command} (with #{ENV['AR']})"
54
+ `#{command}`
55
+ end
56
+ end
57
+ end
58
+
59
+ namespace :test do
60
+
61
+ desc "Test with all configured adapters"
62
+ task :adapters do
63
+ adapters {|a| puts %x{rake test}}
64
+ end
65
+
66
+ desc "Test with all configured Rubies"
67
+ task :rubies do
68
+ rubies {|r| puts %x{rake-#{ENV["RB"]} test}}
69
+ end
70
+
71
+ desc "Test with all configured versions"
72
+ task :versions do
73
+ versions {|v| puts %x{rake test}}
74
+ end
75
+
76
+ desc "Test all rubies, versions and adapters"
77
+ task :prerelease do
78
+ rubies do
79
+ versions do
80
+ adapters do
81
+ puts %x{rake test}
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ desc "Run each test class in a separate process"
88
+ task :isolated do
89
+ dir = File.expand_path("../test", __FILE__)
90
+ Dir["#{dir}/*_test.rb"].each do |test|
91
+ puts "Running #{test}:"
92
+ puts %x{ruby #{test}}
93
+ end
94
+ end
95
+ end
96
+
97
+ namespace :db do
98
+ desc "Set up the database schema"
99
+ task :up do
100
+ require File.expand_path("../test/helper", __FILE__)
101
+ FriendlyId::Test::Schema.up
102
+ end
103
+
104
+ desc "Destroy the database schema"
105
+ task :down do
106
+ require File.expand_path("../test/helper", __FILE__)
107
+ FriendlyId::Test::Schema.down
108
+ end
109
+ end
110
+
111
+ task :doc => :yard
@@ -0,0 +1,142 @@
1
+ # What's new in FriendlyId 4.0?
2
+
3
+ This is a rewrite/rethink of FriendlyId. It will probably be released some time
4
+ in August or September 2011, once I've had the chance to actually use it in a
5
+ real website for a while.
6
+
7
+ It's probably not wise to use this on a real site right now unless you're
8
+ comfortable with the source code and willing to fix bugs that will likely occur.
9
+
10
+ That said, I will soon be deploying this on a high-traffic, production site, so
11
+ I have a personal stake in making this work well. Your feedback is most welcome.
12
+
13
+ If you want to try it out, grab the source, or [install the
14
+ gem](https://rubygems.org/gems/friendly_id4).
15
+
16
+ ## Back to basics
17
+
18
+ This isn't the "big rewrite," it's the "small rewrite."
19
+
20
+ Adding new features with each release is not sustainable. This release *removes*
21
+ features, but makes it possible to add them back as addons. We can also remove
22
+ some complexity by relying on the better default functionality provided by newer
23
+ versions of Active Support and Active Record. Let's see how small we can make
24
+ this!
25
+
26
+ Here's what's changed:
27
+
28
+ ## Active Record 3+ only
29
+
30
+ For 2.3 support, you can use FriendlyId 3, which will continue to be maintained
31
+ until people don't want it any more.
32
+
33
+ ## In-table slugs
34
+
35
+ FriendlyId no longer creates a separate slugs table - it just stores the
36
+ generated slug value in the model table, which is simpler, faster and what most
37
+ people seem to want. Keeping slug history in a separate table is an optional
38
+ add-on for FriendlyId 4.
39
+
40
+ ## No more multiple finds
41
+
42
+ Person.find "joe-schmoe" # Supported
43
+ Person.find ["joe-schmoe", "john-doe"] # No longer supported
44
+
45
+ If you want find by more than one friendly id, build your own query:
46
+
47
+ Person.where(:slug => ["joe-schmoe", "john-doe"])
48
+
49
+ This lets us do *far* less monkeypatching in Active Record. How much less?
50
+ FriendlyId overrides the base find with a mere 2 lines of code, and otherwise
51
+ changes nothing else. This means more stability and less breakage between Rails
52
+ updates.
53
+
54
+ ## No more finder status
55
+
56
+ FriendlyId 3 offered finder statuses to help you determine when an outdated
57
+ or non-friendly id was used to find the record, so that you could decide whether
58
+ to permanently redirect to the canonical URL. However, there's a simpler way to
59
+ do that, so this feature has been removed:
60
+
61
+ if request.path != person_path(@person)
62
+ return redirect_to @person, :status => :moved_permanently
63
+ end
64
+
65
+ ## No more slug history - unless you want it
66
+
67
+ Since slugs are now stored in-table, when you update them, finds for the
68
+ previous slug will no longer work. This can be a problem for permalinks, since
69
+ renaming a friendly_id will lead to 404's.
70
+
71
+ This was transparently handled by FriendlyId 3, but there were three problems:
72
+
73
+ * Not everybody wants or needs this
74
+ * Performance was negatively affected
75
+ * Determining whether a current or old id was used was expensive, clunky, and
76
+ inconsistent when finding inside relations.
77
+
78
+ Here's how to do this in FriendlyId 4:
79
+
80
+ class PostsController < ApplicationController
81
+
82
+ before_filter :find_post
83
+
84
+ ...
85
+
86
+ def find_post
87
+ return unless params[:id]
88
+ @post = begin
89
+ Post.find params[:id]
90
+ rescue ActiveRecord::RecordNotFound
91
+ Post.find_by_friendly_id params[:id]
92
+ end
93
+ # If an old id or a numeric id was used to find the record, then
94
+ # the request path will not match the post_path, and we should do
95
+ # a 301 redirect that uses the current friendly_id
96
+ if request.path != post_path(@post)
97
+ return redirect_to @post, :status => :moved_permanently
98
+ end
99
+ end
100
+
101
+ Under FriendlyId 4 this is a little more verbose, but offers much finer-grained
102
+ control over the finding process, performs better, and has a much simpler
103
+ implementation.
104
+
105
+ ## "Reserved words" are now just a normal validation
106
+
107
+ Rather than use a custom reserved words validator, use the validations provided
108
+ by Active Record. FriendlyId still reserves "new" and "edit" by default to avoid
109
+ routing problems.
110
+
111
+ validates_exclusion_of :name, :in => ["bad", "word"]
112
+
113
+ You can configure the default words reserved by FriendlyId in
114
+ `FriendlyId::Configuration::DEFAULTS[:reserved_words]`.
115
+
116
+ ## "Allow nil" is now just another validation
117
+
118
+ Previous versions of FriendlyId offered a special option to allow nil slug
119
+ values, but this is now the default. If you don't want this, then simply add a
120
+ validation to the slug column, and/or declare the column `NOT NULL` in your
121
+ database.
122
+
123
+ ## Bye-bye Babosa
124
+
125
+ [Babosa](http://github.com/norman/babosa) is FriendlyId 3's slugging library.
126
+
127
+ FriendlyId 4 doesn't use it by default because the most important pieces of it
128
+ were already accepted into Active Support 3.
129
+
130
+ However, Babosa is still useful, for example, for idiomatically transliterating
131
+ Cyrillic ([or other
132
+ language](https://github.com/norman/babosa/tree/master/lib/babosa/transliterator))
133
+ strings to ASCII. It's very easy to include - just override
134
+ `#normalize_friendly_id` in your model:
135
+
136
+ class MyModel < ActiveRecord::Base
137
+ ...
138
+
139
+ def normalize_friendly_id(text)
140
+ text.to_slug.normalize! :transliterate => :russian
141
+ end
142
+ end
@@ -0,0 +1,71 @@
1
+ require File.expand_path("../test/helper", __FILE__)
2
+ require "ffaker"
3
+ require "friendly_id/migration"
4
+
5
+ N = 1000
6
+
7
+ migration do |m|
8
+ m.add_column :users, :slug, :string
9
+ m.add_index :users, :slug, :unique => true
10
+ end
11
+
12
+ migration do |m|
13
+ m.create_table :posts do |t|
14
+ t.string :name
15
+ t.string :slug
16
+ end
17
+ m.add_index :posts, :slug, :unique => true
18
+ end
19
+ CreateFriendlyIdSlugs.up
20
+
21
+
22
+ class Array
23
+ def rand
24
+ self[Kernel.rand(length)]
25
+ end
26
+ end
27
+
28
+ class User
29
+ include FriendlyId::Slugged
30
+ has_friendly_id :name
31
+ end
32
+
33
+ class Post < ActiveRecord::Base
34
+ include FriendlyId::History
35
+ has_friendly_id :name
36
+ end
37
+
38
+ USERS = []
39
+ BOOKS = []
40
+ POSTS = []
41
+
42
+ 100.times do
43
+ name = Faker::Name.name
44
+ USERS << (User.create! :name => name).friendly_id
45
+ POSTS << (Post.create! :name => name).friendly_id
46
+ BOOKS << (Book.create! :name => name).id
47
+ end
48
+
49
+ Benchmark.bmbm do |x|
50
+ x.report 'find (without FriendlyId)' do
51
+ N.times {Book.find BOOKS.rand}
52
+ end
53
+ x.report 'find (in-table slug)' do
54
+ N.times {User.find USERS.rand}
55
+ end
56
+ x.report 'find (external slug)' do
57
+ N.times {Post.find_by_friendly_id POSTS.rand}
58
+ end
59
+
60
+ x.report 'insert (without FriendlyId)' do
61
+ N.times {transaction {Book.create :name => Faker::Name.name}}
62
+ end
63
+
64
+ x.report 'insert (in-table-slug)' do
65
+ N.times {transaction {User.create :name => Faker::Name.name}}
66
+ end
67
+
68
+ x.report 'insert (external slug)' do
69
+ N.times {transaction {Post.create :name => Faker::Name.name}}
70
+ end
71
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ require "friendly_id/version"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "friendly_id4"
8
+ s.version = FriendlyId::Version::STRING
9
+ s.authors = ["Norman Clarke"]
10
+ s.email = ["norman@njclarke.com"]
11
+ s.homepage = "http://norman.github.com/friendly_id"
12
+ s.summary = "A comprehensive slugging and pretty-URL plugin."
13
+ s.rubyforge_project = "friendly_id"
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test}/*`.split("\n")
16
+ s.require_paths = ["lib"]
17
+
18
+ s.add_development_dependency "activerecord", "~> 3.0"
19
+ s.add_development_dependency "sqlite3", "~> 1.3"
20
+ s.add_development_dependency "cutest", "~> 1.1.2"
21
+ s.add_development_dependency "ffaker"
22
+ s.add_development_dependency "maruku"
23
+ s.add_development_dependency "yard"
24
+ s.add_development_dependency "mocha"
25
+
26
+ s.description = <<-EOM
27
+ FriendlyId is the "Swiss Army bulldozer" of slugging and permalink plugins
28
+ for Ruby on Rails. It allows you to create pretty URL's and work with
29
+ human-friendly strings as if they were numeric ids for ActiveRecord models.
30
+ EOM
31
+ end
@@ -0,0 +1,14 @@
1
+ require "friendly_id/base"
2
+ require "friendly_id/model"
3
+ require "friendly_id/object_utils"
4
+ require "friendly_id/configuration"
5
+ require "friendly_id/finder_methods"
6
+
7
+ # FriendlyId is a comprehensive Ruby library for ActiveRecord permalinks and
8
+ # slugs.
9
+ # @author Norman Clarke
10
+ module FriendlyId
11
+ autoload :Slugged, "friendly_id/slugged"
12
+ autoload :Scoped, "friendly_id/scoped"
13
+ autoload :History, "friendly_id/history"
14
+ end
@@ -0,0 +1,29 @@
1
+ module FriendlyId
2
+ # Class methods that will be added to ActiveRecord::Base.
3
+ module Base
4
+ extend self
5
+
6
+ def has_friendly_id(*args)
7
+ options = args.extract_options!
8
+ base = args.shift
9
+ friendly_id_config.set options.merge(:base => base)
10
+ include Model
11
+ # @NOTE: AR-specific code here
12
+ validates_exclusion_of base, :in => Configuration::DEFAULTS[:reserved_words]
13
+ before_save do |record|
14
+ record.instance_eval {@current_friendly_id = friendly_id}
15
+ end
16
+ self
17
+ end
18
+
19
+ def friendly_id_config
20
+ @friendly_id_config ||= Configuration.new(self)
21
+ end
22
+
23
+ def uses_friendly_id?
24
+ defined? @friendly_id_config
25
+ end
26
+ end
27
+ end
28
+
29
+ ActiveRecord::Base.extend FriendlyId::Base
@@ -0,0 +1,31 @@
1
+ module FriendlyId
2
+ # The configuration paramters passed to +has_friendly_id+ will be stored
3
+ # in this object.
4
+ class Configuration
5
+ attr_accessor :base
6
+ attr_reader :klass
7
+
8
+ DEFAULTS = {
9
+ :config_error_message => 'FriendlyId has no such config option "%s"',
10
+ :reserved_words => ["new", "edit"]
11
+ }
12
+
13
+ def initialize(klass, values = nil)
14
+ @klass = klass
15
+ set values
16
+ end
17
+
18
+ def method_missing(symbol, *args, &block)
19
+ option = symbol.to_s.gsub(/=\z/, '')
20
+ raise ArgumentError, DEFAULTS[:config_error_message] % option
21
+ end
22
+
23
+ def set(values)
24
+ values and values.each {|name, value| self.send "#{name}=", value}
25
+ end
26
+
27
+ def query_field
28
+ base
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,14 @@
1
+ module FriendlyId
2
+ # These methods will override the finder methods in ActiveRecord::Relation.
3
+ module FinderMethods
4
+
5
+ protected
6
+
7
+ def find_one(id)
8
+ return super if !@klass.uses_friendly_id? or id.unfriendly_id?
9
+ where(@klass.friendly_id_config.query_field => id).first or super
10
+ end
11
+ end
12
+ end
13
+
14
+ ActiveRecord::Relation.send :include, FriendlyId::FinderMethods