nateabbott-nateabbott-friendly_id 2.1.5

Sign up to get free protection for your applications and to get access to all the features.
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 +116 -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 +155 -0
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ require 'newgem'
2
+ require 'lib/friendly_id/version'
3
+
4
+ $hoe = 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@rubysouth.com', 'adrian@rubysouth.com', 'miloops@gmail.com']
8
+ p.summary = "A comprehensive slugging and pretty-URL plugin for ActiveRecord."
9
+ p.description = 'A comprehensive slugging and pretty-URL plugin for ActiveRecord.'
10
+ p.url = 'http://friendly-id.rubyforge.org/'
11
+ p.test_globs = ['test/**/*_test.rb']
12
+ p.extra_deps << ['activerecord', '>= 2.0.0']
13
+ p.extra_deps << ['activesupport', '>= 2.0.0']
14
+ p.extra_dev_deps << ['newgem', ">= #{::Newgem::VERSION}"]
15
+ p.extra_dev_deps << ['sqlite3-ruby']
16
+ p.remote_rdoc_dir = ""
17
+ end
18
+
19
+ require 'newgem/tasks'
20
+
21
+ desc "Run RCov"
22
+ task :rcov do
23
+ run_coverage Dir["test/**/*_test.rb"]
24
+ end
25
+
26
+ def run_coverage(files)
27
+ rm_f "coverage"
28
+ rm_f "coverage.data"
29
+ if files.length == 0
30
+ puts "No files were specified for testing"
31
+ return
32
+ end
33
+ files = files.join(" ")
34
+ # if RUBY_PLATFORM =~ /darwin/
35
+ # exclude = '--exclude "gems/"'
36
+ # else
37
+ # exclude = '--exclude "rubygems"'
38
+ # end
39
+ rcov = ENV["RCOV"] ? ENV["RCOV"] : "rcov"
40
+ sh "#{rcov} -Ilib:test --sort coverage --text-report #{files}"
41
+ end
42
+
43
+ desc 'Publish RDoc to RubyForge.'
44
+ task :publish_docs => [:clean, :docs] do
45
+ host = "compay@rubyforge.org"
46
+ remote_dir = "/var/www/gforge-projects/friendly-id"
47
+ local_dir = 'doc'
48
+ sh %{rsync -av --delete #{local_dir}/ #{host}:#{remote_dir}}
49
+ end
@@ -0,0 +1,2 @@
1
+ host: compay@rubyforge.org
2
+ remote_dir: /var/www/gforge-projects/friendly-id
@@ -0,0 +1,45 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{nateabbott-friendly_id}
5
+ s.version = "2.1.5"
6
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
7
+ s.authors = ["Norman Clarke", "Adrian Mugnolo", "Emilio Tagua", "Nate Abbott"]
8
+ s.date = %q{2009-06-03}
9
+ s.description = %q{A comprehensive slugging and pretty-URL plugin for ActiveRecord.}
10
+ s.email = ["norman@rubysouth.com", "adrian@rubysouth.com", "miloops@gmail.com", "nate@everlater.com"]
11
+ s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.rdoc"]
12
+ s.files = ["History.txt", "MIT-LICENSE", "Manifest.txt", "README.rdoc", "Rakefile", "config/website.yml", "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/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/contest.rb", "test/custom_slug_normalizer_test.rb", "test/models/book.rb", "test/models/country.rb", "test/models/event.rb", "test/models/novel.rb", "test/models/person.rb", "test/models/post.rb", "test/models/thing.rb", "test/models/user.rb", "test/non_slugged_test.rb", "test/schema.rb", "test/scoped_model_test.rb", "test/slug_test.rb", "test/slugged_model_test.rb", "test/sti_test.rb", "test/test_helper.rb"]
13
+ s.homepage = %q{http://friendly-id.rubyforge.org/}
14
+ s.rdoc_options = ["--main", "README.rdoc"]
15
+ s.require_paths = ["lib"]
16
+ s.rubyforge_project = %q{nate_friendly-id}
17
+ s.rubygems_version = %q{1.3.3}
18
+ s.summary = %q{A comprehensive slugging and pretty-URL plugin for ActiveRecord.}
19
+ s.test_files = ["test/custom_slug_normalizer_test.rb", "test/non_slugged_test.rb", "test/scoped_model_test.rb", "test/slug_test.rb", "test/slugged_model_test.rb", "test/sti_test.rb"]
20
+
21
+ if s.respond_to? :specification_version then
22
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
23
+ s.specification_version = 3
24
+
25
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
26
+ s.add_runtime_dependency(%q<activerecord>, [">= 2.0.0"])
27
+ s.add_runtime_dependency(%q<activesupport>, [">= 2.0.0"])
28
+ s.add_development_dependency(%q<newgem>, [">= 1.4.1"])
29
+ s.add_development_dependency(%q<sqlite3-ruby>, [">= 0"])
30
+ s.add_development_dependency(%q<hoe>, [">= 1.8.0"])
31
+ else
32
+ s.add_dependency(%q<activerecord>, [">= 2.0.0"])
33
+ s.add_dependency(%q<activesupport>, [">= 2.0.0"])
34
+ s.add_dependency(%q<newgem>, [">= 1.4.1"])
35
+ s.add_dependency(%q<sqlite3-ruby>, [">= 0"])
36
+ s.add_dependency(%q<hoe>, [">= 1.8.0"])
37
+ end
38
+ else
39
+ s.add_dependency(%q<activerecord>, [">= 2.0.0"])
40
+ s.add_dependency(%q<activesupport>, [">= 2.0.0"])
41
+ s.add_dependency(%q<newgem>, [">= 1.4.1"])
42
+ s.add_dependency(%q<sqlite3-ruby>, [">= 0"])
43
+ s.add_dependency(%q<hoe>, [">= 1.8.0"])
44
+ end
45
+ 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,3 @@
1
+ # encoding: utf-8
2
+
3
+ require 'friendly_id'
@@ -0,0 +1,101 @@
1
+ # encoding: utf-8
2
+
3
+ require 'friendly_id/helpers'
4
+ require 'friendly_id/slug'
5
+
6
+ # FriendlyId is a comprehensize Rails plugin/gem for slugging and permalinks.
7
+ module FriendlyId
8
+
9
+ # Default options for has_friendly_id.
10
+ DEFAULT_FRIENDLY_ID_OPTIONS = {
11
+ :max_length => 255,
12
+ :method => nil,
13
+ :reserved => ["new", "index"],
14
+ :reserved_message => 'can not be "%s"',
15
+ :scope => nil,
16
+ :strip_diacritics => false,
17
+ :strip_non_ascii => false,
18
+ :use_slug => false }.freeze
19
+
20
+ # Valid keys for has_friendly_id options.
21
+ VALID_FRIENDLY_ID_KEYS = [
22
+ :max_length,
23
+ :reserved,
24
+ :reserved_message,
25
+ :scope,
26
+ :strip_diacritics,
27
+ :strip_non_ascii,
28
+ :use_slug ].freeze
29
+
30
+ # This error is raised when it's not possible to generate a unique slug.
31
+ class SlugGenerationError < StandardError ; end
32
+
33
+ module ClassMethods
34
+
35
+ # Set up an ActiveRecord model to use a friendly_id.
36
+ #
37
+ # The column argument can be one of your model's columns, or a method
38
+ # you use to generate the slug.
39
+ #
40
+ # Options:
41
+ # * <tt>:use_slug</tt> - Defaults to false. Use slugs when you want to use a non-unique text field for friendly ids.
42
+ # * <tt>:max_length</tt> - Defaults to 255. The maximum allowed length for a slug.
43
+ # * <tt>:strip_diacritics</tt> - Defaults to false. If true, it will remove accents, umlauts, etc. from western characters.
44
+ # * <tt>:strip_non_ascii</tt> - Defaults to false. If true, it will all non-ascii ([^a-z0-9]) characters.
45
+ # * <tt>:reserved</tt> - Array of words that are reserved and can't be used as friendly_id's. For sluggable models, 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 ["new", "index"].
46
+ # * <tt>:reserved_message</tt> - The validation message that will be shown when a reserved word is used as a frindly_id. Defaults to '"%s" is reserved'.
47
+ #
48
+ # You can also optionally pass a block if you want to use your own custom
49
+ # slugnormalization routines rather than the default ones that come with
50
+ # friendly_id:
51
+ #
52
+ # require 'stringex'
53
+ # class Post < ActiveRecord::Base
54
+ # has_friendly_id :title, :use_slug => true do |text|
55
+ # # Use stringex to generate the friendly_id rather than the baked-in methods
56
+ # text.to_url
57
+ # end
58
+ # end
59
+ def has_friendly_id(column, options = {}, &block)
60
+ options.assert_valid_keys VALID_FRIENDLY_ID_KEYS
61
+ options = DEFAULT_FRIENDLY_ID_OPTIONS.merge(options).merge(:column => column)
62
+ write_inheritable_attribute :friendly_id_options, options
63
+ class_inheritable_accessor :friendly_id_options
64
+ class_inheritable_reader :slug_normalizer_block
65
+
66
+ if options[:use_slug]
67
+ has_many :slugs, :order => 'id DESC', :as => :sluggable, :dependent => :destroy
68
+ require 'friendly_id/sluggable_class_methods'
69
+ require 'friendly_id/sluggable_instance_methods'
70
+ extend SluggableClassMethods
71
+ include SluggableInstanceMethods
72
+ before_save :set_slug
73
+ if block_given?
74
+ write_inheritable_attribute :slug_normalizer_block, block
75
+ end
76
+ else
77
+ require 'friendly_id/non_sluggable_class_methods'
78
+ require 'friendly_id/non_sluggable_instance_methods'
79
+ extend NonSluggableClassMethods
80
+ include NonSluggableInstanceMethods
81
+ validate :validate_friendly_id
82
+ end
83
+ end
84
+
85
+ end
86
+
87
+ class << self
88
+
89
+ # Load FriendlyId if the gem is included in a Rails app.
90
+ def enable
91
+ return if ActiveRecord::Base.methods.include? 'has_friendly_id'
92
+ ActiveRecord::Base.class_eval { extend FriendlyId::ClassMethods }
93
+ end
94
+
95
+ end
96
+
97
+ end
98
+
99
+ if defined?(ActiveRecord)
100
+ FriendlyId::enable
101
+ end
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+ module FriendlyId
4
+
5
+ module Helpers
6
+ # Calculate expected result size for find_some_with_friendly (taken from
7
+ # active_record/base.rb)
8
+ def expected_size(ids_and_names, options) #:nodoc:#
9
+ size = options[:offset] ? ids_and_names.size - options[:offset] : ids_and_names.size
10
+ size = options[:limit] if options[:limit] && size > options[:limit]
11
+ size
12
+ end
13
+ end
14
+
15
+ end
@@ -0,0 +1,42 @@
1
+ # encoding: utf-8
2
+
3
+ module FriendlyId::NonSluggableClassMethods
4
+
5
+ include FriendlyId::Helpers
6
+
7
+ def self.extended(base) #:nodoc:#
8
+ class << base
9
+ alias_method_chain :find_one, :friendly
10
+ alias_method_chain :find_some, :friendly
11
+ end
12
+ end
13
+
14
+ protected
15
+
16
+ def find_one_with_friendly(id, options) #:nodoc:#
17
+ if id.is_a?(String) && result = send("find_by_#{ friendly_id_options[:column] }", id, options)
18
+ result.send(:found_using_friendly_id=, true)
19
+ else
20
+ result = find_one_without_friendly id, options
21
+ end
22
+ result
23
+ end
24
+
25
+ def find_some_with_friendly(ids_and_names, options) #:nodoc:#
26
+
27
+ results = with_scope :find => options do
28
+ find :all, :conditions => ["#{quoted_table_name}.#{primary_key} IN (?) OR #{friendly_id_options[:column].to_s} IN (?)",
29
+ ids_and_names, ids_and_names]
30
+ end
31
+
32
+ expected = expected_size(ids_and_names, options)
33
+ if results.size != expected
34
+ 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 })"
35
+ end
36
+
37
+ results.each {|r| r.send(:found_using_friendly_id=, true) if ids_and_names.include?(r.friendly_id)}
38
+
39
+ results
40
+
41
+ end
42
+ end
@@ -0,0 +1,43 @@
1
+ # encoding: utf-8
2
+
3
+ module FriendlyId::NonSluggableInstanceMethods
4
+
5
+ attr :found_using_friendly_id
6
+
7
+ # Was the record found using one of its friendly ids?
8
+ def found_using_friendly_id?
9
+ @found_using_friendly_id
10
+ end
11
+
12
+ # Was the record found using its numeric id?
13
+ def found_using_numeric_id?
14
+ !@found_using_friendly_id
15
+ end
16
+ alias has_better_id? found_using_numeric_id?
17
+
18
+ # Returns the friendly_id.
19
+ def friendly_id
20
+ send friendly_id_options[:column]
21
+ end
22
+ alias best_id friendly_id
23
+
24
+ # Returns the friendly id, or if none is available, the numeric id.
25
+ def to_param
26
+ (friendly_id || id).to_s
27
+ end
28
+
29
+ private
30
+
31
+ def validate_friendly_id
32
+ if self.class.friendly_id_options[:reserved].include? friendly_id
33
+ self.errors.add(self.class.friendly_id_options[:column],
34
+ self.class.friendly_id_options[:reserved_message] % friendly_id)
35
+ return false
36
+ end
37
+ end
38
+
39
+ def found_using_friendly_id=(value) #:nodoc#
40
+ @found_using_friendly_id = value
41
+ end
42
+
43
+ end
@@ -0,0 +1,102 @@
1
+ #encoding: utf-8
2
+
3
+ # A Slug is a unique, human-friendly identifier for an ActiveRecord.
4
+ class Slug < ActiveRecord::Base
5
+
6
+ belongs_to :sluggable, :polymorphic => true
7
+ before_save :check_for_blank_name, :set_sequence
8
+
9
+
10
+ ASCII_APPROXIMATIONS = {
11
+ 198 => "AE",
12
+ 208 => "D",
13
+ 216 => "O",
14
+ 222 => "Th",
15
+ 223 => "ss",
16
+ 230 => "ae",
17
+ 240 => "d",
18
+ 248 => "o",
19
+ 254 => "th"
20
+ }.freeze
21
+
22
+ class << self
23
+
24
+ # Sanitizes and dasherizes string to make it safe for URL's.
25
+ #
26
+ # Example:
27
+ #
28
+ # slug.normalize('This... is an example!') # => "this-is-an-example"
29
+ #
30
+ # Note that the Unicode handling in ActiveSupport may fail to process some
31
+ # characters from Polish, Icelandic and other languages. If your
32
+ # application uses these languages, check {out this
33
+ # article}[http://link-coming-soon.com] for information on how to get
34
+ # better urls in your application.
35
+ def normalize(slug_text)
36
+ return "" if slug_text.nil? || slug_text == ""
37
+ ActiveSupport::Multibyte.proxy_class.new(slug_text.to_s).normalize(:kc).
38
+ gsub(/[\W]/u, ' ').
39
+ strip.
40
+ gsub(/\s+/u, '-').
41
+ gsub(/-\z/u, '').
42
+ downcase.
43
+ to_s
44
+ end
45
+
46
+ def parse(friendly_id)
47
+ name, sequence = friendly_id.split('--')
48
+ sequence ||= "1"
49
+ return name, sequence
50
+ end
51
+
52
+ # Remove diacritics (accents, umlauts, etc.) from the string. Borrowed
53
+ # from "The Ruby Way."
54
+ def strip_diacritics(string)
55
+ ActiveSupport::Multibyte.proxy_class.new(string).normalize(:kd).unpack('U*').inject([]) { |a, u|
56
+ if ASCII_APPROXIMATIONS[u]
57
+ a += ASCII_APPROXIMATIONS[u].unpack('U*')
58
+ elsif (u < 0x300 || u > 0x036F)
59
+ a << u
60
+ end
61
+ a
62
+ }.pack('U*')
63
+ end
64
+
65
+
66
+
67
+ # Remove non-ascii characters from the string.
68
+ def strip_non_ascii(string)
69
+ strip_diacritics(string).gsub(/[^a-z0-9]+/i, ' ')
70
+ end
71
+
72
+ private
73
+
74
+ end
75
+
76
+ # Whether or not this slug is the most recent of its owner's slugs.
77
+ def is_most_recent?
78
+ sluggable.slug == self
79
+ end
80
+
81
+ def to_friendly_id
82
+ sequence > 1 ? "#{name}--#{sequence}" : name
83
+ end
84
+
85
+ protected
86
+
87
+ # Raise a FriendlyId::SlugGenerationError if the slug name is blank.
88
+ def check_for_blank_name #:nodoc:#
89
+ if name.blank?
90
+ raise FriendlyId::SlugGenerationError.new("The slug text is blank.")
91
+ end
92
+ end
93
+
94
+ def set_sequence
95
+ return unless new_record?
96
+ last = Slug.find(:first, :conditions => { :name => name, :scope => scope,
97
+ :sluggable_type => sluggable_type}, :order => "sequence DESC",
98
+ :select => 'sequence')
99
+ self.sequence = last.sequence + 1 if last
100
+ end
101
+
102
+ end