friendly_id4 4.0.0.beta6 → 4.0.0.pre

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.
@@ -2,130 +2,31 @@ require "friendly_id/slugged"
2
2
 
3
3
  module FriendlyId
4
4
 
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
5
+ # This module adds scopes to in-table slugs.
80
6
  module Scoped
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
87
- raise "FriendlyId::Scoped is incompatibe with FriendlyId::History" if self < History
88
- include Slugged unless self < Slugged
89
- friendly_id_config.class.send :include, Configuration
90
- friendly_id_config.slug_sequencer_class.send :include, SlugSequencer
91
- end
7
+ def self.included(klass)
8
+ klass.send :include, Slugged unless klass.include? Slugged
92
9
  end
10
+ end
93
11
 
94
- # This module adds the +:scope+ configuration option to
95
- # {FriendlyId::Configuration FriendlyId::Configuration}.
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
104
- attr_accessor :scope
12
+ class SlugSequencer
13
+ alias conflict_without_scope conflict
105
14
 
106
- # Gets the scope column.
107
- #
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
- #
113
- # @return String The scope column
114
- def scope_column
115
- (model_class.reflections[@scope].try(:association_foreign_key) || @scope).to_s
116
- end
15
+ def conflict_with_scope
16
+ column = friendly_id_config.scope_column
17
+ conflicts.where("#{column} = ?", sluggable.send(column)).first
117
18
  end
118
19
 
119
- # This module overrides {FriendlyId::SlugSequencer#conflict} to consider
120
- # scope, to avoid adding sequences to slugs under different scopes.
121
- module SlugSequencer
20
+ def conflict
21
+ friendly_id_config.scope ? conflict_with_scope : conflict_without_scope
22
+ end
23
+ end
122
24
 
123
- private
25
+ class Configuration
26
+ attr_accessor :scope
124
27
 
125
- def conflict
126
- column = friendly_id_config.scope_column
127
- conflicts.where("#{column} = ?", sluggable.send(column)).first
128
- end
28
+ def scope_column
29
+ klass.reflections[@scope].try(:association_foreign_key) || @scope.to_s
129
30
  end
130
31
  end
131
32
  end
@@ -1,221 +1,127 @@
1
- # encoding: utf-8
2
- require "friendly_id/slug_sequencer"
3
-
4
1
  module FriendlyId
5
- =begin
6
- This module adds in-table slugs to a model.
7
-
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
2
 
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"
3
+ # This module adds in-table slugs to an ActiveRecord model.
4
+ module Slugged
57
5
 
58
- ==== Slug Uniqueness
6
+ # @NOTE AR-specific code here
7
+ def self.included(klass)
8
+ klass.before_save :set_slug
9
+ klass.friendly_id_config.use_slugs = true
10
+ end
59
11
 
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:
12
+ # @NOTE AS-specific code here
13
+ def normalize_friendly_id(value)
14
+ value.to_s.parameterize
15
+ end
62
16
 
63
- car = Car.create :title => "Peugot 206"
64
- car2 = Car.create :title => "Peugot 206"
17
+ private
65
18
 
66
- car.friendly_id #=> "peugot-206"
67
- car2.friendly_id #=> "peugot-206--2"
19
+ def set_slug
20
+ send "#{friendly_id_config.slug_column}=", SlugSequencer.new(self).to_s
21
+ end
22
+ end
68
23
 
69
- ==== Changing the Slug Sequence Separator
24
+ class Configuration
25
+ attr_writer :slug_column, :sequence_separator, :use_slugs
70
26
 
71
- You can do this with the {Slugged::Configuration#sequence_separator
72
- sequence_separator} configuration option.
27
+ DEFAULTS[:slug_column] = 'slug'
28
+ DEFAULTS[:sequence_separator] = '--'
73
29
 
74
- ==== Column or Method?
30
+ def query_field
31
+ use_slugs? ? slug_column : base
32
+ end
75
33
 
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.
34
+ def sequence_separator
35
+ @sequence_separator || DEFAULTS[:sequence_separator]
36
+ end
80
37
 
81
- Here's an example of a class that uses a custom method to generate the slug:
38
+ def slug_column
39
+ @slug_column || DEFAULTS[:slug_column]
40
+ end
82
41
 
83
- class Person < ActiveRecord::Base
84
- friendly_id :name_and_location
85
- def name_and_location
86
- "#{name} from #{location}"
42
+ def use_slugs?
43
+ @use_slugs
87
44
  end
88
45
  end
89
46
 
