friendly_id4 4.0.0.beta1

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,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