Tag: timezone

Suporte a TimeZone no Rails 2.1: uma visão geral

13 de abril de 2008 às 16:06 | Carlos Júnior | , ,

Esta é uma tradução do artigo escrito por Geoff Buesing.

Este será o primeiro de muitos posts que farei sobre os novos recursos de time zone (fuso horário) na próxima versão (2.1) do Rails. Neste post, eu darei uma visão geral destes novos recuros, na criação de um novo aplicativo.

Começarei com um aplicativo Rails 2.1 criado através do comando “rails”. No 2.1, suporte a fuso horário estará ligado por padrão no environment.rb, através da opção config.time_zone:

# config/environment.rb
config.time_zone = 'UTC'

– Este estará definido para UTC como padrão, mas você irá normalmente modificar isto para um fuso horário apropriado para sua localidade. As novas tarefas do rake time:zones:all, time:zones:us, e time:zones:local foram adicionadas para ajudá-lo a encontrar os nomes adequados de fuso horário. time:zones:local farão uma tentativa educada baseada no sistema local de horário, então este é um bom lugar para se começar:

$ rake time:zones:local
* UTC -06:00 *
Central America
Central Time (US & Canada)
Guadalajara
Mexico City
Monterrey
Saskatchewan

Carlos: este é o exemplo usado no artigo original, não vou modificar pois os exemplos se baseam nestes resultados…. no meu sistema acontece isso:

$ rake time:zones:local
* UTC -02:00 *
Mid-Atlantic

Para este exemplo, eu usarei config.time_zone para US Central Time:

# config/environment.rb
config.time_zone = 'Central Time (US & Canada)'

A seguir, criarei um simples scaffold para um modelo Task, com um atributo datetime alert_at:

$ script/generate scaffold Task name:string alert_at:datetime
$ rake db:migrate
$ script/server

Irei ao formulário para nova tarefa, e criarei uma:

new_task1.png

A action show exibe a data e hora que eu digitei, seguido pela correção (deslocamento) UTC:

show_task1.png

… para este exemplo, o prefixo UTC é -0500, que é a correção para US Central Time durante o “horário de verão”:

Para mostrar como este horário é guardado no banco de dados, eu vou ao script/console, e usar o método #alert_at_before_type_cast:

>> t = Task.find_by_name('foo')
=>#< Task … >
>> t.alert_at
=> Sun, 06 Apr 2008 10:30:00 CDT -05:00
>> t.alert_at_before_type_cast
=> "2008-04-06 15:30:00"

O banco de dados está guardando a representação UTC de nosso horário: 15:30 UTC é simultâneamente 10:30 CDT (Central Daylight Time – daylight é o horário de verão). A diferença entre os dois horários é a correção UTC (-5 horas, neste caso).

Depois, eu vou editar a tarefa e alterar o mês para Janeiro (fora do horário de verão, lá):

edit_task.png

Note que a correção UTC agora é -0600 – por que o alert_at não está mais no horário de verão.

show_task_updated.png

script/console confirmará que o banco de dados recebeu a representação correta UTC:

>> t = Task.find_by_name('foo')
=> < Task … >
>> t.alert_at
=> Sun, 06 Jan 2008 10:30:00 CST -06:00
>> t.alert_at_before_type_cast
=> "2008-01-06 16:30:00"

A hora do banco de dados é agora 16:30 ao invés de 15:30, por que a correção UTC é -6 horas agora.

Fusos por usuário

O que eu defini anteriormente funcionará bem para um aplicativo onde todos os usuários estão no mesmo fuso. Se o aplicativo eventualmente necessitar suportar usuários em fusos diferentes, é bastante fácil de se fazer isso:

Primeiramente, eu criarei um scaffold de usuário, com um atributo string para guardar o fuso do usuário:

$ script/generate scaffold User name:string time_zone:string
$ rake db:migrate

Mudarei formulário do usuário para usar o combo de fusos ao invés de um campo de texto:

# views/users/new.html.erb
<%= f.time_zone_select :time_zone, TimeZone.us_zones %>

O novo formulário de usuário ficará assim — eu o usarei para criar alguns usuários com fusos diferentes:

new_user.png

Para efeitos de demonstração, eu adicionarei um before_filter simples chamado “login_from_querystring” ao ApplicationController:

