File: history.rb

package info (click to toggle)
ruby-friendly-id 5.5.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 432 kB
  • sloc: ruby: 3,143; makefile: 3
file content (146 lines) | stat: -rw-r--r-- 4,386 bytes parent folder | download
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
module FriendlyId
  # @guide begin
  #
  # ## History: Avoiding 404's When Slugs Change
  #
  # FriendlyId's {FriendlyId::History History} module adds the ability to store a
  # log of a model's slugs, so that when its friendly id changes, it's still
  # possible to perform finds by the old id.
  #
  # The primary use case for this is avoiding broken URLs.
  #
  # ### Setup
  #
  # In order to use this module, you must add a table to your database schema to
  # store the slug records. FriendlyId provides a generator for this purpose:
  #
  #     rails generate friendly_id
  #     rake db:migrate
  #
  # This will add a table named `friendly_id_slugs`, used by the {FriendlyId::Slug}
  # model.
  #
  # ### Considerations
  #
  # Because recording slug history requires creating additional database records,
  # this module has an impact on the performance of the associated model's `create`
  # method.
  #
  # ### Example
  #
  #     class Post < ActiveRecord::Base
  #       extend FriendlyId
  #       friendly_id :title, :use => :history
  #     end
  #
  #     class PostsController < ApplicationController
  #
  #       before_filter :find_post
  #
  #       ...
  #
  #       def find_post
  #         @post = Post.friendly.find params[:id]
  #
  #         # If an old id or a numeric id was used to find the record, then
  #         # the request slug will not match the current slug, and we should do
  #         # a 301 redirect to the new path
  #         if params[:id] != @post.slug
  #           return redirect_to @post, :status => :moved_permanently
  #         end
  #       end
  #     end
  #
  # @guide end
  module History
    module Configuration
      def dependent_value
        dependent.nil? ? :destroy : dependent
      end
    end

    def self.setup(model_class)
      model_class.instance_eval do
        friendly_id_config.use :slugged
        friendly_id_config.class.send :include, History::Configuration
        friendly_id_config.finder_methods = FriendlyId::History::FinderMethods
        FriendlyId::Finders.setup(model_class) if friendly_id_config.uses? :finders
      end
    end

    # Configures the model instance to use the History add-on.
    def self.included(model_class)
      model_class.class_eval do
        has_many :slugs, -> { order(id: :desc) }, **{
          as: :sluggable,
          dependent: @friendly_id_config.dependent_value,
          class_name: Slug.to_s
        }

        after_save :create_slug
      end
    end

    module FinderMethods
      include ::FriendlyId::FinderMethods

      def exists_by_friendly_id?(id)
        super || joins(:slugs).where(slug_history_clause(id)).exists?
      end

      private

      def first_by_friendly_id(id)
        super || slug_table_record(id)
      end

      def slug_table_record(id)
        select(quoted_table_name + ".*").joins(:slugs).where(slug_history_clause(id)).order(Slug.arel_table[:id].desc).first
      end

      def slug_history_clause(id)
        Slug.arel_table[:sluggable_type].eq(base_class.to_s).and(Slug.arel_table[:slug].eq(id))
      end
    end

    private

    # If we're updating, don't consider historic slugs for the same record
    # to be conflicts. This will allow a record to revert to a previously
    # used slug.
    def scope_for_slug_generator
      relation = super.joins(:slugs)
      unless new_record?
        relation = relation.merge(Slug.where("sluggable_id <> ?", id))
      end
      if friendly_id_config.uses?(:scoped)
        relation = relation.where(Slug.arel_table[:scope].eq(serialized_scope))
      end
      relation
    end

    def create_slug
      return unless friendly_id
      return if history_is_up_to_date?
      # Allow reversion back to a previously used slug
      relation = slugs.where(slug: friendly_id)
      if friendly_id_config.uses?(:scoped)
        relation = relation.where(scope: serialized_scope)
      end
      relation.destroy_all unless relation.empty?
      slugs.create! do |record|
        record.slug = friendly_id
        record.scope = serialized_scope if friendly_id_config.uses?(:scoped)
      end
    end

    def history_is_up_to_date?
      latest_history = slugs.first
      check = latest_history.try(:slug) == friendly_id
      if friendly_id_config.uses?(:scoped)
        check &&= latest_history.scope == serialized_scope
      end
      check
    end
  end
end