friendly_id 4.0.0.beta7 → 4.0.0.beta8

Sign up to get free protection for your applications and to get access to all the features.
data/Changelog.md CHANGED
@@ -178,11 +178,11 @@ fork, then this upgrade may causes issues.
178
178
  * New option to pass arguments to `FriendlyId::SlugString#approximate_ascii!`,
179
179
  allowing custom approximations specific to German or Spanish.
180
180
  * FriendlyId now queries against the cached_slug column, which improves performance.
181
- * {FriendlyId::SlugString} class added, allowing finer-grained control over
181
+ * FriendlyId::SlugString class added, allowing finer-grained control over
182
182
  Unicode friendly_id strings.
183
- * {FriendlyId::Configuration} class added, offering more flexible/hackable
183
+ * FriendlyId::Configuration class added, offering more flexible/hackable
184
184
  options.
185
- * FriendlyId now raises subclasses of {FriendlyId::SlugGenerationError}
185
+ * FriendlyId now raises subclasses of FriendlyId::SlugGenerationError
186
186
  depending on the error context.
187
187
  * Simple models now correctly validate friendly_id length.
188
188
  * Passing block into FriendlyId deprecated in favor of overriding
data/README.md CHANGED
@@ -24,8 +24,10 @@ FriendlyId is compatible with Active Record **3.0** and **3.1**.
24
24
 
25
25
  ## Version 4.x
26
26
 
