friendly_id4 4.0.0.beta4 → 4.0.0.beta5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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