friendly_id 2.2.7 → 2.3.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 (73) hide show
  1. data/Changelog.md +225 -0
  2. data/Contributors.md +28 -0
  3. data/Guide.md +509 -0
  4. data/LICENSE +1 -1
  5. data/README.md +76 -0
  6. data/Rakefile +48 -15
  7. data/extras/bench.rb +59 -0
  8. data/extras/extras.rb +31 -0
  9. data/extras/prof.rb +14 -0
  10. data/extras/template-gem.rb +1 -1
  11. data/extras/template-plugin.rb +1 -1
  12. data/generators/friendly_id/friendly_id_generator.rb +1 -1
  13. data/generators/friendly_id/templates/create_slugs.rb +2 -2
  14. data/lib/friendly_id.rb +54 -63
  15. data/lib/friendly_id/active_record2.rb +47 -0
  16. data/lib/friendly_id/active_record2/configuration.rb +66 -0
  17. data/lib/friendly_id/active_record2/finders.rb +140 -0
  18. data/lib/friendly_id/active_record2/simple_model.rb +162 -0
  19. data/lib/friendly_id/active_record2/slug.rb +111 -0
  20. data/lib/friendly_id/active_record2/slugged_model.rb +317 -0
  21. data/lib/friendly_id/active_record2/tasks.rb +66 -0
  22. data/lib/friendly_id/active_record2/tasks/friendly_id.rake +19 -0
  23. data/lib/friendly_id/configuration.rb +132 -0
  24. data/lib/friendly_id/finders.rb +106 -0
  25. data/lib/friendly_id/slug_string.rb +292 -0
  26. data/lib/friendly_id/slugged.rb +91 -0
  27. data/lib/friendly_id/status.rb +35 -0
  28. data/lib/friendly_id/test.rb +168 -0
  29. data/lib/friendly_id/version.rb +5 -5
  30. data/rails/init.rb +2 -0
  31. data/test/active_record2/basic_slugged_model_test.rb +14 -0
  32. data/test/active_record2/cached_slug_test.rb +61 -0
  33. data/test/active_record2/core.rb +93 -0
  34. data/test/active_record2/custom_normalizer_test.rb +20 -0
  35. data/test/active_record2/custom_table_name_test.rb +22 -0
  36. data/test/active_record2/scoped_model_test.rb +111 -0
  37. data/test/active_record2/simple_test.rb +59 -0
  38. data/test/active_record2/slug_test.rb +34 -0
  39. data/test/active_record2/slugged.rb +30 -0
  40. data/test/active_record2/slugged_status_test.rb +61 -0
  41. data/test/active_record2/sti_test.rb +22 -0
  42. data/test/active_record2/support/database.mysql.yml +4 -0
  43. data/test/{support/database.yml.postgres → active_record2/support/database.postgres.yml} +0 -0
  44. data/test/{support/database.yml.sqlite3 → active_record2/support/database.sqlite3.yml} +0 -0
  45. data/test/{support → active_record2/support}/models.rb +28 -0
  46. data/test/active_record2/tasks_test.rb +82 -0
  47. data/test/active_record2/test_helper.rb +107 -0
  48. data/test/friendly_id_test.rb +23 -0
  49. data/test/slug_string_test.rb +74 -0
  50. data/test/test_helper.rb +7 -102
  51. metadata +64 -56
  52. data/History.txt +0 -194
  53. data/README.rdoc +0 -385
  54. data/generators/friendly_id_20_upgrade/friendly_id_20_upgrade_generator.rb +0 -12
  55. data/generators/friendly_id_20_upgrade/templates/upgrade_friendly_id_to_20.rb +0 -19
  56. data/init.rb +0 -1
  57. data/lib/friendly_id/helpers.rb +0 -12
  58. data/lib/friendly_id/non_sluggable_class_methods.rb +0 -34
  59. data/lib/friendly_id/non_sluggable_instance_methods.rb +0 -45
  60. data/lib/friendly_id/slug.rb +0 -98
  61. data/lib/friendly_id/sluggable_class_methods.rb +0 -110
  62. data/lib/friendly_id/sluggable_instance_methods.rb +0 -161
  63. data/lib/friendly_id/tasks.rb +0 -56
  64. data/lib/tasks/friendly_id.rake +0 -25
  65. data/lib/tasks/friendly_id.rb +0 -1
  66. data/test/cached_slug_test.rb +0 -109
  67. data/test/custom_slug_normalizer_test.rb +0 -36
  68. data/test/non_slugged_test.rb +0 -99
  69. data/test/scoped_model_test.rb +0 -64
  70. data/test/slug_test.rb +0 -105
  71. data/test/slugged_model_test.rb +0 -348
  72. data/test/sti_test.rb +0 -49
  73. data/test/tasks_test.rb +0 -105
