friendly_id 3.3.3.0 → 4.0.0.beta7

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.
Files changed (79) hide show
  1. data/.gitignore +11 -0
  2. data/.travis.yml +24 -0
  3. data/.yardopts +4 -0
  4. data/Changelog.md +9 -10
  5. data/README.md +39 -48
  6. data/Rakefile +56 -58
  7. data/WhatsNew.md +95 -0
  8. data/bench.rb +63 -0
  9. data/friendly_id.gemspec +40 -0
  10. data/gemfiles/Gemfile.rails-3.0.rb +18 -0
  11. data/gemfiles/Gemfile.rails-3.0.rb.lock +52 -0
  12. data/gemfiles/Gemfile.rails-3.1.rb +18 -0
  13. data/gemfiles/Gemfile.rails-3.1.rb.lock +57 -0
  14. data/lib/friendly_id.rb +126 -80
  15. data/lib/friendly_id/active_record_adapter/relation.rb +10 -2
  16. data/lib/friendly_id/active_record_adapter/slugged_model.rb +3 -9
  17. data/lib/friendly_id/base.rb +132 -0
  18. data/lib/friendly_id/configuration.rb +65 -152
  19. data/lib/friendly_id/finder_methods.rb +20 -0
  20. data/lib/friendly_id/history.rb +88 -0
  21. data/lib/friendly_id/migration.rb +18 -0
  22. data/lib/friendly_id/model.rb +22 -0
  23. data/lib/friendly_id/object_utils.rb +40 -0
  24. data/lib/friendly_id/reserved.rb +46 -0
  25. data/lib/friendly_id/scoped.rb +131 -0
  26. data/lib/friendly_id/slug.rb +9 -0
  27. data/lib/friendly_id/slug_sequencer.rb +82 -0
  28. data/lib/friendly_id/slugged.rb +191 -76
  29. data/lib/friendly_id/version.rb +2 -2
  30. data/test/base_test.rb +54 -0
  31. data/test/configuration_test.rb +27 -0
  32. data/test/core_test.rb +30 -0
  33. data/test/databases.yml +19 -0
  34. data/test/helper.rb +88 -0
  35. data/test/history_test.rb +55 -0
  36. data/test/object_utils_test.rb +26 -0
  37. data/test/reserved_test.rb +26 -0
  38. data/test/schema.rb +59 -0
  39. data/test/scoped_test.rb +57 -0
  40. data/test/shared.rb +118 -0
  41. data/test/slugged_test.rb +83 -0
  42. data/test/sti_test.rb +48 -0
  43. metadata +110 -102
  44. data/Contributors.md +0 -46
  45. data/Guide.md +0 -626
  46. data/extras/README.txt +0 -3
  47. data/extras/bench.rb +0 -40
  48. data/extras/extras.rb +0 -38
  49. data/extras/prof.rb +0 -19
  50. data/extras/template-gem.rb +0 -26
  51. data/extras/template-plugin.rb +0 -28
  52. data/generators/friendly_id/friendly_id_generator.rb +0 -30
  53. data/generators/friendly_id/templates/create_slugs.rb +0 -18
  54. data/lib/tasks/friendly_id.rake +0 -19
  55. data/rails/init.rb +0 -2
  56. data/test/active_record_adapter/ar_test_helper.rb +0 -149
  57. data/test/active_record_adapter/basic_slugged_model_test.rb +0 -14
  58. data/test/active_record_adapter/cached_slug_test.rb +0 -76
  59. data/test/active_record_adapter/core.rb +0 -138
  60. data/test/active_record_adapter/custom_normalizer_test.rb +0 -20
  61. data/test/active_record_adapter/custom_table_name_test.rb +0 -22
  62. data/test/active_record_adapter/default_scope_test.rb +0 -30
  63. data/test/active_record_adapter/optimistic_locking_test.rb +0 -18
  64. data/test/active_record_adapter/scoped_model_test.rb +0 -129
  65. data/test/active_record_adapter/simple_test.rb +0 -76
  66. data/test/active_record_adapter/slug_test.rb +0 -34
  67. data/test/active_record_adapter/slugged.rb +0 -33
  68. data/test/active_record_adapter/slugged_status_test.rb +0 -28
  69. data/test/active_record_adapter/sti_test.rb +0 -22
  70. data/test/active_record_adapter/support/database.jdbcsqlite3.yml +0 -2
  71. data/test/active_record_adapter/support/database.mysql.yml +0 -4
  72. data/test/active_record_adapter/support/database.mysql2.yml +0 -4
  73. data/test/active_record_adapter/support/database.postgres.yml +0 -6
  74. data/test/active_record_adapter/support/database.sqlite3.yml +0 -2
  75. data/test/active_record_adapter/support/models.rb +0 -104
  76. data/test/active_record_adapter/tasks_test.rb +0 -82
  77. data/test/compatibility/ancestry/Gemfile.lock +0 -34
  78. data/test/friendly_id_test.rb +0 -96
  79. data/test/test_helper.rb +0 -13
