aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/timestamp.rb
blob: c883d368b5da10c7734555d4b45c76a4bd7ee652 (plain) (blame)
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
# frozen_string_literal: true

module ActiveRecord
  # = Active Record \Timestamp
  #
  # Active Record automatically timestamps create and update operations if the
  # table has fields named <tt>created_at/created_on</tt> or
  # <tt>updated_at/updated_on</tt>.
  #
  # Timestamping can be turned off by setting:
  #
  #   config.active_record.record_timestamps = false
  #
  # Timestamps are in UTC by default but you can use the local timezone by setting:
  #
  #   config.active_record.default_timezone = :local
  #
  # == Time Zone aware attributes
  #
  # Active Record keeps all the <tt>datetime</tt> and <tt>time</tt> columns
  # timezone aware. By default, these values are stored in the database as UTC
  # and converted back to the current <tt>Time.zone</tt> when pulled from the database.
  #
  # This feature can be turned off completely by setting:
  #
  #   config.active_record.time_zone_aware_attributes = false
  #
  # You can also specify that only <tt>datetime</tt> columns should be time-zone
  # aware (while <tt>time</tt> should not) by setting:
  #
  #   ActiveRecord::Base.time_zone_aware_types = [:datetime]
  #
  # You can also add database specific timezone aware types. For example, for PostgreSQL:
  #
  #   ActiveRecord::Base.time_zone_aware_types += [:tsrange, :tstzrange]
  #
  # Finally, you can indicate specific attributes of a model for which time zone
  # conversion should not applied, for instance by setting:
  #
  #   class Topic < ActiveRecord::Base
  #     self.skip_time_zone_conversion_for_attributes = [:written_on]
  #   end
  module Timestamp
    extend ActiveSupport::Concern

    included do
      class_attribute :record_timestamps, default: true
    end

    def initialize_dup(other) # :nodoc:
      super
      clear_timestamp_attributes
    end

    module ClassMethods # :nodoc:
      def touch_attributes_with_time(*names, time: nil)
        attribute_names = timestamp_attributes_for_update_in_model
        attribute_names |= names.map(&:to_s)
        attribute_names.index_with(time || current_time_from_proper_timezone)
      end

      def timestamp_attributes_for_create_in_model
        @timestamp_attributes_for_create_in_model ||=
          (timestamp_attributes_for_create & column_names).freeze
      end

      def timestamp_attributes_for_update_in_model
        @timestamp_attributes_for_update_in_model ||=
          (timestamp_attributes_for_update & column_names).freeze
      end

      def all_timestamp_attributes_in_model
        @all_timestamp_attributes_in_model ||=
          (timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model).freeze
      end

      def current_time_from_proper_timezone
        default_timezone == :utc ? Time.now.utc : Time.now
      end

      private
        def timestamp_attributes_for_create
          ["created_at", "created_on"]
        end

        def timestamp_attributes_for_update
          ["updated_at", "updated_on"]
        end

        def reload_schema_from_cache
          @timestamp_attributes_for_create_in_model = nil
          @timestamp_attributes_for_update_in_model = nil
          @all_timestamp_attributes_in_model = nil
          super
        end
    end

  private
    def _create_record
      if record_timestamps
        current_time = current_time_from_proper_timezone

        all_timestamp_attributes_in_model.each do |column|
          if !attribute_present?(column)
            _write_attribute(column, current_time)
          end
        end
      end

      super
    end

    def _update_record
      if @_touch_record && should_record_timestamps?
        current_time = current_time_from_proper_timezone

        timestamp_attributes_for_update_in_model.each do |column|
          next if will_save_change_to_attribute?(column)
          _write_attribute(column, current_time)
        end
      end

      super
    end

    def create_or_update(touch: true, **)
      @_touch_record = touch
      super
    end

    def should_record_timestamps?
      record_timestamps && (!partial_writes? || has_changes_to_save?)
    end

    def timestamp_attributes_for_create_in_model
      self.class.timestamp_attributes_for_create_in_model
    end

    def timestamp_attributes_for_update_in_model
      self.class.timestamp_attributes_for_update_in_model
    end

    def all_timestamp_attributes_in_model
      self.class.all_timestamp_attributes_in_model
    end

    def current_time_from_proper_timezone
      self.class.current_time_from_proper_timezone
    end

    def max_updated_column_timestamp
      timestamp_attributes_for_update_in_model
        .map { |attr| self[attr]&.to_time }
        .compact
        .max
    end

    # Clear attributes and changed_attributes
    def clear_timestamp_attributes
      all_timestamp_attributes_in_model.each do |attribute_name|
        self[attribute_name] = nil
        clear_attribute_changes([attribute_name])
      end
    end
  end
end