nwp-friendly_id 2.1.3

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 (40) hide show
  1. data/History.txt +133 -0
  2. data/MIT-LICENSE +19 -0
  3. data/Manifest.txt +39 -0
  4. data/README.rdoc +343 -0
  5. data/Rakefile +49 -0
  6. data/config/website.yml +2 -0
  7. data/friendly_id.gemspec +45 -0
  8. data/generators/friendly_id/friendly_id_generator.rb +12 -0
  9. data/generators/friendly_id/templates/create_slugs.rb +18 -0
  10. data/generators/friendly_id_20_upgrade/friendly_id_20_upgrade_generator.rb +11 -0
  11. data/generators/friendly_id_20_upgrade/templates/upgrade_friendly_id_to_20.rb +19 -0
  12. data/init.rb +3 -0
  13. data/lib/friendly_id.rb +101 -0
  14. data/lib/friendly_id/helpers.rb +15 -0
  15. data/lib/friendly_id/non_sluggable_class_methods.rb +42 -0
  16. data/lib/friendly_id/non_sluggable_instance_methods.rb +43 -0
  17. data/lib/friendly_id/slug.rb +102 -0
  18. data/lib/friendly_id/sluggable_class_methods.rb +116 -0
  19. data/lib/friendly_id/sluggable_instance_methods.rb +115 -0
  20. data/lib/friendly_id/version.rb +10 -0
  21. data/lib/tasks/friendly_id.rake +50 -0
  22. data/lib/tasks/friendly_id.rb +1 -0
  23. data/test/contest.rb +94 -0
  24. data/test/custom_slug_normalizer_test.rb +35 -0
  25. data/test/models/book.rb +2 -0
  26. data/test/models/country.rb +4 -0
  27. data/test/models/event.rb +3 -0
  28. data/test/models/novel.rb +3 -0
  29. data/test/models/person.rb +6 -0
  30. data/test/models/post.rb +6 -0
  31. data/test/models/thing.rb +6 -0
  32. data/test/models/user.rb +3 -0
  33. data/test/non_slugged_test.rb +98 -0
  34. data/test/schema.rb +55 -0
  35. data/test/scoped_model_test.rb +53 -0
  36. data/test/slug_test.rb +106 -0
  37. data/test/slugged_model_test.rb +284 -0
  38. data/test/sti_test.rb +48 -0
  39. data/test/test_helper.rb +30 -0
  40. metadata +154 -0
