friendly_id4 4.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,27 @@
1
+ require "friendly_id/slug"
2
+
3
+ module FriendlyId
4
+ module History
5
+
6
+ def self.included(base)
7
+ base.class_eval do
8
+ include Slugged unless include? Slugged
9
+ extend Finder
10
+ has_many :friendly_id_slugs, :as => :sluggable, :dependent => :destroy
11
+ before_save :build_friendly_id_slug, :if => lambda {|r| r.slug_sequencer.slug_changed?}
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def build_friendly_id_slug
18
+ self.friendly_id_slugs.build :slug => friendly_id
19
+ end
20
+ end
21
+
22
+ module Finder
23
+ def find_by_friendly_id(*args)
24
+ where("friendly_id_slugs.slug = ?", args.shift).includes(:friendly_id_slugs).first(*args)
25
+ end
26
+ end
27
+ 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 :friendly_id_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.present? ? friendly_id : 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
@@ -0,0 +1,54 @@
1
+ require "friendly_id/slugged"
2
+
3
+ module FriendlyId
4
+
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
17
+ module Scoped
18
+ def self.included(klass)
19
+ klass.send :include, Slugged unless klass.include? Slugged
20
+ end
21
+ end
22
+
23
+ class SlugSequencer
24
+ private
25
+
26
+ alias conflict_without_scope conflict
27
+
28
+ # Checks for naming conflicts, taking scopes into account.
29
+ # @return ActiveRecord::Base
30
+ def conflict_with_scope
31
+ column = friendly_id_config.scope_column
32
+ conflicts.where("#{column} = ?", sluggable.send(column)).first
33
+ end
34
+
35
+ def conflict
36
+ friendly_id_config.scope ? conflict_with_scope : conflict_without_scope
37
+ end
38
+ end
39
+
40
+ class Configuration
41
+ attr_accessor :scope
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
50
+ def scope_column
51
+ (klass.reflections[@scope].try(:association_foreign_key) || @scope).to_s
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,3 @@
1
+ class FriendlyIdSlug < ActiveRecord::Base
2
+ belongs_to :sluggable, :polymorphic => true
3
+ end
@@ -0,0 +1,80 @@
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
+ separator = Regexp.escape friendly_id_config.sequence_separator
27
+ base != sluggable.current_friendly_id.try(:sub, /#{separator}[\d]*\z/, '')
28
+ end
29
+
30
+ private
31
+
32
+ def base
33
+ sluggable.send friendly_id_config.base
34
+ end
35
+
36
+ def column
37
+ sluggable.connection.quote_column_name friendly_id_config.query_field
38
+ end
39
+
40
+ def conflict?
41
+ !! conflict
42
+ end
43
+
44
+ def conflict
45
+ unless defined? @conflict
46
+ @conflict = conflicts.first
47
+ end
48
+ @conflict
49
+ end
50
+
51
+ # @NOTE AR-specific code here
52
+ def conflicts
53
+ pkey = sluggable.class.primary_key
54
+ value = sluggable.send pkey
55
+ scope = sluggable.class.where("#{column} = ? OR #{column} LIKE ?", normalized, wildcard)
56
+ scope = scope.where("#{pkey} <> ?", value) unless sluggable.new_record?
57
+ scope = scope.order("LENGTH(#{column}) DESC, #{column} DESC")
58
+ end
59
+
60
+ def friendly_id_config
61
+ sluggable.friendly_id_config
62
+ end
63
+
64
+ def new_record?
65
+ sluggable.new_record?
66
+ end
67
+
68
+ def normalized
69
+ @normalized ||= sluggable.normalize_friendly_id(base)
70
+ end
71
+
72
+ def separator
73
+ friendly_id_config.sequence_separator
74
+ end
75
+
76
+ def wildcard
77
+ "#{normalized}#{separator}%"
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,51 @@
1
+ require "friendly_id/slug_sequencer"
2
+
3
+ module FriendlyId
4
+
5
+ # This module adds in-table slugs to an ActiveRecord model.
6
+ module Slugged
7
+
8
+ # @NOTE AR-specific code here
9
+ def self.included(klass)
10
+ klass.before_save :set_slug
11
+ klass.friendly_id_config.use_slugs = true
12
+ end
13
+
14
+ # @NOTE AS-specific code here
15
+ def normalize_friendly_id(value)
16
+ value.to_s.parameterize
17
+ end
18
+
19
+ def slug_sequencer
20
+ SlugSequencer.new(self)
21
+ end
22
+
23
+ private
24
+
25
+ def set_slug
26
+ send "#{friendly_id_config.slug_column}=", slug_sequencer.generate
27
+ end
28
+ end
29
+
30
+ class Configuration
31
+ attr :use_slugs
32
+ attr_writer :slug_column, :sequence_separator, :use_slugs
33
+
34
+ DEFAULTS[:slug_column] = 'slug'
35
+ DEFAULTS[:sequence_separator] = '--'
36
+
37
+ undef query_field
38
+
39
+ def query_field
40
+ use_slugs ? slug_column : base
41
+ end
42
+
43
+ def sequence_separator
44
+ @sequence_separator ||= DEFAULTS[:sequence_separator]
45
+ end
46
+
47
+ def slug_column
48
+ @slug_column ||= DEFAULTS[:slug_column]
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,9 @@
1
+ module FriendlyId
2
+ module Version
3
+ MAJOR = 4
4
+ MINOR = 0
5
+ TINY = 0
6
+ BUILD = 'beta1'
7
+ STRING = [MAJOR, MINOR, TINY, BUILD].compact.join('.')
8
+ end
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
@@ -0,0 +1,5 @@
1
+ adapter: mysql
2
+ database: friendly_id_test
3
+ username: root
4
+ hostname: localhost
5
+ encoding: utf8
@@ -0,0 +1,6 @@
1
+ adapter: postgresql
2
+ host: localhost
3
+ port: 5432
4
+ username: postgres
5
+ database: friendly_id_test
6
+ encoding: utf8
@@ -0,0 +1,3 @@
1
+ adapter: sqlite3
2
+ database: ":memory:"
3
+ encoding: utf8
@@ -0,0 +1,43 @@
1
+ require File.expand_path("../helper.rb", __FILE__)
2
+
3
+ Author, Book = 2.times.map do
4
+ Class.new(ActiveRecord::Base) do
5
+ has_friendly_id :name
6
+ end
7
+ end
8
+
9
+ class CoreTest < MiniTest::Unit::TestCase
10
+
11
+ include FriendlyId::Test
12
+ include FriendlyId::Test::Shared
13
+
14
+ def klass
15
+ Author
16
+ end
17
+
18
+ test "models don't use friendly_id by default" do
19
+ assert !Class.new(ActiveRecord::Base).uses_friendly_id?
20
+ end
21
+
22
+ test "model classes should have a friendly id config" do
23
+ assert klass.has_friendly_id(:name).friendly_id_config
24
+ end
25
+
26
+ test "should raise error when bad config options are set" do
27
+ assert_raises ArgumentError do
28
+ klass.has_friendly_id :name, :garbage => :in
29
+ end
30
+ end
31
+
32
+ test "should reserve 'new' and 'edit' by default" do
33
+ ["new", "edit"].each do |word|
34
+ transaction do
35
+ assert_raises(ActiveRecord::RecordInvalid) {klass.create! :name => word}
36
+ end
37
+ end
38
+ end
39
+
40
+ test "instances should have a friendly id" do
41
+ with_instance_of(klass) {|record| assert record.friendly_id}
42
+ end
43
+ end
@@ -0,0 +1,86 @@
1
+ $: << File.expand_path("../../lib", __FILE__)
2
+ $: << File.expand_path("../", __FILE__)
3
+ $:.uniq!
4
+
5
+ require "rubygems"
6
+ require "bundler/setup"
7
+ require "mocha"
8
+ require "minitest/unit"
9
+ require "active_record"
10
+
11
+ if ENV["COVERAGE"]
12
+ require 'simplecov'
13
+ SimpleCov.start
14
+ end
15
+
16
+ require "friendly_id"
17
+
18
+ # If you want to see the ActiveRecord log, invoke the tests using `rake test LOG=true`
19
+ if ENV["LOG"]
20
+ require "logger"
21
+ ActiveRecord::Base.logger = Logger.new($stdout)
22
+ end
23
+
24
+ module FriendlyId
25
+ module Test
26
+
27
+ def self.included(base)
28
+ MiniTest::Unit.autorun
29
+ end
30
+
31
+ def transaction
32
+ ActiveRecord::Base.transaction { yield ; raise ActiveRecord::Rollback }
33
+ end
34
+
35
+ def with_instance_of(*args)
36
+ klass = args.shift
37
+ args[0] ||= {:name => "a"}
38
+ transaction { yield klass.create!(*args) }
39
+ end
40
+
41
+ module Database
42
+ extend self
43
+
44
+ def connect
45
+ ActiveRecord::Base.establish_connection config
46
+ version = ActiveRecord::VERSION::STRING
47
+ driver = FriendlyId::Test::Database.driver
48
+ message = "Using #{RUBY_ENGINE} #{RUBY_VERSION} AR #{version} with #{driver}"
49
+ puts "-" * 72
50
+ if in_memory?
51
+ ActiveRecord::Migration.verbose = false
52
+ Schema.up
53
+ puts "#{message} (in-memory)"
54
+ else
55
+ puts message
56
+ end
57
+ end
58
+
59
+ def config
60
+ @config ||= YAML::load(File.open(config_file))
61
+ end
62
+
63
+ def config_file
64
+ File.expand_path("../config/#{driver}.yml", __FILE__)
65
+ end
66
+
67
+ def driver
68
+ (ENV["DB"] or "sqlite3").downcase
69
+ end
70
+
71
+ def in_memory?
72
+ config["database"] == ":memory:"
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ class Module
79
+ def test(name, &block)
80
+ define_method("test_#{name.gsub(/[^a-z0-9]/i, "_")}".to_sym, &block)
81
+ end
82
+ end
83
+
84
+ require "schema"
85
+ require "shared"
86
+ FriendlyId::Test::Database.connect