TimeZone Fu

от IvanK

И така дойде време да добавям времеви зони във текущия ми проект (все пак 11 часа разлика със сървъра не са чак толкоз малко) … Колко трудно би могло да бъде? А пък дори и да е трудно, все някой би трябвало да се е сетил, нали тъй? Е да, ама нещата не се оказаха толкоз happy, поне във rails .

Та реших да си напиша едно tutorial-че, било то и само да си спомня как се правеше …

първоначални съставки:

Дотук добре, но идва и първата изненада – tztime е писан само за edge rails и гори при сегашната “стабилна” версия (1.2.3), а не можех да мина “на ръба”, ‘щото използвам Ruby-Gettext1.9 gem-а, който пък се дъни на edge rails…

Следва ровене във repository-то на rail-са, за да видя какво точно е т’ва дето са го променили – оказа се че са добавили механизъм, наречен acts_like(:class) , който tztime съвсем нагло използва. Набързо хакнах activesupport/lib/active_support/core_ext/object/misc.rb, добавяйки:

1
2
3
def acts_like?(type)
  self.respond_to?("acts_like_#{type}?")
end

към activesupport/lib/active_support/core_ext/time.rb :

1
2
3
def acts_like_time
  true
end

и към activesupport/lib/active_support/core_ext/date.rb:

1
2
3
def acts_like_date?
  true
end

С тия хакчета успях да подкарам tztime-а.
Как цялото това чудо се използва? На идеята всичко е просто – първо се дефинира потребителската времева зона, и след това всичко което се запазва във базата, влиза със време, обърнато на UTC, а после като се вади, се врътка пак към времето на потребителя, така например някой от Америка като запази нещо, тука в бг ще си го прочета със правилното време…

на практика, обаче, нещата стоят така – в application_controller-а добавям before_filter:

1
2
3
4
5
6
around_filter :set_timezone
def set_timezone
  TzTime.zone = (current_user != :false and current_user.time_zone) ? TzinfoTimezone[current_user.time_zone] : TzinfoTimezone["Sofia"]
   yield
  TzTime.reset!
end  

(TzinfoTimezone[zone] обръща стандартен string на времева зона във такъв на TzTime, който е леко различен (няма Europe, Asia, USA … отпред), и дефинира TimeZone обект със получения резултат)

Като съответно във _form partial-а на user-ите се добавя поле за редактиране на времевите зони:


f.time_zone_select :time_zone, nil

а пък в моделите се добавя следната декларация, за съответните полета:


tz_time_attributes :created_at, :expires_at

и би трябвало всичко да е ток, но все още не може да излезем от кенефа – проблемът е, че сега created_at и expires_at очакват да им се даде datetime обект, и нищо друго, а пък аз му давам този от params директно, който е string (например “02-01-2007 00:00:00”)

след известно количество ровене, намерих идеалното място да сложа това:

1
2
3
4
5
6
7
8
9
10
11
12
define_method "#{attribute}=" do |atr|
  if atr.is_a?(String) and not atr.empty?
      write_attribute(attribute, Date.parse(atr).to_time)
    end
  elsif atr.acts_like?(:time)
    write_attribute(attribute, atr.to_time)
  elsif atr.acts_like?(:date)
    write_attribute(attribute, atr.to_date)
  else
    write_attribute(attribute, atr)
  end
end

а по точно в tz_time/lib/tz_time_helpers/active_record_methods.rb, във class_eval -> attributes.each блок-а, като по този начин всъщност предефинирам “created_at=” и “expires_at=” методите за съответния клас

Супер, браво на мен :)

И сега наистина tricky частта – аджаба, кога се използва локално време и кога UTC? –
навсякъде, където пиша Time, сега вече пиша TzTime, calculation extension-ите (1.day.ago, 2.minutes.since) също трябва да преминат през TzTime.at(time), за да минат в локалното време. Когато пиша някакъв condtion при някой find, се налага да правя TzTime.utc, за да вади правилни резултати, като ”.utc”-то се добавя винаги накрая (1.day.ago.at_midnight.utc работи, докато 1.day.ago.utc.at_midnight – не) и сега един find изглежда тъй:


Timelog.find :all, :conditions => ["timelogs.logged_at >= ? AND timelogs.logged_at < ?",1.day.ago.utc.to_s(:db), TzTime.now.at_midnight.utc.to_s(:db)]

Определено не съм доволен от rails в този случай – знам, че на PHP щеше да ми се разцепят задните части да правя всичко това, но си мечтаех в rails да е по-лесно, аксиомата “just works” нещо им куца…

Коментари:2

  1. Бах, струва ми се прекалено сложно за да е истина… Ако има плъгин, ‘що не ги прави тия всичките добавки той? Ако не, звучи ми като добро поле за плъгинска изява. Някъде из конфига наковаваш часовата зона, или я четеш от посетителя. Ни мой тъй, дет’ са вика.

    Иначе казано, зариби ме. Ще се разтърся тъй как.

  2. То всъшност най-гадното е, че не се знае кога и къде да използваш TzTime и кога TzTime.utc, и че 1.day.ago не са предефинирани, и постоянно трябва да проверяваш в логовете каква всъшност дата отива към SQL-а, иначе има само няколко кофти GOTCHAS, които, ако ги схване човек, е лесно. Ама то с всичко на rails май е така

Коментирай