friendly_id 2.2.7 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -1,45 +0,0 @@
1
- module FriendlyId::NonSluggableInstanceMethods
2
-
3
- def self.included(base)
4
- base.validate :validate_friendly_id
5
- end
6
-
7
- attr :found_using_friendly_id
8
-
9
- # Was the record found using one of its friendly ids?
10
- def found_using_friendly_id?
11
- @found_using_friendly_id
12
- end
13
-
14
- # Was the record found using its numeric id?
15
- def found_using_numeric_id?
16
- !@found_using_friendly_id
17
- end
18
- alias has_better_id? found_using_numeric_id?
19
-
20
- # Returns the friendly_id.
21
- def friendly_id
22
- send friendly_id_options[:method]
23
- end
24
- alias best_id friendly_id
25
-
26
- # Returns the friendly id, or if none is available, the numeric id.
27
- def to_param
28
- (friendly_id || id).to_s
29
- end
30
-
31
- private
32
-
33
- def validate_friendly_id
34
- if self.class.friendly_id_options[:reserved].include? friendly_id
35
- self.errors.add(self.class.friendly_id_options[:method],
36
- self.class.friendly_id_options[:reserved_message] % friendly_id)
37
- return false
38
- end
39
- end
40
-
41
- def found_using_friendly_id=(value) #:nodoc#
42
- @found_using_friendly_id = value
43
- end
44
-
45
- end
@@ -1,98 +0,0 @@
1
- # A Slug is a unique, human-friendly identifier for an ActiveRecord.
2
- class Slug < ActiveRecord::Base
3
-
4
- belongs_to :sluggable, :polymorphic => true
5
- before_save :check_for_blank_name, :set_sequence
6
-
7
-
8
- ASCII_APPROXIMATIONS = {
9
- 198 => "AE",
10
- 208 => "D",
11
- 216 => "O",
12
- 222 => "Th",
13
- 223 => "ss",
14
- 230 => "ae",
15
- 240 => "d",
16
- 248 => "o",
17
- 254 => "th"
18
- }.freeze
19
-
20
- class << self
21
-
22
- # Sanitizes and dasherizes string to make it safe for URL's.
23
- #
24
- # Example:
25
- #
26
- # slug.normalize('This... is an example!') # => "this-is-an-example"
27
- #
28
- # Note that the Unicode handling in ActiveSupport may fail to process some
29
- # characters from Polish, Icelandic and other languages.
30
- def normalize(slug_text)
31
- return "" if slug_text.nil? || slug_text == ""
32
- ActiveSupport::Multibyte.proxy_class.new(slug_text.to_s).normalize(:kc).
33
- gsub(/[\W]/u, ' ').
34
- strip.
35
- gsub(/\s+/u, '-').
36
- gsub(/-\z/u, '').
37
- downcase.
38
- to_s
39
- end
40
-
41
- def parse(friendly_id)
42
- name, sequence = friendly_id.split('--')
43
- sequence ||= "1"
44
- return name, sequence
45
- end
46
-
47
- # Remove diacritics (accents, umlauts, etc.) from the string. Borrowed
48
- # from "The Ruby Way."
49
- def strip_diacritics(string)
50
- a = ActiveSupport::Multibyte.proxy_class.new(string || "").normalize(:kd)
51
- a.unpack('U*').inject([]) { |a, u|
52
- if ASCII_APPROXIMATIONS[u]
53
- a += ASCII_APPROXIMATIONS[u].unpack('U*')
54
- elsif (u < 0x300 || u > 0x036F)
55
- a << u
56
- end
57
- a
58
- }.pack('U*')
59
- end
60
-
61
-
62
-
63
- # Remove non-ascii characters from the string.
64
- def strip_non_ascii(string)
65
- strip_diacritics(string).gsub(/[^a-z0-9]+/i, ' ')
66
- end
67
-
68
- private
69
-
70
- end
71
-
72
- # Whether or not this slug is the most recent of its owner's slugs.
73
- def is_most_recent?
74
- sluggable.slug == self
75
- end
76
-
77
- def to_friendly_id
78
- sequence > 1 ? "#{name}--#{sequence}" : name
79
- end
80
-
81
- protected
82
-
83
- # Raise a FriendlyId::SlugGenerationError if the slug name is blank.
84
- def check_for_blank_name #:nodoc:#
85
- if name.blank?
86
- raise FriendlyId::SlugGenerationError.new("The slug text is blank.")
87
- end
88
- end
89
-
90
- def set_sequence
91
- return unless new_record?
92
- last = Slug.find(:first, :conditions => { :name => name, :scope => scope,
93
- :sluggable_type => sluggable_type}, :order => "sequence DESC",
94
- :select => 'sequence')
95
- self.sequence = last.sequence + 1 if last
96
- end
97
-
98
- end
@@ -1,110 +0,0 @@
1
- module FriendlyId::SluggableClassMethods
2
-
3
- include FriendlyId::Helpers
4
-
5
- # Finds a single record using the friendly id, or the record's id.
6
- def find_one(id_or_name, options) #:nodoc:#
7
-
8
- scope = options.delete(:scope)
9
- scope = scope.to_param if scope && scope.respond_to?(:to_param)
10
-
11
- if id_or_name.is_a?(Integer) || id_or_name.kind_of?(ActiveRecord::Base)
12
- return super(id_or_name, options)
13
- end
14
-
15
- find_options = {:select => "#{self.table_name}.*"}
16
- find_options[:joins] = :slugs unless options[:include] && [*options[:include]].flatten.include?(:slugs)
17
-
18
- name, sequence = Slug.parse(id_or_name)
19
-
20
- find_options[:conditions] = {
21
- "#{Slug.table_name}.name" => name,
22
- "#{Slug.table_name}.scope" => scope,
23
- "#{Slug.table_name}.sequence" => sequence
24
- }
25
-
26
- result = with_scope(:find => find_options) { find_initial(options) }
27
- if result
28
- result.finder_slug_name = id_or_name
29
- elsif id_or_name.to_i.to_s != id_or_name
30
- raise ActiveRecord::RecordNotFound
31
- else
32
- result = super id_or_name, options
33
- end
34
-
35
- result
36
-
37
- rescue ActiveRecord::RecordNotFound => e
38
-
39
- if friendly_id_options[:scope]
40
- if !scope
41
- raise ActiveRecord::RecordNotFound.new("%s; expected scope but got none" % e.message)
42
- else
43
- raise ActiveRecord::RecordNotFound.new("%s and scope=#{scope}" % e.message)
44
- end
45
- end
46
-
47
- raise e
48
-
49
- end
50
-
51
- # Finds multiple records using the friendly ids, or the records' ids.
52
- def find_some(ids_and_names, options) #:nodoc:#
53
-
54
- slugs, ids = get_slugs_and_ids(ids_and_names, options)
55
- results = []
56
-
57
- find_options = {:select => "#{self.table_name}.*"}
58
- find_options[:joins] = :slugs unless options[:include] && [*options[:include]].flatten.include?(:slugs)
59
- find_options[:conditions] = "#{quoted_table_name}.#{primary_key} IN (#{ids.empty? ? 'NULL' : ids.join(',')}) "
60
- find_options[:conditions] << "OR slugs.id IN (#{slugs.to_s(:db)})"
61
-
62
- results = with_scope(:find => find_options) { find_every(options) }.uniq
63
-
64
- expected = expected_size(ids_and_names, options)
65
- if results.size != expected
66
- raise ActiveRecord::RecordNotFound, "Couldn't find all #{ name.pluralize } with IDs (#{ ids_and_names * ', ' }) AND #{ sanitize_sql options[:conditions] } (found #{ results.size } results, but was looking for #{ expected })"
67
- end
68
-
69
- assign_finder_slugs(slugs, results)
70
-
71
- results
72
- end
73
-
74
- def validate_find_options(options) #:nodoc:#
75
- options.assert_valid_keys([:conditions, :include, :joins, :limit, :offset,
76
- :order, :select, :readonly, :group, :from, :lock, :having, :scope])
77
- end
78
-
79
- private
80
-
81
- # Assign finder slugs for the results found in find_some_with_friendly
82
- def assign_finder_slugs(slugs, results) #:nodoc:#
83
- slugs.each do |slug|
84
- results.select { |r| r.id == slug.sluggable_id }.each do |result|
85
- result.send(:finder_slug=, slug)
86
- end
87
- end
88
- end
89
-
90
- # Build arrays of slugs and ids, for the find_some_with_friendly method.
91
- def get_slugs_and_ids(ids_and_names, options) #:nodoc:#
92
- scope = options.delete(:scope)
93
- slugs = []
94
- ids = []
95
- ids_and_names.each do |id_or_name|
96
- name, sequence = Slug.parse id_or_name.to_s
97
- slug = Slug.find(:first, :conditions => {
98
- :name => name,
99
- :scope => scope,
100
- :sequence => sequence,
101
- :sluggable_type => base_class.name
102
- })
103
- # If the slug was found, add it to the array for later use. If not, and
104
- # the id_or_name is a number, assume that it is a regular record id.
105
- slug ? slugs << slug : (ids << id_or_name if id_or_name.to_s =~ /\A\d*\z/)
106
- end
107
- return slugs, ids
108
- end
109
-
110
- end
@@ -1,161 +0,0 @@
1
- module FriendlyId::SluggableInstanceMethods
2
-
3
- def self.included(base)
4
- base.class_eval do
5
- has_many :slugs, :order => 'id DESC', :as => :sluggable, :dependent => :destroy
6
- before_save :set_slug
7
- after_save :set_slug_cache
8
- # only protect the column if the class is not already using attributes_accessible
9
- if !accessible_attributes
10
- if friendly_id_options[:cache_column]
11
- attr_protected friendly_id_options[:cache_column].to_sym
12
- end
13
- attr_protected :cached_slug
14
- end
15
- end
16
-
17
- def base.cache_column
18
- if defined?(@cache_column)
19
- return @cache_column
20
- elsif friendly_id_options[:cache_column]
21
- @cache_column = friendly_id_options[:cache_column].to_sym
22
- elsif columns.any? { |c| c.name == 'cached_slug' }
23
- @cache_column = :cached_slug
24
- else
25
- @cache_column = nil
26
- end
27
- end
28
-
29
- end
30
-
31
- NUM_CHARS_RESERVED_FOR_FRIENDLY_ID_EXTENSION = 2
32
-
33
- attr :finder_slug
34
- attr_accessor :finder_slug_name
35
-
36
- def finder_slug
37
- @finder_slug ||= init_finder_slug or nil
38
- end
39
-
40
- # Was the record found using one of its friendly ids?
41
- def found_using_friendly_id?
42
- !!@finder_slug_name
43
- end
44
-
45
- # Was the record found using its numeric id?
46
- def found_using_numeric_id?
47
- !found_using_friendly_id?
48
- end
49
-
50
- # Was the record found using an old friendly id?
51
- def found_using_outdated_friendly_id?
52
- return false if cache_column && send(cache_column) == @finder_slug_name
53
- finder_slug.id != slug.id
54
- end
55
-
56
- # Was the record found using an old friendly id, or its numeric id?
57
- def has_better_id?
58
- has_a_slug? and found_using_numeric_id? || found_using_outdated_friendly_id?
59
- end
60
-
61
- # Does the record have (at least) one slug?
62
- def has_a_slug?
63
- @finder_slug_name || slug
64
- end
65
-
66
- # Returns the friendly id.
67
- def friendly_id
68
- slug(true).to_friendly_id
69
- end
70
- alias best_id friendly_id
71
-
72
- # Has the basis of our friendly id changed, requiring the generation of a
73
- # new slug?
74
- def new_slug_needed?
75
- !slug || slug_text != slug.name
76
- end
77
-
78
- # Returns the most recent slug, which is used to determine the friendly
79
- # id.
80
- def slug(reload = false)
81
- @most_recent_slug = nil if reload
82
- @most_recent_slug ||= slugs.first(:order => "id DESC")
83
- end
84
-
85
- # Returns the friendly id, or if none is available, the numeric id.
86
- def to_param
87
- if cache_column
88
- read_attribute(cache_column) || id.to_s
89
- else
90
- slug ? slug.to_friendly_id : id.to_s
91
- end
92
- end
93
-
94
- # Get the processed string used as the basis of the friendly id.
95
- def slug_text
96
- base = send friendly_id_options[:method]
97
- if self.slug_normalizer_block
98
- base = self.slug_normalizer_block.call(base)
99
- else
100
- if self.friendly_id_options[:strip_diacritics]
101
- base = Slug::strip_diacritics(base)
102
- end
103
- if self.friendly_id_options[:strip_non_ascii]
104
- base = Slug::strip_non_ascii(base)
105
- end
106
- base = Slug::normalize(base)
107
- end
108
-
109
- if base.mb_chars.length > friendly_id_options[:max_length]
110
- base = base.mb_chars[0...friendly_id_options[:max_length]]
111
- end
112
- if friendly_id_options[:reserved].include?(base)
113
- raise FriendlyId::SlugGenerationError.new("The slug text is a reserved value")
114
- end
115
- return base
116
- end
117
-
118
- private
119
-
120
- def cache_column
121
- self.class.cache_column
122
- end
123
-
124
- def finder_slug=(finder_slug)
125
- @finder_slug_name = finder_slug.name
126
- slug = finder_slug
127
- slug.sluggable = self
128
- slug
129
- end
130
-
131
- def init_finder_slug
132
- return false if !@finder_slug_name
133
- name, sequence = Slug.parse(@finder_slug_name)
134
- slug = Slug.find(:first, :conditions => {:sluggable_id => id, :name => name, :sequence => sequence, :sluggable_type => self.class.base_class.name })
135
- finder_slug = slug
136
- end
137
-
138
- # Set the slug using the generated friendly id.
139
- def set_slug
140
- if self.class.friendly_id_options[:use_slug] && new_slug_needed?
141
- @most_recent_slug = nil
142
- slug_attributes = {:name => slug_text}
143
- if friendly_id_options[:scope]
144
- scope = send(friendly_id_options[:scope])
145
- slug_attributes[:scope] = scope.respond_to?(:to_param) ? scope.to_param : scope.to_s
146
- end
147
- # If we're renaming back to a previously used friendly_id, delete the
148
- # slug so that we can recycle the name without having to use a sequence.
149
- slugs.find(:all, :conditions => {:name => slug_text, :scope => slug_attributes[:scope]}).each { |s| s.destroy }
150
- slugs.build slug_attributes
151
- end
152
- end
153
-
154
- def set_slug_cache
155
- if cache_column && send(cache_column) != slug.to_friendly_id
156
- send "#{cache_column}=", slug.to_friendly_id
157
- send :update_without_callbacks
158
- end
159
- end
160
-
161
- end
@@ -1,56 +0,0 @@
1
- module FriendlyId
2
- class Tasks
3
- class << self
4
-
5
- def make_slugs(klass, options = {})
6
- klass = parse_class_name(klass)
7
- validate_uses_slugs(klass)
8
- options = {:limit => 100, :include => :slugs, :conditions => "slugs.id IS NULL"}.merge(options)
9
- while records = klass.find(:all, options) do
10
- break if records.size == 0
11
- records.each do |r|
12
- r.save!
13
- yield(r) if block_given?
14
- end
15
- end
16
- end
17
-
18
- def delete_slugs_for(klass)
19
- klass = parse_class_name(klass)
20
- validate_uses_slugs(klass)
21
- Slug.destroy_all(["sluggable_type = ?", klass.to_s])
22
- if klass.cache_column
23
- klass.update_all("#{klass.cache_column} = NULL")
24
- end
25
- end
26
-
27
- def delete_old_slugs(days = nil, class_name = nil)
28
- days = days.blank? ? 45 : days.to_i
29
- klass = class_name.blank? ? nil : parse_class_name(class_name.to_s)
30
- conditions = ["created_at < ?", DateTime.now - days.days]
31
- if klass
32
- conditions[0] << " AND sluggable_type = ?"
33
- conditions << klass.to_s
34
- end
35
- slugs = Slug.find :all, :conditions => conditions
36
- slugs.each { |s| s.destroy unless s.is_most_recent? }
37
- end
38
-
39
- def parse_class_name(class_name)
40
- return class_name if class_name.class == Class
41
- if (class_name.split('::').size > 1)
42
- class_name.split('::').inject(Kernel) {|scope, const_name| scope.const_get(const_name)}
43
- else
44
- Object.const_get(class_name)
45
- end
46
- end
47
-
48
- private
49
-
50
- def validate_uses_slugs(klass)
51
- raise "Class '%s' doesn't use slugs" % klass.to_s unless klass.friendly_id_options[:use_slug]
52
- end
53
-
54
- end
55
- end
56
- end