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
@@ -124,8 +124,8 @@ module FriendlyId
124
124
  if result.size == expected_size
125
125
  result
126
126
  else
127
- conditions = arel.where_sql
128
- conditions = " [#{conditions}]" if conditions
127
+ conditions = arel.send(:where_clauses).join(', ')
128
+ conditions = " [WHERE #{conditions}]" if conditions.present?
129
129
  error = "Couldn't find all #{klass.name.pluralize} with IDs "
130
130
  error << "(#{ids.join(", ")})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
131
131
  raise ActiveRecord::RecordNotFound, error
@@ -141,6 +141,14 @@ module FriendlyId
141
141
  end
142
142
  end
143
143
 
144
+ def apply_finder_options(options)
145
+ if options[:scope]
146
+ raise "The :scope finder option has been removed from FriendlyId 3.2.0 " +
147
+ "https://github.com/norman/friendly_id/issues#issue/88"
148
+ end
149
+ super
150
+ end
151
+
144
152
  protected
145
153
 
146
154
  def find_one(id)
@@ -4,19 +4,13 @@ module FriendlyId
4
4
 
5
5
  def self.included(base)
6
6
  base.class_eval do
7
- has_one :slug, :order => 'id DESC', :as => :sluggable, :dependent => :nullify
8
7
  has_many :slugs, :order => 'id DESC', :as => :sluggable, :dependent => :destroy
8
+ has_one :slug, :order => 'id DESC', :as => :sluggable, :dependent => :nullify
9
9
  before_save :build_a_slug
10
10
  after_save :set_slug_cache
11
11
  after_update :update_scope
12
12
  after_update :update_dependent_scopes
13
13
  protect_friendly_id_attributes
14
-
15
- def slug_with_rails_3_2_patch
16
- slug_without_rails_3_2_patch || slugs.first
17
- end
18
-
19
- alias_method_chain :slug, :rails_3_2_patch
20
14
  end
21
15
  end
22
16
 
@@ -80,7 +74,7 @@ module FriendlyId
80
74
  slug.scope = send(friendly_id_config.scope).to_param
81
75
  similar = Slug.similar_to(slug)
82
76
  if !similar.empty?
83
- slug.sequence = similar.last.sequence.succ
77
+ slug.sequence = similar.first.sequence.succ
84
78
  end
85
79
  slug.save!
86
80
  end
@@ -104,7 +98,7 @@ module FriendlyId
104
98
  end
105
99
 
106
100
  # This method was removed in ActiveRecord 3.0.
107
- if !::ActiveRecord::Base.private_method_defined? :update_without_callbacks
101
+ if !ActiveRecord::Base.private_method_defined? :update_without_callbacks
108
102
  def update_without_callbacks
109
103
  attributes_with_values = arel_attributes_values(false, false, attribute_names)
110
104
  return false if attributes_with_values.empty?
