Last active
April 29, 2024 22:24
-
-
Save AlexB52/95f78b113f1c82a4d86f36ff1fb05157 to your computer and use it in GitHub Desktop.
Rails time zoned models
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require "bundler/inline" | |
gemfile do | |
gem "rails" | |
gem "sqlite3" | |
gem "debug" | |
end | |
require "debug" | |
require "sqlite3" | |
require "active_record" | |
require "active_support" | |
require "minitest/autorun" | |
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') | |
ActiveRecord::Schema.define do | |
create_table :posts do |t| | |
t.string :time_zone | |
t.datetime :published_at | |
t.datetime :issued_at | |
t.timestamps | |
end | |
end | |
# Set global time zone for tests | |
Time.zone = 'Paris' | |
module LocalTimezoned | |
extend ActiveSupport::Concern | |
class_methods do | |
def datetime_attributes | |
columns.select {|c| c.type == :datetime}.map(&:name) | |
end | |
def time_zone_bound(only: [], except: [], fallback_time_zone: Time.zone) | |
attributes = if except.present? | |
datetime_attributes - except.map(&:to_s) | |
elsif only.present? | |
only | |
else | |
datetime_attributes | |
end | |
attributes.each do |attribute_name| | |
redefine_method "#{attribute_name}=" do |value| | |
super(value.in_time_zone(local_time_zone)) | |
end | |
redefine_method attribute_name do | |
super().in_time_zone(local_time_zone) | |
end | |
end | |
define_singleton_method "default_time_zone" do | |
fallback_time_zone | |
end | |
redefine_method "assign_attributes" do |attributes| | |
if attributes.key?(:time_zone) || attributes.key?('time_zone') | |
self.time_zone = [ | |
attributes.delete(:time_zone), | |
attributes.delete('time_zone') | |
].compact.first | |
end | |
super(attributes) | |
end | |
end | |
end | |
def local_time_zone | |
time_zone.presence || self.class.default_time_zone | |
end | |
end | |
class Post < ActiveRecord::Base | |
include LocalTimezoned | |
end | |
class PostTimeZoned < Post | |
time_zone_bound | |
end | |
class PostWithSpecificTimeFields < Post | |
time_zone_bound only: %i[issued_at] | |
end | |
class PostExceptSomeTimeFields < Post | |
time_zone_bound except: %i[created_at updated_at] | |
end | |
class PostWithDefaultTimeZone < Post | |
time_zone_bound fallback_time_zone: 'Santiago' | |
end | |
class TestLocalTimezone < Minitest::Test | |
def test_datetime_attributes | |
assert_equal %w[ | |
published_at | |
issued_at | |
created_at | |
updated_at | |
], PostTimeZoned.datetime_attributes | |
end | |
end | |
class TestLocalTimeZone < Minitest::Test | |
def test_local_time_zone_when_no_time_zone_is_populated | |
assert_equal Time.zone, PostTimeZoned.new.local_time_zone | |
assert_equal 'Nairobi', PostTimeZoned.new(time_zone: 'Nairobi').local_time_zone | |
assert_equal 'Santiago', PostWithDefaultTimeZone.new.local_time_zone | |
assert_equal 'Nairobi', PostWithDefaultTimeZone.new(time_zone: 'Nairobi').local_time_zone | |
end | |
end | |
class TestExceptAttributes < Minitest::Test | |
def test_only_specified_attributes_are_time_zoned | |
post = PostExceptSomeTimeFields.create( | |
time_zone: 'Nairobi', | |
issued_at: '2023-04-15 14:00', | |
published_at: '2023-04-15 14:00', | |
created_at: '2023-04-15 14:00', | |
updated_at: '2023-04-15 14:00' | |
) | |
assert_equal '2023-04-15 14:00:00 +0300', post.issued_at.to_s | |
assert_equal '2023-04-15 14:00:00 +0300', post.published_at.to_s | |
assert_equal '2023-04-15 14:00:00 UTC', post.created_at.to_s | |
assert_equal '2023-04-15 14:00:00 UTC', post.updated_at.to_s | |
end | |
end | |
class TestOnlyAttributes < Minitest::Test | |
def test_only_specified_attributes_are_time_zoned | |
post = PostWithSpecificTimeFields.new(time_zone: 'Nairobi') | |
post.issued_at = '2023-04-15 14:00' | |
post.published_at = '2023-04-15 14:00' | |
assert_equal '2023-04-15 14:00:00 +0300', post.issued_at.to_s | |
assert_equal '2023-04-15 14:00:00 UTC', post.published_at.to_s | |
end | |
end | |
class TestTimezoneBound < Minitest::Test | |
def setup | |
@expected_time = '2023-04-14 15:30'.in_time_zone('Nairobi') | |
end | |
def test_datetime_parsed_as_local_time_zone | |
post = PostTimeZoned.create(time_zone: 'Nairobi') | |
post.update published_at: '2023-04-14 15:30' | |
assert_equal @expected_time, post.published_at | |
end | |
def test_multi_assignment_with_favourable_order | |
post = PostTimeZoned.create( | |
time_zone: 'Nairobi', | |
published_at: '2023-04-14 15:30' | |
) | |
assert_equal @expected_time, post.published_at | |
end | |
def test_multi_assignment_with_unfavourable_order | |
assert_equal @expected_time, PostTimeZoned.create( | |
published_at: '2023-04-14 15:30', | |
time_zone: 'Nairobi' | |
).published_at | |
end | |
def test_multi_assignment_with_unfavourable_order_and_strings | |
assert_equal @expected_time, PostTimeZoned.create( | |
'published_at' => '2023-04-14 15:30', | |
'time_zone' => 'Nairobi' | |
).published_at | |
end | |
def test_time_with_offsets_arent_parsed_into_time_zone | |
post = PostTimeZoned.create(time_zone: 'Paris', published_at: @expected_time) | |
assert_equal @expected_time, post.published_at | |
end | |
def test_multiple_assignments_of_time_zone | |
post = PostTimeZoned.create( | |
published_at: '2023-04-14 15:30', | |
'time_zone' => 'Sydney', | |
:time_zone => 'Paris' | |
) | |
assert_equal '2023-04-14 15:30:00 +0200', post.published_at.to_s | |
assert_equal 'Paris', post.time_zone | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment