friendly_id 3.3.3.0 → 4.0.0.beta7

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