@@ -0,0 +1,132 @@
1
+ module FriendlyId
2
+ # Class methods that will be added to model classes that extend {FriendlyId}.
3
+ module Base
4
+
5
+ # Configure FriendlyId's behavior in a model.
6
+ #
7
+ # class Post < ActiveRecord::Base
8
+ # extend FriendlyId
9
+ # friendly_id :title, :use => :slugged
10
+ # end
11
+ #
12
+ # When given the optional block, this method will yield the class's instance
13
+ # of {FriendlyId::Configuration} to the block before evaluating other
14
+ # arguments, so configuration values set in the block may be overwritten by
15
+ # the arguments. This order was chosen to allow passing the same proc to
16
+ # multiple models, while being able to override the values it sets. Here is
17
+ # a contrived example:
18
+ #
19
+ # $friendly_id_config_proc = Proc.new do |config|
20
+ # config.base = :name
21
+ # config.use :slugged
22
+ # end
23
+ #
24
+ # class Foo < ActiveRecord::Base
25
+ # extend FriendlyId
26
+ # friendly_id &$friendly_id_config_proc
27
+ # end
28
+ #
29
+ # class Bar < ActiveRecord::Base
30
+ # extend FriendlyId
31
+ # friendly_id :title, &$friendly_id_config_proc
32
+ # end
33
+ #
34
+ # However, it's usually better to use {FriendlyId.defaults} for this:
35
+ #
36
+ # FriendlyId.defaults do |config|
37
+ # config.base = :name
38
+ # config.use :slugged
39
+ # end
40
+ #
41
+ # class Foo < ActiveRecord::Base
42
+ # extend FriendlyId
43
+ # end
44
+ #
45
+ # class Bar < ActiveRecord::Base
46
+ # extend FriendlyId
47
+ # friendly_id :title
48
+ # end
49
+ #
50
+ # In general you should use the block syntax either because of your personal
51
+ # aesthetic preference, or because you need to share some functionality
52
+ # between multiple models that can't be well encapsulated by
53
+ # {FriendlyId.defaults}.
54
+ #
55
+ # === Order Method Calls in a Block vs Ordering Options
56
+ #
57
+ # When calling this method without a block, you may set the hash options in
58
+ # any order.
59
+ #
60
+ # However, when using block-style invocation, be sure to call
61
+ # FriendlyId::Configuration's {FriendlyId::Configuration#use use} method
62
+ # *prior* to the associated configuration options, because it will include
63
+ # modules into your class, and these modules in turn may add required
64
+ # configuration options to the +@friendly_id_configuraton+'s class:
65
+ #
66
+ # class Person < ActiveRecord::Base
67
+ # friendly_id do |config|
68
+ # # This will work
69
+ # config.use :slugged
70
+ # config.sequence_separator = ":"
71
+ # end
72
+ # end
73
+ #
74
+ # class Person < ActiveRecord::Base
75
+ # friendly_id do |config|
76
+ # # This will fail
77
+ # config.sequence_separator = ":"
78
+ # config.use :slugged
79
+ # end
80
+ # end
81
+ #
82
+ # @option options [Symbol] :use The name of an addon to use. By default,
83
+ # FriendlyId provides {FriendlyId::Slugged :slugged},
84
+ # {FriendlyId::History :history}, {FriendlyId::Reserved :reserved}, and
85
+ # {FriendlyId::Scoped :scoped}.
86
+ #
87
+ # @option options [Array] :reserved_words Available when using +:reserved+,
88
+ # which is loaded by default. Sets an array of words banned for use as
89
+ # the basis of a friendly_id. By default this includes "edit" and "new".
90
+ #
91
+ # @option options [Symbol] :scope Available when using +:scoped+.
92
+ # Sets the relation or column used to scope generated friendly ids. This
93
+ # option has no default value.
94
+ #
95
+ # @option options [Symbol] :sequence_separator Available when using +:slugged+.
96
+ # Configures the sequence of characters used to separate a slug from a
97
+ # sequence. Defaults to +--+.
98
+ #
99
+ # @option options [Symbol] :slug_column Available when using +:slugged+.
100
+ # Configures the name of the column where FriendlyId will store the slug.
101
+ # Defaults to +:slug+.
102
+ #
103
+ # @option options [Symbol] :slug_sequencer_class Available when using +:slugged+.
104
+ # Sets the class used to generate unique slugs. You should not specify this
105
+ # unless you're doing some extensive hacking on FriendlyId. Defaults to
106
+ # {FriendlyId::SlugSequencer}.
107
+ #
108
+ # @yield Provides access to the model class's friendly_id_config, which
109
+ # allows an alternate configuration syntax, and conditional configuration
110
+ # logic.
111
+ #
112
+ # @yieldparam config The model class's {FriendlyId::Configuration friendly_id_config}.
113
+ def friendly_id(base = nil, options = {}, &block)
114
+ yield @friendly_id_config if block_given?
115
+ @friendly_id_config.use options.delete :use
116
+ @friendly_id_config.send :set, base ? options.merge(:base => base) : options
117
+ before_save {|rec| rec.instance_eval {@current_friendly_id = friendly_id}}
118
+ include Model
119
+ end
120
+
121
+ # Returns the model class's {FriendlyId::Configuration friendly_id_config}.
122
+ # @note In the case of Single Table Inheritance (STI), this method will
123
+ # duplicate the parent class's FriendlyId::Configuration instance on first
124
+ # access. If you're concerned about thread safety, then be sure to invoke
125
+ # {#friendly_id} in your class for each model.
126
+ def friendly_id_config
127
+ @friendly_id_config or begin
128
+ @friendly_id_config = base_class.friendly_id_config.dup
129
+ end
130
+ end
131
+ end
132
+ end
@@ -1,166 +1,79 @@
1
1
  module FriendlyId
