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.
Files changed (58) hide show
  1. data/Changelog.md +354 -0
  2. data/Contributors.md +43 -0
  3. data/Guide.md +686 -0
  4. data/MIT-LICENSE +19 -0
  5. data/README.md +99 -0
  6. data/Rakefile +75 -0
  7. data/extras/README.txt +3 -0
  8. data/extras/bench.rb +40 -0
  9. data/extras/extras.rb +38 -0
  10. data/extras/prof.rb +19 -0
  11. data/extras/template-gem.rb +26 -0
  12. data/extras/template-plugin.rb +28 -0
  13. data/generators/friendly_id/friendly_id_generator.rb +30 -0
  14. data/generators/friendly_id/templates/create_slugs.rb +18 -0
  15. data/lib/friendly_id.rb +93 -0
  16. data/lib/friendly_id/active_record.rb +74 -0
  17. data/lib/friendly_id/active_record_adapter/configuration.rb +68 -0
  18. data/lib/friendly_id/active_record_adapter/finders.rb +148 -0
  19. data/lib/friendly_id/active_record_adapter/relation.rb +165 -0
  20. data/lib/friendly_id/active_record_adapter/simple_model.rb +63 -0
  21. data/lib/friendly_id/active_record_adapter/slug.rb +77 -0
  22. data/lib/friendly_id/active_record_adapter/slugged_model.rb +122 -0
  23. data/lib/friendly_id/active_record_adapter/tasks.rb +72 -0
  24. data/lib/friendly_id/configuration.rb +178 -0
  25. data/lib/friendly_id/datamapper.rb +5 -0
  26. data/lib/friendly_id/railtie.rb +22 -0
  27. data/lib/friendly_id/sequel.rb +5 -0
  28. data/lib/friendly_id/slug_string.rb +25 -0
  29. data/lib/friendly_id/slugged.rb +105 -0
  30. data/lib/friendly_id/status.rb +35 -0
  31. data/lib/friendly_id/test.rb +350 -0
  32. data/lib/friendly_id/version.rb +9 -0
  33. data/lib/generators/friendly_id_generator.rb +25 -0
  34. data/lib/tasks/friendly_id.rake +19 -0
  35. data/rails/init.rb +2 -0
  36. data/test/active_record_adapter/ar_test_helper.rb +150 -0
  37. data/test/active_record_adapter/basic_slugged_model_test.rb +14 -0
  38. data/test/active_record_adapter/cached_slug_test.rb +76 -0
  39. data/test/active_record_adapter/core.rb +138 -0
  40. data/test/active_record_adapter/custom_normalizer_test.rb +20 -0
  41. data/test/active_record_adapter/custom_table_name_test.rb +22 -0
  42. data/test/active_record_adapter/default_scope_test.rb +30 -0
  43. data/test/active_record_adapter/optimistic_locking_test.rb +18 -0
  44. data/test/active_record_adapter/scoped_model_test.rb +119 -0
  45. data/test/active_record_adapter/simple_test.rb +76 -0
  46. data/test/active_record_adapter/slug_test.rb +34 -0
  47. data/test/active_record_adapter/slugged.rb +33 -0
  48. data/test/active_record_adapter/slugged_status_test.rb +28 -0
  49. data/test/active_record_adapter/sti_test.rb +22 -0
  50. data/test/active_record_adapter/support/database.jdbcsqlite3.yml +2 -0
  51. data/test/active_record_adapter/support/database.mysql.yml +4 -0
  52. data/test/active_record_adapter/support/database.postgres.yml +6 -0
  53. data/test/active_record_adapter/support/database.sqlite3.yml +2 -0
  54. data/test/active_record_adapter/support/models.rb +104 -0
  55. data/test/active_record_adapter/tasks_test.rb +82 -0
  56. data/test/friendly_id_test.rb +96 -0
  57. data/test/test_helper.rb +13 -0
  58. 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