@@ -0,0 +1,116 @@
1
+ # encoding: utf-8
2
+
3
+ module FriendlyId::SluggableClassMethods
4
+
5
+ include FriendlyId::Helpers
6
+
7
+ def self.extended(base) #:nodoc:#
8
+
9
+ class << base
10
+ alias_method_chain :find_one, :friendly
11
+ alias_method_chain :find_some, :friendly
12
+ alias_method_chain :validate_find_options, :friendly
13
+ end
14
+
15
+ end
16
+
17
+ # Finds a single record using the friendly id, or the record's id.
18
+ def find_one_with_friendly(id_or_name, options) #:nodoc:#
19
+
20
+ scope = options.delete(:scope)
21
+ return find_one_without_friendly(id_or_name, options) if id_or_name.is_a?(Integer)
22
+
23
+ find_options = {:select => "#{self.table_name}.*"}
24
+ find_options[:joins] = :slugs unless options[:include] && [*options[:include]].flatten.include?(:slugs)
25
+
26
+ name, sequence = Slug.parse(id_or_name)
27
+
28
+ find_options[:conditions] = {
29
+ "#{Slug.table_name}.name" => name,
30
+ "#{Slug.table_name}.scope" => scope,
31
+ "#{Slug.table_name}.sequence" => sequence
32
+ }
33
+
34
+ result = with_scope(:find => find_options) { find_initial(options) }
35
+
36
+ if result
37
+ result.finder_slug_name = id_or_name
38
+ else
39
+ result = find_one_without_friendly id_or_name, options
40
+ end
41
+
42
+ result
43
+ rescue ActiveRecord::RecordNotFound => e
44
+
45
+ if friendly_id_options[:scope]
46
+ if !scope
47
+ e.message << "; expected scope but got none"
48
+ else
49
+ e.message << " and scope=#{scope}"
50
+ end
51
+ end
52
+
53
+ raise e
54
+
55
+ end
56
+
57
+ # Finds multiple records using the friendly ids, or the records' ids.
58
+ def find_some_with_friendly(ids_and_names, options) #:nodoc:#
59
+
60
+ slugs, ids = get_slugs_and_ids(ids_and_names, options)
61
+ results = []
62
+
63
+ find_options = {:select => "#{self.table_name}.*"}
64
+ find_options[:joins] = :slugs unless options[:include] && [*options[:include]].flatten.include?(:slugs)
65
+ find_options[:conditions] = "#{quoted_table_name}.#{primary_key} IN (#{ids.empty? ? 'NULL' : ids.join(',')}) "
66
+ find_options[:conditions] << "OR slugs.id IN (#{slugs.to_s(:db)})"
67
+
68
+ results = with_scope(:find => find_options) { find_every(options) }.uniq
69
+
70
+ expected = expected_size(ids_and_names, options)
71
+ if results.size != expected
72
+ 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 })"
73
+ end
74
+
75
+ assign_finder_slugs(slugs, results)
76
+
77
+ results
78
+ end
79
+
80
+ def validate_find_options_with_friendly(options) #:nodoc:#
81
+ options.assert_valid_keys([:conditions, :include, :joins, :limit, :offset,
82
+ :order, :select, :readonly, :group, :from, :lock, :having, :scope])
83
+ end
84
+
85
+ private
86
+
87
+ # Assign finder slugs for the results found in find_some_with_friendly
88
+ def assign_finder_slugs(slugs, results) #:nodoc:#
89
+ slugs.each do |slug|
90
+ results.select { |r| r.id == slug.sluggable_id }.each do |result|
91
+ result.send(:finder_slug=, slug)
92
+ end
93
+ end
94
+ end
95
+
96
+ # Build arrays of slugs and ids, for the find_some_with_friendly method.
97
+ def get_slugs_and_ids(ids_and_names, options) #:nodoc:#
98
+ scope = options.delete(:scope)
99
+ slugs = []
100
+ ids = []
101
+ ids_and_names.each do |id_or_name|
102
+ name, sequence = Slug.parse id_or_name.to_s
103
+ slug = Slug.find(:first, :conditions => {
104
+ :name => name,
105
+ :scope => scope,
106
+ :sequence => sequence,
107
+ :sluggable_type => base_class.name
108
+ })
109
+ # If the slug was found, add it to the array for later use. If not, and
110
+ # the id_or_name is a number, assume that it is a regular record id.
111
+ slug ? slugs << slug : (ids << id_or_name if id_or_name.to_s =~ /\A\d*\z/)
112
+ end
113
+ return slugs, ids
114
+ end
115
+
116
+ end
@@ -0,0 +1,115 @@
1
+ # encoding: utf-8
2
+
3
+ module FriendlyId::SluggableInstanceMethods
4
+
5
+ NUM_CHARS_RESERVED_FOR_FRIENDLY_ID_EXTENSION = 2
6
+
7
+ attr :finder_slug
8
+ attr_accessor :finder_slug_name
9
+
10
+ def finder_slug
11
+ @finder_slug ||= init_finder_slug or nil
12
+ end
13
+
14
+ # Was the record found using one of its friendly ids?
15
+ def found_using_friendly_id?
16
+ finder_slug
17
+ end
18
+
19
+ # Was the record found using its numeric id?
20
+ def found_using_numeric_id?
21
+ !found_using_friendly_id?
22
+ end
23
+
24
+ # Was the record found using an old friendly id?
25
+ def found_using_outdated_friendly_id?
26
+ finder_slug.id != slug.id
27
+ end
28
+
29
+ # Was the record found using an old friendly id, or its numeric id?
30
+ def has_better_id?
31
+ slug and found_using_numeric_id? || found_using_outdated_friendly_id?
32
+ end
33
+
34
+ # Returns the friendly id.
35
+ def friendly_id
36
+ slug(true).to_friendly_id
37
+ end
38
+ alias best_id friendly_id
39
+
40
+ # Has the basis of our friendly id changed, requiring the generation of a
41
+ # new slug?
42
+ def new_slug_needed?
43
+ !slug || slug_text != slug.name
44
+ end
45
+
46
+ # Returns the most recent slug, which is used to determine the friendly
47
+ # id.
48
+ def slug(reload = false)
49
+ @most_recent_slug = nil if reload
50
+ @most_recent_slug ||= slugs.first
51
+ end
52
+
53
+ # Returns the friendly id, or if none is available, the numeric id.
54
+ def to_param
55
+ slug ? slug.to_friendly_id : id.to_s
56
+ end
57
+
58
+ # Get the processed string used as the basis of the friendly id.
59
+ def slug_text
60
+ base = send friendly_id_options[:column]
61
+ if self.slug_normalizer_block
62
+ base = self.slug_normalizer_block.call(base)
63
+ else
64
+ if self.friendly_id_options[:strip_diacritics]
65
+ base = Slug::strip_diacritics(base)
66
+ end
67
+ if self.friendly_id_options[:strip_non_ascii]
68
+ base = Slug::strip_non_ascii(base)
69
+ end
70
+ base = Slug::normalize(base)
71
+ end
72
+
73
+ if base.length > friendly_id_options[:max_length]
74
+ base = base[0...friendly_id_options[:max_length]]
75
+ end
76
+ if friendly_id_options[:reserved].include?(base)
77
+ raise FriendlyId::SlugGenerationError.new("The slug text is a reserved value")
78
+ end
79
+ return base
80
+ end
81
+
82
+ private
83
+
84
+ def finder_slug=(finder_slug)
85
+ @finder_slug_name = finder_slug.name
86
+ slug = finder_slug
87
+ slug.sluggable = self
88
+ slug
89
+ end
90
+
91
+ def init_finder_slug
92
+ return false if !@finder_slug_name
93
+ name, sequence = Slug.parse(@finder_slug_name)
94
+ slug = Slug.find(:first, :conditions => {:sluggable_id => id, :name => name, :sequence => sequence, :sluggable_type => self.class.base_class.name })
95
+ finder_slug = slug
96
+ end
97
+
98
+ # Set the slug using the generated friendly id.
99
+ def set_slug
100
+ if self.class.friendly_id_options[:use_slug] && new_slug_needed?
101
+ @most_recent_slug = nil
102
+ slug_attributes = {:name => slug_text}
103
+ if friendly_id_options[:scope]
104
+ scope = send(friendly_id_options[:scope])
105
+ slug_attributes[:scope] = scope.respond_to?(:to_param) ? scope.to_param : scope.to_s
106
+ end
107
+ # If we're renaming back to a previously used friendly_id, delete the
108
+ # slug so that we can recycle the name without having to use a sequence.
109
+ slugs.find(:all, :conditions => {:name => slug_text, :scope => scope}).each { |s| s.destroy }
110
+ slug = slugs.build slug_attributes
111
+ slug
112
+ end
113
+ end
114
+
115
+ end
@@ -0,0 +1,10 @@
1
+ # encoding: utf-8
2
+
3
+ module FriendlyId #:nodoc:
4
+ module Version #:nodoc:
5
+ MAJOR = 2
6
+ MINOR = 1
7
+ TINY = 3
8
+ STRING = [MAJOR, MINOR, TINY].join('.')
9
+ end
10
+ end
@@ -0,0 +1,50 @@
1
+ # encoding: utf-8
2
+
3
+ namespace :friendly_id do
4
+ desc "Make slugs for a model."
5
+ task :make_slugs => :environment do
6
+ raise 'USAGE: rake friendly_id:make_slugs MODEL=MyModelName' if ENV["MODEL"].nil?
7
+ if !sluggable_class.friendly_id_options[:use_slug]
8
+ raise "Class \"#{sluggable_class.to_s}\" doesn't appear to be using slugs"
9
+ end
10
+ while records = sluggable_class.find(:all, :include => :slugs, :conditions => "slugs.id IS NULL", :limit => 1000) do
11
+ break if records.size == 0
12
+ records.each do |r|
13
+ r.send(:set_slug)
14
+ r.save!
15
+ puts "#{sluggable_class.to_s}(#{r.id}) friendly_id set to \"#{r.slug.name}\""
16
+ end
17
+ end
18
+ end
19
+
20
+ desc "Regenereate slugs for a model."
21
+ task :redo_slugs => :environment do
22
+ raise 'USAGE: rake friendly_id:redo_slugs MODEL=MyModelName' if ENV["MODEL"].nil?
23
+ if !sluggable_class.friendly_id_options[:use_slug]
24
+ raise "Class \"#{sluggable_class.to_s}\" doesn't appear to be using slugs"
25
+ end
26
+ Slug.destroy_all(["sluggable_type = ?", sluggable_class.to_s])
27
+ Rake::Task["friendly_id:make_slugs"].invoke
28
+ end
29
+
30
+ desc "Kill obsolete slugs older than 45 days."
31
+ task :remove_old_slugs => :environment do
32
+ if ENV["DAYS"].nil?
33
+ @days = 45
34
+ else
35
+ @days = ENV["DAYS"].to_i
36
+ end
37
+ slugs = Slug.find(:all, :conditions => ["created_at < ?", DateTime.now - @days.days])
38
+ slugs.each do |s|
39
+ s.destroy if !s.is_most_recent?
40
+ end
41
+ end
42
+ end
43
+
44
+ def sluggable_class
45
+ if (ENV["MODEL"].split('::').size > 1)
46
+ ENV["MODEL"].split('::').inject(Kernel) {|scope, const_name| scope.const_get(const_name)}
47
+ else
48
+ Object.const_get(ENV["MODEL"])
49
+ end
50
+ end
@@ -0,0 +1 @@
1
+ load 'tasks/friendly_id.rake'
data/test/contest.rb ADDED
@@ -0,0 +1,94 @@
1
+ # License
2
+ # -------
3
+ #
4
+ # Contest is copyright (c) 2009 Damian Janowski and Michel Martens for
5
+ # Citrusbyte
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person
8
+ # obtaining a copy of this software and associated documentation
9
+ # files (the "Software"), to deal in the Software without
10
+ # restriction, including without limitation the rights to use,
11
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ # copies of the Software, and to permit persons to whom the
13
+ # Software is furnished to do so, subject to the following
14
+ # conditions:
15
+ #
16
+ # The above copyright notice and this permission notice shall be
17
+ # included in all copies or substantial portions of the Software.
18
+ #
19
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26
+ # OTHER DEALINGS IN THE SOFTWARE.
27
+ require "test/unit"
28
+
29
+ # Test::Unit loads a default test if the suite is empty, whose purpose is to
30
+ # fail. Since having empty contexts is a common practice, we decided to
31
+ # overwrite TestSuite#empty? in order to allow them. Having a failure when no
32
+ # tests have been defined seems counter-intuitive.
33
+ class Test::Unit::TestSuite
34
+ def empty?
35
+ false
36
+ end
37
+ end
38
+
39
+ # Contest adds +teardown+, +test+ and +context+ as class methods, and the
40
+ # instance methods +setup+ and +teardown+ now iterate on the corresponding
41
+ # blocks. Note that all setup and teardown blocks must be defined with the
42
+ # block syntax. Adding setup or teardown instance methods defeats the purpose
43
+ # of this library.
44
+ class Test::Unit::TestCase
45
+ def self.setup(&block)
46
+ define_method :setup do
47
+ super(&block)
48
+ instance_eval(&block)
49
+ end
50
+ end
51
+
52
+ def self.teardown(&block)
53
+ define_method :teardown do
54
+ instance_eval(&block)
55
+ super(&block)
56
+ end
57
+ end
58
+
59
+ def self.context(name, &block)
60
+ subclass = Class.new(self)
61
+ remove_tests(subclass)
62
+ subclass.class_eval(&block)
63
+ const_set(context_name(name), subclass)
64
+ end
65
+
66
+ def self.test(name, &block)
67
+ define_method(test_name(name), &block)
68
+ end
69
+
70
+ class << self
71
+ alias_method :should, :test
72
+ alias_method :describe, :context
73
+ end
74
+
75
+ private
76
+
77
+ def self.context_name(name)
78
+ "Test#{sanitize_name(name).gsub(/(^| )(\w)/) { $2.upcase }}".to_sym
79
+ end
80
+
81
+ def self.test_name(name)
82
+ "test_#{sanitize_name(name).gsub(/\s+/,'_')}".to_sym
83
+ end
84
+
85
+ def self.sanitize_name(name)
86
+ name.gsub(/\W+/, ' ').strip
87
+ end
88
+
89
+ def self.remove_tests(subclass)
90
+ subclass.public_instance_methods.grep(/^test_/).each do |meth|
91
+ subclass.send(:undef_method, meth.to_sym)
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,35 @@
1
+ # encoding: utf-8
2
+
3
+ require File.dirname(__FILE__) + '/test_helper'
4
+
5
+ class CustomSlugNormalizerTest < Test::Unit::TestCase
6
+
7
+ context "A slugged model using a custom slug generator" do
8
+
9
+ setup do
10
+ Thing.friendly_id_options = FriendlyId::DEFAULT_FRIENDLY_ID_OPTIONS.merge(:column => :name, :use_slug => true)
11
+ Thing.delete_all
12
+ Slug.delete_all
13
+ end
14
+
15
+ should "invoke the block code" do
16
+ @thing = Thing.create!(:name => "test")
17
+ assert_equal "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", @thing.friendly_id
18
+ end
19
+
20
+ should "respect the max_length option" do
21
+ Thing.friendly_id_options = Thing.friendly_id_options.merge(:max_length => 10)
22
+ @thing = Thing.create!(:name => "test")
23
+ assert_equal "a94a8fe5cc", @thing.friendly_id
24
+ end
25
+
26
+ should "respect the reserved option" do
27
+ Thing.friendly_id_options = Thing.friendly_id_options.merge(:reserved => ["a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"])
28
+ assert_raises FriendlyId::SlugGenerationError do
29
+ Thing.create!(:name => "test")
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+ end
@@ -0,0 +1,2 @@
1
+ class Book < ActiveRecord::Base
2
+ end
@@ -0,0 +1,4 @@
1
+ class Country < ActiveRecord::Base
2
+ has_many :people
3
+ has_friendly_id :name, :use_slug => true
4
+ end
@@ -0,0 +1,3 @@
1
+ class Event < ActiveRecord::Base
2
+ has_friendly_id :event_date, :use_slug => true
3
+ end