friendly_id 2.2.7 → 2.3.0

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 (73) hide show
  1. data/Changelog.md +225 -0
  2. data/Contributors.md +28 -0
  3. data/Guide.md +509 -0
  4. data/LICENSE +1 -1
  5. data/README.md +76 -0
  6. data/Rakefile +48 -15
  7. data/extras/bench.rb +59 -0
  8. data/extras/extras.rb +31 -0
  9. data/extras/prof.rb +14 -0
  10. data/extras/template-gem.rb +1 -1
  11. data/extras/template-plugin.rb +1 -1
  12. data/generators/friendly_id/friendly_id_generator.rb +1 -1
  13. data/generators/friendly_id/templates/create_slugs.rb +2 -2
  14. data/lib/friendly_id.rb +54 -63
  15. data/lib/friendly_id/active_record2.rb +47 -0
  16. data/lib/friendly_id/active_record2/configuration.rb +66 -0
  17. data/lib/friendly_id/active_record2/finders.rb +140 -0
  18. data/lib/friendly_id/active_record2/simple_model.rb +162 -0
  19. data/lib/friendly_id/active_record2/slug.rb +111 -0
  20. data/lib/friendly_id/active_record2/slugged_model.rb +317 -0
  21. data/lib/friendly_id/active_record2/tasks.rb +66 -0
  22. data/lib/friendly_id/active_record2/tasks/friendly_id.rake +19 -0
  23. data/lib/friendly_id/configuration.rb +132 -0
  24. data/lib/friendly_id/finders.rb +106 -0
  25. data/lib/friendly_id/slug_string.rb +292 -0
  26. data/lib/friendly_id/slugged.rb +91 -0
  27. data/lib/friendly_id/status.rb +35 -0
  28. data/lib/friendly_id/test.rb +168 -0
  29. data/lib/friendly_id/version.rb +5 -5
  30. data/rails/init.rb +2 -0
  31. data/test/active_record2/basic_slugged_model_test.rb +14 -0
  32. data/test/active_record2/cached_slug_test.rb +61 -0
  33. data/test/active_record2/core.rb +93 -0
  34. data/test/active_record2/custom_normalizer_test.rb +20 -0
  35. data/test/active_record2/custom_table_name_test.rb +22 -0
  36. data/test/active_record2/scoped_model_test.rb +111 -0
  37. data/test/active_record2/simple_test.rb +59 -0
  38. data/test/active_record2/slug_test.rb +34 -0
  39. data/test/active_record2/slugged.rb +30 -0
  40. data/test/active_record2/slugged_status_test.rb +61 -0
  41. data/test/active_record2/sti_test.rb +22 -0
  42. data/test/active_record2/support/database.mysql.yml +4 -0
  43. data/test/{support/database.yml.postgres → active_record2/support/database.postgres.yml} +0 -0
  44. data/test/{support/database.yml.sqlite3 → active_record2/support/database.sqlite3.yml} +0 -0
  45. data/test/{support → active_record2/support}/models.rb +28 -0
  46. data/test/active_record2/tasks_test.rb +82 -0
  47. data/test/active_record2/test_helper.rb +107 -0
  48. data/test/friendly_id_test.rb +23 -0
  49. data/test/slug_string_test.rb +74 -0
  50. data/test/test_helper.rb +7 -102
  51. metadata +64 -56
  52. data/History.txt +0 -194
  53. data/README.rdoc +0 -385
  54. data/generators/friendly_id_20_upgrade/friendly_id_20_upgrade_generator.rb +0 -12
  55. data/generators/friendly_id_20_upgrade/templates/upgrade_friendly_id_to_20.rb +0 -19
  56. data/init.rb +0 -1
  57. data/lib/friendly_id/helpers.rb +0 -12
  58. data/lib/friendly_id/non_sluggable_class_methods.rb +0 -34
  59. data/lib/friendly_id/non_sluggable_instance_methods.rb +0 -45
  60. data/lib/friendly_id/slug.rb +0 -98
  61. data/lib/friendly_id/sluggable_class_methods.rb +0 -110
  62. data/lib/friendly_id/sluggable_instance_methods.rb +0 -161
  63. data/lib/friendly_id/tasks.rb +0 -56
  64. data/lib/tasks/friendly_id.rake +0 -25
  65. data/lib/tasks/friendly_id.rb +0 -1
  66. data/test/cached_slug_test.rb +0 -109
  67. data/test/custom_slug_normalizer_test.rb +0 -36
  68. data/test/non_slugged_test.rb +0 -99
  69. data/test/scoped_model_test.rb +0 -64
  70. data/test/slug_test.rb +0 -105
  71. data/test/slugged_model_test.rb +0 -348
  72. data/test/sti_test.rb +0 -49
  73. data/test/tasks_test.rb +0 -105
