Dealing with time zones in rails
Tags: rails, ruby, timezones
Published: 2020-11-17 21:42:16

I really had a lot of fun doing this rails portfolio project. My idea for my rails project was for a prayer gathering. A prayer gathering should have many attendees and should also have prayers to take to the prayer gathering. I also have a host that acts like an administrator and only a host can perform CRUD actions on the gathering. Any user can also create a prayer for the prayer gathering. Here are my relationships tables below

![Prayer gathering associations](https://dev-to-uploads.s3.amazonaws.com/i/cr96lg0skvrn3rzb5gn0.JPG)

Prayer gathering associations

I had 2 areas that were challenging. One was to properly add an address to my gathering form. In this form, I had to create an address object with model. And looking back on it, this was a simple solution for a complex problem. So this is the code that I started with in regard to the address:
```ruby
    def gathering_params
        params.require(:gathering).permit(:name, :meeting_date, :phone_number, :host_id, :url, :timezone, :address => [:address_1, :address_2, :city, :state, :zipcode])
    end
```

And this was my form below:
```ruby
<%= form_for @gathering do |f| %>
<%= f.label :name %><br>
<%= f.text_field :name %><br><br>
<%= f.label :meeting_date %><br>
<%= f.datetime_select :meeting_date, default: Time.now.localtime, ampm: true %><br><br>
<%= f.label :timezone %><br>
<%= f.time_zone_select :timezone, ActiveSupport::TimeZone.us_zones %><br><br>
<%= f.label :phone_number %><br>
<%= f.text_field :phone_number %><br><br>
<%= f.hidden_field :host_id, :value => @host.id %>
<%= f.label :url, "URL" %><br>
<%= f.text_field :url %><br><br>
<%= f.fields_for :address, @address do |a| %>

    <%= a.label :address_1 %><br>
    <%= a.text_field :address_1 %><br><br>
    <%= a.label :address_2 %><br>
    <%= a.text_field :address_2 %><br><br>
    <%= a.label :city %><br>
    <%= a.text_field :city %><br><br>
    <%= a.label :state %><br>
    <%= a.select :state, options_for_select(us_states) %><br><br>
    <%= a.label :zipcode %><br>
    <%= a.text_field :zipcode %><br><br>
    
<% end %>

    <%= f.submit :class => "button" %><br><br>
<% end %>
```
I originally thought that as long as I can accept everything in the hash called address, rails should generate an object called address; however, I quickly realized that rails did not permit the address field. After googling and playing around with the address object several rounds, I realized that I needed address_attributes instead of just address. Also, because my gathering object only had one address, the build method is different. It should be the following:
```ruby
self.build_address(attributes)
```
After doing this and changing my strong params to the one shown below, I was finally able to move on.
```ruby
    def gathering_params
        params.require(:gathering).permit(:name, :meeting_date, :phone_number, :host_id, :url, :timezone, :address_attributes => [:address_1, :address_2, :city, :state, :zipcode])
    end
```
So for my second major issue, it had to do with time validations. I had a datetime object created for the gathering meeting time. At first, my form looked like this:
```ruby
<%= f.label :meeting_date %><br>
<%= f.datetime_select :meeting_date, default: Time.now.localtime, ampm: true %>
```
When I hit the create button, I realized that the datetime created was for UTC time and not the local time. After pulling my hair out, I realized that there was no way around this issue. I had 2 choices. One was to create a meeting time in UTC (which nobody understands other than software engineers), or I had to set a time zone. I realized that resolving this issue would be for my edification, so I decided to painfully add a column to my gatherings table called “timezone” and redo my form to look like this:
```ruby
<%= f.label :meeting_date %><br>
<%= f.datetime_select :meeting_date, default: Time.now.localtime, ampm: true %><br><br>
<%= f.label :timezone %><br>
<%= f.time_zone_select :timezone, ActiveSupport::TimeZone.us_zones %><br><br>
```
Having a time_zone_select would help the user properly identify their timezone and allow me to adjust the timezone offset. My set_in_timezone method looked like this:

```ruby
    def set_in_timezone(time, zone)
        Time.use_zone(zone) { time.to_datetime.change(offset: Time.zone.now.strftime("%z")) }
    end
```
Me gatherings controller looked like this:
```ruby
    def create
        
        @host = current_user
        @gathering = @host.gatherings.build(gathering_params)
				@gathering.set_in_timezone(@gathering.meeting_date, @gathering.timezone)
				@gathering.save
    end    
```
There was no problem doing this until I ran into my update action in my controller. Here was the code.
```ruby
    def update
        @host = current_user
        @gathering = Gathering.find(params[:id])
        #@gathering.meeting_date = set_in_timezone(@gathering.meeting_date, gathering_params[:timezone])
        
        if @gathering.update(gathering_params) && @gathering.address.save
            redirect_to gathering_path(@gathering)
        else
            flash[:error] = @gathering.errors.full_messages.to_sentence + @gathering.address.errors.full_messages.to_sentence
            render :edit
        end
        #binding.pry
        #@gathering.address.update(gathering_update_address_params)
    end
```
The problem here is that updating and validating happen in the @gathering.update method altogether! I found out that I can use the before_validation method. So in order to do this properly, I needed to reset the proper time before the validation. My code looked like the following:
```ruby
    before_validation do
        self.meeting_date = set_in_timezone(self.meeting_date, self.timezone)
    end
```
After finally making that change, I was good to go. Also during validation, I checked to see that the meeting_date created was not in the past.This validation looked like the following:
```ruby
    validate    :future_event
		
    def future_event
        if meeting_date != nil
           
            errors.add(:meeting_date, "cannot be in the past.") if self.meeting_date.in_time_zone < Time.zone.now
        end
        
    end 
```   
After all these were implemented, I could finally move on. Happy coding!