alancse-friendly_id 2.0.1

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 (45) hide show
  1. data/History.txt +82 -0
  2. data/MIT-LICENSE +19 -0
  3. data/Manifest.txt +44 -0
  4. data/README.rdoc +318 -0
  5. data/Rakefile +45 -0
  6. data/friendly_id.gemspec +38 -0
  7. data/generators/friendly_id/friendly_id_generator.rb +12 -0
  8. data/generators/friendly_id/templates/create_slugs.rb +18 -0
  9. data/generators/friendly_id_20_upgrade/friendly_id_20_upgrade_generator.rb +11 -0
  10. data/generators/friendly_id_20_upgrade/templates/upgrade_friendly_id_to_20.rb +19 -0
  11. data/init.rb +1 -0
  12. data/lib/friendly_id.rb +68 -0
  13. data/lib/friendly_id/helpers.rb +13 -0
  14. data/lib/friendly_id/non_sluggable_class_methods.rb +40 -0
  15. data/lib/friendly_id/non_sluggable_instance_methods.rb +33 -0
  16. data/lib/friendly_id/shoulda_macros.rb +36 -0
  17. data/lib/friendly_id/slug.rb +89 -0
  18. data/lib/friendly_id/sluggable_class_methods.rb +103 -0
  19. data/lib/friendly_id/sluggable_instance_methods.rb +105 -0
  20. data/lib/friendly_id/version.rb +8 -0
  21. data/lib/tasks/friendly_id.rake +48 -0
  22. data/lib/tasks/friendly_id.rb +1 -0
  23. data/test/database.yml +3 -0
  24. data/test/fixtures/countries.yml +4 -0
  25. data/test/fixtures/country.rb +4 -0
  26. data/test/fixtures/people.yml +7 -0
  27. data/test/fixtures/person.rb +6 -0
  28. data/test/fixtures/post.rb +3 -0
  29. data/test/fixtures/posts.yml +23 -0
  30. data/test/fixtures/slugs.yml +53 -0
  31. data/test/fixtures/user.rb +3 -0
  32. data/test/fixtures/users.yml +7 -0
  33. data/test/non_slugged_test.rb +85 -0
  34. data/test/rails/2.x/app/controllers/application.rb +0 -0
  35. data/test/rails/2.x/config/boot.rb +109 -0
  36. data/test/rails/2.x/config/database.yml +3 -0
  37. data/test/rails/2.x/config/environment.rb +7 -0
  38. data/test/rails/2.x/config/environments/test.rb +6 -0
  39. data/test/rails/2.x/config/routes.rb +0 -0
  40. data/test/schema.rb +38 -0
  41. data/test/scoped_model_test.rb +21 -0
  42. data/test/slug_test.rb +87 -0
  43. data/test/sluggable_test.rb +185 -0
  44. data/test/test_helper.rb +35 -0
  45. metadata +127 -0
