//DEVGURU

Category archives ‘ruby’

Dynamic cookie domains with Rack’s middleware

Friday, April 17th, 2009

Handling sessions in multi-domain environment is not the simplest things to do, because of the fact that cookies are scoped to a domain they were set by.

Recently we were developing an application with such an idea in mind:

  • Application will work as a base for other mini-applications (which we call sites)
  • Each site can be accessed via different url types: site.example.org and example.org/site
  • We want the users to remain logged in when switching from one url type to another

I won’t be covering application structure, routing, etc. here, I will only write about maintaing the sessions is such an environment.

So this is pretty simple here – all that we needed to do was to set cookie domain to .example.org (note the “dot” at the beginning). This could be done via:

ActionController::Base.session = {
  :domain => ".example.org"
}

However there was an additional requirement that we need to deal with:

  • Each site can be accessed via custom domain – site.com
  • Of course there’s no way here to keep the user logged in when he’s switching from site.com to example.org/site or site.example.org, at least it cannot be done with setting cookie domain to whatever value

Technically, to access the site via site.com, that domain must point to our IP address. Then we need to detect that the site is being accessed via custom domain and set cookie domain respectively.

This could be done via some funky before_filters in an Application Controller, however we found much better and cleaner way.

Rack’s middleware to the rescue

Rack itself is a minimal interface between web server and your ruby framework. It’s used by Ruby on Rails (since 2.3) and Merb. The request comes from web server, goes through middleware layers and enters the application.

So we wrote a middleware layer that detects the host with which our application is accessed and sets cookie domain for the request. Here it is:

app/middlewares/set_cookie_domain.rb

class SetCookieDomain
  def initialize(app, default_domain)
    @app = app
    @default_domain = default_domain
  end

  def call(env)
    host = env["HTTP_HOST"].split(':').first
    env["rack.session.options"][:domain] = custom_domain?(host) ? ".#{host}" : "#{@default_domain}"
    @app.call(env)
  end

  def custom_domain?(host)
    domain = @default_domain.sub(/^\./, '')
    host !~ Regexp.new("#{domain}$", Regexp::IGNORECASE)
  end
end

Now we need to turn it on:

environment.rb

