Detecting font width

http://stackoverflow.com/questions/378887/how-do-i-calculate-a-strings-width-in-ruby

Setting up server hardware time


:~$ sudo date 093019512009
############# ddmmHHMMYYYY

:~$ sudo hwclock --systohc
:~$ sudo hwclock --show

Setting up server hardware time


:~$ sudo date 093019512009
:~$ sudo hwclock --systohc
:~$ sudo hwclock --show

Fixing IE6 & Rails & cookies issue

Problem: Cookies are not set in IE6, as well as session, which depends on cookies.

Problem description: This is caused by default IE6 security option "Block 3rd sites party cookies".

To solve the issue, you need to create Privacy Policy for your site

Here are the steps you need to do in your rails application:

1. Generate privacy policy http://www.alphaworks.ibm.com/tech/p3peditor
2. Put two files p3p.xml and policy.html into /public/w3c/
3. Add

to your apps/views/layout/application.rb
4. Add a header in application_controller.rb in before_filter the following code:
header['P3P'] = 'CP="CAO CURa ..."'

And that's it. Don't forget to clear cookies and restart your IE6.

Useful links:

http://www.oreillynet.com/pub/a/javascript/2002/10/04/p3p.html

Refactoring ruby code #2: Extracting logic to helpers

In this post I want to demonstrate an simple refactoring technique extract method.

Here is an long controller method:



It's obvious that this is not the right place for such logic. So what we do is:
1) Write some tests
2) Create EventsHelper
3) Move all logic from controller to EventsHelper
4) Rewrite test :)

Here is a result:



So, now in views we use previous_moth(@month) instead of @previous_month

What we got:
* Thiner controller
* Separate logic
* Easy testing
* Less variables

Ruby: Refactoring code #1

In these series I'll write some posts on refactoring ruby code.

No matter what language you write, there are always quite similar approach to refactoring.

Refactoring means changing code, improving it's readability and scalability, without functional modification.


Here are some basic steps:
0. Find the code that smells.

1. Check if code has enough test coverage. Write tests if no coverage.
2. Refactor code. Do it by small pieces. Don't try to refactor everything at once. Make this process iterative.
3. Run tests.
4. Refactor tests.
5. Commit changes.


It's really important to refactor code step by step, don't try to refactor big pieces of code. Otherwise you'll lose the clue.

The other important thing is not to change functionality. Even if you see the code has a bug, fix it right after you are done with refactoring.

Testing is very important during whole process of refactoring, you can't be sure for your code without tests.


So, lets jump right to the examples.

First, we need to find stinky code. Most of that you can find in your controllers.

Here is the first candidate for refactoring, we have a CategoriesController, with show method:


def show
@category = Category.find(params[:category_id], :include=>[:category_stats])
@region = Region.find_by_id(params[:region_id]) unless params[:region_id].blank?
@action = Action.find_by_id(params[:action_id]) unless params[:action_id].blank?

@alternatives = @category.alternative_categories.counter_caches.to_a

CategoryStat.category_opened(@category, request.remote_ip)
key = {:category_id=>@category.id}
key.merge!(:region_id=>@region.id) if @region
set_scope_id(key)

unless show_subcategories?(@category, @region)
common_args = {:page => params[:page], :per_page => ADS_PER_PAGE,
:include => [:custom_attributes, :price_unit, :category, :assets, :region],
:order => order, :conditions=>filter_conditions}

ad_scope = AdList.category_or_children(@category)
ad_scope = ad_scope.active
ad_scope = ad_scope.action(@action.id) if @action
ad_scope = ad_scope.region(@region) if @region
ad_scope = ad_scope.created_after(DateFilter.value(date_filter)) if date_filter_applied?

if region_selected?
@regional_ads = ad_scope.region(current_region).paginate(common_args)
ad_scope = ad_scope.not_in_ids(@regional_ads.collect{|a| a.id}) unless @regional_ads.empty?
end

@ads = ad_scope.paginate(common_args)
end
end



