friendly_id_globalize3 3.2.0
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 +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,74 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
|
3
|
+
# Are we running on ActiveRecord 3 or higher?
|
4
|
+
def self.on_ar3?
|
5
|
+
ActiveRecord::VERSION::STRING >= "3"
|
6
|
+
end
|
7
|
+
|
8
|
+
module ActiveRecordAdapter
|
9
|
+
|
10
|
+
include FriendlyId::Base
|
11
|
+
|
12
|
+
def has_friendly_id(method, options = {})
|
13
|
+
if FriendlyId.on_ar3?
|
14
|
+
class_attribute :friendly_id_config
|
15
|
+
self.friendly_id_config = Configuration.new(self, method, options)
|
16
|
+
else
|
17
|
+
class_inheritable_accessor :friendly_id_config
|
18
|
+
write_inheritable_attribute :friendly_id_config, Configuration.new(self, method, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
if friendly_id_config.use_slug?
|
22
|
+
include SluggedModel
|
23
|
+
else
|
24
|
+
include SimpleModel
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# Prevent the cached_slug column from being accidentally or maliciously
|
31
|
+
# overwritten. Note that +attr_protected+ is used to protect the cached_slug
|
32
|
+
# column, unless you have already invoked +attr_accessible+. So if you
|
33
|
+
# wish to use +attr_accessible+, you must invoke it BEFORE you invoke
|
34
|
+
# {#has_friendly_id} in your class.
|
35
|
+
def protect_friendly_id_attributes
|
36
|
+
# only protect the column if the class is not already using attr_accessible
|
37
|
+
unless accessible_attributes.present?
|
38
|
+
if friendly_id_config.custom_cache_column?
|
39
|
+
attr_protected friendly_id_config.cache_column
|
40
|
+
end
|
41
|
+
attr_protected :cached_slug
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
require "friendly_id/active_record_adapter/relation"
|
49
|
+
require "friendly_id/active_record_adapter/configuration"
|
50
|
+
require "friendly_id/active_record_adapter/finders"
|
51
|
+
require "friendly_id/active_record_adapter/simple_model"
|
52
|
+
require "friendly_id/active_record_adapter/slugged_model"
|
53
|
+
require "friendly_id/active_record_adapter/slug"
|
54
|
+
require "friendly_id/active_record_adapter/tasks"
|
55
|
+
|
56
|
+
module ActiveRecord
|
57
|
+
class Base
|
58
|
+
extend FriendlyId::ActiveRecordAdapter
|
59
|
+
unless FriendlyId.on_ar3?
|
60
|
+
class << self
|
61
|
+
VALID_FIND_OPTIONS << :scope
|
62
|
+
VALID_FIND_OPTIONS << :locale
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
if defined? Relation
|
68
|
+
class Relation
|
69
|
+
alias find_one_without_friendly find_one
|
70
|
+
alias find_some_without_friendly find_some
|
71
|
+
include FriendlyId::ActiveRecordAdapter::Relation
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,68 @@
|
|
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 do |klass|
|
38
|
+
klass.friendly_id_config.scopes_over?(configured_class)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def custom_cache_column?
|
43
|
+
!! custom_cache_column
|
44
|
+
end
|
45
|
+
|
46
|
+
def scope_for(record)
|
47
|
+
return nil unless scope?
|
48
|
+
record.send(scope).nil? ? nil : record.send(scope).to_param
|
49
|
+
end
|
50
|
+
|
51
|
+
def scopes_over?(klass)
|
52
|
+
scope? && scope == klass.to_s.underscore.to_sym
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def autodiscover_cache_column
|
58
|
+
:cached_slug if configured_class.columns.any? { |column| column.name == 'cached_slug' }
|
59
|
+
end
|
60
|
+
|
61
|
+
def associated_friendly_classes
|
62
|
+
configured_class.reflect_on_all_associations.compact.select { |assoc|
|
63
|
+
!assoc.options[:polymorphic] && assoc.klass.respond_to?(:friendly_id_config)
|
64
|
+
}.map(&:klass)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
module ActiveRecordAdapter
|
3
|
+
module Finders
|
4
|
+
|
5
|
+
class Find
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators :@klass, :scoped, :friendly_id_config, :quoted_table_name, :table_name, :primary_key,
|
8
|
+
:connection, :name, :sanitize_sql
|
9
|
+
def_delegators :fc, :use_slugs?, :cache_column, :cache_column?
|
10
|
+
alias fc friendly_id_config
|
11
|
+
|
12
|
+
attr :klass
|
13
|
+
attr :id
|
14
|
+
attr :options
|
15
|
+
attr :result
|
16
|
+
attr :friendly_ids
|
17
|
+
attr :unfriendly_ids
|
18
|
+
|
19
|
+
def initialize(klass, id, options)
|
20
|
+
@klass = klass
|
21
|
+
@id = id
|
22
|
+
@options = options
|
23
|
+
if options[:scope]
|
24
|
+
raise "The :scope finder option has been removed from FriendlyId 3.2.0 " +
|
25
|
+
"https://github.com/norman/friendly_id/issues#issue/88"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def find_one
|
30
|
+
return find_one_with_cached_slug if !fc.scope? && cache_column?
|
31
|
+
return find_one_with_slug if use_slugs?
|
32
|
+
@result = scoped(:conditions => ["#{table_name}.#{fc.column} = ?", id]).first(options)
|
33
|
+
assign_status
|
34
|
+
end
|
35
|
+
|
36
|
+
def find_some
|
37
|
+
parse_ids!
|
38
|
+
scope = some_friendly_scope
|
39
|
+
if use_slugs? && @friendly_ids.present?
|
40
|
+
scope = scope.scoped(:joins => :slugs)
|
41
|
+
end
|
42
|
+
options[:readonly] = false unless options[:readonly]
|
43
|
+
@result = scope.all(options).uniq
|
44
|
+
validate_expected_size!
|
45
|
+
@result.each { |record| record.friendly_id_status.name = id }
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def find_one_with_cached_slug
|
51
|
+
@result = scoped(:conditions => ["#{table_name}.#{cache_column} = ?", id]).first(options)
|
52
|
+
assign_status or find_one_with_slug
|
53
|
+
end
|
54
|
+
|
55
|
+
def find_one_with_slug
|
56
|
+
name, seq = id.to_s.parse_friendly_id
|
57
|
+
scope = scoped(:joins => :slugs, :conditions => {:slugs => {:name => name, :sequence => seq}})
|
58
|
+
options[:readonly] = false unless options[:readonly]
|
59
|
+
@result = scope.first(options)
|
60
|
+
assign_status
|
61
|
+
end
|
62
|
+
|
63
|
+
def parse_ids!
|
64
|
+
@id = id.uniq.map do |member|
|
65
|
+
if member.respond_to?(:friendly_id_config)
|
66
|
+
member.id.to_i
|
67
|
+
else
|
68
|
+
member
|
69
|
+
end
|
70
|
+
end
|
71
|
+
@friendly_ids, @unfriendly_ids = @id.partition {|member| member.friendly_id?}
|
72
|
+
end
|
73
|
+
|
74
|
+
def validate_expected_size!
|
75
|
+
expected = expected_size
|
76
|
+
return if @result.size == expected
|
77
|
+
message = "Couldn't find all %s with IDs (%s) AND %s (found %d results, but was looking for %d)" % [
|
78
|
+
name.pluralize,
|
79
|
+
id.join(', '),
|
80
|
+
sanitize_sql(options[:conditions]),
|
81
|
+
result.size,
|
82
|
+
expected
|
83
|
+
]
|
84
|
+
raise ActiveRecord::RecordNotFound, message
|
85
|
+
end
|
86
|
+
|
87
|
+
def assign_status
|
88
|
+
return unless @result
|
89
|
+
name, seq = @id.to_s.parse_friendly_id
|
90
|
+
@result.friendly_id_status.name = name
|
91
|
+
@result.friendly_id_status.sequence = seq if use_slugs?
|
92
|
+
@result
|
93
|
+
end
|
94
|
+
|
95
|
+
def expected_size
|
96
|
+
if options[:limit] && @id.size > options[:limit]
|
97
|
+
options[:limit]
|
98
|
+
else
|
99
|
+
@id.size
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def some_friendly_scope
|
104
|
+
query_slugs = use_slugs? && !cache_column?
|
105
|
+
pkey = "#{quoted_table_name}.#{primary_key}"
|
106
|
+
column = "#{table_name}.#{cache_column || fc.column}"
|
107
|
+
if @unfriendly_ids.present?
|
108
|
+
conditions = ["#{pkey} IN (?)", @unfriendly_ids]
|
109
|
+
if @friendly_ids.present?
|
110
|
+
if query_slugs
|
111
|
+
conditions[0] << " OR #{some_slugged_conditions}"
|
112
|
+
else
|
113
|
+
conditions[0] << " OR #{column} IN (?)"
|
114
|
+
conditions << @friendly_ids
|
115
|
+
end
|
116
|
+
end
|
117
|
+
elsif @friendly_ids.present?
|
118
|
+
conditions = query_slugs ? some_slugged_conditions : ["#{column} IN (?)", @friendly_ids]
|
119
|
+
end
|
120
|
+
scoped(:conditions => conditions)
|
121
|
+
end
|
122
|
+
|
123
|
+
def some_slugged_conditions
|
124
|
+
return unless @friendly_ids.present?
|
125
|
+
slug_table = Slug.quoted_table_name
|
126
|
+
fragment = "(#{slug_table}.name = %s AND #{slug_table}.sequence = %d)"
|
127
|
+
@friendly_ids.inject(nil) do |clause, id|
|
128
|
+
name, seq = id.parse_friendly_id
|
129
|
+
string = fragment % [connection.quote(name), seq]
|
130
|
+
clause ? clause + " OR #{string}" : string
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def find_one(id, options)
|
136
|
+
return super if id.blank? || id.unfriendly_id?
|
137
|
+
finder = Find.new(self, id, options)
|
138
|
+
finder.find_one or super
|
139
|
+
end
|
140
|
+
|
141
|
+
def find_some(ids, options)
|
142
|
+
return super if ids.empty?
|
143
|
+
finder = Find.new(self, ids, options)
|
144
|
+
finder.find_some
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
module FriendlyId
|
2
|
+
module ActiveRecordAdapter
|
3
|
+
module Relation
|
4
|
+
|
5
|
+
class Find
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
attr :relation
|
9
|
+
attr :ids
|
10
|
+
alias id ids
|
11
|
+
|
12
|
+
def_delegators :relation, :arel, :arel_table, :klass, :limit_value, :offset_value, :where
|
13
|
+
def_delegators :klass, :connection, :friendly_id_config
|
14
|
+
alias fc friendly_id_config
|
15
|
+
|
16
|
+
def initialize(relation, ids)
|
17
|
+
@relation = relation
|
18
|
+
@ids = ids
|
19
|
+
end
|
20
|
+
|
21
|
+
def find_one
|
22
|
+
if fc.cache_column?
|
23
|
+
find_one_with_cached_slug
|
24
|
+
elsif fc.use_slugs?
|
25
|
+
find_one_with_slug
|
26
|
+
else
|
27
|
+
find_one_without_slug
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def find_some
|
32
|
+
ids = @ids.compact.uniq.map {|id| id.respond_to?(:friendly_id_config) ? id.id.to_i : id}
|
33
|
+
friendly_ids, unfriendly_ids = ids.partition {|id| id.friendly_id?}
|
34
|
+
return if friendly_ids.empty?
|
35
|
+
records = friendly_records(friendly_ids, unfriendly_ids).each do |record|
|
36
|
+
record.friendly_id_status.name = ids
|
37
|
+
end
|
38
|
+
validate_expected_size!(ids, records)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def assign_status
|
44
|
+
return unless @result
|
45
|
+
name, seq = id.to_s.parse_friendly_id
|
46
|
+
@result.friendly_id_status.name = name
|
47
|
+
@result.friendly_id_status.sequence = seq if fc.use_slugs?
|
48
|
+
@result
|
49
|
+
end
|
50
|
+
|
51
|
+
def find_one_without_slug
|
52
|
+
@result = where(fc.column => id).first
|
53
|
+
assign_status
|
54
|
+
end
|
55
|
+
|
56
|
+
def find_one_with_cached_slug
|
57
|
+
@result = where(fc.cache_column => id).first
|
58
|
+
assign_status or find_one_with_slug
|
59
|
+
end
|
60
|
+
|
61
|
+
def find_one_with_slug
|
62
|
+
sluggable_ids = sluggable_ids_for([id])
|
63
|
+
|
64
|
+
if sluggable_ids.size > 1 && fc.scope?
|
65
|
+
return relation.where(relation.primary_key.in(sluggable_ids)).first
|
66
|
+
end
|
67
|
+
|
68
|
+
sluggable_id = sluggable_ids.first
|
69
|
+
|
70
|
+
if sluggable_id
|
71
|
+
name, seq = id.to_s.parse_friendly_id
|
72
|
+
record = relation.send(:find_one_without_friendly, sluggable_id)
|
73
|
+
record.friendly_id_status.name = name
|
74
|
+
record.friendly_id_status.sequence = seq
|
75
|
+
record
|
76
|
+
else
|
77
|
+
relation.send(:find_one_without_friendly, id)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def friendly_records(friendly_ids, unfriendly_ids)
|
82
|
+
use_slugs_table = fc.use_slugs? && (!fc.cache_column?)
|
83
|
+
return find_some_using_slug(friendly_ids, unfriendly_ids) if use_slugs_table
|
84
|
+
column = fc.cache_column || fc.column
|
85
|
+
friendly = arel_table[column].in(friendly_ids)
|
86
|
+
unfriendly = arel_table[relation.primary_key.name].in unfriendly_ids
|
87
|
+
if friendly_ids.present? && unfriendly_ids.present?
|
88
|
+
where(friendly.or(unfriendly))
|
89
|
+
else
|
90
|
+
where(friendly)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def find_some_using_slug(friendly_ids, unfriendly_ids)
|
95
|
+
ids = [unfriendly_ids + sluggable_ids_for(friendly_ids)].flatten.uniq
|
96
|
+
where(arel_table[relation.primary_key.name].in(ids))
|
97
|
+
end
|
98
|
+
|
99
|
+
def sluggable_ids_for(ids)
|
100
|
+
return [] if ids.empty?
|
101
|
+
fragment = "(slugs.sluggable_type = %s AND slugs.name = %s AND slugs.sequence = %d)"
|
102
|
+
conditions = ids.inject(nil) do |clause, id|
|
103
|
+
name, seq = id.parse_friendly_id
|
104
|
+
string = fragment % [connection.quote(klass.base_class), connection.quote(name), seq]
|
105
|
+
clause ? clause + " OR #{string}" : string
|
106
|
+
end
|
107
|
+
sql = "SELECT sluggable_id FROM slugs WHERE (%s)" % conditions
|
108
|
+
connection.select_values sql
|
109
|
+
end
|
110
|
+
|
111
|
+
def validate_expected_size!(ids, result)
|
112
|
+
expected_size =
|
113
|
+
if limit_value && ids.size > limit_value
|
114
|
+
limit_value
|
115
|
+
else
|
116
|
+
ids.size
|
117
|
+
end
|
118
|
+
|
119
|
+
# 11 ids with limit 3, offset 9 should give 2 results.
|
120
|
+
if offset_value && (ids.size - offset_value < expected_size)
|
121
|
+
expected_size = ids.size - offset_value
|
122
|
+
end
|
123
|
+
|
124
|
+
if result.size == expected_size
|
125
|
+
result
|
126
|
+
else
|
127
|
+
conditions = arel.send(:where_clauses).join(', ')
|
128
|
+
conditions = " [WHERE #{conditions}]" if conditions.present?
|
129
|
+
error = "Couldn't find all #{klass.name.pluralize} with IDs "
|
130
|
+
error << "(#{ids.join(", ")})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
|
131
|
+
raise ActiveRecord::RecordNotFound, error
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def apply_finder_options(options)
|
137
|
+
if options[:scope]
|
138
|
+
raise "The :scope finder option has been removed from FriendlyId 3.2.0 " +
|
139
|
+
"https://github.com/norman/friendly_id/issues#issue/88"
|
140
|
+
end
|
141
|
+
super
|
142
|
+
end
|
143
|
+
|
144
|
+
protected
|
145
|
+
|
146
|
+
def find_one(id)
|
147
|
+
begin
|
148
|
+
return super if !klass.uses_friendly_id? or id.unfriendly_id?
|
149
|
+
find = Find.new(self, id)
|
150
|
+
find.find_one or super
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def find_some(ids)
|
155
|
+
return super unless klass.uses_friendly_id?
|
156
|
+
Find.new(self, ids).find_some or begin
|
157
|
+
# A change in Arel 2.0.x causes find_some to fail with arrays of instances; not sure why.
|
158
|
+
# This is an emergency, temporary fix.
|
159
|
+
ids = ids.map {|id| (id.respond_to?(:friendly_id_config) ? id.id : id).to_i}
|
160
|
+
super
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|