geothird_friendly_id 4.0.9.1

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 +43 -0
  19. data/lib/friendly_id/base.rb +291 -0
  20. data/lib/friendly_id/configuration.rb +80 -0
  21. data/lib/friendly_id/finder_methods.rb +35 -0
  22. data/lib/friendly_id/globalize.rb +115 -0
  23. data/lib/friendly_id/history.rb +134 -0
  24. data/lib/friendly_id/migration.rb +18 -0
  25. data/lib/friendly_id/object_utils.rb +50 -0
  26. data/lib/friendly_id/reserved.rb +68 -0
  27. data/lib/friendly_id/scoped.rb +149 -0
  28. data/lib/friendly_id/simple_i18n.rb +95 -0
  29. data/lib/friendly_id/slug.rb +14 -0
  30. data/lib/friendly_id/slug_generator.rb +80 -0
  31. data/lib/friendly_id/slugged.rb +329 -0
  32. data/lib/friendly_id.rb +114 -0
  33. data/lib/generators/friendly_id_generator.rb +17 -0
  34. data/test/base_test.rb +72 -0
  35. data/test/compatibility/ancestry/Gemfile +8 -0
  36. data/test/compatibility/ancestry/ancestry_test.rb +34 -0
  37. data/test/compatibility/threading/Gemfile +8 -0
  38. data/test/compatibility/threading/Gemfile.lock +37 -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 +247 -0