2
-
3
- # This class is not intended to be used on its own, it is used internally
4
- # by `has_friendly_id` to store a model's configuration and
5
- # configuration-related methods.
6
- #
7
- # The arguments accepted by +has_friendly_id+ correspond to the writeable
8
- # instance attributes of this class; please see the description of the
9
- # attributes below for information on the possible options.
10
- #
11
- # @example
12
- # has_friendly_id :name,
13
- # :use_slug => true,
14
- # :max_length => 150,
15
- # :approximate_ascii => true,
16
- # :ascii_approximation_options => :german,
17
- # :sequence_separator => ":",
18
- # :reserved_words => ["reserved", "words"],
19
- # :scope => :country,
20
- # :cache_column => :my_cache_column_name
21
- # # etc.
2
+ # The configuration paramters passed to +friendly_id+ will be stored in
3
+ # this object.
22
4
  class Configuration
23
5
 
24
- DEFAULTS = {
25
- :allow_nil => false,
26
- :ascii_approximation_options => [],
27
- :max_length => 255,
28
- :reserved_words => ["index", "new"],
29
- :reserved_message => 'can not be "%s"',
30
- :sequence_separator => "--"
31
- }
32
-
33
- # Whether to allow friendly_id and/or slugs to be nil. If this is true then blank
34
- # slugs will automatically be converted to nil, allowing for item names that lack
35
- # sluggable characters.
36
- attr_accessor :allow_nil
37
- alias :allow_nil? :allow_nil
38
-
39
- # Strip diacritics from Western characters.
40
- attr_accessor :approximate_ascii
41
-
42
- # Locale-type options for ASCII approximations.
43
- attr_accessor :ascii_approximation_options
44
-
45
- # The class that's using the configuration.
46
- attr_reader :configured_class
47
-
48
- # The maximum allowed byte length for a friendly_id string. This is checked *after* a
49
- # string is processed by FriendlyId to remove spaces, special characters, etc.
50
- attr_accessor :max_length
51
-
52
- # The method or column that will be used as the basis of the friendly_id string.
53
- attr_reader :method
54
- alias :column :method
55
-
56
- # The message shown when a reserved word is used.
57
- # @see #reserved_words
58
- attr_accessor :reserved_message
59
-
60
- # Array of words that are reserved and can't be used as friendly_id strings.
61
- # If a listed word is used in a sluggable model, it will raise a
62
- # FriendlyId::SlugGenerationError. For Rails applications, you are recommended
63
- # to include "index" and "new", which used as the defaults unless overridden.
64
- attr_accessor :reserved_words
65
-
66
- # The method or relation to use as the friendly_id's scope.
67
- attr_reader :scope
68
-
69
- # The string that separates slug names from slug sequences. Defaults to "--".
70
- attr_accessor :sequence_separator
71
-
72
- # Strip non-ASCII characters from the friendly_id string.
73
- attr_accessor :strip_non_ascii
74
-
75
- # Use slugs for storing the friendly_id string.
76
- attr_accessor :use_slug
77
- alias :use_slugs= :use_slug
78
-
79
- def initialize(configured_class, method, options = nil, &block)
80
- @configured_class = configured_class
81
- @method = method.to_sym
82
- DEFAULTS.merge(options || {}).each do |key, value|
83
- self.send "#{key}=".to_sym, value
84
- end
85
- yield self if block_given?
86
- end
87
-
88
- def cache_column=(value)
89
- @cache_column = value.to_s.strip.to_sym
90
- if value =~ /\s/ || [:slug, :slugs].include?(@cache_column)
91
- raise ArgumentError, "FriendlyId cache column can not be named '#{value}'"
92
- end
93
- @cache_column
94
- end
95
-
96
- # This should be overridden by adapters that implement caching.
97
- def cache_column?
98
- false
6
+ # The base column or method used by FriendlyId as the basis of a friendly id
7
+ # or slug.
8
+ #
9
+ # For models that don't use FriendlyId::Slugged, the base is the column that
10
+ # is used as the FriendlyId directly. For models using FriendlyId::Slugged,
11
+ # the base is a column or method whose value is used as the basis of the
12
+ # slug.
13
+ #
14
+ # For example, if you have a model representing blog posts and that uses
15
+ # slugs, you likely will want to use the "title" attribute as the base, and
16
+ # FriendlyId will take care of transforming the human-readable title into
17
+ # something suitable for use in a URL.
18
+ #
19
+ # @param [Symbol] A symbol referencing a column or method in the model. This
20
+ # value is usually set by passing it as the first argument to
21
+ # {FriendlyId::Base#friendly_id friendly_id}:
22
+ #
23
+ # @example
24
+ # class Book < ActiveRecord::Base
25
+ # extend FriendlyId
26
+ # friendly_id :name
27
+ # end
28
+ attr_accessor :base
29
+
30
+ # The default configuration options.
31
+ attr_reader :defaults
32
+
33
+ # The model class that this configuration belongs to.
34
+ # @return ActiveRecord::Base
35
+ attr_reader :model_class
36
+
37
+ def initialize(model_class, values = nil)
38
+ @model_class = model_class
39
+ @defaults = {}
40
+ set values
99
41
  end
