mil_friendly_id 4.0.9.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/.gitignore +12 -0
  4. data/.travis.yml +20 -0
  5. data/.yardopts +4 -0
  6. data/Changelog.md +86 -0
  7. data/Gemfile +15 -0
  8. data/Guide.rdoc +553 -0
  9. data/MIT-LICENSE +19 -0
  10. data/README.md +150 -0
  11. data/Rakefile +108 -0
  12. data/WhatsNew.md +95 -0
  13. data/bench.rb +63 -0
  14. data/friendly_id.gemspec +43 -0
  15. data/gemfiles/Gemfile.rails-3.0.rb +21 -0
  16. data/gemfiles/Gemfile.rails-3.1.rb +22 -0
  17. data/gemfiles/Gemfile.rails-3.2.rb +22 -0
  18. data/geothird_friendly_id.gemspec +45 -0
  19. data/lib/friendly_id.rb +114 -0
  20. data/lib/friendly_id/base.rb +291 -0
  21. data/lib/friendly_id/configuration.rb +80 -0
  22. data/lib/friendly_id/finder_methods.rb +35 -0
  23. data/lib/friendly_id/globalize.rb +115 -0
  24. data/lib/friendly_id/history.rb +134 -0
  25. data/lib/friendly_id/migration.rb +19 -0
  26. data/lib/friendly_id/object_utils.rb +50 -0
  27. data/lib/friendly_id/reserved.rb +68 -0
  28. data/lib/friendly_id/scoped.rb +149 -0
  29. data/lib/friendly_id/simple_i18n.rb +95 -0
  30. data/lib/friendly_id/slug.rb +14 -0
  31. data/lib/friendly_id/slug_generator.rb +80 -0
  32. data/lib/friendly_id/slugged.rb +329 -0
  33. data/lib/generators/friendly_id_generator.rb +17 -0
  34. data/mil_friendly_id.gemspec +45 -0
  35. data/test/base_test.rb +72 -0
  36. data/test/compatibility/ancestry/Gemfile +8 -0
  37. data/test/compatibility/ancestry/ancestry_test.rb +34 -0
  38. data/test/compatibility/threading/Gemfile +8 -0
  39. data/test/compatibility/threading/threading.rb +45 -0
  40. data/test/configuration_test.rb +48 -0
  41. data/test/core_test.rb +48 -0
  42. data/test/databases.yml +19 -0
  43. data/test/generator_test.rb +20 -0
  44. data/test/globalize_test.rb +57 -0
  45. data/test/helper.rb +87 -0
  46. data/test/history_test.rb +149 -0
  47. data/test/object_utils_test.rb +28 -0
  48. data/test/reserved_test.rb +40 -0
  49. data/test/schema.rb +79 -0
  50. data/test/scoped_test.rb +83 -0
  51. data/test/shared.rb +156 -0
  52. data/test/simple_i18n_test.rb +133 -0
  53. data/test/slugged_test.rb +280 -0
  54. data/test/sti_test.rb +77 -0
  55. metadata +262 -0