@@ -0,0 +1,317 @@
1
+ module FriendlyId
2
+ module ActiveRecord2
3
+ module SluggedModel
4
+
5
+ module SluggedFinder
6
+ # Whether :include => :slugs has been passed as an option.
7
+ def slugs_included?
8
+ [*(options[:include] or [])].flatten.include?(:slugs)
9
+ end
10
+ end
11
+
12
+ class MultipleFinder
13
+
14
+ include FriendlyId::Finders::Base
15
+ include FriendlyId::ActiveRecord2::Finders::Multiple
16
+ include SluggedFinder
17
+
18
+ attr_reader :slugs
19
+
20
+ def find
21
+ @results = with_scope(:find => find_options) { all options }.uniq
22
+ raise ::ActiveRecord::RecordNotFound, error_message if @results.size != expected_size
23
+ @results.each {|result| result.friendly_id_status.name = slug_for(result)}
24
+ end
25
+
26
+ private
27
+
28
+ def find_conditions
29
+ slugs
30
+ # [unfriendly_find_conditions, friendly_find_conditions].compact.join(" OR ")
31
+ ids = (unfriendly_ids + sluggable_ids).join(",")
32
+ "%s IN (%s)" % ["#{quoted_table_name}.#{primary_key}", ids]
33
+ end
34
+
35
+ def friendly_find_conditions
36
+ "slugs.id IN (%s)" % slugs.compact.to_s(:db) if slugs?
37
+ end
38
+
39
+ def find_options
40
+ {:select => "#{table_name}.*", :conditions => find_conditions,
41
+ :joins => slugs_included? ? options[:joins] : :slugs}
42
+ end
43
+
44
+ def sluggable_ids
45
+ if !@sluggable_ids
46
+ @sluggable_ids ||= []
47
+ slugs
48
+ end
49
+ @sluggable_ids
50
+ end
51
+
52
+ def slugs
53
+ @sluggable_ids ||= []
54
+ @slugs ||= friendly_ids.map do |friendly_id|
55
+ name, sequence = friendly_id.parse_friendly_id(friendly_id_config.sequence_separator)
56
+ slug = Slug.first :conditions => {
57
+ :name => name,
58
+ :scope => scope,
59
+ :sequence => sequence,
60
+ :sluggable_type => base_class.name
61
+ }
62
+ sluggable_ids << slug.sluggable_id if slug
63
+ slug
64
+ end
65
+ end
66
+
67
+ def slugs?
68
+ !slugs.empty?
69
+ end
70
+
71
+ def slug_for(result)
72
+ slugs.select {|slug| result.id == slug.sluggable_id}.first
73
+ end
74
+
75
+ def unfriendly_find_conditions
76
+ "%s IN (%s)" % ["#{quoted_table_name}.#{primary_key}", unfriendly_ids.join(",")] if unfriendly_ids?
77
+ end
78
+
79
+ def unfriendly_ids?
80
+ ! unfriendly_ids.empty?
81
+ end
82
+
83
+ end
84
+
85
+ # Performs a find a single friendly_id using the cached_slug column,
86
+ # if available. This is significantly faster, and can be used in all
87
+ # circumstances unless the +:scope+ argument is present.
88
+ class CachedMultipleFinder < SimpleModel::MultipleFinder
89
+ # The column used to store the cached slug.
90
+ def column
91
+ "#{table_name}.#{friendly_id_config.cache_column}"
92
+ end
93
+ end
94
+
95
+ class SingleFinder
96
+
97
+ include FriendlyId::Finders::Base
98
+ include FriendlyId::Finders::Single
99
+ include SluggedFinder
100
+
101
+ def find
102
+ result = with_scope({:find => find_options}) { find_initial options }
103
+ raise ::ActiveRecord::RecordNotFound.new if friendly? and !result
104
+ result.friendly_id_status.name = name if result
105
+ result
106
+ rescue ::ActiveRecord::RecordNotFound => @error
107
+ friendly_id_config.scope? ? raise_scoped_error : (raise @error)
108
+ end
109
+
110
+ private
111
+
112
+ def find_options
113
+ slug_table = Slug.table_name
114
+ {
115
+ :select => "#{model_class.table_name}.*",
116
+ :joins => slugs_included? ? options[:joins] : :slugs,
117
+ :conditions => {
118
+ "#{slug_table}.name" => name,
119
+ "#{slug_table}.scope" => scope,
120
+ "#{slug_table}.sequence" => sequence
121
+ }
122
+ }
123
+ end
124
+
125
+ def raise_scoped_error
126
+ scope_message = options[:scope] || "expected, but none given"
127
+ message = "%s, scope: %s" % [@error.message, scope_message]
128
+ raise ::ActiveRecord::RecordNotFound, message
129
+ end
130
+
131
+ end
132
+
133
+ # Performs a find for multiple friendly_ids using the cached_slug column,
134
+ # if available. This is significantly faster, and can be used in all
135
+ # circumstances unless the +:scope+ argument is present.
136
+ class CachedSingleFinder < SimpleModel::SingleFinder
137
+
138
+ # The column used to store the cached slug.
139
+ def column
140
+ "#{table_name}.#{friendly_id_config.cache_column}"
141
+ end
142
+
143
+ end
144
+
145
+ # The methods in this module override ActiveRecord's +find_one+ and
146
+ # +find_some+ to add FriendlyId's features.
147
+ module FinderMethods
148
+
149
+ protected
150
+
151
+ def find_one(id_or_name, options)
152
+ finder = Finders::FinderProxy.new(id_or_name, self, options)
153
+ finder.unfriendly? ? super : finder.find or super
154
+ end
155
+
156
+ def find_some(ids_and_names, options)
157
+ Finders::FinderProxy.new(ids_and_names, self, options).find
158
+ end
159
+
160
+ # Since Rails goes out of its way to make these options completely
161
+ # inaccessible, we have to copy them here.
162
+ def validate_find_options(options)
163
+ options.assert_valid_keys([:conditions, :include, :joins, :limit, :offset,
164
+ :order, :select, :readonly, :group, :from, :lock, :having, :scope])
165
+ end
166
+
167
+ end
168
+
169
+ # These methods will be removed in FriendlyId 3.0.
170
+ module DeprecatedMethods
171
+
172
+ # @deprecated Please use #to_param
173
+ def best_id
174
+ warn("best_id is deprecated and will be removed in 3.0. Please use #to_param.")
175
+ to_param
176
+ end
177
+
178
+ # @deprecated Please use #friendly_id_status.slug.
179
+ def finder_slug
180
+ warn("finder_slug is deprecated and will be removed in 3.0. Please use #friendly_id_status.slug.")
181
+ friendly_id_status.slug
182
+ end
183
+
184
+ # Was the record found using one of its friendly ids?
185
+ # @deprecated Please use #friendly_id_status.friendly?
186
+ def found_using_friendly_id?
187
+ warn("found_using_friendly_id? is deprecated and will be removed in 3.0. Please use #friendly_id_status.friendly?")
188
+ friendly_id_status.friendly?
189
+ end
190
+
191
+ # Was the record found using its numeric id?
192
+ # @deprecated Please use #friendly_id_status.numeric?
193
+ def found_using_numeric_id?
194
+ warn("found_using_numeric_id is deprecated and will be removed in 3.0. Please use #friendly_id_status.numeric?")
195
+ friendly_id_status.numeric?
196
+ end
197
+
198
+ # Was the record found using an old friendly id?
199
+ # @deprecated Please use #friendly_id_status.outdated?
200
+ def found_using_outdated_friendly_id?
201
+ warn("found_using_outdated_friendly_id is deprecated and will be removed in 3.0. Please use #friendly_id_status.outdated?")
202
+ friendly_id_status.outdated?
203
+ end
204
+
205
+ # Was the record found using an old friendly id, or its numeric id?
206
+ # @deprecated Please use !#friendly_id_status.best?
207
+ def has_better_id?
208
+ warn("has_better_id? is deprecated and will be removed in 3.0. Please use !#friendly_id_status.best?")
209
+ ! friendly_id_status.best?
210
+ end
211
+
212
+ # @deprecated Please use #slug?
213
+ def has_a_slug?
214
+ warn("has_a_slug? is deprecated and will be removed in 3.0. Please use #slug?")
215
+ slug?
216
+ end
217
+
218
+ end
219
+
220
+ def self.included(base)
221
+ base.class_eval do
222
+ has_many :slugs, :order => 'id DESC', :as => :sluggable, :dependent => :destroy
223
+ before_save :build_slug
224
+ after_save :set_slug_cache
225
+ after_update :update_scope
226
+ after_update :update_dependent_scopes
227
+ protect_friendly_id_attributes
228
+ extend FinderMethods
229
+ end
230
+ end
231
+
232
+ include FriendlyId::Slugged::Model
233
+ include DeprecatedMethods
234
+
235
+ def find_slug(name)
236
+ separator = friendly_id_config.sequence_separator
237
+ slugs.find_by_name_and_sequence(*name.to_s.parse_friendly_id(separator))
238
+ end
239
+
240
+ # The model instance's current {FriendlyId::ActiveRecord2::Slug slug}.
241
+ def slug
242
+ return @slug if new_record?
243
+ @slug ||= slugs.first(:order => "id DESC")
244
+ end
245
+
246
+ # Set the slug.
247
+ def slug=(slug)
248
+ @new_friendly_id = slug.to_friendly_id unless slug.nil?
249
+ super
250
+ end
251
+
252
+ # Returns the friendly id, or if none is available, the numeric id.
253
+ def to_param
254
+ friendly_id_config.cache_column ? to_param_from_cache : to_param_from_slug
255
+ end
256
+
257
+ private
258
+
259
+ def scope_changed?
260
+ friendly_id_config.scope? && send(friendly_id_config.scope).to_param != slug.scope
261
+ end
262
+
263
+ # Respond with the cached value if available.
264
+ def to_param_from_cache
265
+ read_attribute(friendly_id_config.cache_column) || id.to_s
266
+ end
267
+
268
+ # Respond with the slugged value if available.
269
+ def to_param_from_slug
270
+ slug? ? slug.to_friendly_id : id.to_s
271
+ end
272
+
273
+ # Build the new slug using the generated friendly id.
274
+ def build_slug
275
+ return unless new_slug_needed?
276
+ self.slug = slugs.build :name => slug_text.to_s, :scope => friendly_id_config.scope_for(self)
277
+ end
278
+
279
+ # Reset the cached friendly_id?
280
+ def new_cache_needed?
281
+ uses_slug_cache? && send(friendly_id_config.cache_column) != slug.to_friendly_id
282
+ end
283
+
284
+ # Reset the cached friendly_id.
285
+ def set_slug_cache
286
+ if new_cache_needed?
287
+ send "#{friendly_id_config.cache_column}=", slug.to_friendly_id
288
+ send :update_without_callbacks
289
+ end
290
+ end
291
+
292
+ def update_scope
293
+ return unless scope_changed?
294
+ slug.update_attributes :scope => send(friendly_id_config.scope).to_param
295
+ rescue ActiveRecord::StatementInvalid
296
+ slug.update_attributes :sequence => Slug.similar_to(slug).first.sequence.succ
297
+ end
298
+
299
+ # Update the slugs for any model that is using this model as its
300
+ # FriendlyId scope.
301
+ def update_dependent_scopes
302
+ if slugs(true).size > 1 && @new_friendly_id
303
+ friendly_id_config.child_scopes.each do |klass|
304
+ Slug.update_all "scope = '#{@new_friendly_id}'", ["sluggable_type = ? AND scope = ?",
305
+ klass.to_s, slugs.second.to_friendly_id]
306
+ end
307
+ end
308
+ end
309
+
310
+ # Does the model use slug caching?
311
+ def uses_slug_cache?
312
+ friendly_id_config.cache_column?
313
+ end
314
+
315
+ end
316
+ end
317
+ end
@@ -0,0 +1,66 @@
1
+ module FriendlyId
2
+ class TaskRunner
3
+
4
+ attr_accessor :days
5
+ attr_accessor :klass
6
+ attr_accessor :task_options
7
+
8
+ OLD_SLUG_DAYS = 45
9
+
10
+ def initialize(&block)
11
+ self.klass = ENV["MODEL"]
12
+ self.days = ENV["DAYS"]
13
+ end
14
+
15
+ def method_missing(*args)
16
+ klass.send(*args)
17
+ end
18
+
19
+ def days=(days)
20
+ @days = days.blank? ? OLD_SLUG_DAYS : days.to_i
21
+ end
22
+
23
+ def klass=(klass)
24
+ @klass = klass.to_s.classify.constantize unless klass.blank?
25
+ end
26
+
27
+ def make_slugs
28
+ validate_uses_slugs
29
+ options = {:limit => 100, :include => :slugs, :conditions => "slugs.id IS NULL"}.merge(task_options || {})
30
+ while records = find(:all, options) do
31
+ break if records.size == 0
32
+ records.each do |record|
33
+ record.save!
34
+ yield(record) if block_given?
35
+ end
36
+ end
37
+ end
38
+
39
+ def delete_slugs
40
+ validate_uses_slugs
41
+ Slug.destroy_all(["sluggable_type = ?", klass.to_s])
42
+ if column = friendly_id_config.cache_column
43
+ update_all("#{column} = NULL")
44
+ end
45
+ end
46
+
47
+ def delete_old_slugs
48
+ conditions = ["created_at < ?", DateTime.now - days.days]
49
+ if klass
50
+ conditions[0] << " AND sluggable_type = ?"
51
+ conditions << klass.to_s
52
+ end
53
+ Slug.all(:conditions => conditions).select(&:outdated?).map(&:destroy)
54
+ end
55
+
56
+ def validate_uses_slugs
57
+ unless friendly_id_config.use_slug?
58
+ raise "Class '%s' doesn't use slugs" % klass.to_s
59
+ end
60
+ rescue NoMethodError
61
+ raise "Class '%s' doesn't use FriendlyId" % klass.to_s
62
+ end
63
+
64
+ end
65
+
66
+ end
@@ -0,0 +1,19 @@
1
+ namespace :friendly_id do
2
+ desc "Make slugs for a model."
3
+ task :make_slugs => :environment do
4
+ FriendlyId::TaskRunner.new.make_slugs do |record|
5
+ puts "%s(%d): friendly_id set to '%s'" % [record.class.to_s, record.id, record.slug.name]
6
+ end
7
+ end
8
+
9
+ desc "Regenereate slugs for a model."
10
+ task :redo_slugs => :environment do
11
+ FriendlyId::TaskRunner.new.delete_slugs
12
+ Rake::Task["friendly_id:make_slugs"].invoke
13
+ end
14
+
15
+ desc "Kill obsolete slugs older than DAYS=45 days."
16
+ task :remove_old_slugs => :environment do
17
+ FriendlyId::TaskRunner.new.delete_old_slugs
18
+ end
19
+ end
@@ -0,0 +1,132 @@
1
+ module FriendlyId
2
+
3
+ # This class is not intended to be used on its own, it is used internally
4
+ # by `has_friendly_id` to store a model's configuration and
5
+ # configuration-related methods.
6
+ #
7
+ # The arguments accepted by +has_friendly_id+ correspond to the writeable
8
+ # instance attributes of this class; please see the description of the
9
+ # attributes below for information on the possible options.
10
+ #
11
+ # @example
12
+ # has_friendly_id :name,
13
+ # :use_slug => true,
14
+ # :max_length => 150,
15
+ # :approximate_ascii => true,
16
+ # :ascii_approximation_options => :german,
17
+ # :sequence_separator => ":",
18
+ # :reserved_words => ["reserved", "words"],
19
+ # :scope => :country,
20
+ # :cache_column => :my_cache_column_name
21
+ # # etc.
22
+ class Configuration
23
+
24
+ DEFAULTS = {
25
+ :ascii_approximation_options => [],
26
+ :max_length => 255,
27
+ :reserved_words => ["index", "new"],
28
+ :reserved_message => 'can not be "%s"',
29
+ :sequence_separator => "--"
30
+ }
31
+
32
+ # Strip diacritics from Western characters.
33
+ attr_accessor :approximate_ascii
34
+
35
+ # Locale-type options for ASCII approximations. These can be any of the
36
+ # values supported by {SlugString#approximate_ascii!}.
37
+ attr_accessor :ascii_approximation_options
38
+
39
+ # The class that's using the configuration.
40
+ attr_reader :configured_class
41
+
42
+ # The maximum allowed length for a friendly_id string. This is checked *after* a
43
+ # string is processed by FriendlyId to remove spaces, special characters, etc.
44
+ attr_accessor :max_length
45
+
46
+ # The method or column that will be used as the basis of the friendly_id string.
47
+ attr_reader :method
48
+ alias :column :method
49
+
50
+ # A block or proc through which to filter the friendly_id text.
51
+ # This method will be removed from FriendlyId 3.0.
52
+ # @deprecated Please override the +normalize_friendly_id+
53
+ # method in your model class rather than passing a block to `has_friendly_id`.
54
+ attr_accessor :normalizer
55
+
56
+ # The message shown when a reserved word is used.
57
+ # @see #reserved_words
58
+ attr_accessor :reserved_message
59
+
60
+ # Array of words that are reserved and can't be used as friendly_id strings.
61
+ # If a listed word is used in a sluggable model, it will raise a
62
+ # FriendlyId::SlugGenerationError. For Rails applications, you are recommended
63
+ # to include "index" and "new", which used as the defaults unless overridden.
64
+ attr_accessor :reserved_words
65
+
66
+ # The method or relation to use as the friendly_id's scope.
67
+ attr_accessor :scope
68
+
69
+ # The string that separates slug names from slug sequences. Defaults to "--".
70
+ attr_accessor :sequence_separator
71
+
72
+ # Strip non-ASCII characters from the friendly_id string.
73
+ attr_accessor :strip_non_ascii
74
+
75
+ # Use slugs for storing the friendly_id string.
76
+ attr_accessor :use_slug
77
+ alias :use_slugs= :use_slug
78
+
79
+ def initialize(configured_class, method, options = nil, &block)
80
+ @configured_class = configured_class
81
+ @method = method.to_sym
82
+ DEFAULTS.merge(options || {}).each do |key, value|
83
+ self.send "#{key}=".to_sym, value
84
+ end
85
+ yield self if block_given?
86
+ end
87
+
88
+ def normalizer=(arg)
89
+ return if arg.nil?
90
+ raise("passing a block to has_friendly_id is deprecated and will be removed from 3.0. Please override #friendly_id_normalizer.")
91
+ @normalizer = arg
92
+ end
93
+
94
+ def reserved_words=(*words)
95
+ @reserved_words = words.flatten.uniq
96
+ end
97
+
98
+ def reserved?(word)
99
+ reserved_words.include? word.to_s
100
+ end
101
+
102
+ def reserved_error_message(word)
103
+ [method, reserved_message % word] if reserved? word
104
+ end
105
+
106
+ # This method will be removed from FriendlyId 3.0.
107
+ # @deprecated Please use {#reserved_words reserved_words}.
108
+ def reserved=(*args)
109
+ warn('The "reserved" option is deprecated and will be removed from FriendlyId 3.0. Please use "reserved_words".')
110
+ self.reserved_words = *args
111
+ end
112
+
113
+ # This method will be removed from FriendlyId 3.0.
114
+ # @deprecated Please use {#approximate_ascii approximate_ascii}.
115
+ def strip_diacritics=(*args)
116
+ warn('strip_diacritics is deprecated and will be removed from 3.0. Please use #approximate_ascii')
117
+ self.strip_diacritics = *args
118
+ end
119
+
120
+ %w[approximate_ascii normalizer scope strip_non_ascii use_slug].each do |method|
121
+ class_eval(<<-EOM)
122
+ def #{method}?
123
+ !! #{method}
124
+ end
125
+ EOM
126
+ end
127
+
128
+ alias :use_slugs? :use_slug?
129
+
130
+ end
131
+
132
+ end