90
- bob = Person.create! :name => "Bob Smith", :location => "New York City"
91
- bob.friendly_id #=> "bob-smith-from-new-york-city"
47
+ # This class offers functionality to check slug strings for uniqueness and,
48
+ # if necessary, append a sequence to ensure it.
49
+ class SlugSequencer
50
+ attr_reader :sluggable
92
51
 
93
- ==== Providing Your Own Slug Processing Method
52
+ def initialize(sluggable)
53
+ @sluggable = sluggable
54
+ end
94
55
 
95
- You can override {Slugged#normalize_friendly_id} in your model for total
96
- control over the slug format.
56
+ def base
57
+ sluggable.send friendly_id_config.base
58
+ end
97
59
 
98
- ==== Locale-specific Transliterations
60
+ def changed?
61
+ base != current_friendly_id.try(:sub, /--[\d]*\z/, '')
62
+ end
99
63
 
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:
64
+ def column
65
+ friendly_id_config.query_field
66
+ end
104
67
 
105
- # config/locales/de.yml
106
- de:
107
- i18n:
108
- transliterate:
109
- rule:
110
- ü: "ue"
111
- ö: "oe"
112
- etc...
68
+ def conflict?
69
+ !! conflict
70
+ end
113
71
 
114
- movie = Movie.create! :title => "Der Preis fürs Überleben"
115
- movie.slug #=> "der-preis-fuers-ueberleben"
72
+ def conflict
73
+ unless defined? @conflict
74
+ @conflict = conflicts.first
75
+ end
76
+ @conflict
77
+ end
116
78
 
117
- This functionality was in fact taken from earlier versions of FriendlyId.
118
- =end
119
- module Slugged
79
+ # @NOTE AR-specific code here
80
+ def conflicts
81
+ pkey = sluggable.class.arel_table.primary_key.name
82
+ value = sluggable.send pkey
83
+ scope = sluggable.class.where("#{column} = ? OR #{column} LIKE ?", normalized, wildcard)
84
+ scope = scope.where("#{pkey} <> ?", value) unless sluggable.new_record?
85
+ scope = scope.order("#{column} DESC")
86
+ end
120
87
 
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
125
- friendly_id_config.class.send :include, Configuration
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)
129
- before_validation :set_slug
130
- end
88
+ def current_friendly_id
89
+ sluggable.instance_variable_get(:@_current_friendly_id)
131
90
  end
132
91
 
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.
168
- def normalize_friendly_id(value)
169
- value.to_s.parameterize
92
+ def friendly_id_config
93
+ sluggable.friendly_id_config
170
94
  end
171
95
 
172
- # Gets a new instance of the configured slug sequencing class.
173
- #
174
- # @see FriendlyId::SlugSequencer
175
- def slug_sequencer
176
- friendly_id_config.slug_sequencer_class.new(self)
96
+ def new_record?
97
+ sluggable.new_record?
177
98
  end
178
99
 
179
- # Sets the slug.
180
- def set_slug
181
- send "#{friendly_id_config.slug_column}=", slug_sequencer.generate
100
+ def normalized
101
+ @normalized ||= sluggable.normalize_friendly_id(base)
182
102
  end
183
- private :set_slug
184
103
 
185
- # This module adds the +:slug_column+, and +:sequence_separator+, and
186
- # +:slug_sequencer_class+ configuration options to
187
- # {FriendlyId::Configuration FriendlyId::Configuration}.
188
- module Configuration
189
- attr_writer :slug_column, :sequence_separator
190
- attr_accessor :slug_sequencer_class
104
+ def separator
105
+ friendly_id_config.sequence_separator
106
+ end
191
107
 
192
- # Makes FriendlyId use the slug column for querying.
193
- # @return String The slug column.
194
- def query_field
195
- slug_column
196
- end
108
+ def next
109
+ sequence = conflict.slug.split(separator)[1].to_i
110
+ next_sequence = sequence == 0 ? 2 : sequence.next
111
+ "#{normalized}#{separator}#{next_sequence}"
112
+ end
197
113
 
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 "+--+".
211
- def sequence_separator
212
- @sequence_separator or defaults[:sequence_separator]
114
+ def to_s
115
+ if changed? or new_record?
116
+ conflict? ? self.next : normalized
117
+ else
118
+ sluggable.friendly_id
213
119
  end
120
+ end
214
121
 
215
- # The column that will be used to store the generated slug.
216
- def slug_column
217
- @slug_column or defaults[:slug_column]
218
- end
122
+ def wildcard
123
+ "#{normalized}#{separator}%"
219
124
  end
220
125
  end
126
+
221
127
  end