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