100
42
 
101
- def reserved_words=(*args)
102
- if args.first.kind_of?(Regexp)
103
- @reserved_words = args.first
104
- else
105
- @reserved_words = args.flatten.uniq
43
+ # Lets you specify the modules to use with FriendlyId.
44
+ #
45
+ # This method is invoked by {FriendlyId::Base#friendly_id friendly_id} when
46
+ # passing the +:use+ option, or when using {FriendlyId::Base#friendly_id
47
+ # friendly_id} with a block.
48
+ #
49
+ # @example
50
+ # class Book < ActiveRecord::Base
51
+ # extend FriendlyId
52
+ # friendly_id :name, :use => :slugged
53
+ # end
54
+ # @param [#to_s] *modules Arguments should be a symbols or strings that
55
+ # correspond with the name of a module inside the FriendlyId namespace. By
56
+ # default FriendlyId provides +:slugged+, +:history+ and +:scoped+.
57
+ def use(*modules)
58
+ modules.to_a.flatten.compact.map do |name|
59
+ mod = FriendlyId.const_get(name.to_s.classify)
60
+ model_class.send(:include, mod) unless model_class < mod
106
61
  end
107
62
  end
108
63
 
109
- def reserved?(word)
110
- word = word.to_s
111
- if reserved_words.kind_of?(Regexp)
112
- reserved_words =~ word
113
- else
114
- reserved_words.include?(word)
115
- end
116
- end
117
-
118
- def reserved_error_message(word)
119
- [method, reserved_message % word] if reserved? word
120
- end
121
-
122
- def scope=(scope)
123
- self.class.scopes_used = true
124
- @scope = scope
125
- end
126
-
127
- def sequence_separator=(string)
128
- if string == "-" || string =~ /\s/
129
- raise ArgumentError, "FriendlyId sequence_separator can not be '#{string}'"
130
- end
131
- @sequence_separator = string
132
- end
133
-
134
- # This will be set if FriendlyId's scope feature is used in any model. It is here
135
- # to provide a way to avoid invoking costly scope lookup methods when the scoped
136
- # slug feature is not being used by any models.
137
- def self.scopes_used=(val)
138
- @scopes_used = !!val
139
- end
140
-
141
- # Are scoped slugs being used by any model?
142
- # @see Configuration.scoped_used=
143
- def self.scopes_used?
144
- @scopes_used
145
- end
146
-
147
- %w[approximate_ascii scope strip_non_ascii use_slug].each do |method|
148
- class_eval(<<-EOM, __FILE__, __LINE__ + 1)
149
- def #{method}?
150
- !! #{method}
151
- end
152
- EOM
64
+ # The column that FriendlyId will use to find the record when querying by
65
+ # friendly id.
66
+ #
67
+ # This method is generally only used internally by FriendlyId.
68
+ # @return String
69
+ def query_field
70
+ base.to_s
153
71
  end
154
72
 
155
- alias :use_slugs? :use_slug?
73
+ private
156
74
 
157
- def babosa_options
158
- {
159
- :to_ascii => strip_non_ascii?,
160
- :transliterate => approximate_ascii?,
161
- :transliterations => ascii_approximation_options,
162
- :max_length => max_length
163
- }
75
+ def set(values)
76
+ values and values.each {|name, value| self.send "#{name}=", value}
164
77
  end
165
78
  end
166
79
  end
@@ -0,0 +1,20 @@
1
+ module FriendlyId
2
+ # These methods will override the finder methods in ActiveRecord::Relation.
3
+ module FinderMethods
4
+
5
+ protected
6
+
7
+ # FriendlyId overrides this method to make it possible to use friendly id's
8
+ # identically to numeric ids in finders.
9
+ #
10
+ # @example
11
+ # person = Person.find(123)
12
+ # person = Person.find("joe")
13
+ #
14
+ # @see FriendlyId::ObjectUtils
15
+ def find_one(id)
16
+ return super if !@klass.respond_to?(:friendly_id) || id.unfriendly_id?
17
+ where(@klass.friendly_id_config.query_field => id).first or super
18
+ end
19
+ end
20
+ end