@@ -1,105 +1,220 @@
1
- module FriendlyId
2
- module Slugged
1
+ # encoding: utf-8
2
+ require "friendly_id/slug_sequencer"
3
3
 
4
- class Status < FriendlyId::Status
4
+ module FriendlyId
5
+ =begin
6
+ This module adds in-table slugs to a model.
7
+
8
+ Slugs are unique id strings that have been processed to remove or replace
9
+ characters that a developer considers inconvenient for use in URLs. For example,
10
+ blog applications typically use a post title to provide the basis of a search
11
+ engine friendly URL:
12
+
13
+ "Gone With The Wind" -> "gone-with-the-wind"
14
+
15
+ FriendlyId generates slugs from a method or column that you specify, and stores
16
+ them in a field in your model. By default, this field must be named +:slug+,
17
+ though you may change this using the
18
+ {FriendlyId::Slugged::Configuration#slug_column slug_column} configuration
19
+ option. You should add an index to this field. You may also wish to constrain it
20
+ to NOT NULL, but this depends on your app's behavior and requirements.
21
+
22
+ === Example Setup
23
+
24
+ # your model
25
+ class Post < ActiveRecord::Base
26
+ extend FriendlyId
27
+ friendly_id :title, :use => :slugged
28
+ validates_presence_of :title, :slug, :body
29
+ end
5
30
 
6
- attr_accessor :sequence, :slug
31
+ # a migration
32
+ class CreatePosts < ActiveRecord::Migration
33
+ def self.up
34
+ create_table :posts do |t|
35
+ t.string :title, :null => false
36
+ t.string :slug, :null => false
37
+ t.text :body
38
+ end
7
39
 
8
- # Did the find operation use the best possible id? True if +id+ is
9
- # numeric, but the model has no slug, or +id+ is friendly and current
10
- def best?
11
- current? || (numeric? && !record.slug)
40
+ add_index :posts, :slug, :unique => true
12
41
  end
13
42
 
14
- # Did the find operation use the current slug?
15
- def current?
16
- !! slug && slug.current?
43
+ def self.down
44
+ drop_table :posts
17
45
  end
46
+ end
18
47
 
19
- # Did the find operation use a friendly id?
20
- def friendly?
21
- !! (name or slug)
22
- end
48
+ === Slug Format
23
49
 
24
- def friendly_id=(friendly_id)
25
- @name, @sequence = friendly_id.parse_friendly_id(record.friendly_id_config.sequence_separator)
26
- end
50
+ By default, FriendlyId uses Active Support's
51
+ paramaterize[http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-parameterize]
52
+ method to create slugs. This method will intelligently replace spaces with
53
+ dashes, and Unicode Latin characters with ASCII approximations:
27
54
 
28
- # Did the find operation use an outdated slug?
29
- def outdated?
30
- !current?
31
- end
55
+ movie = Movie.create! :title => "Der Preis fürs Überleben"
56
+ movie.slug #=> "der-preis-furs-uberleben"
32
57
 
33
- # The slug that was used to find the model.
34
- def slug
35
- @slug ||= record.find_slug(name, sequence)
36
- end
58
+ ==== Slug Uniqueness
59
+
60
+ When you try to insert a record that would generate a duplicate friendly id,
61
+ FriendlyId will append a sequence to the generated slug to ensure uniqueness:
62
+
63
+ car = Car.create :title => "Peugot 206"
64
+ car2 = Car.create :title => "Peugot 206"
65
+
66
+ car.friendly_id #=> "peugot-206"
67
+ car2.friendly_id #=> "peugot-206--2"
68
+
69
+ ==== Changing the Slug Sequence Separator
70
+
71
+ You can do this with the {Slugged::Configuration#sequence_separator
72
+ sequence_separator} configuration option.
73
+
74
+ ==== Column or Method?
75
+
76
+ FriendlyId always uses a method as the basis of the slug text - not a column. It
77
+ first glance, this may sound confusing, but remember that Active Record provides
78
+ methods for each column in a model's associated table, and that's what
79
+ FriendlyId uses.
80
+
81
+ Here's an example of a class that uses a custom method to generate the slug:
37
82
 
83
+ class Person < ActiveRecord::Base
84
+ friendly_id :name_and_location
85
+ def name_and_location
86
+ "#{name} from #{location}"
38
87
  end
88
+ end
39
89
 
40
- module Model
41
- attr_accessor :slug
90
+ bob = Person.create! :name => "Bob Smith", :location => "New York City"
91
+ bob.friendly_id #=> "bob-smith-from-new-york-city"
42
92
 
43
- def find_slug
44
- raise NotImplementedError
45
- end
93
+ ==== Providing Your Own Slug Processing Method
46
94
 
47
- def friendly_id_config
48
- self.class.friendly_id_config
49
- end
95
+ You can override {Slugged#normalize_friendly_id} in your model for total
96
+ control over the slug format.
50
97
 
51
- # Get the {FriendlyId::Status} after the find has been performed.
52
- def friendly_id_status
53
- @friendly_id_status ||= Status.new(:record => self)
54
- end
98
+ ==== Locale-specific Transliterations
55
99
 
56
- # The friendly id.
57
- # @param
58
- def friendly_id(skip_cache = false)
59
- if friendly_id_config.cache_column? && !skip_cache
60
- friendly_id = send(friendly_id_config.cache_column)
61
- end
62
- friendly_id || (slug.to_friendly_id if slug?)
63
- end
100
+ Active Support's +parameterize+ uses
101
+ transliterate[http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-transliterate],
102
+ which in turn can use I18n's transliteration rules to consider the current
103
+ locale when replacing Latin characters:
64
104
 
65
- # Clean up the string before setting it as the friendly_id. You can override
66
- # this method to add your own custom normalization routines.
67
- # @param string An instance of {FriendlyId::SlugString}.
68
- # @return [String]
69
- def normalize_friendly_id(string)
70
- string.normalize_for!(friendly_id_config).to_s
71
- end
105
+ # config/locales/de.yml
106
+ de:
107
+ i18n:
108
+ transliterate:
109
+ rule:
110
+ ü: "ue"
111
+ ö: "oe"
112
+ etc...
72
113
 
73
- # Does the instance have a slug?
74
- def slug?
75
- !! slug
114
+ movie = Movie.create! :title => "Der Preis fürs Überleben"
115
+ movie.slug #=> "der-preis-fuers-ueberleben"
116
+
117
+ This functionality was in fact taken from earlier versions of FriendlyId.
118
+ =end
119
+ module Slugged
120
+
121
+ # Sets up behavior and configuration options for FriendlyId's slugging
122
+ # feature.
123
+ def self.included(model_class)
124
+ model_class.instance_eval do
125
+ friendly_id_config.class.send :include, Configuration
126
+ friendly_id_config.defaults[:slug_column] ||= 'slug'
127
+ friendly_id_config.defaults[:sequence_separator] ||= '--'
128
+ friendly_id_config.slug_sequencer_class ||= Class.new(SlugSequencer)
129
+ before_validation :set_slug
76
130
  end
131
+ end
77
132
 
78
- private
133
+ # Process the given value to make it suitable for use as a slug.
134
+ #
135
+ # This method is not intended to be invoked directly; FriendlyId uses it
136
+ # internaly to process strings into slugs.
137
+ #
138
+ # However, if FriendlyId's default slug generation doesn't suite your needs,
139
+ # you can override this method in your model class to control exactly how
140
+ # slugs are generated.
141
+ #
142
+ # === Example
143
+ #
144
+ # class Person < ActiveRecord::Base
145
+ # friendly_id :name_and_location
146
+ #
147
+ # def name_and_location
148
+ # "#{name} from #{location}"
149
+ # end
150
+ #
151
+ # # Use default slug, but uupper case and with underscores
152
+ # def normalize_friendly_id(string)
153
+ # super.upcase.gsub("-", "_")
154
+ # end
155
+ # end
156
+ #
157
+ # bob = Person.create! :name => "Bob Smith", :location => "New York City"
158
+ # bob.friendly_id #=> "BOB_SMITH_FROM_NEW_YORK_CITY"
159
+ #
160
+ # === More Resources
161
+ #
162
+ # You might want to look into Babosa[https://github.com/norman/babosa],
163
+ # which is the slugging library used by FriendlyId prior to version 4, which
164
+ # offers some specialized functionality missing from Active Support.
165
+ #
166
+ # @param [#to_s] value The value used as the basis of the slug.
167
+ # @return The candidate slug text, without a sequence.
168
+ def normalize_friendly_id(value)
169
+ value.to_s.parameterize
170
+ end
79
171
 
80
- # Get the processed string used as the basis of the friendly id.
81
- def slug_text
82
- text = send(friendly_id_config.method)
83
- text = normalize_friendly_id(SlugString.new(text)) unless text.nil?
84
- text = nil if text.blank?
85
- unless text.nil? && friendly_id_config.allow_nil?
86
- SlugString.new(text).validate_for!(friendly_id_config).to_s
87
- end
172
+ # Gets a new instance of the configured slug sequencing class.
173
+ #
174
+ # @see FriendlyId::SlugSequencer
175
+ def slug_sequencer
176
+ friendly_id_config.slug_sequencer_class.new(self)
177
+ end
178
+
179
+ # Sets the slug.
180
+ def set_slug
181
+ send "#{friendly_id_config.slug_column}=", slug_sequencer.generate
182
+ end
183
+ private :set_slug
184
+
185
+ # This module adds the +:slug_column+, and +:sequence_separator+, and
186
+ # +:slug_sequencer_class+ configuration options to
187
+ # {FriendlyId::Configuration FriendlyId::Configuration}.
188
+ module Configuration
189
+ attr_writer :slug_column, :sequence_separator
190
+ attr_accessor :slug_sequencer_class
191
+
192
+ # Makes FriendlyId use the slug column for querying.
193
+ # @return String The slug column.
194
+ def query_field
195
+ slug_column
88
196
  end
89
197
 
90
- # Has the slug text changed?
91
- def slug_text_changed?
92
- slug_text != slug.name
198
+ # The string used to separate a slug base from a numeric sequence.
199
+ #
200
+ # By default, +--+ is used to separate the slug from the sequence.
201
+ # FriendlyId uses two dashes to distinguish sequences from slugs with
202
+ # numbers in their name.
203
+ #
204
+ # You can change the default separator by setting the
205
+ # {FriendlyId::Slugged::Configuration#sequence_separator
206
+ # sequence_separator} configuration option.
207
+ #
208
+ # For obvious reasons, you should avoid setting it to "+-+" unless you're
209
+ # sure you will never want to have a friendly id with a number in it.
210
+ # @return String The sequence separator string. Defaults to "+--+".
211
+ def sequence_separator
212
+ @sequence_separator or defaults[:sequence_separator]
93
213
  end
94
214
 
95
- # Has the basis of our friendly id changed, requiring the generation of a
96
- # new slug?
97
- def new_slug_needed?
98
- if friendly_id_config.allow_nil?
99
- (!slug? && !slug_text.blank?) || (slug? && slug_text_changed?)
100
- else
101
- !slug? || slug_text_changed?
102
- end
215
+ # The column that will be used to store the generated slug.
216
+ def slug_column
217
+ @slug_column or defaults[:slug_column]
103
218
  end
104
219
  end
105
220
  end
@@ -2,8 +2,8 @@ module FriendlyId
2
2
  module Version
3
3
  MAJOR = 3
4
4
  MINOR = 3
5
- TINY = 3
6
- BUILD = 0
5
+ TINY = 0
6
+ BUILD = 'rc2'
7
7
  STRING = [MAJOR, MINOR, TINY, BUILD].compact.join('.')
8
8
  end
9
9
  end
data/test/base_test.rb ADDED
@@ -0,0 +1,54 @@
1
+ require File.expand_path("../helper.rb", __FILE__)
2
+
3
+ class CoreTest < MiniTest::Unit::TestCase
4
+ include FriendlyId::Test
5
+
6
+ test "friendly_id should accept a base and a hash" do
7
+ klass = Class.new(ActiveRecord::Base) do
8
+ extend FriendlyId
9
+ friendly_id :foo, :use => :slugged, :slug_column => :bar
10
+ end
11
+ assert klass < FriendlyId::Slugged
12
+ assert_equal :foo, klass.friendly_id_config.base
13
+ assert_equal :bar, klass.friendly_id_config.slug_column
14
+ end
15
+
16
+
17
+ test "friendly_id should accept a block" do
18
+ klass = Class.new(ActiveRecord::Base) do
19
+ extend FriendlyId
20
+ friendly_id :foo do |config|
21
+ config.use :slugged
22
+ config.base = :foo
23
+ config.slug_column = :bar
24
+ end
25
+ end
26
+ assert klass < FriendlyId::Slugged
27
+ assert_equal :foo, klass.friendly_id_config.base
28
+ assert_equal :bar, klass.friendly_id_config.slug_column
29
+ end
30
+
31
+ test "the block passed to friendly_id should be evaluated before arguments" do
32
+ klass = Class.new(ActiveRecord::Base) do
33
+ extend FriendlyId
34
+ friendly_id :foo do |config|
35
+ config.base = :bar
36
+ end
37
+ end
38
+ assert_equal :foo, klass.friendly_id_config.base
39
+ end
40
+
41
+ test "should allow defaults to be set via a block" do
42
+ begin
43
+ FriendlyId.defaults do |config|
44
+ config.base = :foo
45
+ end
46
+ klass = Class.new(ActiveRecord::Base) do
47
+ extend FriendlyId
48
+ end
49
+ assert_equal :foo, klass.friendly_id_config.base
50
+ ensure
51
+ FriendlyId.instance_variable_set :@defaults, nil
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,27 @@
1
+ require File.expand_path("../helper", __FILE__)
2
+
3
+ class ConfigurationTest < MiniTest::Unit::TestCase
4
+
5
+ include FriendlyId::Test
6
+
7
+ def setup
8
+ @model_class = Class.new(ActiveRecord::Base)
9
+ end
10
+
11
+ test "should set model class on initialization" do
12
+ config = FriendlyId::Configuration.new @model_class
13
+ assert_equal @model_class, config.model_class
14
+ end
15
+
16
+ test "should set options on initialization if present" do
17
+ config = FriendlyId::Configuration.new @model_class, :base => "hello"
18
+ assert_equal "hello", config.base
19
+ end
20
+
21
+ test "should raise error if passed unrecognized option" do
22
+ assert_raises NoMethodError do
23
+ FriendlyId::Configuration.new @model_class, :foo => "bar"
24
+ end
25
+ end
26
+
27
+ end
data/test/core_test.rb ADDED
@@ -0,0 +1,30 @@
1
+ require File.expand_path("../helper.rb", __FILE__)
2
+
3
+ Author, Book = 2.times.map do
4
+ Class.new(ActiveRecord::Base) do
5
+ extend FriendlyId
6
+ friendly_id :name
7
+ end
8
+ end
9
+
10
+ class CoreTest < MiniTest::Unit::TestCase
11
+
12
+ include FriendlyId::Test
13
+ include FriendlyId::Test::Shared::Core
14
+
15
+ def model_class
16
+ Author
17
+ end
18
+
19
+ test "models don't use friendly_id by default" do
20
+ assert !Class.new(ActiveRecord::Base).respond_to?(:friendly_id)
21
+ end
22
+
23
+ test "model classes should have a friendly id config" do
24
+ assert model_class.friendly_id(:name).friendly_id_config
25
+ end
26
+
27
+ test "instances should have a friendly id" do
28
+ with_instance_of(model_class) {|record| assert record.friendly_id}
29
+ end
30
+ end
@@ -0,0 +1,19 @@
1
+ mysql:
2
+ adapter: mysql2
3
+ database: friendly_id_test
4
+ username: root
5
+ hostname: localhost
6
+ encoding: utf8
7
+
8
+ postgres:
9
+ adapter: postgresql
10
+ host: localhost
11
+ port: 5432
12
+ username: postgres
13
+ database: friendly_id_test
14
+ encoding: utf8
15
+
16
+ sqlite3:
17
+ adapter: sqlite3
18
+ database: ":memory:"
19
+ encoding: utf8
data/test/helper.rb ADDED
@@ -0,0 +1,88 @@
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
+ require 'active_support/core_ext/time/conversions'
11
+ # require "active_support/core_ext/class"
12
+
13
+ if ENV["COVERAGE"]
14
+ require 'simplecov'
15
+ SimpleCov.start do
16
+ add_filter "test/"
17
+ add_filter "friendly_id/migration"
18
+ end
19
+ end
20
+
21
+ require "friendly_id"
22
+
23
+ # If you want to see the ActiveRecord log, invoke the tests using `rake test LOG=true`
24
+ if ENV["LOG"]
25
+ require "logger"
26
+ ActiveRecord::Base.logger = Logger.new($stdout)
27
+ end
28
+
29
+ module FriendlyId
30
+ module Test
31
+
32
+ def self.included(base)
33
+ MiniTest::Unit.autorun
34
+ end
35
+
36
+ def transaction
37
+ ActiveRecord::Base.transaction { yield ; raise ActiveRecord::Rollback }
38
+ end
39
+
40
+ def with_instance_of(*args)
41
+ model_class = args.shift
42
+ args[0] ||= {:name => "a"}
43
+ transaction { yield model_class.create!(*args) }
44
+ end
45
+
46
+ module Database
47
+ extend self
48
+
49
+ def connect
50
+ ActiveRecord::Base.establish_connection config[driver]
51
+ version = ActiveRecord::VERSION::STRING
52
+ driver = FriendlyId::Test::Database.driver
53
+ engine = RUBY_ENGINE rescue "ruby"
54
+ message = "Using #{engine} #{RUBY_VERSION} AR #{version} with #{driver}"
55
+ puts "-" * 72
56
+ if in_memory?
57
+ ActiveRecord::Migration.verbose = false
58
+ Schema.up
59
+ puts "#{message} (in-memory)"
60
+ else
61
+ puts message
62
+ end
63
+ end
64
+
65
+ def config
66
+ @config ||= YAML::load(File.open(File.expand_path("../databases.yml", __FILE__)))
67
+ end
68
+
69
+ def driver
70
+ (ENV["DB"] or "sqlite3").downcase
71
+ end
72
+
73
+ def in_memory?
74
+ config[driver]["database"] == ":memory:"
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ class Module
81
+ def test(name, &block)
82
+ define_method("test_#{name.gsub(/[^a-z0-9']/i, "_")}".to_sym, &block)
83
+ end
84
+ end
85
+
86
+ require "schema"
87
+ require "shared"
88
+ FriendlyId::Test::Database.connect