mil_friendly_id 4.0.9.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/.gitignore +12 -0
  4. data/.travis.yml +20 -0
  5. data/.yardopts +4 -0
  6. data/Changelog.md +86 -0
  7. data/Gemfile +15 -0
  8. data/Guide.rdoc +553 -0
  9. data/MIT-LICENSE +19 -0
  10. data/README.md +150 -0
  11. data/Rakefile +108 -0
  12. data/WhatsNew.md +95 -0
  13. data/bench.rb +63 -0
  14. data/friendly_id.gemspec +43 -0
  15. data/gemfiles/Gemfile.rails-3.0.rb +21 -0
  16. data/gemfiles/Gemfile.rails-3.1.rb +22 -0
  17. data/gemfiles/Gemfile.rails-3.2.rb +22 -0
  18. data/geothird_friendly_id.gemspec +45 -0
  19. data/lib/friendly_id.rb +114 -0
  20. data/lib/friendly_id/base.rb +291 -0
  21. data/lib/friendly_id/configuration.rb +80 -0
  22. data/lib/friendly_id/finder_methods.rb +35 -0
  23. data/lib/friendly_id/globalize.rb +115 -0
  24. data/lib/friendly_id/history.rb +134 -0
  25. data/lib/friendly_id/migration.rb +19 -0
  26. data/lib/friendly_id/object_utils.rb +50 -0
  27. data/lib/friendly_id/reserved.rb +68 -0
  28. data/lib/friendly_id/scoped.rb +149 -0
  29. data/lib/friendly_id/simple_i18n.rb +95 -0
  30. data/lib/friendly_id/slug.rb +14 -0
  31. data/lib/friendly_id/slug_generator.rb +80 -0
  32. data/lib/friendly_id/slugged.rb +329 -0
  33. data/lib/generators/friendly_id_generator.rb +17 -0
  34. data/mil_friendly_id.gemspec +45 -0
  35. data/test/base_test.rb +72 -0
  36. data/test/compatibility/ancestry/Gemfile +8 -0
  37. data/test/compatibility/ancestry/ancestry_test.rb +34 -0
  38. data/test/compatibility/threading/Gemfile +8 -0
  39. data/test/compatibility/threading/threading.rb +45 -0
  40. data/test/configuration_test.rb +48 -0
  41. data/test/core_test.rb +48 -0
  42. data/test/databases.yml +19 -0
  43. data/test/generator_test.rb +20 -0
  44. data/test/globalize_test.rb +57 -0
  45. data/test/helper.rb +87 -0
  46. data/test/history_test.rb +149 -0
  47. data/test/object_utils_test.rb +28 -0
  48. data/test/reserved_test.rb +40 -0
  49. data/test/schema.rb +79 -0
  50. data/test/scoped_test.rb +83 -0
  51. data/test/shared.rb +156 -0
  52. data/test/simple_i18n_test.rb +133 -0
  53. data/test/slugged_test.rb +280 -0
  54. data/test/sti_test.rb +77 -0
  55. metadata +262 -0
