File: scoped.rb

package info (click to toggle)
ruby-friendly-id 5.6.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 436 kB
  • sloc: ruby: 3,248; makefile: 3
file content (175 lines) | stat: -rw-r--r-- 5,581 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
require "friendly_id/slugged"

module FriendlyId
  # @guide begin
  #
  # ## Unique Slugs by Scope
  #
  # The {FriendlyId::Scoped} module allows FriendlyId to generate unique slugs
  # within a scope.
  #
  # This allows, for example, two restaurants in different cities to have the slug
  # `joes-diner`:
  #
  #     class Restaurant < ActiveRecord::Base
  #       extend FriendlyId
  #       belongs_to :city
  #       friendly_id :name, :use => :scoped, :scope => :city
  #     end
  #
  #     class City < ActiveRecord::Base
  #       extend FriendlyId
  #       has_many :restaurants
  #       friendly_id :name, :use => :slugged
  #     end
  #
  #     City.friendly.find("seattle").restaurants.friendly.find("joes-diner")
  #     City.friendly.find("chicago").restaurants.friendly.find("joes-diner")
  #
  # Without :scoped in this case, one of the restaurants would have the slug
  # `joes-diner` and the other would have `joes-diner-f9f3789a-daec-4156-af1d-fab81aa16ee5`.
  #
  # The value for the `:scope` option can be the name of a `belongs_to` relation, or
  # a column.
  #
  # Additionally, the `:scope` option can receive an array of scope values:
  #
  #     class Cuisine < ActiveRecord::Base
  #       extend FriendlyId
  #       has_many :restaurants
  #       friendly_id :name, :use => :slugged
  #     end
  #
  #     class City < ActiveRecord::Base
  #       extend FriendlyId
  #       has_many :restaurants
  #       friendly_id :name, :use => :slugged
  #     end
  #
  #     class Restaurant < ActiveRecord::Base
  #       extend FriendlyId
  #       belongs_to :city
  #       friendly_id :name, :use => :scoped, :scope => [:city, :cuisine]
  #     end
  #
  # All supplied values will be used to determine scope.
  #
  # ### Finding Records by Friendly ID
  #
  # If you are using scopes your friendly ids may not be unique, so a simple find
  # like:
  #
  #     Restaurant.friendly.find("joes-diner")
  #
  # may return the wrong record. In these cases it's best to query through the
  # relation:
  #
  #     @city.restaurants.friendly.find("joes-diner")
  #
  # Alternatively, you could pass the scope value as a query parameter:
  #
  #     Restaurant.where(:city_id => @city.id).friendly.find("joes-diner")
  #
  #
  # ### Finding All Records That Match a Scoped ID
  #
  # Query the slug column directly:
  #
  #     Restaurant.where(:slug => "joes-diner")
  #
  # ### Routes for Scoped Models
  #
  # Recall that FriendlyId is a database-centric library, and does not set up any
  # routes for scoped models. You must do this yourself in your application. Here's
  # an example of one way to set this up:
  #
  #     # in routes.rb
  #     resources :cities do
  #       resources :restaurants
  #     end
  #
  #     # in views
  #     <%= link_to 'Show', [@city, @restaurant] %>
  #
  #     # in controllers
  #     @city = City.friendly.find(params[:city_id])
  #     @restaurant = @city.restaurants.friendly.find(params[:id])
  #
  #     # URLs:
  #     http://example.org/cities/seattle/restaurants/joes-diner
  #     http://example.org/cities/chicago/restaurants/joes-diner
  #
  # @guide end
  module Scoped
    # FriendlyId::Config.use will invoke this method when present, to allow
    # loading dependent modules prior to overriding them when necessary.
    def self.setup(model_class)
      model_class.friendly_id_config.use :slugged
    end

    # Sets up behavior and configuration options for FriendlyId's scoped slugs
    # feature.
    def self.included(model_class)
      model_class.class_eval do
        friendly_id_config.class.send :include, Configuration
      end
    end

    def serialized_scope
      friendly_id_config.scope_columns.sort.map { |column| "#{column}:#{send(column)}" }.join(",")
    end

    def scope_for_slug_generator
      if friendly_id_config.uses?(:History)
        return super
      end
      relation = self.class.base_class.unscoped.friendly
      friendly_id_config.scope_columns.each do |column|
        relation = relation.where(column => send(column))
      end
      primary_key_name = self.class.primary_key
      relation.where(self.class.arel_table[primary_key_name].not_eq(send(primary_key_name)))
    end
    private :scope_for_slug_generator

    def slug_generator
      friendly_id_config.slug_generator_class.new(scope_for_slug_generator, friendly_id_config)
    end
    private :slug_generator

    def should_generate_new_friendly_id?
      (changed & friendly_id_config.scope_columns).any? || super
    end

    # This module adds the `:scope` configuration option to
    # {FriendlyId::Configuration FriendlyId::Configuration}.
    module Configuration
      # Gets the scope value.
      #
      # When setting this value, the argument should be a symbol referencing a
      # `belongs_to` relation, or a column.
      #
      # @return Symbol The scope value
      attr_accessor :scope

      # Gets the scope columns.
      #
      # Checks to see if the `:scope` option passed to
      # {FriendlyId::Base#friendly_id} refers to a relation, and if so, returns
      # the realtion's foreign key. Otherwise it assumes the option value was
      # the name of column and returns it cast to a String.
      #
      # @return String The scope column
      def scope_columns
        [@scope].flatten.map { |s| (reflection_foreign_key(s) or s).to_s }
      end

      private

      def reflection_foreign_key(scope)
        reflection = model_class.reflections[scope] || model_class.reflections[scope.to_s]
        reflection.try(:foreign_key)
      end
    end
  end
end