friendly_id4 4.0.0.pre → 4.0.0.pre3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +9 -0
- data/ABOUT.md +139 -0
- data/Guide.md +405 -0
- data/README.md +65 -47
- data/Rakefile +12 -16
- data/bench.rb +71 -0
- data/friendly_id.gemspec +29 -0
- data/lib/friendly_id.rb +9 -116
- data/lib/friendly_id/base.rb +29 -0
- data/lib/friendly_id/configuration.rb +31 -0
- data/lib/friendly_id/finder_methods.rb +14 -0
- data/lib/friendly_id/history.rb +29 -0
- data/lib/friendly_id/migration.rb +17 -0
- data/lib/friendly_id/model.rb +22 -0
- data/lib/friendly_id/object_utils.rb +27 -0
- data/lib/friendly_id/scoped.rb +24 -2
- data/lib/friendly_id/slug_sequencer.rb +79 -0
- data/lib/friendly_id/slugged.rb +11 -84
- data/lib/friendly_id/version.rb +1 -1
- data/lib/generators/friendly_id_generator.rb +21 -0
- data/test/core_test.rb +23 -56
- data/test/helper.rb +41 -0
- data/test/history_test.rb +46 -0
- data/test/object_utils_test.rb +18 -0
- data/test/scoped_test.rb +30 -49
- data/test/shared.rb +47 -0
- data/test/slugged_test.rb +31 -52
- metadata +56 -22
- data/lib/friendly_id/test.rb +0 -23
- data/lib/friendly_id/test/generic.rb +0 -84
- data/test/test_helper.rb +0 -23
@@ -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
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
module History
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.class_eval do
|
6
|
+
include Slugged unless include? Slugged
|
7
|
+
extend Finder
|
8
|
+
has_many :friendly_id_slugs, :as => :sluggable, :dependent => :destroy
|
9
|
+
before_save :build_friendly_id_slug, :if => lambda {|r| r.slug_sequencer.slug_changed?}
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def build_friendly_id_slug
|
16
|
+
self.friendly_id_slugs.build :slug => friendly_id
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module Finder
|
21
|
+
def find_by_friendly_id(*args)
|
22
|
+
where("friendly_id_slugs.slug = ?", args.shift).includes(:friendly_id_slugs).first(*args)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class FriendlyIdSlug < ActiveRecord::Base
|
28
|
+
belongs_to :sluggable, :polymorphic => true
|
29
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class CreateFriendlyIdSlugs < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :friendly_id_slugs do |t|
|
4
|
+
t.string :slug, :null => false
|
5
|
+
t.integer :sluggable_id, :null => false
|
6
|
+
t.string :sluggable_type, :limit => 40
|
7
|
+
t.datetime :created_at
|
8
|
+
end
|
9
|
+
add_index :friendly_id_slugs, :sluggable_id
|
10
|
+
add_index :friendly_id_slugs, [:slug, :sluggable_type], :unique => true
|
11
|
+
add_index :friendly_id_slugs, :sluggable_type
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.down
|
15
|
+
drop_table :slugs
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
# Instance methods that will be added to all classes using FriendlyId.
|
3
|
+
module Model
|
4
|
+
|
5
|
+
attr_reader :current_friendly_id
|
6
|
+
|
7
|
+
# Convenience method for accessing the class method of the same name.
|
8
|
+
def friendly_id_config
|
9
|
+
self.class.friendly_id_config
|
10
|
+
end
|
11
|
+
|
12
|
+
# Get the instance's friendly_id.
|
13
|
+
def friendly_id
|
14
|
+
send friendly_id_config.query_field
|
15
|
+
end
|
16
|
+
|
17
|
+
# Either the friendly_id, or the numeric id cast to a string.
|
18
|
+
def to_param
|
19
|
+
(friendly_id or id).to_s
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
# Utility methods that are in Object because it's impossible to predict what
|
3
|
+
# kinds of objects get passed into FinderMethods#find_one and
|
4
|
+
# Model#normalize_friendly_id.
|
5
|
+
module ObjectUtils
|
6
|
+
|
7
|
+
# True is the id is definitely friendly, false if definitely unfriendly,
|
8
|
+
# else nil.
|
9
|
+
def friendly_id?
|
10
|
+
if kind_of?(Integer) or kind_of?(Symbol) or self.class.respond_to? :friendly_id_config
|
11
|
+
false
|
12
|
+
elsif to_i.to_s != to_s
|
13
|
+
true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# True if the id is definitely unfriendly, false if definitely friendly,
|
18
|
+
# else nil.
|
19
|
+
def unfriendly_id?
|
20
|
+
val = friendly_id? ; !val unless val.nil?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Object
|
26
|
+
include FriendlyId::ObjectUtils
|
27
|
+
end
|
data/lib/friendly_id/scoped.rb
CHANGED
@@ -2,7 +2,18 @@ require "friendly_id/slugged"
|
|
2
2
|
|
3
3
|
module FriendlyId
|
4
4
|
|
5
|
-
# This module adds scopes to in-table slugs.
|
5
|
+
# This module adds scopes to in-table slugs. It's not loaded by default,
|
6
|
+
# so in order to active this feature you must include the module in your
|
7
|
+
# class.
|
8
|
+
#
|
9
|
+
# You can scope by an explicit column, or by a `belongs_to` relation.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# class Restaurant < ActiveRecord::Base
|
13
|
+
# belongs_to :city
|
14
|
+
# include FriendlyId::Scoped
|
15
|
+
# has_friendly_id :name, :scope => :city
|
16
|
+
# end
|
6
17
|
module Scoped
|
7
18
|
def self.included(klass)
|
8
19
|
klass.send :include, Slugged unless klass.include? Slugged
|
@@ -10,8 +21,12 @@ module FriendlyId
|
|
10
21
|
end
|
11
22
|
|
12
23
|
class SlugSequencer
|
24
|
+
private
|
25
|
+
|
13
26
|
alias conflict_without_scope conflict
|
14
27
|
|
28
|
+
# Checks for naming conflicts, taking scopes into account.
|
29
|
+
# @return ActiveRecord::Base
|
15
30
|
def conflict_with_scope
|
16
31
|
column = friendly_id_config.scope_column
|
17
32
|
conflicts.where("#{column} = ?", sluggable.send(column)).first
|
@@ -25,8 +40,15 @@ module FriendlyId
|
|
25
40
|
class Configuration
|
26
41
|
attr_accessor :scope
|
27
42
|
|
43
|
+
# Gets the scope column.
|
44
|
+
#
|
45
|
+
# Checks to see if the +:scope+ option passed to {#has_friendly_id}
|
46
|
+
# refers to a relation, and if so, returns the realtion's foreign key.
|
47
|
+
# Otherwise it assumes the option value was the name of column and returns
|
48
|
+
# it cast to a String.
|
49
|
+
# @return String The scope column
|
28
50
|
def scope_column
|
29
|
-
klass.reflections[@scope].try(:association_foreign_key) || @scope.to_s
|
51
|
+
(klass.reflections[@scope].try(:association_foreign_key) || @scope).to_s
|
30
52
|
end
|
31
53
|
end
|
32
54
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
# This class offers functionality to check slug strings for uniqueness and,
|
3
|
+
# if necessary, append a sequence to ensure it.
|
4
|
+
class SlugSequencer
|
5
|
+
attr_reader :sluggable
|
6
|
+
|
7
|
+
def initialize(sluggable)
|
8
|
+
@sluggable = sluggable
|
9
|
+
end
|
10
|
+
|
11
|
+
def next
|
12
|
+
sequence = conflict.slug.split(separator)[1].to_i
|
13
|
+
next_sequence = sequence == 0 ? 2 : sequence.next
|
14
|
+
"#{normalized}#{separator}#{next_sequence}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def generate
|
18
|
+
if slug_changed? or new_record?
|
19
|
+
conflict? ? self.next : normalized
|
20
|
+
else
|
21
|
+
sluggable.friendly_id
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def slug_changed?
|
26
|
+
base != sluggable.current_friendly_id.try(:sub, /--[\d]*\z/, '')
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def base
|
32
|
+
sluggable.send friendly_id_config.base
|
33
|
+
end
|
34
|
+
|
35
|
+
def column
|
36
|
+
friendly_id_config.query_field
|
37
|
+
end
|
38
|
+
|
39
|
+
def conflict?
|
40
|
+
!! conflict
|
41
|
+
end
|
42
|
+
|
43
|
+
def conflict
|
44
|
+
unless defined? @conflict
|
45
|
+
@conflict = conflicts.first
|
46
|
+
end
|
47
|
+
@conflict
|
48
|
+
end
|
49
|
+
|
50
|
+
# @NOTE AR-specific code here
|
51
|
+
def conflicts
|
52
|
+
pkey = sluggable.class.arel_table.primary_key.name
|
53
|
+
value = sluggable.send pkey
|
54
|
+
scope = sluggable.class.where("#{column} = ? OR #{column} LIKE ?", normalized, wildcard)
|
55
|
+
scope = scope.where("#{pkey} <> ?", value) unless sluggable.new_record?
|
56
|
+
scope = scope.order("#{column} DESC")
|
57
|
+
end
|
58
|
+
|
59
|
+
def friendly_id_config
|
60
|
+
sluggable.friendly_id_config
|
61
|
+
end
|
62
|
+
|
63
|
+
def new_record?
|
64
|
+
sluggable.new_record?
|
65
|
+
end
|
66
|
+
|
67
|
+
def normalized
|
68
|
+
@normalized ||= sluggable.normalize_friendly_id(base)
|
69
|
+
end
|
70
|
+
|
71
|
+
def separator
|
72
|
+
friendly_id_config.sequence_separator
|
73
|
+
end
|
74
|
+
|
75
|
+
def wildcard
|
76
|
+
"#{normalized}#{separator}%"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/friendly_id/slugged.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require "friendly_id/slug_sequencer"
|
2
|
+
|
1
3
|
module FriendlyId
|
2
4
|
|
3
5
|
# This module adds in-table slugs to an ActiveRecord model.
|
@@ -14,10 +16,14 @@ module FriendlyId
|
|
14
16
|
value.to_s.parameterize
|
15
17
|
end
|
16
18
|
|
19
|
+
def slug_sequencer
|
20
|
+
SlugSequencer.new(self)
|
21
|
+
end
|
22
|
+
|
17
23
|
private
|
18
24
|
|
19
25
|
def set_slug
|
20
|
-
send "#{friendly_id_config.slug_column}=",
|
26
|
+
send "#{friendly_id_config.slug_column}=", slug_sequencer.generate
|
21
27
|
end
|
22
28
|
end
|
23
29
|
|
@@ -27,101 +33,22 @@ module FriendlyId
|
|
27
33
|
DEFAULTS[:slug_column] = 'slug'
|
28
34
|
DEFAULTS[:sequence_separator] = '--'
|
29
35
|
|
36
|
+
undef query_field
|
37
|
+
|
30
38
|
def query_field
|
31
39
|
use_slugs? ? slug_column : base
|
32
40
|
end
|
33
41
|
|
34
42
|
def sequence_separator
|
35
|
-
@sequence_separator
|
43
|
+
@sequence_separator ||= DEFAULTS[:sequence_separator]
|
36
44
|
end
|
37
45
|
|
38
46
|
def slug_column
|
39
|
-
@slug_column
|
47
|
+
@slug_column ||= DEFAULTS[:slug_column]
|
40
48
|
end
|
41
49
|
|
42
50
|
def use_slugs?
|
43
51
|
@use_slugs
|
44
52
|
end
|
45
53
|
end
|
46
|
-
|
47
|
-
# This class offers functionality to check slug strings for uniqueness and,
|
48
|
-
# if necessary, append a sequence to ensure it.
|
49
|
-
class SlugSequencer
|
50
|
-
attr_reader :sluggable
|
51
|
-
|
52
|
-
def initialize(sluggable)
|
53
|
-
@sluggable = sluggable
|
54
|
-
end
|
55
|
-
|
56
|
-
def base
|
57
|
-
sluggable.send friendly_id_config.base
|
58
|
-
end
|
59
|
-
|
60
|
-
def changed?
|
61
|
-
base != current_friendly_id.try(:sub, /--[\d]*\z/, '')
|
62
|
-
end
|
63
|
-
|
64
|
-
def column
|
65
|
-
friendly_id_config.query_field
|
66
|
-
end
|
67
|
-
|
68
|
-
def conflict?
|
69
|
-
!! conflict
|
70
|
-
end
|
71
|
-
|
72
|
-
def conflict
|
73
|
-
unless defined? @conflict
|
74
|
-
@conflict = conflicts.first
|
75
|
-
end
|
76
|
-
@conflict
|
77
|
-
end
|
78
|
-
|
79
|
-
# @NOTE AR-specific code here
|
80
|
-
def conflicts
|
81
|
-
pkey = sluggable.class.arel_table.primary_key.name
|
82
|
-
value = sluggable.send pkey
|
83
|
-
scope = sluggable.class.where("#{column} = ? OR #{column} LIKE ?", normalized, wildcard)
|
84
|
-
scope = scope.where("#{pkey} <> ?", value) unless sluggable.new_record?
|
85
|
-
scope = scope.order("#{column} DESC")
|
86
|
-
end
|
87
|
-
|
88
|
-
def current_friendly_id
|
89
|
-
sluggable.instance_variable_get(:@_current_friendly_id)
|
90
|
-
end
|
91
|
-
|
92
|
-
def friendly_id_config
|
93
|
-
sluggable.friendly_id_config
|
94
|
-
end
|
95
|
-
|
96
|
-
def new_record?
|
97
|
-
sluggable.new_record?
|
98
|
-
end
|
99
|
-
|
100
|
-
def normalized
|
101
|
-
@normalized ||= sluggable.normalize_friendly_id(base)
|
102
|
-
end
|
103
|
-
|
104
|
-
def separator
|
105
|
-
friendly_id_config.sequence_separator
|
106
|
-
end
|
107
|
-
|
108
|
-
def next
|
109
|
-
sequence = conflict.slug.split(separator)[1].to_i
|
110
|
-
next_sequence = sequence == 0 ? 2 : sequence.next
|
111
|
-
"#{normalized}#{separator}#{next_sequence}"
|
112
|
-
end
|
113
|
-
|
114
|
-
def to_s
|
115
|
-
if changed? or new_record?
|
116
|
-
conflict? ? self.next : normalized
|
117
|
-
else
|
118
|
-
sluggable.friendly_id
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
def wildcard
|
123
|
-
"#{normalized}#{separator}%"
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
54
|
end
|
data/lib/friendly_id/version.rb
CHANGED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
|
4
|
+
class FriendlyIdGenerator < Rails::Generators::Base
|
5
|
+
include Rails::Generators::Migration
|
6
|
+
|
7
|
+
source_root File.expand_path('../../friendly_id', __FILE__)
|
8
|
+
|
9
|
+
def copy_files(*args)
|
10
|
+
migration_template 'migration.rb', 'db/migrate/create_friendly_id_slugs.rb'
|
11
|
+
end
|
12
|
+
|
13
|
+
# Taken from ActiveRecord's migration generator
|
14
|
+
def self.next_migration_number(dirname) #:nodoc:
|
15
|
+
if ActiveRecord::Base.timestamped_migrations
|
16
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
17
|
+
else
|
18
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/test/core_test.rb
CHANGED
@@ -1,68 +1,35 @@
|
|
1
|
-
require File.expand_path("../
|
1
|
+
require File.expand_path("../helper.rb", __FILE__)
|
2
2
|
|
3
|
-
|
3
|
+
setup { Class.new ActiveRecord::Base }
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
@instance ||= klass.create!(:name => name)
|
9
|
-
end
|
10
|
-
|
11
|
-
# @TODO - this kind of setup is repeated in a few places and should probably
|
12
|
-
# be abstracted.
|
13
|
-
def klass
|
14
|
-
@@klass ||= make_model("core", :name) do |t|
|
15
|
-
t.string :name, :unique => true
|
16
|
-
t.boolean :active, :default => true
|
17
|
-
end
|
18
|
-
@@klass
|
19
|
-
end
|
20
|
-
|
21
|
-
def other_class
|
22
|
-
@@other_class ||= make_model("core_two", :name) do |t|
|
23
|
-
t.string :name, :unique => true
|
24
|
-
t.boolean :active, :default => true
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
def setup
|
29
|
-
klass.stubs(:name).returns("Cores")
|
30
|
-
other_class.stubs(:name).returns("CoreTwos")
|
31
|
-
end
|
32
|
-
|
33
|
-
test "models don't use friendly_id by default" do
|
34
|
-
assert !Class.new(ActiveRecord::Base).uses_friendly_id?
|
35
|
-
end
|
5
|
+
test "models don't use friendly_id by default" do |klass|
|
6
|
+
assert !klass.uses_friendly_id?
|
7
|
+
end
|
36
8
|
|
37
|
-
|
38
|
-
|
39
|
-
|
9
|
+
test "model classes should have a friendly id config" do |klass|
|
10
|
+
assert klass.has_friendly_id(:name).friendly_id_config
|
11
|
+
end
|
40
12
|
|
41
|
-
|
42
|
-
|
13
|
+
test "should raise error when bad config options are set" do |klass|
|
14
|
+
assert_raise ArgumentError do
|
15
|
+
klass.has_friendly_id :name, :garbage => :in
|
43
16
|
end
|
17
|
+
end
|
44
18
|
|
45
|
-
|
46
|
-
assert_equal nil, "1".friendly_id?
|
47
|
-
assert_equal nil, "1".unfriendly_id?
|
48
|
-
end
|
19
|
+
[User, Book].map {|klass| klass.has_friendly_id :name}
|
49
20
|
|
50
|
-
|
51
|
-
assert "a".friendly_id?
|
52
|
-
end
|
21
|
+
setup {User}
|
53
22
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
end
|
59
|
-
|
60
|
-
test "reserves 'new' and 'edit' by default" do
|
61
|
-
FriendlyId::Configuration::DEFAULTS[:reserved_words].each do |word|
|
62
|
-
assert_raises ActiveRecord::RecordInvalid do
|
63
|
-
klass.create! :name => word
|
64
|
-
end
|
23
|
+
test "should reserve 'new' and 'edit' by default" do |klass|
|
24
|
+
["new", "edit"].each do |word|
|
25
|
+
transaction do
|
26
|
+
assert_raise(ActiveRecord::RecordInvalid) {klass.create! :name => word}
|
65
27
|
end
|
66
28
|
end
|
29
|
+
end
|
67
30
|
|
31
|
+
test "instances should have a friendly id" do |klass|
|
32
|
+
with_instance_of(klass) {|record| assert record.friendly_id}
|
68
33
|
end
|
34
|
+
|
35
|
+
require File.expand_path("../shared.rb", __FILE__)
|