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.
Files changed (56) hide show
  1. data/Changelog.md +277 -0
  2. data/Contributors.md +39 -0
  3. data/Guide.md +561 -0
  4. data/LICENSE +19 -0
  5. data/README.md +83 -0
  6. data/Rakefile +66 -0
  7. data/extras/README.txt +3 -0
  8. data/extras/bench.rb +36 -0
  9. data/extras/extras.rb +38 -0
  10. data/extras/prof.rb +14 -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 +73 -0
  16. data/lib/friendly_id/active_record.rb +52 -0
  17. data/lib/friendly_id/active_record_adapter/configuration.rb +67 -0
  18. data/lib/friendly_id/active_record_adapter/finders.rb +156 -0
  19. data/lib/friendly_id/active_record_adapter/simple_model.rb +123 -0
  20. data/lib/friendly_id/active_record_adapter/slug.rb +66 -0
  21. data/lib/friendly_id/active_record_adapter/slugged_model.rb +238 -0
  22. data/lib/friendly_id/active_record_adapter/tasks.rb +69 -0
  23. data/lib/friendly_id/configuration.rb +113 -0
  24. data/lib/friendly_id/finders.rb +109 -0
  25. data/lib/friendly_id/railtie.rb +20 -0
  26. data/lib/friendly_id/sequel.rb +5 -0
  27. data/lib/friendly_id/slug_string.rb +391 -0
  28. data/lib/friendly_id/slugged.rb +102 -0
  29. data/lib/friendly_id/status.rb +35 -0
  30. data/lib/friendly_id/test.rb +291 -0
  31. data/lib/friendly_id/version.rb +9 -0
  32. data/lib/generators/friendly_id_generator.rb +25 -0
  33. data/lib/tasks/friendly_id.rake +19 -0
  34. data/rails/init.rb +2 -0
  35. data/test/active_record_adapter/ar_test_helper.rb +119 -0
  36. data/test/active_record_adapter/basic_slugged_model_test.rb +14 -0
  37. data/test/active_record_adapter/cached_slug_test.rb +61 -0
  38. data/test/active_record_adapter/core.rb +98 -0
  39. data/test/active_record_adapter/custom_normalizer_test.rb +20 -0
  40. data/test/active_record_adapter/custom_table_name_test.rb +22 -0
  41. data/test/active_record_adapter/scoped_model_test.rb +118 -0
  42. data/test/active_record_adapter/simple_test.rb +76 -0
  43. data/test/active_record_adapter/slug_test.rb +34 -0
  44. data/test/active_record_adapter/slugged.rb +30 -0
  45. data/test/active_record_adapter/slugged_status_test.rb +25 -0
  46. data/test/active_record_adapter/sti_test.rb +22 -0
  47. data/test/active_record_adapter/support/database.jdbcsqlite3.yml +2 -0
  48. data/test/active_record_adapter/support/database.mysql.yml +4 -0
  49. data/test/active_record_adapter/support/database.postgres.yml +6 -0
  50. data/test/active_record_adapter/support/database.sqlite3.yml +2 -0
  51. data/test/active_record_adapter/support/models.rb +87 -0
  52. data/test/active_record_adapter/tasks_test.rb +82 -0
  53. data/test/friendly_id_test.rb +55 -0
  54. data/test/slug_string_test.rb +88 -0
  55. data/test/test_helper.rb +15 -0
  56. metadata +168 -0