@@ -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
@@ -0,0 +1,149 @@
1
+ require "friendly_id/slugged"
2
+
3
+ module FriendlyId
4
+
5
+ =begin
6
+
7
+ == Unique Slugs by Scope
8
+
9
+ The {FriendlyId::Scoped} module allows FriendlyId to generate unique slugs
10
+ within a scope.
11
+
12
+ This allows, for example, two restaurants in different cities to have the slug
13
+ +joes-diner+:
14
+
15
+ class Restaurant < ActiveRecord::Base
16
+ extend FriendlyId
17
+ belongs_to :city
18
+ friendly_id :name, :use => :scoped, :scope => :city
19
+ end
20
+
21
+ class City < ActiveRecord::Base
22
+ extend FriendlyId
23
+ has_many :restaurants
24
+ friendly_id :name, :use => :slugged
25
+ end
26
+
27
+ City.find("seattle").restaurants.find("joes-diner")
28
+ City.find("chicago").restaurants.find("joes-diner")
29
+
30
+ Without :scoped in this case, one of the restaurants would have the slug
31
+ +joes-diner+ and the other would have +joes-diner--2+.
32
+
33
+ The value for the +:scope+ option can be the name of a +belongs_to+ relation, or
34
+ a column.
35
+
36
+ === Finding Records by Friendly ID
37
+
38
+ If you are using scopes your friendly ids may not be unique, so a simple find
39
+ like
40
+
41
+ Restaurant.find("joes-diner")
42
+
43
+ may return the wrong record. In these cases it's best to query through the
44
+ relation:
45
+
46
+ @city.restaurants.find("joes-diner")
47
+
48
+ Alternatively, you could pass the scope value as a query parameter:
49
+
50
+ Restaurant.find("joes-diner").where(:city_id => @city.id)
51
+
52
+
53
+ === Finding All Records That Match a Scoped ID
54
+
55
+ Query the slug column directly:
56
+
57
+ Restaurant.find_all_by_slug("joes-diner")
58
+
59
+ === Routes for Scoped Models
60
+
61
+ Recall that FriendlyId is a database-centric library, and does not set up any
62
+ routes for scoped models. You must do this yourself in your application. Here's
63
+ an example of one way to set this up:
64
+
65
+ # in routes.rb
66
+ resources :cities do
67
+ resources :restaurants
68
+ end
69
+
70
+ # in views
71
+ <%= link_to 'Show', [@city, @restaurant] %>
72
+
73
+ # in controllers
74
+ @city = City.find(params[:city_id])
75
+ @restaurant = @city.restaurants.find(params[:id])
76
+
77
+ # URLs:
78
+ http://example.org/cities/seattle/restaurants/joes-diner
79
+ http://example.org/cities/chicago/restaurants/joes-diner
80
+
81
+ =end
82
+ module Scoped
83
+
84
+
85
+ # Sets up behavior and configuration options for FriendlyId's scoped slugs
86
+ # feature.
87
+ def self.included(model_class)
88
+ model_class.instance_eval do
89
+ raise "FriendlyId::Scoped is incompatibe with FriendlyId::History" if self < History
90
+ include Slugged unless self < Slugged
91
+ friendly_id_config.class.send :include, Configuration
92
+ friendly_id_config.slug_generator_class.send :include, SlugGenerator
93
+ end
94
+ end
95
+
96
+ # This module adds the +:scope+ configuration option to
97
+ # {FriendlyId::Configuration FriendlyId::Configuration}.
98
+ module Configuration
99
+
100
+ # Gets the scope value.
101
+ #
102
+ # When setting this value, the argument should be a symbol referencing a
103
+ # +belongs_to+ relation, or a column.
104
+ #
105
+ # @return Symbol The scope value
106
+ attr_accessor :scope
107
+
108
+ # Gets the scope columns.
109
+ #
110
+ # Checks to see if the +:scope+ option passed to
111
+ # {FriendlyId::Base#friendly_id} refers to a relation, and if so, returns
112
+ # the realtion's foreign key. Otherwise it assumes the option value was
113
+ # the name of column and returns it cast to a String.
114
+ #
115
+ # @return String The scope column
116
+ def scope_columns
117
+ [@scope].flatten.map { |s| (reflection_foreign_key(s) or s).to_s }
118
+ end
119
+
120
+ private
121
+
122
+ if ActiveRecord::VERSION::STRING < "3.1"
123
+ def reflection_foreign_key(scope)
124
+ model_class.reflections[scope].try(:primary_key_name)
125
+ end
126
+ else
127
+ def reflection_foreign_key(scope)
128
+ model_class.reflections[scope].try(:foreign_key)
129
+ end
130
+ end
131
+ end
132
+
133
+ # This module overrides {FriendlyId::SlugGenerator#conflict} to consider
134
+ # scope, to avoid adding sequences to slugs under different scopes.
135
+ module SlugGenerator
136
+
137
+ private
138
+
139
+ def conflict
140
+ columns = friendly_id_config.scope_columns
141
+ matched = columns.inject(conflicts) do |memo, column|
142
+ memo.where(column => sluggable.send(column))
143
+ end
144
+
145
+ matched.first
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,95 @@
1
+ require "i18n"
2
+
3
+ module FriendlyId
4
+
5
+ =begin
6
+
7
+ == Translating Slugs Using Simple I18n
8
+
9
+ The {FriendlyId::SimpleI18n SimpleI18n} module adds very basic i18n support to
10
+ FriendlyId.
11
+
12
+ In order to use this module, your model must have a slug column for each locale.
13
+ By default FriendlyId looks for columns named, for example, "slug_en",
14
+ "slug_es", etc. The first part of the name can be configured by passing the
15
+ +:slug_column+ option if you choose. Note that the column for the default locale
16
+ must also include the locale in its name.
17
+
18
+ This module is most suitable to applications that need to support few locales.
19
+ If you need to support two or more locales, you may wish to use the
20
+ {FriendlyId::Globalize Globalize} module instead.
21
+
22
+ === Example migration
23
+
24
+ def self.up
25
+ create_table :posts do |t|
26
+ t.string :title
27
+ t.string :slug_en
28
+ t.string :slug_es
29
+ t.text :body
30
+ end
31
+ add_index :posts, :slug_en
32
+ add_index :posts, :slug_es
33
+ end
34
+
35
+ === Finds
36
+
37
+ Finds will take into consideration the current locale:
38
+
39
+ I18n.locale = :es
40
+ Post.find("la-guerra-de-las-galaxas")
41
+ I18n.locale = :en
42
+ Post.find("star-wars")
43
+
44
+ To find a slug by an explicit locale, perform the find inside a block
45
+ passed to I18n's +with_locale+ method:
46
+
47
+ I18n.with_locale(:es) do
48
+ Post.find("la-guerra-de-las-galaxas")
49
+ end
50
+
51
+ === Creating Records
52
+
53
+ When new records are created, the slug is generated for the current locale only.
54
+
55
+ === Translating Slugs
56
+
57
+ To translate an existing record's friendly_id, use
58
+ {FriendlyId::SimpleI18n::Model#set_friendly_id}. This will ensure that the slug
59
+ you add is properly escaped, transliterated and sequenced:
60
+
61
+ post = Post.create :name => "Star Wars"
62
+ post.set_friendly_id("La guerra de las galaxas", :es)
63
+
64
+ If you don't pass in a locale argument, FriendlyId::SimpleI18n will just use the
65
+ current locale:
66
+
67
+ I18n.with_locale(:es) do
68
+ post.set_friendly_id("La guerra de las galaxas")
69
+ end
70
+ =end
71
+ module SimpleI18n
72
+
73
+ def self.included(model_class)
74
+ model_class.instance_eval do
75
+ friendly_id_config.use :slugged
76
+ friendly_id_config.class.send :include, Configuration
77
+ include Model
78
+ end
79
+ end
80
+
81
+ module Model
82
+ def set_friendly_id(text, locale = nil)
83
+ I18n.with_locale(locale || I18n.locale) do
84
+ set_slug(normalize_friendly_id(text))
85
+ end
86
+ end
87
+ end
88
+
89
+ module Configuration
90
+ def slug_column
91
+ "#{super}_#{I18n.locale}"
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,14 @@
1
+ module FriendlyId
2
+ # A FriendlyId slug stored in an external table.
3
+ #
4
+ # @see FriendlyId::History
5
+ class Slug < ActiveRecord::Base
6
+ self.table_name = "friendly_id_slugs"
7
+ belongs_to :sluggable, :polymorphic => true
8
+
9
+ def to_param
10
+ slug
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,80 @@
1
+ module FriendlyId
2
+ # The default slug generator offers functionality to check slug strings for
3
+ # uniqueness and, if necessary, appends a sequence to guarantee it.
4
+ class SlugGenerator
5
+ attr_reader :sluggable, :normalized
6
+
7
+ # Create a new slug generator.
8
+ def initialize(sluggable, normalized)
9
+ @sluggable = sluggable
10
+ @normalized = normalized
11
+ end
12
+
13
+ # Given a slug, get the next available slug in the sequence.
14
+ def next
15
+ "#{normalized}#{separator}#{next_in_sequence}"
16
+ end
17
+
18
+ # Generate a new sequenced slug.
19
+ def generate
20
+ conflict? ? self.next : normalized
21
+ end
22
+
23
+ private
24
+
25
+ def next_in_sequence
26
+ last_in_sequence == 0 ? 2 : last_in_sequence.next
27
+ end
28
+
29
+ def last_in_sequence
30
+ @_last_in_sequence ||= extract_sequence_from_slug(conflict.to_param)
31
+ end
32
+
33
+ def extract_sequence_from_slug(slug)
34
+ slug.split("#{normalized}#{separator}").last.to_i
35
+ end
36
+
37
+ def column
38
+ sluggable.connection.quote_column_name friendly_id_config.slug_column
39
+ end
40
+
41
+ def conflict?
42
+ !! conflict
43
+ end
44
+
45
+ def conflict
46
+ unless defined? @conflict
47
+ @conflict = conflicts.first
48
+ end
49
+ @conflict
50
+ end
51
+
52
+ def conflicts
53
+ sluggable_class = friendly_id_config.model_class.base_class
54
+
55
+ pkey = sluggable_class.primary_key
56
+ value = sluggable.send pkey
57
+ base = "#{column} = ? OR #{column} LIKE ?"
58
+ # Awful hack for SQLite3, which does not pick up '\' as the escape character without this.
59
+ base << "ESCAPE '\\'" if sluggable.connection.adapter_name =~ /sqlite/i
60
+ scope = sluggable_class.unscoped.where(base, normalized, wildcard)
61
+ scope = scope.where("#{pkey} <> ?", value) unless sluggable.new_record?
62
+ scope = scope.order("LENGTH(#{column}) DESC, #{column} DESC")
63
+ end
64
+
65
+ def friendly_id_config
66
+ sluggable.friendly_id_config
67
+ end
68
+
69
+ def separator
70
+ friendly_id_config.sequence_separator
71
+ end
72
+
73
+ def wildcard
74
+ # Underscores (matching a single character) and percent signs (matching
75
+ # any number of characters) need to be escaped
76
+ # (While this seems like an excessive number of backslashes, it is correct)
77
+ "#{normalized}#{separator}".gsub(/[_%]/, '\\\\\&') + '%'
78
+ end
79
+ end
80
+ end