friendly_id4 4.0.0.beta4 → 4.0.0.beta5

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.
@@ -1,4 +1,5 @@
1
1
  class CreateFriendlyIdSlugs < ActiveRecord::Migration
2
+
2
3
  def self.up
3
4
  create_table :friendly_id_slugs do |t|
4
5
  t.string :slug, :null => false
@@ -1,7 +1,11 @@
1
1
  module FriendlyId
2
- # Utility methods that are in Object because it's impossible to predict what
3
- # kinds of objects get passed into FinderMethods#find_one and
4
- # Model#normalize_friendly_id.
2
+ # Utility methods for determining whether any object is a friendly id.
3
+ #
4
+ # Monkey-patching Object is a somewhat extreme measure not to be taken lightly
5
+ # by libraries, but in this case I decided to do it because to me, it feels
6
+ # cleaner than adding a module method to {FriendlyId}. I've given the methods
7
+ # names that unambigously refer to the library of their origin, which should
8
+ # be sufficient to avoid conflicts with other libraries.
5
9
  module ObjectUtils
6
10
 
7
11
  # True is the id is definitely friendly, false if definitely unfriendly,
@@ -9,6 +13,14 @@ module FriendlyId
9
13
  #
10
14
  # An object is considired "definitely unfriendly" if its class is or
11
15
  # inherits from Numeric, Symbol or ActiveRecord::Base.
16
+ #
17
+ # An object is considered "definitely friendly" if it responds to +to_i+,
18
+ # and its value when cast to an integer and then back to a string is
19
+ # different from its value when merely cast to a string:
20
+ #
21
+ # 123.friendly_id? #=> false
22
+ # "123".friendly_id? #=> nil
23
+ # "abc123".friendly_id? #=> true
12
24
  def friendly_id?
13
25
  if [Numeric, Symbol, ActiveRecord::Base].detect {|klass| self.class < klass}
14
26
  false
@@ -25,6 +37,4 @@ module FriendlyId
25
37
  end
26
38
  end
27
39
 
28
- class Object
29
- include FriendlyId::ObjectUtils
30
- end
40
+ Object.send :include, FriendlyId::ObjectUtils
@@ -0,0 +1,46 @@
1
+ module FriendlyId
2
+
3
+ =begin
4
+ This module adds the ability to exlude a list of words from use as
5
+ FriendlyId slugs.
6
+
7
+ By default, FriendlyId reserves the words "new" and "edit" when this module
8
+ is included. You can configure this globally by using {FriendlyId.defaults FriendlyId.defaults}:
9
+
10
+ FriendlyId.defaults do |config|
11
+ config.use :reserved
12
+ # Reserve words for English and Spanish URLs
13
+ config.reserved_words = %w(new edit nueva nuevo editar)
14
+ end
15
+ =end
16
+ module Reserved
17
+
18
+ # When included, this module adds configuration options to the model class's
19
+ # friendly_id_config.
20
+ def self.included(model_class)
21
+ model_class.class_eval do
22
+ friendly_id_config.class.send :include, Reserved::Configuration
23
+ friendly_id_config.defaults[:reserved_words] ||= ["new", "edit"]
24
+ end
25
+ end
26
+
27
+ # This module adds the +:reserved_words+ configuration option to
28
+ # {FriendlyId::Configuration FriendlyId::Configuration}.
29
+ module Configuration
30
+ attr_writer :reserved_words
31
+
32
+ # Overrides {FriendlyId::Configuration#base} to add a validation to the
33
+ # model class.
34
+ def base=(base)
35
+ super
36
+ reserved_words = model_class.friendly_id_config.reserved_words
37
+ model_class.validates_exclusion_of base, :in => reserved_words
38
+ end
39
+
40
+ # An array of words forbidden as slugs.
41
+ def reserved_words
42
+ @reserved_words ||= @defaults[:reserved_words]
43
+ end
44
+ end
45
+ end
46
+ end
@@ -2,21 +2,88 @@ require "friendly_id/slugged"
2
2
 
