friendly_id4 4.0.0.pre → 4.0.0.pre3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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__)