friendly_id4 4.0.0.pre → 4.0.0.pre3

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.
@@ -0,0 +1,14 @@
1
+ module FriendlyId
2
+ # These methods will override the finder methods in ActiveRecord::Relation.
3
+ module FinderMethods
4
+
5
+ protected
6
+
7
+ def find_one(id)
8
+ return super if !@klass.uses_friendly_id? or id.unfriendly_id?
9
+ where(@klass.friendly_id_config.query_field => id).first or super
10
+ end
11
+ end
12
+ end
13
+
14
+ ActiveRecord::Relation.send :include, FriendlyId::FinderMethods
@@ -0,0 +1,29 @@
1
+ module FriendlyId
2
+ module History
3
+
4
+ def self.included(base)
5
+ base.class_eval do
6
+ include Slugged unless include? Slugged
7
+ extend Finder
8
+ has_many :friendly_id_slugs, :as => :sluggable, :dependent => :destroy
9
+ before_save :build_friendly_id_slug, :if => lambda {|r| r.slug_sequencer.slug_changed?}
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def build_friendly_id_slug
16
+ self.friendly_id_slugs.build :slug => friendly_id
17
+ end
18
+ end
19
+
20
+ module Finder
21
+ def find_by_friendly_id(*args)
22
+ where("friendly_id_slugs.slug = ?", args.shift).includes(:friendly_id_slugs).first(*args)
23
+ end
24
+ end
25
+ end
26
+
27
+ class FriendlyIdSlug < ActiveRecord::Base
28
+ belongs_to :sluggable, :polymorphic => true
29
+ end
@@ -0,0 +1,17 @@
1
+ class CreateFriendlyIdSlugs < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :friendly_id_slugs do |t|
4
+ t.string :slug, :null => false
5
+ t.integer :sluggable_id, :null => false
6
+ t.string :sluggable_type, :limit => 40
7
+ t.datetime :created_at
8
+ end
9
+ add_index :friendly_id_slugs, :sluggable_id
10
+ add_index :friendly_id_slugs, [:slug, :sluggable_type], :unique => true
11
+ add_index :friendly_id_slugs, :sluggable_type
12
+ end
13
+
14
+ def self.down
15
+ drop_table :slugs
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ module FriendlyId
2
+ # Instance methods that will be added to all classes using FriendlyId.
3
+ module Model
4
+
5
+ attr_reader :current_friendly_id
6
+
7
+ # Convenience method for accessing the class method of the same name.
8
+ def friendly_id_config
9
+ self.class.friendly_id_config
10
+ end
11
+
12
+ # Get the instance's friendly_id.
13
+ def friendly_id
14
+ send friendly_id_config.query_field
15
+ end
16
+
17
+ # Either the friendly_id, or the numeric id cast to a string.
18
+ def to_param
19
+ (friendly_id or id).to_s
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ module FriendlyId
2
+ # Utility methods that are in Object because it's impossible to predict what
3
+ # kinds of objects get passed into FinderMethods#find_one and
4
+ # Model#normalize_friendly_id.
5
+ module ObjectUtils
6
+
7
+ # True is the id is definitely friendly, false if definitely unfriendly,
8
+ # else nil.
9
+ def friendly_id?
10
+ if kind_of?(Integer) or kind_of?(Symbol) or self.class.respond_to? :friendly_id_config
11
+ false
12
+ elsif to_i.to_s != to_s
13
+ true
14
+ end
15
+ end
16
+
17
+ # True if the id is definitely unfriendly, false if definitely friendly,
18
+ # else nil.
19
+ def unfriendly_id?
20
+ val = friendly_id? ; !val unless val.nil?
21
+ end
22
+ end
23
+ end
24
+
25
+ class Object
26
+ include FriendlyId::ObjectUtils
27
+ end
@@ -2,7 +2,18 @@ require "friendly_id/slugged"
2
2
 
3
3
  module FriendlyId
4
4
 