@@ -0,0 +1,103 @@
1
+ module FriendlyId::SluggableClassMethods
2
+
3
+ include FriendlyId::Helpers
4
+
5
+ def self.extended(base) #:nodoc:#
6
+
7
+ class << base
8
+ alias_method_chain :find_one, :friendly
9
+ alias_method_chain :find_some, :friendly
10
+ alias_method_chain :validate_find_options, :friendly
11
+ end
12
+
13
+ end
14
+
15
+ # Finds a single record using the friendly id, or the record's id.
16
+ def find_one_with_friendly(id_or_name, options) #:nodoc:#
17
+
18
+ scope = options.delete(:scope)
19
+ return find_one_without_friendly(id_or_name, options) if id_or_name.is_a?(Fixnum)
20
+
21
+ find_options = {:select => "#{self.table_name}.*"}
22
+ find_options[:joins] = :slugs unless options[:include] && [*options[:include]].flatten.include?(:slugs)
23
+
24
+ name, sequence = Slug.parse(id_or_name)
25
+
26
+ find_options[:conditions] = {
27
+ "#{Slug.table_name}.name" => name,
28
+ "#{Slug.table_name}.scope" => scope,
29
+ "#{Slug.table_name}.sequence" => sequence
30
+ }
31
+
32
+ result = with_scope(:find => find_options) { find_initial(options) }
33
+
34
+ if result
35
+ result.finder_slug_name = id_or_name
36
+ else
37
+ result = find_one_without_friendly id_or_name, options
38
+ end
39
+
40
+ result
41
+
42
+ end
43
+
44
+ # Finds multiple records using the friendly ids, or the records' ids.
45
+ def find_some_with_friendly(ids_and_names, options) #:nodoc:#
46
+
47
+ slugs, ids = get_slugs_and_ids(ids_and_names, options)
48
+ results = []
49
+
50
+ find_options = {:select => "#{self.table_name}.*"}
51
+ find_options[:joins] = :slugs unless options[:include] && [*options[:include]].flatten.include?(:slugs)
52
+ find_options[:conditions] = "#{quoted_table_name}.#{primary_key} IN (#{ids.empty? ? 'NULL' : ids.join(',')}) "
53
+ find_options[:conditions] << "OR slugs.id IN (#{slugs.to_s(:db)})"
54
+
55
+ results = with_scope(:find => find_options) { find_every(options) }
56
+
57
+ expected = expected_size(ids_and_names, options)
58
+ if results.size != expected
59
+ 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 })"
60
+ end
61
+
62
+ assign_finder_slugs(slugs, results)
63
+
64
+ results
65
+ end
66
+
67
+ def validate_find_options_with_friendly(options) #:nodoc:#
68
+ options.assert_valid_keys([:conditions, :include, :joins, :limit, :offset,
69
+ :order, :select, :readonly, :group, :from, :lock, :having, :scope])
70
+ end
71
+
72
+ private
73
+
74
+ # Assign finder slugs for the results found in find_some_with_friendly
75
+ def assign_finder_slugs(slugs, results) #:nodoc:#
76
+ slugs.each do |slug|
77
+ results.select { |r| r.id == slug.sluggable_id }.each do |result|
78
+ result.send(:finder_slug=, slug)
79
+ end
80
+ end
81
+ end
82
+
83
+ # Build arrays of slugs and ids, for the find_some_with_friendly method.
84
+ def get_slugs_and_ids(ids_and_names, options) #:nodoc:#
85
+ scope = options.delete(:scope)
86
+ slugs = []
87
+ ids = []
88
+ ids_and_names.each do |id_or_name|
89
+ name, sequence = Slug.parse id_or_name
90
+ slug = Slug.find(:first, :readonly => true, :conditions => {
91
+ :name => name,
92
+ :scope => scope,
93
+ :sequence => sequence,
94
+ :sluggable_type => base_class.name
95
+ })
96
+ # If the slug was found, add it to the array for later use. If not, and
97
+ # the id_or_name is a number, assume that it is a regular record id.
98
+ slug ? slugs << slug : (ids << id_or_name if id_or_name =~ /\A\d*\z/)
99
+ end
100
+ return slugs, ids
101
+ end
102
+
103
+ end
@@ -0,0 +1,105 @@
1
+ module FriendlyId::SluggableInstanceMethods
2
+
3
+ NUM_CHARS_RESERVED_FOR_FRIENDLY_ID_EXTENSION = 2
4
+
5
+ attr :finder_slug
6
+ attr_accessor :finder_slug_name
7
+
8
+ def finder_slug
9
+ @finder_slug ||= init_finder_slug
10
+ end
11
+
12
+ # Was the record found using one of its friendly ids?
13
+ def found_using_friendly_id?
14
+ finder_slug
15
+ end
16
+
17
+ # Was the record found using its numeric id?
18
+ def found_using_numeric_id?
19
+ !found_using_friendly_id?
20
+ end
21
+
22
+ # Was the record found using an old friendly id?
23
+ def found_using_outdated_friendly_id?
24
+ finder_slug.id != slug.id
25
+ end
26
+
27
+ # Was the record found using an old friendly id, or its numeric id?
28
+ def has_better_id?
29
+ slug and found_using_numeric_id? || found_using_outdated_friendly_id?
30
+ end
31
+
32
+ # Returns the friendly id.
33
+ def friendly_id
34
+ slug(true).to_friendly_id
35
+ end
36
+ alias best_id friendly_id
37
+
38
+ # Has the basis of our friendly id changed, requiring the generation of a
39
+ # new slug?
40
+ def new_slug_needed?
41
+ !slug || slug_text != slug.name
42
+ end
43
+
44
+ # Returns the most recent slug, which is used to determine the friendly
45
+ # id.
46
+ def slug(reload = false)
47
+ @most_recent_slug = nil if reload
48
+ @most_recent_slug ||= slugs.first
49
+ end
50
+
51
+ # Returns the friendly id, or if none is available, the numeric id.
52
+ def to_param
53
+ slug ? slug.to_friendly_id : id.to_s
54
+ end
55
+
56
+ # Get the processed string used as the basis of the friendly id.
57
+ def slug_text
58
+ base = send friendly_id_options[:column]
59
+ if self.friendly_id_options[:strip_diacritics]
60
+ base = Slug::normalize(Slug::strip_diacritics(base))
61
+ else
62
+ base = Slug::normalize(base)
63
+ end
64
+ if base.length > friendly_id_options[:max_length]
65
+ base = base[0...friendly_id_options[:max_length]]
66
+ end
67
+ if friendly_id_options[:reserved].include?(base)
68
+ raise FriendlyId::SlugGenerationError.new("The slug text is a reserved value")
69
+ end
70
+ return base
71
+ end
72
+
73
+ private
74
+
75
+ def finder_slug=(finder_slug)
76
+ @finder_slug_name = finder_slug.name
77
+ slug = finder_slug
78
+ slug.sluggable = self
79
+ slug
80
+ end
81
+
82
+ def init_finder_slug
83
+ return false if !@finder_slug_name
84
+ slug = Slug.find(:first, :conditions => {:sluggable_id => id, :name => @finder_slug_name, :sluggable_type => self.class.name })
85
+ finder_slug = slug
86
+ end
87
+
88
+ # Set the slug using the generated friendly id.
89
+ def set_slug
90
+ if self.class.friendly_id_options[:use_slug] && new_slug_needed?
91
+ @most_recent_slug = nil
92
+ slug_attributes = {:name => slug_text}
93
+ if friendly_id_options[:scope]
94
+ scope = send(friendly_id_options[:scope])
95
+ slug_attributes[:scope] = scope.respond_to?(:to_param) ? scope.to_param.to_s : scope.to_s
96
+ end
97
+ # If we're renaming back to a previously used friendly_id, delete the
98
+ # slug so that we can recycle the name without having to use a sequence.
99
+ slugs.find(:all, :conditions => {:name => slug_text, :scope => slug_attributes[:scope]}).each { |s| s.destroy }
100
+ slug = slugs.build slug_attributes
101
+ slug
102
+ end
103
+ end
104
+
105
+ end
@@ -0,0 +1,8 @@
1
+ module FriendlyId #:nodoc:
2
+ module Version #:nodoc:
3
+ MAJOR = 2
4
+ MINOR = 0
5
+ TINY = 0
6
+ STRING = [MAJOR, MINOR, TINY].join('.')
7
+ end
8
+ end
@@ -0,0 +1,48 @@
1
+ namespace :friendly_id do
2
+ desc "Make slugs for a model."
3
+ task :make_slugs => :environment do
4
+ raise 'USAGE: rake friendly_id:make_slugs MODEL=MyModelName' if ENV["MODEL"].nil?
5
+ if !sluggable_class.friendly_id_options[:use_slug]
6
+ raise "Class \"#{sluggable_class.to_s}\" doesn't appear to be using slugs"
7
+ end
8
+ while records = sluggable_class.find(:all, :include => :slugs, :conditions => "slugs.id IS NULL", :limit => 1000) do
9
+ break if records.size == 0
10
+ records.each do |r|
11
+ r.send(:set_slug)
12
+ r.save!
13
+ puts "#{sluggable_class.to_s}(#{r.id}) friendly_id set to \"#{r.slug.name}\""
14
+ end
15
+ end
16
+ end
17
+
18
+ desc "Regenereate slugs for a model."
19
+ task :redo_slugs => :environment do
20
+ raise 'USAGE: rake friendly_id:redo_slugs MODEL=MyModelName' if ENV["MODEL"].nil?
21
+ if !sluggable_class.friendly_id_options[:use_slug]
22
+ raise "Class \"#{sluggable_class.to_s}\" doesn't appear to be using slugs"
23
+ end
24
+ Slug.destroy_all(["sluggable_type = ?", sluggable_class.to_s])
25
+ Rake::Task["friendly_id:make_slugs"].invoke
26
+ end
27
+
28
+ desc "Kill obsolete slugs older than 45 days."
29
+ task :remove_old_slugs => :environment do
30
+ if ENV["DAYS"].nil?
31
+ @days = 45
32
+ else
33
+ @days = ENV["DAYS"].to_i
34
+ end
35
+ slugs = Slug.find(:all, :conditions => ["created_at < ?", DateTime.now - @days.days])
36
+ slugs.each do |s|
37
+ s.destroy if !s.is_most_recent?
38
+ end
39
+ end
40
+ end
41
+
42
+ def sluggable_class
43
+ if (ENV["MODEL"].split('::').size > 1)
44
+ ENV["MODEL"].split('::').inject(Kernel) {|scope, const_name| scope.const_get(const_name)}
45
+ else
46
+ Object.const_get(ENV["MODEL"])
47
+ end
48
+ end
@@ -0,0 +1 @@
1
+ load 'tasks/friendly_id.rake'
data/test/database.yml ADDED
@@ -0,0 +1,3 @@
1
+ sqlite3:
2
+ adapter: sqlite3
3
+ database: ":memory:"
@@ -0,0 +1,4 @@
1
+ argentina:
2
+ name: Argentina
3
+ usa:
4
+ name: USA
@@ -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,7 @@
1
+ john_smith:
2
+ name: John Smith
3
+ country: argentina
4
+
5
+ john_smith2:
6
+ name: John Smith
7
+ country: usa
@@ -0,0 +1,6 @@
1
+ class Person < ActiveRecord::Base
2
+
3
+ belongs_to :country
4
+ has_friendly_id :name, :use_slug => true, :scope => :country
5
+
6
+ end
@@ -0,0 +1,3 @@
1
+ class Post < ActiveRecord::Base
2
+ has_friendly_id :name, :use_slug => true, :reserved => ['new', 'edit']
3
+ end
@@ -0,0 +1,23 @@
1
+ without_slug:
2
+ name: Without a slug
3
+ content: Content without a slug
4
+
5
+ with_one_slug:
6
+ name: With one slug
7
+ content: Content with one slug
8
+
9
+ with_two_slugs:
10
+ name: With two slugs
11
+ content: Content with two slugs
12
+
13
+ common_title:
14
+ name: Common Title
15
+ content: A post with a very common title
16
+
17
+ common_title2:
18
+ name: Common Title
19
+ content: A second post with a very common title
20
+
21
+ john_smith:
22
+ name: John Smith
23
+ content: Should allow for identical slug names between sluggable types
@@ -0,0 +1,53 @@
1
+ post_with_same_friendly_id_as_person:
2
+ name: john-smith
3
+ sluggable: john_smith (Post)
4
+
5
+ person_with_same_friendly_id_as_post:
6
+ name: john-smith
7
+ sluggable: john_smith (Person)
8
+
9
+ one:
10
+ name: with-one-slug
11
+ sluggable: with_one_slug
12
+ sluggable_type: Post
13
+ sequence: 1
14
+
15
+ two_old:
16
+ name: with-two-slugs
17
+ sluggable: with_two_slugs (Post)
18
+ sequence: 1
19
+
20
+ two_new:
21
+ name: with-two-slugs-new
22
+ sluggable: with_two_slugs (Post)
23
+ sequence: 1
24
+
25
+ common_title:
26
+ name: common-title
27
+ sluggable: common_title (Post)
28
+ sequence: 1
29
+
30
+ common_title2:
31
+ name: common-title
32
+ sluggable: common_title2 (Post)
33
+ sequence: 2
34
+
35
+ john_smith:
36
+ name: john-smith
37
+ sluggable: john_smith (Person)
38
+ sequence: 1
39
+ scope: argentina
40
+
41
+ john_smith2:
42
+ name: john-smith
43
+ sluggable: john_smith2 (Person)
44
+ sequence: 1
45
+ scope: usa
46
+
47
+ argentina:
48
+ name: argentina
49
+ sluggable: argentina (Country)
50
+
51
+ usa:
52
+ name: usa
53
+ sluggable: usa (Country)
@@ -0,0 +1,3 @@
1
+ class User < ActiveRecord::Base
2
+ has_friendly_id :login
3
+ end
@@ -0,0 +1,7 @@
1
+ joe:
2
+ login: joe
3
+ email: joe@example.org
4
+
5
+ jane:
6
+ login: jane
7
+ email: jane@example.org
@@ -0,0 +1,85 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class NonSluggedTest < Test::Unit::TestCase
4
+
5
+ fixtures :users
6
+
7
+ def setup
8
+ end
9
+
10
+ def test_should_find_user_using_friendly_id
11
+ assert User.find(users(:joe).friendly_id)
12
+ end
13
+
14
+ def test_should_find_users_using_friendly_id
15
+ assert User.find([users(:joe).friendly_id])
16
+ end
17
+
18
+ def test_to_param_should_return_a_string
19
+ assert_equal String, users(:joe).to_param.class
20
+ end
21
+
22
+ def test_should_not_find_users_using_non_existent_friendly_ids
23
+ assert_raises ActiveRecord::RecordNotFound do
24
+ User.find(['bad', 'bad2'])
25
+ end
26
+ end
27
+
28
+ def test_finding_by_array_with_friendly_and_non_friendly_id_for_same_record_raises_error
29
+ assert_raises ActiveRecord::RecordNotFound do
30
+ User.find([users(:joe).id, "joe"]).size
31
+ end
32
+ end
33
+
34
+ def test_finding_with_mixed_array_should_indicate_whether_found_by_numeric_or_friendly
35
+ @users = User.find([users(:jane).id, "joe"], :order => "login ASC")
36
+ assert @users[0].found_using_numeric_id?
37
+ assert @users[1].found_using_friendly_id?
38
+ end
39
+
40
+ def test_finder_options_are_not_ignored
41
+ assert_raises ActiveRecord::RecordNotFound do
42
+ User.find(users(:joe).friendly_id, :conditions => "1 = 2")
43
+ end
44
+ assert_raises ActiveRecord::RecordNotFound do
45
+ User.find([users(:joe).friendly_id], :conditions => "1 = 2")
46
+ end
47
+ end
48
+
49
+ def test_user_should_have_friendly_id_options
50
+ assert_not_nil User.friendly_id_options
51
+ end
52
+
53
+ def test_user_should_not_be_found_using_friendly_id_unless_it_really_was
54
+ assert !User.find(users(:joe).id).found_using_friendly_id?
55
+ end
56
+
57
+ def test_users_should_not_be_found_using_friendly_id_unless_they_really_were
58
+ @users = User.find([users(:jane).id])
59
+ assert @users[0].found_using_numeric_id?
60
+ end
61
+
62
+ def test_user_should_be_considered_found_by_numeric_id_as_default
63
+ @user = User.new
64
+ assert @user.found_using_numeric_id?
65
+ end
66
+
67
+ def test_user_should_indicate_if_it_was_found_using_numeric_id
68
+ @user = User.find(users(:joe).id)
69
+ assert @user.found_using_numeric_id?
70
+ assert !@user.found_using_friendly_id?
71
+ end
72
+
73
+ def test_user_should_indicate_if_it_was_found_using_friendly_id
74
+ @user = User.find(users(:joe).friendly_id)
75
+ assert !@user.found_using_numeric_id?
76
+ assert @user.found_using_friendly_id?
77
+ end
78
+
79
+ def test_should_indicate_there_is_a_better_id_if_found_by_numeric_id
80
+ @user = User.find(users(:joe).id)
81
+ assert @user.found_using_numeric_id?
82
+ assert @user.has_better_id?
83
+ end
84
+
85
+ end