rdavila_friendly_id 2.2.6

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.
@@ -0,0 +1,113 @@
1
+ module FriendlyId::SluggableClassMethods
2
+
3
+ include FriendlyId::Helpers
4
+
5
+ # Finds a single record using the friendly id, or the record's id.
6
+ def find_one(id_or_name, options) #:nodoc:#
7
+ scope = options.delete(:scope)
8
+ scope = scope.to_param if scope && scope.respond_to?(:to_param)
9
+
10
+ if id_or_name.is_a?(Integer) || id_or_name.kind_of?(ActiveRecord::Base)
11
+ return super(id_or_name, options)
12
+ else
13
+ if self.cache_column
14
+ find_options = { :conditions => { self.cache_column => id_or_name } }
15
+ else
16
+ find_options = {:select => "#{self.table_name}.*"}
17
+ find_options[:joins] = :slugs unless options[:include] && [*options[:include]].flatten.include?(:slugs)
18
+
19
+ name, sequence = Slug.parse(id_or_name)
20
+
21
+ find_options[:conditions] = {
22
+ "#{Slug.table_name}.name" => name,
23
+ "#{Slug.table_name}.scope" => scope,
24
+ "#{Slug.table_name}.sequence" => sequence
25
+ }
26
+ end
27
+
28
+ result = with_scope(:find => find_options) { find_initial(options) }
29
+ if result
30
+ result.finder_slug_name = id_or_name
31
+ elsif id_or_name.to_i.to_s != id_or_name
32
+ raise ActiveRecord::RecordNotFound
33
+ else
34
+ result = super id_or_name, options
35
+ end
36
+
37
+ result
38
+ end
39
+
40
+ rescue ActiveRecord::RecordNotFound => e
41
+
42
+ if friendly_id_options[:scope]
43
+ if !scope
44
+ raise ActiveRecord::RecordNotFound.new("%s; expected scope but got none" % e.message)
45
+ else
46
+ raise ActiveRecord::RecordNotFound.new("%s and scope=#{scope}" % e.message)
47
+ end
48
+ end
49
+
50
+ raise e
51
+
52
+ end
53
+
54
+ # Finds multiple records using the friendly ids, or the records' ids.
55
+ def find_some(ids_and_names, options) #:nodoc:#
56
+
57
+ slugs, ids = get_slugs_and_ids(ids_and_names, options)
58
+ results = []
59
+
60
+ find_options = {:select => "#{self.table_name}.*"}
61
+ find_options[:joins] = :slugs unless options[:include] && [*options[:include]].flatten.include?(:slugs)
62
+ find_options[:conditions] = "#{quoted_table_name}.#{primary_key} IN (#{ids.empty? ? 'NULL' : ids.join(',')}) "
63
+ find_options[:conditions] << "OR slugs.id IN (#{slugs.to_s(:db)})"
64
+
65
+ results = with_scope(:find => find_options) { find_every(options) }.uniq
66
+
67
+ expected = expected_size(ids_and_names, options)
68
+ if results.size != expected
69
+ 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 })"
70
+ end
71
+
72
+ assign_finder_slugs(slugs, results)
73
+
74
+ results
75
+ end
76
+
77
+ def validate_find_options(options) #:nodoc:#
78
+ options.assert_valid_keys([:conditions, :include, :joins, :limit, :offset,
79
+ :order, :select, :readonly, :group, :from, :lock, :having, :scope])
80
+ end
81
+
82
+ private
83
+
84
+ # Assign finder slugs for the results found in find_some_with_friendly
85
+ def assign_finder_slugs(slugs, results) #:nodoc:#
86
+ slugs.each do |slug|
87
+ results.select { |r| r.id == slug.sluggable_id }.each do |result|
88
+ result.send(:finder_slug=, slug)
89
+ end
90
+ end
91
+ end
92
+
93
+ # Build arrays of slugs and ids, for the find_some_with_friendly method.
94
+ def get_slugs_and_ids(ids_and_names, options) #:nodoc:#
95
+ scope = options.delete(:scope)
96
+ slugs = []
97
+ ids = []
98
+ ids_and_names.each do |id_or_name|
99
+ name, sequence = Slug.parse id_or_name.to_s
100
+ slug = Slug.find(:first, :conditions => {
101
+ :name => name,
102
+ :scope => scope,
103
+ :sequence => sequence,
104
+ :sluggable_type => base_class.name
105
+ })
106
+ # If the slug was found, add it to the array for later use. If not, and
107
+ # the id_or_name is a number, assume that it is a regular record id.
108
+ slug ? slugs << slug : (ids << id_or_name if id_or_name.to_s =~ /\A\d*\z/)
109
+ end
110
+ return slugs, ids
111
+ end
112
+
113
+ end
@@ -0,0 +1,161 @@
1
+ module FriendlyId::SluggableInstanceMethods
2
+
3
+ def self.included(base)
4
+ base.class_eval do
5
+ has_many :slugs, :order => 'id DESC', :as => :sluggable, :dependent => :destroy
6
+ before_save :set_slug
7
+ after_save :set_slug_cache
8
+ # only protect the column if the class is not already using attributes_accessible
9
+ if !accessible_attributes
10
+ if friendly_id_options[:cache_column]
11
+ attr_protected friendly_id_options[:cache_column].to_sym
12
+ end
13
+ attr_protected :cached_slug
14
+ end
15
+ end
16
+
17
+ def base.cache_column
18
+ if defined?(@cache_column)
19
+ return @cache_column
20
+ elsif friendly_id_options[:cache_column]
21
+ @cache_column = friendly_id_options[:cache_column].to_sym
22
+ elsif columns.any? { |c| c.name == 'cached_slug' }
23
+ @cache_column = :cached_slug
24
+ else
25
+ @cache_column = nil
26
+ end
27
+ end
28
+
29
+ end
30
+
31
+ NUM_CHARS_RESERVED_FOR_FRIENDLY_ID_EXTENSION = 2
32
+
33
+ attr :finder_slug
34
+ attr_accessor :finder_slug_name
35
+
36
+ def finder_slug
37
+ @finder_slug ||= init_finder_slug or nil
38
+ end
39
+
40
+ # Was the record found using one of its friendly ids?
41
+ def found_using_friendly_id?
42
+ !!@finder_slug_name
43
+ end
44
+
45
+ # Was the record found using its numeric id?
46
+ def found_using_numeric_id?
47
+ !found_using_friendly_id?
48
+ end
49
+
50
+ # Was the record found using an old friendly id?
51
+ def found_using_outdated_friendly_id?
52
+ return false if cache_column && send(cache_column) == @finder_slug_name
53
+ finder_slug.id != slug.id
54
+ end
55
+
56
+ # Was the record found using an old friendly id, or its numeric id?
57
+ def has_better_id?
58
+ has_a_slug? and found_using_numeric_id? || found_using_outdated_friendly_id?
59
+ end
60
+
61
+ # Does the record have (at least) one slug?
62
+ def has_a_slug?
63
+ @finder_slug_name || slug
64
+ end
65
+
66
+ # Returns the friendly id.
67
+ def friendly_id
68
+ slug(true).to_friendly_id
69
+ end
70
+ alias best_id friendly_id
71
+
72
+ # Has the basis of our friendly id changed, requiring the generation of a
73
+ # new slug?
74
+ def new_slug_needed?
75
+ !slug || slug_text != slug.name
76
+ end
77
+
78
+ # Returns the most recent slug, which is used to determine the friendly
79
+ # id.
80
+ def slug(reload = false)
81
+ @most_recent_slug = nil if reload
82
+ @most_recent_slug ||= slugs.first(:order => "id DESC")
83
+ end
84
+
85
+ # Returns the friendly id, or if none is available, the numeric id.
86
+ def to_param
87
+ if cache_column
88
+ read_attribute(cache_column) || id.to_s
89
+ else
90
+ slug ? slug.to_friendly_id : id.to_s
91
+ end
92
+ end
93
+
94
+ # Get the processed string used as the basis of the friendly id.
95
+ def slug_text
96
+ base = send friendly_id_options[:method]
97
+ if self.slug_normalizer_block
98
+ base = self.slug_normalizer_block.call(base)
99
+ else
100
+ if self.friendly_id_options[:strip_diacritics]
101
+ base = Slug::strip_diacritics(base)
102
+ end
103
+ if self.friendly_id_options[:strip_non_ascii]
104
+ base = Slug::strip_non_ascii(base)
105
+ end
106
+ base = Slug::normalize(base)
107
+ end
108
+
109
+ if base.mb_chars.length > friendly_id_options[:max_length]
110
+ base = base.mb_chars[0...friendly_id_options[:max_length]]
111
+ end
112
+ if friendly_id_options[:reserved].include?(base)
113
+ raise FriendlyId::SlugGenerationError.new("The slug text is a reserved value")
114
+ end
115
+ return base
116
+ end
117
+
118
+ private
119
+
120
+ def cache_column
121
+ self.class.cache_column
122
+ end
123
+
124
+ def finder_slug=(finder_slug)
125
+ @finder_slug_name = finder_slug.name
126
+ slug = finder_slug
127
+ slug.sluggable = self
128
+ slug
129
+ end
130
+
131
+ def init_finder_slug
132
+ return false if !@finder_slug_name
133
+ name, sequence = Slug.parse(@finder_slug_name)
134
+ slug = Slug.find(:first, :conditions => {:sluggable_id => id, :name => name, :sequence => sequence, :sluggable_type => self.class.base_class.name })
135
+ finder_slug = slug
136
+ end
137
+
138
+ # Set the slug using the generated friendly id.
139
+ def set_slug
140
+ if self.class.friendly_id_options[:use_slug] && new_slug_needed?
141
+ @most_recent_slug = nil
142
+ slug_attributes = {:name => slug_text}
143
+ if friendly_id_options[:scope]
144
+ scope = send(friendly_id_options[:scope])
145
+ slug_attributes[:scope] = scope.respond_to?(:to_param) ? scope.to_param : scope.to_s
146
+ end
147
+ # If we're renaming back to a previously used friendly_id, delete the
148
+ # slug so that we can recycle the name without having to use a sequence.
149
+ slugs.find(:all, :conditions => {:name => slug_text, :scope => slug_attributes[:scope]}).each { |s| s.destroy }
150
+ slugs.build slug_attributes
151
+ end
152
+ end
153
+
154
+ def set_slug_cache
155
+ if cache_column && send(cache_column) != slug.to_friendly_id
156
+ send "#{cache_column}=", slug.to_friendly_id
157
+ send :update_without_callbacks
158
+ end
159
+ end
160
+
161
+ end
@@ -0,0 +1,92 @@
1
+ module FriendlyId
2
+ class Tasks
3
+ class << self
4
+
5
+ def make_slugs(klass, options = {})
6
+ klass = parse_class_name(klass)
7
+ validate_uses_slugs(klass)
8
+ options = {:limit => 100, :include => :slugs, :conditions => "slugs.id IS NULL"}.merge(options)
9
+ while records = klass.find(:all, options) do
10
+ break if records.size == 0
11
+ records.each do |r|
12
+ r.save!
13
+ yield(r) if block_given?
14
+ end
15
+ end
16
+ end
17
+
18
+ def make_slugs_faster(klass, options = {})
19
+ klass = parse_class_name(klass)
20
+ validate_uses_slugs(klass)
21
+ options = {:limit => 100, :include => :slugs, :conditions => "slugs.id IS NULL"}.merge(options)
22
+ while records = klass.find(:all, options) do
23
+ break if records.size == 0
24
+ slugs = []
25
+ records.each do |r|
26
+ slug = r.slugs.build :name => r.slug_text
27
+ slug.send :set_sequence
28
+ r.instance_eval do
29
+ if friendly_id_options[:scope]
30
+ scope = send(friendly_id_options[:scope])
31
+ slug.scope = scope.respond_to?(:to_param) ? scope.to_param : scope.to_s
32
+ end
33
+ end
34
+ r.instance_eval do
35
+ slugs.reverse.each do |mem_slug|
36
+ if mem_slug.name == slug.name
37
+ if friendly_id_options[:scope]
38
+ slug.sequence = mem_slug.sequence + 1 if mem_slug.scope == slug.scope
39
+ break
40
+ else
41
+ slug.sequence = mem_slug.sequence + 1
42
+ break
43
+ end
44
+ end
45
+ end
46
+ end
47
+ klass.update_all({:cached_slug => slug.to_friendly_id}, :id => r.id)
48
+ slugs << slug
49
+ yield(r) if block_given?
50
+ end
51
+ Slug.import slugs, :validate => false
52
+ end
53
+ end
54
+
55
+ def delete_slugs_for(klass)
56
+ klass = parse_class_name(klass)
57
+ validate_uses_slugs(klass)
58
+ Slug.destroy_all(["sluggable_type = ?", klass.to_s])
59
+ if klass.cache_column
60
+ klass.update_all("#{klass.cache_column} = NULL")
61
+ end
62
+ end
63
+
64
+ def delete_old_slugs(days = nil, class_name = nil)
65
+ days = days.blank? ? 45 : days.to_i
66
+ klass = class_name.blank? ? nil : parse_class_name(class_name.to_s)
67
+ conditions = ["created_at < ?", DateTime.now - days.days]
68
+ if klass
69
+ conditions[0] << " AND sluggable_type = ?"
70
+ conditions << klass.to_s
71
+ end
72
+ slugs = Slug.find :all, :conditions => conditions
73
+ slugs.each { |s| s.destroy unless s.is_most_recent? }
74
+ end
75
+
76
+ def parse_class_name(class_name)
77
+ return class_name if class_name.class == Class
78
+ if (class_name.split('::').size > 1)
79
+ class_name.split('::').inject(Kernel) {|scope, const_name| scope.const_get(const_name)}
80
+ else
81
+ Object.const_get(class_name)
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def validate_uses_slugs(klass)
88
+ raise "Class '%s' doesn't use slugs" % klass.to_s unless klass.friendly_id_options[:use_slug]
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,8 @@
1
+ module FriendlyId #:nodoc:
2
+ module Version #:nodoc:
3
+ MAJOR = 2
4
+ MINOR = 2
5
+ TINY = 6
6
+ STRING = [MAJOR, MINOR, TINY].join('.')
7
+ end
8
+ end
@@ -0,0 +1,40 @@
1
+ namespace :friendly_id do
2
+ desc "Make slugs for a model."
3
+ task :make_slugs => :environment do
4
+ validate_model_given
5
+ FriendlyId::Tasks.make_slugs(ENV["MODEL"]) do |r|
6
+ puts "%s(%d) friendly_id set to '%s'" % [r.class.to_s, r.id, r.slug.name]
7
+ end
8
+ end
9
+
10
+ desc "Make slugs for a model with large dataset."
11
+ task :make_slugs_faster => :environment do
12
+ validate_model_given
13
+ FriendlyId::Tasks.make_slugs_faster(ENV["MODEL"], :limit => 1000) do |r|
14
+ puts "%s(%d) friendly_id set to '%s'" % [r.class.to_s, r.id, r.slugs.first.name]
15
+ end
16
+ end
17
+
18
+ desc "Regenereate slugs for a model with large dataset."
19
+ task :redo_slugs_faster => :environment do
20
+ validate_model_given
21
+ FriendlyId::Tasks.delete_slugs_for(ENV["MODEL"])
22
+ Rake::Task["friendly_id:make_slugs_faster"].invoke
23
+ end
24
+
25
+ desc "Regenereate slugs for a model."
26
+ task :redo_slugs => :environment do
27
+ validate_model_given
28
+ FriendlyId::Tasks.delete_slugs_for(ENV["MODEL"])
29
+ Rake::Task["friendly_id:make_slugs"].invoke
30
+ end
31
+
32
+ desc "Kill obsolete slugs older than DAYS=45 days."
33
+ task :remove_old_slugs => :environment do
34
+ FriendlyId::Task.delete_old_slugs(ENV["DAYS"], ENV["MODEL"])
35
+ end
36
+ end
37
+
38
+ def validate_model_given
39
+ raise 'USAGE: rake friendly_id:make_slugs MODEL=MyModelName' if ENV["MODEL"].nil?
40
+ end
@@ -0,0 +1 @@
1
+ load 'tasks/friendly_id.rake'
@@ -0,0 +1,109 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class CachedSlugModelTest < Test::Unit::TestCase
4
+
5
+ context "A slugged model with a cached_slugs column" do
6
+
7
+ setup do
8
+ @paris = City.new(:name => "Paris")
9
+ @paris.save!
10
+ end
11
+
12
+ teardown do
13
+ City.delete_all
14
+ Slug.delete_all
15
+ end
16
+
17
+ should "have a slug" do
18
+ assert_not_nil @paris.slug
19
+ end
20
+
21
+ should "have a cached slug" do
22
+ assert_not_nil @paris.my_slug
23
+ end
24
+
25
+ should "have a to_param method that returns the cached slug" do
26
+ assert_equal "paris", @paris.to_param
27
+ end
28
+
29
+ should "protect the cached slug value" do
30
+ @paris.update_attributes(:my_slug => "Madrid")
31
+ @paris.reload
32
+ assert_equal "paris", @paris.my_slug
33
+ end
34
+
35
+ should "cache the incremented sequence for duplicate slug names" do
36
+ paris2 = City.create!(:name => "Paris")
37
+ assert_equal 2, paris2.slug.sequence
38
+ assert_equal "paris--2", paris2.my_slug
39
+ end
40
+
41
+ should "not update the cached slug column if it has not changed" do
42
+ @paris.population = 10_000_000
43
+ @paris.expects(:my_slug=).never
44
+ @paris.save
45
+ end
46
+
47
+
48
+ context "found by its friendly id" do
49
+
50
+ setup do
51
+ @paris = City.find(@paris.friendly_id)
52
+ end
53
+
54
+ should "not indicate that it has a better id" do
55
+ assert !@paris.has_better_id?
56
+ end
57
+
58
+ end
59
+
60
+ context "found by its numeric id" do
61
+
62
+ setup do
63
+ @paris = City.find(@paris.id)
64
+ end
65
+
66
+ should "indicate that it has a better id" do
67
+ assert @paris.has_better_id?
68
+ end
69
+
70
+ end
71
+
72
+
73
+ context "with a new slug" do
74
+
75
+ setup do
76
+ @paris.name = "Paris, France"
77
+ @paris.save!
78
+ @paris.reload
79
+ end
80
+
81
+ should "have its cached slug updated" do
82
+ assert_equal "paris-france", @paris.my_slug
83
+ end
84
+
85
+ should "have its cached slug synchronized with its friendly_id" do
86
+ assert_equal @paris.my_slug, @paris.friendly_id
87
+ end
88
+
89
+ end
90
+
91
+
92
+ context "with a cached_slug column" do
93
+
94
+ setup do
95
+ District.delete_all
96
+ @district = District.new(:name => "Latin Quarter")
97
+ @district.save!
98
+ end
99
+
100
+ should "have its cached_slug filled automatically" do
101
+ assert_equal @district.cached_slug, "latin-quarter"
102
+ end
103
+
104
+ end
105
+
106
+ end
107
+
108
+ end
109
+