cmassimo-friendly_id 3.0.4.2

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 (56) hide show
  1. data/Changelog.md +277 -0
  2. data/Contributors.md +39 -0
  3. data/Guide.md +561 -0
  4. data/LICENSE +19 -0
  5. data/README.md +83 -0
  6. data/Rakefile +66 -0
  7. data/extras/README.txt +3 -0
  8. data/extras/bench.rb +36 -0
  9. data/extras/extras.rb +38 -0
  10. data/extras/prof.rb +14 -0
  11. data/extras/template-gem.rb +26 -0
  12. data/extras/template-plugin.rb +28 -0
  13. data/generators/friendly_id/friendly_id_generator.rb +30 -0
  14. data/generators/friendly_id/templates/create_slugs.rb +18 -0
  15. data/lib/friendly_id.rb +73 -0
  16. data/lib/friendly_id/active_record.rb +52 -0
  17. data/lib/friendly_id/active_record_adapter/configuration.rb +67 -0
  18. data/lib/friendly_id/active_record_adapter/finders.rb +156 -0
  19. data/lib/friendly_id/active_record_adapter/simple_model.rb +123 -0
  20. data/lib/friendly_id/active_record_adapter/slug.rb +66 -0
  21. data/lib/friendly_id/active_record_adapter/slugged_model.rb +238 -0
  22. data/lib/friendly_id/active_record_adapter/tasks.rb +69 -0
  23. data/lib/friendly_id/configuration.rb +113 -0
  24. data/lib/friendly_id/finders.rb +109 -0
  25. data/lib/friendly_id/railtie.rb +20 -0
  26. data/lib/friendly_id/sequel.rb +5 -0
  27. data/lib/friendly_id/slug_string.rb +391 -0
  28. data/lib/friendly_id/slugged.rb +102 -0
  29. data/lib/friendly_id/status.rb +35 -0
  30. data/lib/friendly_id/test.rb +291 -0
  31. data/lib/friendly_id/version.rb +9 -0
  32. data/lib/generators/friendly_id_generator.rb +25 -0
  33. data/lib/tasks/friendly_id.rake +19 -0
  34. data/rails/init.rb +2 -0
  35. data/test/active_record_adapter/ar_test_helper.rb +119 -0
  36. data/test/active_record_adapter/basic_slugged_model_test.rb +14 -0
  37. data/test/active_record_adapter/cached_slug_test.rb +61 -0
  38. data/test/active_record_adapter/core.rb +98 -0
  39. data/test/active_record_adapter/custom_normalizer_test.rb +20 -0
  40. data/test/active_record_adapter/custom_table_name_test.rb +22 -0
  41. data/test/active_record_adapter/scoped_model_test.rb +118 -0
  42. data/test/active_record_adapter/simple_test.rb +76 -0
  43. data/test/active_record_adapter/slug_test.rb +34 -0
  44. data/test/active_record_adapter/slugged.rb +30 -0
  45. data/test/active_record_adapter/slugged_status_test.rb +25 -0
  46. data/test/active_record_adapter/sti_test.rb +22 -0
  47. data/test/active_record_adapter/support/database.jdbcsqlite3.yml +2 -0
  48. data/test/active_record_adapter/support/database.mysql.yml +4 -0
  49. data/test/active_record_adapter/support/database.postgres.yml +6 -0
  50. data/test/active_record_adapter/support/database.sqlite3.yml +2 -0
  51. data/test/active_record_adapter/support/models.rb +87 -0
  52. data/test/active_record_adapter/tasks_test.rb +82 -0
  53. data/test/friendly_id_test.rb +55 -0
  54. data/test/slug_string_test.rb +88 -0
  55. data/test/test_helper.rb +15 -0
  56. metadata +168 -0
