Skip to content

Instantly share code, notes, and snippets.

@AlexB52
Last active April 29, 2024 22:24
Show Gist options
  • Save AlexB52/95f78b113f1c82a4d86f36ff1fb05157 to your computer and use it in GitHub Desktop.
Save AlexB52/95f78b113f1c82a4d86f36ff1fb05157 to your computer and use it in GitHub Desktop.
Rails time zoned models
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