четверг, июля 05, 2007

Завершение саги о датах


Никак не мог успокоиться: все решения, которые я находил, не содержали решения для выдачи ошибок, если дату невозможно распарсить. Чаще всего, если попытка перевода строки в дату не была успешной, вместо даты - nil и никаких тебе ошибок.

Наконец мне удалось сложить все кусочки воедино и получить достаточно неплохое, на мой взгляд, решение.

1. Как редактировать.
Как уже обсуждалось в моих предыдущих постах, самое правильное при редактировании даты - отказаться от трех списков и оставить только одно текстовое поле, куда можно вводить строковое представление даты в определенном формате. Плюс можно прикрутить жаваскриптовый календарик к этому текстовому полю.
Как же высветить введенное значение, если распарсить строку не удалось ? Вспоминаем, что для этого есть *_before_type_cast атрибуты. Напомню: когда мы присваиваем значения конкретному атрибуту, это значение сохраняется внутри экземпляра модели нетронутым (про многопараметровые значения мы не говорим). Конвертация же происходит, когда мы пытаемся получить значение атрибута экзепляра класса модели, а *_before_type_cast возвращает значение, обходя конвертацию. Поэтому, надо только сделать date_select хэлпер, который будет пытаться сконвертировать значение атрибута (сконвертированное, т.е. типа Date) в строку, если это значение не nil, иначе - выкладывать строковое представление значение *_before_type_cast. Плюс код для яваскриптового календарика (например, этого).
Отлично, теперь можно вводить даты в стандартных форматах (например, YYYY-MM-DD) и не терять введенное значение, если преобразовать его в дату не удастся.

2. Нужные форматы даты.
В моем приложении просто необходимо, чтобы дату можно было вводить в русском формате (т.е. DD.MM.YYYY). Поэтому, надо как-то научить дату понимать такой формат.
Поизучав исходники, я пришел к тому, что конвертация из строки в дату производится экземпляром объекта, который представляет соответствующий столбец базы данных. По реализации становится ясно, что конвертация осуществляется методом ParseDate#parsedate стандартной библиотеки Ruby. Недолго думая, расширяем этот метод, чтобы он понимал нужный нам формат даты:
ParseDate.class_eval do
class << self
def parsedate_with_ru_format(str, comp=false)
str = str.to_s
str = "#{$3}-#{$2}-#{$1}" if /(
\d{2})\.(\d{2})\.(\d{4})/ =~ str
parsedate_without_ru_format(str, comp)
end

alias_method_chain :parsedate, :ru_format
end
end

Подключаем этот код в config/environment.rb и вуаля: Rails понимает нужный нам формат даты.

3. Ошибка при неправильном формате даты.
Теперь формат понимаем, неправильное значение никуда не девается, осталось только понять, что формат даты неверный. В текущем состоянии при ошибке конвертации вместо даты имеем nil. Это может быть немного confusing для пользователя, если он думал, что он заполнил (необязательное) поле с датой и сохранил его, а потом выяснит, что в базе вместо значения получился nil.
При этом, не хотелось бы добавлять никаких явных валидаций (типа, validates_date_format :foo и т.п.). Итак: прикручиваем к ActiveRecord::Base валидацию по умолчанию, которая перебирает все столбцы модели и для всех столбцов типа :date проверяет, что если сконвертированное значение == nil, а значение *_before_type_cast не пустое (!foo_before_type_cast.empty?), то добавляет ошибку на это поле.
ActiveRecord::Base.class_eval do
class << self
def date_columns
columns.select { |column| column.type == :date }
end
end

def validate_dates_format(record)
record.class.date_columns.each do |column|
if record.send(column.name).nil? &&
record.send("#{column.name}_before_type_cast").empty?
record.errors.add(column.name, "has invalid date format")
end
end
end

validate :validate_dates_format
end

Данное решение избавляет еще и от проблем с невозможностью легко выставить дату для атрибута, который защищен от массового присвоения (посредством, например, attr_protected).

Надеюсь, описанный выше способ пригодится кому-нибудь (лично меня он полностью устраивает) или послужит источником знаний/вдохновения для написания "следующего самого клевого плагина для Rails".

PS Конечно, расширение стандартной библиотеки для поддержки своих форматов строк не самое изящное регение и черевато проблемами, если вдруг разработчики решат использовать какой-либо другой метод конвертации строк в даты. Но я считаю, что на данный момент этого решения вполне достаточно.