nateabbott-friendly_id 2.1.4
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 +133 -0
- data/MIT-LICENSE +19 -0
- data/Manifest.txt +39 -0
- data/README.rdoc +343 -0
- data/Rakefile +49 -0
- data/config/website.yml +2 -0
- data/friendly_id.gemspec +45 -0
- data/generators/friendly_id/friendly_id_generator.rb +12 -0
- data/generators/friendly_id/templates/create_slugs.rb +18 -0
- data/generators/friendly_id_20_upgrade/friendly_id_20_upgrade_generator.rb +11 -0
- data/generators/friendly_id_20_upgrade/templates/upgrade_friendly_id_to_20.rb +19 -0
- data/init.rb +3 -0
- data/lib/friendly_id/helpers.rb +15 -0
- data/lib/friendly_id/non_sluggable_class_methods.rb +42 -0
- data/lib/friendly_id/non_sluggable_instance_methods.rb +43 -0
- data/lib/friendly_id/slug.rb +102 -0
- data/lib/friendly_id/sluggable_class_methods.rb +116 -0
- data/lib/friendly_id/sluggable_instance_methods.rb +116 -0
- data/lib/friendly_id/version.rb +10 -0
- data/lib/friendly_id.rb +101 -0
- data/lib/tasks/friendly_id.rake +50 -0
- data/lib/tasks/friendly_id.rb +1 -0
- data/test/contest.rb +94 -0
- data/test/custom_slug_normalizer_test.rb +35 -0
- data/test/models/book.rb +2 -0
- data/test/models/country.rb +4 -0
- data/test/models/event.rb +3 -0
- data/test/models/novel.rb +3 -0
- data/test/models/person.rb +6 -0
- data/test/models/post.rb +6 -0
- data/test/models/thing.rb +6 -0
- data/test/models/user.rb +3 -0
- data/test/non_slugged_test.rb +98 -0
- data/test/schema.rb +55 -0
- data/test/scoped_model_test.rb +53 -0
- data/test/slug_test.rb +106 -0
- data/test/slugged_model_test.rb +284 -0
- data/test/sti_test.rb +48 -0
- data/test/test_helper.rb +30 -0
- metadata +155 -0
@@ -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,15 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FriendlyId
|
4
|
+
|
5
|
+
module Helpers
|
6
|
+
# Calculate expected result size for find_some_with_friendly (taken from
|
7
|
+
# active_record/base.rb)
|
8
|
+
def expected_size(ids_and_names, options) #:nodoc:#
|
9
|
+
size = options[:offset] ? ids_and_names.size - options[:offset] : ids_and_names.size
|
10
|
+
size = options[:limit] if options[:limit] && size > options[:limit]
|
11
|
+
size
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FriendlyId::NonSluggableClassMethods
|
4
|
+
|
5
|
+
include FriendlyId::Helpers
|
6
|
+
|
7
|
+
def self.extended(base) #:nodoc:#
|
8
|
+
class << base
|
9
|
+
alias_method_chain :find_one, :friendly
|
10
|
+
alias_method_chain :find_some, :friendly
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def find_one_with_friendly(id, options) #:nodoc:#
|
17
|
+
if id.is_a?(String) && result = send("find_by_#{ friendly_id_options[:column] }", id, options)
|
18
|
+
result.send(:found_using_friendly_id=, true)
|
19
|
+
else
|
20
|
+
result = find_one_without_friendly id, options
|
21
|
+
end
|
22
|
+
result
|
23
|
+
end
|
24
|
+
|
25
|
+
def find_some_with_friendly(ids_and_names, options) #:nodoc:#
|
26
|
+
|
27
|
+
results = with_scope :find => options do
|
28
|
+
find :all, :conditions => ["#{quoted_table_name}.#{primary_key} IN (?) OR #{friendly_id_options[:column].to_s} IN (?)",
|
29
|
+
ids_and_names, ids_and_names]
|
30
|
+
end
|
31
|
+
|
32
|
+
expected = expected_size(ids_and_names, options)
|
33
|
+
if results.size != expected
|
34
|
+
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 })"
|
35
|
+
end
|
36
|
+
|
37
|
+
results.each {|r| r.send(:found_using_friendly_id=, true) if ids_and_names.include?(r.friendly_id)}
|
38
|
+
|
39
|
+
results
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FriendlyId::NonSluggableInstanceMethods
|
4
|
+
|
5
|
+
attr :found_using_friendly_id
|
6
|
+
|
7
|
+
# Was the record found using one of its friendly ids?
|
8
|
+
def found_using_friendly_id?
|
9
|
+
@found_using_friendly_id
|
10
|
+
end
|
11
|
+
|
12
|
+
# Was the record found using its numeric id?
|
13
|
+
def found_using_numeric_id?
|
14
|
+
!@found_using_friendly_id
|
15
|
+
end
|
16
|
+
alias has_better_id? found_using_numeric_id?
|
17
|
+
|
18
|
+
# Returns the friendly_id.
|
19
|
+
def friendly_id
|
20
|
+
send friendly_id_options[:column]
|
21
|
+
end
|
22
|
+
alias best_id friendly_id
|
23
|
+
|
24
|
+
# Returns the friendly id, or if none is available, the numeric id.
|
25
|
+
def to_param
|
26
|
+
(friendly_id || id).to_s
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def validate_friendly_id
|
32
|
+
if self.class.friendly_id_options[:reserved].include? friendly_id
|
33
|
+
self.errors.add(self.class.friendly_id_options[:column],
|
34
|
+
self.class.friendly_id_options[:reserved_message] % friendly_id)
|
35
|
+
return false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def found_using_friendly_id=(value) #:nodoc#
|
40
|
+
@found_using_friendly_id = value
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
|
3
|
+
# A Slug is a unique, human-friendly identifier for an ActiveRecord.
|
4
|
+
class Slug < ActiveRecord::Base
|
5
|
+
|
6
|
+
belongs_to :sluggable, :polymorphic => true
|
7
|
+
before_save :check_for_blank_name, :set_sequence
|
8
|
+
|
9
|
+
|
10
|
+
ASCII_APPROXIMATIONS = {
|
11
|
+
198 => "AE",
|
12
|
+
208 => "D",
|
13
|
+
216 => "O",
|
14
|
+
222 => "Th",
|
15
|
+
223 => "ss",
|
16
|
+
230 => "ae",
|
17
|
+
240 => "d",
|
18
|
+
248 => "o",
|
19
|
+
254 => "th"
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
class << self
|
23
|
+
|
24
|
+
# Sanitizes and dasherizes string to make it safe for URL's.
|
25
|
+
#
|
26
|
+
# Example:
|
27
|
+
#
|
28
|
+
# slug.normalize('This... is an example!') # => "this-is-an-example"
|
29
|
+
#
|
30
|
+
# Note that the Unicode handling in ActiveSupport may fail to process some
|
31
|
+
# characters from Polish, Icelandic and other languages. If your
|
32
|
+
# application uses these languages, check {out this
|
33
|
+
# article}[http://link-coming-soon.com] for information on how to get
|
34
|
+
# better urls in your application.
|
35
|
+
def normalize(slug_text)
|
36
|
+
return "" if slug_text.nil? || slug_text == ""
|
37
|
+
ActiveSupport::Multibyte.proxy_class.new(slug_text.to_s).normalize(:kc).
|
38
|
+
gsub(/[\W]/u, ' ').
|
39
|
+
strip.
|
40
|
+
gsub(/\s+/u, '-').
|
41
|
+
gsub(/-\z/u, '').
|
42
|
+
downcase.
|
43
|
+
to_s
|
44
|
+
end
|
45
|
+
|
46
|
+
def parse(friendly_id)
|
47
|
+
name, sequence = friendly_id.split('--')
|
48
|
+
sequence ||= "1"
|
49
|
+
return name, sequence
|
50
|
+
end
|
51
|
+
|
52
|
+
# Remove diacritics (accents, umlauts, etc.) from the string. Borrowed
|
53
|
+
# from "The Ruby Way."
|
54
|
+
def strip_diacritics(string)
|
55
|
+
ActiveSupport::Multibyte.proxy_class.new(string).normalize(:kd).unpack('U*').inject([]) { |a, u|
|
56
|
+
if ASCII_APPROXIMATIONS[u]
|
57
|
+
a += ASCII_APPROXIMATIONS[u].unpack('U*')
|
58
|
+
elsif (u < 0x300 || u > 0x036F)
|
59
|
+
a << u
|
60
|
+
end
|
61
|
+
a
|
62
|
+
}.pack('U*')
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
|
67
|
+
# Remove non-ascii characters from the string.
|
68
|
+
def strip_non_ascii(string)
|
69
|
+
strip_diacritics(string).gsub(/[^a-z0-9]+/i, ' ')
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
# Whether or not this slug is the most recent of its owner's slugs.
|
77
|
+
def is_most_recent?
|
78
|
+
sluggable.slug == self
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_friendly_id
|
82
|
+
sequence > 1 ? "#{name}--#{sequence}" : name
|
83
|
+
end
|
84
|
+
|
85
|
+
protected
|
86
|
+
|
87
|
+
# Raise a FriendlyId::SlugGenerationError if the slug name is blank.
|
88
|
+
def check_for_blank_name #:nodoc:#
|
89
|
+
if name.blank?
|
90
|
+
raise FriendlyId::SlugGenerationError.new("The slug text is blank.")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def set_sequence
|
95
|
+
return unless new_record?
|
96
|
+
last = Slug.find(:first, :conditions => { :name => name, :scope => scope,
|
97
|
+
:sluggable_type => sluggable_type}, :order => "sequence DESC",
|
98
|
+
:select => 'sequence')
|
99
|
+
self.sequence = last.sequence + 1 if last
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FriendlyId::SluggableClassMethods
|
4
|
+
|
5
|
+
include FriendlyId::Helpers
|
6
|
+
|
7
|
+
def self.extended(base) #:nodoc:#
|
8
|
+
|
9
|
+
class << base
|
10
|
+
alias_method_chain :find_one, :friendly
|
11
|
+
alias_method_chain :find_some, :friendly
|
12
|
+
alias_method_chain :validate_find_options, :friendly
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
# Finds a single record using the friendly id, or the record's id.
|
18
|
+
def find_one_with_friendly(id_or_name, options) #:nodoc:#
|
19
|
+
|
20
|
+
scope = options.delete(:scope)
|
21
|
+
return find_one_without_friendly(id_or_name, options) if id_or_name.is_a?(Fixnum)
|
22
|
+
|
23
|
+
find_options = {:select => "#{self.table_name}.*"}
|
24
|
+
find_options[:joins] = :slugs unless options[:include] && [*options[:include]].flatten.include?(:slugs)
|
25
|
+
|
26
|
+
name, sequence = Slug.parse(id_or_name)
|
27
|
+
|
28
|
+
find_options[:conditions] = {
|
29
|
+
"#{Slug.table_name}.name" => name,
|
30
|
+
"#{Slug.table_name}.scope" => scope,
|
31
|
+
"#{Slug.table_name}.sequence" => sequence
|
32
|
+
}
|
33
|
+
|
34
|
+
result = with_scope(:find => find_options) { find_initial(options) }
|
35
|
+
|
36
|
+
if result
|
37
|
+
result.finder_slug_name = id_or_name
|
38
|
+
else
|
39
|
+
result = find_one_without_friendly id_or_name, options
|
40
|
+
end
|
41
|
+
|
42
|
+
result
|
43
|
+
rescue ActiveRecord::RecordNotFound => e
|
44
|
+
|
45
|
+
if friendly_id_options[:scope]
|
46
|
+
if !scope
|
47
|
+
e.message << "; expected scope but got none"
|
48
|
+
else
|
49
|
+
e.message << " and scope=#{scope}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
raise e
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
# Finds multiple records using the friendly ids, or the records' ids.
|
58
|
+
def find_some_with_friendly(ids_and_names, options) #:nodoc:#
|
59
|
+
|
60
|
+
slugs, ids = get_slugs_and_ids(ids_and_names, options)
|
61
|
+
results = []
|
62
|
+
|
63
|
+
find_options = {:select => "#{self.table_name}.*"}
|
64
|
+
find_options[:joins] = :slugs unless options[:include] && [*options[:include]].flatten.include?(:slugs)
|
65
|
+
find_options[:conditions] = "#{quoted_table_name}.#{primary_key} IN (#{ids.empty? ? 'NULL' : ids.join(',')}) "
|
66
|
+
find_options[:conditions] << "OR slugs.id IN (#{slugs.to_s(:db)})"
|
67
|
+
|
68
|
+
results = with_scope(:find => find_options) { find_every(options) }.uniq
|
69
|
+
|
70
|
+
expected = expected_size(ids_and_names, options)
|
71
|
+
if results.size != expected
|
72
|
+
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 })"
|
73
|
+
end
|
74
|
+
|
75
|
+
assign_finder_slugs(slugs, results)
|
76
|
+
|
77
|
+
results
|
78
|
+
end
|
79
|
+
|
80
|
+
def validate_find_options_with_friendly(options) #:nodoc:#
|
81
|
+
options.assert_valid_keys([:conditions, :include, :joins, :limit, :offset,
|
82
|
+
:order, :select, :readonly, :group, :from, :lock, :having, :scope])
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
# Assign finder slugs for the results found in find_some_with_friendly
|
88
|
+
def assign_finder_slugs(slugs, results) #:nodoc:#
|
89
|
+
slugs.each do |slug|
|
90
|
+
results.select { |r| r.id == slug.sluggable_id }.each do |result|
|
91
|
+
result.send(:finder_slug=, slug)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Build arrays of slugs and ids, for the find_some_with_friendly method.
|
97
|
+
def get_slugs_and_ids(ids_and_names, options) #:nodoc:#
|
98
|
+
scope = options.delete(:scope)
|
99
|
+
slugs = []
|
100
|
+
ids = []
|
101
|
+
ids_and_names.each do |id_or_name|
|
102
|
+
name, sequence = Slug.parse id_or_name.to_s
|
103
|
+
slug = Slug.find(:first, :conditions => {
|
104
|
+
:name => name,
|
105
|
+
:scope => scope,
|
106
|
+
:sequence => sequence,
|
107
|
+
:sluggable_type => base_class.name
|
108
|
+
})
|
109
|
+
# If the slug was found, add it to the array for later use. If not, and
|
110
|
+
# the id_or_name is a number, assume that it is a regular record id.
|
111
|
+
slug ? slugs << slug : (ids << id_or_name if id_or_name.to_s =~ /\A\d*\z/)
|
112
|
+
end
|
113
|
+
return slugs, ids
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FriendlyId::SluggableInstanceMethods
|
4
|
+
|
5
|
+
NUM_CHARS_RESERVED_FOR_FRIENDLY_ID_EXTENSION = 2
|
6
|
+
|
7
|
+
attr :finder_slug
|
8
|
+
attr_accessor :finder_slug_name
|
9
|
+
|
10
|
+
def finder_slug
|
11
|
+
@finder_slug ||= init_finder_slug or nil
|
12
|
+
end
|
13
|
+
|
14
|
+
# Was the record found using one of its friendly ids?
|
15
|
+
def found_using_friendly_id?
|
16
|
+
finder_slug
|
17
|
+
end
|
18
|
+
|
19
|
+
# Was the record found using its numeric id?
|
20
|
+
def found_using_numeric_id?
|
21
|
+
!found_using_friendly_id?
|
22
|
+
end
|
23
|
+
|
24
|
+
# Was the record found using an old friendly id?
|
25
|
+
def found_using_outdated_friendly_id?
|
26
|
+
finder_slug.id != slug.id
|
27
|
+
end
|
28
|
+
|
29
|
+
# Was the record found using an old friendly id, or its numeric id?
|
30
|
+
def has_better_id?
|
31
|
+
slug and found_using_numeric_id? || found_using_outdated_friendly_id?
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns the friendly id.
|
35
|
+
def friendly_id
|
36
|
+
slug(true).to_friendly_id
|
37
|
+
end
|
38
|
+
alias best_id friendly_id
|
39
|
+
|
40
|
+
# Has the basis of our friendly id changed, requiring the generation of a
|
41
|
+
# new slug?
|
42
|
+
def new_slug_needed?
|
43
|
+
!slug || slug_text != slug.name
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the most recent slug, which is used to determine the friendly
|
47
|
+
# id.
|
48
|
+
def slug(reload = false)
|
49
|
+
@most_recent_slug = nil if reload
|
50
|
+
@most_recent_slug ||= slugs.first
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns the friendly id, or if none is available, the numeric id.
|
54
|
+
def to_param
|
55
|
+
slug ? slug.to_friendly_id : id.to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
# Get the processed string used as the basis of the friendly id.
|
59
|
+
def slug_text
|
60
|
+
base = send friendly_id_options[:column]
|
61
|
+
if self.slug_normalizer_block
|
62
|
+
base = self.slug_normalizer_block.call(base)
|
63
|
+
else
|
64
|
+
if self.friendly_id_options[:strip_diacritics]
|
65
|
+
base = Slug::strip_diacritics(base)
|
66
|
+
end
|
67
|
+
if self.friendly_id_options[:strip_non_ascii]
|
68
|
+
base = Slug::strip_non_ascii(base)
|
69
|
+
end
|
70
|
+
base = Slug::normalize(base)
|
71
|
+
end
|
72
|
+
|
73
|
+
if base.length > friendly_id_options[:max_length]
|
74
|
+
base = base[0...friendly_id_options[:max_length]]
|
75
|
+
end
|
76
|
+
if friendly_id_options[:reserved].include?(base)
|
77
|
+
raise FriendlyId::SlugGenerationError.new("The slug text is a reserved value")
|
78
|
+
end
|
79
|
+
return base
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def finder_slug=(finder_slug)
|
85
|
+
@finder_slug_name = finder_slug.name
|
86
|
+
slug = finder_slug
|
87
|
+
slug.sluggable = self
|
88
|
+
slug
|
89
|
+
end
|
90
|
+
|
91
|
+
def init_finder_slug
|
92
|
+
return false if !@finder_slug_name
|
93
|
+
name, sequence = Slug.parse(@finder_slug_name)
|
94
|
+
slug = Slug.find(:first, :conditions => {:sluggable_id => id, :name => name, :sequence => sequence, :sluggable_type => self.class.base_class.name })
|
95
|
+
finder_slug = slug
|
96
|
+
end
|
97
|
+
|
98
|
+
# Set the slug using the generated friendly id.
|
99
|
+
def set_slug
|
100
|
+
if self.class.friendly_id_options[:use_slug] && new_slug_needed?
|
101
|
+
@most_recent_slug = nil
|
102
|
+
slug_attributes = {:name => slug_text}
|
103
|
+
if friendly_id_options[:scope]
|
104
|
+
scope = self.class.friendly_id_options[:scope] # originally: scope = send(friendly_id_options[:scope])
|
105
|
+
slug_attributes[:scope] = scope.respond_to?(:to_param) ? scope.to_param : scope.to_s
|
106
|
+
end
|
107
|
+
# If we're renaming back to a previously used friendly_id, delete the
|
108
|
+
# slug so that we can recycle the name without having to use a sequence.
|
109
|
+
# ORIG: slugs.find(:all, :conditions => {:name => slug_text, :scope => scope}).each { |s| s.destroy }
|
110
|
+
slugs.find(:all, :conditions => {:name => slug_text, :scope => slug_attributes[:scope]}).each
|
111
|
+
slug = slugs.build slug_attributes
|
112
|
+
slug
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
data/lib/friendly_id.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'friendly_id/helpers'
|
4
|
+
require 'friendly_id/slug'
|
5
|
+
|
6
|
+
# FriendlyId is a comprehensize Rails plugin/gem for slugging and permalinks.
|
7
|
+
module FriendlyId
|
8
|
+
|
9
|
+
# Default options for has_friendly_id.
|
10
|
+
DEFAULT_FRIENDLY_ID_OPTIONS = {
|
11
|
+
:max_length => 255,
|
12
|
+
:method => nil,
|
13
|
+
:reserved => ["new", "index"],
|
14
|
+
:reserved_message => 'can not be "%s"',
|
15
|
+
:scope => nil,
|
16
|
+
:strip_diacritics => false,
|
17
|
+
:strip_non_ascii => false,
|
18
|
+
:use_slug => false }.freeze
|
19
|
+
|
20
|
+
# Valid keys for has_friendly_id options.
|
21
|
+
VALID_FRIENDLY_ID_KEYS = [
|
22
|
+
:max_length,
|
23
|
+
:reserved,
|
24
|
+
:reserved_message,
|
25
|
+
:scope,
|
26
|
+
:strip_diacritics,
|
27
|
+
:strip_non_ascii,
|
28
|
+
:use_slug ].freeze
|
29
|
+
|
30
|
+
# This error is raised when it's not possible to generate a unique slug.
|
31
|
+
class SlugGenerationError < StandardError ; end
|
32
|
+
|
33
|
+
module ClassMethods
|
34
|
+
|
35
|
+
# Set up an ActiveRecord model to use a friendly_id.
|
36
|
+
#
|
37
|
+
# The column argument can be one of your model's columns, or a method
|
38
|
+
# you use to generate the slug.
|
39
|
+
#
|
40
|
+
# Options:
|
41
|
+
# * <tt>:use_slug</tt> - Defaults to false. Use slugs when you want to use a non-unique text field for friendly ids.
|
42
|
+
# * <tt>:max_length</tt> - Defaults to 255. The maximum allowed length for a slug.
|
43
|
+
# * <tt>:strip_diacritics</tt> - Defaults to false. If true, it will remove accents, umlauts, etc. from western characters.
|
44
|
+
# * <tt>:strip_non_ascii</tt> - Defaults to false. If true, it will all non-ascii ([^a-z0-9]) characters.
|
45
|
+
# * <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 be treated the same as if that slug was already taken (numeric extension will be appended). Defaults to ["new", "index"].
|
46
|
+
# * <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'.
|
47
|
+
#
|
48
|
+
# You can also optionally pass a block if you want to use your own custom
|
49
|
+
# slugnormalization routines rather than the default ones that come with
|
50
|
+
# friendly_id:
|
51
|
+
#
|
52
|
+
# require 'stringex'
|
53
|
+
# class Post < ActiveRecord::Base
|
54
|
+
# has_friendly_id :title, :use_slug => true do |text|
|
55
|
+
# # Use stringex to generate the friendly_id rather than the baked-in methods
|
56
|
+
# text.to_url
|
57
|
+
# end
|
58
|
+
# end
|
59
|
+
def has_friendly_id(column, options = {}, &block)
|
60
|
+
options.assert_valid_keys VALID_FRIENDLY_ID_KEYS
|
61
|
+
options = DEFAULT_FRIENDLY_ID_OPTIONS.merge(options).merge(:column => column)
|
62
|
+
write_inheritable_attribute :friendly_id_options, options
|
63
|
+
class_inheritable_accessor :friendly_id_options
|
64
|
+
class_inheritable_reader :slug_normalizer_block
|
65
|
+
|
66
|
+
if options[:use_slug]
|
67
|
+
has_many :slugs, :order => 'id DESC', :as => :sluggable, :dependent => :destroy
|
68
|
+
require 'friendly_id/sluggable_class_methods'
|
69
|
+
require 'friendly_id/sluggable_instance_methods'
|
70
|
+
extend SluggableClassMethods
|
71
|
+
include SluggableInstanceMethods
|
72
|
+
before_save :set_slug
|
73
|
+
if block_given?
|
74
|
+
write_inheritable_attribute :slug_normalizer_block, block
|
75
|
+
end
|
76
|
+
else
|
77
|
+
require 'friendly_id/non_sluggable_class_methods'
|
78
|
+
require 'friendly_id/non_sluggable_instance_methods'
|
79
|
+
extend NonSluggableClassMethods
|
80
|
+
include NonSluggableInstanceMethods
|
81
|
+
validate :validate_friendly_id
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
class << self
|
88
|
+
|
89
|
+
# Load FriendlyId if the gem is included in a Rails app.
|
90
|
+
def enable
|
91
|
+
return if ActiveRecord::Base.methods.include? 'has_friendly_id'
|
92
|
+
ActiveRecord::Base.class_eval { extend FriendlyId::ClassMethods }
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
if defined?(ActiveRecord)
|
100
|
+
FriendlyId::enable
|
101
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
namespace :friendly_id do
|
4
|
+
desc "Make slugs for a model."
|
5
|
+
task :make_slugs => :environment do
|
6
|
+
raise 'USAGE: rake friendly_id:make_slugs MODEL=MyModelName' if ENV["MODEL"].nil?
|
7
|
+
if !sluggable_class.friendly_id_options[:use_slug]
|
8
|
+
raise "Class \"#{sluggable_class.to_s}\" doesn't appear to be using slugs"
|
9
|
+
end
|
10
|
+
while records = sluggable_class.find(:all, :include => :slugs, :conditions => "slugs.id IS NULL", :limit => 1000) do
|
11
|
+
break if records.size == 0
|
12
|
+
records.each do |r|
|
13
|
+
r.send(:set_slug)
|
14
|
+
r.save!
|
15
|
+
puts "#{sluggable_class.to_s}(#{r.id}) friendly_id set to \"#{r.slug.name}\""
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
desc "Regenereate slugs for a model."
|
21
|
+
task :redo_slugs => :environment do
|
22
|
+
raise 'USAGE: rake friendly_id:redo_slugs MODEL=MyModelName' if ENV["MODEL"].nil?
|
23
|
+
if !sluggable_class.friendly_id_options[:use_slug]
|
24
|
+
raise "Class \"#{sluggable_class.to_s}\" doesn't appear to be using slugs"
|
25
|
+
end
|
26
|
+
Slug.destroy_all(["sluggable_type = ?", sluggable_class.to_s])
|
27
|
+
Rake::Task["friendly_id:make_slugs"].invoke
|
28
|
+
end
|
29
|
+
|
30
|
+
desc "Kill obsolete slugs older than 45 days."
|
31
|
+
task :remove_old_slugs => :environment do
|
32
|
+
if ENV["DAYS"].nil?
|
33
|
+
@days = 45
|
34
|
+
else
|
35
|
+
@days = ENV["DAYS"].to_i
|
36
|
+
end
|
37
|
+
slugs = Slug.find(:all, :conditions => ["created_at < ?", DateTime.now - @days.days])
|
38
|
+
slugs.each do |s|
|
39
|
+
s.destroy if !s.is_most_recent?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def sluggable_class
|
45
|
+
if (ENV["MODEL"].split('::').size > 1)
|
46
|
+
ENV["MODEL"].split('::').inject(Kernel) {|scope, const_name| scope.const_get(const_name)}
|
47
|
+
else
|
48
|
+
Object.const_get(ENV["MODEL"])
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
load 'tasks/friendly_id.rake'
|