5
- # This module adds scopes to in-table slugs.
5
+ # This module adds scopes to in-table slugs. It's not loaded by default,
6
+ # so in order to active this feature you must include the module in your
7
+ # class.
8
+ #
9
+ # You can scope by an explicit column, or by a `belongs_to` relation.
10
+ #
11
+ # @example
12
+ # class Restaurant < ActiveRecord::Base
13
+ # belongs_to :city
14
+ # include FriendlyId::Scoped
15
+ # has_friendly_id :name, :scope => :city
16
+ # end
6
17
  module Scoped
7
18
  def self.included(klass)
8
19
  klass.send :include, Slugged unless klass.include? Slugged
@@ -10,8 +21,12 @@ module FriendlyId
10
21
  end
11
22
 
12
23
  class SlugSequencer
24
+ private
25
+
13
26
  alias conflict_without_scope conflict
14
27
 
28
+ # Checks for naming conflicts, taking scopes into account.
29
+ # @return ActiveRecord::Base
15
30
  def conflict_with_scope
16
31
  column = friendly_id_config.scope_column
17
32
  conflicts.where("#{column} = ?", sluggable.send(column)).first
@@ -25,8 +40,15 @@ module FriendlyId
25
40
  class Configuration
26
41
  attr_accessor :scope
27
42
 
43
+ # Gets the scope column.
44
+ #
45
+ # Checks to see if the +:scope+ option passed to {#has_friendly_id}
46
+ # refers to a relation, and if so, returns the realtion's foreign key.
47
+ # Otherwise it assumes the option value was the name of column and returns
48
+ # it cast to a String.
49
+ # @return String The scope column
28
50
  def scope_column
29
- klass.reflections[@scope].try(:association_foreign_key) || @scope.to_s
51
+ (klass.reflections[@scope].try(:association_foreign_key) || @scope).to_s
30
52
  end
31
53
  end
32
54
  end
@@ -0,0 +1,79 @@
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
5
+ attr_reader :sluggable
6
+
7
+ def initialize(sluggable)
8
+ @sluggable = sluggable
9
+ end
10
+
11
+ def next
12
+ sequence = conflict.slug.split(separator)[1].to_i
13
+ next_sequence = sequence == 0 ? 2 : sequence.next
14
+ "#{normalized}#{separator}#{next_sequence}"
15
+ end
16
+
17
+ def generate
18
+ if slug_changed? or new_record?
19
+ conflict? ? self.next : normalized
20
+ else
21
+ sluggable.friendly_id
22
+ end
23
+ end
24
+
25
+ def slug_changed?
26
+ base != sluggable.current_friendly_id.try(:sub, /--[\d]*\z/, '')
27
+ end
28
+
29
+ private
30
+
31
+ def base
32
+ sluggable.send friendly_id_config.base
33
+ end
34
+
35
+ def column
36
+ friendly_id_config.query_field
37
+ end
38
+
39
+ def conflict?
40
+ !! conflict
41
+ end
42
+
43
+ def conflict
44
+ unless defined? @conflict
45
+ @conflict = conflicts.first
46
+ end
47
+ @conflict
48
+ end
49
+
50
+ # @NOTE AR-specific code here
51
+ def conflicts
52
+ pkey = sluggable.class.arel_table.primary_key.name
53
+ value = sluggable.send pkey
54
+ scope = sluggable.class.where("#{column} = ? OR #{column} LIKE ?", normalized, wildcard)
55
+ scope = scope.where("#{pkey} <> ?", value) unless sluggable.new_record?
56
+ scope = scope.order("#{column} DESC")
57
+ end
58
+
59
+ def friendly_id_config
60
+ sluggable.friendly_id_config
61
+ end
62
+
63
+ def new_record?
64
+ sluggable.new_record?
65
+ end
66
+
67
+ def normalized
68
+ @normalized ||= sluggable.normalize_friendly_id(base)
69
+ end
70
+
71
+ def separator
72
+ friendly_id_config.sequence_separator
73
+ end
74
+
75
+ def wildcard
76
+ "#{normalized}#{separator}%"
77
+ end
78
+ end
79
+ end
@@ -1,3 +1,5 @@
1
+ require "friendly_id/slug_sequencer"
2
+
1
3
  module FriendlyId
2
4
 
