nateabbott-friendly-id 2.2.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.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Nate Abbott
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,18 @@
1
+ = nateabbott-friendly-id
2
+
3
+ Description goes here.
4
+
5
+ == Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (if you want to have your own version, that is fine but
13
+ bump version in a commit by itself I can ignore when I pull)
14
+ * Send me a pull request. Bonus points for topic branches.
15
+
16
+ == Copyright
17
+
18
+ Copyright (c) 2009 Nate Abbott. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "nateabbott-friendly-id"
8
+ gem.summary = "A comprehensive slugging and pretty-URL plugin for ActiveRecord."
9
+ gem.description = 'A comprehensive slugging and pretty-URL plugin for ActiveRecord.'
10
+ gem.email = ['norman@njclarke.com', 'adrian@mugnolo.com', 'miloops@gmail.com']
11
+ gem.homepage = "http://github.com/nateabbott/nateabbott-friendly-id"
12
+ gem.authors = ['Norman Clarke', 'Adrian Mugnolo', 'Emilio Tagua']
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ Jeweler::GemcutterTasks.new
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
18
+ end
19
+
20
+ require 'rake/testtask'
21
+ Rake::TestTask.new(:test) do |test|
22
+ test.libs << 'lib' << 'test'
23
+ test.pattern = 'test/**/test_*.rb'
24
+ test.verbose = true
25
+ end
26
+
27
+ begin
28
+ require 'rcov/rcovtask'
29
+ Rcov::RcovTask.new do |test|
30
+ test.libs << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+ rescue LoadError
35
+ task :rcov do
36
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
37
+ end
38
+ end
39
+
40
+ task :test => :check_dependencies
41
+
42
+ task :default => :test
43
+
44
+ require 'rake/rdoctask'
45
+ Rake::RDocTask.new do |rdoc|
46
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
47
+
48
+ rdoc.rdoc_dir = 'rdoc'
49
+ rdoc.title = "nateabbott-friendly-id #{version}"
50
+ rdoc.rdoc_files.include('README*')
51
+ rdoc.rdoc_files.include('lib/**/*.rb')
52
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 2.2.1
@@ -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,35 @@
1
+ # encoding: utf-8
2
+
3
+ module FriendlyId::NonSluggableClassMethods
4
+
5
+ include FriendlyId::Helpers
6
+
7
+ protected
8
+
9
+ def find_one(id, options) #:nodoc:#
10
+ if id.is_a?(String) && result = send("find_by_#{ friendly_id_options[:column] }", id, options)
11
+ result.send(:found_using_friendly_id=, true)
12
+ else
13
+ result = super id, options
14
+ end
15
+ result
16
+ end
17
+
18
+ def find_some(ids_and_names, options) #:nodoc:#
19
+
20
+ results = with_scope :find => options do
21
+ find :all, :conditions => ["#{quoted_table_name}.#{primary_key} IN (?) OR #{friendly_id_options[:column].to_s} IN (?)",
22
+ ids_and_names, ids_and_names]
23
+ end
24
+
25
+ expected = expected_size(ids_and_names, options)
26
+ if results.size != expected
27
+ 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 })"
28
+ end
29
+
30
+ results.each {|r| r.send(:found_using_friendly_id=, true) if ids_and_names.include?(r.friendly_id)}
31
+
32
+ results
33
+
34
+ end
35
+ 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,100 @@
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.
32
+ def normalize(slug_text)
33
+ return "" if slug_text.nil? || slug_text == ""
34
+ ActiveSupport::Multibyte.proxy_class.new(slug_text.to_s).normalize(:kc).
35
+ gsub(/[\W]/u, ' ').
36
+ strip.
37
+ gsub(/\s+/u, '-').
38
+ gsub(/-\z/u, '').
39
+ downcase.
40
+ to_s
41
+ end
42
+
43
+ def parse(friendly_id)
44
+ name, sequence = friendly_id.split('--')
45
+ sequence ||= "1"
46
+ return name, sequence
47
+ end
48
+
49
+ # Remove diacritics (accents, umlauts, etc.) from the string. Borrowed
50
+ # from "The Ruby Way."
51
+ def strip_diacritics(string)
52
+ a = ActiveSupport::Multibyte.proxy_class.new(string).normalize(:kd) || ""
53
+ a.unpack('U*').inject([]) { |a, u|
54
+ if ASCII_APPROXIMATIONS[u]
55
+ a += ASCII_APPROXIMATIONS[u].unpack('U*')
56
+ elsif (u < 0x300 || u > 0x036F)
57
+ a << u
58
+ end
59
+ a
60
+ }.pack('U*')
61
+ end
62
+
63
+
64
+
65
+ # Remove non-ascii characters from the string.
66
+ def strip_non_ascii(string)
67
+ strip_diacritics(string).gsub(/[^a-z0-9]+/i, ' ')
68
+ end
69
+
70
+ private
71
+
72
+ end
73
+
74
+ # Whether or not this slug is the most recent of its owner's slugs.
75
+ def is_most_recent?
76
+ sluggable.slug == self
77
+ end
78
+
79
+ def to_friendly_id
80
+ sequence > 1 ? "#{name}--#{sequence}" : name
81
+ end
82
+
83
+ protected
84
+
85
+ # Raise a FriendlyId::SlugGenerationError if the slug name is blank.
86
+ def check_for_blank_name #:nodoc:#
87
+ if name.blank?
88
+ raise FriendlyId::SlugGenerationError.new("The slug text is blank.")
89
+ end
90
+ end
91
+
92
+ def set_sequence
93
+ return unless new_record?
94
+ last = Slug.find(:first, :conditions => { :name => name, :scope => scope,
95
+ :sluggable_type => sluggable_type}, :order => "sequence DESC",
96
+ :select => 'sequence')
97
+ self.sequence = last.sequence + 1 if last
98
+ end
99
+
100
+ end
@@ -0,0 +1,109 @@
1
+ # encoding: utf-8
2
+
3
+ module FriendlyId::SluggableClassMethods
4
+
5
+ include FriendlyId::Helpers
6
+
7
+ # Finds a single record using the friendly id, or the record's id.
8
+ def find_one(id_or_name, options) #:nodoc:#
9
+
10
+ scope = options.delete(:scope)
11
+ return super(id_or_name, options) if id_or_name.is_a?(Integer)
12
+
13
+ find_options = {:select => "#{self.table_name}.*"}
14
+ find_options[:joins] = :slugs unless options[:include] && [*options[:include]].flatten.include?(:slugs)
15
+
16
+ name, sequence = Slug.parse(id_or_name)
17
+
18
+ find_options[:conditions] = {
19
+ "#{Slug.table_name}.name" => name,
20
+ "#{Slug.table_name}.scope" => scope,
21
+ "#{Slug.table_name}.sequence" => sequence
22
+ }
23
+
24
+ result = with_scope(:find => find_options) { find_initial(options) }
25
+
26
+ if result
27
+ result.finder_slug_name = id_or_name
28
+ elsif id_or_name.to_i.to_s != id_or_name
29
+ raise ActiveRecord::RecordNotFound
30
+ else
31
+ result = super id_or_name, options
32
+ end
33
+
34
+ result
35
+
36
+ rescue ActiveRecord::RecordNotFound => e
37
+
38
+ if friendly_id_options[:scope]
39
+ if !scope
40
+ raise ActiveRecord::RecordNotFound.new("%s; expected scope but got none" % e.message)
41
+ else
42
+ raise ActiveRecord::RecordNotFound.new("%s and scope=#{scope}" % e.message)
43
+ end
44
+ end
45
+
46
+ raise e
47
+
48
+ end
49
+
50
+ # Finds multiple records using the friendly ids, or the records' ids.
51
+ def find_some(ids_and_names, options) #:nodoc:#
52
+
53
+ slugs, ids = get_slugs_and_ids(ids_and_names, options)
54
+ results = []
55
+
56
+ find_options = {:select => "#{self.table_name}.*"}
57
+ find_options[:joins] = :slugs unless options[:include] && [*options[:include]].flatten.include?(:slugs)
58
+ find_options[:conditions] = "#{quoted_table_name}.#{primary_key} IN (#{ids.empty? ? 'NULL' : ids.join(',')}) "
59
+ find_options[:conditions] << "OR slugs.id IN (#{slugs.to_s(:db)})"
60
+
61
+ results = with_scope(:find => find_options) { find_every(options) }.uniq
62
+
63
+ expected = expected_size(ids_and_names, options)
64
+ if results.size != expected
65
+ 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 })"
66
+ end
67
+
68
+ assign_finder_slugs(slugs, results)
69
+
70
+ results
71
+ end
72
+
73
+ def validate_find_options(options) #:nodoc:#
74
+ options.assert_valid_keys([:conditions, :include, :joins, :limit, :offset,
75
+ :order, :select, :readonly, :group, :from, :lock, :having, :scope])
76
+ end
77
+
78
+ private
79
+
80
+ # Assign finder slugs for the results found in find_some_with_friendly
81
+ def assign_finder_slugs(slugs, results) #:nodoc:#
82
+ slugs.each do |slug|
83
+ results.select { |r| r.id == slug.sluggable_id }.each do |result|
84
+ result.send(:finder_slug=, slug)
85
+ end
86
+ end
87
+ end
88
+
89
+ # Build arrays of slugs and ids, for the find_some_with_friendly method.
90
+ def get_slugs_and_ids(ids_and_names, options) #:nodoc:#
91
+ scope = options.delete(:scope)
92
+ slugs = []
93
+ ids = []
94
+ ids_and_names.each do |id_or_name|
95
+ name, sequence = Slug.parse id_or_name.to_s
96
+ slug = Slug.find(:first, :conditions => {
97
+ :name => name,
98
+ :scope => scope,
99
+ :sequence => sequence,
100
+ :sluggable_type => base_class.name
101
+ })
102
+ # If the slug was found, add it to the array for later use. If not, and
103
+ # the id_or_name is a number, assume that it is a regular record id.
104
+ slug ? slugs << slug : (ids << id_or_name if id_or_name.to_s =~ /\A\d*\z/)
105
+ end
106
+ return slugs, ids
107
+ end
108
+
109
+ end
@@ -0,0 +1,132 @@
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_name
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
+ if cache = friendly_id_options[:cache_column]
27
+ return false if send(cache) == @finder_slug_name
28
+ end
29
+ finder_slug.id != slug.id
30
+ end
31
+
32
+ # Was the record found using an old friendly id, or its numeric id?
33
+ def has_better_id?
34
+ has_a_slug? and found_using_numeric_id? || found_using_outdated_friendly_id?
35
+ end
36
+
37
+ # Does the record have (at least) one slug?
38
+ def has_a_slug?
39
+ @finder_slug_name || slug
40
+ end
41
+
42
+ # Returns the friendly id.
43
+ def friendly_id
44
+ slug(true).to_friendly_id
45
+ end
46
+ alias best_id friendly_id
47
+
48
+ # Has the basis of our friendly id changed, requiring the generation of a
49
+ # new slug?
50
+ def new_slug_needed?
51
+ !slug || slug_text != slug.name
52
+ end
53
+
54
+ # Returns the most recent slug, which is used to determine the friendly
55
+ # id.
56
+ def slug(reload = false)
57
+ @most_recent_slug = nil if reload
58
+ @most_recent_slug ||= slugs.first(:order => "id DESC")
59
+ end
60
+
61
+ # Returns the friendly id, or if none is available, the numeric id.
62
+ def to_param
63
+ if cache = friendly_id_options[:cache_column]
64
+ return read_attribute(cache) || id.to_s
65
+ end
66
+ slug ? slug.to_friendly_id : id.to_s
67
+ end
68
+
69
+ # Get the processed string used as the basis of the friendly id.
70
+ def slug_text
71
+ base = send friendly_id_options[:column]
72
+ if self.slug_normalizer_block
73
+ base = self.slug_normalizer_block.call(base)
74
+ else
75
+ if self.friendly_id_options[:strip_diacritics]
76
+ base = Slug::strip_diacritics(base)
77
+ end
78
+ if self.friendly_id_options[:strip_non_ascii]
79
+ base = Slug::strip_non_ascii(base)
80
+ end
81
+ base = Slug::normalize(base)
82
+ end
83
+
84
+ if base.mb_chars.length > friendly_id_options[:max_length]
85
+ base = base.mb_chars[0...friendly_id_options[:max_length]]
86
+ end
87
+ if friendly_id_options[:reserved].include?(base)
88
+ raise FriendlyId::SlugGenerationError.new("The slug text is a reserved value")
89
+ end
90
+ return base
91
+ end
92
+
93
+ private
94
+
95
+ def finder_slug=(finder_slug)
96
+ @finder_slug_name = finder_slug.name
97
+ slug = finder_slug
98
+ slug.sluggable = self
99
+ slug
100
+ end
101
+
102
+ def init_finder_slug
103
+ return false if !@finder_slug_name
104
+ name, sequence = Slug.parse(@finder_slug_name)
105
+ slug = Slug.find(:first, :conditions => {:sluggable_id => id, :name => name, :sequence => sequence, :sluggable_type => self.class.base_class.name })
106
+ finder_slug = slug
107
+ end
108
+
109
+ # Set the slug using the generated friendly id.
110
+ def set_slug
111
+ if self.class.friendly_id_options[:use_slug] && new_slug_needed?
112
+ @most_recent_slug = nil
113
+ slug_attributes = {:name => slug_text}
114
+ if friendly_id_options[:scope]
115
+ scope = send(friendly_id_options[:scope])
116
+ slug_attributes[:scope] = scope.respond_to?(:to_param) ? scope.to_param : scope.to_s
117
+ end
118
+ # If we're renaming back to a previously used friendly_id, delete the
119
+ # slug so that we can recycle the name without having to use a sequence.
120
+ slugs.find(:all, :conditions => {:name => slug_text, :scope => scope}).each { |s| s.destroy }
121
+ slugs.build slug_attributes
122
+ end
123
+ end
124
+
125
+ def set_slug_cache
126
+ if friendly_id_options[:cache_column] && send(friendly_id_options[:cache_column]) != slug.name
127
+ send "#{friendly_id_options[:cache_column]}=", slug.name
128
+ send :update_without_callbacks
129
+ end
130
+ end
131
+
132
+ end