friendly_id 4.0.0.beta10 → 4.0.0.beta11

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -9,3 +9,4 @@ coverage
9
9
  *.gem
10
10
  *.sqlite3
11
11
  *.rbc
12
+ *.lock
@@ -3,10 +3,6 @@ rvm:
3
3
  - 1.9.2
4
4
  - 1.8.7
5
5
  - rbx-2.0
6
- # Temporarily not testing JRuby in CI because ActiveRecord JDBC does not
7
- # support 3.1 well yet. Rest assured I'm testing this and want to target
8
- # JRuby, I just don't want to see broken build messages all the time when I
9
- # already know what the probelem is.
10
6
  # - jruby
11
7
 
12
8
  branches:
data/README.md CHANGED
@@ -18,7 +18,7 @@ instead of:
18
18
  ## FriendlyId Features
19
19
 
20
20
  FriendlyId offers many advanced features, including: slug history and
21
- versioning, scoped slugs, reserved words, and custom slug generators.
21
+ versioning, i18n, scoped slugs, reserved words, and custom slug generators.
22
22
 
23
23
  FriendlyId is compatible with Active Record **3.0** and **3.1**.
24
24
 
@@ -26,7 +26,7 @@ FriendlyId is compatible with Active Record **3.0** and **3.1**.
26
26
 
27
27
  FriendlyId 4.x introduces many changes incompatible with 3.x. If you're
