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