rdavila_friendly_id 2.2.6
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/History.txt +189 -0
- data/LICENSE +19 -0
- data/README.rdoc +384 -0
- data/Rakefile +35 -0
- data/extras/README.txt +3 -0
- data/extras/template-gem.rb +26 -0
- data/extras/template-plugin.rb +28 -0
- data/generators/friendly_id/friendly_id_generator.rb +28 -0
- data/generators/friendly_id/templates/create_slugs.rb +18 -0
- data/generators/friendly_id_20_upgrade/friendly_id_20_upgrade_generator.rb +12 -0
- data/generators/friendly_id_20_upgrade/templates/upgrade_friendly_id_to_20.rb +19 -0
- data/init.rb +1 -0
- data/lib/friendly_id.rb +83 -0
- data/lib/friendly_id/helpers.rb +12 -0
- data/lib/friendly_id/non_sluggable_class_methods.rb +34 -0
- data/lib/friendly_id/non_sluggable_instance_methods.rb +45 -0
- data/lib/friendly_id/slug.rb +98 -0
- data/lib/friendly_id/sluggable_class_methods.rb +113 -0
- data/lib/friendly_id/sluggable_instance_methods.rb +161 -0
- data/lib/friendly_id/tasks.rb +92 -0
- data/lib/friendly_id/version.rb +8 -0
- data/lib/tasks/friendly_id.rake +40 -0
- data/lib/tasks/friendly_id.rb +1 -0
- data/test/cached_slug_test.rb +109 -0
- data/test/custom_slug_normalizer_test.rb +36 -0
- data/test/non_slugged_test.rb +99 -0
- data/test/scoped_model_test.rb +64 -0
- data/test/slug_test.rb +105 -0
- data/test/slugged_model_test.rb +348 -0
- data/test/sti_test.rb +49 -0
- data/test/support/database.yml.postgres +6 -0
- data/test/support/database.yml.sqlite3 +2 -0
- data/test/support/models.rb +45 -0
- data/test/tasks_test.rb +105 -0
- data/test/test_helper.rb +104 -0
- metadata +144 -0
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'rake/clean'
|
6
|
+
|
7
|
+
CLEAN << "pkg" << "docs" << "coverage"
|
8
|
+
|
9
|
+
task :default => :test
|
10
|
+
|
11
|
+
Rake::TestTask.new(:test) { |t| t.pattern = 'test/**/*_test.rb' }
|
12
|
+
Rake::GemPackageTask.new(eval(File.read("friendly_id.gemspec"))) { |pkg| }
|
13
|
+
Rake::RDocTask.new do |r|
|
14
|
+
r.rdoc_dir = "docs"
|
15
|
+
r.main = "README.rdoc"
|
16
|
+
r.rdoc_files.include "README.rdoc", "History.txt", "lib/**/*.rb"
|
17
|
+
end
|
18
|
+
|
19
|
+
begin
|
20
|
+
require "yard"
|
21
|
+
YARD::Rake::YardocTask.new do |t|
|
22
|
+
t.options = ["--output-dir=docs"]
|
23
|
+
end
|
24
|
+
rescue LoadError
|
25
|
+
end
|
26
|
+
|
27
|
+
begin
|
28
|
+
require 'rcov/rcovtask'
|
29
|
+
Rcov::RcovTask.new do |r|
|
30
|
+
r.test_files = FileList['test/*_test.rb']
|
31
|
+
r.verbose = true
|
32
|
+
r.rcov_opts << "--exclude gems/*"
|
33
|
+
end
|
34
|
+
rescue LoadError
|
35
|
+
end
|
data/extras/README.txt
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
run "rm public/index.html"
|
2
|
+
gem "friendly_id"
|
3
|
+
gem "haml"
|
4
|
+
gem "will_paginate"
|
5
|
+
run "haml --rails ."
|
6
|
+
generate "friendly_id"
|
7
|
+
generate :haml_scaffold, "post title:string"
|
8
|
+
route "map.root :controller => 'posts', :action => 'index'"
|
9
|
+
rake "db:migrate"
|
10
|
+
rake "db:fixtures:load"
|
11
|
+
file 'app/models/post.rb',
|
12
|
+
%q{class Post < ActiveRecord::Base
|
13
|
+
has_friendly_id :title, :use_slug => true
|
14
|
+
end}
|
15
|
+
file 'test/fixtures/slugs.yml',
|
16
|
+
%q{
|
17
|
+
one:
|
18
|
+
name: mystring
|
19
|
+
sequence: 1
|
20
|
+
sluggable: one (Post)
|
21
|
+
|
22
|
+
two:
|
23
|
+
name: mystring
|
24
|
+
sequence: 2
|
25
|
+
sluggable: two (Post)
|
26
|
+
}
|
@@ -0,0 +1,28 @@
|
|
1
|
+
run "rm public/index.html"
|
2
|
+
inside 'vendor/plugins' do
|
3
|
+
run "git clone ../../../../ friendly_id"
|
4
|
+
end
|
5
|
+
gem "haml"
|
6
|
+
gem "will_paginate"
|
7
|
+
run "haml --rails ."
|
8
|
+
generate "friendly_id"
|
9
|
+
generate :haml_scaffold, "post title:string"
|
10
|
+
route "map.root :controller => 'posts', :action => 'index'"
|
11
|
+
rake "db:migrate"
|
12
|
+
rake "db:fixtures:load"
|
13
|
+
file 'app/models/post.rb',
|
14
|
+
%q{class Post < ActiveRecord::Base
|
15
|
+
has_friendly_id :title, :use_slug => true
|
16
|
+
end}
|
17
|
+
file 'test/fixtures/slugs.yml',
|
18
|
+
%q{
|
19
|
+
one:
|
20
|
+
name: mystring
|
21
|
+
sequence: 1
|
22
|
+
sluggable: one (Post)
|
23
|
+
|
24
|
+
two:
|
25
|
+
name: mystring
|
26
|
+
sequence: 2
|
27
|
+
sluggable: two (Post)
|
28
|
+
}
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class FriendlyIdGenerator < Rails::Generator::Base
|
2
|
+
|
3
|
+
def manifest
|
4
|
+
record do |m|
|
5
|
+
unless options[:skip_migration]
|
6
|
+
m.migration_template('create_slugs.rb', 'db/migrate', :migration_file_name => 'create_slugs')
|
7
|
+
end
|
8
|
+
unless options[:skip_tasks]
|
9
|
+
m.directory "lib/tasks"
|
10
|
+
m.file "/../../../lib/tasks/friendly_id.rake", "lib/tasks/friendly_id.rake"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def add_options!(opt)
|
18
|
+
opt.separator ''
|
19
|
+
opt.separator 'Options:'
|
20
|
+
opt.on("--skip-migration", "Don't generate a migration for the slugs table") do |value|
|
21
|
+
options[:skip_migration] = value
|
22
|
+
end
|
23
|
+
opt.on("--skip-tasks", "Don't add friendly_id Rake tasks to lib/tasks") do |value|
|
24
|
+
options[:skip_tasks] = value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class CreateSlugs < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :slugs do |t|
|
4
|
+
t.string :name
|
5
|
+
t.integer :sluggable_id
|
6
|
+
t.integer :sequence, :null => false, :default => 1
|
7
|
+
t.string :sluggable_type, :limit => 40
|
8
|
+
t.string :scope, :limit => 40
|
9
|
+
t.datetime :created_at
|
10
|
+
end
|
11
|
+
add_index :slugs, [:name, :sluggable_type, :scope, :sequence], :name => "index_slugs_on_n_s_s_and_s", :unique => true
|
12
|
+
add_index :slugs, :sluggable_id
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.down
|
16
|
+
drop_table :slugs
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class FriendlyId20UpgradeGenerator < Rails::Generator::Base
|
2
|
+
def manifest
|
3
|
+
record do |m|
|
4
|
+
unless options[:skip_migration]
|
5
|
+
m.migration_template(
|
6
|
+
'upgrade_friendly_id_to_20.rb', 'db/migrate', :migration_file_name => 'upgrade_friendly_id_to_20'
|
7
|
+
)
|
8
|
+
m.file "/../../../lib/tasks/friendly_id.rake", "lib/tasks/friendly_id.rake"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class UpgradeFriendlyIdTo20 < ActiveRecord::Migration
|
2
|
+
|
3
|
+
def self.up
|
4
|
+
remove_column :slugs, :updated_at
|
5
|
+
remove_index :slugs, :column => [:name, :sluggable_type]
|
6
|
+
add_column :slugs, :sequence, :integer, :null => false, :default => 1
|
7
|
+
add_column :slugs, :scope, :string, :limit => 40
|
8
|
+
add_index :slugs, [:name, :sluggable_type, :scope, :sequence], :unique => true, :name => "index_slugs_on_n_s_s_and_s"
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.down
|
12
|
+
remove_index :slugs, :name => "index_slugs_on_n_s_s_and_s"
|
13
|
+
remove_column :slugs, :scope
|
14
|
+
remove_column :slugs, :sequence
|
15
|
+
add_column :slugs, :updated_at, :datetime
|
16
|
+
add_index :slugs, [:name, :sluggable_type], :unique => true
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "friendly_id"
|
data/lib/friendly_id.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require "friendly_id/helpers"
|
2
|
+
require "friendly_id/slug"
|
3
|
+
require "friendly_id/sluggable_class_methods"
|
4
|
+
require "friendly_id/sluggable_instance_methods"
|
5
|
+
require "friendly_id/non_sluggable_class_methods"
|
6
|
+
require "friendly_id/non_sluggable_instance_methods"
|
7
|
+
|
8
|
+
begin
|
9
|
+
require 'ar-extensions'
|
10
|
+
require 'ar-extensions/adapters/mysql'
|
11
|
+
require 'ar-extensions/import/mysql'
|
12
|
+
rescue LoadError
|
13
|
+
puts "Please install ar-extensions if you want to use friendly_id:make_slugs_faster or friendly_id:redo_slugs_faster tasks."
|
14
|
+
end
|
15
|
+
|
16
|
+
# FriendlyId is a comprehensive Ruby library for slugging and permalinks with
|
17
|
+
# ActiveRecord.
|
18
|
+
module FriendlyId
|
19
|
+
|
20
|
+
# Default options for has_friendly_id.
|
21
|
+
DEFAULT_OPTIONS = {
|
22
|
+
:max_length => 255,
|
23
|
+
:reserved => ["new", "index"],
|
24
|
+
:reserved_message => 'can not be "%s"'
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
# The names of all valid configuration options.
|
28
|
+
VALID_OPTIONS = (DEFAULT_OPTIONS.keys + [
|
29
|
+
:cache_column,
|
30
|
+
:scope,
|
31
|
+
:strip_diacritics,
|
32
|
+
:strip_non_ascii,
|
33
|
+
:use_slug
|
34
|
+
]).freeze
|
35
|
+
|
36
|
+
# This error is raised when it's not possible to generate a unique slug.
|
37
|
+
class SlugGenerationError < StandardError ; end
|
38
|
+
|
39
|
+
# Set up an ActiveRecord model to use a friendly_id.
|
40
|
+
#
|
41
|
+
# The column argument can be one of your model's columns, or a method
|
42
|
+
# you use to generate the slug.
|
43
|
+
#
|
44
|
+
# Options:
|
45
|
+
# * <tt>:use_slug</tt> - Defaults to nil. Use slugs when you want to use a non-unique text field for friendly ids.
|
46
|
+
# * <tt>:max_length</tt> - Defaults to 255. The maximum allowed length for a slug.
|
47
|
+
# * <tt>:cache_column</tt> - Defaults to nil. Use this column as a cache for generating to_param (experimental) Note that if you use this option, any calls to +attr_accessible+ must be made BEFORE any calls to has_friendly_id in your class.
|
48
|
+
# * <tt>:strip_diacritics</tt> - Defaults to nil. If true, it will remove accents, umlauts, etc. from western characters.
|
49
|
+
# * <tt>:strip_non_ascii</tt> - Defaults to nil. If true, it will remove all non-ASCII characters.
|
50
|
+
# * <tt>:reserved</tt> - Array of words that are reserved and can't be used as friendly_id's. For sluggable models, if such a word is used, it will raise a FriendlyId::SlugGenerationError. Defaults to ["new", "index"].
|
51
|
+
# * <tt>:reserved_message</tt> - The validation message that will be shown when a reserved word is used as a frindly_id. Defaults to '"%s" is reserved'.
|
52
|
+
#
|
53
|
+
# You can also optionally pass a block if you want to use your own custom
|
54
|
+
# slug normalization routines rather than the default ones that come with
|
55
|
+
# friendly_id:
|
56
|
+
#
|
57
|
+
# require "stringex"
|
58
|
+
# class Post < ActiveRecord::Base
|
59
|
+
# has_friendly_id :title, :use_slug => true do |text|
|
60
|
+
# # Use stringex to generate the friendly_id rather than the baked-in methods
|
61
|
+
# text.to_url
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
def has_friendly_id(method, options = {}, &block)
|
65
|
+
options.assert_valid_keys VALID_OPTIONS
|
66
|
+
options = DEFAULT_OPTIONS.merge(options).merge(:method => method)
|
67
|
+
write_inheritable_attribute :friendly_id_options, options
|
68
|
+
class_inheritable_accessor :friendly_id_options
|
69
|
+
class_inheritable_reader :slug_normalizer_block
|
70
|
+
write_inheritable_attribute(:slug_normalizer_block, block) if block_given?
|
71
|
+
if friendly_id_options[:use_slug]
|
72
|
+
extend SluggableClassMethods
|
73
|
+
include SluggableInstanceMethods
|
74
|
+
else
|
75
|
+
extend NonSluggableClassMethods
|
76
|
+
include NonSluggableInstanceMethods
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class ActiveRecord::Base #:nodoc:#
|
82
|
+
extend FriendlyId #:nodoc:#
|
83
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
|
3
|
+
module Helpers
|
4
|
+
# Calculate expected result size for find_some_with_friendly (taken from
|
5
|
+
# active_record/base.rb)
|
6
|
+
def expected_size(ids_and_names, options) #:nodoc:#
|
7
|
+
size = options[:offset] ? ids_and_names.size - options[:offset] : ids_and_names.size
|
8
|
+
size = options[:limit] if options[:limit] && size > options[:limit]
|
9
|
+
size
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module FriendlyId::NonSluggableClassMethods
|
2
|
+
|
3
|
+
include FriendlyId::Helpers
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def find_one(id, options) #:nodoc:#
|
8
|
+
if id.respond_to?(:to_str) && result = send("find_by_#{ friendly_id_options[:method] }", id.to_str, options)
|
9
|
+
result.send(:found_using_friendly_id=, true)
|
10
|
+
else
|
11
|
+
result = super id, options
|
12
|
+
end
|
13
|
+
result
|
14
|
+
end
|
15
|
+
|
16
|
+
def find_some(ids_and_names, options) #:nodoc:#
|
17
|
+
|
18
|
+
names, ids = ids_and_names.partition {|id_or_name| id_or_name.respond_to?(:to_str) && id_or_name.to_str }
|
19
|
+
results = with_scope :find => options do
|
20
|
+
find :all, :conditions => ["#{quoted_table_name}.#{primary_key} IN (?) OR #{friendly_id_options[:method]} IN (?)",
|
21
|
+
ids, names]
|
22
|
+
end
|
23
|
+
|
24
|
+
expected = expected_size(ids_and_names, options)
|
25
|
+
if results.size != expected
|
26
|
+
raise ActiveRecord::RecordNotFound, "Couldn't find all #{ name.pluralize } with IDs (#{ ids_and_names * ', ' }) AND #{ sanitize_sql options[:conditions] } (found #{ results.size } results, but was looking for #{ expected })"
|
27
|
+
end
|
28
|
+
|
29
|
+
results.each {|r| r.send(:found_using_friendly_id=, true) if names.include?(r.friendly_id)}
|
30
|
+
|
31
|
+
results
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module FriendlyId::NonSluggableInstanceMethods
|
2
|
+
|
3
|
+
def self.included(base)
|
4
|
+
base.validate :validate_friendly_id
|
5
|
+
end
|
6
|
+
|
7
|
+
attr :found_using_friendly_id
|
8
|
+
|
9
|
+
# Was the record found using one of its friendly ids?
|
10
|
+
def found_using_friendly_id?
|
11
|
+
@found_using_friendly_id
|
12
|
+
end
|
13
|
+
|
14
|
+
# Was the record found using its numeric id?
|
15
|
+
def found_using_numeric_id?
|
16
|
+
!@found_using_friendly_id
|
17
|
+
end
|
18
|
+
alias has_better_id? found_using_numeric_id?
|
19
|
+
|
20
|
+
# Returns the friendly_id.
|
21
|
+
def friendly_id
|
22
|
+
send friendly_id_options[:method]
|
23
|
+
end
|
24
|
+
alias best_id friendly_id
|
25
|
+
|
26
|
+
# Returns the friendly id, or if none is available, the numeric id.
|
27
|
+
def to_param
|
28
|
+
(friendly_id || id).to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def validate_friendly_id
|
34
|
+
if self.class.friendly_id_options[:reserved].include? friendly_id
|
35
|
+
self.errors.add(self.class.friendly_id_options[:method],
|
36
|
+
self.class.friendly_id_options[:reserved_message] % friendly_id)
|
37
|
+
return false
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def found_using_friendly_id=(value) #:nodoc#
|
42
|
+
@found_using_friendly_id = value
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# A Slug is a unique, human-friendly identifier for an ActiveRecord.
|
2
|
+
class Slug < ActiveRecord::Base
|
3
|
+
|
4
|
+
belongs_to :sluggable, :polymorphic => true
|
5
|
+
before_save :check_for_blank_name, :set_sequence
|
6
|
+
|
7
|
+
|
8
|
+
ASCII_APPROXIMATIONS = {
|
9
|
+
198 => "AE",
|
10
|
+
208 => "D",
|
11
|
+
216 => "O",
|
12
|
+
222 => "Th",
|
13
|
+
223 => "ss",
|
14
|
+
230 => "ae",
|
15
|
+
240 => "d",
|
16
|
+
248 => "o",
|
17
|
+
254 => "th"
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
class << self
|
21
|
+
|
22
|
+
# Sanitizes and dasherizes string to make it safe for URL's.
|
23
|
+
#
|
24
|
+
# Example:
|
25
|
+
#
|
26
|
+
# slug.normalize('This... is an example!') # => "this-is-an-example"
|
27
|
+
#
|
28
|
+
# Note that the Unicode handling in ActiveSupport may fail to process some
|
29
|
+
# characters from Polish, Icelandic and other languages.
|
30
|
+
def normalize(slug_text)
|
31
|
+
return "" if slug_text.nil? || slug_text == ""
|
32
|
+
ActiveSupport::Multibyte.proxy_class.new(slug_text.to_s).normalize(:kc).
|
33
|
+
gsub(/[\W]/u, ' ').
|
34
|
+
strip.
|
35
|
+
gsub(/\s+/u, '-').
|
36
|
+
gsub(/-\z/u, '').
|
37
|
+
downcase.
|
38
|
+
to_s
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse(friendly_id)
|
42
|
+
name, sequence = friendly_id.split('--')
|
43
|
+
sequence ||= "1"
|
44
|
+
return name, sequence
|
45
|
+
end
|
46
|
+
|
47
|
+
# Remove diacritics (accents, umlauts, etc.) from the string. Borrowed
|
48
|
+
# from "The Ruby Way."
|
49
|
+
def strip_diacritics(string)
|
50
|
+
a = ActiveSupport::Multibyte.proxy_class.new(string || "").normalize(:kd)
|
51
|
+
a.unpack('U*').inject([]) { |a, u|
|
52
|
+
if ASCII_APPROXIMATIONS[u]
|
53
|
+
a += ASCII_APPROXIMATIONS[u].unpack('U*')
|
54
|
+
elsif (u < 0x300 || u > 0x036F)
|
55
|
+
a << u
|
56
|
+
end
|
57
|
+
a
|
58
|
+
}.pack('U*')
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
|
63
|
+
# Remove non-ascii characters from the string.
|
64
|
+
def strip_non_ascii(string)
|
65
|
+
strip_diacritics(string).gsub(/[^a-z0-9]+/i, ' ')
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
# Whether or not this slug is the most recent of its owner's slugs.
|
73
|
+
def is_most_recent?
|
74
|
+
sluggable.slug == self
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_friendly_id
|
78
|
+
sequence > 1 ? "#{name}--#{sequence}" : name
|
79
|
+
end
|
80
|
+
|
81
|
+
protected
|
82
|
+
|
83
|
+
# Raise a FriendlyId::SlugGenerationError if the slug name is blank.
|
84
|
+
def check_for_blank_name #:nodoc:#
|
85
|
+
if name.blank?
|
86
|
+
raise FriendlyId::SlugGenerationError.new("The slug text is blank.")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def set_sequence
|
91
|
+
return unless new_record?
|
92
|
+
last = Slug.find(:first, :conditions => { :name => name, :scope => scope,
|
93
|
+
:sluggable_type => sluggable_type}, :order => "sequence DESC",
|
94
|
+
:select => 'sequence')
|
95
|
+
self.sequence = last.sequence + 1 if last
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|