cmassimo-friendly_id 3.0.4.2
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/Changelog.md +277 -0
- data/Contributors.md +39 -0
- data/Guide.md +561 -0
- data/LICENSE +19 -0
- data/README.md +83 -0
- data/Rakefile +66 -0
- data/extras/README.txt +3 -0
- data/extras/bench.rb +36 -0
- data/extras/extras.rb +38 -0
- data/extras/prof.rb +14 -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 +73 -0
- data/lib/friendly_id/active_record.rb +52 -0
- data/lib/friendly_id/active_record_adapter/configuration.rb +67 -0
- data/lib/friendly_id/active_record_adapter/finders.rb +156 -0
- data/lib/friendly_id/active_record_adapter/simple_model.rb +123 -0
- data/lib/friendly_id/active_record_adapter/slug.rb +66 -0
- data/lib/friendly_id/active_record_adapter/slugged_model.rb +238 -0
- data/lib/friendly_id/active_record_adapter/tasks.rb +69 -0
- data/lib/friendly_id/configuration.rb +113 -0
- data/lib/friendly_id/finders.rb +109 -0
- data/lib/friendly_id/railtie.rb +20 -0
- data/lib/friendly_id/sequel.rb +5 -0
- data/lib/friendly_id/slug_string.rb +391 -0
- data/lib/friendly_id/slugged.rb +102 -0
- data/lib/friendly_id/status.rb +35 -0
- data/lib/friendly_id/test.rb +291 -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 +119 -0
- data/test/active_record_adapter/basic_slugged_model_test.rb +14 -0
- data/test/active_record_adapter/cached_slug_test.rb +61 -0
- data/test/active_record_adapter/core.rb +98 -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/scoped_model_test.rb +118 -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 +30 -0
- data/test/active_record_adapter/slugged_status_test.rb +25 -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 +87 -0
- data/test/active_record_adapter/tasks_test.rb +82 -0
- data/test/friendly_id_test.rb +55 -0
- data/test/slug_string_test.rb +88 -0
- data/test/test_helper.rb +15 -0
- metadata +168 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
|
3
|
+
module ActiveRecordAdapter
|
4
|
+
|
5
|
+
module Compat
|
6
|
+
def self.scope_method
|
7
|
+
ActiveRecord::VERSION::STRING >= "3" ? :scope : :named_scope
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
include FriendlyId::Base
|
12
|
+
|
13
|
+
def has_friendly_id(method, options = {})
|
14
|
+
class_inheritable_accessor :friendly_id_config
|
15
|
+
write_inheritable_attribute :friendly_id_config, Configuration.new(self, method, options)
|
16
|
+
if friendly_id_config.use_slug?
|
17
|
+
include SluggedModel
|
18
|
+
else
|
19
|
+
include SimpleModel
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# Prevent the cached_slug column from being accidentally or maliciously
|
26
|
+
# overwritten. Note that +attr_protected+ is used to protect the cached_slug
|
27
|
+
# column, unless you have already invoked +attr_accessible+. So if you
|
28
|
+
# wish to use +attr_accessible+, you must invoke it BEFORE you invoke
|
29
|
+
# {#has_friendly_id} in your class.
|
30
|
+
def protect_friendly_id_attributes
|
31
|
+
# only protect the column if the class is not already using attributes_accessible
|
32
|
+
if !accessible_attributes
|
33
|
+
if friendly_id_config.custom_cache_column?
|
34
|
+
attr_protected friendly_id_config.cache_column
|
35
|
+
end
|
36
|
+
attr_protected :cached_slug
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class ActiveRecord::Base
|
44
|
+
extend FriendlyId::ActiveRecordAdapter
|
45
|
+
end
|
46
|
+
|
47
|
+
require File.join(File.dirname(__FILE__), "active_record_adapter", "configuration")
|
48
|
+
require File.join(File.dirname(__FILE__), "active_record_adapter", "finders")
|
49
|
+
require File.join(File.dirname(__FILE__), "active_record_adapter", "simple_model")
|
50
|
+
require File.join(File.dirname(__FILE__), "active_record_adapter", "slugged_model")
|
51
|
+
require File.join(File.dirname(__FILE__), "active_record_adapter", "slug")
|
52
|
+
require File.join(File.dirname(__FILE__), "active_record_adapter", "tasks")
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
|
3
|
+
module ActiveRecordAdapter
|
4
|
+
|
5
|
+
# Extends FriendlyId::Configuration with some implementation details and
|
6
|
+
# features specific to ActiveRecord.
|
7
|
+
class Configuration < FriendlyId::Configuration
|
8
|
+
|
9
|
+
# The column used to cache the friendly_id string. If no column is specified,
|
10
|
+
# FriendlyId will look for a column named +cached_slug+ and use it automatically
|
11
|
+
# if it exists. If for some reason you have a column named +cached_slug+
|
12
|
+
# but don't want FriendlyId to modify it, pass the option
|
13
|
+
# +:cache_column => false+ to {FriendlyId::ActiveRecordAdapter#has_friendly_id has_friendly_id}.
|
14
|
+
attr_accessor :cache_column
|
15
|
+
|
16
|
+
# An array of classes for which the configured class serves as a
|
17
|
+
# FriendlyId scope.
|
18
|
+
attr_reader :child_scopes
|
19
|
+
|
20
|
+
attr_reader :custom_cache_column
|
21
|
+
|
22
|
+
def cache_column
|
23
|
+
return @cache_column if defined?(@cache_column)
|
24
|
+
@cache_column = autodiscover_cache_column
|
25
|
+
end
|
26
|
+
|
27
|
+
def cache_column?
|
28
|
+
!! cache_column
|
29
|
+
end
|
30
|
+
|
31
|
+
def cache_column=(cache_column)
|
32
|
+
@cache_column = cache_column
|
33
|
+
@custom_cache_column = cache_column
|
34
|
+
end
|
35
|
+
|
36
|
+
def child_scopes
|
37
|
+
@child_scopes ||= associated_friendly_classes.select { |klass| klass.friendly_id_config.scopes_over?(configured_class) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def custom_cache_column?
|
41
|
+
!! custom_cache_column
|
42
|
+
end
|
43
|
+
|
44
|
+
def scope_for(record)
|
45
|
+
scope? ? record.send(scope).to_param : nil
|
46
|
+
end
|
47
|
+
|
48
|
+
def scopes_over?(klass)
|
49
|
+
scope? && scope == klass.to_s.underscore.to_sym
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def autodiscover_cache_column
|
55
|
+
:cached_slug if configured_class.columns.any? { |column| column.name == 'cached_slug' }
|
56
|
+
end
|
57
|
+
|
58
|
+
def associated_friendly_classes
|
59
|
+
configured_class.reflect_on_all_associations.select { |assoc|
|
60
|
+
!assoc.options[:polymorphic] &&
|
61
|
+
assoc.klass.uses_friendly_id?
|
62
|
+
}.map(&:klass)
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
|
3
|
+
# The adapter for Ruby on Rails's ActiveRecord. Compatible with AR 2.2.x -
|
4
|
+
# 2.3.x.
|
5
|
+
module ActiveRecordAdapter
|
6
|
+
|
7
|
+
# The classes in this module are used internally by FriendlyId, and exist
|
8
|
+
# largely to avoid polluting the ActiveRecord models with too many
|
9
|
+
# FriendlyId-specific methods.
|
10
|
+
module Finders
|
11
|
+
|
12
|
+
# FinderProxy is used to choose which finder class to instantiate;
|
13
|
+
# depending on the model_class's +friendly_id_config+ and the options
|
14
|
+
# passed into the constructor, it will decide whether to use simple or
|
15
|
+
# slugged finder, a single or multiple finder, and in the case of slugs,
|
16
|
+
# a cached or uncached finder.
|
17
|
+
class FinderProxy
|
18
|
+
|
19
|
+
extend Forwardable
|
20
|
+
|
21
|
+
attr_reader :finder
|
22
|
+
attr :finder_class
|
23
|
+
attr :ids
|
24
|
+
attr :model_class
|
25
|
+
attr :options
|
26
|
+
|
27
|
+
def_delegators :finder, :find, :unfriendly?
|
28
|
+
|
29
|
+
def initialize(model_class, *args, &block)
|
30
|
+
@model_class = model_class
|
31
|
+
@ids = args.shift
|
32
|
+
@options = args.first.kind_of?(Hash) ? args.first : {}
|
33
|
+
end
|
34
|
+
|
35
|
+
# Perform the find query.
|
36
|
+
def finder
|
37
|
+
@finder ||= finder_class.new(ids, model_class, options)
|
38
|
+
end
|
39
|
+
|
40
|
+
def finder_class
|
41
|
+
@finder_class ||= slugged? ? slugged_finder_class : simple_finder_class
|
42
|
+
end
|
43
|
+
|
44
|
+
def multiple?
|
45
|
+
ids.kind_of? Array
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def cache_available?
|
51
|
+
!! model_class.friendly_id_config.cache_column
|
52
|
+
end
|
53
|
+
|
54
|
+
def multiple_slugged_finder_class
|
55
|
+
use_cache? ? SluggedModel::CachedMultipleFinder : SluggedModel::MultipleFinder
|
56
|
+
end
|
57
|
+
|
58
|
+
def simple_finder_class
|
59
|
+
multiple? ? SimpleModel::MultipleFinder : SimpleModel::SingleFinder
|
60
|
+
end
|
61
|
+
|
62
|
+
def slugged?
|
63
|
+
!! model_class.friendly_id_config.use_slug?
|
64
|
+
end
|
65
|
+
|
66
|
+
def slugged_finder_class
|
67
|
+
multiple? ? multiple_slugged_finder_class : single_slugged_finder_class
|
68
|
+
end
|
69
|
+
|
70
|
+
def scoped?
|
71
|
+
!! options[:scope]
|
72
|
+
end
|
73
|
+
|
74
|
+
def single_slugged_finder_class
|
75
|
+
use_cache? ? SluggedModel::CachedSingleFinder : SluggedModel::SingleFinder
|
76
|
+
end
|
77
|
+
|
78
|
+
def use_cache?
|
79
|
+
cache_available? and !scoped?
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
# Wraps finds for multiple records using an array of friendly_ids.
|
85
|
+
# @abstract
|
86
|
+
module Multiple
|
87
|
+
|
88
|
+
include FriendlyId::Finders::Base
|
89
|
+
|
90
|
+
attr_reader :friendly_ids, :results, :unfriendly_ids
|
91
|
+
|
92
|
+
def initialize(ids, model_class, options={})
|
93
|
+
@friendly_ids, @unfriendly_ids = ids.partition {|id| FriendlyId::Finders::Base.friendly?(id) }
|
94
|
+
@unfriendly_ids = @unfriendly_ids.map {|id| id.class.respond_to?(:friendly_id_config) ? id.id : id}
|
95
|
+
super
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
# An error message to use when the wrong number of results was returned.
|
101
|
+
def error_message
|
102
|
+
"Couldn't find all %s with IDs (%s) AND %s (found %d results, but was looking for %d)" % [
|
103
|
+
model_class.name.pluralize,
|
104
|
+
ids.join(', '),
|
105
|
+
sanitize_sql(options[:conditions]),
|
106
|
+
results.size,
|
107
|
+
expected_size
|
108
|
+
]
|
109
|
+
end
|
110
|
+
|
111
|
+
# How many results do we expect?
|
112
|
+
def expected_size
|
113
|
+
limited? ? limit : offset_size
|
114
|
+
end
|
115
|
+
|
116
|
+
# The limit option passed to the find.
|
117
|
+
def limit
|
118
|
+
options[:limit]
|
119
|
+
end
|
120
|
+
|
121
|
+
# Is the find limited?
|
122
|
+
def limited?
|
123
|
+
offset_size > limit if limit
|
124
|
+
end
|
125
|
+
|
126
|
+
# The offset used for the find. If no offset was passed, 0 is returned.
|
127
|
+
def offset
|
128
|
+
options[:offset].to_i
|
129
|
+
end
|
130
|
+
|
131
|
+
# The number of ids, minus the offset.
|
132
|
+
def offset_size
|
133
|
+
ids.size - offset
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
# The methods in this module override ActiveRecord's +find_one+ and
|
141
|
+
# +find_some+ to add FriendlyId's features.
|
142
|
+
module FinderMethods
|
143
|
+
|
144
|
+
def find(*args, &block)
|
145
|
+
finder = Finders::FinderProxy.new(self, *args, &block)
|
146
|
+
if finder.multiple?
|
147
|
+
finder.find
|
148
|
+
else
|
149
|
+
finder.unfriendly? ? super : finder.find or super
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
module ActiveRecordAdapter
|
3
|
+
|
4
|
+
module SimpleModel
|
5
|
+
|
6
|
+
# Some basic methods common to {MultipleFinder} and {SingleFinder}.
|
7
|
+
module SimpleFinder
|
8
|
+
|
9
|
+
# The column used to store the friendly_id.
|
10
|
+
def column
|
11
|
+
"#{table_name}.#{friendly_id_config.column}"
|
12
|
+
end
|
13
|
+
|
14
|
+
# The model's fully-qualified and quoted primary key.
|
15
|
+
def primary_key
|
16
|
+
"#{quoted_table_name}.#{model_class.send :primary_key}"
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
class MultipleFinder
|
22
|
+
|
23
|
+
include FriendlyId::ActiveRecordAdapter::Finders::Multiple
|
24
|
+
include SimpleFinder
|
25
|
+
|
26
|
+
def find
|
27
|
+
@results = model_class.scoped(:conditions => conditions).all(options).uniq
|
28
|
+
raise(::ActiveRecord::RecordNotFound, error_message) if @results.size != expected_size
|
29
|
+
friendly_results.each { |result| result.friendly_id_status.name = result.to_param }
|
30
|
+
@results
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def conditions
|
36
|
+
["#{primary_key} IN (?) OR #{column} IN (?)", unfriendly_ids, friendly_ids]
|
37
|
+
end
|
38
|
+
|
39
|
+
def friendly_results
|
40
|
+
results.select { |result| friendly_ids.include? result.to_param }
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
class SingleFinder
|
46
|
+
|
47
|
+
include FriendlyId::Finders::Base
|
48
|
+
include FriendlyId::Finders::Single
|
49
|
+
include SimpleFinder
|
50
|
+
|
51
|
+
def find
|
52
|
+
result = model_class.scoped(find_options).first(options)
|
53
|
+
raise ::ActiveRecord::RecordNotFound.new if friendly? && !result
|
54
|
+
result.friendly_id_status.name = id if result
|
55
|
+
result
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def find_options
|
61
|
+
@find_options ||= {:conditions => {column => id}}
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.included(base)
|
67
|
+
base.class_eval do
|
68
|
+
column = friendly_id_config.column
|
69
|
+
validate :validate_friendly_id, :unless => :skip_friendly_id_validations
|
70
|
+
validates_presence_of column, :unless => :skip_friendly_id_validations
|
71
|
+
validates_length_of column, :maximum => friendly_id_config.max_length, :unless => :skip_friendly_id_validations
|
72
|
+
after_update :update_scopes
|
73
|
+
extend FriendlyId::ActiveRecordAdapter::FinderMethods
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Get the {FriendlyId::Status} after the find has been performed.
|
78
|
+
def friendly_id_status
|
79
|
+
@friendly_id_status ||= Status.new :record => self
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns the friendly_id.
|
83
|
+
def friendly_id
|
84
|
+
send friendly_id_config.column
|
85
|
+
end
|
86
|
+
alias best_id friendly_id
|
87
|
+
|
88
|
+
# Returns the friendly id, or if none is available, the numeric id.
|
89
|
+
def to_param
|
90
|
+
(friendly_id || id).to_s
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
# The old and new values for the friendly_id column.
|
96
|
+
def friendly_id_changes
|
97
|
+
changes[friendly_id_config.column.to_s]
|
98
|
+
end
|
99
|
+
|
100
|
+
# Update the slugs for any model that is using this model as its
|
101
|
+
# FriendlyId scope.
|
102
|
+
def update_scopes
|
103
|
+
if changes = friendly_id_changes
|
104
|
+
friendly_id_config.child_scopes.each do |klass|
|
105
|
+
Slug.update_all "scope = '#{changes[1]}'", ["sluggable_type = ? AND scope = ?", klass.to_s, changes[0]]
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def skip_friendly_id_validations
|
111
|
+
friendly_id.nil? && friendly_id_config.allow_nil?
|
112
|
+
end
|
113
|
+
|
114
|
+
def validate_friendly_id
|
115
|
+
if result = friendly_id_config.reserved_error_message(friendly_id)
|
116
|
+
self.errors.add(*result)
|
117
|
+
return false
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# A Slug is a unique, human-friendly identifier for an ActiveRecord.
|
2
|
+
class Slug < ::ActiveRecord::Base
|
3
|
+
|
4
|
+
table_name = "slugs"
|
5
|
+
belongs_to :sluggable, :polymorphic => true
|
6
|
+
before_save :enable_name_reversion, :set_sequence
|
7
|
+
validate :validate_name
|
8
|
+
send FriendlyId::ActiveRecordAdapter::Compat.scope_method, :similar_to, lambda {|slug| {:conditions => {
|
9
|
+
:name => slug.name,
|
10
|
+
:scope => slug.scope,
|
11
|
+
:sluggable_type => slug.sluggable_type
|
12
|
+
},
|
13
|
+
:order => "sequence ASC"
|
14
|
+
}
|
15
|
+
}
|
16
|
+
|
17
|
+
# Whether this slug is the most recent of its owner's slugs.
|
18
|
+
def current?
|
19
|
+
sluggable.slug == self
|
20
|
+
end
|
21
|
+
|
22
|
+
def outdated?
|
23
|
+
!current?
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_friendly_id
|
27
|
+
sequence > 1 ? friendly_id_with_sequence : name
|
28
|
+
end
|
29
|
+
|
30
|
+
# Raise a FriendlyId::SlugGenerationError if the slug name is blank.
|
31
|
+
def validate_name
|
32
|
+
if name.blank?
|
33
|
+
raise FriendlyId::BlankError.new("slug.name can not be blank.")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# If we're renaming back to a previously used friendly_id, delete the
|
40
|
+
# slug so that we can recycle the name without having to use a sequence.
|
41
|
+
def enable_name_reversion
|
42
|
+
sluggable.slugs.find_all_by_name_and_scope(name, scope).each { |slug| slug.destroy }
|
43
|
+
end
|
44
|
+
|
45
|
+
def friendly_id_with_sequence
|
46
|
+
"#{name}#{separator}#{sequence}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def similar_to_other_slugs?
|
50
|
+
!similar_slugs.empty?
|
51
|
+
end
|
52
|
+
|
53
|
+
def similar_slugs
|
54
|
+
self.class.similar_to(self)
|
55
|
+
end
|
56
|
+
|
57
|
+
def separator
|
58
|
+
sluggable.friendly_id_config.sequence_separator
|
59
|
+
end
|
60
|
+
|
61
|
+
def set_sequence
|
62
|
+
return unless new_record?
|
63
|
+
self.sequence = similar_slugs.last.sequence.succ if similar_to_other_slugs?
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|