3
5
  # This module adds in-table slugs to an ActiveRecord model.
@@ -14,10 +16,14 @@ module FriendlyId
14
16
  value.to_s.parameterize
15
17
  end
16
18
 
19
+ def slug_sequencer
20
+ SlugSequencer.new(self)
21
+ end
22
+
17
23
  private
18
24
 
19
25
  def set_slug
20
- send "#{friendly_id_config.slug_column}=", SlugSequencer.new(self).to_s
26
+ send "#{friendly_id_config.slug_column}=", slug_sequencer.generate
21
27
  end
22
28
  end
23
29
 
@@ -27,101 +33,22 @@ module FriendlyId
27
33
  DEFAULTS[:slug_column] = 'slug'
28
34
  DEFAULTS[:sequence_separator] = '--'
29
35
 
36
+ undef query_field
37
+
30
38
  def query_field
31
39
  use_slugs? ? slug_column : base
32
40
  end
33
41
 
34
42
  def sequence_separator
35
- @sequence_separator || DEFAULTS[:sequence_separator]
43
+ @sequence_separator ||= DEFAULTS[:sequence_separator]
36
44
  end
37
45
 
38
46
  def slug_column
39
- @slug_column || DEFAULTS[:slug_column]
47
+ @slug_column ||= DEFAULTS[:slug_column]
40
48
  end
41
49
 
42
50
  def use_slugs?
43
51
  @use_slugs
44
52
  end
45
53
  end
46
-
47
- # This class offers functionality to check slug strings for uniqueness and,
48
- # if necessary, append a sequence to ensure it.
49
- class SlugSequencer
50
- attr_reader :sluggable
51
-
52
- def initialize(sluggable)
53
- @sluggable = sluggable
54
- end
55
-
56
- def base
57
- sluggable.send friendly_id_config.base
58
- end
59
-
60
- def changed?
61
- base != current_friendly_id.try(:sub, /--[\d]*\z/, '')
62
- end
63
-
64
- def column
65
- friendly_id_config.query_field
66
- end
67
-
68
- def conflict?
69
- !! conflict
70
- end
71
-
72
- def conflict
73
- unless defined? @conflict
74
- @conflict = conflicts.first
75
- end
76
- @conflict
77
- end
78
-
79
- # @NOTE AR-specific code here
80
- def conflicts
81
- pkey = sluggable.class.arel_table.primary_key.name
82
- value = sluggable.send pkey
83
- scope = sluggable.class.where("#{column} = ? OR #{column} LIKE ?", normalized, wildcard)
84
- scope = scope.where("#{pkey} <> ?", value) unless sluggable.new_record?
85
- scope = scope.order("#{column} DESC")
86
- end
87
-
88
- def current_friendly_id
89
- sluggable.instance_variable_get(:@_current_friendly_id)
90
- end
91
-
92
- def friendly_id_config
93
- sluggable.friendly_id_config
94
- end
95
-
96
- def new_record?
97
- sluggable.new_record?
98
- end
99
-
100
- def normalized
101
- @normalized ||= sluggable.normalize_friendly_id(base)
102
- end
103
-
104
- def separator
105
- friendly_id_config.sequence_separator
106
- end
107
-
108
- def next
109
- sequence = conflict.slug.split(separator)[1].to_i
110
- next_sequence = sequence == 0 ? 2 : sequence.next
111
- "#{normalized}#{separator}#{next_sequence}"
112
- end
113
-
114
- def to_s
115
- if changed? or new_record?
116
- conflict? ? self.next : normalized
117
- else
118
- sluggable.friendly_id
119
- end
120
- end
121
-
122
- def wildcard
123
- "#{normalized}#{separator}%"
124
- end
125
- end
126
-
127
54
  end
@@ -3,7 +3,7 @@ module FriendlyId
3
3
  MAJOR = 4
4
4
  MINOR = 0
5
5
  TINY = 0
6
- BUILD = 'pre'
6
+ BUILD = 'pre3'
7
7
  STRING = [MAJOR, MINOR, TINY, BUILD].compact.join('.')
8
8
  end
9
9
  end
