mil_friendly_id 4.0.9.8

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.
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