@@ -0,0 +1,238 @@
1
+ module FriendlyId
2
+ module ActiveRecordAdapter
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
+
11
+ def handle_friendly_result
12
+ raise ::ActiveRecord::RecordNotFound.new unless @result
13
+ @result.friendly_id_status.friendly_id = id
14
+ end
15
+
16
+ end
17
+
18
+ class MultipleFinder
19
+
20
+ include FriendlyId::ActiveRecordAdapter::Finders::Multiple
21
+ include SluggedFinder
22
+
23
+ attr_reader :slugs
24
+
25
+ def find
26
+ @results = model_class.scoped(find_options).all(options).uniq
27
+ raise ::ActiveRecord::RecordNotFound, error_message if @results.size != expected_size
28
+ @results.each {|result| result.friendly_id_status.name = slug_for(result)}
29
+ end
30
+
31
+ private
32
+
33
+ def find_conditions
34
+ "%s IN (%s)" % [
35
+ "#{quoted_table_name}.#{primary_key}",
36
+ (unfriendly_ids + sluggable_ids).join(",")
37
+ ]
38
+ end
39
+
40
+ def find_options
41
+ {:select => "#{quoted_table_name}.*", :conditions => find_conditions,
42
+ :joins => slugs_included? ? options[:joins] : :slugs}
43
+ end
44
+
45
+ def sluggable_ids
46
+ @sluggable_ids ||= slugs.map(&:sluggable_id)
47
+ end
48
+
49
+ def slugs
50
+ @slugs ||= friendly_ids.map do |friendly_id|
51
+ name, sequence = friendly_id.parse_friendly_id(friendly_id_config.sequence_separator)
52
+ Slug.first :conditions => {
53
+ :name => name,
54
+ :scope => scope,
55
+ :sequence => sequence,
56
+ :sluggable_type => base_class.name
57
+ }
58
+ end.compact
59
+ end
60
+
61
+ def slug_for(result)
62
+ slugs.detect {|slug| result.id == slug.sluggable_id}
63
+ end
64
+
65
+ end
66
+
67
+ # Performs a find a single friendly_id using the cached_slug column,
68
+ # if available. This is significantly faster, and can be used in all
69
+ # circumstances unless the +:scope+ argument is present.
70
+ class CachedMultipleFinder < SimpleModel::MultipleFinder
71
+ # The column used to store the cached slug.
72
+ def column
73
+ "#{table_name}.#{friendly_id_config.cache_column}"
74
+ end
75
+ end
76
+
77
+ class SingleFinder
78
+
79
+ include FriendlyId::Finders::Base
80
+ include FriendlyId::Finders::Single
81
+ include SluggedFinder
82
+
83
+ def find
84
+ @result = model_class.scoped(find_options).first(options)
85
+ handle_friendly_result if friendly?
86
+ @result
87
+ rescue ::ActiveRecord::RecordNotFound => @error
88
+ friendly_id_config.scope? ? raise_scoped_error : (raise @error)
89
+ end
90
+
91
+ private
92
+
93
+ def find_options
94
+ slug_table = Slug.table_name
95
+ {
96
+ :select => "#{model_class.quoted_table_name}.*",
97
+ :joins => slugs_included? ? options[:joins] : :slugs,
98
+ :conditions => {
99
+ "#{slug_table}.name" => name,
100
+ "#{slug_table}.scope" => scope,
101
+ "#{slug_table}.sequence" => sequence
102
+ }
103
+ }
104
+ end
105
+
106
+ def raise_scoped_error
107
+ scope_message = scope || "expected, but none given"
108
+ message = "%s, scope: %s" % [@error.message, scope_message]
109
+ raise ::ActiveRecord::RecordNotFound, message
110
+ end
111
+
112
+ end
113
+
114
+ # Performs a find for multiple friendly_ids using the cached_slug column,
115
+ # if available. This is significantly faster, and can be used in all
116
+ # circumstances unless the +:scope+ argument is present.
117
+ class CachedSingleFinder < SimpleModel::SingleFinder
118
+
119
+ include SluggedFinder
120
+
121
+ def find
122
+ @result = model_class.scoped(find_options).first(options)
123
+ handle_friendly_result if friendly?
124
+ @result
125
+ rescue ActiveRecord::RecordNotFound
126
+ SingleFinder.new(id, model_class, options).find
127
+ end
128
+
129
+ # The column used to store the cached slug.
130
+ def column
131
+ "#{table_name}.#{friendly_id_config.cache_column}"
132
+ end
133
+
134
+ end
135
+
136
+ def self.included(base)
137
+ base.class_eval do
138
+ has_many :slugs, :order => 'id DESC', :as => :sluggable, :dependent => :destroy
139
+ before_create :build_slug
140
+ after_create :set_slug_cache
141
+ after_update :update_scope
142
+ after_update :update_dependent_scopes
143
+ after_update :update_slug_from_cache
144
+ protect_friendly_id_attributes
145
+ extend FriendlyId::ActiveRecordAdapter::FinderMethods
146
+ end
147
+ end
148
+
149
+ include FriendlyId::Slugged::Model
150
+
151
+ def find_slug(name, sequence)
152
+ slugs.find_by_name_and_sequence(name, sequence)
153
+ end
154
+
155
+ # The model instance's current {FriendlyId::ActiveRecordAdapter::Slug slug}.
156
+ def slug
157
+ return @slug if new_record?
158
+ @slug ||= slugs.first(:order => "id DESC")
159
+ end
160
+
161
+ # Set the slug.
162
+ def slug=(slug)
163
+ @new_friendly_id = slug.to_friendly_id unless slug.nil?
164
+ super
165
+ end
166
+
167
+ # Returns the friendly id, or if none is available, the numeric id.
168
+ def to_param
169
+ friendly_id_config.cache_column ? to_param_from_cache : to_param_from_slug
170
+ end
171
+
172
+ private
173
+
174
+ def scope_changed?
175
+ friendly_id_config.scope? && send(friendly_id_config.scope).to_param != slug.scope
176
+ end
177
+
178
+ # Respond with the cached value if available.
179
+ def to_param_from_cache
180
+ read_attribute(friendly_id_config.cache_column) || id.to_s
181
+ end
182
+
183
+ # Respond with the slugged value if available.
184
+ def to_param_from_slug
185
+ slug? ? slug.to_friendly_id : id.to_s
186
+ end
187
+
188
+ # Build the new slug using the generated friendly id.
189
+ def build_slug
190
+ return unless new_slug_needed?
191
+ self.slug = slugs.build :name => slug_text.to_s, :scope => friendly_id_config.scope_for(self)
192
+ end
193
+
194
+ # Reset the cached friendly_id?
195
+ def new_cache_needed?
196
+ uses_slug_cache? && slug? && send(friendly_id_config.cache_column) != slug.to_friendly_id
197
+ end
198
+
199
+ # Reset the cached friendly_id.
200
+ def set_slug_cache
201
+ if new_cache_needed?
202
+ send "#{friendly_id_config.cache_column}=", slug.to_friendly_id
203
+ send :update_without_callbacks
204
+ end
205
+ end
206
+
207
+ def update_scope
208
+ return unless slug && scope_changed?
209
+ slug.update_attributes :scope => send(friendly_id_config.scope).to_param
210
+ rescue ActiveRecord::StatementInvalid
211
+ slug.update_attributes :sequence => Slug.similar_to(slug).first.sequence.succ
212
+ end
213
+
214
+ # Update the slugs for any model that is using this model as its
215
+ # FriendlyId scope.
216
+ def update_dependent_scopes
217
+ if slugs(true).size > 1 && @new_friendly_id
218
+ friendly_id_config.child_scopes.each do |klass|
219
+ Slug.update_all "scope = '#{@new_friendly_id}'", ["sluggable_type = ? AND scope = ?",
220
+ klass.to_s, slugs.second.to_friendly_id]
221
+ end
222
+ end
223
+ end
224
+
225
+ # Creates a new slug when the cached friendly_id is modified
226
+ def update_slug_from_cache
227
+ if uses_slug_cache? && send("#{friendly_id_config.cache_column}_changed?")
228
+ self.slug = slugs.create :name => send(friendly_id_config.cache_column).to_s, :scope => friendly_id_config.scope_for(self)
229
+ end
230
+ end
231
+ # Does the model use slug caching?
232
+ def uses_slug_cache?
233
+ friendly_id_config.cache_column?
234
+ end
235
+
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,69 @@
1
+ module FriendlyId
2
+ class TaskRunner
3
+
4
+ extend Forwardable
5
+
6
+ attr_accessor :days
7
+ attr_accessor :klass
8
+ attr_accessor :task_options
9
+
10
+ def_delegators :klass, :find, :friendly_id_config, :update_all
11
+
12
+ OLD_SLUG_DAYS = 45
13
+
14
+ def initialize(&block)
15
+ self.klass = ENV["MODEL"]
16
+ self.days = ENV["DAYS"]
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
+ cond = "slugs.id IS NULL"
30
+ options = {:limit => 100, :include => :slugs, :conditions => cond, :order => "#{klass.table_name}.id ASC"}.merge(task_options || {})
31
+ while records = find(:all, options) do
32
+ break if records.size == 0
33
+ records.each do |record|
34
+ record.save(:validate => false)
35
+ yield(record) if block_given?
36
+ end
37
+ options[:conditions] = cond + " and #{klass.table_name}.id > #{records.last.id}"
38
+ end
39
+ end
40
+
41
+ def delete_slugs
42
+ validate_uses_slugs
43
+ Slug.destroy_all(["sluggable_type = ?", klass.to_s])
44
+ if column = friendly_id_config.cache_column
45
+ update_all("#{column} = NULL")
46
+ end
47
+ end
48
+
49
+ def delete_old_slugs
50
+ conditions = ["created_at < ?", DateTime.now - days]
51
+ if klass
52
+ conditions[0] << " AND sluggable_type = ?"
53
+ conditions << klass.to_s
54
+ end
55
+ Slug.all(:conditions => conditions).select(&:outdated?).map(&:destroy)
56
+ end
57
+
58
+ def validate_uses_slugs
59
+ (raise "You need to pass a MODEL=<model name> argument to rake") if klass.blank?
60
+ unless friendly_id_config.use_slug?
61
+ raise "Class '%s' doesn't use slugs" % klass.to_s
62
+ end
63
+ rescue NoMethodError
64
+ raise "Class '%s' doesn't use FriendlyId" % klass.to_s
65
+ end
66
+
67
+ end
68
+
69
+ end
@@ -0,0 +1,113 @@
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
+ :allow_nil => false,
26
+ :ascii_approximation_options => [],
27
+ :max_length => 255,
28
+ :reserved_words => ["index", "new"],
29
+ :reserved_message => 'can not be "%s"',
30
+ :sequence_separator => "--"
31
+ }
32
+
33
+ # Whether to allow friendly_id and/or slugs to be nil. This is not
34
+ # generally useful on its own, but may allow you greater flexibility to
35
+ # customize your application.
36
+ attr_accessor :allow_nil
37
+ alias :allow_nil? :allow_nil
38
+
39
+ # Strip diacritics from Western characters.
40
+ attr_accessor :approximate_ascii
41
+
42
+ # Locale-type options for ASCII approximations. These can be any of the
43
+ # values supported by {SlugString#approximate_ascii!}.
44
+ attr_accessor :ascii_approximation_options
45
+
46
+ # The class that's using the configuration.
47
+ attr_reader :configured_class
48
+
49
+ # The maximum allowed length for a friendly_id string. This is checked *after* a
50
+ # string is processed by FriendlyId to remove spaces, special characters, etc.
51
+ attr_accessor :max_length
52
+
53
+ # The method or column that will be used as the basis of the friendly_id string.
54
+ attr_reader :method
55
+ alias :column :method
56
+
57
+ # The message shown when a reserved word is used.
58
+ # @see #reserved_words
59
+ attr_accessor :reserved_message
60
+
61
+ # Array of words that are reserved and can't be used as friendly_id strings.
62
+ # If a listed word is used in a sluggable model, it will raise a
63
+ # FriendlyId::SlugGenerationError. For Rails applications, you are recommended
64
+ # to include "index" and "new", which used as the defaults unless overridden.
65
+ attr_accessor :reserved_words
66
+
67
+ # The method or relation to use as the friendly_id's scope.
68
+ attr_accessor :scope
69
+
70
+ # The string that separates slug names from slug sequences. Defaults to "--".
71
+ attr_accessor :sequence_separator
72
+
73
+ # Strip non-ASCII characters from the friendly_id string.
74
+ attr_accessor :strip_non_ascii
75
+
76
+ # Use slugs for storing the friendly_id string.
77
+ attr_accessor :use_slug
78
+ alias :use_slugs= :use_slug
79
+
80
+ def initialize(configured_class, method, options = nil, &block)
81
+ @configured_class = configured_class
82
+ @method = method.to_sym
83
+ DEFAULTS.merge(options || {}).each do |key, value|
84
+ self.send "#{key}=".to_sym, value
85
+ end
86
+ yield self if block_given?
87
+ end
88
+
89
+ def reserved_words=(*words)
90
+ @reserved_words = words.flatten.uniq
91
+ end
92
+
93
+ def reserved?(word)
94
+ reserved_words.include? word.to_s
95
+ end
96
+
97
+ def reserved_error_message(word)
98
+ [method, reserved_message % word] if reserved? word
99
+ end
100
+
101
+ %w[approximate_ascii scope strip_non_ascii use_slug].each do |method|
102
+ class_eval(<<-EOM)
103
+ def #{method}?
104
+ !! #{method}
105
+ end
106
+ EOM
107
+ end
108
+
109
+ alias :use_slugs? :use_slug?
110
+
111
+ end
112
+
113
+ end
@@ -0,0 +1,109 @@
1
+ module FriendlyId
2
+
3
+ module Finders
4
+
5
+ module Base
6
+
7
+ extend Forwardable
8
+
9
+ def_delegators :model_class, :base_class, :friendly_id_config,
10
+ :primary_key, :quoted_table_name, :sanitize_sql, :table_name
11
+
12
+ # Is the id friendly or numeric? Not that the return value here is
13
+ # +false+ if the +id+ is definitely not friendly, and +nil+ if it can
14
+ # not be determined.
15
+ # The return value will be:
16
+ # * +true+ - if the id is definitely friendly (i.e., any string with non-numeric characters)
17
+ # * +false+ - if the id is definitely unfriendly (i.e., an Integer, a model instance, etc.)
18
+ # * +nil+ - if it can not be determined (i.e., a numeric string like "206".)
19
+ # @return [true, false, nil]
20
+ # @see #unfriendly?
21
+ def self.friendly?(id)
22
+ if id.is_a?(Integer) or id.is_a?(Symbol) or id.class.respond_to? :friendly_id_config
23
+ return false
24
+ elsif id.to_i.to_s != id.to_s
25
+ return true
26
+ else
27
+ return nil
28
+ end
29
+ end
30
+
31
+ # Is the id numeric?
32
+ # @return [true, false, nil] +true+ if definitely unfriendly, +false+ if
33
+ # definitely friendly, else +nil+.
34
+ # @see #friendly?
35
+ def self.unfriendly?(id)
36
+ !friendly?(id) unless friendly?(id) == nil
37
+ end
38
+
39
+ def initialize(ids, model_class, options={})
40
+ self.ids = ids
41
+ self.options = options
42
+ self.model_class = model_class
43
+ self.scope = options.delete :scope
44
+ end
45
+
46
+ # An array of ids; can be both friendly and unfriendly.
47
+ attr_accessor :ids
48
+
49
+ # The ActiveRecord query options
50
+ attr_accessor :options
51
+
52
+ # The FriendlyId scope
53
+ attr_accessor :scope
54
+
55
+ # The model class being used to perform the query.
56
+ attr_accessor :model_class
57
+
58
+ # Perform the find.
59
+ def find
60
+ raise NotImplementedError
61
+ end
62
+
63
+ private
64
+
65
+ def ids=(ids)
66
+ @ids = [ids].flatten
67
+ end
68
+ alias :id= :ids=
69
+
70
+ def scope=(scope)
71
+ unless scope.nil?
72
+ @scope = scope.respond_to?(:to_param) ? scope.to_param : scope.to_s
73
+ end
74
+ end
75
+ end
76
+
77
+ module Single
78
+ # Is the id definitely friendly?
79
+ # @see Finder::friendly?
80
+ def friendly?
81
+ Base.friendly?(id)
82
+ end
83
+
84
+ # Is the id definitely unfriendly?
85
+ # @see Finder::unfriendly?
86
+ def unfriendly?
87
+ Base.unfriendly?(id)
88
+ end
89
+
90
+ private
91
+
92
+ # The id (numeric or friendly).
93
+ def id
94
+ ids[0]
95
+ end
96
+
97
+ # The slug name; i.e. if "my-title--2", then "my-title".
98
+ def name
99
+ id.to_s.parse_friendly_id(friendly_id_config.sequence_separator)[0]
100
+ end
101
+
102
+ # The slug sequence; i.e. if "my-title--2", then "2".
103
+ def sequence
104
+ id.to_s.parse_friendly_id(friendly_id_config.sequence_separator)[1]
105
+ end
106
+ end
107
+
108
+ end
109
+ end