As we can see, the method is really big, and this is kind of problem. So the first step would be getting rid of show_subcategories? conditions by spliting it to two different methods.

So we have the following:



def messages
@category = Category.find(params[:category_id], :include=>[:category_stats])
@region = Region.find_by_id(params[:region_id]) unless params[:region_id].blank?
@action = Action.find_by_id(params[:action_id]) unless params[:action_id].blank?

@alternatives = @category.alternative_categories.counter_caches.to_a

CategoryStat.category_opened(@category, request.remote_ip)
key = {:category_id=>@category.id}
key.merge!(:region_id=>@region.id) if @region
set_scope_id(key)

common_args = {:page => params[:page], :per_page => ADS_PER_PAGE,
:include => [:custom_attributes, :price_unit, :category, :assets, :region],
:order => order, :conditions=>filter_conditions}

ad_scope = AdList.category_or_children(@category)
ad_scope = ad_scope.active
ad_scope = ad_scope.action(@action.id) if @action
ad_scope = ad_scope.region(@region) if @region
ad_scope = ad_scope.created_after(DateFilter.value(date_filter)) if date_filter_applied?

if region_selected?
@regional_ads = ad_scope.region(current_region).paginate(common_args)
ad_scope = ad_scope.not_in_ids(@regional_ads.collect{|a| a.id}) unless @regional_ads.empty?
end

@ads = ad_scope.paginate(common_args)
end

def show
@category = Category.find(params[:category_id], :include=>[:category_stats])
@region = Region.find_by_id(params[:region_id]) unless params[:region_id].blank?
@action = Action.find_by_id(params[:action_id]) unless params[:action_id].blank?

@alternatives = @category.alternative_categories.counter_caches.to_a

CategoryStat.category_opened(@category, request.remote_ip)
key = {:category_id=>@category.id}
key.merge!(:region_id=>@region.id) if @region
set_scope_id(key)
end



So now we have a small duplication, here is how we can handle this:




before_filter :find_category_region_action, :only => [:show, :messages]

def find_category_region_action
@category = Category.find(params[:category_id], :include=>[:category_stats])
@region = Region.find_by_id(params[:region_id]) unless params[:region_id].blank?
@action = Action.find_by_id(params[:action_id]) unless params[:action_id].blank?

@alternatives = @category.alternative_categories.counter_caches.to_a

CategoryStat.category_opened(@category, request.remote_ip)
key = {:category_id=>@category.id}
key.merge!(:region_id=>@region.id) if @region
set_scope_id(key)
end



So now we have two clean methods with no duplication.

Wrap long text with ruby

Here is a code snipet on how to wrap long texts.

It actually do long lines ('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') and ('adas asdas asdas dad asdasd ad') wrapping with ' ' and




Thanks to http://blog.macromates.com/2006/wrapping-text-with-regular-expressions/

Data migrations with rails

data_migration plugin

Sinatra. First aplication

Recently tried a sinatra framework for a small web app.

Sinatra is actually cool thing. It's easy to start and configure. But I still not sure if it fits for larger than small applications.

I've coded a small app with almost the same structure as a rails application. It could be useful to get started.

Testing Rails plugins

Here's a short list on how to test your plugins.

1) Structure


vendor/
plugins/
yourplugin/
test/
helper.rb
your_test.rb
database.yml
schema.rb
init.rb
Rakefile


2) Rakefile


3) helper.rb


4) your_test.rb


5) schema.rb


6) Running tests
Go to your plugin root folder and run
$ rake test

To see full example, refer to http://github.com/balepc/ip_geolocation/

Here are some other useful links:

Rails plugin testing guide
Another guide

Geolocation by IP address

Detecting user's location by his ip address is quite a common task in many web applications. You can find lot's of them in Internet. Here are a short list:

http://hostip.info/
http://ipinfodb.com/
http://www.wipmania.com/


All of them have pretty simple interface. Here is a ruby wrapper for that http://github.com/balepc/ip_geolocation

Multi server capistrano recepie

It's a quite common situation when you have one production and some staging servers. So to handle it with capistrano there's a great technique described here.