# controllers/application.rb
before_filter :login_from_querystring
def login_from_querystring
  @current_user = User.find_by_name(params[:user])
end

Então adicionarei um outro before_filter “set_time_zone”, que definirá o Time.zone para o fuso do usuário atual:

# controllers/application.rb
before_filter :set_time_zone
def set_time_zone
  Time.zone = @current_user.time_zone if @current_user
end

Adicionarei um cabeçalho ao layout para mostrar quem está logado, seu fuso horário e o hora atual em seu fuso horário:

# views/layouts/tasks.html.erb
Current user: <%= @current_user.name if @current_user %>
Current time zone: <%= Time.zone.name %>
Current time: <%= Time.zone.now.inspect %>
<hr />

Finalmente, modificarei a view da index das tarefas para usar a representação #inspect do alert_at, para nos revelar alguns detalhes adicionais:

# views/tasks/index.html.erb
<%=h task.alert_at.inspect %>

Agora, se eu logar com um dos usuários que eu criei, eu verei a tarefa que criei anteriormente, com o alert_at ajustado para o fuso do usuário atual:

index_usuario1.png

…note que o horário exibido para a tarefa é 11:30 EST – que é o mesmo que 10:30 CST.

Para o usuário em US Mountain Time, a tarefa será mostrada como 9:30 MST:

index_usuario2.png

Sem um usuário logado, o fuso usado é o definido em config.time_zone:

index_usuario21.png

Métodos para criar horários no Time.zone atual

Anteriormente, nós estávamos nos apoiando no ActiveRecord para automaticamente converter atributos do model para o horário local do usuário. Para casos onde você precisa criar novas instâncias de Time no fuso local do usuário, os métodos Time.zone.local(), Time.zone.parse() e Time.zone.now() estão disponíveis, assim como Time.zone.now():

>> Time.zone = 'Hawaii'
=> "Hawaii"
> Time.zone.now
=> Wed, 09 Apr 2008 15:48:18 HST -10:00
>> Time.zone.local(2008, 4, 9, 15, 48, 18)
=> Wed, 09 Apr 2008 15:48:18 HST -10:00
>> Time.zone.parse('2008-04-09 15:48:18')
=> Wed, 09 Apr 2008 15:48:18 HST -10:00
>> Time.zone.at(1207792098)
=> Wed, 09 Apr 2008 15:48:18 HST -10:00

Time e DateTime #in_time_zone converterá qualquer instância para o fuso em Time.zone:

>> Time.zone = 'Alaska'
=> "Alaska"
>> t = Time.utc(2000)
=> Sat Jan 01 00:00:00 UTC 2000
>> t.in_time_zone
=> Fri, 31 Dec 1999 15:00:00 AKST -09:00

… ou, para qualquer fuso ou identificador de fuso (ex.: nome, inteiro ou Duration):

>> t.in_time_zone('Hawaii')
=> Fri, 31 Dec 1999 14:00:00 HST -10:00
>> t.in_time_zone(-6.hours)
=> Fri, 31 Dec 1999 18:00:00 CST -06:00

Dica: Atualizando seu aplicativo

  1. o novo recurso de fuso horário assume que seu banco de dados está guardando horários em UTC, então se você está atualmente guardando horários em um fuso diferente de UTC, você precisará migrar os dados existentes para UTC.
  2. se você instalou o plugin tzinfo_timezone, você precisará removê-lo, dado que este sobrescreve a classe TimeZone no ActiveSupport.
  3. a gem TZInfo não é mais necessárioa, dado que esta está agora dentro do ActiveSupport. De toda forma, se você tem uma versão recente desta gem instalada, Rails favorecerá a gem sobre a versão incluída no ActiveSupport.
  4. A versão do TZInfo incluída no ActiveSupport é uma versão compacta da gem, então se você está interagindo com a API do TZInfo diretamente, você precisará da gem instalada.
  5. Se você não deseja usar o novo recurso de fuso horário – este novo recurso não deve interferir em seu código existente, desde que você não declare o config.time_zone em seu environment.rb.

No próximo capítulo…

Nos próximos posts, tentarei cobrir mais coisas “escondidas”, mas esperamos que este post ajude vocês

Se você acha fuso horário, correções UTC, e horário de verão confuso, você talvez queira ver estas validações de fuso horário, que talvez te deixem ainda mais confuso…