28
28
  upgrading, please [read the
29
- docs](http://norman.github.com/friendly_id/file.WhatsNew.html) to see what's
29
+ docs](http://rubydoc.info/github/norman/friendly_id/master/file/WhatsNew.md) to see what's
30
30
  new.
31
31
 
32
32
  ## Docs
@@ -73,7 +73,7 @@ The current docs can always be found
73
73
  ## Benchmarks
74
74
 
75
75
  The latest benchmarks for FriendlyId are maintained
76
- [here](https://gist.github.com/1129745).
76
+ [here](http://bit.ly/friendly-id-benchmarks).
77
77
 
78
78
 
79
79
  ## Bugs
@@ -38,8 +38,8 @@ It also adds a new "defaults" method for configuring all models:
38
38
 
39
39
  ## Active Record 3+ only
40
40
 
41
- For 2.3 support, you can use FriendlyId 3.x, which will continue to be maintained
42
- until people don't want it any more.
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
43
 
44
44
  ## In-table slugs
45
45
 
data/bench.rb CHANGED
@@ -17,14 +17,12 @@ Book = Class.new ActiveRecord::Base
17
17
 
18
18
  class Journalist < ActiveRecord::Base
19
19
  extend FriendlyId
20
- include FriendlyId::Slugged
21
- friendly_id :name
20
+ friendly_id :name, :use => :slugged
22
21
  end
23
22
 
24
23
  class Manual < ActiveRecord::Base
25
24
  extend FriendlyId
26
- include FriendlyId::History
27
- friendly_id :name
25
+ friendly_id :name, :use => :history
28
26
  end
29
27
 
30
28
  BOOKS = []
@@ -38,6 +36,8 @@ MANUALS = []
38
36
  MANUALS << (Manual.create! :name => name).friendly_id
39
37
  end
40
38
 
39
+ ActiveRecord::Base.connection.execute "UPDATE manuals SET slug = NULL"
40
+
41
41
  Benchmark.bmbm do |x|
42
42
  x.report 'find (without FriendlyId)' do
43
43
  N.times {Book.find BOOKS.rand}
@@ -46,7 +46,7 @@ Benchmark.bmbm do |x|
46
46
  N.times {Journalist.find JOURNALISTS.rand}
47
47
  end
48
48
  x.report 'find (external slug)' do
49
- N.times {Manual.find_by_friendly_id MANUALS.rand}
49
+ N.times {Manual.find MANUALS.rand}
50
50
  end
51
51
 
52
52
  x.report 'insert (without FriendlyId)' do
@@ -60,4 +60,4 @@ Benchmark.bmbm do |x|
60
60
  x.report 'insert (external slug)' do
61
61
  N.times {transaction {Manual.create :name => Faker::Name.name}}
62
62
  end
63
- end
63
+ end
@@ -15,14 +15,15 @@ Gem::Specification.new do |s|
15
15
  s.test_files = `git ls-files -- {test}/*`.split("\n")
16
16
  s.require_paths = ["lib"]
17
17
 
18
- s.add_development_dependency "activerecord", "~> 3.0"
18
+ s.add_development_dependency "activerecord", "~> 3.1"
19
19
  s.add_development_dependency "sqlite3", "~> 1.3"
20
20
  s.add_development_dependency "minitest", "~> 2.4.0"
21
21
  s.add_development_dependency "mocha", "~> 0.9.12"
22
22
  s.add_development_dependency "ffaker", "~> 1.8.0"
23
23
  s.add_development_dependency "maruku", "~> 0.6.0"
24
24
  s.add_development_dependency "yard", "~> 0.7.2"
25
- s.add_development_dependency "i18n", "~> 0.5.0"
25
+ s.add_development_dependency "i18n", "~> 0.6.0"
26
+ s.add_development_dependency "simplecov"
26
27
 
27
28
  s.description = <<-EOM
28
29
  FriendlyId is the "Swiss Army bulldozer" of slugging and permalink plugins for
@@ -1,10 +1,10 @@
1
1
  source :rubygems
2
2
 
3
- platform :jruby do
4
- gem "activerecord-jdbcmysql-adapter"
5
- gem "activerecord-jdbcpostgresql-adapter"
6
- gem "activerecord-jdbcsqlite3-adapter"
7
- end
3
+ # platform :jruby do
4
+ # gem "activerecord-jdbcmysql-adapter"
5
+ # gem "activerecord-jdbcpostgresql-adapter"
6
+ # gem "activerecord-jdbcsqlite3-adapter"
7
+ # end
8
8
 
9
9
  platform :ruby do
10
10
  gem "mysql2", "~> 0.2.0"
@@ -1,10 +1,11 @@
1
1
  source :rubygems
2
2
 
3
- platform :jruby do
4
- gem "activerecord-jdbcmysql-adapter"
5
- gem "activerecord-jdbcpostgresql-adapter"
6
- gem "activerecord-jdbcsqlite3-adapter"
7
- end
3
+ # platform :jruby do
4
+ # gem "activerecord-jdbc-adapter", :git => "https://github.com/nicksieger/activerecord-jdbc-adapter.git"
5
+ # gem "activerecord-jdbcmysql-adapter"
6
+ # gem "activerecord-jdbcpostgresql-adapter"
7
+ # gem "activerecord-jdbcsqlite3-adapter"
8
+ # end
8
9
 
9
10
  platform :ruby do
10
11
  gem "mysql2", "~> 0.3.6"
@@ -81,7 +81,7 @@ In general, use slugs by default unless you know for sure you don't need them.
81
81
  module FriendlyId
82
82
 
83
83
  # The current version.
84
- VERSION = "4.0.0.beta10"
84
+ VERSION = "4.0.0.beta11"
85
85
 
86
86
  @mutex = Mutex.new
87
87
 
@@ -107,19 +107,21 @@ module FriendlyId
107
107
  # {FriendlyId::Configuration} directly. This isolates other models from large
108
108
  # feature changes an addon to FriendlyId could potentially introduce.
109
109
  #
110
- # The upshot of this is, you can htwo Active Record models that both have a
111
- # @friendly_id_config, but each config object can have different methods and
112
- # behaviors depending on what modules have been loaded, without conflicts.
113
- # Keep this in mind if you're hacking on FriendlyId.
110
+ # The upshot of this is, you can have two Active Record models that both have
111
+ # a @friendly_id_config, but each config object can have different methods
112
+ # and behaviors depending on what modules have been loaded, without
113
+ # conflicts. Keep this in mind if you're hacking on FriendlyId.
114
114
  #
115
115
  # For examples of this, see the source for {Scoped.included}.
116
116
  def self.extended(model_class)
117
+ class << model_class
118
+ alias relation_without_friendly_id relation
119
+ end
117
120
  model_class.instance_eval do
118
121
  extend Base
119
122
  @friendly_id_config = Class.new(Configuration).new(self)
120
123
  FriendlyId.defaults.call @friendly_id_config
121
124
  end
122
- ActiveRecord::Relation.send :include, FinderMethods
123
125
  end
124
126
 
125
127
  # Set global defaults for all models using FriendlyId.
@@ -100,10 +100,10 @@ module FriendlyId
100
100
  # Configures the name of the column where FriendlyId will store the slug.
101
101
  # Defaults to +:slug+.
102
102
  #
103
- # @option options [Symbol] :slug_sequencer_class Available when using +:slugged+.
103
+ # @option options [Symbol] :slug_generator_class Available when using +:slugged+.
104
104
  # Sets the class used to generate unique slugs. You should not specify this
105
105
  # unless you're doing some extensive hacking on FriendlyId. Defaults to
106
- # {FriendlyId::SlugSequencer}.
106
+ # {FriendlyId::SlugGenerator}.
107
107
  #
108
108
  # @yield Provides access to the model class's friendly_id_config, which
109
109
  # allows an alternate configuration syntax, and conditional configuration
@@ -128,5 +128,45 @@ module FriendlyId
128
128
  @friendly_id_config = base_class.friendly_id_config.dup
129
129
  end
130
130
  end
131
+
132
+ private
133
+
134
+ # Gets an instance of an anonymous subclass of ActiveRecord::Relation.
135
+ # @see #relation_class
136
+ def relation
137
+ @relation = nil unless @relation.class <= relation_class
138
+ @relation ||= relation_class.new(self, arel_table)
139
+ super
140
+ end
141
+
142
+ # Gets an anonymous subclass of the model's relation class.
143
+ #
144
+ # Rather than including FriendlyId's overridden finder methods in
145
+ # ActiveRecord::Relation directly, FriendlyId adds them to the anonymous
146
+ # subclass, and makes #relation return an instance of this class. By doing
147
+ # this, we ensure that only models that specifically extend FriendlyId have
148
+ # their finder methods overridden.
149
+ #
150
+ # Note that this method does not directly subclass ActiveRecord::Relation,
151
+ # but rather whatever class the @relation class instance variable is an
152
+ # instance of. In practice, this will almost always end up being
153
+ # ActiveRecord::Relation, but in case another plugin is using this same
154
+ # pattern to extend a model's finder functionality, FriendlyId will not
155
+ # replace it, but rather override it.
156
+ #
157
+ # This pattern can be seen as a poor man's "refinement"
158
+ # (http://timelessrepo.com/refinements-in-ruby), and while I **think** it
159
+ # will work quite well, I realize that it could cause unexpected issues,
160
+ # since the authors of Rails are probably not intending this kind of usage
161
+ # against a private API. If this ends up being problematic I will probably
162
+ # revert back to the old behavior of simply extending
163
+ # ActiveRecord::Relation.
164
+ def relation_class
165
+ @relation_class ||= Class.new(relation_without_friendly_id.class) do
166
+ alias_method :find_one_without_friendly_id, :find_one
167
+ alias_method :exists_without_friendly_id?, :exists?
168
+ include FriendlyId::FinderMethods
169
+ end
170
+ end
131
171
  end
132
172
  end
@@ -1,5 +1,5 @@
1
1
  module FriendlyId
2
- # These methods will override the finder methods in ActiveRecord::Relation.
2
+ # These methods will be added to the model's {FriendlyId::Base#relation_class relation_class}.
3
3
  module FinderMethods
4
4
 
5
5
  protected
@@ -13,8 +13,21 @@ module FriendlyId
13
13
  #
14
14
  # @see FriendlyId::ObjectUtils
15
15
  def find_one(id)
16
- return super if !@klass.respond_to?(:friendly_id) || id.unfriendly_id?
16
+ return super if id.unfriendly_id?
17
17
  where(@klass.friendly_id_config.query_field => id).first or super
18
18
  end
19
+
20
+ # FriendlyId overrides this method to make it possible to use friendly id's
21
+ # identically to numeric ids in finders.
22
+ #
23
+ # @example
24
+ # person = Person.exists?(123)
25
+ # person = Person.exists?("joe")
26
+ #
27
+ # @see FriendlyId::ObjectUtils
28
+ def exists?(id = nil)
29
+ return super if id.unfriendly_id?
30
+ super @klass.friendly_id_config.query_field => id
31
+ end
19
32
  end
20
33
  end
@@ -41,12 +41,8 @@ method.
41
41
  ...
42
42
 
43
43
  def find_post
44
- return unless params[:id]
45
- @post = begin
46
- Post.find params[:id]
47
- rescue ActiveRecord::RecordNotFound
48
- Post.find_by_friendly_id params[:id]
49
- end
44
+ Post.find params[:id]
45
+
50
46
  # If an old id or a numeric id was used to find the record, then
51
47
  # the request path will not match the post_path, and we should do
52
48
  # a 301 redirect that uses the current friendly id.
@@ -59,14 +55,13 @@ method.
59
55
  module History
60
56
 
61
57
  # Configures the model instance to use the History add-on.
62
- def self.included(klass)
63
- klass.instance_eval do
64
- raise "FriendlyId::History is incompatibe with FriendlyId::Scoped" if self < Scoped
58
+ def self.included(model_class)
59
+ model_class.instance_eval do
60
+ raise "FriendlyId::History is incompatible with FriendlyId::Scoped" if self < Scoped
65
61
  @friendly_id_config.use :slugged
66
62
  has_many :slugs, :as => :sluggable, :dependent => :destroy, :class_name => Slug.to_s
67
- before_save :build_slug, :if => lambda {|r| r.slug_sequencer.slug_changed?}
68
- scope :with_friendly_id, lambda {|id| includes(:slugs).where("#{Slug.table_name}.slug" => id)}
69
- extend Finder
63
+ before_save :build_slug, :if => lambda {|r| r.should_generate_new_friendly_id?}
64
+ relation_class.send :include, FinderMethods
70
65
  end
71
66
  end
72
67
 
@@ -75,14 +70,35 @@ method.
75
70
  def build_slug
76
71
  slugs.build :slug => friendly_id
77
72
  end
78
- end
79
73
 
80
- # Adds a finder that explictly uses slugs from the slug table.
81
- module Finder
74
+ # Adds a finder that explictly uses slugs from the slug table.
75
+ module FinderMethods
76
+
77
+ # Search for a record in the slugs table using the specified slug.
78
+ def find_one(id)
79
+ return super if id.unfriendly_id?
80
+ where(@klass.friendly_id_config.query_field => id).first or
81
+ with_old_friendly_id(id) {|x| find_one_without_friendly_id(x)} or
82
+ find_one_without_friendly_id(id)
83
+ end
82
84
 
83
- # Search for a record in the slugs table using the specified slug.
84
- def find_by_friendly_id(*args)
85
- with_friendly_id(args.shift).first(*args)
85
+ # Search for a record in the slugs table using the specified slug.
86
+ def exists?(id = nil)
87
+ return super if id.unfriendly_id?
88
+ exists_without_friendly_id?(@klass.friendly_id_config.query_field => id) or
89
+ with_old_friendly_id(id) {|x| exists_without_friendly_id?(x)} or
90
+ exists_without_friendly_id?(id)
91
+ end
92
+
93
+ private
94
+
95
+ # Accepts a slug, and yields a corresponding sluggable_id into the block.
96
+ def with_old_friendly_id(slug, &block)
97
+ sql = "SELECT sluggable_id FROM #{Slug.quoted_table_name} WHERE sluggable_type = %s AND slug = %s"
98
+ sql = sql % [@klass.base_class.name, slug].map {|x| connection.quote(x)}
99
+ sluggable_id = connection.select_values(sql).first
100
+ yield sluggable_id if sluggable_id
101
+ end
86
102
  end
87
103
  end
88
- end
104
+ end
@@ -6,20 +6,21 @@ module FriendlyId
6
6
  This module adds very basic i18n support to FriendlyId.
7
7
 
8
8
  In order to use this module, your model must have a slug column for each locale.
9
- The slug column for your default locale should, however, still be named "slug".
10
- An i18n module that uses external slugs and doesn't require a column for each
11
- locale is planned, but has not been created yet.
9
+ By default FriendlyId looks for columns named, for example, "slug_en",
10
+ "slug_es", etc. The first part of the name can be configured by passing the
11
+ +:slug_column+ option if you choose. Note that as of 4.0.0.beta11, the column
12
+ for the default locale must also include the locale in its name.
12
13
 
13
14
  == Example migration
14
15
 
15
16
  def self.up
16
17
  create_table :posts do |t|
17
18
  t.string :title
18
- t.string :slug
19
+ t.string :slug_en
19
20
  t.string :slug_es
20
21
  t.text :body
21
22
  end
22
- add_index :posts, :slug
23
+ add_index :posts, :slug_en
23
24
  add_index :posts, :slug_es
24
25
  end
25
26
 
@@ -59,6 +60,7 @@ current locale:
59
60
  end
60
61
  =end
61
62
  module I18n
63
+
62
64
  def self.included(model_class)
63
65
  model_class.instance_eval do
64
66
  friendly_id_config.use :slugged
@@ -70,15 +72,14 @@ current locale:
70
72
  module Model
71
73
  def set_friendly_id(text, locale = nil)
72
74
  ::I18n.with_locale(locale || ::I18n.current_locale) do
73
- slug = slug_sequencer(normalize_friendly_id(text)).generate
74
- send "#{friendly_id_config.slug_column}=", slug
75
+ set_slug(normalize_friendly_id(text))
75
76
  end
76
77
  end
77
78
  end
78
79
 
79
80
  module Configuration
80
81
  def slug_column
81
- ::I18n.locale == ::I18n.default_locale ? super : [super, "_", ::I18n.locale].join
82
+ "#{super}_#{::I18n.locale}"
82
83
  end
83
84
  end
84
85
  end
@@ -16,7 +16,11 @@ module FriendlyId
16
16
 
17
17
  # Either the friendly_id, or the numeric id cast to a string.
18
18
  def to_param
19
- (friendly_id.present? ? friendly_id : id).to_s
19
+ if diff = changes[friendly_id_config.query_field]
20
+ diff.first
21
+ else
22
+ friendly_id.present? ? friendly_id : id.to_s
23
+ end
20
24
  end
21
25
  end
22
26
  end
@@ -87,7 +87,7 @@ an example of one way to set this up:
87
87
  raise "FriendlyId::Scoped is incompatibe with FriendlyId::History" if self < History
88
88
  include Slugged unless self < Slugged
89
89
  friendly_id_config.class.send :include, Configuration
90
- friendly_id_config.slug_sequencer_class.send :include, SlugSequencer
90
+ friendly_id_config.slug_generator_class.send :include, SlugGenerator
91
91
  end
92
92
  end
93
93
 
@@ -112,13 +112,25 @@ an example of one way to set this up:
112
112
  #
113
113
  # @return String The scope column
114
114
  def scope_column
115
- (model_class.reflections[@scope].try(:association_foreign_key) || @scope).to_s
115
+ (reflection_foreign_key or @scope).to_s
116
+ end
117
+
118
+ private
119
+
120
+ if ActiveRecord::VERSION::STRING < "3.1"
121
+ def reflection_foreign_key
122
+ model_class.reflections[@scope].try(:primary_key_name)
123
+ end
124
+ else
125
+ def reflection_foreign_key
126
+ model_class.reflections[@scope].try(:foreign_key)
127
+ end
116
128
  end
117
129
  end
118
130
 
119
- # This module overrides {FriendlyId::SlugSequencer#conflict} to consider
131
+ # This module overrides {FriendlyId::SlugGenerator#conflict} to consider
120
132
  # scope, to avoid adding sequences to slugs under different scopes.
121
- module SlugSequencer
133
+ module SlugGenerator
122
134
 
123
135
  private
124
136
 
@@ -1,9 +1,10 @@
1
1
  module FriendlyId
2
- # This class offers functionality to check slug strings for uniqueness and,
3
- # if necessary, append a sequence to ensure it.
4
- class SlugSequencer
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
5
  attr_reader :sluggable, :normalized
6
6
 
7
+ # Create a new slug generator.
7
8
  def initialize(sluggable, normalized)
8
9
  @sluggable = sluggable
9
10
  @normalized = normalized
@@ -18,25 +19,11 @@ module FriendlyId
18
19
 
19
20
  # Generate a new sequenced slug.
20
21
  def generate
21
- if new_record? or slug_changed?
22
- conflict? ? self.next : normalized
23
- else
24
- sluggable.friendly_id
25
- end
26
- end
27
-
28
- # Whether or not the model instance's slug has changed.
29
- def slug_changed?
30
- separator = Regexp.escape friendly_id_config.sequence_separator
31
- base != sluggable.current_friendly_id.try(:sub, /#{separator}[\d]*\z/, '')
22
+ conflict? ? self.next : normalized
32
23
  end
33
24
 
34
25
  private
35
26
 
36
- def base
37
- sluggable.send friendly_id_config.base
38
- end
39
-
40
27
  def column
41
28
  sluggable.connection.quote_column_name friendly_id_config.slug_column
42
29
  end
@@ -64,10 +51,6 @@ module FriendlyId
64
51
  sluggable.friendly_id_config
65
52
  end
66
53
 
67
- def new_record?
68
- sluggable.new_record?
69
- end
70
-
71
54
  def separator
72
55
  friendly_id_config.sequence_separator
73
56
  end
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
- require "friendly_id/slug_sequencer"
2
+ require "friendly_id/slug_generator"
3
3
 
4
4
  module FriendlyId
5
5
  =begin
@@ -95,6 +95,27 @@ Here's an example of a class that uses a custom method to generate the slug:
95
95
  You can override {Slugged#normalize_friendly_id} in your model for total
96
96
  control over the slug format.
97
97
 
98
+ ==== Deciding when to generate new slugs
99
+
100
+ Overriding {Slugged#should_generate_new_friendly_id?} lets you control whether
101
+ new friendly ids are created when a model is updated. For example, if you only
102
+ want to generate slugs once and then treat them as read-only:
103
+
104
+ class Post < ActiveRecord::Base
105
+ extend FriendlyId
106
+ friendly_id :title, :use => :slugged
107
+
108
+ def should_generate_new_friendly_id?
109
+ new_record?
110
+ end
111
+ end
112
+
113
+ post = Post.create!(:title => "Hello world!")
114
+ post.slug #=> "hello-world"
115
+ post.title = "Hello there, world!"
116
+ post.save!
117
+ post.slug #=> "hello-world"
118
+
98
119
  ==== Locale-specific Transliterations
99
120
 
100
121
  Active Support's +parameterize+ uses
@@ -125,7 +146,7 @@ This functionality was in fact taken from earlier versions of FriendlyId.
125
146
  friendly_id_config.class.send :include, Configuration
126
147
  friendly_id_config.defaults[:slug_column] ||= 'slug'
127
148
  friendly_id_config.defaults[:sequence_separator] ||= '--'
128
- friendly_id_config.slug_sequencer_class ||= Class.new(SlugSequencer)
149
+ friendly_id_config.slug_generator_class ||= Class.new(SlugGenerator)
129
150
  before_validation :set_slug
130
151
  end
131
152
  end
@@ -169,26 +190,33 @@ This functionality was in fact taken from earlier versions of FriendlyId.
169
190
  value.to_s.parameterize
170
191
  end
171
192
 
172
- # Gets a new instance of the configured slug sequencing class.
193
+ # Whether to generate a new slug.
173
194
  #
174
- # @see FriendlyId::SlugSequencer
175
- def slug_sequencer(normalized = nil)
176
- normalized ||= normalize_friendly_id(send(friendly_id_config.base))
177
- friendly_id_config.slug_sequencer_class.new(self, normalized)
195
+ # You can override this method in your model if, for example, you only want
196
+ # slugs to be generated once, and then never updated.
197
+ def should_generate_new_friendly_id?
198
+ return true if new_record?
199
+ slug_base = send friendly_id_config.base
200
+ separator = Regexp.escape friendly_id_config.sequence_separator
201
+ slug_base != current_friendly_id.try(:sub, /#{separator}[\d]*\z/, '')
178
202
  end
179
203
 
180
204
  # Sets the slug.
181
- def set_slug
182
- send "#{friendly_id_config.slug_column}=", slug_sequencer.generate
205
+ def set_slug(normalized_slug = nil)
206
+ if should_generate_new_friendly_id?
207
+ normalized_slug ||= normalize_friendly_id send(friendly_id_config.base)
208
+ generator = friendly_id_config.slug_generator_class.new self, normalized_slug
209
+ send "#{friendly_id_config.slug_column}=", generator.generate
210
+ end
183
211
  end
184
212
  private :set_slug
185
213
 
186
214
  # This module adds the +:slug_column+, and +:sequence_separator+, and
187
- # +:slug_sequencer_class+ configuration options to
215
+ # +:slug_generator_class+ configuration options to
188
216
  # {FriendlyId::Configuration FriendlyId::Configuration}.
189
217
  module Configuration
190
218
  attr_writer :slug_column, :sequence_separator
191
- attr_accessor :slug_sequencer_class
219
+ attr_accessor :slug_generator_class
192
220
 
193
221
  # Makes FriendlyId use the slug column for querying.
194
222
  # @return String The slug column.
@@ -13,6 +13,8 @@ class FriendlyIdGenerator < Rails::Generators::Base
13
13
  migration_template 'migration.rb', 'db/migrate/create_friendly_id_slugs.rb'
14
14
  end
15
15
 
16
+ # TODO: use the module provided with Rails, no need to do this
17
+ # any more
16
18
  # Taken from ActiveRecord's migration generator
17
19
  def self.next_migration_number(dirname) #:nodoc:
18
20
  if ActiveRecord::Base.timestamped_migrations
@@ -21,4 +23,4 @@ class FriendlyIdGenerator < Rails::Generators::Base
21
23
  "%.3d" % (current_migration_number(dirname) + 1)
22
24
  end
23
25
  end
24
- end
26
+ end
@@ -1,10 +1,13 @@
1
1
  require File.expand_path("../helper.rb", __FILE__)
2
2
 
3
- Author, Book = 2.times.map do
4
- Class.new(ActiveRecord::Base) do
5
- extend FriendlyId
6
- friendly_id :name
7
- end
3
+ class Author < ActiveRecord::Base
4
+ extend FriendlyId
5
+ friendly_id :name
6
+ end
7
+
8
+ class Book < ActiveRecord::Base
9
+ extend FriendlyId
10
+ friendly_id :name
8
11
  end
9
12
 
10
13
  class CoreTest < MiniTest::Unit::TestCase
@@ -8,7 +8,7 @@ require "mocha"
8
8
  require "minitest/unit"
9
9
  require "active_record"
10
10
  require 'active_support/core_ext/time/conversions'
11
- # require "active_support/core_ext/class"
11
+
12
12
 
13
13
  if ENV["COVERAGE"]
14
14
  require 'simplecov'
@@ -47,11 +47,22 @@ module FriendlyId
47
47
  extend self
48
48
 
49
49
  def connect
50
- ActiveRecord::Base.establish_connection config[driver]
51
50
  version = ActiveRecord::VERSION::STRING
52
51
  driver = FriendlyId::Test::Database.driver
53
- engine = RUBY_ENGINE rescue "ruby"
52
+ engine = RUBY_ENGINE rescue "ruby"
53
+
54
+ # This hack is needed to get AR 3.1 + JDBC to play nicely together
55
+ if version >= "3.1" && engine == "jruby"
56
+ if driver == "sqlite3"
57
+ puts "Skipping SQLite3 test on JRuby with AR 3.1; it doesn't currently work."
58
+ exit 0
59
+ end
60
+ config.each { |_, value| value["adapter"] = "jdbc" + value["adapter"].gsub(/2\z/, '') }
61
+ end
62
+
63
+ ActiveRecord::Base.establish_connection config[driver]
54
64
  message = "Using #{engine} #{RUBY_VERSION} AR #{version} with #{driver}"
65
+
55
66
  puts "-" * 72
56
67
  if in_memory?
57
68
  ActiveRecord::Migration.verbose = false
@@ -85,4 +96,5 @@ end
85
96
 
86
97
  require "schema"
87
98
  require "shared"
88
- FriendlyId::Test::Database.connect
99
+ FriendlyId::Test::Database.connect
100
+ at_exit {ActiveRecord::Base.connection.disconnect!}
@@ -39,11 +39,25 @@ class HistoryTest < MiniTest::Unit::TestCase
39
39
  old_friendly_id = record.friendly_id
40
40
  record.name = record.name + "b"
41
41
  record.save!
42
- assert found = model_class.find_by_friendly_id(old_friendly_id)
43
- assert !found.readonly?
42
+ begin
43
+ assert model_class.find(old_friendly_id)
44
+ assert model_class.exists?(old_friendly_id), "should exist? by old id"
45
+ rescue ActiveRecord::RecordNotFound
46
+ flunk "Could not find record by old id"
47
+ end
44
48
  end
45
49
  end
46
50
 
51
+ test "should not be read only when found by old slug" do
52
+ with_instance_of(model_class) do |record|
53
+ old_friendly_id = record.friendly_id
54
+ record.name = record.name + "b"
55
+ record.save!
56
+ assert !model_class.find(old_friendly_id).readonly?
57
+ end
58
+ end
59
+
60
+
47
61
  test "should raise error if used with scoped" do
48
62
  model_class = Class.new(ActiveRecord::Base)
49
63
  model_class.extend FriendlyId
@@ -26,14 +26,14 @@ class I18nTest < MiniTest::Unit::TestCase
26
26
  I18n.with_locale(I18n.default_locale) do
27
27
  journalist = Journalist.new(:name => "John Doe")
28
28
  journalist.valid?
29
- assert_equal "john-doe", journalist.slug
29
+ assert_equal "john-doe", journalist.slug_en
30
30
  assert_nil journalist.slug_es
31
31
  end
32
32
  I18n.with_locale(:es) do
33
33
  journalist = Journalist.new(:name => "John Doe")
34
34
  journalist.valid?
35
35
  assert_equal "john-doe", journalist.slug_es
36
- assert_nil journalist.slug
36
+ assert_nil journalist.slug_en
37
37
  end
38
38
  end
39
39
 
@@ -92,9 +92,9 @@ class I18nTest < MiniTest::Unit::TestCase
92
92
  end
93
93
  end
94
94
 
95
- test "should not add locale to slug column for default locale" do
95
+ test "should add locale to slug column for default locale" do
96
96
  I18n.with_locale(I18n.default_locale) do
97
- assert_equal "slug", Journalist.friendly_id_config.slug_column
97
+ assert_equal "slug_en", Journalist.friendly_id_config.slug_column
98
98
  end
99
99
  end
100
100
  end
@@ -39,9 +39,10 @@ module FriendlyId
39
39
  add_column :journalists, "type", :string
40
40
 
41
41
  # These will be used to test i18n
42
+ add_column :journalists, "slug_en", :string
42
43
  add_column :journalists, "slug_es", :string
43
44
  add_column :journalists, "slug_de", :string
44
-
45
+
45
46
  @done = true
46
47
  end
47
48
 
@@ -25,7 +25,7 @@ module FriendlyId
25
25
  test "should not add slug sequence on update after other conflicting slugs were added" do
26
26
  with_instance_of model_class do |record|
27
27
  old = record.friendly_id
28
- record2 = model_class.create! :name => record.name
28
+ model_class.create! :name => record.name
29
29
  record.save!
30
30
  record.reload
31
31
  assert_equal old, record.to_param
@@ -66,6 +66,15 @@ module FriendlyId
66
66
  with_instance_of(model_class) {|record| assert model_class.find record.friendly_id}
67
67
  end
68
68
 
69
+ test "should exist? by friendly id" do
70
+ with_instance_of(model_class) do |record|
71
+ assert model_class.exists? record.id
72
+ assert model_class.exists? record.friendly_id
73
+ assert !model_class.exists?(record.friendly_id + "-hello")
74
+ assert !model_class.exists?(0)
75
+ end
76
+ end
77
+
69
78
  test "should be findable by id as integer" do
70
79
  with_instance_of(model_class) {|record| assert model_class.find record.id.to_i}
71
80
  end
@@ -1,10 +1,13 @@
1
1
  require File.expand_path("../helper.rb", __FILE__)
2
2
 
3
- Journalist, Article = 2.times.map do
4
- Class.new(ActiveRecord::Base) do
5
- extend FriendlyId
6
- friendly_id :name, :use => :slugged
7
- end
3
+ class Journalist < ActiveRecord::Base
4
+ extend FriendlyId
5
+ friendly_id :name, :use => :slugged
6
+ end
7
+
8
+ class Article < ActiveRecord::Base
9
+ extend FriendlyId
10
+ friendly_id :name, :use => :slugged
8
11
  end
9
12
 
10
13
  class SluggedTest < MiniTest::Unit::TestCase
@@ -27,7 +30,7 @@ class SluggedTest < MiniTest::Unit::TestCase
27
30
 
28
31
  end
29
32
 
30
- class SlugSequencerTest < MiniTest::Unit::TestCase
33
+ class SlugGeneratorTest < MiniTest::Unit::TestCase
31
34
 
32
35
  include FriendlyId::Test
33
36
 
@@ -67,9 +70,9 @@ class SlugSeparatorTest < MiniTest::Unit::TestCase
67
70
  test "should detect when a sequenced slug has changed" do
68
71
  with_instance_of model_class do |record|
69
72
  record2 = model_class.create! :name => record.name
70
- assert !record2.slug_sequencer.slug_changed?
73
+ assert !record2.should_generate_new_friendly_id?
71
74
  record2.name = "hello world"
72
- assert record2.slug_sequencer.slug_changed?
75
+ assert record2.should_generate_new_friendly_id?
73
76
  end
74
77
  end
75
78
  end
@@ -114,3 +117,27 @@ class SluggedRegressionsTest < MiniTest::Unit::TestCase
114
117
  end
115
118
  end
116
119
  end
120
+
121
+ # https://github.com/norman/friendly_id/issues/148
122
+ class FailedValidationAfterUpdateRegressionTest < MiniTest::Unit::TestCase
123
+ include FriendlyId::Test
124
+
125
+ class Journalist < ActiveRecord::Base
126
+ extend FriendlyId
127
+ friendly_id :name, :use => :slugged
128
+ validates_presence_of :slug_de
129
+ end
130
+
131
+ test "to_param should return the unchanged value if the slug changes before validation fails" do
132
+ transaction do
133
+ journalist = Journalist.create! :name => "Joseph Pulitzer", :slug_de => "value"
134
+ assert_equal "joseph-pulitzer", journalist.to_param
135
+ assert journalist.valid?
136
+ assert journalist.persisted?
137
+ journalist.name = "Joe Pulitzer"
138
+ journalist.slug_de = nil
139
+ assert !journalist.valid?
140
+ assert_equal "joseph-pulitzer", journalist.to_param
141
+ end
142
+ end
143
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: friendly_id
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0.beta10
4
+ version: 4.0.0.beta11
5
5
  prerelease: 6
6
6
  platform: ruby
7
7
  authors:
@@ -9,22 +9,22 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-08-31 00:00:00.000000000 Z
12
+ date: 2011-09-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
16
- requirement: &70208447070040 !ruby/object:Gem::Requirement
16
+ requirement: &70177920323920 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
20
20
  - !ruby/object:Gem::Version
21
- version: '3.0'
21
+ version: '3.1'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *70208447070040
24
+ version_requirements: *70177920323920
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: sqlite3
27
- requirement: &70208447069380 !ruby/object:Gem::Requirement
27
+ requirement: &70177920323260 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '1.3'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *70208447069380
35
+ version_requirements: *70177920323260
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: minitest
38
- requirement: &70208447068760 !ruby/object:Gem::Requirement
38
+ requirement: &70177920322440 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ~>
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: 2.4.0
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *70208447068760
46
+ version_requirements: *70177920322440
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: mocha
49
- requirement: &70208447068060 !ruby/object:Gem::Requirement
49
+ requirement: &70177920321640 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ~>
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: 0.9.12
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *70208447068060
57
+ version_requirements: *70177920321640
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: ffaker
60
- requirement: &70208447067380 !ruby/object:Gem::Requirement
60
+ requirement: &70177920321060 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ~>
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: 1.8.0
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *70208447067380
68
+ version_requirements: *70177920321060
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: maruku
71
- requirement: &70208447066800 !ruby/object:Gem::Requirement
71
+ requirement: &70177920499500 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ~>
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: 0.6.0
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *70208447066800
79
+ version_requirements: *70177920499500
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: yard
82
- requirement: &70208447066100 !ruby/object:Gem::Requirement
82
+ requirement: &70177920498640 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ~>
@@ -87,18 +87,29 @@ dependencies:
87
87
  version: 0.7.2
88
88
  type: :development
89
89
  prerelease: false
90
- version_requirements: *70208447066100
90
+ version_requirements: *70177920498640
91
91
  - !ruby/object:Gem::Dependency
92
92
  name: i18n
93
- requirement: &70208447065460 !ruby/object:Gem::Requirement
93
+ requirement: &70177920497740 !ruby/object:Gem::Requirement
94
94
  none: false
95
95
  requirements:
96
96
  - - ~>
97
97
  - !ruby/object:Gem::Version
98
- version: 0.5.0
98
+ version: 0.6.0
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: *70177920497740
102
+ - !ruby/object:Gem::Dependency
103
+ name: simplecov
104
+ requirement: &70177920496960 !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
99
110
  type: :development
100
111
  prerelease: false
101
- version_requirements: *70208447065460
112
+ version_requirements: *70177920496960
102
113
  description: ! 'FriendlyId is the "Swiss Army bulldozer" of slugging and permalink
103
114
  plugins for
104
115
 
@@ -126,9 +137,7 @@ files:
126
137
  - bench.rb
127
138
  - friendly_id.gemspec
128
139
  - gemfiles/Gemfile.rails-3.0.rb
129
- - gemfiles/Gemfile.rails-3.0.rb.lock
130
140
  - gemfiles/Gemfile.rails-3.1.rb
131
- - gemfiles/Gemfile.rails-3.1.rb.lock
132
141
  - lib/friendly_id.rb
133
142
  - lib/friendly_id/base.rb
134
143
  - lib/friendly_id/configuration.rb
@@ -141,7 +150,7 @@ files:
141
150
  - lib/friendly_id/reserved.rb
142
151
  - lib/friendly_id/scoped.rb
143
152
  - lib/friendly_id/slug.rb
144
- - lib/friendly_id/slug_sequencer.rb
153
+ - lib/friendly_id/slug_generator.rb
145
154
  - lib/friendly_id/slugged.rb
146
155
  - lib/generators/friendly_id_generator.rb
147
156
  - test/base_test.rb
@@ -1,52 +0,0 @@
1
- GEM
2
- remote: http://rubygems.org/
3
- specs:
4
- activemodel (3.0.10)
5
- activesupport (= 3.0.10)
6
- builder (~> 2.1.2)
7
- i18n (~> 0.5.0)
8
- activerecord (3.0.10)
9
- activemodel (= 3.0.10)
10
- activesupport (= 3.0.10)
11
- arel (~> 2.0.10)
12
- tzinfo (~> 0.3.23)
13
- activerecord-jdbc-adapter (1.1.3)
14
- activerecord-jdbcmysql-adapter (1.1.3)
15
- activerecord-jdbc-adapter (= 1.1.3)
16
- jdbc-mysql (~> 5.1.0)
17
- activerecord-jdbcpostgresql-adapter (1.1.3)
18
- activerecord-jdbc-adapter (= 1.1.3)
19
- jdbc-postgres (~> 9.0.0)
20
- activerecord-jdbcsqlite3-adapter (1.1.3)
21
- activerecord-jdbc-adapter (= 1.1.3)
22
- jdbc-sqlite3 (~> 3.7.0)
23
- activesupport (3.0.10)
24
- arel (2.0.10)
25
- builder (2.1.2)
26
- i18n (0.5.0)
27
- jdbc-mysql (5.1.13)
28
- jdbc-postgres (9.0.801)
29
- jdbc-sqlite3 (3.7.2)
30
- minitest (2.4.0)
31
- mocha (0.9.12)
32
- mysql2 (0.2.11)
33
- pg (0.11.0)
34
- rake (0.9.2)
35
- sqlite3 (1.3.4)
36
- tzinfo (0.3.29)
37
-
38
- PLATFORMS
39
- java
40
- ruby
41
-
42
- DEPENDENCIES
43
- activerecord (= 3.0.10)
44
- activerecord-jdbcmysql-adapter
45
- activerecord-jdbcpostgresql-adapter
46
- activerecord-jdbcsqlite3-adapter
47
- minitest (~> 2.4.0)
48
- mocha (~> 0.9.12)
49
- mysql2 (~> 0.2.0)
50
- pg (~> 0.11.0)
51
- rake
52
- sqlite3 (~> 1.3.4)
@@ -1,57 +0,0 @@
1
- GEM
2
- remote: http://rubygems.org/
3
- specs:
4
- activemodel (3.1.0)
5
- activesupport (= 3.1.0)
6
- bcrypt-ruby (~> 3.0.0)
7
- builder (~> 3.0.0)
8
- i18n (~> 0.6)
9
- activerecord (3.1.0)
10
- activemodel (= 3.1.0)
11
- activesupport (= 3.1.0)
12
- arel (~> 2.2.1)
13
- tzinfo (~> 0.3.29)
14
- activerecord-jdbc-adapter (1.1.3)
15
- activerecord-jdbcmysql-adapter (1.1.3)
16
- activerecord-jdbc-adapter (= 1.1.3)
17
- jdbc-mysql (~> 5.1.0)
18
- activerecord-jdbcpostgresql-adapter (1.1.3)
19
- activerecord-jdbc-adapter (= 1.1.3)
20
- jdbc-postgres (~> 9.0.0)
21
- activerecord-jdbcsqlite3-adapter (1.1.3)
22
- activerecord-jdbc-adapter (= 1.1.3)
23
- jdbc-sqlite3 (~> 3.7.0)
24
- activesupport (3.1.0)
25
- multi_json (~> 1.0)
26
- arel (2.2.1)
27
- bcrypt-ruby (3.0.0)
28
- bcrypt-ruby (3.0.0-java)
29
- builder (3.0.0)
30
- i18n (0.6.0)
31
- jdbc-mysql (5.1.13)
32
- jdbc-postgres (9.0.801)
33
- jdbc-sqlite3 (3.7.2)
34
- minitest (2.4.0)
35
- mocha (0.9.12)
36
- multi_json (1.0.3)
37
- mysql2 (0.3.6)
38
- pg (0.11.0)
39
- rake (0.9.2)
40
- sqlite3 (1.3.4)
41
- tzinfo (0.3.29)
42
-
43
- PLATFORMS
44
- java
45
- ruby
46
-
47
- DEPENDENCIES
48
- activerecord (~> 3.1.0)
49
- activerecord-jdbcmysql-adapter
50
- activerecord-jdbcpostgresql-adapter
51
- activerecord-jdbcsqlite3-adapter
52
- minitest (~> 2.4.0)
53
- mocha (~> 0.9.12)
54
- mysql2 (~> 0.3.6)
55
- pg (~> 0.11.0)
56
- rake
57
- sqlite3 (~> 1.3.4)