Token generator (ruby)


require 'digest/md5'

Digest::MD5.hexdigest('some_value' + Time.now.to_s)

Меняем значение сессии и перегружем страницу динамически

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

В контроллере:

def change_session
session[:key] = params[:value]
end

Во вьюхе:


...
дата


В скриптах (application.js):

function change_session(value) {
$.ajax({
type: "GET",
url: "/controller/change_session",
dataType: "script",
data: {
value: value
}
});
}

И поправить роуты елси приложение RESTfull (routes.rb):

map.resources :resource_name, :collection=>{:change_session=>:get}

Довольно просто, но коряво, в том плане, что при каждом ajax запросе будет перегружаться страница.

Shutdown по таймеру

Полезная штука:


sudo shutdown -h +30
sudo shutdown -h 21:30

Crawling данных

Для миграции legacy данных потребовалось написать небольшой crawler.

В качестве парсера был выбран nokogiri, а для получения файла обычная open-uri библиотека.

Оснавная проблема с которой столкнулся это неправильная работа парсера с кодировками, отличными от utf-8. После недолгих поисков и изучения проблемы, было решено получать страницу вручную, конвертировать с помощью iconv, а потом уже передавать это дело парсеру.

Итак к самой задаче. Есть каталог изделий распределенный по категориям. Сначала нужно получить список всех ссылок, далее получить более детальную информацию о конкретном товаре и составить список аттрибутов товара:


require 'rubygems'
require 'nokogiri'
require 'open-uri'

class ItemsCrawler

def crawl
results = []
get_categories_urls.each do |url|
data = extract_data(url)
results << data if data
end
results
end

private
def get_categories_urls
links = []
doc = Nokogiri::HTML(open("#{HOST}/catalogue"))
doc.css('div.list a').each do |link|
links << link['href'] if link['href'].include?('htm')
end
links
end

def extract_data(url)
puts "Getting - #{HOST+url}"
html = open(HOST+url).read
html = Iconv.iconv('utf-8', 'cp1251', html)

# Нужно заменить, иначе nokogiri его трактует как windows-1251
doc = Nokogiri::HTML(html[0].gsub('charset=windows-1251"', 'charset=utf-8"'))

# Далее получение аттрибутов с помощью xpath или css селектора

title = doc.search('title').first.contet
description = doc.search('div.content').first.inner_html # Получение содержания со всеми тегами
{:title => title, :description => description}
end


Вот в принципе и все, получился довольно простой парсер.

Просмотр request в rails

Появилась необходимость просмотреть body запроса который приходит в rails приложение. Для этого подключил rack.

Стартуем сервер:

Rack::Handler::WEBrick.run
lambda { |env| raise env['rack.input'].read.inspect }, :Port => 1234

Тестируем:

curl --header "Content-Type: text/xml" --data @aaa.xml http://localhost:1234/



Больше ресурсов о rack:
http://remi.org/2009/02/19/rack-basics.html

Определение страны по ip адресу

Довольно простое и быстрое решение геолокации.


=> require 'open-uri'
>> []
=> open("http://api.wipmania.com/#{ip_address}?yoursite.com").read
>> "LV"


Дополнительная информация тут.

Можно также все это дело реализовать у себя. wipmania предоставляет свою базу как в sql, cidr так и в txt формате.

Анализ текстовых докуметов. Получение ключевых слов.

Появилась интересная задачка по вычленению ключевых слов из текстового документа. Необходимо разработать такой модуль, который на входе получает текстовой файл, а на выходе выдает ключевые слова которые могли бы дать представление о чем идет речь в документе, с помощью ключевых слов.

К примеру, есть статья о хокейном матче между динамо рига - динамо москва, ключевыми словами должны быть: хоккей, динамо рига, динамо москва, матч, чемпионат.

Существует несколько алгоритмов, основанных на алгоритмах обработки естественного языка, но они довольно сложные и не укладываются в временные рамки. К тому же алгоритмы для каждого языка отличаются.