3
3
  module FriendlyId
4
4
 
5
- # This module adds scopes to in-table slugs. It's not loaded by default,
6
- # so in order to active this feature you must include the module in your
7
- # class.
8
- #
9
- # You can scope by an explicit column, or by a `belongs_to` relation.
10
- #
11
- # @example
12
- # class Restaurant < ActiveRecord::Base
13
- # belongs_to :city
14
- # include FriendlyId::Scoped
15
- # friendly_id :name, :scope => :city
16
- # end
5
+ =begin
6
+ This module allows FriendlyId to generate unique slugs within a scope.
7
+
8
+ This allows, for example, two restaurants in different cities to have the slug
9
+ +joes-diner+:
10
+
11
+ class Restaurant < ActiveRecord::Base
12
+ extend FriendlyId
13
+ belongs_to :city
14
+ friendly_id :name, :use => :scoped, :scope => :city
15
+ end
16
+
17
+ class City < ActiveRecord::Base
18
+ extend FriendlyId
19
+ has_many :restaurants
20
+ friendly_id :name, :use => :slugged
21
+ end
22
+
23
+ City.find("seattle").restaurants.find("joes-diner")
24
+ City.find("chicago").restaurants.find("joes-diner")
25
+
26
+ Without :scoped in this case, one of the restaurants would have the slug
27
+ +joes-diner+ and the other would have +joes-diner--2+.
28
+
29
+ The value for the +:scope+ option can be the name of a +belongs_to+ relation, or
30
+ a column.
31
+
32
+ == Tips For Working With Scoped Slugs
33
+
34
+ === Finding Records by Friendly ID
35
+
36
+ If you are using scopes your friendly ids may not be unique, so a simple find
37
+ like
38
+
39
+ Restaurant.find("joes-diner")
40
+
41
+ may return the wrong record. In these cases it's best to query through the
42
+ relation:
43
+
44
+ @city.restaurants.find("joes-diner")
45
+
46
+ Alternatively, you could pass the scope value as a query parameter:
47
+
48
+ Restaurant.find("joes-diner").where(:city_id => @city.id)
49
+
50
+
51
+ === Finding All Records That Match a Scoped ID
52
+
53
+ Query the slug column directly:
54
+
55
+ Restaurant.find_all_by_slug("joes-diner")
56
+
57
+ === Routes for Scoped Models
58
+
59
+ Recall that FriendlyId is a database-centric library, and does not set up any
60
+ routes for scoped models. You must do this yourself in your application. Here's
61
+ an example of one way to set this up:
62
+
63
+ # in routes.rb
64
+ resources :cities do
65
+ resources :restaurants
66
+ end
67
+
68
+ # in views
69
+ <%= link_to 'Show', [@city, @restaurant] %>
70
+
71
+ # in controllers
72
+ @city = City.find(params[:city_id])
73
+ @restaurant = @city.restaurants.find(params[:id])
74
+
75
+ # URLs:
76
+ http://example.org/cities/seattle/restaurants/joes-diner
77
+ http://example.org/cities/chicago/restaurants/joes-diner
78
+
79
+ =end
17
80
  module Scoped
18
- def self.included(klass)
19
- klass.instance_eval do
81
+
82
+
83
+ # Sets up behavior and configuration options for FriendlyId's scoped slugs
84
+ # feature.
85
+ def self.included(model_class)
86
+ model_class.instance_eval do
20
87
  raise "FriendlyId::Scoped is incompatibe with FriendlyId::History" if self < History
21
88
  include Slugged unless self < Slugged
22
89
  friendly_id_config.class.send :include, Configuration
@@ -24,22 +91,37 @@ module FriendlyId
24
91
  end
25
92
  end
26
93
 
94
+ # This module adds the +:scope+ configuration option to
95
+ # {FriendlyId::Configuration FriendlyId::Configuration}.
27
96
  module Configuration