config.load_paths += %W( #{RAILS_ROOT}/app/middlewares )

production.rb

config.middleware.use "SetCookieDomain", ".example.org"

.example.org is the default domain that will be used unless the application is accessed via custom domain (like site.com), we give it different values depending on environment (production/staging/development etc).

And since we’re fans of test driven development, here’s the test that ensures us that everything works as expected:

tests/integration/set_cookie_domain_test.rb

require 'test_helper'

class SetCookieDomainTest < ActionController::IntegrationTest

  context "when accessing site at example.org" do
    setup do
      host! 'example.org'
      visit '/'
    end

    should "set cookie_domain to .example.org" do
      assert_equal '.example.org', @integration_session.controller.request.session_options[:domain]
    end
  end

  context "when accessing site at site.com" do
    setup do
      host! 'site.com'
      visit '/'
    end

    should "set cookie_domain to .site.com" do
      assert_equal '.site.com', @integration_session.controller.request.session_options[:domain]
    end
  end

  context "when accessing site at site.example.org" do
    setup do
      host! 'site.example.org'
      visit '/'
    end

    should "set cookie_domain to .example.org" do
      assert_equal '.example.org', @integration_session.controller.request.session_options[:domain]
    end
  end

end

Test is sponsored by great Shoulda and Webrat gems.

Feel free to comment and share.

Managing hosts on OS X with Ghost Gem

Tuesday, January 13th, 2009

Managing local hosts on OS X was pretty easy back before 10.5 with Network Utility, but since Leopard it’s not so nice – we can edit manually /etc/hosts file or play with few not-so-user-friendly terminal commands.

If we want to manage local hosts painlessly, we can use a ghost gem (found via Robby on Rails).
After launching

we can use ghost commands:

Works for me.

Ruby Tuesday

Tuesday, January 6th, 2009

I can proudly announce that the second Ruby Tuesday meeting (informal beer-meeting of an informal group called poznan.rb – Poznań ruby coders) will take place in Głośna Samotność Dragon Pub, Poznań on January 13th. More informations can be found on rubytuesday.pl. Everyone’s invited, we’re open to new people – feel free to come, no matter if you’re earning big money while developing ruby or just coding it for fun. Also it would be nice to spread the word among any ruby developers near Poznań – if you know somebody, let them know.

Przyspieszanie nowych Railsów: memoization

Tuesday, December 9th, 2008

Problem?

Termin memoization nie jest niczym nowym w programowaniu, prawie-rzetelne źródła podają rok 1968 jako pierwsze pojawienie się go.

Na czym polega memoizacja (że tak sobie pozwolę na spolszczenie)? Proste: metoda zapamiętuje (można o tym pomyśleć jak o keszowaniu) swój wynik (w zależności od podanych argumentów) i wywołana kolejny raz – zwraca zapamiętaną i zamrożoną wartość.

Implementacja w Railsach

Memoization było często stosowaną praktyką – pisane “z ręki”, ale jako metoda ActiveSupport weszło do core railsów kilka m-cy temu, dzięki czemu wyszło wraz z wersją 2.2. Spróbujmy!

W ten sposób metoda wywołana kolejny raz nie będzie wykonywać skomplikowanych obliczeń, a jedynie zwróci swoją zmemoizowaną wartość. Co więcej, metoda może przyjmować argumenty – i zapamiętuje swoją wartość w zależności od podanego argumentu!

Tak więc mamy bardzo miły sposób na nie-zarzynanie rubiego.

Kiedy nie stosować memoizacji?

W przypadku dynamiczności zwracanych wyników (kiedy metoda korzysta np. z Time.now). Wyniki są zamrożone dla danego requestu i jeśli chcemy, żeby podczas niego obliczała coś kilka razy – odpuścmy sobie memoizację (lub wykonajmy zmemoizowaną metodę z symbolem :reload jako ostatnim argumentem).

Eksport do CSV w Ruby on Rails

Monday, October 13th, 2008

Wtyczek służacych do eksportu do CSV jest sporo, do tego w Rubym mamy nawet klasę CSV – co jednak, jeśli chcemy skorzystać z najprostszego sposobu eksportu do CSV?

Skorzystamy z Ruport.

Ruport, czyli Ruby Reports, w bardzo “czysty” sposób pozwala nam wyekportować dane do formatów takich jak CSV, PDF, HTML czy ładnie sformatowanego tekstu. W połączeniu z wtyczką acts_as_reportable eksport do CSV to banalna sprawa.

Instalacja

w config/environment.rb dodajemy linijki:

config.gem "ruport"
config.gem "acts_as_reportable"

a następnie za pomocą rake gems:install dodajemy je do naszej maszyny jeśli ich jeszcze nie mamy (uwaga – możliwe, że będziemy musieli zainstalować jeszcze gema fastercsv w odpowiedniej wersji – możemy dokonać tego za pomocą polecenia sudo gem install fastercsv -v 1.2.3).

Użycie

Następnie w odpowiednim modelu (przykładowo – User) dodajemy linię:

acts_as_reportable :only => [:name, :email, :company, :url]

Oczywiście argument :only nie jest wymagany, lecz jeśli nie chcemy zdradzać wszystkim haseł (nawet zakodowanych) czy ID – warto z niego skorzystać. Mamy też dostęp do analogicznie działającego :except czy kilku innych – warto przejrzeć rdocsy wtyczki.

Teraz stworzymy metodę w kontrolerze:

def download_csv
  filename = "lista uzytkownikow"
  response.headers['Content-Type' = 'text/csv'
  response.headers['Content-Disposition'] = 'attachment; filename="' + filename + '.csv"'
  render :text => User.report_table(:all).as(:csv)
end

I w ten sposób wywołując tę akcję dostajemy “ściągalny” plik z wybranymi danymi użytkowników. Banał, prawda?

Oczywiście zastosowań jest więcej – możemy ograniczyć użytkowników (za pomocą conditions czy też named scopes), zapisywać pdf na serwerze czy też zrobić co tylko chcemy – ważne, że wszystko jest proste i czyste.

Fixing fixtures

Thursday, October 9th, 2008

Według American Heritage Dictionary słowo fixture oznacza:

fix·ture (fĭks’chər) n.

  1. Something securely fixed in place.
  2. Something attached as a permanent appendage, apparatus, or appliance: plumbing fixtures.
  3. Law A chattel bound to realty.
  4. One that is invariably present in and long associated with a place: a journalist who became a Washington fixture.
  1. The act or process of fixing.
  2. The condition of being fixed.
Otóż, nic bardziej mylnego. Railsowe fixtures możemy (i niektórzy w netguru z tej możliwości zawzięcie korzystają) z łatwością zamienić na coś innego. Chłopaki z thoughtbot bowiem wypuścili już jakiś czas temu gem factory_girl, który można bardzo łatwo zaprząc do zapełniania aplikacji danymi testowymi.
W tym miejscu należy się chyba odrobina wyjaśnienia do czego ja używam fixtures i dlaczego tradycyjna metoda mi nie odpowiada:
  1. Populacja bazy danych celem przeprowadzenia testów.
  2. Populacja bazy danych celem wygodniejszego ręcznego testowania aplikacji.
W pierwszym przypadku wolę przed konkretnymi testami tworzyć samemu potrzebne modele, żeby później zaglądając do pliku z testami mieć jasność na jakich danych operuję. Konieczność utrzymywania dużej bazy różnorodnych fixtures, żeby pasowały do wszystkich testów, które sobie wymyślę, jest męcząca.
W drugim przypadku tradycyjne fixtures odpadają ze względu na paskudny sposób definiowania relacji miedzy obiektami. Podawanie id z ręki dla mnie to czynność, którą można było wykonywać w XX wieku, a nie teraz. Foxy fixtures natomias generują tak kosmiczne id, że Sphinx sobie kiepsko radzi z indeksowaniem, mikroskopijnej przecież zwykle, bazy.
Po tej krótkiej dygresji na temat tego co mi się nie podoba, czas przejść do tego co mi się podoba, czyli wspomnianego wcześniej factory_girl. Opisywać instalacji ani dokumentacji nie bedę, bo każdy sobie sam po angielsku przeczyta, tylko napiszę co mi się w tym podoba.
Otóż mam możliwość prostego generowania obiektów, nawet tych bardziej skomplikowanych. Przykładowo mamy w aplikacji użytkowników, których logowanie jest obsługiwane przez restful_authentication. Czyli od samego początku mamy już ładne kilka pól w bazie do zdefiniowania przy tworzeniu takowego. Dzięki factory_girl (i dalej opisanej definicji modelu) możemy stworzyć go za pomocą prostego
Factory :user
Jeśli jednak zależy nam na użytkowniku, który ma konkretny login i nie jest jeszcze aktywowany, to możemy nadpisać domyślne ustawienia:
Factory :user, :login => ‘gracjan’, :activated_at => nil
Aby powyższe zadziałało wystarczy zdefiniować gdzieś (np. w lib) obiekty, które będziemy generować. Przykładową definicja (ze względu na czytelność) można obejrzeć na pastie.org.
Dodam jeszcze, że generowanie obiektów w ten sposób uruchamia wszelkie before- i after_create oraz walidacje i już mamy pełne wyjaśnienie dlaczego zapomniałem tradycyjne fixtures i poznałem starych Rolling Stonesów.

Test-driven development w RoR – wprowadzenie

Monday, August 18th, 2008

Zacznę może od oczywistości doskonale znanej wszystkim, którzy mieli okazję pracować nad projektem większym, niż “blog w 15 minut”: wraz z rozwojem aplikacji rośnie liczba błędów, które wkradają się niepostrzeżenie “gdzieś, kiedyś”. Drobna zmiana jednej funkcjonalności potrafi wywalić połowę aplikacji – a to niezalogowany użytkownik dostaje w twarz wyjątek (bo nam, zalogowanym na developmencie przecież działa!), a to mail się nie wyśle, bo nie lubi użytkownika nil. Częstą praktyką jest zostawianie wyławiania takich “smaczków” na moment “tydzień-przed-wdrożeniem”. Często też “tydzień-przed-wdrożeniem” okazuje się być “potrzebujemy-jeszcze-pięciu-kolejnych-dni”, “trzeba-to-przepisać-bo-nie-działa” czy tym podobnymi… Na szczęście można temu zaradzić. Panie i panowie (i programiści), kłania się przed wami Test-driven development.

Zamysł jest taki: zanim napiszemy faktyczny kod – piszemy krótkie “przypadki testowe” (z ang. test cases). Przykłady?

  • zanim napiszemy funkcję do autentykacji użytkownika – piszemy test case, który sprawdza, czy logowanie przykładowego użytkownika powiodło się
  • zanim napiszemy faktyczne powiązania pomiędzy modelami – opisujemy je w testach
  • zanim napiszemy walidację konkretnego modelu – rozpisujemy komplet przypadków testowych, który atakuje model poprawnymi (lub niepoprawnymi) danymi i sprawdzamy, czy się zapisuje (lub też nie)

Po napisaniu test case zabieramy się za pisanie właściwej funkcjonalności, odpalamy testy i cieszymy się, jeśli wszystkie przeszły – a jeśli nie, to poprawiamy kod. Następnie piszemy kolejny test. Piszemy funkcjonalność. Odpalamy testy…
“Po co mam pisać dwa razy to samo?”, “czy to się nie kłóci z agile software development?”. Jeśli nie zadaliście sobie tych pytań, to szkoda. Ja zadałem. Zyskujemy kilka rzeczy:

  • Kompletny zestaw testów. Pozwala mocno zaoszczędzić czas (nawet, jeśli z początku wydaje się być inaczej) – poza wspomnianym już problemem debugowania przed wdrożeniem, testy dają nam również pewność, że aplikacja działa w różnych środowiskach (development, staging, production…), które nie zawsze przecież są na tych samych maszynach. Do tego jesteśmy świadomi błędów gdy zmiana jednej funkcjonalności, od której zależne są inne, spowoduje wysypanie się któregoś miejsca aplikacji, nawet bardzo ukrytego (jeśli jest pokryte testami, oczywiście).
  • Klarowne spojrzenie na cały projekt. Możemy bardziej skupić się na tym, jak aplikacja powinna działać w środku – pisząc test nie obchodzi nas, jak będzie wyglądała funkcja logująca użytkownika – obchodzi nas za to, w jaki sposób może zalogować się użytkownik. (Prosta myśl, ale trudna do przekazania).

Ok, uwierzcie mi: testy są przydatne. Do tego pisanie testów może być przyjemne i zautomatyzowane. Pisanie testów może uratować życia. Od czego zacząć?

Na start polecam shoulda plugin. Załóżmy, że chcemy mieć dzieci… Co powinno mieć dziecko? Imię, wagę, wzrost. Generujemy model za pomocą script/generate. Dostajemy model w /app/models i test w /test/units. Zacznijmy od testu.


require File.dirname(__FILE__) + '/../test_helper'

class ChildTest < ActiveSupport::TestCase
  should_have_db_columns :name, :weight, :height, :gender, :father_id
end

Następnie tworzymy migrację. Uwzględniamy w niej wymagane pola. Odpalamy rake db:migrate. Odpalamy test: rake test:units. Cieszymy się.

Dodajmy przykładowe dziecko do testów. Do pliku /test/fixtures/children.yml wrzućmy Timmy’ego:


timmy:
  id: 1
  name: Timmy
  height: 103
  weight: 15
  father_id: 1

Sporo waży. Dziecko powinno też mieć ojca:


require File.dirname(__FILE__) + '/../test_helper'

class ChildTest < ActiveSupport::TestCase
  should_have_db_columns :name, :weight, :height, :gender, :father_id

  should_belong_to :father

end

W modelu dodajemy odpowiednią relację:


belongs_to :father

Odpalamy testy. Cieszymy się. Co jednak, gdy dziecko nie ma ojca? Jest sierotą. Sprawdzimy to. Najpierw testem:


require File.dirname(__FILE__) + '/../test_helper'

class ChildTest < ActiveSupport::TestCase
  should_have_db_columns :name, :weight, :height, :gender, :father_id
  should_require_attributes :name

  should_belong_to :father

  context "A child" do
    setup do
      @child = children(:timmy)
    end
    should "be an orphan when father_id is null" do
      @child.father_id = nil
      assert_equal true, @child.orphan?
    end
  end
end

Następnie metoda w modelu:


def orphan?
  return self.father_id.nil?
end

Odpalamy test. Przechodzi. Dziecko jest sierotą. Możemy napisać kolejny test sprawdzający, czy dziecko mające ojca nie jest sierotą. Możemy sprawdzić, czy ojciec ma więcej dzieci i w tym przypadku sprawdzić, czy jest jedynakiem. Możemy sprawdzić walidację dziecka bez wagi (wiadomo, że takie nie istnieją)…

W następnym poście opiszę narzędzia, które pozwalają na zautomatyzowanie testowania czy też sprawdzanie procentowego pokrycia kodu testami – a póki co polecam przejrzeć dokumentację rdocs pluginu shoulda. To najlepsze źródło do zrozumienia idei TDD w railsach, na jakie trafiłem.

Import ticketów do Trac-a z pomocą Ruby, część 2

Tuesday, July 22nd, 2008

Jest to uaktualnienie pomysłu Wiktora na automatyczny i masowy import ticketów do Trac-a. Dla przypomnienia: założenie jest takie, że tickety mamy w pliku tekstowym, gdzie jedna linia = jeden ticket.

Nazwy pól formularza dodawania nowego ticketu zostały zmienione w obecnej wersji Trac-a (0.11), dlatego też trzeba było uaktualinić je również w skrypcie. Przy okazji skrypt został przekształcony w rake task, do którego parametry podaje się z linii poleceń.

Kod pliku znajduje się na pastie.

Użycie:

rake trac:import url=http://path.to.svn/trac.cgi/login user=username
  pass=password owner=username file=tickets.txt

url – adres URL strony logowania do Trac-a
user i pass – dane konta do logowania
owner – komu przypisać ticket
file – plik z ticketami (jedna linia = jeden ticket)

W razie potrzeb łatwo zedytować plik dopisując dodatkowe pola do wypełnienia w formularzu.

Kod korzysta z gema Mechanize. Instalacja: gem install mechanize.

Webistrano – bezbolesny deployment

Tuesday, June 3rd, 2008

Peritor Webistrano, Flaker - Mozilla Firefox

Jak pewnie gdzieś już pisałem od dawna korzystamy podczas developmentu z SVN oraz z Capistrano. Niedawno nasi zachodni sąsiedzi z firmy peritor stworzyli narzędzie webistrano, które po wprowadzeniu drobnych poprawek (prawa dostępu i style) wdrożyliśmy u siebie.

Dlaczego warto skorzystać z webistrano?

  1. Prosta instalacja i względnie prosta konfiguracja.
  2. Bezpieczeństwo (nie ma konieczności dawania dostępu do shella)
  3. Prosta obsługa – deployment polega na kliknięciu w przycisk deploy.
  4. Łatwe cofanie problematycznych deployów (deploy:rollback)
  5. Przejrzysta historia deployów wraz z informacją kto go wykonał (wiadomo na kogo zwalić winę jak coś nie działa :P)
  6. Obsługa Ruby on Rails (mongrel, passenger) jak i PHP
  7. Automatyczne nadawanie praw odpowiednim katalogo.
  8. Wiele innych możliwości (jeśli umiesz pisać skrypty do rake’a)

PS: Jak ktoś ma propozycję na polski odpowiednik słowa deploy/deployment to proszę o komentarz :-)

Caching in Rails – prezentacja z RuPy 2008

Tuesday, June 3rd, 2008

Jak to mówią – lepiej późno niż później…

Poniżej prezentacja jaką miałem przyjemność wygłosić podczas RuPy 2008:

Import ticketów do Trac-a z pomocą ruby

Saturday, November 3rd, 2007

Kolejna historyjka z życia wzięta…

Klient przesyła feedback do projektu w pliku doc/txt czy po prostu w mailu. Załóżmy, że jest w miarę porządny i każda linijka odpowiada jednemu problemowi. I teraz co ma zrobić PM? Przepisywać kolejne tickety do firmowego Traca?

Nie wiem jak Wy, ale jak ja mam zrobić tą samą czynność więcej niż 5 razy to wolę wymyślić sposób, aby zrobił to za mnie ktoś inny (najlepiej komputer).

Oto jak to może wyglądać:

Jeśli masz dostęp do bazy (czyli hostujesz traca u siebie) skorzystasz z TracImport. Gorzej jeśli kupiłeś pakiet SVN+Trac na zewnętrznym hoście i nie masz danych mySQL.

Druga możliwość jaka się pojawia to wywołanie przez skrypt bezpośrednio przez POST formularza “New Ticket”. Dwa problemy: logowanie i “__FORM_TOKEN” – zabezpieczenie Traca przed CSRF. Niestety prosty skrypt z curl nie załatwi sprawy.

Na szczęście jest Ruby i Mechanize… Czyli krótka piłka:

  1. Logujemy się
  2. Pobieramy formularz New Ticket
  3. Wpisujemy dane
  4. Submitujemy
  5. Wróć do 2 jeśli pozostały tickety do dodania

Skrypt do pobrania na pastie.

W razie pytań piszcie w komentarzach…

Serwer dedykowany z Twoją aplikacją Ruby on Rails w 10 minut

Wednesday, October 17th, 2007

No dobrze, czyli obejrzałeś kilka screencastów, przerobiłeś parę tutoriali i postanowiłeś zrobić swoją własną aplikację Ruby on Rails?

Lokalnie wszystko działa, mongrel serwuje (wolno, bo wolno) strony, baza przetwarza rekordy, testy przechodzą w 100%.

Skończyłeś aplikację i pojawia się problem: Na jakim serwerze ją umieścić i jak go skonfigurować?

Zaczynasz od Dreamhosta… po tygodniu walki z killowanymi procesami odpuszczasz.

Wszyscy mówią: “serwer dedykowany”… i niestety mają rację. Jeśli Twoja aplikacja kierowana jest na rynek US – polecam SliceHost, a jeśli startujesz na Polskę/Europę – Hetzner.

OK, to teraz tylko jeden problem: Jak z czystej instalacji Ubuntu zrobić serwer Railsowy?

Z całej tej przygody jest to wbrew pozorom najłatwiejsze…

Deprec pozwala w 10 minut ustawić czyściutką instalację Ubuntu. Oto jak:

  1. Upewniamy się, że nasza aplikacja jest dostępna w repozytorium SVN (jakże mogłoby być inaczej)
  2. Upewniamy się, że w config/database.yml dane serwera dla production to localhost/root/[bez_hasła]
  3. Logujemy się na serwer i tworzymy usera (np. deploy) nadajemy mu prawa sudo (dodajemy jego login w /etc/sudoers)
  4. Logujemy się na serwer jako wcześniej stworzony user (deploy) i robimy testowego checkouta z repozytorium podając usera i hasło (aby zostało zapamiętane) – jak zacznie ściągać można anulować Ctrl-C i usunąć tymczasowy katalog
  5. Ściągamy na domowym komputerze gem deprec (gem install -y deprec)
  6. W katalogu głównym naszej aplikacji uruchamiamy ‘deprec –apply-to .’ (z kropką)
  7. Edytujemy plik config/deploy.rb dodając odpowiednie wartości dla domain (adres/IP serwera), application (nazwa aplikacji – dowolna) i repository (ścieżka do repozytorium SVN)
  8. No i po tych krótkich przygotowania już tylko sam lukier – uruchamiamy na domowym komputerze w katalogu głównym aplikacji po kolei:
    cap install_rails_stack – instaluje wszystko – od apache do gemów
    cap setup – ustawia naszą aplikację
    cap deploy_with_migrations – wrzuca aplikację na serwer i migruje bazę
    cap restart_apache – wiadomo…
  9. Przy odrobinie szczęścia i szybkim serwerze po 5 minutach aplikacja jest już LIVE!

Jeden z ostatnich argumentów przeciwników RoR pada… instalacja i deployment serwisów w Ralisach przy użyciu deprec i capistrano to kaszka z mleczkiem ;-)