@@ -0,0 +1,52 @@
1
+ module FriendlyId
2
+
3
+ module ActiveRecordAdapter
4
+
5
+ module Compat
6
+ def self.scope_method
7
+ ActiveRecord::VERSION::STRING >= "3" ? :scope : :named_scope
8
+ end
9
+ end
10
+
11
+ include FriendlyId::Base
12
+
13
+ def has_friendly_id(method, options = {})
14
+ class_inheritable_accessor :friendly_id_config
15
+ write_inheritable_attribute :friendly_id_config, Configuration.new(self, method, options)
16
+ if friendly_id_config.use_slug?
17
+ include SluggedModel
18
+ else
19
+ include SimpleModel
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ # Prevent the cached_slug column from being accidentally or maliciously
26
+ # overwritten. Note that +attr_protected+ is used to protect the cached_slug
27
+ # column, unless you have already invoked +attr_accessible+. So if you
28
+ # wish to use +attr_accessible+, you must invoke it BEFORE you invoke
29
+ # {#has_friendly_id} in your class.
30
+ def protect_friendly_id_attributes
31
+ # only protect the column if the class is not already using attributes_accessible
32
+ if !accessible_attributes
33
+ if friendly_id_config.custom_cache_column?
34
+ attr_protected friendly_id_config.cache_column
35
+ end
36
+ attr_protected :cached_slug
37
+ end
38
+ end
39
+
40
+ end
41
+ end
42
+
43
+ class ActiveRecord::Base
44
+ extend FriendlyId::ActiveRecordAdapter
45
+ end
46
+
47
+ require File.join(File.dirname(__FILE__), "active_record_adapter", "configuration")
48
+ require File.join(File.dirname(__FILE__), "active_record_adapter", "finders")
49
+ require File.join(File.dirname(__FILE__), "active_record_adapter", "simple_model")
50
+ require File.join(File.dirname(__FILE__), "active_record_adapter", "slugged_model")
51
+ require File.join(File.dirname(__FILE__), "active_record_adapter", "slug")
52
+ require File.join(File.dirname(__FILE__), "active_record_adapter", "tasks")
@@ -0,0 +1,67 @@
1
+ module FriendlyId
2
+
3
+ module ActiveRecordAdapter
4
+
5
+ # Extends FriendlyId::Configuration with some implementation details and
6
+ # features specific to ActiveRecord.
7
+ class Configuration < FriendlyId::Configuration
8
+
9
+ # The column used to cache the friendly_id string. If no column is specified,
10
+ # FriendlyId will look for a column named +cached_slug+ and use it automatically
11
+ # if it exists. If for some reason you have a column named +cached_slug+
12
+ # but don't want FriendlyId to modify it, pass the option
13
+ # +:cache_column => false+ to {FriendlyId::ActiveRecordAdapter#has_friendly_id has_friendly_id}.
14
+ attr_accessor :cache_column
15
+
16
+ # An array of classes for which the configured class serves as a
17
+ # FriendlyId scope.
18
+ attr_reader :child_scopes
19
+
20
+ attr_reader :custom_cache_column
21
+
22
+ def cache_column
23
+ return @cache_column if defined?(@cache_column)
24
+ @cache_column = autodiscover_cache_column
25
+ end
26
+
27
+ def cache_column?
28
+ !! cache_column
29
+ end
30
+
31
+ def cache_column=(cache_column)
32
+ @cache_column = cache_column
33
+ @custom_cache_column = cache_column
34
+ end
35
+
36
+ def child_scopes
37
+ @child_scopes ||= associated_friendly_classes.select { |klass| klass.friendly_id_config.scopes_over?(configured_class) }
38
+ end
39
+
40
+ def custom_cache_column?
41
+ !! custom_cache_column
42
+ end
43
+
44
+ def scope_for(record)
45
+ scope? ? record.send(scope).to_param : nil
46
+ end
47
+
48
+ def scopes_over?(klass)
49
+ scope? && scope == klass.to_s.underscore.to_sym
50
+ end
51
+
52
+ private
53
+
54
+ def autodiscover_cache_column
55
+ :cached_slug if configured_class.columns.any? { |column| column.name == 'cached_slug' }
56
+ end
57
+
58
+ def associated_friendly_classes
59
+ configured_class.reflect_on_all_associations.select { |assoc|
60
+ !assoc.options[:polymorphic] &&
61
+ assoc.klass.uses_friendly_id?
62
+ }.map(&:klass)
63
+ end
64
+
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,156 @@
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 ActiveRecordAdapter
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
+ extend Forwardable
20
+
21
+ attr_reader :finder
22
+ attr :finder_class
23
+ attr :ids
24
+ attr :model_class
25
+ attr :options
26
+
27
+ def_delegators :finder, :find, :unfriendly?
28
+
29
+ def initialize(model_class, *args, &block)
30
+ @model_class = model_class
31
+ @ids = args.shift
32
+ @options = args.first.kind_of?(Hash) ? args.first : {}
33
+ end
34
+
35
+ # Perform the find query.
36
+ def finder
37
+ @finder ||= finder_class.new(ids, model_class, options)
38
+ end
39
+
40
+ def finder_class
41
+ @finder_class ||= slugged? ? slugged_finder_class : simple_finder_class
42
+ end
43
+
44
+ def multiple?
45
+ ids.kind_of? Array
46
+ end
47
+
48
+ private
49
+
50
+ def cache_available?
51
+ !! model_class.friendly_id_config.cache_column
52
+ end
53
+
54
+ def multiple_slugged_finder_class
55
+ use_cache? ? SluggedModel::CachedMultipleFinder : SluggedModel::MultipleFinder
56
+ end
57
+
58
+ def simple_finder_class
59
+ multiple? ? SimpleModel::MultipleFinder : SimpleModel::SingleFinder
60
+ end
61
+
62
+ def slugged?
63
+ !! model_class.friendly_id_config.use_slug?
64
+ end
65
+
66
+ def slugged_finder_class
67
+ multiple? ? multiple_slugged_finder_class : single_slugged_finder_class
68
+ end
69
+
70
+ def scoped?
71
+ !! options[:scope]
72
+ end
73
+
74
+ def single_slugged_finder_class
75
+ use_cache? ? SluggedModel::CachedSingleFinder : SluggedModel::SingleFinder
76
+ end
77
+
78
+ def use_cache?
79
+ cache_available? and !scoped?
80
+ end
81
+
82
+ end
83
+
84
+ # Wraps finds for multiple records using an array of friendly_ids.
85
+ # @abstract
86
+ module Multiple
87
+
88
+ include FriendlyId::Finders::Base
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
+
140
+ # The methods in this module override ActiveRecord's +find_one+ and
141
+ # +find_some+ to add FriendlyId's features.
142
+ module FinderMethods
143
+
144
+ def find(*args, &block)
145
+ finder = Finders::FinderProxy.new(self, *args, &block)
146
+ if finder.multiple?
147
+ finder.find
148
+ else
149
+ finder.unfriendly? ? super : finder.find or super
150
+ end
151
+ end
152
+
153
+ end
154
+
155
+ end
156
+ end
@@ -0,0 +1,123 @@
1
+ module FriendlyId
2
+ module ActiveRecordAdapter
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::ActiveRecordAdapter::Finders::Multiple
24
+ include SimpleFinder
25
+
26
+ def find
27
+ @results = model_class.scoped(:conditions => conditions).all(options).uniq
28
+ raise(::ActiveRecord::RecordNotFound, error_message) if @results.size != expected_size
29
+ friendly_results.each { |result| result.friendly_id_status.name = result.to_param }
30
+ @results
31
+ end
32
+
33
+ private
34
+
35
+ def conditions
36
+ ["#{primary_key} IN (?) OR #{column} IN (?)", unfriendly_ids, friendly_ids]
37
+ end
38
+
39
+ def friendly_results
40
+ results.select { |result| friendly_ids.include? result.to_param }
41
+ end
42
+
43
+ end
44
+
45
+ class SingleFinder
46
+
47
+ include FriendlyId::Finders::Base
48
+ include FriendlyId::Finders::Single
49
+ include SimpleFinder
50
+
51
+ def find
52
+ result = model_class.scoped(find_options).first(options)
53
+ raise ::ActiveRecord::RecordNotFound.new if friendly? && !result
54
+ result.friendly_id_status.name = id if result
55
+ result
56
+ end
57
+
58
+ private
59
+
60
+ def find_options
61
+ @find_options ||= {:conditions => {column => id}}
62
+ end
63
+
64
+ end
65
+
66
+ def self.included(base)
67
+ base.class_eval do
68
+ column = friendly_id_config.column
69
+ validate :validate_friendly_id, :unless => :skip_friendly_id_validations
70
+ validates_presence_of column, :unless => :skip_friendly_id_validations
71
+ validates_length_of column, :maximum => friendly_id_config.max_length, :unless => :skip_friendly_id_validations
72
+ after_update :update_scopes
73
+ extend FriendlyId::ActiveRecordAdapter::FinderMethods
74
+ end
75
+ end
76
+
77
+ # Get the {FriendlyId::Status} after the find has been performed.
78
+ def friendly_id_status
79
+ @friendly_id_status ||= Status.new :record => self
80
+ end
81
+
82
+ # Returns the friendly_id.
83
+ def friendly_id
84
+ send friendly_id_config.column
85
+ end
86
+ alias best_id friendly_id
87
+
88
+ # Returns the friendly id, or if none is available, the numeric id.
89
+ def to_param
90
+ (friendly_id || id).to_s
91
+ end
92
+
93
+ private
94
+
95
+ # The old and new values for the friendly_id column.
96
+ def friendly_id_changes
97
+ changes[friendly_id_config.column.to_s]
98
+ end
99
+
100
+ # Update the slugs for any model that is using this model as its
101
+ # FriendlyId scope.
102
+ def update_scopes
103
+ if changes = friendly_id_changes
104
+ friendly_id_config.child_scopes.each do |klass|
105
+ Slug.update_all "scope = '#{changes[1]}'", ["sluggable_type = ? AND scope = ?", klass.to_s, changes[0]]
106
+ end
107
+ end
108
+ end
109
+
110
+ def skip_friendly_id_validations
111
+ friendly_id.nil? && friendly_id_config.allow_nil?
112
+ end
113
+
114
+ def validate_friendly_id
115
+ if result = friendly_id_config.reserved_error_message(friendly_id)
116
+ self.errors.add(*result)
117
+ return false
118
+ end
119
+ end
120
+
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,66 @@
1
+ # A Slug is a unique, human-friendly identifier for an ActiveRecord.
2
+ class Slug < ::ActiveRecord::Base
3
+
4
+ table_name = "slugs"
5
+ belongs_to :sluggable, :polymorphic => true
6
+ before_save :enable_name_reversion, :set_sequence
7
+ validate :validate_name
8
+ send FriendlyId::ActiveRecordAdapter::Compat.scope_method, :similar_to, lambda {|slug| {:conditions => {
9
+ :name => slug.name,
10
+ :scope => slug.scope,
11
+ :sluggable_type => slug.sluggable_type
12
+ },
13
+ :order => "sequence ASC"
14
+ }
15
+ }
16
+
17
+ # Whether this slug is the most recent of its owner's slugs.
18
+ def current?
19
+ sluggable.slug == self
20
+ end
21
+
22
+ def outdated?
23
+ !current?
24
+ end
25
+
26
+ def to_friendly_id
27
+ sequence > 1 ? friendly_id_with_sequence : name
28
+ end
29
+
30
+ # Raise a FriendlyId::SlugGenerationError if the slug name is blank.
31
+ def validate_name
32
+ if name.blank?
33
+ raise FriendlyId::BlankError.new("slug.name can not be blank.")
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ # If we're renaming back to a previously used friendly_id, delete the
40
+ # slug so that we can recycle the name without having to use a sequence.
41
+ def enable_name_reversion
42
+ sluggable.slugs.find_all_by_name_and_scope(name, scope).each { |slug| slug.destroy }
43
+ end
44
+
45
+ def friendly_id_with_sequence
46
+ "#{name}#{separator}#{sequence}"
47
+ end
48
+
49
+ def similar_to_other_slugs?
50
+ !similar_slugs.empty?
51
+ end
52
+
53
+ def similar_slugs
54
+ self.class.similar_to(self)
55
+ end
56
+
57
+ def separator
58
+ sluggable.friendly_id_config.sequence_separator
59
+ end
60
+
61
+ def set_sequence
62
+ return unless new_record?
63
+ self.sequence = similar_slugs.last.sequence.succ if similar_to_other_slugs?
64
+ end
65
+
66
+ end