@@ -0,0 +1,149 @@
1
+ require "friendly_id/slugged"
2
+
3
+ module FriendlyId
4
+
5
+ =begin
6
+
7
+ == Unique Slugs by Scope
8
+
9
+ The {FriendlyId::Scoped} module allows FriendlyId to generate unique slugs
10
+ within a scope.
11
+
12
+ This allows, for example, two restaurants in different cities to have the slug
13
+ +joes-diner+:
14
+
15
+ class Restaurant < ActiveRecord::Base
16
+ extend FriendlyId
17
+ belongs_to :city
18
+ friendly_id :name, :use => :scoped, :scope => :city
19
+ end
20
+
21
+ class City < ActiveRecord::Base
22
+ extend FriendlyId
23
+ has_many :restaurants
24
+ friendly_id :name, :use => :slugged
25
+ end
26
+
27
+ City.find("seattle").restaurants.find("joes-diner")
28
+ City.find("chicago").restaurants.find("joes-diner")
29
+
30
+ Without :scoped in this case, one of the restaurants would have the slug
31
+ +joes-diner+ and the other would have +joes-diner--2+.
32
+
33
+ The value for the +:scope+ option can be the name of a +belongs_to+ relation, or
34
+ a column.
35
+
36
+ === Finding Records by Friendly ID
37
+
38
+ If you are using scopes your friendly ids may not be unique, so a simple find
39
+ like
40
+
41
+ Restaurant.find("joes-diner")
42
+
43
+ may return the wrong record. In these cases it's best to query through the
44
+ relation:
45
+
46
+ @city.restaurants.find("joes-diner")
47
+
48
+ Alternatively, you could pass the scope value as a query parameter:
49
+
50
+ Restaurant.find("joes-diner").where(:city_id => @city.id)
51
+
52
+
53
+ === Finding All Records That Match a Scoped ID
54
+
55
+ Query the slug column directly:
56
+
57
+ Restaurant.find_all_by_slug("joes-diner")
58
+
59
+ === Routes for Scoped Models
60
+
61
+ Recall that FriendlyId is a database-centric library, and does not set up any
62
+ routes for scoped models. You must do this yourself in your application. Here's
63
+ an example of one way to set this up:
64
+
65
+ # in routes.rb
66
+ resources :cities do
67
+ resources :restaurants
68
+ end
69
+
70
+ # in views
71
+ <%= link_to 'Show', [@city, @restaurant] %>
72
+
73
+ # in controllers
74
+ @city = City.find(params[:city_id])
75
+ @restaurant = @city.restaurants.find(params[:id])
76
+
77
+ # URLs:
78
+ http://example.org/cities/seattle/restaurants/joes-diner
79
+ http://example.org/cities/chicago/restaurants/joes-diner
80
+
81
+ =end
82
+ module Scoped
83
+
84
+
85
+ # Sets up behavior and configuration options for FriendlyId's scoped slugs
86
+ # feature.
87
+ def self.included(model_class)
88
+ model_class.instance_eval do
89
+ raise "FriendlyId::Scoped is incompatibe with FriendlyId::History" if self < History
90
+ include Slugged unless self < Slugged
91
+ friendly_id_config.class.send :include, Configuration
92
+ friendly_id_config.slug_generator_class.send :include, SlugGenerator
93
+ end
94
+ end
95
+
96
+ # This module adds the +:scope+ configuration option to
97
+ # {FriendlyId::Configuration FriendlyId::Configuration}.
98
+ module Configuration
99
+
100
+ # Gets the scope value.
101
+ #
102
+ # When setting this value, the argument should be a symbol referencing a
103
+ # +belongs_to+ relation, or a column.
104
+ #
105
+ # @return Symbol The scope value
106
+ attr_accessor :scope
107
+
108
+ # Gets the scope columns.
109
+ #
110
+ # Checks to see if the +:scope+ option passed to
111
+ # {FriendlyId::Base#friendly_id} refers to a relation, and if so, returns
112
+ # the realtion's foreign key. Otherwise it assumes the option value was
113
+ # the name of column and returns it cast to a String.
114
+ #
115
+ # @return String The scope column
116
+ def scope_columns
117
+ [@scope].flatten.map { |s| (reflection_foreign_key(s) or s).to_s }
118
+ end
119
+
120
+ private
121
+
122
+ if ActiveRecord::VERSION::STRING < "3.1"
123
+ def reflection_foreign_key(scope)
124
+ model_class.reflections[scope].try(:primary_key_name)
125
+ end
126
+ else
127
+ def reflection_foreign_key(scope)
128
+ model_class.reflections[scope].try(:foreign_key)
129
+ end
130
+ end
131
+ end
132
+
133
+ # This module overrides {FriendlyId::SlugGenerator#conflict} to consider
134
+ # scope, to avoid adding sequences to slugs under different scopes.
135
+ module SlugGenerator
136
+
137
+ private
138
+
139
+ def conflict
140
+ columns = friendly_id_config.scope_columns
141
+ matched = columns.inject(conflicts) do |memo, column|
142
+ memo.with_deleted.where(column => sluggable.send(column))
143
+ end
144
+
145
+ matched.first
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,95 @@
1
+ require "i18n"
2
+
3
+ module FriendlyId
4
+
5
+ =begin
6
+
7
+ == Translating Slugs Using Simple I18n
8
+
9
+ The {FriendlyId::SimpleI18n SimpleI18n} module adds very basic i18n support to
10
+ FriendlyId.
11
+
12
+ In order to use this module, your model must have a slug column for each locale.
13
+ By default FriendlyId looks for columns named, for example, "slug_en",
14
+ "slug_es", etc. The first part of the name can be configured by passing the
15
+ +:slug_column+ option if you choose. Note that the column for the default locale
16
+ must also include the locale in its name.
17
+
18
+ This module is most suitable to applications that need to support few locales.
19
+ If you need to support two or more locales, you may wish to use the
20
+ {FriendlyId::Globalize Globalize} module instead.
21
+
22
+ === Example migration
23
+
24
+ def self.up
25
+ create_table :posts do |t|
26
+ t.string :title
27
+ t.string :slug_en
28
+ t.string :slug_es
29
+ t.text :body
30
+ end
31
+ add_index :posts, :slug_en
32
+ add_index :posts, :slug_es
33
+ end
34
+
35
+ === Finds
36
+
37
+ Finds will take into consideration the current locale:
38
+
39
+ I18n.locale = :es
40
+ Post.find("la-guerra-de-las-galaxas")
41
+ I18n.locale = :en
42
+ Post.find("star-wars")
43
+
44
+ To find a slug by an explicit locale, perform the find inside a block
45
+ passed to I18n's +with_locale+ method:
46
+
47
+ I18n.with_locale(:es) do
48
+ Post.find("la-guerra-de-las-galaxas")
49
+ end
50
+
51
+ === Creating Records
52
+
53
+ When new records are created, the slug is generated for the current locale only.
54
+
55
+ === Translating Slugs
56
+
57
+ To translate an existing record's friendly_id, use
58
+ {FriendlyId::SimpleI18n::Model#set_friendly_id}. This will ensure that the slug
59
+ you add is properly escaped, transliterated and sequenced:
60
+
61
+ post = Post.create :name => "Star Wars"
62
+ post.set_friendly_id("La guerra de las galaxas", :es)
63
+
64
+ If you don't pass in a locale argument, FriendlyId::SimpleI18n will just use the
65
+ current locale:
66
+
67
+ I18n.with_locale(:es) do
68
+ post.set_friendly_id("La guerra de las galaxas")
69
+ end
70
+ =end
71
+ module SimpleI18n
72
+
73
+ def self.included(model_class)
74
+ model_class.instance_eval do
75
+ friendly_id_config.use :slugged
76
+ friendly_id_config.class.send :include, Configuration
77
+ include Model
78
+ end
79
+ end
80
+
81
+ module Model
82
+ def set_friendly_id(text, locale = nil)
83
+ I18n.with_locale(locale || I18n.locale) do
84
+ set_slug(normalize_friendly_id(text))
85
+ end
86
+ end
87
+ end
88
+
89
+ module Configuration
90
+ def slug_column
91
+ "#{super}_#{I18n.locale}"
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,14 @@
1
+ module FriendlyId
2
+ # A FriendlyId slug stored in an external table.
3
+ #
4
+ # @see FriendlyId::History
5
+ class Slug < ActiveRecord::Base
6
+ acts_as_paranoid column_type: 'boolean'
7
+ belongs_to :sluggable, :polymorphic => true
8
+
9
+ def to_param
10
+ slug
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,80 @@
1
+ module FriendlyId
2
+ # The default slug generator offers functionality to check slug strings for
3
+ # uniqueness and, if necessary, appends a sequence to guarantee it.
4
+ class SlugGenerator
5
+ attr_reader :sluggable, :normalized
6
+
7
+ # Create a new slug generator.
8
+ def initialize(sluggable, normalized)
9
+ @sluggable = sluggable
10
+ @normalized = normalized
11
+ end
12
+
13
+ # Given a slug, get the next available slug in the sequence.
14
+ def next
15
+ "#{normalized}#{separator}#{next_in_sequence}"
16
+ end
17
+
18
+ # Generate a new sequenced slug.
19
+ def generate
20
+ conflict? ? self.next : normalized
21
+ end
22
+
23
+ private
24
+
25
+ def next_in_sequence
26
+ last_in_sequence == 0 ? 2 : last_in_sequence.next
27
+ end
28
+
29
+ def last_in_sequence
30
+ @_last_in_sequence ||= extract_sequence_from_slug(conflict.to_param)
31
+ end
32
+
33
+ def extract_sequence_from_slug(slug)
34
+ slug.split("#{normalized}#{separator}").last.to_i
35
+ end
36
+
37
+ def column
38
+ sluggable.connection.quote_column_name friendly_id_config.slug_column
39
+ end
40
+
41
+ def conflict?
42
+ !! conflict
43
+ end
44
+
45
+ def conflict
46
+ unless defined? @conflict
47
+ @conflict = conflicts.first
48
+ end
49
+ @conflict
50
+ end
51
+
52
+ def conflicts
53
+ sluggable_class = friendly_id_config.model_class.base_class
54
+
55
+ pkey = sluggable_class.primary_key
56
+ value = sluggable.send pkey
57
+ base = "#{column} = ? OR #{column} LIKE ?"
58
+ # Awful hack for SQLite3, which does not pick up '\' as the escape character without this.
59
+ base << "ESCAPE '\\'" if sluggable.connection.adapter_name =~ /sqlite/i
60
+ scope = sluggable_class.unscoped.with_deleted.where(base, normalized, wildcard)
61
+ scope = scope.where("#{pkey} <> ?", value) unless sluggable.new_record?
62
+ scope = scope.order("LENGTH(#{column}) DESC, #{column} DESC")
63
+ end
64
+
65
+ def friendly_id_config
66
+ sluggable.friendly_id_config
67
+ end
68
+
69
+ def separator
70
+ friendly_id_config.sequence_separator
71
+ end
72
+
73
+ def wildcard
74
+ # Underscores (matching a single character) and percent signs (matching
75
+ # any number of characters) need to be escaped
76
+ # (While this seems like an excessive number of backslashes, it is correct)
77
+ "#{normalized}#{separator}".gsub(/[_%]/, '\\\\\&') + '%'
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,329 @@
1
+ # encoding: utf-8
2
+ require "friendly_id/slug_generator"
3
+
4
+ module FriendlyId
5
+ =begin
6
+
7
+ == Slugged Models
8
+
9
+ FriendlyId can use a separate column to store slugs for models which require
10
+ some text processing.
11
+
12
+ For example, blog applications typically use a post title to provide the basis
13
+ of a search engine friendly URL. Such identifiers typically lack uppercase
14
+ characters, use ASCII to approximate UTF-8 character, and strip out other
15
+ characters which may make them aesthetically unappealing or error-prone when
16
+ used in a URL.
17
+
18
+ class Post < ActiveRecord::Base
19
+ extend FriendlyId
20
+ friendly_id :title, :use => :slugged
21
+ end
22
+
23
+ @post = Post.create(:title => "This is the first post!")
24
+ @post.friendly_id # returns "this-is-the-first-post"
25
+ redirect_to @post # the URL will be /posts/this-is-the-first-post
26
+
27
+ In general, use slugs by default unless you know for sure you don't need them.
28
+ To activate the slugging functionality, use the {FriendlyId::Slugged} module.
29
+
30
+ FriendlyId will generate slugs from a method or column that you specify, and
31
+ store them in a field in your model. By default, this field must be named
32
+ +:slug+, though you may change this using the
33
+ {FriendlyId::Slugged::Configuration#slug_column slug_column} configuration
34
+ option. You should add an index to this column, and in most cases, make it
35
+ unique. You may also wish to constrain it to NOT NULL, but this depends on your
36
+ app's behavior and requirements.
37
+
38
+ === Example Setup
39
+
40
+ # your model
41
+ class Post < ActiveRecord::Base
42
+ extend FriendlyId
43
+ friendly_id :title, :use => :slugged
44
+ validates_presence_of :title, :slug, :body
45
+ end
46
+
47
+ # a migration
48
+ class CreatePosts < ActiveRecord::Migration
49
+ def self.up
50
+ create_table :posts do |t|
51
+ t.string :title, :null => false
52
+ t.string :slug, :null => false
53
+ t.text :body
54
+ end
55
+
56
+ add_index :posts, :slug, :unique => true
57
+ end
58
+
59
+ def self.down
60
+ drop_table :posts
61
+ end
62
+ end
63
+
64
+ === Working With Slugs
65
+
66
+ ==== Formatting
67
+
68
+ By default, FriendlyId uses Active Support's
69
+ paramaterize[http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-parameterize]
70
+ method to create slugs. This method will intelligently replace spaces with
71
+ dashes, and Unicode Latin characters with ASCII approximations:
72
+
73
+ movie = Movie.create! :title => "Der Preis fürs Überleben"
74
+ movie.slug #=> "der-preis-furs-uberleben"
75
+
76
+ ==== Uniqueness
77
+
78
+ When you try to insert a record that would generate a duplicate friendly id,
79
+ FriendlyId will append a sequence to the generated slug to ensure uniqueness:
80
+
81
+ car = Car.create :title => "Peugot 206"
82
+ car2 = Car.create :title => "Peugot 206"
83
+
84
+ car.friendly_id #=> "peugot-206"
85
+ car2.friendly_id #=> "peugot-206--2"
86
+
87
+ ==== Sequence Separator - The Two Dashes
88
+
89
+ By default, FriendlyId uses two dashes to separate the slug from a sequence.
90
+
91
+ You can change this with the {FriendlyId::Slugged::Configuration#sequence_separator
92
+ sequence_separator} configuration option.
93
+
94
+ ==== Column or Method?
95
+
96
+ FriendlyId always uses a method as the basis of the slug text - not a column. It
97
+ first glance, this may sound confusing, but remember that Active Record provides
98
+ methods for each column in a model's associated table, and that's what
99
+ FriendlyId uses.
100
+
101
+ Here's an example of a class that uses a custom method to generate the slug:
102
+
103
+ class Person < ActiveRecord::Base
104
+ friendly_id :name_and_location
105
+ def name_and_location
106
+ "#{name} from #{location}"
107
+ end
108
+ end
109
+
110
+ bob = Person.create! :name => "Bob Smith", :location => "New York City"
111
+ bob.friendly_id #=> "bob-smith-from-new-york-city"
112
+
113
+ ==== Providing Your Own Slug Processing Method
114
+
115
+ You can override {FriendlyId::Slugged#normalize_friendly_id} in your model for
116
+ total control over the slug format.
117
+
118
+ ==== Deciding When to Generate New Slugs
119
+
120
+ Overriding {FriendlyId::Slugged#should_generate_new_friendly_id?} lets you
121
+ control whether new friendly ids are created when a model is updated. For
122
+ example, if you only want to generate slugs once and then treat them as
123
+ read-only:
124
+
125
+ class Post < ActiveRecord::Base
126
+ extend FriendlyId
127
+ friendly_id :title, :use => :slugged
128
+
129
+ def should_generate_new_friendly_id?
130
+ new_record?
131
+ end
132
+ end
133
+
134
+ post = Post.create!(:title => "Hello world!")
135
+ post.slug #=> "hello-world"
136
+ post.title = "Hello there, world!"
137
+ post.save!
138
+ post.slug #=> "hello-world"
139
+
140
+ ==== Locale-specific Transliterations
141
+
142
+ Active Support's +parameterize+ uses
143
+ transliterate[http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-transliterate],
144
+ which in turn can use I18n's transliteration rules to consider the current
145
+ locale when replacing Latin characters:
146
+
147
+ # config/locales/de.yml
148
+ de:
149
+ i18n:
150
+ transliterate:
151
+ rule:
152
+ ü: "ue"
153
+ ö: "oe"
154
+ etc...
155
+
156
+ movie = Movie.create! :title => "Der Preis fürs Überleben"
157
+ movie.slug #=> "der-preis-fuers-ueberleben"
158
+
159
+ This functionality was in fact taken from earlier versions of FriendlyId.
160
+
161
+ ==== Gotchas: Common Problems
162
+
163
+ ===== Slugs That Begin With Numbers
164
+
165
+ Ruby's `to_i` function casts strings to integers in such a way that +23abc.to_i+
166
+ returns 23. Because FriendlyId falls back to finding by numeric id, this means
167
+ that if you attempt to find a record with a non-existant slug, and that slug
168
+ begins with a number, your find will probably return the wrong record.
169
+
170
+ There are two fairly simple ways to avoid this:
171
+
172
+ * Use validations to ensure that slugs don't begin with numbers.
173
+ * Use explicit finders like +find_by_id+ to always find by the numeric id, or
174
+ +find_by_slug+ to always find using the friendly id.
175
+
176
+ ===== Concurrency Issues
177
+
178
+ FriendlyId uses a before_validation callback to generate and set the slug. This
179
+ means that if you create two model instances before saving them, it's possible
180
+ they will generate the same slug, and the second save will fail.
181
+
182
+ This can happen in two fairly normal cases: the first, when a model using nested
183
+ attributes creates more than one record for a model that uses friendly_id. The
184
+ second, in concurrent code, either in threads or multiple processes.
185
+
186
+ To solve the nested attributes issue, I recommend simply avoiding them when
187
+ creating more than one nested record for a model that uses FriendlyId. See {this
188
+ Github issue}[https://github.com/norman/friendly_id/issues/185] for discussion.
189
+
190
+ To solve the concurrency issue, I recommend locking the model's table against
191
+ inserts while when saving the record. See {this Github
192
+ issue}[https://github.com/norman/friendly_id/issues/180] for discussion.
193
+
194
+ =end
195
+ module Slugged
196
+
197
+ # Sets up behavior and configuration options for FriendlyId's slugging
198
+ # feature.
199
+ def self.included(model_class)
200
+ model_class.friendly_id_config.instance_eval do
201
+ self.class.send :include, Configuration
202
+ self.slug_generator_class ||= Class.new(SlugGenerator)
203
+ defaults[:slug_column] ||= 'slug'
204
+ defaults[:sequence_separator] ||= '--'
205
+ end
206
+ model_class.before_validation :set_slug
207
+ end
208
+
209
+ # Process the given value to make it suitable for use as a slug.
210
+ #
211
+ # This method is not intended to be invoked directly; FriendlyId uses it
212
+ # internaly to process strings into slugs.
213
+ #
214
+ # However, if FriendlyId's default slug generation doesn't suite your needs,
215
+ # you can override this method in your model class to control exactly how
216
+ # slugs are generated.
217
+ #
218
+ # === Example
219
+ #
220
+ # class Person < ActiveRecord::Base
221
+ # friendly_id :name_and_location
222
+ #
223
+ # def name_and_location
224
+ # "#{name} from #{location}"
225
+ # end
226
+ #
227
+ # # Use default slug, but upper case and with underscores
228
+ # def normalize_friendly_id(string)
229
+ # super.upcase.gsub("-", "_")
230
+ # end
231
+ # end
232
+ #
233
+ # bob = Person.create! :name => "Bob Smith", :location => "New York City"
234
+ # bob.friendly_id #=> "BOB_SMITH_FROM_NEW_YORK_CITY"
235
+ #
236
+ # === More Resources
237
+ #
238
+ # You might want to look into Babosa[https://github.com/norman/babosa],
239
+ # which is the slugging library used by FriendlyId prior to version 4, which
240
+ # offers some specialized functionality missing from Active Support.
241
+ #
242
+ # @param [#to_s] value The value used as the basis of the slug.
243
+ # @return The candidate slug text, without a sequence.
244
+ def normalize_friendly_id(value)
245
+ # Fix to number based slugs which get mistaken as id's
246
+ value = value.to_s.parameterize
247
+ is_number = true if Float(value) rescue false
248
+ if is_number
249
+ return "#{rand_slug}#{value}"
250
+ end
251
+ value
252
+ end
253
+
254
+ # Generate random 10 character string for use in number error situations
255
+ # Instead of rejecting id/number based slug
256
+ def rand_slug
257
+ (0...10).map{ ('a'..'z').to_a[rand(26)] }.join
258
+ end
259
+
260
+ # Whether to generate a new slug.
261
+ #
262
+ # You can override this method in your model if, for example, you only want
263
+ # slugs to be generated once, and then never updated.
264
+ def should_generate_new_friendly_id?
265
+ base = send(friendly_id_config.base)
266
+ slug_value = send(friendly_id_config.slug_column)
267
+
268
+ # If the slug base is nil, and the slug field is nil, then we're going to
269
+ # leave the slug column NULL.
270
+ return false if base.nil? && slug_value.nil?
271
+ # Otherwise, if this is a new record, we're definitely going to try to
272
+ # create a new slug.
273
+ return true if new_record?
274
+ slug_base = normalize_friendly_id(base)
275
+ separator = Regexp.escape friendly_id_config.sequence_separator
276
+ # If the slug base (with and without sequence) is different from either the current
277
+ # friendly id or the slug value, then we'll generate a new friendly_id.
278
+ compare = (current_friendly_id || slug_value)
279
+ slug_base != compare && slug_base != compare.try(:sub, /#{separator}[\d]*\z/, '')
280
+ end
281
+
282
+ # Sets the slug.
283
+ # FIXME: This method sucks and the logic is pretty dubious.
284
+ def set_slug(normalized_slug = nil)
285
+ if normalized_slug || should_generate_new_friendly_id?
286
+ normalized_slug ||= normalize_friendly_id send(friendly_id_config.base)
287
+ generator = friendly_id_config.slug_generator_class.new self, normalized_slug
288
+ send "#{friendly_id_config.slug_column}=", generator.generate
289
+ end
290
+ end
291
+ private :set_slug
292
+
293
+ # This module adds the +:slug_column+, and +:sequence_separator+, and
294
+ # +:slug_generator_class+ configuration options to
295
+ # {FriendlyId::Configuration FriendlyId::Configuration}.
296
+ module Configuration
297
+ attr_writer :slug_column, :sequence_separator
298
+ attr_accessor :slug_generator_class
299
+
300
+ # Makes FriendlyId use the slug column for querying.
301
+ # @return String The slug column.
302
+ def query_field
303
+ slug_column
304
+ end
305
+
306
+ # The string used to separate a slug base from a numeric sequence.
307
+ #
308
+ # By default, +--+ is used to separate the slug from the sequence.
309
+ # FriendlyId uses two dashes to distinguish sequences from slugs with
310
+ # numbers in their name.
311
+ #
312
+ # You can change the default separator by setting the
313
+ # {FriendlyId::Slugged::Configuration#sequence_separator
314
+ # sequence_separator} configuration option.
315
+ #
316
+ # For obvious reasons, you should avoid setting it to "+-+" unless you're
317
+ # sure you will never want to have a friendly id with a number in it.
318
+ # @return String The sequence separator string. Defaults to "+--+".
319
+ def sequence_separator
320
+ @sequence_separator or defaults[:sequence_separator]
321
+ end
322
+
323
+ # The column that will be used to store the generated slug.
324
+ def slug_column
325
+ @slug_column or defaults[:slug_column]
326
+ end
327
+ end
328
+ end
329
+ end