Поэтому было решено для начала разработать такой алгоритм только для анлг языка, на основании описанного тут алгоритма.

Алгоритм включает следующие шаги:

0. Определение языка

1. Stemming, на основании алгоритма Потера

2. Построение frequency таблицы

3. Кластеризация часты термов

4. Вычисление совстречаемости термов

5. Вычисление хи-квадрата для термов

6. Выбор терма с наибольшим значением хи-квадрата - это и будет ключевое слово


Далее каждый шаг рассматривается детально.

0. Для определения языка можно поставить гем: whatlanguage. Работает он предельно просто:
>> "Je suis un homme".language
=> :french

1. Для стеминга существует несколько алгоритвом реализованных в руби
* Номер раз
* Номер два

Второй вариант написан на C и автор утверждает что он быстрее. Использовать это очень просто:

$ script/console
=> 'running'.stem
>> 'run'

2. Построение таблицы частот

Тут я реализовал простенький алгоритм, в дальнейшем его планирую переписать на с:


class FrequencyHash < Hash

def occur(key)
if include?(key)
store(key, self[key] + 1)
else
store(key, 1)
end
end

def frequent_keys(min_frequency)
self.keys.reject{|key| self[key] < min_frequency}.sort{|a,b| self[b] <=> self[a] && a <=> b}
end

end

require 'stemmer'
require 'whatlanguage'

module KeywordsHelper
MIN_LENGHT = 3

def get_frequences(full_text)
words = FrequencyHash.new
full_text.split(/\W/).each do |word|
word = word.stem
words.occur(word) if word.length >= MIN_LENGHT
end
words.frequent_keys(2)
end

end


Далее переходим к шагу 3...

To be continued...

Блокировка пользователей по ip адресу

Не так давно на один из моих сайтов стали проведить DDoS атаки, отсылая с разных ip адресов запросы на однин и тот же адрес. Количество запросов не такое большое, но сам action довольно тяжеловесный и занимает от 5 до 30 секунд.

Собственно поэтому и решил написать небольшую надстройку, для занесения нежелательных ip адресов в black-list.

У всех запросов была одна закономерность, запрос вываливался при валидации токена формы verify_authenticity_token. Поэтому и было решено начать оттуда.

Первое что было сделано, это логгирование "плохих" запросов (config/initializers/forgery_protection.rb):


module ActionController
module RequestForgeryProtection

def verify_authenticity_token
verified_request? || block_user! || raise(ActionController::InvalidAuthenticityToken)
end

def block_user!
BlockedIp.unverified_request!(request.remote_ip)
end

end

end


Далее собственно модель с тремя аттрибутами (ip адрес, количество попыток, статус) (apps/models/blocked_ip):

'
class BlockedIp < ActiveRecord::Base

before_save :change_status

STATUS_NONE = 'none'
STATUS_BLOCK = 'block'
STATUS_BLOCKED = 'blocked'

ALLOWED_ATEMTPS = 5

named_scope :pending_for_block, {:conditions => ["status = ?", STATUS_BLOCK]}

def BlockedIp.unverified_request!(ip_address)
ip = BlockedIp.find_by_ip_address(ip_address)
ip ||= BlockedIp.new(:ip_address => ip_address)
ip.violation!
false
end

def violation!
self.attempts += 1
self.save
end

def block!
`iptables -I INPUT -s #{self.ip_address} -j DROP`
self.update_attribute(:status, STATUS_BLOCKED)
AdminNotifier.deliver_ip_blocked(self)
self.save
end

private
def change_status
if self.attempts > ALLOWED_ATEMTPS and self.status != STATUS_BLOCKED
self.status = STATUS_BLOCK
end
end

end


Тут все довольно просто, есть три статуса:

* none - пока ничего страшного
* block - кандидат на блокирование
* blocked - заблокирован

Ну и последнее, так как команда iptables должна выполнятся в режиме root'a, то самым простым решением было написать rake таск и запускать его из рутового кронтаба:

$ sudo crontab -e
*/10 * * * * cd /your/apps/here && RAILS_ENV=production rake

И собственно сам rake таск:


namespace :maintenance do

desc "Block ips"
task :block_ips => :environment do
BlockedIp.pending_for_block.each do |ip|
ip.block!
end
end

end


В принципе при больших нагрузках можно снизить планку ALLOWED_ATEMTPS, но появляется вероятность отсеить не того пользователя.

Rails: страница в странице

В статье об архитектуре Amazon, говориться что все приложение состоит из сотни отдельно-работающих сервисов которые компонуются каким либо образом.

Совсем недавно пришлось сделать что-то подобное. Необходимо на странице отобразить еще несколько страниц со своего же сайта.

Вот что получилось сделать с помощью jquery


$.ajax({
url: "controller/action",
cache: false,
success: function(html){
$("#conatiner").append(html);
}
});


Контроллер же просто по вызову `action` отдает необходимый контент. Причем можно использовать любые параметры. Мне сама идея очень понравилась. Для подгрузки не основных модулей.
Долгое время парился с тем что не мог прологировать запросы которые отсылает ActiveResource. Оказалось все просто, надо добавить одну строчку в enviroment.rb:


ActiveResource::Base.logger = ActiveRecord::Base.logger
Последнее время появилось много обсуждений по поводу rspec, того что он вгрызается в core языка и фреймворка. На самом деле я сам сталкивался с проблемами, когда для того чтобы просто заставить работать rspec требовалось много часов тупого плутания по коду.

В ror2ru многие советуют shoulda, наконец решил его попробовать.

Почитав документации тут и тут написал несколько тестов:

Тестируемый код:

module KeywordsHelper
MIN_LENGHT = 3

def get_keywords(full_text)
words = FrequencyHash.new
full_text.split(/\W/).each do |word|
words.occur(word) if word.length >= MIN_LENGHT
end
words.frequent_keys(2)
end

end


Cам тест:


require 'test_helper'

class KeywordsTest < Test::Unit::TestCase
include KeywordsHelper

context "get_keywords" do
setup do
end

should "return keywords that occur more than 2 times" do
assert_equal ['lazy'], get_keywords("quick lazy frog jumps over the lazy dog")
assert_equal ['frog', 'lazy'], get_keywords("quick lazy frog jumps over the lazy frog")
end

should "not return keywords with lenght smaller than 3 symbols" do
assert_equal [], get_keywords("do do do do")
end

should "return keywords sorted by frequency" do
assert_equal ["dog", "fox"], get_keywords("fox fox dog dog fox dog dog")
end

should "return keywords sorted alphabetically" do
assert_equal ["dog", "fox"], get_keywords("fox fox dog dog fox dog")
end

end

end


В итоге получилось достаточно не плохо, в отличии от стандартных тестов, появились контексты, не надо использовать underscore, плюс масса полезных хелперов.

Все это дело безболезненно заработало вместе с rails 2.3.2

Теперь планирую поюзать shoulda для тестирования контроллеров и проинтегрять с mocha. Если все пройдет гладко, то можно пробовать вводить его для некоторых проектов.
Столкнулся с проблемой, когда при обновлении версий на production сервере код и миграции накатываются в разных транзакциях. В итоге приходит большое количество ошибок, так как модели еще не проапдейтились. Решил написать capistrano таск, который бы вводил сайт в режим "технические работы" и отдавал статическую страничку с чем-то типа "тех обслуживание сайта".

Выглядеть это должно так:


cap maintenance:start
cap maintenance:stop


Причем необходимо сделать зависимости, к примеру если происходит запуск cap deploy после определенного периода времени, автоматически включать статус maintenance, а также выключать его по прошествии определенного времени.

Как это все реализовать еще обсуждается.

Парсинг с помощью libxml

Наконец добрался и до lib-xml. Особых отличий в использовании я не увидел. Производительность на уровне nokogiri.

Вот их документация.

И небольшой пример:


xml = File.read(self.filename)
remove_unused_nodes(xml)

