friendly_id 4.1.0.beta.1 → 5.0.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,95 +1,3 @@
1
- # What's New in FriendlyId 4?
1
+ # What's New in FriendlyId 5?
2
2
 
3
- ## Back to basics
4
-
5
- FriendlyId is mostly a different codebase from FriendlyId 3. However, this isn't
6
- the "big rewrite," it's the "small rewrite:"
7
-
8
- Adding new features with each release is not sustainable. This release *removes*
9
- features, but makes it possible to add them back as addons. We can also remove
10
- some complexity by relying on the better default functionality provided by newer
11
- versions of Active Support and Active Record.
12
-
13
- Here's what's changed:
14
-
15
- ## New configuration and setup
16
-
17
- FriendlyId is no longer added to Active Record by default, you must explicitly
18
- add it to each model you want to use it in. The method and options have also
19
- changed:
20
-
21
- # FriendlyId 3
22
- class Post < ActiveRecord::Base
23
- has_friendly_id :title, :use_slugs => true
24
- end
25
-
26
- # FriendlyId 4
27
- class Post < ActiveRecord::Base
28
- extend FriendlyId
29
- friendly_id :title, :use => :slugged
30
- end
31
-
32
- It also adds a new "defaults" method for configuring all models:
33
-
34
- FriendlyId.defaults do |config|
35
- config.use :slugged, :reserved
36
- config.base = :name
37
- end
38
-
39
- ## Active Record 3+ only
40
-
41
- For 2.3 support, you can use FriendlyId 3.x, which will continue to be
42
- maintained until people don't want it any more.
43
-
44
- ## In-table slugs
45
-
46
- FriendlyId no longer creates a separate slugs table - it just stores the
47
- generated slug value in the model table, which is simpler, faster and what most
48
- want by default. Keeping slug history in a separate table is an
49
- {FriendlyId::History optional add-on} for FriendlyId 4.
50
-
51
- ## No more multiple finds
52
-
53
- Person.find "joe-schmoe" # Supported
54
- Person.find ["joe-schmoe", "john-doe"] # No longer supported
55
-
56
- If you want find by more than one friendly id, build your own query:
57
-
58
- Person.where(:slug => ["joe-schmoe", "john-doe"])
59
-
60
- This lets us do *far* less monkeypatching in Active Record. How much less?
61
- FriendlyId overrides the base find with a mere 2 lines of code, and otherwise
62
- changes nothing else. This means more stability and less breakage between Rails
63
- updates.
64
-
65
- ## No more finder status
66
-
67
- FriendlyId 3 offered finder statuses to help you determine when an outdated
68
- or non-friendly id was used to find the record, so that you could decide whether
69
- to permanently redirect to the canonical URL. However, there's a simpler way to
70
- do that, so this feature has been removed:
71
-
72
- if request.path != person_path(@person)
73
- return redirect_to @person, :status => :moved_permanently
74
- end
75
-
76
- ## Bye-bye Babosa
77
-
78
- [Babosa](http://github.com/norman/babosa) is FriendlyId 3's slugging library.
79
-
80
- FriendlyId 4 doesn't use it by default because the most important pieces of it
81
- were already accepted into Active Support 3.
82
-
83
- However, Babosa is still useful, for example, for idiomatically transliterating
84
- Cyrillic ([or other
85
- language](https://github.com/norman/babosa/tree/master/lib/babosa/transliterator))
86
- strings to ASCII. It's very easy to include - just override
87
- `#normalize_friendly_id` in your model:
88
-
89
- class MyModel < ActiveRecord::Base
90
- ...
91
-
92
- def normalize_friendly_id(text)
93
- text.to_slug.normalize! :transliterations => :russian
94
- end
95
- end
3
+ ## Write me
@@ -1,43 +1,33 @@
1
1
  # encoding: utf-8
2
- $:.push File.expand_path("../lib", __FILE__)
3
-
4
- require "friendly_id"
2
+ require File.expand_path("../lib/friendly_id/version", __FILE__)
5
3
 
6
4
  Gem::Specification.new do |s|
7
5
  s.name = "friendly_id"
8
6
  s.version = FriendlyId::VERSION
9
7
  s.authors = ["Norman Clarke", "Philip Arndt"]
10
- s.email = ["norman@njclarke.com", "parndt@gmail.com"]
11
- s.homepage = "http://github.com/norman/friendly_id"
8
+ s.email = ["norman@njclarke.com", "p@arndt.io"]
9
+ s.homepage = "http://github.com/FriendlyId/friendly_id"
12
10
  s.summary = "A comprehensive slugging and pretty-URL plugin."
13
11
  s.rubyforge_project = "friendly_id"
14
12
  s.files = `git ls-files`.split("\n")
15
13
  s.test_files = `git ls-files -- {test}/*`.split("\n")
16
14
  s.require_paths = ["lib"]
15
+ s.license = 'MIT'
16
+
17
+ s.required_ruby_version = '>= 1.9.3'
17
18
 
18
- s.add_development_dependency "railties", "~> 3.2.0"
19
- s.add_development_dependency "activerecord", "~> 3.2.0"
20
- s.add_development_dependency "minitest", "3.2.0"
21
- s.add_development_dependency "mocha"
22
- s.add_development_dependency "maruku"
19
+ s.add_development_dependency "railties", "~> 4.0.0"
20
+ s.add_development_dependency "activerecord", "~> 4.0.0"
21
+ s.add_development_dependency "minitest", ">= 4.4.0"
22
+ s.add_development_dependency "mocha", "~> 0.13.3"
23
23
  s.add_development_dependency "yard"
24
24
  s.add_development_dependency "i18n"
25
25
  s.add_development_dependency "ffaker"
26
26
  s.add_development_dependency "simplecov"
27
- s.add_development_dependency "globalize3"
28
27
 
29
28
  s.description = <<-EOM
30
29
  FriendlyId is the "Swiss Army bulldozer" of slugging and permalink plugins for
31
30
  Ruby on Rails. It allows you to create pretty URLs and work with human-friendly
32
31
  strings as if they were numeric ids for Active Record models.
33
32
  EOM
34
-
35
- s.post_install_message = <<-EOM
36
- NOTE: FriendlyId 4.x breaks compatibility with 3.x. If you're upgrading
37
- from 3.x, please see this document:
38
-
39
- http://rubydoc.info/github/norman/friendly_id/master/file/WhatsNew.md
40
-
41
- EOM
42
-
43
33
  end
@@ -0,0 +1,19 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec path: '../'
4
+
5
+ # Database Configuration
6
+ group :development, :test do
7
+ platforms :jruby do
8
+ gem 'activerecord-jdbcsqlite3-adapter', '>= 1.3.0.beta2'
9
+ gem 'activerecord-jdbcmysql-adapter', '>= 1.3.0.beta2'
10
+ gem 'activerecord-jdbcpostgresql-adapter', '>= 1.3.0.beta2'
11
+ gem 'jruby-openssl'
12
+ end
13
+
14
+ platforms :ruby do
15
+ gem 'sqlite3'
16
+ gem 'mysql2'
17
+ gem 'pg'
18
+ end
19
+ end
@@ -3,7 +3,7 @@ require "thread"
3
3
  require "friendly_id/base"
4
4
  require "friendly_id/object_utils"
5
5
  require "friendly_id/configuration"
6
- require "friendly_id/finder_methods"
6
+ require "friendly_id/scopes"
7
7
 
8
8
  =begin
9
9
 
@@ -44,9 +44,6 @@ with numeric ids:
44
44
  =end
45
45
  module FriendlyId
46
46
 
47
- # The current version.
48
- VERSION = "4.1.0.beta.1"
49
-
50
47
  @mutex = Mutex.new
51
48
 
52
49
  autoload :History, "friendly_id/history"
@@ -55,7 +52,6 @@ module FriendlyId
55
52
  autoload :Reserved, "friendly_id/reserved"
56
53
  autoload :Scoped, "friendly_id/scoped"
57
54
  autoload :Slugged, "friendly_id/slugged"
58
- autoload :Globalize, "friendly_id/globalize"
59
55
 
60
56
  # FriendlyId takes advantage of `extended` to do basic model setup, primarily
61
57
  # extending {FriendlyId::Base} to add {FriendlyId::Base#friendly_id
@@ -84,10 +80,11 @@ module FriendlyId
84
80
  class << model_class
85
81
  alias relation_without_friendly_id relation
86
82
  end
87
- model_class.instance_eval do
83
+ model_class.class_eval do
88
84
  extend Base
89
85
  @friendly_id_config = Class.new(Configuration).new(self)
90
86
  FriendlyId.defaults.call @friendly_id_config
87
+ include Model
91
88
  end
92
89
  end
93
90
 
@@ -111,4 +108,12 @@ module FriendlyId
111
108
  @defaults ||= lambda {|config| config.use :reserved}
112
109
  end
113
110
  end
111
+
112
+ # Set the ActiveRecord table name prefix to friendly_id_
113
+ #
114
+ # This makes 'slugs' into 'friendly_id_slugs' and also respects any
115
+ # 'global' table_name_prefix set on ActiveRecord::Base.
116
+ def self.table_name_prefix
117
+ "#{ActiveRecord::Base.table_name_prefix}friendly_id_"
118
+ end
114
119
  end
@@ -0,0 +1 @@
1
+ version.rb merge=ours
@@ -156,8 +156,7 @@ often better and easier to use {FriendlyId::Slugged slugs}.
156
156
  # @option options [Symbol,Module] :use The addon or name of an addon to use.
157
157
  # By default, FriendlyId provides {FriendlyId::Slugged :slugged},
158
158
  # {FriendlyId::History :history}, {FriendlyId::Reserved :reserved}, and
159
- # {FriendlyId::Scoped :scoped}, {FriendlyId::SimpleI18n :simple_i18n},
160
- # and {FriendlyId::Globalize :globalize}.
159
+ # {FriendlyId::Scoped :scoped}, and {FriendlyId::SimpleI18n :simple_i18n}.
161
160
  #
162
161
  # @option options [Array] :reserved_words Available when using +:reserved+,
163
162
  # which is loaded by default. Sets an array of words banned for use as
@@ -169,7 +168,7 @@ often better and easier to use {FriendlyId::Slugged slugs}.
169
168
  #
170
169
  # @option options [Symbol] :sequence_separator Available when using +:slugged+.
171
170
  # Configures the sequence of characters used to separate a slug from a
172
- # sequence. Defaults to +--+.
171
+ # sequence. Defaults to +-+.
173
172
  #
174
173
  # @option options [Symbol] :slug_column Available when using +:slugged+.
175
174
  # Configures the name of the column where FriendlyId will store the slug.
@@ -189,8 +188,8 @@ often better and easier to use {FriendlyId::Slugged slugs}.
189
188
  yield friendly_id_config if block_given?
190
189
  friendly_id_config.use options.delete :use
191
190
  friendly_id_config.send :set, base ? options.merge(:base => base) : options
192
- before_save {|rec| rec.instance_eval {@current_friendly_id = friendly_id}}
193
191
  include Model
192
+ extend Scopes
194
193
  end
195
194
 
196
195
  # Returns the model class's {FriendlyId::Configuration friendly_id_config}.
@@ -201,73 +200,15 @@ often better and easier to use {FriendlyId::Slugged slugs}.
201
200
  def friendly_id_config
202
201
  @friendly_id_config ||= base_class.friendly_id_config.dup.tap do |config|
203
202
  config.model_class = self
204
- @relation_class = base_class.send(:relation_class)
205
- end
206
- end
207
-
208
- private
209
-
210
- # Gets an instance of an the relation class.
211
- #
212
- # With FriendlyId this will be a subclass of ActiveRecord::Relation, rather than
213
- # Relation itself, in order to avoid tainting all Active Record models with
214
- # FriendlyId.
215
- #
216
- # Note that this method is essentially copied and pasted from Rails 3.2.9.rc1,
217
- # with the exception of changing the relation class. Obviously this is less than
218
- # ideal, but I know of no better way to accomplish this.
219
- # @see #relation_class
220
- def relation #:nodoc:
221
- relation = relation_class.new(self, arel_table)
222
-
223
- if finder_needs_type_condition?
224
- relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
225
- else
226
- relation
227
- end
228
- end
229
-
230
- # Gets (and if necessary, creates) a subclass of the model's relation class.
231
- #
232
- # Rather than including FriendlyId's overridden finder methods in
233
- # ActiveRecord::Relation directly, FriendlyId adds them to a subclass
234
- # specific to the AR model, and makes #relation return an instance of this
235
- # class. By doing this, we ensure that only models that specifically extend
236
- # FriendlyId have their finder methods overridden.
237
- #
238
- # Note that this method does not directly subclass ActiveRecord::Relation,
239
- # but rather whatever class the @relation class instance variable is an
240
- # instance of. In practice, this will almost always end up being
241
- # ActiveRecord::Relation, but in case another plugin is using this same
242
- # pattern to extend a model's finder functionality, FriendlyId will not
243
- # replace it, but rather override it.
244
- #
245
- # This pattern can be seen as a poor man's "refinement"
246
- # (http://timelessrepo.com/refinements-in-ruby), and while I **think** it
247
- # will work quite well, I realize that it could cause unexpected issues,
248
- # since the authors of Rails are probably not intending this kind of usage
249
- # against a private API. If this ends up being problematic I will probably
250
- # revert back to the old behavior of simply extending
251
- # ActiveRecord::Relation.
252
- def relation_class
253
- @relation_class or begin
254
- @relation_class = Class.new(relation_without_friendly_id.class) do
255
- alias_method :find_one_without_friendly_id, :find_one
256
- alias_method :exists_without_friendly_id?, :exists?
257
- include FriendlyId::FinderMethods
258
- end
259
- # Set a name so that model instances can be marshalled. Use a
260
- # ridiculously long name that will not conflict with anything.
261
- # TODO: just use the constant, no need for the @relation_class variable.
262
- const_set('FriendlyIdActiveRecordRelation', @relation_class)
263
203
  end
264
204
  end
265
205
  end
266
206
 
267
207
  # Instance methods that will be added to all classes using FriendlyId.
268
208
  module Model
269
-
270
- attr_reader :current_friendly_id
209
+ def self.included(model_class)
210
+ return if model_class.respond_to?(:friendly)
211
+ end
271
212
 
272
213
  # Convenience method for accessing the class method of the same name.
273
214
  def friendly_id_config
@@ -0,0 +1,41 @@
1
+ require 'securerandom'
2
+
3
+ module FriendlyId
4
+
5
+ class Candidates
6
+
7
+ include Enumerable
8
+
9
+ def initialize(object, *array)
10
+ @object = object
11
+ @candidates = to_candidate_array(object, array.flatten(1))
12
+ end
13
+
14
+ def each(*args, &block)
15
+ @candidates.each(*args) do |candidate|
16
+ yield @object.normalize_friendly_id(candidate.map(&:call).join(' '))
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def to_candidate_array(object, array)
23
+ array.map do |candidate|
24
+ case candidate
25
+ when String
26
+ [->{candidate}]
27
+ when Array
28
+ to_candidate_array(object, candidate).flatten
29
+ when Symbol
30
+ [object.method(candidate)]
31
+ else
32
+ if candidate.respond_to?(:call)
33
+ [candidate]
34
+ else
35
+ [->{candidate.to_s}]
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -58,18 +58,18 @@ module FriendlyId
58
58
  # @param [#to_s,Module] *modules Arguments should be Modules, or symbols or
59
59
  # strings that correspond with the name of a module inside the FriendlyId
60
60
  # namespace. By default FriendlyId provides +:slugged+, +:history+,
61
- # +:simple_i18n+, +:globalize+, and +:scoped+.
61
+ # +:simple_i18n+, and +:scoped+.
62
62
  def use(*modules)
63
63
  modules.to_a.flatten.compact.map do |object|
64
- mod = object.kind_of?(Module) ? object : FriendlyId.const_get(object.to_s.classify)
65
- model_class.send(:include, mod)
66
- @modules << mod.name.split("::").last.downcase.to_sym if mod.name.present?
64
+ mod = get_module(object)
65
+ mod.setup(@model_class) if mod.respond_to?(:setup)
66
+ @model_class.send(:include, mod) unless uses? object
67
67
  end
68
68
  end
69
69
 
70
70
  # Returns whether the given module is in use
71
- def uses?(modul)
72
- @modules.include?(modul)
71
+ def uses?(mod)
72
+ @model_class < get_module(mod)
73
73
  end
74
74
 
75
75
  # The column that FriendlyId will use to find the record when querying by
@@ -83,6 +83,10 @@ module FriendlyId
83
83
 
84
84
  private
85
85
 
86
+ def get_module(object)
87
+ Module === object ? object : FriendlyId.const_get(object.to_s.classify)
88
+ end
89
+
86
90
  def set(values)
87
91
  values and values.each {|name, value| self.send "#{name}=", value}
88
92
  end
@@ -58,13 +58,26 @@ method.
58
58
 
59
59
  # Configures the model instance to use the History add-on.
60
60
  def self.included(model_class)
61
- model_class.instance_eval do
61
+ model_class.class_eval do
62
62
  @friendly_id_config.use :slugged
63
- has_many :slugs, :as => :sluggable, :dependent => :destroy,
64
- :class_name => Slug.to_s, :order => "#{Slug.quoted_table_name}.id DESC"
63
+
64
+ has_many :slugs, -> {order("#{Slug.quoted_table_name}.id DESC")}, {
65
+ :as => :sluggable,
66
+ :dependent => :destroy,
67
+ :class_name => Slug.to_s
68
+ }
65
69
  after_save :create_slug
66
- relation_class.send :include, FinderMethods
67
- friendly_id_config.slug_generator_class.send :include, SlugGenerator
70
+ def self.find_by_friendly_id(id)
71
+ includes(:slugs).where(slug_history_clause(id)).references(:slugs).first
72
+ end
73
+
74
+ def self.exists_by_friendly_id?(id)
75
+ includes(:slugs).where(arel_table[friendly_id_config.query_field].eq(id).or(slug_history_clause(id))).exists?
76
+ end
77
+
78
+ def self.slug_history_clause(id)
79
+ Slug.arel_table[:sluggable_type].eq(base_class.to_s).and(Slug.arel_table[:slug].eq(id))
80
+ end
68
81
  end
69
82
  end
70
83
 
@@ -75,65 +88,14 @@ method.
75
88
  return if slugs.first.try(:slug) == friendly_id
76
89
  # Allow reversion back to a previously used slug
77
90
  relation = slugs.where(:slug => friendly_id)
78
- result = relation.select("id").lock(true).all
79
- relation.delete_all unless result.empty?
91
+ if friendly_id_config.uses?(:scoped)
92
+ relation = relation.where(:scope => serialized_scope)
93
+ end
94
+ relation.delete_all
80
95
  slugs.create! do |record|
81
96
  record.slug = friendly_id
82
97
  record.scope = serialized_scope if friendly_id_config.uses?(:scoped)
83
98
  end
84
99
  end
85
-
86
- # Adds a finder that explictly uses slugs from the slug table.
87
- module FinderMethods
88
-
89
- # Search for a record in the slugs table using the specified slug.
90
- def find_one(id)
91
- return super(id) if id.unfriendly_id?
92
- where(@klass.friendly_id_config.query_field => id).first or
93
- with_old_friendly_id(id) {|x| where(:id => x).first} or
94
- find_one_without_friendly_id(id)
95
- end
96
-
97
- # Search for a record in the slugs table using the specified slug.
98
- def exists?(id = false)
99
- return super if id.unfriendly_id?
100
- exists_without_friendly_id?(@klass.friendly_id_config.query_field => id) or
101
- with_old_friendly_id(id) {|x| exists_without_friendly_id?(:id => x)} or
102
- exists_without_friendly_id?(id)
103
- end
104
-
105
- private
106
-
107
- # Accepts a slug, and yields a corresponding sluggable_id into the block.
108
- def with_old_friendly_id(slug, &block)
109
- sql = "SELECT sluggable_id FROM #{Slug.quoted_table_name} WHERE sluggable_type = %s AND slug = %s"
110
- sql = sql % [@klass.base_class.to_s, slug].map {|x| connection.quote(x)}
111
- sluggable_ids = connection.select_values(sql)
112
- yield sluggable_ids if sluggable_ids
113
- end
114
- end
115
-
116
- # This module overrides {FriendlyId::SlugGenerator#conflicts} to consider
117
- # all historic slugs for that model.
118
- module SlugGenerator
119
-
120
- private
121
-
122
- def conflicts
123
- sluggable_class = friendly_id_config.model_class.base_class
124
- pkey = sluggable_class.primary_key
125
- value = sluggable.send pkey
126
-
127
- scope = Slug.where("slug = ? OR slug LIKE ?", normalized, wildcard)
128
- scope = scope.where(:sluggable_type => sluggable_class.to_s)
129
- scope = scope.where("sluggable_id <> ?", value) unless sluggable.new_record?
130
- if sluggable.friendly_id_config.uses?(:scoped)
131
- scope = scope.where("scope = ?", sluggable.serialized_scope)
132
- end
133
- length_command = "LENGTH"
134
- length_command = "LEN" if sluggable.connection.adapter_name =~ /sqlserver/i
135
- scope.order("#{length_command}(slug) DESC, slug DESC")
136
- end
137
- end
138
100
  end
139
101
  end