@@ -0,0 +1,80 @@
1
+ module FriendlyId
2
+ # The configuration paramters passed to +friendly_id+ will be stored in
3
+ # this object.
4
+ class Configuration
5
+
6
+ # The base column or method used by FriendlyId as the basis of a friendly id
7
+ # or slug.
8
+ #
9
+ # For models that don't use FriendlyId::Slugged, the base is the column that
10
+ # is used as the FriendlyId directly. For models using FriendlyId::Slugged,
11
+ # the base is a column or method whose value is used as the basis of the
12
+ # slug.
13
+ #
14
+ # For example, if you have a model representing blog posts and that uses
15
+ # slugs, you likely will want to use the "title" attribute as the base, and
16
+ # FriendlyId will take care of transforming the human-readable title into
17
+ # something suitable for use in a URL.
18
+ #
19
+ # @param [Symbol] A symbol referencing a column or method in the model. This
20
+ # value is usually set by passing it as the first argument to
21
+ # {FriendlyId::Base#friendly_id friendly_id}:
22
+ #
23
+ # @example
24
+ # class Book < ActiveRecord::Base
25
+ # extend FriendlyId
26
+ # friendly_id :name
27
+ # end
28
+ attr_accessor :base
29
+
30
+ # The default configuration options.
31
+ attr_reader :defaults
32
+
33
+ # The model class that this configuration belongs to.
34
+ # @return ActiveRecord::Base
35
+ attr_accessor :model_class
36
+
37
+ def initialize(model_class, values = nil)
38
+ @model_class = model_class
39
+ @defaults = {}
40
+ set values
41
+ end
42
+
43
+ # Lets you specify the modules to use with FriendlyId.
44
+ #
45
+ # This method is invoked by {FriendlyId::Base#friendly_id friendly_id} when
46
+ # passing the +:use+ option, or when using {FriendlyId::Base#friendly_id
47
+ # friendly_id} with a block.
48
+ #
49
+ # @example
50
+ # class Book < ActiveRecord::Base
51
+ # extend FriendlyId
52
+ # friendly_id :name, :use => :slugged
53
+ # end
54
+ # @param [#to_s,Module] *modules Arguments should be Modules, or symbols or
55
+ # strings that correspond with the name of a module inside the FriendlyId
56
+ # namespace. By default FriendlyId provides +:slugged+, +:history+,
57
+ # +:simple_i18n+, +:globalize+, and +:scoped+.
58
+ def use(*modules)
59
+ modules.to_a.flatten.compact.map do |object|
60
+ mod = object.kind_of?(Module) ? object : FriendlyId.const_get(object.to_s.classify)
61
+ model_class.send(:include, mod)
62
+ end
63
+ end
64
+
65
+ # The column that FriendlyId will use to find the record when querying by
66
+ # friendly id.
67
+ #
68
+ # This method is generally only used internally by FriendlyId.
69
+ # @return String
70
+ def query_field
71
+ base.to_s
72
+ end
73
+
74
+ private
75
+
76
+ def set(values)
77
+ values and values.each {|name, value| self.send "#{name}=", value}
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,35 @@
1
+ module FriendlyId
2
+ # These methods will be added to the model's {FriendlyId::Base#relation_class relation_class}.
3
+ module FinderMethods
4
+
5
+ protected
6
+
7
+ # FriendlyId overrides this method to make it possible to use friendly id's
8
+ # identically to numeric ids in finders.
9
+ #
10
+ # @example
11
+ # person = Person.find(123)
12
+ # person = Person.find("joe")
13
+ #
14
+ # @see FriendlyId::ObjectUtils
15
+ def find_one(id)
16
+ return super if id.unfriendly_id?
17
+ where(@klass.friendly_id_config.query_field => id).first or super
18
+ end
19
+
20
+ # FriendlyId overrides this method to make it possible to use friendly id's
21
+ # identically to numeric ids in finders.
22
+ #
23
+ # @example
24
+ # person = Person.exists?(123)
25
+ # person = Person.exists?("joe")
26
+ # person = Person.exists?({:name => 'joe'})
27
+ # person = Person.exists?(['name = ?', 'joe'])
28
+ #
29
+ # @see FriendlyId::ObjectUtils
30
+ def exists?(id = false)
31
+ return super if id.unfriendly_id?
32
+ super @klass.friendly_id_config.query_field => id
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,115 @@
1
+ require 'i18n'
2
+
3
+ module FriendlyId
4
+
5
+ =begin
6
+
7
+ == Translating Slugs Using Globalize
8
+
9
+ The {FriendlyId::Globalize Globalize} module lets you use
10
+ Globalize[https://github.com/svenfuchs/globalize3] to translate slugs. This
11
+ module is most suitable for applications that need to be localized to many
12
+ languages. If your application only needs to be localized to one or two
13
+ languages, you may wish to consider the {FriendlyId::SimpleI18n SimpleI18n}
14
+ module.
15
+
16
+ In order to use this module, your model's table and translation table must both
17
+ have a slug column, and your model must set the +slug+ field as translatable
18
+ with Globalize:
19
+
20
+ class Post < ActiveRecord::Base
21
+ translates :title, :slug
22
+ extend FriendlyId
23
+ friendly_id :title, :use => :globalize
24
+ end
25
+
26
+ === Finds
27
+
28
+ Finds will take the current locale into consideration:
29
+
30
+ I18n.locale = :it
31
+ Post.find("guerre-stellari")
32
+ I18n.locale = :en
33
+ Post.find("star-wars")
34
+
35
+ Additionally, finds will fall back to the default locale:
36
+
37
+ I18n.locale = :it
38
+ Post.find("star-wars")
39
+
40
+ To find a slug by an explicit locale, perform the find inside a block
41
+ passed to I18n's +with_locale+ method:
42
+
43
+ I18n.with_locale(:it) { Post.find("guerre-stellari") }
44
+
45
+ === Creating Records
46
+
47
+ When new records are created, the slug is generated for the current locale only.
48
+
49
+ === Translating Slugs
50
+
51
+ To translate an existing record's friendly_id, use
52
+ {FriendlyId::Globalize::Model#set_friendly_id}. This will ensure that the slug
53
+ you add is properly escaped, transliterated and sequenced:
54
+
55
+ post = Post.create :name => "Star Wars"
56
+ post.set_friendly_id("Guerre stellari", :it)
57
+
58
+ If you don't pass in a locale argument, FriendlyId::Globalize will just use the
59
+ current locale:
60
+
61
+ I18n.with_locale(:it) { post.set_friendly_id("Guerre stellari") }
62
+
63
+ =end
64
+ module Globalize
65
+
66
+ def self.included(model_class)
67
+ model_class.instance_eval do
68
+ friendly_id_config.use :slugged
69
+ relation_class.send :include, FinderMethods
70
+ include Model
71
+ # Check if slug field is enabled to be translated with Globalize
72
+ unless respond_to?('translated_attribute_names') || translated_attribute_names.exclude?(friendly_id_config.query_field.to_sym)
73
+ puts "\n[FriendlyId] You need to translate '#{friendly_id_config.query_field}' field with Globalize (add 'translates :#{friendly_id_config.query_field}' in your model '#{self.class.name}')\n\n"
74
+ end
75
+ end
76
+ end
77
+
78
+ module Model
79
+ def set_friendly_id(text, locale)
80
+ I18n.with_locale(locale || I18n.locale) do
81
+ set_slug(normalize_friendly_id(text))
82
+ end
83
+ end
84
+ end
85
+
86
+ module FinderMethods
87
+ # FriendlyId overrides this method to make it possible to use friendly id's
88
+ # identically to numeric ids in finders.
89
+ #
90
+ # @example
91
+ # person = Person.find(123)
92
+ # person = Person.find("joe")
93
+ #
94
+ # @see FriendlyId::ObjectUtils
95
+ def find_one(id)
96
+ return super if id.unfriendly_id?
97
+ found = where(@klass.friendly_id_config.query_field => id).first
98
+ found = includes(:translations).
99
+ where(translation_class.arel_table[:locale].in([I18n.locale, I18n.default_locale])).
100
+ where(translation_class.arel_table[@klass.friendly_id_config.query_field].eq(id)).first if found.nil?
101
+
102
+ if found
103
+ # Reload the translations for the found records.
104
+ found.tap { |f| f.translations.reload }
105
+ else
106
+ # if locale is not translated fallback to default locale
107
+ super
108
+ end
109
+ end
110
+
111
+ protected :find_one
112
+
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,134 @@
1
+ module FriendlyId
2
+
3
+ =begin
4
+
5
+ == History: Avoiding 404's When Slugs Change
6
+
7
+ FriendlyId's {FriendlyId::History History} module adds the ability to store a
8
+ log of a model's slugs, so that when its friendly id changes, it's still
9
+ possible to perform finds by the old id.
10
+
11
+ The primary use case for this is avoiding broken URLs.
12
+
13
+ === Setup
14
+
15
+ In order to use this module, you must add a table to your database schema to
16
+ store the slug records. FriendlyId provides a generator for this purpose:
17
+
18
+ rails generate friendly_id
19
+ rake db:migrate
20
+
21
+ This will add a table named +friendly_id_slugs+, used by the {FriendlyId::Slug}
22
+ model.
23
+
24
+ === Considerations
25
+
26
+ This module is incompatible with the +:scoped+ module.
27
+
28
+ Because recording slug history requires creating additional database records,
29
+ this module has an impact on the performance of the associated model's +create+
30
+ method.
31
+
32
+ === Example
33
+
34
+ class Post < ActiveRecord::Base
35
+ extend FriendlyId
36
+ friendly_id :title, :use => :history
37
+ end
38
+
39
+ class PostsController < ApplicationController
40
+
41
+ before_filter :find_post
42
+
43
+ ...
44
+
45
+ def find_post
46
+ @post = Post.find params[:id]
47
+
48
+ # If an old id or a numeric id was used to find the record, then
49
+ # the request path will not match the post_path, and we should do
50
+ # a 301 redirect that uses the current friendly id.
51
+ if request.path != post_path(@post)
52
+ return redirect_to @post, :status => :moved_permanently
53
+ end
54
+ end
55
+ end
56
+ =end
57
+ module History
58
+
59
+ # Configures the model instance to use the History add-on.
60
+ def self.included(model_class)
61
+ model_class.instance_eval do
62
+ raise "FriendlyId::History is incompatible with FriendlyId::Scoped" if self < Scoped
63
+ @friendly_id_config.use :slugged
64
+ has_many :slugs, :as => :sluggable, :dependent => :destroy,
65
+ :class_name => Slug.to_s, :order => "#{Slug.quoted_table_name}.id DESC"
66
+ after_save :create_slug
67
+ relation_class.send :include, FinderMethods
68
+ friendly_id_config.slug_generator_class.send :include, SlugGenerator
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def create_slug
75
+ return unless friendly_id
76
+ return if slugs.first.try(:slug) == friendly_id
77
+ # Allow reversion back to a previously used slug
78
+ relation = slugs.where(:slug => friendly_id)
79
+ result = relation.select("id").lock(true).all
80
+ relation.delete_all unless result.empty?
81
+ slugs.create! do |record|
82
+ record.slug = friendly_id
83
+ end
84
+ end
85
+
86
+ # Adds a finder that explictly uses slugs from the slug table.
87
+ module FinderMethods
88
+
89
+ # Search for a record in the slugs table using the specified slug.
90
+ def find_one(id)
91
+ return super(id) if id.unfriendly_id?
92
+ where(@klass.friendly_id_config.query_field => id).first or
93
+ with_old_friendly_id(id) {|x| find_one_without_friendly_id(x)} or
94
+ find_one_without_friendly_id(id)
95
+ end
96
+
97
+ # Search for a record in the slugs table using the specified slug.
98
+ def exists?(id = false)
99
+ return super if id.unfriendly_id?
100
+ exists_without_friendly_id?(@klass.friendly_id_config.query_field => id) or
101
+ with_old_friendly_id(id) {|x| exists_without_friendly_id?(x)} or
102
+ exists_without_friendly_id?(id)
103
+ end
104
+
105
+ private
106
+
107
+ # Accepts a slug, and yields a corresponding sluggable_id into the block.
108
+ def with_old_friendly_id(slug, &block)
109
+ sql = "SELECT sluggable_id FROM #{Slug.quoted_table_name} WHERE sluggable_type = %s AND slug = %s"
110
+ sql = sql % [@klass.base_class.to_s, slug].map {|x| connection.quote(x)}
111
+ sluggable_id = connection.select_values(sql).first
112
+ yield sluggable_id if sluggable_id
113
+ end
114
+ end
115
+
116
+ # This module overrides {FriendlyId::SlugGenerator#conflicts} to consider
117
+ # all historic slugs for that model.
118
+ module SlugGenerator
119
+
120
+ private
121
+
122
+ def conflicts
123
+ sluggable_class = friendly_id_config.model_class.base_class
124
+ pkey = sluggable_class.primary_key
125
+ value = sluggable.send pkey
126
+
127
+ scope = Slug.with_deleted.where("slug = ? OR slug LIKE ?", normalized, wildcard)
128
+ scope = scope.where(:sluggable_type => sluggable_class.to_s)
129
+ scope = scope.where("sluggable_id <> ?", value) unless sluggable.new_record?
130
+ scope.order("LENGTH(slug) DESC, slug DESC")
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,19 @@
1
+ class CreateSlugs < ActiveRecord::Migration
2
+
3
+ def self.up
4
+ create_table :slugs do |t|
5
+ t.string :slug, :null => false
6
+ t.integer :sluggable_id, :null => false
7
+ t.string :sluggable_type, :limit => 40
8
+ t.datetime :created_at
9
+ t.deleted_at :boolean
10
+ end
11
+ add_index :slugs, :sluggable_id
12
+ add_index :slugs, [:slug, :sluggable_type], :unique => true
13
+ add_index :slugs, :sluggable_type
14
+ end
15
+
16
+ def self.down
17
+ drop_table :slugs
18
+ end
19
+ end
@@ -0,0 +1,50 @@
1
+ module FriendlyId
2
+ # Utility methods for determining whether any object is a friendly id.
3
+ #
4
+ # Monkey-patching Object is a somewhat extreme measure not to be taken lightly
5
+ # by libraries, but in this case I decided to do it because to me, it feels
6
+ # cleaner than adding a module method to {FriendlyId}. I've given the methods
7
+ # names that unambigously refer to the library of their origin, which should
8
+ # be sufficient to avoid conflicts with other libraries.
9
+ module ObjectUtils
10
+
11
+ # True is the id is definitely friendly, false if definitely unfriendly,
12
+ # else nil.
13
+ #
14
+ # An object is considired "definitely unfriendly" if its class is or
15
+ # inherits from ActiveRecord::Base, Array, Hash, NilClass, Numeric, or
16
+ # Symbol.
17
+ #
18
+ # An object is considered "definitely friendly" if it responds to +to_i+,
19
+ # and its value when cast to an integer and then back to a string is
20
+ # different from its value when merely cast to a string:
21
+ #
22
+ # 123.friendly_id? #=> false
23
+ # :id.friendly_id? #=> false
24
+ # {:name => 'joe'}.friendly_id? #=> false
25
+ # ['name = ?', 'joe'].friendly_id? #=> false
26
+ # nil.friendly_id? #=> false
27
+ # "123".friendly_id? #=> nil
28
+ # "abc123".friendly_id? #=> true
29
+ def friendly_id?
30
+ # Considered unfriendly if this is an instance of an unfriendly class or
31
+ # one of its descendants.
32
+ unfriendly_classes = [ActiveRecord::Base, Array, Hash, NilClass, Numeric,
33
+ Symbol, TrueClass, FalseClass]
34
+
35
+ if unfriendly_classes.detect {|klass| self.class <= klass}
36
+ false
37
+ elsif respond_to?(:to_i) && to_i.to_s != to_s
38
+ true
39
+ end
40
+ end
41
+
42
+ # True if the id is definitely unfriendly, false if definitely friendly,
43
+ # else nil.
44
+ def unfriendly_id?
45
+ val = friendly_id? ; !val unless val.nil?
46
+ end
47
+ end
48
+ end
49
+
50
+ Object.send :include, FriendlyId::ObjectUtils
@@ -0,0 +1,68 @@
1
+ module FriendlyId
2
+
3
+ =begin
4
+
5
+ == Reserved Words
6
+
7
+ The {FriendlyId::Reserved Reserved} module adds the ability to exlude a list of
8
+ words from use as FriendlyId slugs.
9
+
10
+ By default, FriendlyId reserves the words "new" and "edit" when this module is
11
+ included. You can configure this globally by using {FriendlyId.defaults
12
+ FriendlyId.defaults}:
13
+
14
+ FriendlyId.defaults do |config|
15
+ config.use :reserved
16
+ # Reserve words for English and Spanish URLs
17
+ config.reserved_words = %w(new edit nueva nuevo editar)
18
+ end
19
+
20
+ Note that the error message will appear on the field +:friendly_id+. If you are
21
+ using Rails's scaffolded form errors display, then it will have no field to
22
+ highlight. If you'd like to change this so that scaffolding works as expected,
23
+ one way to accomplish this is to move the error message to a different field.
24
+ For example:
25
+
26
+ class Person < ActiveRecord::Base
27
+ extend FriendlyId
28
+ friendly_id :name, use: :slugged
29
+
30
+ after_validation :move_friendly_id_error_to_name
31
+
32
+ def move_friendly_id_error_to_name
33
+ errors.add :name, *errors.delete(:friendly_id) if errors[:friendly_id].present?
34
+ end
35
+ end
36
+
37
+ =end
38
+ module Reserved
39
+
40
+ # When included, this module adds configuration options to the model class's
41
+ # friendly_id_config.
42
+ def self.included(model_class)
43
+ model_class.class_eval do
44
+ friendly_id_config.class.send :include, Reserved::Configuration
45
+ friendly_id_config.defaults[:reserved_words] ||= ["new", "edit"]
46
+ end
47
+ end
48
+
49
+ # This module adds the +:reserved_words+ configuration option to
50
+ # {FriendlyId::Configuration FriendlyId::Configuration}.
51
+ module Configuration
52
+ attr_writer :reserved_words
53
+
54
+ # Overrides {FriendlyId::Configuration#base} to add a validation to the
55
+ # model class.
56
+ def base=(base)
57
+ super
58
+ reserved_words = model_class.friendly_id_config.reserved_words
59
+ model_class.validates_exclusion_of :friendly_id, :in => reserved_words
60
+ end
61
+
62
+ # An array of words forbidden as slugs.
63
+ def reserved_words
64
+ @reserved_words ||= @defaults[:reserved_words]
65
+ end
66
+ end
67
+ end
68
+ end