Posts from Maxim Kulkin...
Maxim Kulkin, Tue Feb 02 11:18:00 +0300 2010
Every time I visit someone's page on LiveJournal, I see something like
this.
It reminds me a
Homer Simpson's web page. How people can use that crap ? Duh.
Maxim Kulkin, Mon Sep 14 11:10:00 +0400 2009
This keeps popping up over and over again. About 2 years ago I mentioned solution on how to build tree out of nested set, but the solution was lost in google groups. I'm going to publish it here.
So, nested set: every item has LEFT and RIGHT numbers. LEFT number is less than any LEFT or RIGHT number of any descendant item. RIGHT number is greater than any LEFT or RIGHT number of any descendant item. To get a subtree, select all items with LEFT greater than given LEFT and RIGHT less than given RIGHT number. You get the flat array of items of that tree structure. Next thing is to convert it to tree which means for any given item you need the ability to iterate over all of it's children.
First thing you need to do is to order items by LEFT number, than by RIGHT number.
A small sidenote: we will assume that LEFT and RIGHT numbers are compact so that if your root item has LEFT=1 and RIGHT=10 then for each number X in interval [1, 10] there is item for which X is the value of LEFT or RIGHT. This allows easy calculation of how many descendants particular item has.
So, say we have an flat array and we need to know how many items in array you need to skip to get to given item's next sibling. And that number is (RIGHT-LEFT) (minus one if you do not count the item itself).
class NestedSetTreePresenter
include Enumerable
def initialize(items, offset = 0, count = nil)
@items = items
@offset = offset
@count = count || @items.size
end
def each
i = @offset
bound = @offset @count
bound = @items.size if bound > @items.size
while i<bound
item = @items[i]
descendants_count = (item.rgt-item.lft)/2
yield item
i = 1 descendants_count
end
end
def each_with_children
i = @offset
bound = @offset @count
bound = @items.size if bound > @items.size
while i<bound
item = @items[i]
descendants_count = (item.rgt-item.lft)/2
yield item, NestedSetTreePresenter.new(@items, i 1, descendants_count)
i = 1 descendants_count
end
end
end
I chose not to yield a proxy, container or anything else to minimize the number of objects created.
Here is how to use it:
def print_tree(items, level = 0)
items.each_with_children do |item, children|
puts " "*level item.name
print_tree(children, level 1)
end
end
print_tree(NestedSetTreePresenter.new(items))
Maxim Kulkin, Tue Aug 25 10:11:00 +0400 2009
I've used to use and like RSpec. The problem was that it always broke with new versions of Rails and internals were too complex to understand. Then appeared Shoulda and removed the pain of writing RUnit tests, so I switched to using it. But I always missed the elegance of RSpec's matchers.
Until
Matchy appeared.
The cool thing about it is that you can create custom matchers easily with #custom_matcher() method provided. Nice.
Maxim Kulkin, Wed Jul 22 01:09:00 +0400 2009
I've been using Darcs since early 2006 and I do not see any reason to switch to any other (D)VCS like git.
Today at work we decided to put our new project into Darcs and I needed to setup access for others to my repository. The readonly access is not a problem, but allowing others to push into my repositories was a bit trickier. There is a protocol wrapper called "darcs-server", but I don't like that solution. I wanted to configure Darcs to do get/pull/push with minimal additional components.
The traditional way to do Push is via SSH. You can create a separate account for Darcs VCS or just share your own. Then, you set up access via public/private keys and here we go: you can do all operations. The only thing is that everybody can do anything with your account, which is bad. You need to restrict users to running just a small set of commands sufficient for normal Darcs operation. With Darcs 2 it turns out that you only need to allow running "darcs" with various arguments (as Darcs 2 uses special tunelling to do all work while Darcs 1 used to open tons of connections). After several experiments, here is what you need:
#!/bin/sh
first_word() { echo $1; }
line="$SSH_ORIGINAL_COMMAND"
command=$(first_word $line)
if [ "$command" = "darcs" ]; then
sh -c "$line"
else
echo "$command is not allowed"
fi
Then, you add "command='/home/username/bin/darcs-wrapper' " at the beginning of your collegues' public key lines in ~/.ssh/authorized_keys and boom - they are restricted to running only darcs. You can also add no-port-forwarding, no-X11-forwarding, no-agent-forwarding, no-pty to tighten security. And you can set environment variable DARCS_LOGNAME to name of particular user to get proper names in darcs changes logs: add "environment='DARCS_LOGNAME=username'" to your authorized_keys file.
Hope that helps.
UPDATE: after thinking a little, I found a case when this can be workaround to execute any command, e.g. "darcs help; rm -rf *". The problem is with "sh -c". Originally, it was introduced in place of call to "exec" to workaround problem with extra quotation marks that are not removed when parsing line and which cause Darcs to handle arguments improperly. It turns out, that "exec" is really needed there and unquoting should be done manually. Here is the corrected version:
#!/bin/sh
first_word() { echo $1; }
unquoted_words() {
for word in $@; do
echo $(echo $word | sed -E "s/^'(.*)'$/\1/; s/^\"(.*)\"$/\1/")
done
}
line="$SSH_ORIGINAL_COMMAND"
command=$(first_word $line)
if [ "$command" = "darcs" ]; then
exec $(unquoted_words $line)
else
echo "$command is not allowed"
fi
UPDATE2: that "sed -E" thing seem to not work on Linux. Linux Sed's option for full regular expressions is called "-r". Probably need to try rewriting regexps to normal Sed's functionality. Later.
UPDATE3: very handy thing in debugging Darcs' SSH is using "--debug --verbose" options to "darcs". Then, you can see what commands are sent to server and how response is interpreted. Then, you can issue those commands manually and see if server's wrapper script outputs errors.
Maxim Kulkin, Sat Jan 10 22:02:00 +0300 2009
I want to develop admin controller to do simple CRUD operations on not-so-complex models. I want it to be DRY and to allow adding features to all of them easily. This is my second attempt, the first one was way too ugly: it used inheritance from admin's base controller and descendants passed customization parameters to base class; also, have used custom "render" methods to find template for specific controller or, if the specific template is missing, fall back to general template.
And then it broke. Yeah, this could be fixed, but the whole idea seemed bad.
I just finished the second attempt and it looks promising.
First, it includes only one controller and uses routes to determine the actual model to operate on. Every route includes additional parameter that is passed to controller that contains name of model:
map.resources :users, :controller => 'items',
:requirements => { :item_type => 'User' }
map.resources :regions, :controller => 'items',
:requirements => { :item_type => 'Region' }
Second, the most troublesome was to make it work like this: I have default views in app/views/items... I wanted to have subdirectories with name of particular model that would contain overrides to default templates:
app/views/items
index.html.erb
new.html.erb
edit.html.erb
_item.html.erb
_form.html.erb
app/views/items/users
_item.html.erb
_form.html.erb
app/views/items/regions
_form.html.erb
Regions doesn't need custom 'item' partial because default one (which outputs only item.to_s) fits well.
Obviously, controller's view_paths should be manipulated to search first in concrete subfolders and then in default place. But the problem was that it always tried to prepend controller name and so find it in corresponding subfolder. It's not very nice being forced to create additional directories like this:
app/views/items
app/views/items/users/items
_item.html.erb
Well, after some digging the way to do it elegantly I found this solution:
1. Redefine controller's controller_path to return nil. After this all templates are searched in app/views/ instead of app/views/items, so it needs to be fixed, so
2. Redefine controller's view_paths to base in app/views/items.
3. Based on current item type prepend corresponding view path.
The result is as follows:
class ItemsController < ApplicationController
def self.controller_path
nil
end
self.view_paths = [ RAILS_ROOT '/app/views/items' ]
before_filter :setup_model
# ... action code
private
def setup_model
@model = params[:item_type].constantize
prepend_view_path(RAILS_ROOT '/app/views/items/' params[:item_type].underscore)
end
end
Happy coding!
Maxim Kulkin, Fri Jan 09 17:37:00 +0300 2009
Lately I wasn't doing any Rails development because I was busy re-learning C/C "the right way". Turns out it's not that painful as they say.
Then I had to do some Rails coding using latest and greatest Rails 2.2 and I find that that the simple things are broken (or somewhat obscure).
Case 1.
I was developing some general resource controller and wanted to spec out a redirect after #create action. My controller should not be bound to any specific resource url (as it is planned to bind many different resources with that controller), so I decided to make use of Rails' url rewriting. Spec was like this:
it "should redirect to #index after item creation" do
@model.stubs(:create!)
post :create
response.should redirect_to(:action => :index)
end
And this spec failed because of MethodNotAllowed: Only get and post requests are allowed.
WTF? It's the very basic functionality. Why does it fail to work ?
After half a day of investigation I found out that this is a bug in rspec-rails package and redirect_to matcher fails to handle hash-like url specifier.
Case 2.
I wanted to handle AR::RecordInvalid exceptions with one handler to DRY up controller code. I wrote corresponding "rescue_from" call in controller and wrote specs (in fact, the reverse order: specs then code). And those specs failed too.
The other several hours of investigation revealed hidden option: RSpec-rails overrides default Rails' error handling (which is based on ActiveSupport's #rescue_from feature) with pass-through exception handler, and you have to write following code to make YOUR exception handlers work:
before do
controller.use_rails_error_handling!
end
Yeah, Rails is nice, but upgrading to a new version is a pain.
Maxim Kulkin, Wed May 21 15:34:00 +0400 2008
On my recent Rails project I decided to try Sphinx search engine. Before that I used Ferret and then - SOLR. I abandoned Ferret because it's instability and lack of tools to track problems (like Luke - index browser for Lucene).
So, Sphinx. I have found two plugins for Rails - acts_as_sphinx and Unltrasphinx. Ok, I've heard in mailing lists that Ultrasphinx is better (and Sphinx recipe in Advanced Rails Recipes book also involves it) so I decided to use it.
First I have to install Sphinx from sources because version in MacPorts (although, latest released version) is too old and Ultrasphinx requiresa newer one (at that point - release candidate 2 of next revision, which is 0.9.8rc2 vs 0.9.7). Then I had to do various dances around Sphinx to compile it on my Mac OS X (which are described in my
previous post.).
And then it began:
1. It was needed that new data (e.g. new articles) could be searched with Sphinx right after it was added. And then I find out, that it is not encouraged to do such updates often, you'd better run full updates once a day... Wtf? I have heard that there are something called "deltas", but as I found out from plugin, it doesn't install any hooks on models, so I assume that deltas need to be built periodically (which is also unacceptable).
After consulting with other people who have used Sphinx I found out that:
1) they don't use plugins and do all communication with some low level library (Riddle, as far as I remember) manually.
2) they install their own hooks on models and call indexer manually to reindex their models. I have tried to to install after_save hook but it is run BEFORE transaction is commited and indexer can't see inserted/updated data, so I don't know how this can be accomplished easily.
2. Sphinx configuration is one big scary thing. Although Ultrasphinx managed to build it for me, I needed to do some tweaks to it and then next time I needed to build configuration for another model my tweaks were lost.
3. Model on which I have used Ultrasphinx (called ::is_indexed) failed to load by automatic dependency loader. After several hours of tracking this problem I stuck "require 'my_model'" into environment.rb (which helped) and cursed that Sphinx and all it's plugins.
So, for me Sphinx definitely FAIL.
PS
I have tried SOLR and it worked like charm:
1) almost no configuration.
SOLR configuration consists of type definitions (which describe how value of that type should be analyzed), field definitions and dynamic field definitions (acts_as_solr rails plugin uses only dynamic fields). Acts_as_solr comes with default SOLR configuration which contains commonly used types and dynamic field definitions for them.
2) no compilation needed. Acts_as_solr comes with SOLR JAR files, so you just need proper version of Java runtime installed.
3) works like charm. Everything you would expect from full-text search engine.
Maxim Kulkin, Thu May 15 00:57:00 +0400 2008
One of my ongoing projects required using Sphinx search engine. While I'm more comfortable with Lucene based solutions (I'm evaluating SOLR currently), I decided to give Sphinx a try.
I have installed Sphinx via MacPorts system and was going to use UltraSphinx Rails plugin. But when I tried to run some test query, it complained, that server version is older than the client version. So, I needed to update Sphinx as UltraSphinx depends on some development release of Sphinx. But when I downloaded sources, configured and issued "make" command, the build failed because there were link problems with iconv library:
Undefined symbols:
"_iconv_close", referenced from:
xmlUnknownEncoding(void*, char const*, XML_Encoding*)in libsphinx.a(sphinx.o)
"_iconv", referenced from:
xmlUnknownEncoding(void*, char const*, XML_Encoding*)in libsphinx.a(sphinx.o)
"_iconv_open", referenced from:
xmlUnknownEncoding(void*, char const*, XML_Encoding*)in libsphinx.a(sphinx.o)
ld: symbol(s) not found
I saw some
posts on how to overcome and they suggest downloading and installing iconv library. But I was strongly against doing such things (and also iconv library installed on my system was the latest one), so after some trial-and-error I have figured how to build it: just give standard system's library path precedence:
export LDFLAGS="-L/usr/lib"
./configure
make
sudo make install
Hope this will help somebody.
Maxim Kulkin, Wed Apr 30 16:47:00 +0400 2008
I see more and more posts like
this or
this recently and I always see that when people are telling about how poor the Prototype framework is they really don't know even half of it's possibilities.
As it always happens, the code comparison in those posts is written by person who is not experienced in one framework or another.
I agree, that jQuery has some nice features: 1) actions on groups of objects (that you have mentioned), 2) more powerful selectors: prototype has support for css3 selectors only, but jQuery can handle more sophisticated queries like selecting hidden elements or inputs of particular type.
I haven't dug into what it takes to write custom effect or plugin (plan to do it soon), but I don't see how you can base your effects on existing ones in a way other than aggregating/compositing (the thing with Prototype effects is that you can inherit your effect from some base effect and have some nice features like configurable 'easing' function, which can control effect speed over time, or some other stuff for free).
But seeing syntax comparisons like that just makes me mad.
First, adding a class name to element in Prototype is just as easy as in jQuery:
$('element').addClassName('className')
$('element').removeClassName('classsName')
Second, the Ajax syntax. Prototype's variant is designed to do POST requests by default. You don't need to specify 'method' parameter if you do POST request. As for GET requests, in Prototype you can do just as in jQuery:
new Ajax.Updater('element', url '&' parameters)
(I believe, the '&' thing is needed in jQuery also, but author of code comparison just 'forgot' to mention that)
Moreover, if you don't like the wordy syntax of Ajax.Updater, you can extend Element to support whatever syntax you like:
Element.addMethods({
load: function(element, url) {
new Ajax.Updater(element, url)
}
})
and then you can do
$('element').load(url params)
just as in jQuery.
And, yes, Prototype doesn't handle working with sets of elements by default. To add a particular class to a set of elements in Prototype you need to do some extra stuff, but not as it is mentioned in code comparison:
$$('.element').invoke('addClassName', 'className')
And yes, I agree, that Prototype and Scriptaculous have bad documentation and I personally have bought a book on them (see Pragmatic Programmers bookshelf) and my problems with Prototype and Scripaculous have gone since then.
I don't see any big reason to switch to jQuery.
PS There are lots of other javascript frameworks like mooTools (if I spell it correctly), Dojo, Rhino and such and I always haven't paid much attention to those, because usually my web application require custom solutions, not those that are present in such frameworks. But recently I came across some book ("Mastering Dojo" published by Pragmatic Programmers) and a podcast on it and it got my attention. They say that lot's of fortune 500 companies are using their framework because it is very solid and stable. And I'm curious about what particular features of Dojo make them think so. I have visited Dojo's site and tried to see at some of the examples (e.g. fish eye component) and they were running way too slow on my latest Firefox 2.x (which I consider as rather modern web browser). Probably, I need to give it one more chance and, probably, read that book.
Maxim Kulkin, Tue Dec 11 11:36:00 +0300 2007
I see more and more people nowadays complain about other people using destroy_all to delete a bunch of records without understanding the performance decrease compared to using delete_all.
Well, I'll tell you what: in general they do the right thing. My opinion is that having such shortcut methods like delete_all (or update_attribute that issues an update command without triggering validation/callbacks) are evil. While ActiveRecord tries to provide a foundation for building your domain model (smart model, not just containers for data) those methods allow to mess everything up. E.g. you have a model, and when you decide to provide a means to delete all objects, you use delete_all because of better performance than destroy_all. Later on you decide to add some on_destroy callbacks, and then you find out that you need to change every call to delete_all to destroy_all.
Calling delete_all just breaks an invariant for your objects.
What it would be nice is to optimize destroy_all to just call delete_all if nothing else is required. The main reason to call destroy_all is the on_destroy callback. So, ActiveRecord could check if there is any callbacks and if there is no callbacks, then just call delete_all operation.
The same could be done for updates (and it was discussed for a long time): update only those fields that have changed. Then, instead of calling "special" method to update just one field, you will just update the required field and call regular #save. AR will detect all changed fields and issue an update command for just those fields.
How this can be implemented ? The ideal solution is to keep a set of "unmodified" data and compare object's data to that "unmodified" set on save. But on big objects it could require two times more memory. Instead, it could be easier to track what fields were assigned values to and set "changed" flag for that field. If the flag wasn't yet set and the assigned value differs from the one that it is now, then set the flag. If the flag was already set, then don't update it. Of course, it will sometimes update the field to the same value if you first set it to some other value and then set the value that was before. But I believe that such cases are rare and it is not a problem if we update it with the same value.
Storing a boolean flag per database column is not an issue also.
Maxim Kulkin, Tue Nov 06 09:36:00 +0300 2007
Giles Bowkett: IRB: What Was That Method Again?
why should someone bother adding just another method to do stuff that is easily done with existing methods ?
"any arbitrary string".methods.grep("ch")
Maxim Kulkin, Tue Nov 06 09:04:00 +0300 2007
There is an
auto_migrations plugin that people nowadays start using more and more often. This plugin allows to maintain only one file with database schema definitions and it changes DB structure to match that definition automagically.
Giles Bowkett
blogged about it recently and about problems with regular migrations. But wait a minute, I've seen (and experienced) only one type of situations when you can't migrate from zero up to latest migration: it's situation when your model has changed through version and e.g. some method vital for that migration was removed or some new validation was added and you try to create objects in migration that have that field set to an invalid value.
How can changing DB structure make migrations invalid ?
And having that in mind, how that
auto_migrations thing can help ? Also, how will you actually migrate your data with auto_migrations ?
PS btw, the way to fix broken migrations in situation described above is to use schema dump that rails creates anyways in db/ directory: when you create from scratch, there could be no data to migrate, so you could just create the DB structure. But if there are migrations to e.g. create an admin user, then you could need to run actual migrations. And the only way I see (when they fail half way) is to use version control system to update to each revision where new migration was added (so that source code match the migration), do the migration, then update to next version where new migration was introduced, do the migration and so forth.
Probably, this could be automated. Assuming it is needed only when new member joins the development team, it is a rear operation so the performance is not critical.
Maxim Kulkin, Thu Jul 05 00:52:00 +0400 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 Конечно, расширение стандартной библиотеки для поддержки своих форматов строк не самое изящное регение и черевато проблемами, если вдруг разработчики решат использовать какой-либо другой метод конвертации строк в даты. Но я считаю, что на данный момент этого решения вполне достаточно.
Maxim Kulkin, Wed Jun 27 23:05:00 +0400 2007
После долгих раздумий, я пришел к мнению, что многопараметровые значения все-таки зло:
- Пользоваться ими без ActiveRecord нельзя. Представьте вариант, когда у Вас есть модель (назовем ее Task) с кучей полей поле с датой, которое вы хотите заполнять только в опрделенном случае (скажем, при завершени Task'а), соответственно, не хотите, чтобы пользователь мог его менять самостоятельно (назовем это поле closed_at). Для этого поле делается защищенным от массового присвоения (
attr_protected :closed_at). Теперь у вас есть действие контроллера, которое закрывает Task. Там надо поменять значения состояния Task'а и выставить дату. Если у Вас дата - мультипараметр (представлена тремя списками), то Вам надо превратить этот мультипараметр в дату.
Сборка мультипараметров предусмотрена только через метод attributes=, который в то же время делает проверку на атрибуты, защищенные от массового присвоения (т.е. в нашем случае это не будет работать). Придется собирать вручную (или прибегать к черной магии, способ описывать не буду).
- Ошибки сборки мультипараметров надо обрабатывать отдельно, нет способа показать неправильное значение, если ошибка произошла.
Я для себя решил не пользоваться мультипараметрами, а собирать единое значение на клиенте скриптом, а потом обрабатывать (парсить) его на сервере. С одной стороны, это плохо, т.к. требует поддержки и активации яваскрипта в браузере. Есть альтернатива: иметь текстовое поле, в котором можно будет ввести дату в определенном формате кнопочку для активации яваскриптового календаря для наглядного выбора даты (который в свою очередь вставит нужную дату в текстовое поле). Таким образом все будут довольны. А на сервере преобразовать дату будет так же просто: надо будет всего лишь сделать
Date.strptime(params[:date_value], date_format).
Осталось только выбрать подходящий плагин и жаваскриптовую библиотеку для скриптового календаря.
Maxim Kulkin, Tue Jun 26 09:51:00 +0400 2007
Работал над тем, чтобы добавить нормальную обработку неправильных значений для дат. У меня одно из требований заказчика - чтобы даты можно было вводить и изменять с клавиатуры. При этом вводить день и особенно год проще текстом, нежели выбирать их из списка. Поэтому элементы для редактирования дат у меня выглядят как текстбокс для дня, список для месяца и текстбокс для года.
Далее, хочется, чтобы пустые значения в любом из тех текст боксов отрабатывались правильно. Как известно, внутри рельсов даты являются многопараметровым (multiparameter) значением (т.е. оно складывается из значений нескольких элементов на форме), и логика преобразования типов там простая: позвать to_i для каждого значения и скормить получившиеся значения в Date.new... При этом (!) там стоит логика, что пустые значения не обрабатываются. Из за чего, если, например, у вас на форме не заполнен год (а он должен идти первым аргументом в конструкторе Date), то в результате его значение обработано не будет и на вход Date.new будет передано не 3, а 2 значения, что означает, что номер месяца (который идет вслед за годом в аргументах Date.new) будет расценен как номер года, а номер дня - как номер месяца... Такая же проблема, насколько я понимаю, происходит и со стандартным date_select (тот, который состоит из трех списков), если включен
:include_blank...
Я считаю, такое поведение неоправданным, поэтому я сделал запил N1 - не выбрасывать пустые атрбуты (см код ниже, метод extract_callstack_for_multiparameter_attributes).
Зачем же вообще код в ActiveRecord был написан так, чтобы выбрасывать пустые значения ? Сделано это, по видимому, было для того, чтобы обрабатывать пустые значения (как видно из
if values.empty?
send(name "=", nil)
в execute_callstack_for_multiparameter_attributes). Вот это очень важно, т.к. не хотелось бы в последствие иметь ошибки на пустых и не обязательных к заполнению датах. Поэтому, (!) запил N2 - в конце обработки параметров проверить, если для любого из атрибутов заготовлен только массив из nil'ов, то заменить этот массив на пустой.
Таким образом, если у вас любой из трех компонент даты окажется пустым, то он так и будет занимать свое законное место в списке аргументов, но будучи сконвертированным в нужный тип данных (в нашем случае - целое число). Конвертация осуществляется вызовом метода, начинающегося с "to_" и продолжающегося одной буквой соответствующего типа (например, "i" - "to_i").. to_i, как известно, пустые строки превращает в 0... Значит, если у вас дата будет пустая, то это превратится в Date.new(0, 0, 0), что не очень хорошо, т.к. если 0 в качестве параметра для года и допускается, то передача 0 для месяца или дня порождает исключение (которое потом порождает "ошибку присвоение атрибута с несколькими параметрами" - MultiparameterAssignmentError)... Хотелось бы и с этим забороться, а также не трактовать пустое поле год как нулевой год.
И тут мне пришел в голову самый аццкий запил (N3):
class String
def to_n
Integer(self) rescue nil
end
end
Если кто не знает, Integer(string) преобразует строковое представление числа в, собссно, число, но кидает исключение, если строка хоть немного не число (содержит посторонние символы, пустая и т.п.)
Далее, модифицируем хелпер date_select (который мы и так уже модифицировали, см. требования заказчика в начале) так, чтобы к названию полей он добавлял не (1i) / (2i) / (3i), а (1n) / (2n) и (3n). Тогда ActiveRecord будет пытаться преобразовать значения этих полей, используя наш метод String#to_n.
Отлично! Теперь у нас вылетает MultiparameterAssignmentError, если данные дата не является корректными. Теперь осталось только обработать эту ошибку. Вообще, мне не понятно, почему такие ошибки не включаются в список ошибок валидации, а выбрасываются отдельным исключением. Некоторые люди даже научились бороться с этим разными извращенными способами (например,
так)
Я же хотел, чтобы мне ничего для этого менять не нужно было (ни ловить исключение, ни доставать список ошибок из каких-то дополнительных мест). Поэтому: 1) исключение надо ловить внутри, 2) ошибки аккуратно складывать все в тот же errors. Я уже было начал делать очередной запил =), но наткнулся на этот замечательный
плагин .. Добрые люди уже все сделали именно так, как я хотел.
Еще бы мои запилы оформить в виде плагина, а лучше (если нет каких-то концептуальных препятствий) - закомитить в само ядро.
Надеюсь, вам, люди, эта информация пригодится.
Ну и, собссно, мои запилы:
def extract_callstack_for_multiparameter_attributes(pairs)
attributes = { }
for pair in pairs
multiparameter_name, value = pair
attribute_name = multiparameter_name.split("(").first
attributes[attribute_name] = [] unless attributes.include?(attribute_name)
# запил N1: не пропускать пустые значения
#unless value.empty?
attributes[attribute_name] <<
[ find_parameter_position(multiparameter_name), type_cast_attribute_value(multiparameter_name, value) ]
#end
end
# запил N2: заменять массив из только nil'ов на пустой
attributes.each do |name, values|
attributes[name] = values.sort_by{ |v| v.first }.collect { |v| v.last }
attributes[name] = [] unless attributes[name].detect { |x| !x.nil? }
end
end
Maxim Kulkin, Fri Dec 22 08:55:00 +0300 2006
Очень интересный пост по поводу использования разных языков программирования в IT индустрии. Очень рекомендую:
What if there are no gods ?
Maxim Kulkin, Wed Nov 08 17:59:00 +0300 2006
Недавно я продолжил изучение внутренностей ActiveRecord, потому как для меня до сих пор оставалось загадкой, как же правильно работать с данными в ActiveRecord (например, осуществлять конвертацию).
Начнем с сердца экземпляра ActiveRecord - переменной @attributes. Это то самое хранилище, где хранятся все данные экземпляра AR. По сути, это хэш "атрибут => значение" и именно его (и ничего больше) инициализирует класс, вынимая запись из базы данных.
Но изменять значения напрямую - не самая хорошая идея. Для доступа к значениям есть методы read_attribute(name) / write_attribute(name, value). Помимо вынимания/засовывания значения из/в @attributes, эти методы делают некоторые преобразования:
- read_attribute осуществляет приведение типа к типу, соответствующему типу столбца БД (так, например, данные sql типа данных date превращаются в экземпляры класса Date). Также этот метод осуществляет десериализацию данных из YAML (помните еще о такой возможности ? =)). Если атрибуту не соответствует столбец базы данных - значение возвращается как есть.
- write_attribute проще: он преобразовывает boolean данные в числа, пустую строку - в nil для числовых столбцов.
Для этой парочки есть укороченные варианты: [] / []=. Т.е. если вы переопределили аццессор для вашего атрибута, то читать / писать данные внутри аццессора надо посредством этих "индексных" методов.
Далее, есть свойство attributes. При чтении, оно, в принципе, просто возвращает копии всех атрибутов (не пытайтесь менять значения того, что вернул attributes - сам экземпляр AR не изменится), прогнанных сквозь read_attribute (т.е. данные с правильными типами и десериализованные). Также этот метод поддерживает опции, позволяющие исключить некоторые атрибуты или оставить конкретные из них (опции :except и :only).
Врайтер attributes= или же направляет значение соответствующему врайтеру атрибута (например, firstname=), если это обычный атрибут, или же собирает мультиатрибут (тот, элементы которыго, имеют одинаковое имя и суффикс в виле порякового номера (и опционально - типа данных) в круглых скобках).
И теперь самое интересное: каждый атрибут поддерживает _before_type_cast ридер. Этот ридер возвращает значение соответствующего ключа @attributes напрямую, минуя преобразование данных как в read_attribute.
Выводы:
* нельзя получить значения частей мультиатрибута (потому что они не сохраняются в @attributes. В предыдущей статье я описывал один из способов сделать сборку / разборку данных посредством composed_of. Так вот в этом способе нельзя сохранить на форме введенные неправильные данные.
* при обновлении атрибутов можно получить сырые обновленные данные с поправкой на то, что для числовых столбцов True/False будет преобразован в 1/0, пустая строка - в nil, посредством _before_type_cast аццессора.
Теперь попробуем переделать предыдущий пример с временем в календаре: сделаем так, чтобы можно было редактировать время как текст HH:MM. Для этого:
1. Сделаем отдельный аттрибут для текстового представления:
class Event < ActiveRecord::Base
def time_text
"%d:d" % [self.time/60, self.time`]
end
def time_text=(value)
self.time = begin
parts = value.split ':'
parts[0].to_i*60 parts.to_i
rescue
nil
end
end
end
Отлично! Теперь делаем на форме text_field :event, :time_text и оно уже отображается. Но он стирает текст, если в нем есть ошибки. Хотелось бы сохранять его.2. Надо сохранять несконвертированное значение:
class Event < ActiveRecord::Base
def time_text
self[:time_text] || ("%d:d" % [self.time/60, self.time`])
end
def time_text=(value)
self[:time_text] = value
self.time = begin
parts = value.split ':'
parts[0].to_i*60 parts[1].to_i
rescue
nil
end
end
end
text_field один из немногих (если не единственный) хелперов, которые используют _before_type_cast аццессоры. Теперь, поскольку мы сохранили неконвертированное значение, он сможет вывести его, если возникли ошибки.Теперь надо подумать про валидацию:
class Event < ActiveRecord::Base
validates_format_of :time_text, :with => /\d?\d\:\d\d/
end
Ну вот, собственно, и все.
Maxim Kulkin, Thu Nov 02 19:00:00 +0300 2006
Недавно понадобился такой функционал:
В базе в поле типа integer хранится время события (кол-во минут с начала суток).
Надо организовать редактирование этих данных в привычном для пользователя виде (т.е. часы : минуты). Пришлось залазить в кишки рельсов.
В HTML редактирование времени выглядит как 2 SELECT'а (часы и минуты). Проблема: собрать два этих параметра в одно поле (общее_количество_минут), чтобы потом сохранить его в базе.
В рельсах есть поддержка механизма сборки одного поля из нескольких request-параметров. Она активируется, если поле имеет суффикс вида "(x)", где x - порядковый номер параметра. Если рельсы встречают такие параметры, они их собирают в отдельную кучку и начинают по ним создавать "правильный" тип данных. Для этого они сначала выводят тип (класс) будущего значения, и вызывают у него метод new с соответствующим (соответствующим кол-во параметров в мультипараметре) количеством аргументов.
Теперь надо понять, как подсунуть нужный класс. Класс будет или классом типа данных соответствующего столбца (имеющего то же имя) в базе данных, или именем класса аггрегации. Нас интересует именно эта аггрегация. В рельсах объявление агрегации осуществляется посредством вызова метода composed_of.
Итак начнем писать наш тип данных для времени. Нам понадобится конструктор с одним параметром - число минут с начала суток (данные, которые будут храниться в базе). Также, сразу же объявим ридеры для часов и минут, и метод to_s:
class CalendarTime
attr_reader :minutes_since_midnight
def initialize(minutes)
@minutes_since_midnight = minutes
end
def hour
@minutes_since_midnight / 60 rescue nil
end
def min
@minutes_since_midnight % 60 rescue nil
end
def to_s
"%d:d" % [ hour, min ]
end
end
Теперь добавим агрегацию в модель:
class CalendarEvent < ActiveRecord::Base
composed_of :time,
:class_name => 'CalendarTime',
# отображаем AR атрибут time на поле
# minutes_since_midnight нашего класса
:mapping => [:time, :minutes_since_midnight ],
# разрешить nil в качестве значения агрегата
:allow_nil => true
end
Отлично. Теперь при обращении к event.time мы получим экземпляр CalendarTime.
Теперь представление. Надо сделать хелпер который будет выдавать два селекта с хитрыми именами.
module ApplicationHelper
def time_select(object_name, method, options = {})
# Получим сам объект
object = options[:object] ||
instance_variable_get('@' object_name)
# и значение
value = object.send(method)
# Подготовим опции
options.delete :object
object[:discard_type] = true
# Подготовим шаблон для имени элементов
name = "#{object_name}[#{attr}(%di)]"
# Наш объект CalendarTime поддерживает свойства hour и minute
# поэтому можно будет воспользоваться стандартными хэлперами
select_hour(value, options.merge :prefix => name%1) ' : '
select_minute(value, options.merge :prefix => name%2)
end
end
Теперь в представлении сделаем вызов этого хелпера:
<p>
<label for="event_time">Time</label><br/>
<%= time_select 'event', 'time', :include_blank => true %>
</p>
Отлично, осталось только собрать данные назад.
Как я уже говорил, при обработке мультипараметров ActiveRecord сконструирует агрегирующий объект (в нашем случае - CalendarTime) с соответствующим количеством параметров. Значит надо обработать два параметра в конструкторе CalendarTime. Для этого перепишем его так:
class CalendarTime
def initialize(*args)
if args.size == 1 && args[0].is_a?(Numeric)
@minutes_since_midnight = args[0]
elsif (args.size == 2 &&
args[0].is_a?(Numeric) &&
args[1].is_a?(Numeric))
@minutes_since_midnight = args[0]*60 args[1]
end
end
end
Собственно, все.
Чтобы еще больше понимать механизмы работы рельсов, было полезно узнать, как (и когда) работает отображение данных в composed_of. Так вот, агрегат создается по каждому требованию и ему в качестве аргемнтов конструктора передаются значения всех замапленых атрибутов (перечисленных в :mapping опции) в указанном порядке (поэтому значение этой опции или массив из двух элементов, или массив массивов из двух элементов - чтобы можно было отследить порядок аргументов). Обратное присвоение происходит при присвоении модели нового значения агрегата.
PS есть один gotcha: при сборке агрегата из мультипараметра, рельсы по каким-то причинам отфильтровывают все пустые строки или nil. Будьте осторожны с этим. Вам в конструкторе могут подсунуть аргументов меньше, чем должно быть.
Maxim Kulkin, Tue Oct 03 18:16:00 +0400 2006
В Rails уже давно есть возможность брать контент экшена не из файла, а передовать его в строке. Таким образом легко сделать простой CMS: берем шаблон из базы и делаем render :inline => . Проблема же построения более менее пригодного CMS заключалась в том, что подобную операцию нельзя было провести с лэйаутами.
Недавно я написал маленький плагин - Nested layouts (http://nested-layouts.rubyforge.org). Я решил, что возможность использования inline лэйаутов будет полезной добавкой к нему.
<% inside_inline_layout @layout_template do %>
content
<% end %>
Так что, кому надо - берите, пользуйте, пишите впечатления.
Maxim Kulkin, Mon Oct 02 15:52:00 +0400 2006
Недавние занятия программированием с Ajax в Rails привели к созданию патча для поддержки создания IF блоков в javascript'е. Основная идея (помимо условий в виде строк) - поддержка элемент-прокси:
page.if page['element_id'].visible do
page['element_id'].hide
end
Проблема в том, что page['element_id'].visible сгенерит код до того, как будет обернуто в IF блок.
Также можно использовать более традиционные методы:
page.unless "$('element_id').visible()" do
page['element_id'].show
end
Патч был отклонен в core, но было предложено сделать плангин для обкатки.
Собственно, вот и он: http://rubyforge.org/projects/js-if-blocks. Инсталировать можно так:
./script/plugin install svn://rubyforge.org/var/svn/js-if-blocks/trunk/js-if-blocks
Тепрь думаю о добавлении возможности повторного использования proxy объектов, например:
element = page['element_id]
page.if element.visible do
element.hide
end