97
+
98
+ # Gets the scope value.
99
+ #
100
+ # When setting this value, the argument should be a symbol referencing a
101
+ # +belongs_to+ relation, or a column.
102
+ #
103
+ # @return Symbol The scope value
28
104
  attr_accessor :scope
29
105
 
30
106
  # Gets the scope column.
31
107
  #
32
- # Checks to see if the +:scope+ option passed to {#friendly_id}
33
- # refers to a relation, and if so, returns the realtion's foreign key.
34
- # Otherwise it assumes the option value was the name of column and returns
35
- # it cast to a String.
108
+ # Checks to see if the +:scope+ option passed to
109
+ # {FriendlyId::Base#friendly_id} refers to a relation, and if so, returns
110
+ # the realtion's foreign key. Otherwise it assumes the option value was
111
+ # the name of column and returns it cast to a String.
112
+ #
36
113
  # @return String The scope column
37
114
  def scope_column
38
- (klass.reflections[@scope].try(:association_foreign_key) || @scope).to_s
115
+ (model_class.reflections[@scope].try(:association_foreign_key) || @scope).to_s
39
116
  end
40
117
  end
41
118
 
119
+ # This module overrides {FriendlyId::SlugSequencer#conflict} to consider
120
+ # scope, to avoid adding sequences to slugs under different scopes.
42
121
  module SlugSequencer
122
+
123
+ private
124
+
43
125
  def conflict
44
126
  column = friendly_id_config.scope_column
45
127
  conflicts.where("#{column} = ?", sluggable.send(column)).first
@@ -1,3 +1,6 @@
1
+ # A FriendlyId slug stored in an external table.
2
+ #
3
+ # @see FriendlyId::History
1
4
  class FriendlyIdSlug < ActiveRecord::Base
2
5
  belongs_to :sluggable, :polymorphic => true
3
6
  end
@@ -8,12 +8,14 @@ module FriendlyId
8
8
  @sluggable = sluggable
9
9
  end
10
10
 
11
+ # Given a slug, get the next available slug in the sequence.
11
12
  def next
12
13
  sequence = conflict.slug.split(separator)[1].to_i
13
14
  next_sequence = sequence == 0 ? 2 : sequence.next
14
15
  "#{normalized}#{separator}#{next_sequence}"
15
16
  end
16
17
 
18
+ # Generate a new sequenced slug.
17
19
  def generate
18
20
  if new_record? or slug_changed?
19
21
  conflict? ? self.next : normalized
@@ -22,6 +24,7 @@ module FriendlyId
22
24
  end
23
25
  end
24
26
 
27
+ # Whether or not the model instance's slug has changed.
25
28
  def slug_changed?
26
29
  separator = Regexp.escape friendly_id_config.sequence_separator
