friendly_id 4.0.0.beta10 → 4.0.0.beta11

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.
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)