@@ -0,0 +1,47 @@
1
+ require File.join(File.dirname(__FILE__), "active_record2", "configuration")
2
+ require File.join(File.dirname(__FILE__), "active_record2", "finders")
3
+ require File.join(File.dirname(__FILE__), "active_record2", "simple_model")
4
+ require File.join(File.dirname(__FILE__), "active_record2", "slugged_model")
5
+ require File.join(File.dirname(__FILE__), "active_record2", "slug")
6
+ require File.join(File.dirname(__FILE__), "active_record2", "tasks")
7
+
8
+ module FriendlyId
9
+
10
+ module ActiveRecord2
11
+
12
+ include FriendlyId::Base
13
+
14
+ def has_friendly_id(method, options = {}, &block)
15
+ class_inheritable_accessor :friendly_id_config
16
+ write_inheritable_attribute :friendly_id_config, Configuration.new(self,
17
+ method, options.merge(:normalizer => block))
18
+ if friendly_id_config.use_slug?
19
+ include SluggedModel
20
+ else
21
+ include SimpleModel
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ # Prevent the cached_slug column from being accidentally or maliciously
28
+ # overwritten. Note that +attr_protected+ is used to protect the cached_slug
29
+ # column, unless you have already invoked +attr_accessible+. So if you
30
+ # wish to use +attr_accessible+, you must invoke it BEFORE you invoke
31
+ # {#has_friendly_id} in your class.
32
+ def protect_friendly_id_attributes
33
+ # only protect the column if the class is not already using attributes_accessible
34
+ if !accessible_attributes
35
+ if friendly_id_config.custom_cache_column?
36
+ attr_protected friendly_id_config.cache_column
37
+ end
38
+ attr_protected :cached_slug
39
+ end
40
+ end
41
+
42
+ end
43
+ end
44
+
45
+ class ActiveRecord::Base
46
+ extend FriendlyId::ActiveRecord2
47
+ end
@@ -0,0 +1,66 @@
1
+ module FriendlyId
2
+ module ActiveRecord2
3
+
4
+ class Configuration < FriendlyId::Configuration
5
+
6
+ # The column used to cache the friendly_id string. If no column is specified,
7
+ # FriendlyId will look for a column named +cached_slug+ and use it automatically
8
+ # if it exists. If for some reason you have a column named +cached_slug+
9
+ # but don't want FriendlyId to modify it, pass the option
10
+ # +:cache_column => false+ to {FriendlyId::ActiveRecord2#has_friendly_id has_friendly_id}.
11
+ attr_accessor :cache_column
12
+
13
+ # An array of classes for which the configured class serves as a
14
+ # FriendlyId scope.
15
+ attr_reader :child_scopes
16
+
17
+ attr_reader :custom_cache_column
18
+
19
+ def cache_column
20
+ return @cache_column if defined?(@cache_column)
21
+ @cache_column = autodiscover_cache_column
22
+ end
23
+
24
+ def cache_column?
25
+ !! cache_column
26
+ end
27
+
28
+ def cache_column=(cache_column)
29
+ @cache_column = cache_column
30
+ @custom_cache_column = cache_column
31
+ end
32
+
33
+ def cache_finders?
34
+ !! cache_column
35
+ end
36
+
37
+ def child_scopes
38
+ @child_scopes ||= associated_friendly_classes.select { |klass| klass.friendly_id_config.scopes_over?(configured_class) }
39
+ end
40
+
41
+ def custom_cache_column?
42
+ !! custom_cache_column
43
+ end
44
+
45
+ def scope_for(record)
46
+ scope? ? record.send(scope).to_param : nil
47
+ end
48
+
49
+ def scopes_over?(klass)
50
+ scope? && scope == klass.to_s.underscore.to_sym
51
+ end
52
+
53
+ private
54
+
55
+ def autodiscover_cache_column
56
+ :cached_slug if configured_class.columns.any? { |column| column.name == 'cached_slug' }
57
+ end
58
+
59
+ def associated_friendly_classes
60
+ configured_class.reflect_on_all_associations.select { |assoc|
61
+ assoc.klass.uses_friendly_id? }.map(&:klass)
62
+ end
63
+
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,140 @@
1
+ module FriendlyId
2
+
3
+ # The adapter for Ruby on Rails's ActiveRecord. Compatible with AR 2.2.x -
4
+ # 2.3.x.
5
+ module ActiveRecord2
6
+
7
+ # The classes in this module are used internally by FriendlyId, and exist
8
+ # largely to avoid polluting the ActiveRecord models with too many
9
+ # FriendlyId-specific methods.
10
+ module Finders
11
+
12
+ # FinderProxy is used to choose which finder class to instantiate;
13
+ # depending on the model_class's +friendly_id_config+ and the options
14
+ # passed into the constructor, it will decide whether to use simple or
15
+ # slugged finder, a single or multiple finder, and in the case of slugs,
16
+ # a cached or uncached finder.
17
+ class FinderProxy
18
+
19
+ attr_reader :finder
20
+ attr :finder_class
21
+ attr :ids
22
+ attr :model_class
23
+ attr :options
24
+
25
+ def initialize(ids, model_class, options={})
26
+ @ids = ids
27
+ @model_class = model_class
28
+ @options = options
29
+ end
30
+
31
+ def method_missing(symbol, *args)
32
+ finder.send(symbol, *args)
33
+ end
34
+
35
+ # Perform the find query.
36
+ def finder
37
+ @finder ||= finder_class.new(ids, model_class, options)
38
+ end
39
+
40
+ private
41
+
42
+ def finder_class
43
+ @finder_class ||= slugged? ? slugged_finder_class : simple_finder_class
44
+ end
45
+
46
+ private
47
+
48
+ def cache_available?
49
+ !! model_class.friendly_id_config.cache_column
50
+ end
51
+
52
+ def multiple?
53
+ ids.kind_of? Array
54
+ end
55
+
56
+ def multiple_slugged_finder_class
57
+ use_cache? ? SluggedModel::CachedMultipleFinder : SluggedModel::MultipleFinder
58
+ end
59
+
60
+ def simple_finder_class
61
+ multiple? ? SimpleModel::MultipleFinder : SimpleModel::SingleFinder
62
+ end
63
+
64
+ def slugged?
65
+ !! model_class.friendly_id_config.use_slug?
66
+ end
67
+
68
+ def slugged_finder_class
69
+ multiple? ? multiple_slugged_finder_class : single_slugged_finder_class
70
+ end
71
+
72
+ def scoped?
73
+ !! options[:scope]
74
+ end
75
+
76
+ def single_slugged_finder_class
77
+ use_cache? ? SluggedModel::CachedSingleFinder : SluggedModel::SingleFinder
78
+ end
79
+
80
+ def use_cache?
81
+ cache_available? and !scoped?
82
+ end
83
+
84
+ end
85
+
86
+ # Wraps finds for multiple records using an array of friendly_ids.
87
+ # @abstract
88
+ module Multiple
89
+
90
+ attr_reader :friendly_ids, :results, :unfriendly_ids
91
+
92
+ def initialize(ids, model_class, options={})
93
+ @friendly_ids, @unfriendly_ids = ids.partition {|id| FriendlyId::Finders::Base.friendly?(id) }
94
+ @unfriendly_ids = @unfriendly_ids.map {|id| id.class.respond_to?(:friendly_id_config) ? id.id : id}
95
+ super
96
+ end
97
+
98
+ private
99
+
100
+ # An error message to use when the wrong number of results was returned.
101
+ def error_message
102
+ "Couldn't find all %s with IDs (%s) AND %s (found %d results, but was looking for %d)" % [
103
+ model_class.name.pluralize,
104
+ ids.join(', '),
105
+ sanitize_sql(options[:conditions]),
106
+ results.size,
107
+ expected_size
108
+ ]
109
+ end
110
+
111
+ # How many results do we expect?
112
+ def expected_size
113
+ limited? ? limit : offset_size
114
+ end
115
+
116
+ # The limit option passed to the find.
117
+ def limit
118
+ options[:limit]
119
+ end
120
+
121
+ # Is the find limited?
122
+ def limited?
123
+ offset_size > limit if limit
124
+ end
125
+
126
+ # The offset used for the find. If no offset was passed, 0 is returned.
127
+ def offset
128
+ options[:offset].to_i
129
+ end
130
+
131
+ # The number of ids, minus the offset.
132
+ def offset_size
133
+ ids.size - offset
134
+ end
135
+
136
+ end
137
+
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,162 @@
1
+ module FriendlyId
2
+ module ActiveRecord2
3
+
4
+ module SimpleModel
5
+
6
+ # Some basic methods common to {MultipleFinder} and {SingleFinder}.
7
+ module SimpleFinder
8
+
9
+ # The column used to store the friendly_id.
10
+ def column
11
+ "#{table_name}.#{friendly_id_config.column}"
12
+ end
13
+
14
+ # The model's fully-qualified and quoted primary key.
15
+ def primary_key
16
+ "#{quoted_table_name}.#{model_class.send :primary_key}"
17
+ end
18
+
19
+ end
20
+
21
+ class MultipleFinder
22
+
23
+ include FriendlyId::Finders::Base
24
+ include FriendlyId::ActiveRecord2::Finders::Multiple
25
+ include SimpleFinder
26
+
27
+ def find
28
+ @results = with_scope(:find => options) { find_every :conditions => conditions }
29
+ raise(::ActiveRecord::RecordNotFound, error_message) if @results.size != expected_size
30
+ friendly_results.each { |result| result.friendly_id_status.name = result.to_param }
31
+ @results
32
+ end
33
+
34
+ private
35
+
36
+ def conditions
37
+ ["#{primary_key} IN (?) OR #{column} IN (?)", unfriendly_ids, friendly_ids]
38
+ end
39
+
40
+ def friendly_results
41
+ results.select { |result| friendly_ids.include? result.to_param }
42
+ end
43
+
44
+ end
45
+
46
+ class SingleFinder
47
+
48
+ include FriendlyId::Finders::Base
49
+ include FriendlyId::Finders::Single
50
+ include SimpleFinder
51
+
52
+ def find
53
+ result = with_scope(:find => find_options) { find_initial options }
54
+ raise ::ActiveRecord::RecordNotFound.new if friendly? && !result
55
+ result.friendly_id_status.name = id if result
56
+ result
57
+ end
58
+
59
+ private
60
+
61
+ def find_options
62
+ @find_options ||= {:conditions => {column => id}}
63
+ end
64
+
65
+ end
66
+
67
+ # The methods in this module override ActiveRecord's +find_one+ and
68
+ # +find_some+ to add FriendlyId's features.
69
+ module FinderMethods
70
+ protected
71
+
72
+ def find_one(id, options)
73
+ finder = Finders::FinderProxy.new(id, self, options)
74
+ !finder.friendly? ? super : finder.find
75
+ end
76
+
77
+ def find_some(ids_and_names, options)
78
+ Finders::FinderProxy.new(ids_and_names, self, options).find
79
+ end
80
+ end
81
+
82
+ # These methods will be removed in FriendlyId 3.0.
83
+ module DeprecatedMethods
84
+
85
+ # Was the record found using one of its friendly ids?
86
+ # @deprecated Please use #friendly_id_status.friendly?
87
+ def found_using_friendly_id?
88
+ warn("found_using_friendly_id? is deprecated and will be removed in 3.0. Please use #friendly_id_status.friendly?")
89
+ friendly_id_status.friendly?
90
+ end
91
+
92
+ # Was the record found using its numeric id?
93
+ # @deprecated Please use #friendly_id_status.numeric?
94
+ def found_using_numeric_id?
95
+ warn("found_using_numeric_id is deprecated and will be removed in 3.0. Please use #friendly_id_status.numeric?")
96
+ friendly_id_status.numeric?
97
+ end
98
+
99
+ # Was the record found using an old friendly id, or its numeric id?
100
+ # @deprecated Please use !#friendly_id_status.best?
101
+ def has_better_id?
102
+ warn("has_better_id? is deprecated and will be removed in 3.0. Please use !#friendly_id_status.best?")
103
+ ! friendly_id_status.best?
104
+ end
105
+
106
+ end
107
+
108
+ def self.included(base)
109
+ base.class_eval do
110
+ column = friendly_id_config.column
111
+ validate :validate_friendly_id
112
+ validates_presence_of column
113
+ validates_length_of column, :maximum => friendly_id_config.max_length
114
+ after_update :update_scopes
115
+ extend FinderMethods
116
+ include DeprecatedMethods
117
+ end
118
+ end
119
+
120
+ # Get the {FriendlyId::Status} after the find has been performed.
121
+ def friendly_id_status
122
+ @friendly_id_status ||= Status.new :record => self
123
+ end
124
+
125
+ # Returns the friendly_id.
126
+ def friendly_id
127
+ send friendly_id_config.column
128
+ end
129
+ alias best_id friendly_id
130
+
131
+ # Returns the friendly id, or if none is available, the numeric id.
132
+ def to_param
133
+ (friendly_id || id).to_s
134
+ end
135
+
136
+ private
137
+
138
+ # The old and new values for the friendly_id column.
139
+ def friendly_id_changes
140
+ changes[friendly_id_config.column.to_s]
141
+ end
142
+
143
+ # Update the slugs for any model that is using this model as its
144
+ # FriendlyId scope.
145
+ def update_scopes
146
+ if changes = friendly_id_changes
147
+ friendly_id_config.child_scopes.each do |klass|
148
+ Slug.update_all "scope = '#{changes[1]}'", ["sluggable_type = ? AND scope = ?", klass.to_s, changes[0]]
149
+ end
150
+ end
151
+ end
152
+
153
+ def validate_friendly_id
154
+ if result = friendly_id_config.reserved_error_message(friendly_id)
155
+ self.errors.add(*result)
156
+ return false
157
+ end
158
+ end
159
+
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,111 @@
1
+ module FriendlyId
2
+ module ActiveRecord2
3
+
4
+ module DeprecatedSlugMethods
5
+ # @deprecated Please use String#parse_friendly_id
6
+ def parse(string)
7
+ warn("Slug#parse is deprecated and will be removed in FriendlyId 3.0. Please use String#parse_friendly_id.")
8
+ string.to_s.parse_friendly_id
9
+ end
10
+
11
+ # @deprecated Please use SlugString#normalize.
12
+ def normalize(slug_text)
13
+ warn("Slug#normalize is deprecated and will be removed in FriendlyId 3.0. Please use SlugString#normalize.")
14
+ raise BlankError if slug_text.blank?
15
+ SlugString.new(slug_text.to_s).normalize.to_s
16
+ end
17
+
18
+ # @deprecated Please use SlugString#approximate_ascii."
19
+ def strip_diacritics(string)
20
+ warn("Slug#strip_diacritics is deprecated and will be removed in FriendlyId 3.0. Please use SlugString#approximate_ascii.")
21
+ raise BlankError if string.blank?
22
+ SlugString.new(string).approximate_ascii
23
+ end
24
+
25
+ # @deprecated Please use SlugString#to_ascii.
26
+ def strip_non_ascii(string)
27
+ warn("Slug#strip_non_ascii is deprecated and will be removed in FriendlyId 3.0. Please use SlugString#to_ascii.")
28
+ raise BlankError if string.blank?
29
+ SlugString.new(string).to_ascii
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+
38
+ # A Slug is a unique, human-friendly identifier for an ActiveRecord.
39
+ class Slug < ::ActiveRecord::Base
40
+
41
+ extend FriendlyId::ActiveRecord2::DeprecatedSlugMethods
42
+
43
+ table_name = "slugs"
44
+ belongs_to :sluggable, :polymorphic => true
45
+ before_save :enable_name_reversion, :set_sequence
46
+ validate :validate_name
47
+ named_scope :similar_to, lambda {|slug| {:conditions => {
48
+ :name => slug.name,
49
+ :scope => slug.scope,
50
+ :sluggable_type => slug.sluggable_type
51
+ },
52
+ :order => "sequence ASC"
53
+ }
54
+ }
55
+
56
+ # Whether this slug is the most recent of its owner's slugs.
57
+ def current?
58
+ sluggable.slug == self
59
+ end
60
+
61
+ def outdated?
62
+ !current?
63
+ end
64
+
65
+ # @deprecated Please used Slug#current?
66
+ def is_most_recent?
67
+ warn("Slug#is_most_recent? is deprecated and will be removed in FriendlyId 3.0. Please use Slug#current?")
68
+ current?
69
+ end
70
+
71
+ def to_friendly_id
72
+ sequence > 1 ? friendly_id_with_sequence : name
73
+ end
74
+
75
+ # Raise a FriendlyId::SlugGenerationError if the slug name is blank.
76
+ def validate_name
77
+ if name.blank?
78
+ raise FriendlyId::BlankError.new("slug.name can not be blank.")
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ # If we're renaming back to a previously used friendly_id, delete the
85
+ # slug so that we can recycle the name without having to use a sequence.
86
+ def enable_name_reversion
87
+ sluggable.slugs.find_all_by_name_and_scope(name, scope).each { |slug| slug.destroy }
88
+ end
89
+
90
+ def friendly_id_with_sequence
91
+ "#{name}#{separator}#{sequence}"
92
+ end
93
+
94
+ def similar_to_other_slugs?
95
+ !similar_slugs.empty?
96
+ end
97
+
98
+ def similar_slugs
99
+ self.class.similar_to(self)
100
+ end
101
+
102
+ def separator
103
+ sluggable.friendly_id_config.sequence_separator
104
+ end
105
+
106
+ def set_sequence
107
+ return unless new_record?
108
+ self.sequence = similar_slugs.last.sequence.succ if similar_to_other_slugs?
109
+ end
110
+
111
+ end