doc = XML::Document.string(xml)
# doc = XML::Document.file(self.filename) # Можно и так создавать документ

doc.find('//VisioDocument/Masters/Master').each do |s|
master = {}
master.store(:base_id, s['BaseID'])
master.store(:master_id, s['ID'])

self.masters << master
end


В целом документ размером в 5-10мб парсит меньше чем пол секунды, для моей задачи более чем приемлемо. На этом я думаю мои искания парсеров закончены :)

Парсинг средствами nokogiri

Возращаясь к теме парсинга файлов. Появилась необходимость распарсить файлы генерируемые Microsoft Visio. Файлы размера порядка 5-10Мб.

Изначально использовал Rexml и сразу стало понятно что при парсинге файлов больше 1мб парсер никуда не годиться. Файл объемом в 10мб он парсил две-три минуты, при этом съедая 400 и больще мб ОЗУ.

Далее попробовал hpricot, но и он не показал особых улучшений. Примерно все стало на 15-20% быстрее, но это все равно долго.

В итоге я добрался до nokogiri, вот он меня приятно порадовал. Файлы размером в 10мб парсились за неполные 2 секунды и памяти при это почти не съедал. Nokogiri для парсинга использует XPath соответственно код получается довольно наглядным и простым. Ниже пример:


xml = File.read(filename)
doc = Nokogiri::XML(xml)

self.page_height = doc.xpath('//VisioDocument/Pages/Page/PageSheet/PageProps')
.search('.//PageHeight').inner_html

doc.xpath('//VisioDocument/Pages/Page/Shapes/Shape').each do |s|
shape = {}
shape.store(:master_id, s.attributes['Master'].text) if s.attributes['Master']

if line_object?(s)
xform1d = s.at('.//XForm1D')
shape.store(:x, xform1d.at('.//BeginX').inner_html)
shape.store(:y, xform1d.at('.//BeginY').inner_html)
shape.store(:to_x, xform1d.at('.//EndX').inner_html)
shape.store(:to_y, xform1d.at('.//EndY').inner_html)

self.line_shapes << shape
end

end


Возможно разница в производительности связана с неправильными использованием библиoтек, но я старался делать все по оригинальной документации.

Парсинг XML файлов в Ruby

Утилитку для парсинга написал довольно давно, но на днях пришлось снова попользоваться, оказалось, что работает она вполне адекватно.

Работает она просто, весь xml файл, вместе с аттрибутами и вложенными тегами, конвертирует в хэш.


module XmlParser

def parse(xml_text, prefix)
items = []
doc = REXML::Document.new(xml_text)
REXML::XPath.each(doc, prefix) do |xml|
item = {}
xml.attributes.each do |k, value|
item.store(k.to_sym, value)
end
translate_children(xml, item)
items << item
end
return items
end

private
def translate_children(xml, item)
return if xml.children.empty?

xml.each_child do |child|
if child.kind_of? REXML::Element
sub_item = {}
if item.include?(child.name.to_sym)
if item[child.name.to_sym].is_a? Array
item.store(child.name.to_sym,
item[child.name.to_sym] << sub_item)
else
item.store(child.name.to_sym,
[item[child.name.to_sym], sub_item])
end
else
item.store(child.name.to_sym, sub_item)
end
if child.has_text?
sub_item[:text] = child.text.strip
end
child.attributes.each do |k, value|
sub_item.store(k.to_sym, value)
end
translate_children(child, sub_item)
end
end
end
end


Пример использования


include XmlParser
parse(xml_text, '/projects')

Клево, теперь можно писать код


public class MyClass {

private int myInt = -1;

public int getMyInt() {
return this.myInt;
}

public void setMyInt(int myInt) {
this.myInt = myInt;
}
}



class Person
attr_reader :name, :age
def initialize(name, age)
@name, @age = name, age
end
def <=>(person) # Comparison operator for sorting
@age <=> person.age
end
def to_s
"#@name (#@age)"
end
end





SELECT *
FROM Book
WHERE price > 100.00
ORDER BY title;