27
30
  base != sluggable.current_friendly_id.try(:sub, /#{separator}[\d]*\z/, '')
@@ -1,46 +1,218 @@
1
+ # encoding: utf-8
1
2
  require "friendly_id/slug_sequencer"
2
3
 
3
4
  module FriendlyId
5
+ =begin
6
+ This module adds in-table slugs to a model.
4
7
 
5
- # This module adds in-table slugs to an ActiveRecord model.
8
+ Slugs are unique id strings that have been processed to remove or replace
9
+ characters that a developer considers inconvenient for use in URLs. For example,
10
+ blog applications typically use a post title to provide the basis of a search
11
+ engine friendly URL:
12
+
13
+ "Gone With The Wind" -> "gone-with-the-wind"
14
+
15
+ FriendlyId generates slugs from a method or column that you specify, and stores
16
+ them in a field in your model. By default, this field must be named +:slug+,
17
+ though you may change this using the
18
+ {FriendlyId::Slugged::Configuration#slug_column slug_column} configuration
19
+ option. You should add an index to this field. You may also wish to constrain it
20
+ to NOT NULL, but this depends on your app's behavior and requirements.
21
+
22
+ === Example Setup
23
+
24
+ # your model
25
+ class Post < ActiveRecord::Base
26
+ extend FriendlyId
27
+ friendly_id :title, :use => :slugged
28
+ validates_presence_of :title, :slug, :body
29
+ end
30
+
31
+ # a migration
32
+ class CreatePosts < ActiveRecord::Migration
33
+ def self.up
34
+ create_table :posts do |t|
35
+ t.string :title, :null => false
36
+ t.string :slug, :null => false
37
+ t.text :body
38
+ end
39
+
40
+ add_index :posts, :slug, :unique => true
41
+ end
42
+
43
+ def self.down
44
+ drop_table :posts
45
+ end
46
+ end
47
+
48
+ === Slug Format
49
+
50
+ By default, FriendlyId uses Active Support's
51
+ paramaterize[http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-parameterize]
52
+ method to create slugs. This method will intelligently replace spaces with
53
+ dashes, and Unicode Latin characters with ASCII approximations:
54
+
55
+ movie = Movie.create! :title => "Der Preis fürs Überleben"
56
+ movie.slug #=> "der-preis-furs-uberleben"
57
+
58
+ ==== Slug Uniqueness
59
+
60
+ When you try to insert a record that would generate a duplicate friendly id,
61
+ FriendlyId will append a sequence to the generated slug to ensure uniqueness:
62
+
63
+ car = Car.create :title => "Peugot 206"
64
+ car2 = Car.create :title => "Peugot 206"
65
+
66
+ car.friendly_id #=> "peugot-206"
67
+ car2.friendly_id #=> "peugot-206--2"
68
+
69
+ ==== Changing the Slug Sequence Separator
70
+
71
+ You can do this with the {Slugged::Configuration#sequence_separator
72
+ sequence_separator} configuration option.
73
+
74
+ ==== Column or Method?
75
+
76
+ FriendlyId always uses a method as the basis of the slug text - not a column. It
77
+ first glance, this may sound confusing, but remember that Active Record provides
78
+ methods for each column in a model's associated table, and that's what
79
+ FriendlyId uses.
80
+
81
+ Here's an example of a class that uses a custom method to generate the slug:
82
+
83
+ class Person < ActiveRecord::Base
84
+ friendly_id :name_and_location
85
+ def name_and_location
86
+ "#{name} from #{location}"
87
+ end
88
+ end
89
+
90
+ bob = Person.create! :name => "Bob Smith", :location => "New York City"
91
+ bob.friendly_id #=> "bob-smith-from-new-york-city"
92
+
93
+ ==== Providing Your Own Slug Processing Method
94
+
95
+ You can override {Slugged#normalize_friendly_id} in your model for total
96
+ control over the slug format.
97
+
98
+ ==== Locale-specific Transliterations
99
+
100
+ Active Support's +parameterize+ uses
101
+ transliterate[http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-transliterate],
102
+ which in turn can use I18n's transliteration rules to consider the current
103
+ locale when replacing Latin characters:
104
+
105
+ # config/locales/de.yml
106
+ de:
107
+ i18n:
108
+ transliterate:
109
+ rule:
110
+ ü: "ue"
111
+ ö: "oe"
112
+ etc...
113
+
114
+ movie = Movie.create! :title => "Der Preis fürs Überleben"
115
+ movie.slug #=> "der-preis-fuers-ueberleben"
116
+
117
+ This functionality was in fact taken from earlier versions of FriendlyId.
118
+ =end
6
119
  module Slugged
7
120
 
8
- def self.included(klass)
9
- klass.instance_eval do
121
+ # Sets up behavior and configuration options for FriendlyId's slugging
122
+ # feature.
123
+ def self.included(model_class)
124
+ model_class.instance_eval do
10
125
  friendly_id_config.class.send :include, Configuration
11
- friendly_id_config.defaults[:slug_column] = 'slug'
12
- friendly_id_config.defaults[:sequence_separator] = '--'
13
- friendly_id_config.slug_sequencer_class = Class.new(SlugSequencer)
126
+ friendly_id_config.defaults[:slug_column] ||= 'slug'
127
+ friendly_id_config.defaults[:sequence_separator] ||= '--'
128
+ friendly_id_config.slug_sequencer_class ||= Class.new(SlugSequencer)
14
129
  before_validation :set_slug
15
130
  end
16
131
  end
17
132
 
133
+ # Process the given value to make it suitable for use as a slug.
134
+ #
135
+ # This method is not intended to be invoked directly; FriendlyId uses it
136
+ # internaly to process strings into slugs.
137
+ #
138
+ # However, if FriendlyId's default slug generation doesn't suite your needs,
139
+ # you can override this method in your model class to control exactly how
140
+ # slugs are generated.
141
+ #
142
+ # === Example
143
+ #
144
+ # class Person < ActiveRecord::Base
145
+ # friendly_id :name_and_location
146
+ #
147
+ # def name_and_location
148
+ # "#{name} from #{location}"
149
+ # end
150
+ #
151
+ # # Use default slug, but uupper case and with underscores
152
+ # def normalize_friendly_id(string)
153
+ # super.upcase.gsub("-", "_")
154
+ # end
155
+ # end
156
+ #
157
+ # bob = Person.create! :name => "Bob Smith", :location => "New York City"
158
+ # bob.friendly_id #=> "BOB_SMITH_FROM_NEW_YORK_CITY"
159
+ #
160
+ # === More Resources
161
+ #
162
+ # You might want to look into Babosa[https://github.com/norman/babosa],
163
+ # which is the slugging library used by FriendlyId prior to version 4, which
164
+ # offers some specialized functionality missing from Active Support.
165
+ #
166
+ # @param [#to_s] value The value used as the basis of the slug.
167
+ # @return The candidate slug text, without a sequence.
18
168
  def normalize_friendly_id(value)
19
169
  value.to_s.parameterize
20
170
  end
21
171
 
172
+ # Gets a new instance of the configured slug sequencing class.
173
+ #
174
+ # @see FriendlyId::SlugSequencer
22
175
  def slug_sequencer
23
176
  friendly_id_config.slug_sequencer_class.new(self)
24
177
  end
25
178
 
26
- private
27
-
179
+ # Sets the slug.
28
180
  def set_slug
29
181
  send "#{friendly_id_config.slug_column}=", slug_sequencer.generate
30
182
  end
183
+ private :set_slug
31
184
 
185
+ # This module adds the +:slug_column+, and +:sequence_separator+, and
186
+ # +:slug_sequencer_class+ configuration options to
187
+ # {FriendlyId::Configuration FriendlyId::Configuration}.
32
188
  module Configuration
33
189
  attr_writer :slug_column, :sequence_separator
34
190
  attr_accessor :slug_sequencer_class
35
191
 
192
+ # Makes FriendlyId use the slug column for querying.
193
+ # @return String The slug column.
36
194
  def query_field
37
195
  slug_column
38
196
  end
39
197
 
198
+ # The string used to separate a slug base from a numeric sequence.
199
+ #
200
+ # By default, +--+ is used to separate the slug from the sequence.
201
+ # FriendlyId uses two dashes to distinguish sequences from slugs with
202
+ # numbers in their name.
203
+ #
204
+ # You can change the default separator by setting the
205
+ # {FriendlyId::Slugged::Configuration#sequence_separator
206
+ # sequence_separator} configuration option.
207
+ #
208
+ # For obvious reasons, you should avoid setting it to "+-+" unless you're
209
+ # sure you will never want to have a friendly id with a number in it.
210
+ # @return String The sequence separator string. Defaults to "+--+".
40
211
  def sequence_separator
41
212
  @sequence_separator or defaults[:sequence_separator]
42
213
  end
43
214
 
215
+ # The column that will be used to store the generated slug.
44
216
  def slug_column
45
217
  @slug_column or defaults[:slug_column]
46
218
  end