friendly_id_globalize3 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Changelog.md +354 -0
- data/Contributors.md +43 -0
- data/Guide.md +686 -0
- data/MIT-LICENSE +19 -0
- data/README.md +99 -0
- data/Rakefile +75 -0
- data/extras/README.txt +3 -0
- data/extras/bench.rb +40 -0
- data/extras/extras.rb +38 -0
- data/extras/prof.rb +19 -0
- data/extras/template-gem.rb +26 -0
- data/extras/template-plugin.rb +28 -0
- data/generators/friendly_id/friendly_id_generator.rb +30 -0
- data/generators/friendly_id/templates/create_slugs.rb +18 -0
- data/lib/friendly_id.rb +93 -0
- data/lib/friendly_id/active_record.rb +74 -0
- data/lib/friendly_id/active_record_adapter/configuration.rb +68 -0
- data/lib/friendly_id/active_record_adapter/finders.rb +148 -0
- data/lib/friendly_id/active_record_adapter/relation.rb +165 -0
- data/lib/friendly_id/active_record_adapter/simple_model.rb +63 -0
- data/lib/friendly_id/active_record_adapter/slug.rb +77 -0
- data/lib/friendly_id/active_record_adapter/slugged_model.rb +122 -0
- data/lib/friendly_id/active_record_adapter/tasks.rb +72 -0
- data/lib/friendly_id/configuration.rb +178 -0
- data/lib/friendly_id/datamapper.rb +5 -0
- data/lib/friendly_id/railtie.rb +22 -0
- data/lib/friendly_id/sequel.rb +5 -0
- data/lib/friendly_id/slug_string.rb +25 -0
- data/lib/friendly_id/slugged.rb +105 -0
- data/lib/friendly_id/status.rb +35 -0
- data/lib/friendly_id/test.rb +350 -0
- data/lib/friendly_id/version.rb +9 -0
- data/lib/generators/friendly_id_generator.rb +25 -0
- data/lib/tasks/friendly_id.rake +19 -0
- data/rails/init.rb +2 -0
- data/test/active_record_adapter/ar_test_helper.rb +150 -0
- data/test/active_record_adapter/basic_slugged_model_test.rb +14 -0
- data/test/active_record_adapter/cached_slug_test.rb +76 -0
- data/test/active_record_adapter/core.rb +138 -0
- data/test/active_record_adapter/custom_normalizer_test.rb +20 -0
- data/test/active_record_adapter/custom_table_name_test.rb +22 -0
- data/test/active_record_adapter/default_scope_test.rb +30 -0
- data/test/active_record_adapter/optimistic_locking_test.rb +18 -0
- data/test/active_record_adapter/scoped_model_test.rb +119 -0
- data/test/active_record_adapter/simple_test.rb +76 -0
- data/test/active_record_adapter/slug_test.rb +34 -0
- data/test/active_record_adapter/slugged.rb +33 -0
- data/test/active_record_adapter/slugged_status_test.rb +28 -0
- data/test/active_record_adapter/sti_test.rb +22 -0
- data/test/active_record_adapter/support/database.jdbcsqlite3.yml +2 -0
- data/test/active_record_adapter/support/database.mysql.yml +4 -0
- data/test/active_record_adapter/support/database.postgres.yml +6 -0
- data/test/active_record_adapter/support/database.sqlite3.yml +2 -0
- data/test/active_record_adapter/support/models.rb +104 -0
- data/test/active_record_adapter/tasks_test.rb +82 -0
- data/test/friendly_id_test.rb +96 -0
- data/test/test_helper.rb +13 -0
- metadata +193 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
|
4
|
+
class FriendlyIdGenerator < Rails::Generators::Base
|
5
|
+
|
6
|
+
include Rails::Generators::Migration
|
7
|
+
|
8
|
+
MIGRATIONS_FILE = File.join(File.dirname(__FILE__), "..", "..", "generators", "friendly_id", "templates", "create_slugs.rb")
|
9
|
+
|
10
|
+
class_option :"skip-migration", :type => :boolean, :desc => "Don't generate a migration for the slugs table"
|
11
|
+
|
12
|
+
def copy_files(*args)
|
13
|
+
migration_template MIGRATIONS_FILE, "db/migrate/create_slugs.rb" unless options["skip-migration"]
|
14
|
+
end
|
15
|
+
|
16
|
+
# Taken from ActiveRecord's migration generator
|
17
|
+
def self.next_migration_number(dirname) #:nodoc:
|
18
|
+
if ActiveRecord::Base.timestamped_migrations
|
19
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
20
|
+
else
|
21
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
namespace :friendly_id do
|
2
|
+
desc "Make slugs for a model."
|
3
|
+
task :make_slugs => :environment do
|
4
|
+
FriendlyId::TaskRunner.new.make_slugs do |record|
|
5
|
+
puts "#{record.class}(#{record.id}): friendly_id set to '#{record.slug.name}'" if record.slug
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
desc "Regenereate slugs for a model."
|
10
|
+
task :redo_slugs => :environment do
|
11
|
+
FriendlyId::TaskRunner.new.delete_slugs
|
12
|
+
Rake::Task["friendly_id:make_slugs"].invoke
|
13
|
+
end
|
14
|
+
|
15
|
+
desc "Destroy obsolete slugs older than DAYS=45 days."
|
16
|
+
task :remove_old_slugs => :environment do
|
17
|
+
FriendlyId::TaskRunner.new.delete_old_slugs
|
18
|
+
end
|
19
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
require File.expand_path('../../test_helper', __FILE__)
|
2
|
+
require "logger"
|
3
|
+
require "active_record"
|
4
|
+
begin
|
5
|
+
require "active_support/log_subscriber"
|
6
|
+
rescue LoadError
|
7
|
+
end
|
8
|
+
|
9
|
+
# If you want to see the ActiveRecord log, invoke the tests using `rake test LOG=true`
|
10
|
+
ActiveRecord::Base.logger = Logger.new($stdout) if ENV["LOG"]
|
11
|
+
|
12
|
+
require "friendly_id/active_record"
|
13
|
+
require File.expand_path("../../../generators/friendly_id/templates/create_slugs", __FILE__)
|
14
|
+
require File.expand_path("../support/models", __FILE__)
|
15
|
+
require File.expand_path('../core', __FILE__)
|
16
|
+
require File.expand_path('../slugged', __FILE__)
|
17
|
+
|
18
|
+
driver = (ENV["DB"] or "sqlite3").downcase
|
19
|
+
db_yaml = File.expand_path("../support/database.#{driver}.yml", __FILE__)
|
20
|
+
ActiveRecord::Base.establish_connection(YAML::load(File.open(db_yaml)))
|
21
|
+
|
22
|
+
class ActiveRecord::Base
|
23
|
+
def log_protected_attribute_removal(*args) end
|
24
|
+
end
|
25
|
+
|
26
|
+
ActiveRecord::Base.connection.tables.each do |table|
|
27
|
+
ActiveRecord::Base.connection.drop_table(table)
|
28
|
+
end
|
29
|
+
ActiveRecord::Migration.verbose = false
|
30
|
+
CreateSlugs.up
|
31
|
+
CreateSupportModels.up
|
32
|
+
|
33
|
+
# A model that uses the automagically configured "cached_slug" column
|
34
|
+
class District < ActiveRecord::Base
|
35
|
+
has_friendly_id :name, :use_slug => true
|
36
|
+
before_save :say_hello
|
37
|
+
|
38
|
+
def say_hello
|
39
|
+
@said_hello = true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# A model with optimistic locking enabled
|
44
|
+
class Region < ActiveRecord::Base
|
45
|
+
has_friendly_id :name, :use_slug => true
|
46
|
+
after_create do |obj|
|
47
|
+
other_instance = Region.find obj.id
|
48
|
+
other_instance.update_attributes :note => name + "!"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# A model that specifies a custom cached slug column
|
53
|
+
class City < ActiveRecord::Base
|
54
|
+
has_friendly_id :name, :use_slug => true, :cache_column => "my_slug"
|
55
|
+
end
|
56
|
+
|
57
|
+
# A model with a custom slug text normalizer
|
58
|
+
class Person < ActiveRecord::Base
|
59
|
+
has_friendly_id :name, :use_slug => true
|
60
|
+
|
61
|
+
def normalize_friendly_id(string)
|
62
|
+
string.upcase
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
# A model that doesn't use FriendlyId
|
68
|
+
class Unfriendly < ActiveRecord::Base
|
69
|
+
end
|
70
|
+
|
71
|
+
# A slugged model that uses a scope
|
72
|
+
class Resident < ActiveRecord::Base
|
73
|
+
belongs_to :country
|
74
|
+
has_friendly_id :name, :use_slug => true, :scope => :country
|
75
|
+
end
|
76
|
+
|
77
|
+
# Like resident, but has a cached slug
|
78
|
+
class Tourist < ActiveRecord::Base
|
79
|
+
belongs_to :country
|
80
|
+
has_friendly_id :name, :use_slug => true, :scope => :country
|
81
|
+
end
|
82
|
+
|
83
|
+
# A slugged model used as a scope
|
84
|
+
class Country < ActiveRecord::Base
|
85
|
+
has_many :people
|
86
|
+
has_many :residents
|
87
|
+
has_friendly_id :name, :use_slug => true
|
88
|
+
end
|
89
|
+
|
90
|
+
# A model that doesn't use slugs
|
91
|
+
class User < ActiveRecord::Base
|
92
|
+
has_friendly_id :name
|
93
|
+
has_many :houses
|
94
|
+
end
|
95
|
+
|
96
|
+
# Another model that doesn"t use slugs
|
97
|
+
class Author < ActiveRecord::Base
|
98
|
+
has_friendly_id :name
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
# A model that uses a non-slugged model for its scope
|
103
|
+
class House < ActiveRecord::Base
|
104
|
+
belongs_to :user
|
105
|
+
has_friendly_id :name, :use_slug => true, :scope => :user
|
106
|
+
end
|
107
|
+
|
108
|
+
# A model that uses default slug settings and has a named scope
|
109
|
+
class Post < ActiveRecord::Base
|
110
|
+
has_friendly_id :name, :use_slug => true
|
111
|
+
def self.named_scope(*args) scope(*args) end if FriendlyId.on_ar3?
|
112
|
+
named_scope :published, :conditions => { :published => true }
|
113
|
+
end
|
114
|
+
|
115
|
+
# Model that uses a custom table name
|
116
|
+
class Place < ActiveRecord::Base
|
117
|
+
self.table_name = "legacy_table"
|
118
|
+
has_friendly_id :name, :use_slug => true
|
119
|
+
end
|
120
|
+
|
121
|
+
# A model that uses a datetime field for its friendly_id
|
122
|
+
class Event < ActiveRecord::Base
|
123
|
+
has_friendly_id :event_date, :use_slug => true
|
124
|
+
end
|
125
|
+
|
126
|
+
# A base model for single table inheritence
|
127
|
+
class Book < ActiveRecord::Base ; end
|
128
|
+
|
129
|
+
# A model that uses STI
|
130
|
+
class Novel < ::Book
|
131
|
+
has_friendly_id :name, :use_slug => true
|
132
|
+
end
|
133
|
+
|
134
|
+
# A model with no table
|
135
|
+
class Question < ActiveRecord::Base
|
136
|
+
has_friendly_id :name, :use_slug => true
|
137
|
+
end
|
138
|
+
|
139
|
+
# A model to test polymorphic associations
|
140
|
+
class Site < ActiveRecord::Base
|
141
|
+
belongs_to :owner, :polymorphic => true
|
142
|
+
has_friendly_id :name, :use_slug => true
|
143
|
+
end
|
144
|
+
|
145
|
+
# A model used as a polymorphic owner
|
146
|
+
class Company < ActiveRecord::Base
|
147
|
+
has_many :sites, :as => :owner
|
148
|
+
end
|
149
|
+
|
150
|
+
puts "Using: #{RUBY_VERSION}, #{driver}, AR#{ENV["AR"] or 3}"
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require File.expand_path('../ar_test_helper', __FILE__)
|
2
|
+
|
3
|
+
module FriendlyId
|
4
|
+
module Test
|
5
|
+
module ActiveRecordAdapter
|
6
|
+
class BasicSluggedModelTest < ::Test::Unit::TestCase
|
7
|
+
include FriendlyId::Test::Generic
|
8
|
+
include FriendlyId::Test::Slugged
|
9
|
+
include FriendlyId::Test::ActiveRecordAdapter::Slugged
|
10
|
+
include FriendlyId::Test::ActiveRecordAdapter::Core
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require File.expand_path("../ar_test_helper", __FILE__)
|
2
|
+
|
3
|
+
module FriendlyId
|
4
|
+
module Test
|
5
|
+
module ActiveRecordAdapter
|
6
|
+
|
7
|
+
class CachedSlugTest < ::Test::Unit::TestCase
|
8
|
+
|
9
|
+
include FriendlyId::Test::Generic
|
10
|
+
include FriendlyId::Test::Slugged
|
11
|
+
include FriendlyId::Test::ActiveRecordAdapter::Slugged
|
12
|
+
include FriendlyId::Test::ActiveRecordAdapter::Core
|
13
|
+
|
14
|
+
def klass
|
15
|
+
District
|
16
|
+
end
|
17
|
+
|
18
|
+
def other_class
|
19
|
+
Post
|
20
|
+
end
|
21
|
+
|
22
|
+
def cached_slug
|
23
|
+
instance.send(cache_column)
|
24
|
+
end
|
25
|
+
|
26
|
+
def cache_column
|
27
|
+
klass.friendly_id_config.cache_column
|
28
|
+
end
|
29
|
+
|
30
|
+
test "should have a cached_slug" do
|
31
|
+
assert_equal cached_slug, instance.slug.to_friendly_id
|
32
|
+
end
|
33
|
+
|
34
|
+
test "should protect the cached slug value" do
|
35
|
+
old_value = cached_slug
|
36
|
+
instance.update_attributes(cache_column => "Madrid")
|
37
|
+
instance.reload
|
38
|
+
assert_equal old_value, cached_slug
|
39
|
+
end
|
40
|
+
|
41
|
+
test "should update the cached slug when updating the slug" do
|
42
|
+
instance.update_attributes(:name => "new name")
|
43
|
+
assert_equal instance.slug.to_friendly_id, cached_slug
|
44
|
+
end
|
45
|
+
|
46
|
+
test "should not update the cached slug column if it has not changed" do
|
47
|
+
instance.note = "a note"
|
48
|
+
instance.expects("#{cache_column}=".to_sym).never
|
49
|
+
instance.save!
|
50
|
+
end
|
51
|
+
|
52
|
+
test "should cache the incremented sequence for duplicate slug names" do
|
53
|
+
instance_2 = klass.create!(:name => instance.name)
|
54
|
+
assert_match(/2\z/, instance_2.send(cache_column))
|
55
|
+
end
|
56
|
+
|
57
|
+
test "#friendly_id should check the cached value by default" do
|
58
|
+
instance.expects(:slug).never
|
59
|
+
instance.friendly_id
|
60
|
+
end
|
61
|
+
|
62
|
+
test "#friendly_id should skip the cache if invoked with true" do
|
63
|
+
instance.expects(:slug)
|
64
|
+
instance.friendly_id(true)
|
65
|
+
end
|
66
|
+
|
67
|
+
test "should not fire callbacks when updating slug cache" do
|
68
|
+
instance.expects(:say_hello).once
|
69
|
+
instance.update_attributes(:name => "new name")
|
70
|
+
assert_equal instance.slug.to_friendly_id, cached_slug
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require File.expand_path("../ar_test_helper", __FILE__)
|
2
|
+
|
3
|
+
module FriendlyId
|
4
|
+
|
5
|
+
module Test
|
6
|
+
|
7
|
+
module ActiveRecordAdapter
|
8
|
+
|
9
|
+
module Core
|
10
|
+
|
11
|
+
def teardown
|
12
|
+
klass.delete_all
|
13
|
+
other_class.delete_all
|
14
|
+
Slug.delete_all
|
15
|
+
end
|
16
|
+
|
17
|
+
def find_method
|
18
|
+
:find
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_method
|
22
|
+
:create!
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete_all_method
|
26
|
+
:delete_all
|
27
|
+
end
|
28
|
+
|
29
|
+
def save_method
|
30
|
+
:save!
|
31
|
+
end
|
32
|
+
|
33
|
+
def unfriendly_class
|
34
|
+
Unfriendly
|
35
|
+
end
|
36
|
+
|
37
|
+
def validation_exceptions
|
38
|
+
[ActiveRecord::RecordInvalid, FriendlyId::ReservedError, FriendlyId::BlankError]
|
39
|
+
end
|
40
|
+
|
41
|
+
test "should return their friendly_id for #to_param" do
|
42
|
+
assert_match(instance.friendly_id, instance.to_param)
|
43
|
+
end
|
44
|
+
|
45
|
+
test "instances should be findable by their own instance" do
|
46
|
+
assert_equal instance, klass.find(instance)
|
47
|
+
end
|
48
|
+
|
49
|
+
test "instances should be findable by an array of friendly_ids" do
|
50
|
+
second = klass.create!(:name => "second_instance")
|
51
|
+
third = klass.create!(:name => "third_instance")
|
52
|
+
assert_equal 2, klass.find([instance.friendly_id, second.friendly_id]).size
|
53
|
+
end
|
54
|
+
|
55
|
+
test "failing finds with array of unfriendly_id should raise errors normally" do
|
56
|
+
assert_raise ActiveRecord::RecordNotFound do
|
57
|
+
klass.find([0, -1])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
test "instances should be findable by an array of numeric ids" do
|
62
|
+
second = klass.create!(:name => "second_instance")
|
63
|
+
third = klass.create!(:name => "third_instance")
|
64
|
+
assert_equal 2, klass.find([instance.id.to_i, second.id.to_i]).size
|
65
|
+
end
|
66
|
+
|
67
|
+
test "instances should be findable by an array of numeric ids as strings" do
|
68
|
+
second = klass.create!(:name => "second_instance")
|
69
|
+
third = klass.create!(:name => "third_instance")
|
70
|
+
assert_equal 2, klass.find([instance.id.to_s, second.id.to_s]).size
|
71
|
+
end
|
72
|
+
|
73
|
+
test "instances should be findable by an array of instances" do
|
74
|
+
second = klass.create!(:name => "second_instance")
|
75
|
+
third = klass.create!(:name => "third_instance")
|
76
|
+
assert_equal 2, klass.find([instance, second]).size
|
77
|
+
end
|
78
|
+
|
79
|
+
test "instances should be findable by an array of mixed types" do
|
80
|
+
second = klass.create!(:name => "second_instance")
|
81
|
+
third = klass.create!(:name => "third_instance")
|
82
|
+
assert_equal 2, klass.find([instance.friendly_id, second]).size
|
83
|
+
end
|
84
|
+
|
85
|
+
test "should not raise error when finding with empty array" do
|
86
|
+
assert_nothing_raised do
|
87
|
+
klass.find []
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
test "models should raise an error when not all records are found" do
|
92
|
+
assert_raises(ActiveRecord::RecordNotFound) do
|
93
|
+
klass.find([instance.friendly_id, 'bad-friendly-id'])
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
test "models should respect finder conditions" do
|
98
|
+
assert_raise ActiveRecord::RecordNotFound do
|
99
|
+
klass.find(instance.friendly_id, :conditions => "1 = 2")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# This emulates a fairly common issue where id's generated by fixtures are very high.
|
104
|
+
test "should continue to admit very large ids" do
|
105
|
+
klass.connection.execute("INSERT INTO #{klass.table_name} (id, name) VALUES (2047483647, 'An instance')")
|
106
|
+
assert_nothing_raised do
|
107
|
+
klass.base_class.find(2047483647)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
test "should not change failure behavior for models not using friendly_id" do
|
112
|
+
assert_raise ActiveRecord::RecordNotFound do
|
113
|
+
unfriendly_class.find(-1)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
test "failing finds with unfriendly_id should raise errors normally" do
|
118
|
+
assert_raise ActiveRecord::RecordNotFound do
|
119
|
+
klass.send(find_method, 0)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
test "instances found by a single id should not be read-only" do
|
124
|
+
i = klass.find(instance.friendly_id)
|
125
|
+
assert !i.readonly?, "expected instance not to be readonly"
|
126
|
+
end
|
127
|
+
|
128
|
+
test "instances found by an array of ids should not be read-only" do
|
129
|
+
second = klass.create!(:name => "second_instance")
|
130
|
+
third = klass.create!(:name => "third_instance")
|
131
|
+
klass.find([instance.friendly_id, second.friendly_id]).each do |record|
|
132
|
+
assert !record.readonly?, "expected instance not to be readonly"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require File.expand_path("../ar_test_helper", __FILE__)
|
2
|
+
|
3
|
+
module FriendlyId
|
4
|
+
module Test
|
5
|
+
module ActiveRecordAdapter
|
6
|
+
|
7
|
+
class CustomNormalizerTest < ::Test::Unit::TestCase
|
8
|
+
|
9
|
+
include FriendlyId::Test::ActiveRecordAdapter::Core
|
10
|
+
include FriendlyId::Test::ActiveRecordAdapter::Slugged
|
11
|
+
include FriendlyId::Test::CustomNormalizer
|
12
|
+
|
13
|
+
def klass
|
14
|
+
Person
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|