geothird_friendly_id 4.0.9.1

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