rdavila_friendly_id 2.2.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+