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
data/Rakefile ADDED
@@ -0,0 +1,45 @@
1
+ require 'newgem'
2
+ require 'lib/friendly_id/version'
3
+
4
+ Hoe.new("friendly_id", FriendlyId::Version::STRING) do |p|
5
+ p.rubyforge_name = "friendly-id"
6
+ p.author = ['Norman Clarke', 'Adrian Mugnolo', 'Emilio Tagua']
7
+ p.email = ['norman@randomba.org', 'adrian@randomba.org', 'miloops@gmail.com']
8
+ p.summary = "A comprehensive slugging and pretty-URL plugin for Ruby on Rails."
9
+ p.description = 'A comprehensive slugging and pretty-URL plugin for Ruby on Rails.'
10
+ p.url = 'http://randomba.org'
11
+ p.need_tar = true
12
+ p.need_zip = true
13
+ p.test_globs = ['test/**/*_test.rb']
14
+ p.extra_deps << ['unicode', '>= 0.1']
15
+ p.extra_dev_deps = [
16
+ ['newgem', ">= #{::Newgem::VERSION}"]
17
+ ]
18
+ p.rdoc_pattern = /^(lib|bin|ext)|txt|rdoc$/
19
+ changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
20
+ p.remote_rdoc_dir = ""
21
+ end
22
+
23
+ desc "Run RCov"
24
+ task :rcov do
25
+ run_coverage Dir["test/**/*_test.rb"]
26
+ end
27
+
28
+ def run_coverage(files)
29
+ rm_f "coverage"
30
+ rm_f "coverage.data"
31
+ if files.length == 0
32
+ puts "No files were specified for testing"
33
+ return
34
+ end
35
+ files = files.join(" ")
36
+ if PLATFORM =~ /darwin/
37
+ exclude = '--exclude "gems/"'
38
+ else
39
+ exclude = '--exclude "rubygems"'
40
+ end
41
+ rcov = "rcov -Ilib:test --sort coverage --text-report #{exclude} --no-validator-links"
42
+ cmd = "#{rcov} #{files}"
43
+ puts cmd
44
+ sh cmd
45
+ end
@@ -0,0 +1,38 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = %q{alancse-friendly_id}
3
+ s.version = "2.0.1"
4
+
5
+ #s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
6
+ s.authors = ["Norman Clarke", "Adrian Mugnolo", "Emilio Tagua"]
7
+ #s.cert_chain = ["/Users/norman/.gem/gem-public_cert.pem"]
8
+ s.date = %q{2009-01-10}
9
+ s.description = %q{A comprehensive slugging and pretty-URL plugin for Ruby on Rails.}
10
+ s.email = ["norman@randomba.org", "adrian@randomba.org", "miloops@gmail.com"]
11
+ s.extra_rdoc_files = ["History.txt", "Manifest.txt"]
12
+ s.files = ["History.txt", "MIT-LICENSE", "Manifest.txt", "README.rdoc", "Rakefile", "friendly_id.gemspec", "generators/friendly_id/friendly_id_generator.rb", "generators/friendly_id/templates/create_slugs.rb", "generators/friendly_id_20_upgrade/friendly_id_20_upgrade_generator.rb", "generators/friendly_id_20_upgrade/templates/upgrade_friendly_id_to_20.rb", "init.rb", "lib/friendly_id.rb", "lib/friendly_id/helpers.rb", "lib/friendly_id/non_sluggable_class_methods.rb", "lib/friendly_id/non_sluggable_instance_methods.rb", "lib/friendly_id/shoulda_macros.rb", "lib/friendly_id/slug.rb", "lib/friendly_id/sluggable_class_methods.rb", "lib/friendly_id/sluggable_instance_methods.rb", "lib/friendly_id/version.rb", "lib/tasks/friendly_id.rake", "lib/tasks/friendly_id.rb", "test/database.yml", "test/fixtures/countries.yml", "test/fixtures/country.rb", "test/fixtures/people.yml", "test/fixtures/person.rb", "test/fixtures/post.rb", "test/fixtures/posts.yml", "test/fixtures/slugs.yml", "test/fixtures/user.rb", "test/fixtures/users.yml", "test/non_slugged_test.rb", "test/rails/2.x/app/controllers/application.rb", "test/rails/2.x/config/boot.rb", "test/rails/2.x/config/database.yml", "test/rails/2.x/config/environment.rb", "test/rails/2.x/config/environments/test.rb", "test/rails/2.x/config/routes.rb", "test/schema.rb", "test/scoped_model_test.rb", "test/slug_test.rb", "test/sluggable_test.rb", "test/test_helper.rb"]
13
+ s.has_rdoc = true
14
+ s.homepage = %q{http://randomba.org}
15
+ s.rdoc_options = ["--main", "README.txt"]
16
+ s.require_paths = ["lib"]
17
+ s.rubyforge_project = %q{friendly-id}
18
+ s.rubygems_version = %q{1.3.1}
19
+ #s.signing_key = %q{/Users/norman/.gem/gem-private_key.pem}
20
+ s.summary = %q{A comprehensive slugging and pretty-URL plugin for Ruby on Rails.}
21
+ s.test_files = ["test/non_slugged_test.rb", "test/scoped_model_test.rb", "test/slug_test.rb", "test/sluggable_test.rb"]
22
+
23
+ if s.respond_to? :specification_version then
24
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
25
+ s.specification_version = 2
26
+
27
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
28
+ s.add_runtime_dependency(%q<unicode>, [">= 0.1"])
29
+ s.add_development_dependency(%q<hoe>, [">= 1.8.2"])
30
+ else
31
+ s.add_dependency(%q<unicode>, [">= 0.1"])
32
+ s.add_dependency(%q<hoe>, [">= 1.8.2"])
33
+ end
34
+ else
35
+ s.add_dependency(%q<unicode>, [">= 0.1"])
36
+ s.add_dependency(%q<hoe>, [">= 1.8.2"])
37
+ end
38
+ end
@@ -0,0 +1,12 @@
1
+ class FriendlyIdGenerator < Rails::Generator::Base
2
+ def manifest
3
+ record do |m|
4
+ unless options[:skip_migration]
5
+ m.migration_template(
6
+ 'create_slugs.rb', 'db/migrate', :migration_file_name => 'create_slugs'
7
+ )
8
+ m.file "/../../../lib/tasks/friendly_id.rake", "lib/tasks/friendly_id.rake"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ class CreateSlugs < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :slugs do |t|
4
+ t.string :name
5
+ t.integer :sluggable_id
6
+ t.integer :sequence, :null => false, :default => 1
7
+ t.string :sluggable_type, :limit => 40
8
+ t.string :scope, :limit => 40
9
+ t.datetime :created_at
10
+ end
11
+ add_index :slugs, [:name, :sluggable_type, :scope, :sequence], :unique => true
12
+ add_index :slugs, :sluggable_id
13
+ end
14
+
15
+ def self.down
16
+ drop_table :slugs
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ class FriendlyId20UpgradeGenerator < Rails::Generator::Base
2
+ def manifest
3
+ record do |m|
4
+ unless options[:skip_migration]
5
+ m.migration_template(
6
+ 'upgrade_friendly_id_to_20.rb', 'db/migrate', :migration_file_name => 'upgrade_friendly_id_to_20'
7
+ )
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ class UpgradeFriendlyIdTo20 < ActiveRecord::Migration
2
+
3
+ def self.up
4
+ remove_column :slugs, :updated_at
5
+ remove_index :slugs, :column => [:name, :sluggable_type]
6
+ add_column :slugs, :sequence, :integer, :null => false, :default => 1
7
+ add_column :slugs, :scope, :string, :limit => 40
8
+ add_index :slugs, [:name, :sluggable_type, :scope, :sequence], :unique => true, :name => "index_slugs_on_n_s_s_and_s"
9
+ end
10
+
11
+ def self.down
12
+ remove_index :slugs, :name => "index_slugs_on_n_s_s_and_s"
13
+ remove_column :slugs, :scope
14
+ remove_column :slugs, :sequence
15
+ add_column :slugs, :updated_at, :datetime
16
+ add_index :slugs, [:name, :sluggable_type], :unique => true
17
+ end
18
+
19
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'friendly_id'
@@ -0,0 +1,68 @@
1
+ require 'unicode'
2
+ require 'friendly_id/helpers'
3
+ require 'friendly_id/slug'
4
+ require 'friendly_id/shoulda_macros'
5
+
6
+
7
+ # FriendlyId is a comprehensize Rails plugin/gem for slugging and permalinks.
8
+ module FriendlyId
9
+
10
+ # This error is raised when it's not possible to generate a unique slug.
11
+ class SlugGenerationError < StandardError ; end
12
+
13
+ module ClassMethods
14
+
15
+ # Default options for friendly_id.
16
+ DEFAULT_FRIENDLY_ID_OPTIONS = {:method => nil, :use_slug => false, :max_length => 255, :reserved => [], :strip_diacritics => false, :scope => nil}.freeze
17
+ VALID_FRIENDLY_ID_KEYS = [:use_slug, :max_length, :reserved, :strip_diacritics, :scope].freeze
18
+
19
+ # Set up an ActiveRecord model to use a friendly_id.
20
+ #
21
+ # The column argument can be one of your model's columns, or a method
22
+ # you use to generate the slug.
23
+ #
24
+ # Options:
25
+ # * <tt>:use_slug</tt> - Defaults to false. Use slugs when you want to use a non-unique text field for friendly ids.
26
+ # * <tt>:max_length</tt> - Defaults to 255. The maximum allowed length for a slug.
27
+ # * <tt>:strip_diacritics</tt> - Defaults to false. If true, it will remove accents, umlauts, etc. from western characters.
28
+ # * <tt>:reseved</tt> - Array of words that are reserved and can't be used as slugs. If such a word is used, it will be treated the same as if that slug was already taken (numeric extension will be appended). Defaults to [].
29
+ def has_friendly_id(column, options = {})
30
+ options.assert_valid_keys VALID_FRIENDLY_ID_KEYS
31
+ options = DEFAULT_FRIENDLY_ID_OPTIONS.merge(options).merge(:column => column)
32
+ write_inheritable_attribute :friendly_id_options, options
33
+ class_inheritable_reader :friendly_id_options
34
+
35
+ if options[:use_slug]
36
+ has_many :slugs, :order => 'id DESC', :as => :sluggable, :dependent => :destroy, :readonly => true
37
+ require 'friendly_id/sluggable_class_methods'
38
+ require 'friendly_id/sluggable_instance_methods'
39
+ extend SluggableClassMethods
40
+ include SluggableInstanceMethods
41
+ before_save :set_slug
42
+ else
43
+ require 'friendly_id/non_sluggable_class_methods'
44
+ require 'friendly_id/non_sluggable_instance_methods'
45
+ extend NonSluggableClassMethods
46
+ include NonSluggableInstanceMethods
47
+ end
48
+ end
49
+
50
+ end
51
+
52
+ class << self
53
+
54
+ # Load FriendlyId if the gem is included in a Rails app.
55
+ def enable
56
+ return if ActiveRecord::Base.methods.include? 'has_friendly_id'
57
+ ActiveRecord::Base.class_eval { extend FriendlyId::ClassMethods }
58
+ Test::Unit::TestCase.class_eval { include FriendlyId::ShouldaMacros }
59
+ end
60
+
61
+ end
62
+
63
+
64
+ end
65
+
66
+ if defined?(ActiveRecord)
67
+ FriendlyId::enable
68
+ end
@@ -0,0 +1,13 @@
1
+ module FriendlyId
2
+
3
+ module Helpers
4
+ # Calculate expected result size for find_some_with_friendly (taken from
5
+ # active_record/base.rb)
6
+ def expected_size(ids_and_names, options) #:nodoc:#
7
+ size = options[:offset] ? ids_and_names.size - options[:offset] : ids_and_names.size
8
+ size = options[:limit] if options[:limit] && size > options[:limit]
9
+ size
10
+ end
11
+ end
12
+
13
+ end
@@ -0,0 +1,40 @@
1
+ module FriendlyId::NonSluggableClassMethods
2
+
3
+ include FriendlyId::Helpers
4
+
5
+ def self.extended(base) #:nodoc:#
6
+ class << base
7
+ alias_method_chain :find_one, :friendly
8
+ alias_method_chain :find_some, :friendly
9
+ end
10
+ end
11
+
12
+ protected
13
+
14
+ def find_one_with_friendly(id, options) #:nodoc:#
15
+ if id.is_a?(String) && result = send("find_by_#{ friendly_id_options[:column] }", id, options)
16
+ result.send(:found_using_friendly_id=, true)
17
+ else
18
+ result = find_one_without_friendly id, options
19
+ end
20
+ result
21
+ end
22
+
23
+ def find_some_with_friendly(ids_and_names, options) #:nodoc:#
24
+
25
+ results = with_scope :find => options do
26
+ find :all, :conditions => ["#{ quoted_table_name }.#{ primary_key } IN (?) OR #{friendly_id_options[:column].to_s} IN (?)",
27
+ ids_and_names, ids_and_names]
28
+ end
29
+
30
+ expected = expected_size(ids_and_names, options)
31
+ if results.size != expected
32
+ 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 })"
33
+ end
34
+
35
+ results.each {|r| r.send(:found_using_friendly_id=, true) if ids_and_names.include?(r.friendly_id)}
36
+
37
+ results
38
+
39
+ end
40
+ end
@@ -0,0 +1,33 @@
1
+ module FriendlyId::NonSluggableInstanceMethods
2
+
3
+ attr :found_using_friendly_id
4
+
5
+ # Was the record found using one of its friendly ids?
6
+ def found_using_friendly_id?
7
+ @found_using_friendly_id
8
+ end
9
+
10
+ # Was the record found using its numeric id?
11
+ def found_using_numeric_id?
12
+ !@found_using_friendly_id
13
+ end
14
+ alias has_better_id? found_using_numeric_id?
15
+
16
+ # Returns the friendly_id.
17
+ def friendly_id
18
+ send friendly_id_options[:column]
19
+ end
20
+ alias best_id friendly_id
21
+
22
+ # Returns the friendly id, or if none is available, the numeric id.
23
+ def to_param
24
+ friendly_id.to_s || id.to_s
25
+ end
26
+
27
+ private
28
+
29
+ def found_using_friendly_id=(value) #:nodoc#
30
+ @found_using_friendly_id = value
31
+ end
32
+
33
+ end
@@ -0,0 +1,36 @@
1
+ module FriendlyId
2
+
3
+ # A Shoulda[http://www.thoughtbot.com/projects/shoulda/] macros for testing
4
+ # models using FriendlyId.
5
+ module ShouldaMacros
6
+
7
+ # Ensure that a model is using FriendlyId.
8
+ def self.should_have_friendly_id(column, options = {})
9
+
10
+ options.assert_valid_keys(:use_slug)
11
+ klass = self.model_class
12
+
13
+ should "have friendly id for #{method}" do
14
+ assert_respond_to klass, :friendly_id_options,
15
+ "#{klass} does not respond to friendly_id_options"
16
+ assert_equal column, klass.friendly_id_options[:method]
17
+ end
18
+
19
+ if options[:use_slug]
20
+ should "include/extend friendly_id's sluggable modules" do
21
+ assert klass.extended_by.include?(FriendlyId::SluggableClassMethods),
22
+ "#{klass} does not extend FriendlyId::SluggableClassMethods"
23
+ assert klass.include?(FriendlyId::SluggableInstanceMethods),
24
+ "#{klass} not include FriendlyId::SluggableInstanceMethods"
25
+ end
26
+ else
27
+ should "include/extend friendly_id's non-sluggable modules" do
28
+ assert klass.extended_by.include?(FriendlyId::NonSluggableClassMethods),
29
+ "#{klass} does not extend FriendlyId::NonSluggableClassMethods"
30
+ assert klass.include?(FriendlyId::NonSluggableInstanceMethods),
31
+ "#{klass} not include FriendlyId::NonSluggableInstanceMethods"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,89 @@
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
+ class << self
8
+
9
+ # Sanitizes and dasherizes string to make it safe for URL's.
10
+ #
11
+ # Example:
12
+ #
13
+ # slug.normalize('This... is an example!') # => "this-is-an-example"
14
+ #
15
+ # Note that Rails 2.2.x offers a parameterize method for this. It's not
16
+ # used here because it assumes you want to strip away accented characters,
17
+ # and this may not always be your desire.
18
+ #
19
+ # At the time of writing, it also handles several characters incorrectly,
20
+ # for instance replacing Icelandic's "thorn" character with "y" rather
21
+ # than "d." This might be pedantic, but I don't want to piss off the
22
+ # Vikings. The last time anyone pissed them off, they uleashed a wave of
23
+ # terror in Europe unlike anything ever seen before or after. I'm not
24
+ # taking any chances.
25
+ def normalize(slug_text)
26
+ return "" if slug_text.blank?
27
+ slug_text.
28
+ send(chars_func).
29
+ # For some reason Spanish ¡ and ¿ are not detected as non-word
30
+ # characters. Bug in Ruby?
31
+ normalize.gsub(/[\W|¡|¿]/u, ' ').
32
+ strip.
33
+ gsub(/\s+/u, '-').
34
+ gsub(/-\z/u, '').
35
+ downcase.
36
+ to_s
37
+ end
38
+
39
+ def parse(friendly_id)
40
+ name, sequence = friendly_id.split('--')
41
+ sequence ||= "1"
42
+ return name, sequence
43
+ end
44
+
45
+ # Remove diacritics (accents, umlauts, etc.) from the string.
46
+ def strip_diacritics(string)
47
+ require 'unicode'
48
+ Unicode::normalize_KD(string).unpack('U*').select { |cp|
49
+ cp < 0x300 || cp > 0x036F
50
+ }.pack('U*')
51
+ end
52
+
53
+ private
54
+
55
+ def chars_func
56
+ Rails.version =~ /2.2.[\d]*/ ? :mb_chars : :chars
57
+ rescue NoMethodError
58
+ :chars
59
+ end
60
+
61
+ end
62
+
63
+ # Whether or not this slug is the most recent of its owner's slugs.
64
+ def is_most_recent?
65
+ sluggable.slug == self
66
+ end
67
+
68
+ def to_friendly_id
69
+ sequence > 1 ? "#{name}--#{sequence}" : name
70
+ end
71
+
72
+ protected
73
+
74
+ # Raise a FriendlyId::SlugGenerationError if the slug name is blank.
75
+ def check_for_blank_name #:nodoc:#
76
+ if name.blank?
77
+ raise FriendlyId::SlugGenerationError.new("The slug text is blank.")
78
+ end
79
+ end
80
+
81
+ def set_sequence
82
+ return unless new_record?
83
+ last = Slug.find(:first, :conditions => { :name => name, :scope => scope,
84
+ :sluggable_type => sluggable_type}, :order => "sequence DESC",
85
+ :select => 'sequence')
86
+ self.sequence = last.sequence + 1 if last
87
+ end
88
+
89
+ end