@@ -0,0 +1,21 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+
4
+ class FriendlyIdGenerator < Rails::Generators::Base
5
+ include Rails::Generators::Migration
6
+
7
+ source_root File.expand_path('../../friendly_id', __FILE__)
8
+
9
+ def copy_files(*args)
10
+ migration_template 'migration.rb', 'db/migrate/create_friendly_id_slugs.rb'
11
+ end
12
+
13
+ # Taken from ActiveRecord's migration generator
14
+ def self.next_migration_number(dirname) #:nodoc:
15
+ if ActiveRecord::Base.timestamped_migrations
16
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
17
+ else
18
+ "%.3d" % (current_migration_number(dirname) + 1)
19
+ end
20
+ end
21
+ end
@@ -1,68 +1,35 @@
1
- require File.expand_path("../test_helper", __FILE__)
1
+ require File.expand_path("../helper.rb", __FILE__)
2
2
 
3
- class ModelTest < Test::Unit::TestCase
3
+ setup { Class.new ActiveRecord::Base }
4
4
 
5
- include FriendlyId::Test::Generic
6
-
7
- def instance(name = "user")
8
- @instance ||= klass.create!(:name => name)
9
- end
10
-
11
- # @TODO - this kind of setup is repeated in a few places and should probably
12
- # be abstracted.
13
- def klass
14
- @@klass ||= make_model("core", :name) do |t|
15
- t.string :name, :unique => true
16
- t.boolean :active, :default => true
17
- end
18
- @@klass
19
- end
20
-
21
- def other_class
22
- @@other_class ||= make_model("core_two", :name) do |t|
23
- t.string :name, :unique => true
24
- t.boolean :active, :default => true
25
- end
26
- end
27
-
28
- def setup
29
- klass.stubs(:name).returns("Cores")
30
- other_class.stubs(:name).returns("CoreTwos")
31
- end
32
-
33
- test "models don't use friendly_id by default" do
34
- assert !Class.new(ActiveRecord::Base).uses_friendly_id?
35
- end
5
+ test "models don't use friendly_id by default" do |klass|
6
+ assert !klass.uses_friendly_id?
7
+ end
36
8
 
37
- test "integers should be unfriendly ids" do
38
- assert 1.unfriendly_id?
39
- end
9
+ test "model classes should have a friendly id config" do |klass|
10
+ assert klass.has_friendly_id(:name).friendly_id_config
11
+ end
40
12
 
41
- test "ActiveRecord::Base instances should be unfriendly_ids" do
42
- assert klass.new.unfriendly_id?
13
+ test "should raise error when bad config options are set" do |klass|
14
+ assert_raise ArgumentError do
15
+ klass.has_friendly_id :name, :garbage => :in
43
16
  end
17
+ end
44
18
 
45
- test "numeric strings are neither friendly nor unfriendly" do
46
- assert_equal nil, "1".friendly_id?
47
- assert_equal nil, "1".unfriendly_id?
48
- end
19
+ [User, Book].map {|klass| klass.has_friendly_id :name}
49
20
 
50
- test "strings with letters are friendly_ids" do
51
- assert "a".friendly_id?
52
- end
21
+ setup {User}
53
22
 
54
- test "should raise error when bad config options are set" do
55
- assert_raises ArgumentError do
56
- klass.has_friendly_id :smeg, :garbage => :in
57
- end
58
- end
59
-
60
- test "reserves 'new' and 'edit' by default" do
61
- FriendlyId::Configuration::DEFAULTS[:reserved_words].each do |word|
62
- assert_raises ActiveRecord::RecordInvalid do
63
- klass.create! :name => word
64
- end
23
+ test "should reserve 'new' and 'edit' by default" do |klass|
24
+ ["new", "edit"].each do |word|
25
+ transaction do
26
+ assert_raise(ActiveRecord::RecordInvalid) {klass.create! :name => word}
65
27
  end
66
28
  end
29
+ end
67
30
 
31
+ test "instances should have a friendly id" do |klass|
32
+ with_instance_of(klass) {|record| assert record.friendly_id}
68
33
  end
34
+
35
+ require File.expand_path("../shared.rb", __FILE__)