27
- FriendlyId 4.x introduces many changes incompatible with 3.x. If you're upgrading,
28
- please read the docs to see what's new.
27
+ FriendlyId 4.x introduces many changes incompatible with 3.x. If you're
28
+ upgrading, please [read the
29
+ docs](http://norman.github.com/friendly_id/file.WhatsNew.html) to see what's
30
+ new.
29
31
 
30
32
  ## Rails Quickstart
31
33
 
@@ -35,7 +37,7 @@ please read the docs to see what's new.
35
37
 
36
38
  cd my_app
37
39
 
38
- gem "friendly_id", "~> 4.0.0.beta7"
40
+ gem "friendly_id", "~> 4.0.0.beta8"
39
41
 
40
42
 
41
43
  rails generate scaffold user name:string slug:string
@@ -60,7 +62,7 @@ please read the docs to see what's new.
60
62
  ## Docs
61
63
 
62
64
  The current docs can be found
63
- [here](http://rdoc.info/github/norman/friendly_id/a4128af31d85ee29ad8f/frames).
65
+ [here](http://norman.github.com/friendly_id/)
64
66
 
65
67
  ## Benchmarks
66
68
 
data/lib/friendly_id.rb CHANGED
@@ -81,7 +81,7 @@ In general, use slugs by default unless you know for sure you don't need them.
81
81
  module FriendlyId
82
82
 
83
83
  # The current version.
84
- VERSION = "4.0.0.beta7"
84
+ VERSION = "4.0.0.beta8"
85
85
 
86
86
  @mutex = Mutex.new
87
87
 
@@ -1,16 +1,16 @@
1
1
  require 'rails/generators'
2
2
  require 'rails/generators/migration'
3
3
 
4
+ # This generator adds a migration for the {FriendlyId::History
5
+ # FriendlyId::History} addon.
4
6
  class FriendlyIdGenerator < Rails::Generators::Base
5
-
6
7
  include Rails::Generators::Migration
7
8
 
8
- source_root File.expand_path('../../../generators/friendly_id/templates', __FILE__)
9
-
10
- class_option :"skip-migration", :type => :boolean, :desc => "Don't generate a migration for the slugs table"
9
+ source_root File.expand_path('../../friendly_id', __FILE__)
11
10
 
11
+ # Copies the migration template to db/migrate.
12
12
  def copy_files(*args)
13
- migration_template 'create_slugs.rb', "db/migrate/create_slugs.rb" unless options["skip-migration"]
13
+ migration_template 'migration.rb', 'db/migrate/create_friendly_id_slugs.rb'
14
14
  end
15
15
 
16
16
  # Taken from ActiveRecord's migration generator
@@ -21,5 +21,4 @@ class FriendlyIdGenerator < Rails::Generators::Base
21
21
  "%.3d" % (current_migration_number(dirname) + 1)
22
22
  end
23
23
  end
24
-
25
24
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: friendly_id
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0.beta7
4
+ version: 4.0.0.beta8
5
5
  prerelease: 6
6
6
  platform: ruby
7
7
  authors:
@@ -14,7 +14,7 @@ default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
17
- requirement: &70318438630040 !ruby/object:Gem::Requirement
17
+ requirement: &70172556099120 !ruby/object:Gem::Requirement
18
18
  none: false
19
19
  requirements:
20
20
  - - ~>
@@ -22,10 +22,10 @@ dependencies:
22
22
  version: '3.0'
23
23
  type: :development
24
24
  prerelease: false
25
- version_requirements: *70318438630040
25
+ version_requirements: *70172556099120
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: sqlite3
28
- requirement: &70318438629540 !ruby/object:Gem::Requirement
28
+ requirement: &70172556098620 !ruby/object:Gem::Requirement
29
29
  none: false
30
30
  requirements:
31
31
  - - ~>
@@ -33,10 +33,10 @@ dependencies:
33
33
  version: '1.3'
34
34
  type: :development
35
35
  prerelease: false
36
- version_requirements: *70318438629540
36
+ version_requirements: *70172556098620
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: minitest
39
- requirement: &70318438629080 !ruby/object:Gem::Requirement
39
+ requirement: &70172556098140 !ruby/object:Gem::Requirement
40
40
  none: false
41
41
  requirements:
42
42
  - - ~>
@@ -44,10 +44,10 @@ dependencies:
44
44
  version: 2.4.0
45
45
  type: :development
46
46
  prerelease: false
47
- version_requirements: *70318438629080
47
+ version_requirements: *70172556098140
48
48
  - !ruby/object:Gem::Dependency
49
49
  name: mocha
50
- requirement: &70318438628620 !ruby/object:Gem::Requirement
50
+ requirement: &70172556097640 !ruby/object:Gem::Requirement
51
51
  none: false
52
52
  requirements:
53
53
  - - ~>
@@ -55,10 +55,10 @@ dependencies:
55
55
  version: 0.9.12
56
56
  type: :development
57
57
  prerelease: false
58
- version_requirements: *70318438628620
58
+ version_requirements: *70172556097640
59
59
  - !ruby/object:Gem::Dependency
60
60
  name: ffaker
61
- requirement: &70318438628120 !ruby/object:Gem::Requirement
61
+ requirement: &70172556097180 !ruby/object:Gem::Requirement
62
62
  none: false
63
63
  requirements:
64
64
  - - ~>
@@ -66,10 +66,10 @@ dependencies:
66
66
  version: 1.8.0
67
67
  type: :development
68
68
  prerelease: false
69
- version_requirements: *70318438628120
69
+ version_requirements: *70172556097180
70
70
  - !ruby/object:Gem::Dependency
71
71
  name: maruku
72
- requirement: &70318438627560 !ruby/object:Gem::Requirement
72
+ requirement: &70172556096720 !ruby/object:Gem::Requirement
73
73
  none: false
74
74
  requirements:
75
75
  - - ~>
@@ -77,10 +77,10 @@ dependencies:
77
77
  version: 0.6.0
78
78
  type: :development
79
79
  prerelease: false
80
- version_requirements: *70318438627560
80
+ version_requirements: *70172556096720
81
81
  - !ruby/object:Gem::Dependency
82
82
  name: yard
83
- requirement: &70318438627040 !ruby/object:Gem::Requirement
83
+ requirement: &70172556096260 !ruby/object:Gem::Requirement
84
84
  none: false
85
85
  requirements:
86
86
  - - ~>
@@ -88,7 +88,7 @@ dependencies:
88
88
  version: 0.7.2
89
89
  type: :development
90
90
  prerelease: false
91
- version_requirements: *70318438627040
91
+ version_requirements: *70172556096260
92
92
  description: ! 'FriendlyId is the "Swiss Army bulldozer" of slugging and permalink
93
93
  plugins for
94
94
 
@@ -120,32 +120,18 @@ files:
120
120
  - gemfiles/Gemfile.rails-3.1.rb
121
121
  - gemfiles/Gemfile.rails-3.1.rb.lock
122
122
  - lib/friendly_id.rb
123
- - lib/friendly_id/active_record.rb
124
- - lib/friendly_id/active_record_adapter/configuration.rb
125
- - lib/friendly_id/active_record_adapter/relation.rb
126
- - lib/friendly_id/active_record_adapter/simple_model.rb
127
- - lib/friendly_id/active_record_adapter/slug.rb
128
- - lib/friendly_id/active_record_adapter/slugged_model.rb
129
- - lib/friendly_id/active_record_adapter/tasks.rb
130
123
  - lib/friendly_id/base.rb
131
124
  - lib/friendly_id/configuration.rb
132
- - lib/friendly_id/datamapper.rb
133
125
  - lib/friendly_id/finder_methods.rb
134
126
  - lib/friendly_id/history.rb
135
127
  - lib/friendly_id/migration.rb
136
128
  - lib/friendly_id/model.rb
137
129
  - lib/friendly_id/object_utils.rb
138
- - lib/friendly_id/railtie.rb
139
130
  - lib/friendly_id/reserved.rb
140
131
  - lib/friendly_id/scoped.rb
141
- - lib/friendly_id/sequel.rb
142
132
  - lib/friendly_id/slug.rb
143
133
  - lib/friendly_id/slug_sequencer.rb
144
- - lib/friendly_id/slug_string.rb
145
134
  - lib/friendly_id/slugged.rb
146
- - lib/friendly_id/status.rb
147
- - lib/friendly_id/test.rb
148
- - lib/friendly_id/version.rb
149
135
  - lib/generators/friendly_id_generator.rb
150
136
  - test/base_test.rb
151
137
  - test/configuration_test.rb
@@ -1,56 +0,0 @@
1
- module FriendlyId
2
-
3
- module ActiveRecordAdapter
4
-
5
- include FriendlyId::Base
6
-
7
- def has_friendly_id(method, options = {})
8
-
9
- class_attribute :friendly_id_config
10
- self.friendly_id_config = Configuration.new(self, method, options)
11
-
12
- if friendly_id_config.use_slug?
13
- include SluggedModel
14
- else
15
- include SimpleModel
16
- end
17
- end
18
-
19
- private
20
-
21
- # Prevent the cached_slug column from being accidentally or maliciously
22
- # overwritten. Note that +attr_protected+ is used to protect the cached_slug
23
- # column, unless you have already invoked +attr_accessible+. So if you
24
- # wish to use +attr_accessible+, you must invoke it BEFORE you invoke
25
- # {#has_friendly_id} in your class.
26
- def protect_friendly_id_attributes
27
- # only protect the column if the class is not already using attr_accessible
28
- unless accessible_attributes.present?
29
- if friendly_id_config.custom_cache_column?
30
- attr_protected friendly_id_config.cache_column
31
- end
32
- attr_protected :cached_slug
33
- end
34
- end
35
-
36
- end
37
- end
38
-
39
- require "friendly_id/active_record_adapter/relation"
40
- require "friendly_id/active_record_adapter/configuration"
41
- require "friendly_id/active_record_adapter/simple_model"
42
- require "friendly_id/active_record_adapter/slugged_model"
43
- require "friendly_id/active_record_adapter/slug"
44
- require "friendly_id/active_record_adapter/tasks"
45
-
46
- module ActiveRecord
47
- class Base
48
- extend FriendlyId::ActiveRecordAdapter
49
- end
50
-
51
- class Relation
52
- alias find_one_without_friendly find_one
53
- alias find_some_without_friendly find_some
54
- include FriendlyId::ActiveRecordAdapter::Relation
55
- end
56
- end
@@ -1,68 +0,0 @@
1
- module FriendlyId
2
-
3
- module ActiveRecordAdapter
4
-
5
- # Extends FriendlyId::Configuration with some implementation details and
6
- # features specific to ActiveRecord.
7
- class Configuration < FriendlyId::Configuration
8
-
9
- # The column used to cache the friendly_id string. If no column is specified,
10
- # FriendlyId will look for a column named +cached_slug+ and use it automatically
11
- # if it exists. If for some reason you have a column named +cached_slug+
12
- # but don't want FriendlyId to modify it, pass the option
13
- # +:cache_column => false+ to {FriendlyId::ActiveRecordAdapter#has_friendly_id has_friendly_id}.
14
- attr_accessor :cache_column
15
-
16
- # An array of classes for which the configured class serves as a
17
- # FriendlyId scope.
18
- attr_reader :child_scopes
19
-
20
- attr_reader :custom_cache_column
21
-
22
- def cache_column
23
- return @cache_column if defined?(@cache_column)
24
- @cache_column = autodiscover_cache_column
25
- end
26
-
27
- def cache_column?
28
- !! cache_column
29
- end
30
-
31
- def cache_column=(cache_column)
32
- @cache_column = cache_column
33
- @custom_cache_column = cache_column
34
- end
35
-
36
- def child_scopes
37
- @child_scopes ||= associated_friendly_classes.select do |klass|
38
- klass.friendly_id_config.scopes_over?(configured_class)
39
- end
40
- end
41
-
42
- def custom_cache_column?
43
- !! custom_cache_column
44
- end
45
-
46
- def scope_for(record)
47
- return nil unless scope?
48
- record.send(scope).nil? ? nil : record.send(scope).to_param
49
- end
50
-
51
- def scopes_over?(klass)
52
- scope? && scope == klass.to_s.underscore.to_sym
53
- end
54
-
55
- private
56
-
57
- def autodiscover_cache_column
58
- :cached_slug if configured_class.columns.any? { |column| column.name == 'cached_slug' }
59
- end
60
-
61
- def associated_friendly_classes
62
- configured_class.reflect_on_all_associations.compact.select { |assoc|
63
- !assoc.options[:polymorphic] && assoc.klass.respond_to?(:friendly_id_config)
64
- }.map(&:klass)
65
- end
66
- end
67
- end
68
- end
@@ -1,173 +0,0 @@
1
- module FriendlyId
2
- module ActiveRecordAdapter
3
- module Relation
4
-
5
- class Find
6
- extend Forwardable
7
-
8
- attr :relation
9
- attr :ids
10
- alias id ids
11
-
12
- def_delegators :relation, :arel, :arel_table, :klass, :limit_value, :offset_value, :where
13
- def_delegators :klass, :connection, :friendly_id_config
14
- alias fc friendly_id_config
15
-
16
- def initialize(relation, ids)
17
- @relation = relation
18
- @ids = ids
19
- end
20
-
21
- def find_one
22
- if fc.cache_column?
23
- find_one_with_cached_slug
24
- elsif fc.use_slugs?
25
- find_one_with_slug
26
- else
27
- find_one_without_slug
28
- end
29
- end
30
-
31
- def find_some
32
- ids = @ids.compact.uniq.map {|id| id.respond_to?(:friendly_id_config) ? id.id : id}
33
- friendly_ids, unfriendly_ids = ids.partition {|id| id.friendly_id?}
34
- return if friendly_ids.empty?
35
- records = friendly_records(friendly_ids, unfriendly_ids).each do |record|
36
- record.friendly_id_status.name = ids
37
- end
38
- validate_expected_size!(ids, records)
39
- end
40
-
41
- private
42
-
43
- def assign_status
44
- return unless @result
45
- name, seq = id.to_s.parse_friendly_id
46
- @result.friendly_id_status.name = name
47
- @result.friendly_id_status.sequence = seq if fc.use_slugs?
48
- @result
49
- end
50
-
51
- def find_one_without_slug
52
- @result = where(fc.column => id).first
53
- assign_status
54
- end
55
-
56
- def find_one_with_cached_slug
57
- @result = where(fc.cache_column => id).first
58
- assign_status or find_one_with_slug
59
- end
60
-
61
- def find_one_with_slug
62
- sluggable_ids = sluggable_ids_for([id])
63
-
64
- if sluggable_ids.size > 1 && fc.scope?
65
- return relation.where(primary_key_column.in(sluggable_ids)).first
66
- end
67
-
68
- sluggable_id = sluggable_ids.first
69
-
70
- if sluggable_id
71
- name, seq = id.to_s.parse_friendly_id
72
- record = relation.send(:find_one_without_friendly, sluggable_id)
73
- record.friendly_id_status.name = name
74
- record.friendly_id_status.sequence = seq
75
- record
76
- else
77
- relation.send(:find_one_without_friendly, id)
78
- end
79
- end
80
-
81
- def friendly_records(friendly_ids, unfriendly_ids)
82
- use_slugs_table = fc.use_slugs? && (!fc.cache_column?)
83
- return find_some_using_slug(friendly_ids, unfriendly_ids) if use_slugs_table
84
- column = fc.cache_column || fc.column
85
- friendly = arel_table[column].in(friendly_ids)
86
- unfriendly = primary_key_column.in unfriendly_ids
87
- if friendly_ids.present? && unfriendly_ids.present?
88
- where(friendly.or(unfriendly))
89
- else
90
- where(friendly)
91
- end
92
- end
93
-
94
- def find_some_using_slug(friendly_ids, unfriendly_ids)
95
- ids = [unfriendly_ids + sluggable_ids_for(friendly_ids)].flatten.uniq
96
- where(primary_key_column.in(ids))
97
- end
98
-
99
- def sluggable_ids_for(ids)
100
- return [] if ids.empty?
101
- fragment = "(slugs.sluggable_type = %s AND slugs.name = %s AND slugs.sequence = %d)"
102
- conditions = ids.inject(nil) do |clause, id|
103
- name, seq = id.parse_friendly_id
104
- string = fragment % [connection.quote(klass.base_class.name), connection.quote(name), seq]
105
- clause ? clause + " OR #{string}" : string
106
- end
107
- sql = "SELECT sluggable_id FROM slugs WHERE (%s)" % conditions
108
- connection.select_values sql
109
- end
110
-
111
- def validate_expected_size!(ids, result)
112
- expected_size =
113
- if limit_value && ids.size > limit_value
114
- limit_value
115
- else
116
- ids.size
117
- end
118
-
119
- # 11 ids with limit 3, offset 9 should give 2 results.
120
- if offset_value && (ids.size - offset_value < expected_size)
121
- expected_size = ids.size - offset_value
122
- end
123
-
124
- if result.size == expected_size
125
- result
126
- else
127
- conditions = arel.send(:where_clauses).join(', ')
128
- conditions = " [WHERE #{conditions}]" if conditions.present?
129
- error = "Couldn't find all #{klass.name.pluralize} with IDs "
130
- error << "(#{ids.join(", ")})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
131
- raise ActiveRecord::RecordNotFound, error
132
- end
133
- end
134
-
135
- def primary_key_column
136
- if relation.primary_key.is_a?(String)
137
- arel_table[relation.primary_key]
138
- else
139
- arel_table[relation.primary_key.name]
140
- end
141
- end
142
- end
143
-
144
- def apply_finder_options(options)
145
- if options[:scope]
146
- raise "The :scope finder option has been removed from FriendlyId 3.2.0 " +
147
- "https://github.com/norman/friendly_id/issues#issue/88"
148
- end
149
- super
150
- end
151
-
152
- protected
153
-
154
- def find_one(id)
155
- begin
156
- return super if !klass.uses_friendly_id? or id.unfriendly_id?
157
- find = Find.new(self, id)
158
- find.find_one or super
159
- end
160
- end
161
-
162
- def find_some(ids)
163
- return super unless klass.uses_friendly_id?
164
- Find.new(self, ids).find_some or begin
165
- # A change in Arel 2.0.x causes find_some to fail with arrays of instances; not sure why.
166
- # This is an emergency, temporary fix.
167
- ids = ids.map {|id| (id.respond_to?(:friendly_id_config) ? id.id : id)}
168
- super
169
- end
170
- end
171
- end
172
- end
173
- end
@@ -1,62 +0,0 @@
1
- module FriendlyId
2
- module ActiveRecordAdapter
3
-
4
- module SimpleModel
5
-
6
- def self.included(base)
7
- base.class_eval do
8
- column = friendly_id_config.column
9
- validate :validate_friendly_id, :unless => :skip_friendly_id_validations
10
- validates_presence_of column, :unless => :skip_friendly_id_validations
11
- validates_length_of column, :maximum => friendly_id_config.max_length, :unless => :skip_friendly_id_validations
12
- after_update :update_scopes
13
- end
14
- end
15
-
16
- # Get the {FriendlyId::Status} after the find has been performed.
17
- def friendly_id_status
18
- @friendly_id_status ||= Status.new :record => self
19
- end
20
-
21
- # Returns the friendly_id.
22
- def friendly_id
23
- send friendly_id_config.column
24
- end
25
- alias best_id friendly_id
26
-
27
- # Returns the friendly id, or if none is available, the numeric id.
28
- def to_param
29
- (friendly_id || id).to_s
30
- end
31
-
32
- private
33
-
34
- # The old and new values for the friendly_id column.
35
- def friendly_id_changes
36
- changes[friendly_id_config.column.to_s]
37
- end
38
-
39
- # Update the slugs for any model that is using this model as its
40
- # FriendlyId scope.
41
- def update_scopes
42
- if changes = friendly_id_changes
43
- friendly_id_config.child_scopes.each do |klass|
44
- Slug.update_all "scope = '#{changes[1]}'", ["sluggable_type = ? AND scope = ?", klass.to_s, changes[0]]
45
- end
46
- end
47
- end
48
-
49
- def skip_friendly_id_validations
50
- friendly_id.nil? && friendly_id_config.allow_nil?
51
- end
52
-
53
- def validate_friendly_id
54
- if result = friendly_id_config.reserved_error_message(friendly_id)
55
- self.errors.add(*result)
56
- return false
57
- end
58
- end
59
-
60
- end
61
- end
62
- end
@@ -1,84 +0,0 @@
1
- # A Slug is a unique, human-friendly identifier for an ActiveRecord.
2
- class Slug < ::ActiveRecord::Base
3
- attr_writer :sluggable
4
- attr_accessible :name, :scope, :sluggable, :sequence
5
- table_name = "slugs"
6
- before_save :enable_name_reversion, :set_sequence
7
- validate :validate_name
8
- scope :similar_to, lambda {|slug| {:conditions => {
9
- :name => slug.name,
10
- :scope => slug.scope,
11
- :sluggable_type => slug.sluggable_type
12
- },
13
- :order => "sequence ASC"
14
- }
15
- }
16
-
17
- def sluggable
18
- sluggable_id && !@sluggable and begin
19
- klass = sluggable_type.constantize
20
- klass.send(:with_exclusive_scope) do
21
- @sluggable = klass.find(sluggable_id)
22
- end
23
- end
24
- @sluggable
25
- end
26
-
27
- # Whether this slug is the most recent of its owner's slugs.
28
- def current?
29
- sluggable.slug == self
30
- end
31
-
32
- def outdated?
33
- !current?
34
- end
35
-
36
- def to_friendly_id
37
- sequence > 1 ? friendly_id_with_sequence : name
38
- end
39
-
40
- # Raise a FriendlyId::SlugGenerationError if the slug name is blank.
41
- def validate_name
42
- if name.blank?
43
- raise FriendlyId::BlankError.new("slug.name can not be blank.")
44
- end
45
- end
46
-
47
- def save(*args)
48
- persisted? && !scope_changed? ? true : super
49
- end
50
-
51
- def save!(*args)
52
- persisted? && !scope_changed? ? true : super
53
- end
54
-
55
- private
56
-
57
- # If we're renaming back to a previously used friendly_id, delete the
58
- # slug so that we can recycle the name without having to use a sequence.
59
- def enable_name_reversion
60
- sluggable.slugs.find_all_by_name_and_scope(name, scope).each { |slug| slug.destroy }
61
- end
62
-
63
- def friendly_id_with_sequence
64
- "#{name}#{separator}#{sequence}"
65
- end
66
-
67
- def similar_to_other_slugs?
68
- !similar_slugs.empty?
69
- end
70
-
71
- def similar_slugs
72
- self.class.similar_to(self)
73
- end
74
-
75
- def separator
76
- sluggable.friendly_id_config.sequence_separator
77
- end
78
-
79
- def set_sequence
80
- return unless new_record?
81
- self.sequence = similar_slugs.last.sequence.succ if similar_to_other_slugs?
82
- end
83
-
84
- end
@@ -1,110 +0,0 @@
1
- module FriendlyId
2
- module ActiveRecordAdapter
3
- module SluggedModel
4
-
5
- def self.included(base)
6
- base.class_eval do
7
- has_many :slugs, :order => 'id DESC', :as => :sluggable, :dependent => :destroy
8
- has_one :slug, :order => 'id DESC', :as => :sluggable, :dependent => :nullify
9
- before_save :build_a_slug
10
- after_save :set_slug_cache
11
- after_update :update_scope
12
- after_update :update_dependent_scopes
13
- protect_friendly_id_attributes
14
- end
15
- end
16
-
17
- include FriendlyId::Slugged::Model
18
-
19
- def find_slug(name, sequence)
20
- slugs.find_by_name_and_sequence(name, sequence)
21
- end
22
-
23
- # Returns the friendly id, or if none is available, the numeric id. Note that this
24
- # method will use the cached_slug value if present, unlike {#friendly_id}.
25
- def to_param
26
- friendly_id_config.cache_column ? to_param_from_cache : to_param_from_slug
27
- end
28
-
29
- private
30
-
31
- def scope_changed?
32
- friendly_id_config.scope? && send(friendly_id_config.scope).to_param != slug.scope
33
- end
34
-
35
- # Respond with the cached value if available.
36
- def to_param_from_cache
37
- read_attribute(friendly_id_config.cache_column) || id.to_s
38
- end
39
-
40
- # Respond with the slugged value if available.
41
- def to_param_from_slug
42
- slug? ? slug.to_friendly_id : id.to_s
43
- end
44
-
45
- # Build the new slug using the generated friendly id.
46
- def build_a_slug
47
- return unless new_slug_needed?
48
- self.slug = slugs.build :name => slug_text.to_s, :scope => friendly_id_config.scope_for(self),
49
- :sluggable => self
50
- @new_friendly_id = self.slug.to_friendly_id
51
- end
52
-
53
- # Reset the cached friendly_id?
54
- def new_cache_needed?
55
- uses_slug_cache? && slug? && send(friendly_id_config.cache_column) != slug.to_friendly_id
56
- end
57
-
58
- # Reset the cached friendly_id.
59
- def set_slug_cache
60
- if new_cache_needed?
61
- begin
62
- send "#{friendly_id_config.cache_column}=", slug.to_friendly_id
63
- update_without_callbacks
64
- rescue ActiveRecord::StaleObjectError
65
- reload
66
- retry
67
- end
68
- end
69
- end
70
-
71
- def update_scope
72
- return unless slug && scope_changed?
73
- self.class.transaction do
74
- slug.scope = send(friendly_id_config.scope).to_param
75
- similar = Slug.similar_to(slug)
76
- if !similar.empty?
77
- slug.sequence = similar.first.sequence.succ
78
- end
79
- slug.save!
80
- end
81
- end
82
-
83
- # Update the slugs for any model that is using this model as its
84
- # FriendlyId scope.
85
- def update_dependent_scopes
86
- return unless friendly_id_config.class.scopes_used?
87
- if slugs(true).size > 1 && @new_friendly_id
88
- friendly_id_config.child_scopes.each do |klass|
89
- Slug.update_all "scope = '#{@new_friendly_id}'", ["sluggable_type = ? AND scope = ?",
90
- klass.to_s, slugs.second.to_friendly_id]
91
- end
92
- end
93
- end
94
-
95
- # Does the model use slug caching?
96
- def uses_slug_cache?
97
- friendly_id_config.cache_column?
98
- end
99
-
100
- # This method was removed in ActiveRecord 3.0.
101
- if !ActiveRecord::Base.private_method_defined? :update_without_callbacks
102
- def update_without_callbacks
103
- attributes_with_values = arel_attributes_values(false, false, attribute_names)
104
- return false if attributes_with_values.empty?
105
- self.class.unscoped.where(self.class.arel_table[self.class.primary_key].eq(id)).arel.update(attributes_with_values)
106
- end
107
- end
108
- end
109
- end
110
- end
@@ -1,72 +0,0 @@
1
- module FriendlyId
2
- class TaskRunner
3
-
4
- extend Forwardable
5
-
6
- attr_accessor :days
7
- attr_accessor :klass
8
- attr_accessor :task_options
9
-
10
- def_delegators :klass, :find, :friendly_id_config, :update_all
11
-
12
- OLD_SLUG_DAYS = 45
13
-
14
- def initialize(&block)
15
- self.klass = ENV["MODEL"]
16
- self.days = ENV["DAYS"]
17
- end
18
-
19
- def days=(days)
20
- @days ||= days.blank? ? OLD_SLUG_DAYS : days.to_i
21
- end
22
-
23
- def klass=(klass)
24
- @klass ||= klass.to_s.classify.constantize unless klass.blank?
25
- end
26
-
27
- def make_slugs
28
- validate_uses_slugs
29
- options = {
30
- :include => :slug,
31
- :limit => (ENV["LIMIT"] || 100).to_i,
32
- :offset => 0,
33
- :order => ENV["ORDER"] || "#{klass.table_name}.#{klass.primary_key} ASC",
34
- }.merge(task_options || {})
35
-
36
- while records = find(:all, options) do
37
- break if records.size == 0
38
- records.each do |record|
39
- record.save(:validate => false) unless record.slug?
40
- yield(record) if block_given?
41
- end
42
- options[:offset] += options[:limit]
43
- end
44
- end
45
-
46
- def delete_slugs
47
- validate_uses_slugs
48
- Slug.destroy_all(["sluggable_type = ?", klass.to_s])
49
- if column = friendly_id_config.cache_column
50
- update_all("#{column} = NULL")
51
- end
52
- end
53
-
54
- def delete_old_slugs
55
- conditions = ["created_at < ?", DateTime.now - days]
56
- if klass
57
- conditions[0] << " AND sluggable_type = ?"
58
- conditions << klass.to_s
59
- end
60
- Slug.all(:conditions => conditions).select(&:outdated?).map(&:destroy)
61
- end
62
-
63
- def validate_uses_slugs
64
- (raise "You need to pass a MODEL=<model name> argument to rake") if klass.blank?
65
- unless friendly_id_config.use_slug?
66
- raise "Class '%s' doesn't use slugs" % klass.to_s
67
- end
68
- rescue NoMethodError
69
- raise "Class '%s' doesn't use FriendlyId" % klass.to_s
70
- end
71
- end
72
- end
@@ -1,5 +0,0 @@
1
- begin
2
- require 'friendly_id_datamapper'
3
- rescue LoadError
4
- raise "To use FriendlyId's DataMapper adapter, please `gem install friendly_id_datamapper`"
5
- end
@@ -1,22 +0,0 @@
1
- module FriendlyId
2
- class Railtie < Rails::Railtie
3
-
4
- initializer "friendly_id.configure_rails_initialization" do |app|
5
- # Experimental Sequel support. See: http://github.com/norman/friendly_id_sequel
6
- if app.config.generators.rails[:orm] == :sequel
7
- require "friendly_id/sequel"
8
- # Experimental DataMapper support. See: http://github.com/myabc/friendly_id_datamapper
9
- elsif app.config.generators.rails[:orm] == :data_mapper
10
- require 'friendly_id/datamapper'
11
- else
12
- # AR is the default.
13
- require "friendly_id/active_record"
14
- end
15
- end
16
-
17
- rake_tasks do
18
- load "tasks/friendly_id.rake"
19
- end
20
-
21
- end
22
- end
@@ -1,5 +0,0 @@
1
- begin
2
- require "friendly_id_sequel"
3
- rescue LoadError
4
- raise "To use FriendlyId's Sequel adapter, please `gem install friendly_id_sequel`"
5
- end
@@ -1,25 +0,0 @@
1
- # encoding: utf-8
2
- module FriendlyId
3
-
4
- class SlugString < Babosa::Identifier
5
- # Normalize the string for a given {FriendlyId::Configuration}.
6
- # @param config [FriendlyId::Configuration]
7
- # @return String
8
- def normalize_for!(config)
9
- normalize!(config.babosa_options)
10
- end
11
-
12
- # Validate that the slug string is not blank or reserved, and truncate
13
- # it to the max length if necessary.
14
- # @param config [FriendlyId::Configuration]
15
- # @return String
16
- # @raise FriendlyId::BlankError
17
- # @raise FriendlyId::ReservedError
18
- def validate_for!(config)
19
- truncate_bytes!(config.max_length)
20
- raise FriendlyId::BlankError if empty?
21
- raise FriendlyId::ReservedError if config.reserved?(self)
22
- self
23
- end
24
- end
25
- end
@@ -1,35 +0,0 @@
1
- module FriendlyId
2
-
3
- # FriendlyId::Status presents information about the status of the
4
- # id that was used to find the model. This class can be useful for figuring
5
- # out when to redirect to a new URL.
6
- class Status
7
-
8
- # The id or name used as the finder argument
9
- attr_accessor :name
10
-
11
- # The found result, if any
12
- attr_accessor :record
13
-
14
- def initialize(options={})
15
- options.each {|key, value| self.send("#{key}=".to_sym, value)}
16
- end
17
-
18
- # Did the find operation use a friendly id?
19
- def friendly?
20
- !! name
21
- end
22
-
23
- # Did the find operation use a numeric id?
24
- def numeric?
25
- !friendly?
26
- end
27
-
28
- # Did the find operation use the best available id?
29
- def best?
30
- record.friendly_id ? friendly? : true
31
- end
32
-
33
- end
34
-
35
- end
@@ -1,364 +0,0 @@
1
- # encoding: utf-8
2
- Module.send :include, Module.new {
3
- def test(name, &block)
4
- define_method("test_#{name.gsub(/[^a-z0-9]/i, "_")}".to_sym, &block)
5
- end
6
- alias :should :test
7
- }
8
-
9
- module FriendlyId
10
- module Test
11
-
12
- # Tests for any model that implements FriendlyId. Any test that tests model
13
- # features should include this module.
14
- module Generic
15
-
16
- def setup
17
- klass.send delete_all_method
18
- end
19
-
20
- def teardown
21
- klass.send delete_all_method
22
- end
23
-
24
- def instance
25
- raise NotImplementedError
26
- end
27
-
28
- def klass
29
- raise NotImplementedError
30
- end
31
-
32
- def other_class
33
- raise NotImplementedError
34
- end
35
-
36
- def find_method
37
- raise NotImplementedError
38
- end
39
-
40
- def create_method
41
- raise NotImplementedError
42
- end
43
-
44
- def update_method
45
- raise NotImplementedError
46
- end
47
-
48
- def validation_exceptions
49
- return RuntimeError
50
- end
51
-
52
- def assert_validation_error
53
- if validation_exceptions
54
- assert_raise(*[validation_exceptions].flatten) do
55
- yield
56
- end
57
- else # DataMapper does not raise Validation Errors
58
- i = yield
59
- if i.kind_of?(TrueClass) || i.kind_of?(FalseClass)
60
- assert !i
61
- else
62
- instance = i
63
- assert !instance.errors.empty?
64
- end
65
- end
66
- end
67
-
68
- test "models should have a friendly id config" do
69
- assert_not_nil klass.friendly_id_config
70
- end
71
-
72
- test "instances should have a friendly id by default" do
73
- assert_not_nil instance.friendly_id
74
- end
75
-
76
- test "instances should have a friendly id status" do
77
- assert_not_nil instance.friendly_id_status
78
- end
79
-
80
- test "instances should be findable by their friendly id" do
81
- assert_equal instance, klass.send(find_method, instance.friendly_id)
82
- end
83
-
84
- test "instances should be findable by their numeric id as an integer" do
85
- assert_equal instance, klass.send(find_method, instance.id.to_i)
86
- end
87
-
88
- test "instances should be findable by their numeric id as a string" do
89
- assert_equal instance, klass.send(find_method, instance.id.to_s)
90
- end
91
-
92
- test "instances should be findable by a numeric friendly_id" do
93
- instance = klass.send(create_method, :name => "206")
94
- assert_equal instance, klass.send(find_method, "206")
95
- end
96
-
97
- test "creation should raise an error if the friendly_id text is reserved" do
98
- assert_validation_error do
99
- klass.send(create_method, :name => "new")
100
- end
101
- end
102
-
103
- test "creation should raise an error if the friendly_id text is an empty string" do
104
- assert_validation_error do
105
- klass.send(create_method, :name => "")
106
- end
107
- end
108
-
109
- test "creation should raise an error if the friendly_id text is a blank string" do
110
- assert_validation_error do
111
- klass.send(create_method, :name => " ")
112
- end
113
- end
114
-
115
- test "creation should raise an error if the friendly_id text is nil and allow_nil is false" do
116
- assert_validation_error do
117
- klass.send(create_method, :name => nil)
118
- end
119
- end
120
-
121
- test "creation should succeed if the friendly_id text is nil and allow_nil is true" do
122
- klass.friendly_id_config.stubs(:allow_nil?).returns(true)
123
- assert klass.send(create_method, :name => nil)
124
- end
125
-
126
- test "creation should succeed if the friendly_id text is empty and allow_nil is true" do
127
- klass.friendly_id_config.stubs(:allow_nil?).returns(true)
128
- begin
129
- assert klass.send(create_method, :name => "")
130
- rescue ActiveRecord::RecordInvalid => e
131
- raise unless e.message =~ /Name can't be blank/ # Test not applicable in this case
132
- end
133
- end
134
-
135
- test "creation should succeed if the friendly_id text converts to empty and allow_nil is true" do
136
- klass.friendly_id_config.stubs(:allow_nil?).returns(true)
137
- assert klass.send(create_method, :name => "*")
138
- end
139
-
140
- test "should allow the same friendly_id across models" do
141
- other_instance = other_class.send(create_method, :name => instance.name)
142
- assert_equal other_instance.friendly_id, instance.friendly_id
143
- end
144
-
145
- test "reserved words can be specified as a regular expression" do
146
- klass.friendly_id_config.stubs(:reserved_words).returns(/jo/)
147
- assert_validation_error do
148
- klass.send(create_method, :name => "joe")
149
- end
150
- end
151
-
152
- test "should not raise reserved error unless regexp matches" do
153
- klass.friendly_id_config.stubs(:reserved_words).returns(/ddsadad/)
154
- assert_nothing_raised do
155
- klass.send(create_method, :name => "joe")
156
- end
157
- end
158
-
159
- end
160
-
161
- module Simple
162
-
163
- test "should allow friendly_id to be nillable if allow_nil is true" do
164
- klass.friendly_id_config.stubs(:allow_nil?).returns(true)
165
- instance = klass.send(create_method, :name => "hello")
166
- assert instance.friendly_id
167
- instance.name = nil
168
- assert instance.send(save_method)
169
- end
170
-
171
- end
172
-
173
- # Tests for any model that implements slugs.
174
- module Slugged
175
-
176
- test "should have a slug" do
177
- assert_not_nil instance.slug
178
- end
179
-
180
- test "should not make a new slug unless the friendly_id method value has changed" do
181
- instance.note = instance.note.to_s << " updated"
182
- instance.send save_method
183
- assert_equal 1, instance.slugs.size
184
- end
185
-
186
- test "should make a new slug if the friendly_id method value has changed" do
187
- instance.name = "Changed title"
188
- instance.send save_method
189
- slugs = if instance.slugs.respond_to?(:reload)
190
- instance.slugs.reload
191
- else
192
- instance.slugs(true)
193
- end
194
- assert_equal 2, slugs.size
195
- end
196
-
197
- test "should be able to reuse an old friendly_id without incrementing the sequence" do
198
- old_title = instance.name
199
- old_friendly_id = instance.friendly_id
200
- instance.name = "A changed title"
201
- instance.send save_method
202
- instance.name = old_title
203
- instance.send save_method
204
- assert_equal old_friendly_id, instance.friendly_id
205
- end
206
-
207
- test "should increment the slug sequence for duplicate friendly ids" do
208
- instance2 = klass.send(create_method, :name => instance.name)
209
- assert_match(/2\z/, instance2.friendly_id)
210
- end
211
-
212
- test "should find instance with a sequenced friendly_id" do
213
- instance2 = klass.send(create_method, :name => instance.name)
214
- assert_equal instance2, klass.send(find_method, instance2.friendly_id)
215
- end
216
-
217
- test "should indicate correct status when found with a sequence" do
218
- instance2 = klass.send(create_method, :name => instance.name)
219
- instance2 = klass.send(find_method, instance2.friendly_id)
220
- assert instance2.friendly_id_status.best?
221
- end
222
-
223
- test "should indicate correct status when found by a numeric friendly_id" do
224
- instance = klass.send(create_method, :name => "100")
225
- instance2 = klass.send(find_method, "100")
226
- assert instance2.friendly_id_status.best?, "status expected to be best but isn't."
227
- assert instance2.friendly_id_status.current?, "status expected to be current but isn't."
228
- end
229
-
230
- test "should remain findable by previous slugs" do
231
- old_friendly_id = instance.friendly_id
232
- instance.name = "#{old_friendly_id} updated"
233
- instance.send(save_method)
234
- assert_not_equal old_friendly_id, instance.friendly_id
235
- assert_equal instance, klass.send(find_method, old_friendly_id)
236
- end
237
-
238
- test "should not create a slug when allow_nil is true and friendy_id text is blank" do
239
- klass.friendly_id_config.stubs(:allow_nil?).returns(true)
240
- instance = klass.send(create_method, :name => nil)
241
- assert_nil instance.slug
242
- end
243
-
244
- test "should not allow friendly_id to be nillable even if allow_nil is true" do
245
- klass.friendly_id_config.stubs(:allow_nil?).returns(true)
246
- instance = klass.send(create_method, :name => "hello")
247
- assert instance.friendly_id
248
- instance.name = nil
249
- assert_validation_error do
250
- instance.send(save_method)
251
- end
252
- end
253
-
254
- test "should approximate ascii if configured" do
255
- klass.friendly_id_config.stubs(:approximate_ascii?).returns(true)
256
- instance = klass.send(create_method, :name => "Cañón")
257
- assert_equal "canon", instance.friendly_id
258
- end
259
-
260
- test "should approximate ascii with options if configured" do
261
- klass.friendly_id_config.stubs(:approximate_ascii?).returns(true)
262
- klass.friendly_id_config.stubs(:ascii_approximation_options).returns(:spanish)
263
- instance = klass.send(create_method, :name => "Cañón")
264
- assert_equal "canion", instance.friendly_id
265
- end
266
- end
267
-
268
- # Tests for FriendlyId::Status.
269
- module Status
270
-
271
- test "should default to not friendly" do
272
- assert !status.friendly?
273
- end
274
-
275
- test "should default to numeric" do
276
- assert status.numeric?
277
- end
278
-
279
- end
280
-
281
- # Tests for FriendlyId::Status for a model that uses slugs.
282
- module SluggedStatus
283
-
284
- test "should be friendly if slug is set" do
285
- status.slug = Slug.new
286
- assert status.friendly?
287
- end
288
-
289
- test "should be friendly if name is set" do
290
- status.name = "name"
291
- assert status.friendly?
292
- end
293
-
294
- test "should be current if current slug is set" do
295
- status.slug = instance.slug
296
- assert status.current?
297
- end
298
-
299
- test "should not be current if non-current slug is set" do
300
- status.slug = Slug.new(:sluggable => instance)
301
- assert !status.current?
302
- end
303
-
304
- test "should be best if it is current" do
305
- status.slug = instance.slug
306
- assert status.best?
307
- end
308
-
309
- test "should be best if it is numeric, but record has no slug" do
310
- instance.slugs = []
311
- instance.slug = nil
312
- assert status.best?
313
- end
314
-
315
- [:record, :name].each do |symbol|
316
- test "should have #{symbol} after find using friendly_id" do
317
- instance2 = klass.send(find_method, instance.friendly_id)
318
- assert_not_nil instance2.friendly_id_status.send(symbol)
319
- end
320
- end
321
-
322
- def status
323
- @status ||= instance.friendly_id_status
324
- end
325
-
326
- def klass
327
- raise NotImplementedError
328
- end
329
-
330
- def instance
331
- raise NotImplementedError
332
- end
333
-
334
- end
335
-
336
- # Tests for models to ensure that they properly implement using the
337
- # +normalize_friendly_id+ method to allow developers to hook into the
338
- # slug string generation.
339
- module CustomNormalizer
340
-
341
- test "should invoke the custom normalizer" do
342
- assert_equal "JOE SCHMOE", klass.send(create_method, :name => "Joe Schmoe").friendly_id
343
- end
344
-
345
- test "should respect the max_length option" do
346
- klass.friendly_id_config.stubs(:max_length).returns(3)
347
- assert_equal "JOE", klass.send(create_method, :name => "Joe Schmoe").friendly_id
348
- end
349
-
350
- test "should raise an error if the friendly_id text is reserved" do
351
- klass.friendly_id_config.stubs(:reserved_words).returns(["JOE"])
352
- if validation_exceptions
353
- assert_raise(*[validation_exceptions].flatten) do
354
- klass.send(create_method, :name => "Joe")
355
- end
356
- else # DataMapper does not raise Validation Errors
357
- instance = klass.send(create_method, :name => "Joe")
358
- assert !instance.errors.empty?
359
- end
360
- end
361
-
362
- end
363
- end
364
- end
@@ -1,9 +0,0 @@
1
- module FriendlyId
2
- module Version
3
- MAJOR = 3
4
- MINOR = 3
5
- TINY = 0
6
- BUILD = 'rc2'
7
- STRING = [MAJOR, MINOR, TINY, BUILD].compact.join('.')
8
- end
9
- end