Aplikacja Django na Google App Engine - Część I (konfiguracja aplikacji i pierwsze wdrożenie)

Platforma SaaS o nazwie Google App Engine (w skrócie GAE) jest wspaniałą alternatywą dla kosztownych opcji hostowania aplikacji opartych o serwery dedykowane czy konta współdzielone. Udostępnia ona całą infrastrukturę tworzenia aplikacji z wykorzystaniem języka Python lub języków opartych o JVM. Oczywiście istnieją ograniczenia związane z architekturą GAE, wymuszając pewne sposoby pracy z platformą (niejako wymuszając właściwe rozwiązania, również ze względu na optymalizacje kosztów)

GAE udostępnia tak naprawdę standardową infrastrukturę aplikacji opartą o sprawdzone rozwiązania firmy Google. Od jakiegoś czasu istnieje również możliwość uzyskania komercyjnego wsparcia.

Wykorzystując GAE mamy możliwość skorzystania z darmowych limitów przysługujących na każdą aplikacje by wraz ze wzrostem popularności naszej aplikacji przesiąść się na komercyjne konto. Takie podejście pozwala nam również na całkowite zrzucenie odpowiedzialności i zadań związanych ze skalowaniem naszej aplikacji co jest największą zaletą tej platformy.

W dzisiejszym wpisie chciałbym "pokrótce" przeprowadzić czytelnika przez proces uruchomienia przykładowej aplikacji bazującej na GAE używając zmodyfikowanej wersji Django (popularnego frameworka bazującego na pythonie).

Standardowa wersja Django (obecna wersja 1.2) nie posiada jeszcze wsparcia dla nierelacyjnych baz danych takich jak BigTable czy MongoDB. Na szczęście dzięki pracy panów z All Buttons Pressed mamy możliwość wykorzystania zmodyfikowanej wersji Django, która zawiera odpowiednie backendy wspierające w/w bazy danych.

Pierwszą czynnością którą, musimy wykonać (zaraz po zainstalowaniu GAE na swoim systemie) celem stworzenia aplikacji bazującej na GAE, jest zalogowanie się na stronie Google App Engine:

Podstawową zaletą korzystania z usług Google jest użycie tego samego konta do wielu usług w tym również do GAE. W tym celu wprowadzamy nazwę użytkownika i logujemy się:

Zaraz po zalogowaniu widzimy zaproszenie do rejestracji nowej aplikacji (w przypadku późniejszego logowania w tym miejscu pojawi się również list naszych aplikacji). Obecnie limit własnych aplikacji wynosi 10 i nie obejmuje aplikacji udostępnionych nam przez inne osoby.

W przypadku pierwszej rejestracji aplikacji wymagana jest aktywacja poprzez SMS:

której dokonujemy wprowadzając kod aktywacyjny przesłany na naszą komórkę:

Po poprawnej aktywacji przechodzimy do rejestracji aplikacji wpisując unikalną nazwę naszej aplikacji, dokonując dodatkowych ustawień oraz akceptując regulamin usługi.

Jak widać na obrazku rejestrowana aplikacja będzie dostępna pod adresem officeshoppinglist.appspot.com zaraz po wgraniu pierwszej wersji kodu. Istnieje możliwość podpięcia naszej aplikacji pod własny adres URL lecz istnieje ograniczenie pozwalające na podpięcie tylko jako poddomeny np.:

http://www.mojadomena.pl (zamiast http://mojadomena.pl)

Następnie powinniśmy zobaczyć informacje o prawidłowo zarejestrowanej aplikacji:

Po przejściu na dashboard powinniśmy uzyskać dostęp do podstawowych opcji konfiguracyjnych naszej aplikacji jak również zarządzania i podglądu stanu:

Kolejnym krokiem po zarejestrowaniu naszej aplikacji będzie stworzenie repozytorium (w tym przypadku repozytorium GIT, na github.com)

Zgodnie z tymi instrukcjami przygotujemy nasz pusty projekt:

A więc...

tworzymy katalog:

~/gae$ mkdir officeshoppinglist

tworzymy pusty plik:

~/gae$ cd officeshoppinglist
~/gae/officeshoppinglist$ touch .gitignore

inicjujemy repozytorium GIT'a:

~/gae/officeshoppinglist$ git init
Initialized empty Git repository in /Users/andrzejsliwa/gae/officeshoppinglist/.git/

dodajemy nasz pusty plik:

~/gae/officeshoppinglist$ git add .
~/gae/officeshoppinglist$ git commit -m "initial commit."
[master (root-commit) d142cc5] initial commit.
 0 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 .gitignore

podpinamy zdalne repozytorium oraz wysyłamy je w obecnym stanie na serwer:

~/gae/officeshoppinglist[master]$ git remote add origin git@github.com:andrzejsliwa/officeshoppinglist.git
~/gae/officeshoppinglist[master]$ git push origin master
Counting objects: 3, done.
Writing objects: 100% (3/3), 219 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
To git@github.com:andrzejsliwa/officeshoppinglist.git
 * [new branch]      master -> master

Wynikiem naszych działań powinno być dostępne repozytorium:

W tym momencie możemy przystąpić do konfigurowania naszego projektu. W tym celu pobieramy niezbędne zależności w formie repozytoriów mercuriala.

Pobieramy zmodyfikowaną wersję Django 1.2 która wspiera nierelacyjne bazy danych (w tym przypadku BigTable):

~/gae/officeshoppinglist[master]$ cd ..
~/gae$ hg clone http://bitbucket.org/wkornewald/django-nonrel
destination directory: django-nonrel
requesting all changes
adding changesets
adding manifests
adding file changes
added 8537 changesets with 21389 changes to 2919 files
updating to branch default
2266 files updated, 0 files merged, 0 files removed, 0 files unresolved

oraz dodatkowe wymagane repozytoria:

~/gae$ hg clone http://bitbucket.org/wkornewald/djangoappengine
destination directory: djangoappengine
requesting all changes
adding changesets
adding manifests
adding file changes
added 98 changesets with 194 changes to 47 files
updating to branch default
34 files updated, 0 files merged, 0 files removed, 0 files unresolved

~/gae$ hg clone http://bitbucket.org/wkornewald/djangotoolbox
destination directory: djangotoolbox
requesting all changes
adding changesets
adding manifests
adding file changes
added 40 changesets with 86 changes to 46 files
updating to branch default
23 files updated, 0 files merged, 0 files removed, 0 files unresolved

W tym momencie możemy podlinkować nasze zależności do projektu za pomocą linków symbolicznych:

ln -s ~/gae/django-nonrel/django officeshoppinglist/django
ln -s ~/gae/djangoappengine/ officeshoppinglist/djangoappengine
ln -s ~/gae/djangotoolbox/djangotoolbox officeshoppinglist/djangotoolbox

Następnie wracamy do naszego projektu i dodajemy reguły ignorowania do naszego pliku .gitignore:

~/gae$ cd officeshoppinglist
.DS_Store
*.pyc
*.swp
djangoappengine
djangotoolbox
django

Tworzymy plik app.yml, gdzie wpis application musi się zgadzać z nazwą zarejestrowanej aplikacji:

application: officeshoppinglist
version: 1
runtime: python
api_version: 1

default_expiration: '365d'

handlers:
- url: /remote_api
  script: $PYTHON_LIB/google/appengine/ext/remote_api/handler.py
  login: admin

- url: /_ah/queue/deferred
  script: djangoappengine/deferred/handler.py
  login: admin

- url: /media/admin
  static_dir: django/contrib/admin/media

- url: /media
  static_dir: media

- url: /robots.txt
  static_files: robots.txt
  upload: robots.txt
  secure: optional

- url: /.*
  script: djangoappengine/main/main.py

Kolejno tworzymy: plik cron.yml:

cron:
- description: keep alive
  url: /
  schedule: every 2 minutes

plik index.yaml:

indexes:

- kind: django_admin_log
  properties:
  - name: user_id
  - name: action_time
    direction: desc

- kind: django_content_type
  properties:
  - name: app_label
  - name: name

# AUTOGENERATED

# This index.yaml is automatically updated whenever the dev_appserver
# detects that a new type of query is run.  If you want to manage the
# index.yaml file manually, remove the above marker line (the line
# saying "# AUTOGENERATED").  If you want to manage some indexes
# manually, move them above the marker line.  The index.yaml file is
# automatically uploaded to the admin console when you next deploy
# your application using appcfg.py.

plik robots.txt:

User-agent: * Disallow: /

plik manage.py (oraz dodajemy mu prawa do wykonania):

#!/usr/bin/env python

# Add "common-apps" folder to sys.path if it exists
import os, sys
common_dir = os.path.join(os.path.dirname(__file__), 'common-apps')
if os.path.exists(common_dir):
    sys.path.append(common_dir)

# Initialize App Engine SDK if djangoappengine backend is installed
try:
    from djangoappengine.boot import setup_env
except ImportError:
    pass
else:
    setup_env()

from django.core.management import execute_manager
try:
    import settings # Assumed to be in the same directory.
except ImportError:
    import sys
    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customi
    sys.exit(1)

if __name__ == "__main__":
    execute_manager(settings)
chmod +x manage.py

plik urls.py:

from django.conf.urls.defaults import *
# Uncomment the next two lines to enable the admin
from django.contrib import admin

urlpatterns = patterns('',
    # Uncomment the next line to enable the admin:
    url(r'^admin/', include(admin.site.urls)),
)

plik settings.py:

try:
    from djangoappengine.settings_base import *
    has_djangoappengine = True
except ImportError:
    has_djangoappengine = False
    DEBUG = True
    TEMPLATE_DEBUG = DEBUG

import os

SECRET_KEY = '!6r1e$z801cxu#d#rcgsnpvw0g#bn62nqz10#-ci+qlvalaf&1'

INSTALLED_APPS = (
    'djangotoolbox',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.sessions',
    'django.contrib.contenttypes',
)

if has_djangoappengine:
    INSTALLED_APPS = ('djangoappengine',) + INSTALLED_APPS

ADMIN_MEDIA_PREFIX = '/media/admin/'
MEDIA_ROOT = os.path.join(os.path.dirname(__file__), 'media')
TEMPLATE_DIRS = (os.path.join(os.path.dirname(__file__), 'templates'),)

ROOT_URLCONF = 'urls'

oraz bardzo ważny pusty plik init.py:

~/gae/officeshoppinglist[master]$ touch __init__.py

Tworzymy pliki szablonów i standardowych błędów zgodne z systemem django http://github.com/andrzejsliwa/officeshoppinglist/tree/master/templates/. Wynikiem naszych działań powinna być taka oto struktura projektu:

~/gae/officeshoppinglist[master*]$ tree
.
|-- __init__.py
|-- __init__.pyc
|-- app.yaml
|-- cron.yaml
|-- django -> /Users/andrzejsliwa/gae/django-nonrel/django
|-- djangoappengine -> /Users/andrzejsliwa/gae/djangoappengine/
|-- djangotoolbox -> /Users/andrzejsliwa/gae/djangotoolbox/djangotoolbox
|-- index.yaml
|-- manage.py
|-- robots.txt
|-- settings.py
|-- settings.pyc
|-- templates
|   |-- 404.html
|   |-- 500.html
|   `-- base.html
|-- urls.py
`-- urls.pyc

4 directories, 14 files

W tym momencie jesteśmy gotowi do testowego uruchomienia naszej aplikacji za pomocą polecenia:

~/gae/officeshoppinglist[master]$ ./manage.py runserver

Po otwarciu aplikacji pod adresem http://localhost:8000 powinniśmy zobaczyć:

oraz taki output w konsoli:

W tym momencie możemy uruchomić wdrożenie naszej aplikacji na serwer produkcyjny:

./manage.py deploy

Wynikiem tego polecenia (które pyta o dane logowania by nas uwierzytelnić) jest:

./manage.py deploy
Application: officeshoppinglist; version: 1.
Server: appengine.google.com.
Scanning files on local disk.
Scanned 500 files.
Scanned 1000 files.
Initiating update.
Email: sliwa.andrzej@gmail.com
Password for sliwa.andrzej@gmail.com:
Cloning 79 static files.
Cloning 1266 application files.
Cloned 100 files.
Cloned 200 files.
Cloned 300 files.
Cloned 400 files.
Cloned 500 files.
Cloned 600 files.
Cloned 700 files.
Cloned 800 files.
Cloned 900 files.
Cloned 1000 files.
Cloned 1100 files.
Cloned 1200 files.
Deploying new version.
Checking if new version is ready to serve.
Will check again in 1 seconds.
Checking if new version is ready to serve.
Will check again in 2 seconds.
Checking if new version is ready to serve.
Will check again in 4 seconds.
Checking if new version is ready to serve.
Will check again in 8 seconds.
Checking if new version is ready to serve.
Will check again in 16 seconds.
Checking if new version is ready to serve.
Closing update: new version is ready to start serving.
Uploading index definitions.
Uploading cron entries.
Running syncdb.
Login via Google Account:sliwa.andrzej@gmail.com
Password:
Traceback (most recent call last):
  File "./manage.py", line 26, in <module>
    execute_manager(settings)
  File "/Users/andrzejsliwa/gae/officeshoppinglist/django/core/management/__init__.py", line 438, in execute_manager
    utility.execute()
  File "/Users/andrzejsliwa/gae/officeshoppinglist/django/core/management/__init__.py", line 379, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/Users/andrzejsliwa/gae/officeshoppinglist/djangoappengine/management/commands/deploy.py", line 72, in run_from_argv
    run_appcfg(argv)
  File "/Users/andrzejsliwa/gae/officeshoppinglist/djangoappengine/management/commands/deploy.py", line 53, in run_appcfg
    call_command('syncdb', remote=True, interactive=True)
  File "/Users/andrzejsliwa/gae/officeshoppinglist/django/core/management/__init__.py", line 166, in call_command
    return klass.execute(*args, **defaults)
  File "/Users/andrzejsliwa/gae/officeshoppinglist/django/core/management/base.py", line 218, in execute
    output = self.handle(*args, **options)
  File "/Users/andrzejsliwa/gae/officeshoppinglist/django/core/management/base.py", line 347, in handle
    return self.handle_noargs(**options)
  File "/Users/andrzejsliwa/gae/officeshoppinglist/django/core/management/commands/syncdb.py", line 103, in handle_noargs
    emit_post_sync_signal(created_models, verbosity, interactive, db)
  File "/Users/andrzejsliwa/gae/officeshoppinglist/django/core/management/sql.py", line 185, in emit_post_sync_signal
    interactive=interactive, db=db)
  File "/Users/andrzejsliwa/gae/officeshoppinglist/django/dispatch/dispatcher.py", line 162, in send
    response = receiver(signal=self, sender=sender, **named)
  File "/Users/andrzejsliwa/gae/officeshoppinglist/django/contrib/contenttypes/management.py", line 11, in update_contenttypes
    content_types = list(ContentType.objects.filter(app_label=app.__name__.split('.')[-2]))
  File "/Users/andrzejsliwa/gae/officeshoppinglist/django/db/models/query.py", line 83, in __len__
    self._result_cache.extend(list(self._iter))
  File "/Users/andrzejsliwa/gae/officeshoppinglist/django/db/models/query.py", line 269, in iterator
    for row in compiler.results_iter():
  File "/Users/andrzejsliwa/gae/officeshoppinglist/djangotoolbox/db/basecompiler.py", line 219, in results_iter
    for entity in self.build_query(fields).fetch(low_mark, high_mark):
  File "/Users/andrzejsliwa/gae/officeshoppinglist/djangoappengine/db/compiler.py", line 95, in fetch
    results = query.Run(**kw)
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/api/datastore.py", line 1148, in Run
    return self._Run(**kwargs)
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/api/datastore.py", line 1185, in _Run
    str(exc) + '\nThis query needs this index:\n' + yaml)
google.appengine.api.datastore_errors.NeedIndexError: The index for this query is not ready to serve. See the Datastore Indexes page in the Admin Console.
This query needs this index:
- kind: django_content_type
  properties:
  - name: app_label
  - name: name

Wyjątek w tym miejscu jest czymś normalnym ze względu na to że indeksy potrzebują czasu na zbudowanie co możemy potwierdzić obserwując nasz dashboard w sekcji Datastore Indexes:

Po poprawnym wdrożeniu powinniśmy zobaczyć taki oto wynik (wprowadzenia adresu: http://officeshoppinglist.appspot.com/admin/):

oraz taki wynik (wprowadzenia adresu: http://officeshoppinglist.appspot.com/):

Po zbudowaniu indeksów kolejne wdrożenie odbędzie się już bez błędów:

~/gae/officeshoppinglist[master*]$ ./manage.py deploy
Application: officeshoppinglist; version: 1.
Server: appengine.google.com.
Scanning files on local disk.
Scanned 500 files.
Scanned 1000 files.
Initiating update.
Email: sliwa.andrzej@gmail.com
Password for sliwa.andrzej@gmail.com:
Cloning 79 static files.
Cloning 1266 application files.
Cloned 100 files.
Cloned 200 files.
Cloned 300 files.
Cloned 400 files.
Cloned 500 files.
Cloned 600 files.
Cloned 700 files.
Cloned 800 files.
Cloned 900 files.
Cloned 1000 files.
Cloned 1100 files.
Cloned 1200 files.
Uploading 1 files and blobs.
Uploaded 1 files and blobs
Deploying new version.
Checking if new version is ready to serve.
Will check again in 1 seconds.
Checking if new version is ready to serve.
Will check again in 2 seconds.
Checking if new version is ready to serve.
Will check again in 4 seconds.
Checking if new version is ready to serve.
Will check again in 8 seconds.
Checking if new version is ready to serve.
Will check again in 16 seconds.
Checking if new version is ready to serve.
Will check again in 32 seconds.
Checking if new version is ready to serve.
Closing update: new version is ready to start serving.
Uploading index definitions.
Uploading cron entries.
Running syncdb.
Login via Google Account:sliwa.andrzej@gmail.com
Password:
No fixtures found.

Polecam również przyjrzeć się bliżej dostępnym poleceniom manage.py:

~/gae/officeshoppinglist[master*]$ ./manage.py
Usage: manage.py subcommand [options] [args]

Options:
  -v VERBOSITY, --verbosity=VERBOSITY
                        Verbosity level; 0=minimal output, 1=normal output,
                        2=all output
  --settings=SETTINGS   The Python path to a settings module, e.g.
                        "myproject.settings.main". If this isn't provided, the
                        DJANGO_SETTINGS_MODULE environment variable will be
                        used.
  --pythonpath=PYTHONPATH
                        A directory to add to the Python path, e.g.
                        "/home/djangoprojects/myproject".
  --traceback           Print traceback on exception
  --version             show program's version number and exit
  -h, --help            show this help message and exit

Type 'manage.py help <subcommand>' for help on a specific subcommand.

Available subcommands:
  changepassword
  cleanup
  compilemessages
  createcachetable
  createsuperuser
  dbshell
  deploy
  diffsettings
  dumpdata
  flush
  inspectdb
  loaddata
  makemessages
  remote
  reset
  runfcgi
  runserver
  shell
  sql
  sqlall
  sqlclear
  sqlcustom
  sqlflush
  sqlindexes
  sqlinitialdata
  sqlreset
  sqlsequencereset
  startapp
  syncdb
  test
  testserver
  validate

Szczególnie poleceniu remote, które pozwala nam na wykonywanie pozostałych poleceń na zdalnym wdrożonym systemie:

~/gae/officeshoppinglist[master*]$ ./manage.py remote syncdb
INFO     2010-08-02 07:44:17,050 base.py:154] Setting up remote_api for "officeshoppinglist" at http://officeshoppinglist.appspot.com/remote_api
INFO     2010-08-02 07:44:17,061 appengine_rpc.py:159] Server: officeshoppinglist.appspot.com
INFO     2010-08-02 07:44:17,061 base.py:162] Now using the remote datastore for "officeshoppinglist" at http://officeshoppinglist.appspot.com/remote_api
Login via Google Account:sliwa.andrzej@gmail.com
Password:
No fixtures found.
~/gae/officeshoppinglist[master*]$ ./manage.py remote shell
INFO     2010-08-02 07:45:52,392 base.py:154] Setting up remote_api for "officeshoppinglist" at http://officeshoppinglist.appspot.com/remote_api
INFO     2010-08-02 07:45:52,405 appengine_rpc.py:159] Server: officeshoppinglist.appspot.com
INFO     2010-08-02 07:45:52,407 base.py:162] Now using the remote datastore for "officeshoppinglist" at http://officeshoppinglist.appspot.com/remote_api
Python 2.5.5 (r255:77872, Jun 12 2010, 00:13:50)
[GCC 4.2.1 (Apple Inc. build 5659)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>

Na koniec dodajemy wszystkie pliki do repozytorium i wypycham je na zdalny serwer GIT'a:

~/gae/officeshoppinglist[master*]$ git add .
~/gae/officeshoppinglist[master*]$ git commit -m "initial import."
[master c6b310e] initial import.
 11 files changed, 186 insertions(+), 0 deletions(-)
 create mode 100644 __init__.py
 create mode 100644 app.yaml
 create mode 100644 cron.yaml
 create mode 100644 index.yaml
 create mode 100755 manage.py
 create mode 100644 robots.txt
 create mode 100644 settings.py
 create mode 100644 templates/404.html
 create mode 100644 templates/500.html
 create mode 100644 templates/base.html
 create mode 100644 urls.py

~/gae/officeshoppinglist[master]$ git push
Counting objects: 16, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (13/13), done.
Writing objects: 100% (14/14), 3.16 KiB, done.
Total 14 (delta 0), reused 0 (delta 0)
To git@github.com:andrzejsliwa/officeshoppinglist.git
   d142cc5..c6b310e  master -> master

Aktualny kod: http://github.com/andrzejsliwa/officeshoppinglist

Lektura obowiązkowa: http://www.allbuttonspressed.com/projects/django-nonrel http://www.allbuttonspressed.com/blog/django/2010/01/Native-Django-on-App-Engine http://arrogantprogrammer.blogspot.com/2010/03/django-nonrel-and-google-app-engine.html http://css.dzone.com/articles/django-nonrel-picking-momentum http://docs.djangoproject.com/en/1.2/ http://code.google.com/appengine/docs/python/overview.html

Prezentacja GIT

W ubiegłym roku miałem przyjemność popełnić prezentacje na temat GIT, na konferencji Java4People. Dzisiaj znalazłem dzięki uprzejmości kolegi nagranie tej prezentacji:

W razie problemów z odtwarzaniem polecam link bezpośredni: Prezentacja GIT

Cucumber - obsługa kilku sesji

Większość standardowych zadań związanych z testowaniem aplikacji jest w prosty sposób do zrealizowania z użyciem domyślnych kroków Cucumbera. Z założenia Cucumber służy do testów funkcjonalnych, lecz można go również zastosować do realizacji "testów" integracyjnych. Chodzi mi o taką sytuację kiedy chcemy przetestować w jednym scenariuszu interakcje pomiędzy działaniami kilku użytkowników, szczególnie gdy z jakiś powodów nie możemy używać ponownego wylogowania i zalogowania, gdyż wpływa ono w jakiś sposób na stan aplikacji. Na ten problem zwrócił mi uwagę mój kolega Michał Papis

W przypadku standardowych wbudowanych mechanizmów testowania możemy skorzystać z bloku open_session:

def login(user)
  open_session do |sess|
    sess.extend(CustomDsl)
    u = users(user)
    sess.https!
    sess.post "/login", :username => u.username, :password => u.password
    assert_equal '/welcome', path
    sess.https!(false)
  end
end

Lecz w przypadku cucumbera, który opiera się o poszczególne definicje kroków konieczne jest znalezienie rozwiązania pasującego do formy w jakiej tworzone są scenariusze.

W tym celu przygotowałem taki oto plik kroków (mizzeria_steps.rb):

module ActionController
  module Integration
    class Session
      def switch_session_by_name(name)
        if @sessions_by_name.nil?
          @sessions_by_name = { :default => @response.session.clone }
        end
        @sessions_by_name[name.to_sym] ||= @sessions_by_name[:default].clone
        @response.session = @sessions_by_name[name.to_sym]
      end
    end
  end
end

Given /^session name is "([^\"]*)"$/ do |name|
  switch_session_by_name(name)
end

Użycie tego mechanizmu (multiple session) jest trywialnie proste, wykonujemy następujący krok:

Given session name is "new user"

W tym przypadku tworzona jest nazwana sesja która jest nie zależna od innych (również od domyślnej). Dostęp do domyślnej nazwanej sesji odbywa się poprzez użycie nazwy: default

Given session name is "default"

Jak to mówią małe proste i funkcjonalne rozwiązanie, a cieszy :)

Lektura obowiązkowa: http://guides.rubyonrails.org/testing.html#integration-testing-examples

Rails 3 od zera ... (część 1.)

Framework Ruby on Rails doczekał się w końcu wersji 3, czas więc na pierwszą "szpachle" ;)

Pierwszą niezbędną wg mnie rzeczą jaką będziemy potrzebować jest RVM (Ruby Version Manager), który znacznie ułatwia zarówno eksperymenty, testowanie jak codzienną pracę. RVM pozwala na instalacje kilku wersji interpretera Ruby oraz sprawne i szybkie przełaczanie między nimi. Największą jego zaletą jest również fakt instalacji interpreterów w katalogu użytkownika, eliminując konieczność posiadania uprawnień do instalacji oprogramowania i używania polecenia sudo

Proponuje zainstalować go bezpośrednio ze źródeł, odcinając tym samym pępowine od systemowej instalacji języka Ruby:

Last login: Fri Feb 26 18:29:46 on ttys004
mkdir -p ~/.rvm/src/ && cd ~/.rvm/src && rm -rf ./rvm/ && git clone git://github.com/wayneeseguin/rvm.git && cd rvm && ./install

Następnie dodajemy niezbędno linijkę do pliku ~/.profile:

if [[ -s /Users/andrzejsliwa/.rvm/scripts/rvm ]] ; then source /Users/andrzejsliwa/.rvm/scripts/rvm ; fi     

W tym momencie musimy uruchomić nową sesję terminala tak by dodane przez nas zmiany odniosły skutek. Poprawną instalację powinnień potwierdzić taki oto rezultat polecenia rvm list:

mac:~ andrzejsliwa$ rvm list

rvm Rubies

System Ruby

   system [ x86_64 i386 ppc ]

mac:~ andrzejsliwa$ rvm install 1.9.1

Installing Ruby from source to: /Users/andrzejsliwa/.rvm/rubies/ruby-1.9.1-p378
Downloading ruby-1.9.1-p378, this may take a while depending on your connection...

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 8862k  100 8862k    0     0   100k      0  0:01:28  0:01:28 --:--:--  121k

Extracting ruby-1.9.1-p378 ...
Configuring ruby-1.9.1-p378, this may take a while depending on your cpu(s)...
Compiling ruby-1.9.1-p378, this may take a while, depending on your cpu(s)...
Installing ruby-1.9.1-p378
Installation of ruby-1.9.1-p378 is complete.
Updating rubygems for ruby-1.9.1-p378
Installing gems for ruby-1.9.1-p378.
Installing rake
Installation of gems for ruby-1.9.1-p378 is complete.

Następnie instalujemy interpreter Rubego 1.9.1

mac:~ andrzejsliwa$ rvm 1.9.1 --default

Poprawność instalacji możemy zweryfikować w następujący sposób:

mac:~ andrzejsliwa$ which ruby
/Users/andrzejsliwa/.rvm/rubies/ruby-1.9.1-p378/bin/ruby

mac:~ andrzejsliwa$ which gem
/Users/andrzejsliwa/.rvm/rubies/ruby-1.9.1-p378/bin/gem

mac:~ andrzejsliwa$ ruby -v 
ruby 1.9.1p378 (2010-01-10 revision 26273) [i386-darwin10.2.0]

mac:~ andrzejsliwa$ gem -v
1.3.6

Chciałbym wyraźnie w tym miejscu przypomnieć że począwszy od wersji 1.3.6 RubyGems, domyślnym źródłem gemów jest strona http://rubygems.org/ (dawny gemcutter.org)

Jedną z ciekawych funkcji RVM jest możliwość definiowania różnych nazwanych zbiorów gemów które można przełączać w ramach jednego interpretera. (domyślnym zbiorem gemów jest %global)

Tworzymy zbiór gemów odpowiedzialny za testowanie Rails w wersji 3.0 pre:

mac:~ andrzejsliwa$ rvm gemset create rails3pre
Gemset 'rails3pre' created.

Następnie wybieramy go jako bierzący:

mac:~ andrzejsliwa$ rvm 1.9.1%rails3pre

mac:~ andrzejsliwa$ rvm list

rvm Rubies

=> ruby-1.9.1-p378 [ x86_64 ]

Default Ruby (for new shells)

   ruby-1.9.1-p378 [ x86_64 ]

System Ruby

   system [ x86_64 i386 ppc ]

Następnie instalujemy Ruby on Rails

mac:~ andrzejsliwa$ gem install rails --pre

Due to a rubygems bug, you must uninstall older versions of the bundler gem for 0.9 to work.
If you still need bundler 0.8, install the 'bundler08' gem.
Successfully installed i18n-0.3.5
Successfully installed tzinfo-0.3.16
Successfully installed builder-2.1.2
Successfully installed memcache-client-1.7.8
Successfully installed activesupport-3.0.0.beta
Successfully installed activemodel-3.0.0.beta
Successfully installed rack-1.1.0
Successfully installed rack-test-0.5.3
Successfully installed rack-mount-0.4.7
Successfully installed abstract-1.0.0
Successfully installed erubis-2.6.5
Successfully installed actionpack-3.0.0.beta
Successfully installed arel-0.2.1
Successfully installed activerecord-3.0.0.beta
Successfully installed activeresource-3.0.0.beta
Successfully installed mime-types-1.16
Successfully installed mail-2.1.3
Successfully installed text-hyphen-1.0.0
Successfully installed text-format-1.0.0
Successfully installed actionmailer-3.0.0.beta
Successfully installed thor-0.13.4
Successfully installed railties-3.0.0.beta
Successfully installed bundler-0.9.9
Successfully installed rails-3.0.0.beta
24 gems installed
Installing ri documentation for i18n-0.3.5...
Installing ri documentation for tzinfo-0.3.16...
Installing ri documentation for builder-2.1.2...
Installing ri documentation for memcache-client-1.7.8...
Installing ri documentation for activesupport-3.0.0.beta...
Installing ri documentation for activemodel-3.0.0.beta...
Installing ri documentation for rack-1.1.0...
Installing ri documentation for rack-test-0.5.3...
Installing ri documentation for rack-mount-0.4.7...
Installing ri documentation for abstract-1.0.0...
Installing ri documentation for erubis-2.6.5...
Installing ri documentation for actionpack-3.0.0.beta...
Installing ri documentation for arel-0.2.1...
Installing ri documentation for activerecord-3.0.0.beta...
Installing ri documentation for activeresource-3.0.0.beta...
Installing ri documentation for mime-types-1.16...
Installing ri documentation for mail-2.1.3...
Installing ri documentation for text-hyphen-1.0.0...
Installing ri documentation for text-format-1.0.0...
Installing ri documentation for actionmailer-3.0.0.beta...
Installing ri documentation for thor-0.13.4...
Installing ri documentation for railties-3.0.0.beta...
Installing ri documentation for bundler-0.9.9...
Installing ri documentation for rails-3.0.0.beta...
Installing RDoc documentation for i18n-0.3.5...
Installing RDoc documentation for tzinfo-0.3.16...
Installing RDoc documentation for builder-2.1.2...
Installing RDoc documentation for memcache-client-1.7.8...
Installing RDoc documentation for activesupport-3.0.0.beta...
Installing RDoc documentation for activemodel-3.0.0.beta...
Installing RDoc documentation for rack-1.1.0...
Installing RDoc documentation for rack-test-0.5.3...
Installing RDoc documentation for rack-mount-0.4.7...
Installing RDoc documentation for abstract-1.0.0...
Installing RDoc documentation for erubis-2.6.5...
Installing RDoc documentation for actionpack-3.0.0.beta...
Installing RDoc documentation for arel-0.2.1...
Installing RDoc documentation for activerecord-3.0.0.beta...
Installing RDoc documentation for activeresource-3.0.0.beta...
Installing RDoc documentation for mime-types-1.16...
Installing RDoc documentation for mail-2.1.3...
Installing RDoc documentation for text-hyphen-1.0.0...
Installing RDoc documentation for text-format-1.0.0...
Installing RDoc documentation for actionmailer-3.0.0.beta...
Installing RDoc documentation for thor-0.13.4...
Installing RDoc documentation for railties-3.0.0.beta...
Installing RDoc documentation for bundler-0.9.9...
Installing RDoc documentation for rails-3.0.0.beta...

Aby upewnić się że pracujemy na naszym zbiorze gemów przełączamy się na domyślny i weryfikujemy że nie ma zainstalowanych gemów związanych z rails 3

mac:~ andrzejsliwa$ rvm 1.9.1
mac:~ andrzejsliwa$ gem list

*** LOCAL GEMS ***

rake (0.8.7)
rubygems-update (1.3.6)

Następnie wracamy ponownie do naszego zbioru rails3pre:

mac:~ andrzejsliwa$ rvm 1.9.1%rails3pre
mac:~ andrzejsliwa$ gem list

*** LOCAL GEMS ***

abstract (1.0.0)
actionmailer (3.0.0.beta)
actionpack (3.0.0.beta)
activemodel (3.0.0.beta)
activerecord (3.0.0.beta)
activeresource (3.0.0.beta)
activesupport (3.0.0.beta)
arel (0.2.1)
builder (2.1.2)
bundler (0.9.9)
erubis (2.6.5)
i18n (0.3.5)
mail (2.1.3)
memcache-client (1.7.8)
mime-types (1.16)
rack (1.1.0)
rack-mount (0.4.7)
rack-test (0.5.3)
rails (3.0.0.beta)
railties (3.0.0.beta)
rake (0.8.7)
rubygems-update (1.3.6)
text-format (1.0.0)
text-hyphen (1.0.0)
thor (0.13.4)
tzinfo (0.3.16)

Kolejnym krokiem jest stworzenie naszej przykładowej aplikacji

mac:work andrzejsliwa$ rails testapp -d postgresql
      create  
      create  README
      create  .gitignore
      create  Rakefile
      create  config.ru
      create  Gemfile
      create  app
      create  app/controllers/application_controller.rb
      create  app/helpers/application_helper.rb
      create  app/models
      create  app/views/layouts
      create  config
      create  config/routes.rb
      create  config/application.rb
      create  config/environment.rb
      create  config/environments
      create  config/environments/development.rb
      create  config/environments/production.rb
      create  config/environments/test.rb
      create  config/initializers
      create  config/initializers/backtrace_silencers.rb
      create  config/initializers/cookie_verification_secret.rb
      create  config/initializers/inflections.rb
      create  config/initializers/mime_types.rb
      create  config/initializers/session_store.rb
      create  config/locales
      create  config/locales/en.yml
      create  config/boot.rb
      create  config/database.yml
      create  db
      create  db/seeds.rb
      create  doc
      create  doc/README_FOR_APP
      create  lib
      create  lib/tasks
      create  lib/tasks/.gitkeep
      create  log
      create  log/server.log
      create  log/production.log
      create  log/development.log
      create  log/test.log
      create  public
      create  public/404.html
      create  public/422.html
      create  public/500.html
      create  public/favicon.ico
      create  public/index.html
      create  public/robots.txt
      create  public/images
      create  public/images/rails.png
      create  public/stylesheets
      create  public/stylesheets/.gitkeep
      create  public/javascripts
      create  public/javascripts/application.js
      create  public/javascripts/controls.js
      create  public/javascripts/dragdrop.js
      create  public/javascripts/effects.js
      create  public/javascripts/prototype.js
      create  public/javascripts/rails.js
      create  script
      create  script/rails
      create  test
      create  test/performance/browsing_test.rb
      create  test/test_helper.rb
      create  test/fixtures
      create  test/functional
      create  test/integration
      create  test/unit
      create  tmp
      create  tmp/sessions
      create  tmp/sockets
      create  tmp/cache
      create  tmp/pids
      create  vendor/plugins
      create  vendor/plugins/.gitkeep
  
mac:work andrzejsliwa$ cd testapp/

Instalujemy zależności za pomocą narzędzia bundler:

mac:testapp andrzejsliwa$ bundle -h
Tasks:
  bundle check        # Checks if the dependencies listed in Gemfile are satisfied by currently installed gems
  bundle exec         # Run the command in context of the bundle
  bundle help [TASK]  # Describe available tasks or one specific task
  bundle init         # Generates a Gemfile into the current working directory
  bundle install      # Install the current environment to the system
  bundle lock         # Locks the bundle to the current set of dependencies, including all child dependencies.
  bundle package      # Locks and then caches all of the gems into vendor/cache
  bundle show         # Shows all gems that are part of the bundle.
  bundle unlock       # Unlock the bundle. This allows gem versions to be changed
  bundle version      # Prints the bundler's version information

mac:testapp andrzejsliwa$ bundle install
Fetching source index from http://gemcutter.org/
Resolving dependencies
Installing abstract (1.0.0) from system gems 
Installing actionmailer (3.0.0.beta) from system gems 
Installing actionpack (3.0.0.beta) from system gems 
Installing activemodel (3.0.0.beta) from system gems 
Installing activerecord (3.0.0.beta) from system gems 
Installing activeresource (3.0.0.beta) from system gems 
Installing activesupport (3.0.0.beta) from system gems 
Installing arel (0.2.1) from system gems 
Installing builder (2.1.2) from system gems 
Installing bundler (0.9.9) from system gems 
Installing erubis (2.6.5) from system gems 
Installing i18n (0.3.5) from system gems 
Installing mail (2.1.3) from system gems 
Installing memcache-client (1.7.8) from system gems 
Installing mime-types (1.16) from system gems 
Installing pg (0.8.0) from rubygems repository at http://gemcutter.org/ with native extensions 
Installing rack (1.1.0) from system gems 
Installing rack-mount (0.4.7) from system gems 
Installing rack-test (0.5.3) from system gems 
Installing rails (3.0.0.beta) from system gems 
Installing railties (3.0.0.beta) from system gems 
Installing rake (0.8.7) from system gems 
Installing text-format (1.0.0) from system gems 
Installing text-hyphen (1.0.0) from system gems 
Installing thor (0.13.4) from system gems 
Installing tzinfo (0.3.16) from system gems 
Your bundle is complete!

W tym momencie możemy zmodyfikować plik .gitignore taka aby wyglądał następująco:

.bundle                                                                                                                           
.DS_Store                                                                                                                         
db/*.sqlite3                                                                                                                      
log/*.log                                                                                                                         
tmp/**/*                                                                                                                          
config/database.yml

Następnie modyfikujemy plik config/database.yml

# PostgreSQL. Versions 7.4 and 8.x are supported.
#
# Install the ruby-postgres driver:
#   gem install ruby-postgres
# On Mac OS X:
#   gem install ruby-postgres -- --include=/usr/local/pgsql
# On Windows:
#   gem install ruby-postgres
#       Choose the win32 build.
#       Install PostgreSQL and put its /bin directory on your path.
base: &base
  adapter: postgresql
  encoding: unicode
  pool: 5
  username: postgres
  password:

development:
  database: testapp_development
  <<: *base

  # Connect on a TCP socket. Omitted by default since the client uses a
  # domain socket that doesn't need configuration. Windows does not have
  # domain sockets, so uncomment these lines.
  #host: localhost
  #port: 5432

  # Schema search path. The server defaults to $user,public
  #schema_search_path: myapp,sharedapp,public

  # Minimum log levels, in increasing order:
  #   debug5, debug4, debug3, debug2, debug1,
  #   log, notice, warning, error, fatal, and panic
  # The server defaults to notice.
  #min_messages: warning

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  database: testapp_test
  <<: *base

production:
  database: testapp_production
  <<: *base

Kopiujemy plik jako przykładowy:

mac:testapp andrzejsliwa$ cp config/database.yml config/database.yml.example

Możemy w tym momencie również się rozejrzeć po aplikacji i zadaniach rake

mac:testapp andrzejsliwa$ rake -T
(in /Users/andrzejsliwa/work/testapp)
rake about                                # Explain the current environment
rake db:abort_if_pending_migrations       # Raises an error if there are pending migrations
rake db:charset                           # Retrieves the charset for the current environment's database
rake db:collation                         # Retrieves the collation for the current environment's database
rake db:create                            # Create the database defined in config/database.yml for the current Rails.env - al...
rake db:create:all                        # Create all the local databases defined in config/database.yml
rake db:drop                              # Drops the database for the current Rails.env
rake db:drop:all                          # Drops all the local databases defined in config/database.yml
rake db:fixtures:identify                 # Search for a fixture given a LABEL or ID.
rake db:fixtures:load                     # Load fixtures into the current environment's database.
rake db:forward                           # Pushes the schema to the next version.
rake db:migrate                           # Migrate the database through scripts in db/migrate and update db/schema.rb by inv...
rake db:migrate:down                      # Runs the "down" for a given migration VERSION.
rake db:migrate:redo                      # Rollbacks the database one migration and re migrate up.
rake db:migrate:reset                     # Resets your database using your migrations for the current environment
rake db:migrate:up                        # Runs the "up" for a given migration VERSION.
rake db:reset                             # Drops and recreates the database from db/schema.rb for the current environment an...
rake db:rollback                          # Rolls the schema back to the previous version.
rake db:schema:dump                       # Create a db/schema.rb file that can be portably used against any DB supported by AR
rake db:schema:load                       # Load a schema.rb file into the database
rake db:seed                              # Load the seed data from db/seeds.rb
rake db:sessions:clear                    # Clear the sessions table
rake db:sessions:create                   # Creates a sessions migration for use with ActiveRecord::SessionStore
rake db:setup                             # Create the database, load the schema, and initialize with the seed data
rake db:structure:dump                    # Dump the database structure to a SQL file
rake db:test:clone                        # Recreate the test database from the current environment's database schema
rake db:test:clone_structure              # Recreate the test databases from the development structure
rake db:test:load                         # Recreate the test database from the current schema.rb
rake db:test:prepare                      # Check for pending migrations and load the test schema
rake db:test:purge                        # Empty the test database
rake db:version                           # Retrieves the current schema version number
rake doc:app                              # Build the RDOC HTML Files
rake doc:clobber_app                      # Remove rdoc products
rake doc:clobber_plugins                  # Remove plugin documentation
rake doc:clobber_rails                    # Remove rdoc products
rake doc:guides                           # Generate Rails guides
rake doc:plugins                          # Generate documentation for all installed plugins
rake doc:rails                            # Build the RDOC HTML Files
rake doc:reapp                            # Force a rebuild of the RDOC files
rake doc:rerails                          # Force a rebuild of the RDOC files
rake log:clear                            # Truncates all *.log files in log/ to zero bytes
rake middleware                           # Prints out your Rack middleware stack
rake notes                                # Enumerate all annotations
rake notes:custom                         # Enumerate a custom annotation, specify with ANNOTATION=CUSTOM
rake notes:fixme                          # Enumerate all FIXME annotations
rake notes:optimize                       # Enumerate all OPTIMIZE annotations
rake notes:todo                           # Enumerate all TODO annotations
rake rails:freeze:edge                    # The freeze:edge command has been deprecated, specify the path setting in your app...
rake rails:freeze:gems                    # The rails:freeze:gems is deprecated, please use bundle install instead
rake rails:template                       # Applies the template supplied by LOCATION=/path/to/template
rake rails:unfreeze                       # The unfreeze command has been deprecated, please use bundler commands instead
rake rails:update                         # Update both configs, scripts and public/javascripts from Rails
rake rails:update:application_controller  # Rename application.rb to application_controller.rb
rake rails:update:configs                 # Update config/boot.rb from your current rails install
rake rails:update:javascripts             # Update Prototype javascripts from your current rails install
rake rails:update:scripts                 # Adds new scripts to the application script/ directory
rake routes                               # Print out all defined routes in match order, with names.
rake secret                               # Generate a crytographically secure secret key.
rake stats                                # Report code statistics (KLOCs, etc) from the application
rake test                                 # Run all unit, functional and integration tests
rake test:benchmark                       # Run tests for {:benchmark=>"db:test:prepare"} / Benchmark the performance tests
rake test:functionals                     # Run tests for {:functionals=>"db:test:prepare"} / Run the functional tests in tes...
rake test:integration                     # Run tests for {:integration=>"db:test:prepare"} / Run the integration tests in te...
rake test:plugins                         # Run tests for {:plugins=>:environment} / Run the plugin tests in vendor/plugins/*...
rake test:profile                         # Run tests for {:profile=>"db:test:prepare"} / Profile the performance tests
rake test:recent                          # Run tests for {:recent=>"db:test:prepare"} / Test recent changes
rake test:uncommitted                     # Run tests for {:uncommitted=>"db:test:prepare"} / Test changes since last checkin...
rake test:units                           # Run tests for {:units=>"db:test:prepare"} / Run the unit tests in test/unit
rake time:zones:all                       # Displays names of all time zones recognized by the Rails TimeZone class, grouped ...
rake time:zones:local                     # Displays names of time zones recognized by the Rails TimeZone class with the same...
rake time:zones:us                        # Displays names of US time zones recognized by the Rails TimeZone class, grouped b...
rake tmp:cache:clear                      # Clears all files and directories in tmp/cache
rake tmp:clear                            # Clear session, cache, and socket files from tmp/
rake tmp:create                           # Creates tmp directories for sessions, cache, sockets, and pids
rake tmp:pids:clear                       # Clears all files in tmp/pids
rake tmp:sessions:clear                   # Clears all files in tmp/sessions
rake tmp:sockets:clear                    # Clears all files in tmp/sockets

W wersji 3.0 ulegl zmianie sposób wywoływania podstawowych poleceń, zastąpiono pojedyńcze skrypty poleceniem rails i odpowiednim parametrem (lub jego skrutem):

mac:testapp andrzejsliwa$ rails -h
Usage: rails COMMAND [ARGS]

The most common rails commands are:
 generate    Generate new code (short-cut alias: "g")
 console     Start the Rails console (short-cut alias: "c")
 server      Start the Rails server (short-cut alias: "s")
 dbconsole   Start a console for the database specified in config/database.yml
             (short-cut alias: "db")

In addition to those, there are:
 application  Generate the Rails application code
 destroy      Undo code generated with "generate"
 benchmarker  See how fast a piece of code runs
 profiler     Get profile information from a piece of code
 plugin       Install a plugin
 runner       Run a piece of code in the application environment

All commands can be run with -h for more information.

Kolejnym krokiem przed uruchomieniem naszej przykladowej aplikacji jest stworzenie niezbędnych baz oraz uruchomienie serwera:

mac:testapp andrzejsliwa$ rake db:create:all
(in /Users/andrzejsliwa/work/testapp)

mac:testapp andrzejsliwa$ rails s
=> Booting WEBrick
=> Rails 3.0.0.beta application starting in development on http://0.0.0.0:3000
=> Call with -d to detach
=> Ctrl-C to shutdown server
[2010-02-26 19:37:50] INFO  WEBrick 1.3.1
[2010-02-26 19:37:50] INFO  ruby 1.9.1 (2010-01-10) [i386-darwin10.2.0]
[2010-02-26 19:37:50] INFO  WEBrick::HTTPServer#start: pid=44009 port=3000

Teraz możemy użyć generatorów które zostały w wersji 3.0 w całości przebudowane (tym samym stając się nie kompatybilne z poprzednią wersją). Między innymi wprowadzono w nich uchwyty pozwalające na wymienne stosowanie np. frameworków testowania, widoku itp:

mac:testapp andrzejsliwa$ r g 
Usage: rails generate GENERATOR [args] [options]

General options:
  -h, [--help]     # Print generators options and usage
  -p, [--pretend]  # Run but do not make any changes
  -f, [--force]    # Overwrite files that already exist
  -s, [--skip]     # Skip files that already exist
  -q, [--quiet]    # Supress status output

Please choose a generator below.

Rails:
  controller
  generator
  helper
  integration_test
  mailer
  metal
  migration
  model
  model_subclass
  observer
  performance_test
  plugin
  resource
  scaffold
  scaffold_controller
  session_migration
  stylesheets

ActiveRecord:
  active_record:migration
  active_record:model
  active_record:observer
  active_record:session_migration

Erb:
  erb:controller
  erb:mailer
  erb:scaffold

TestUnit:
  test_unit:controller
  test_unit:helper
  test_unit:integration
  test_unit:mailer
  test_unit:model
  test_unit:observer
  test_unit:performance
  test_unit:plugin
  test_unit:scaffold


mac-2:testapp andrzejsliwa$ r g scaffold Post title:string content:text
      invoke  active_record
      create    db/migrate/20100226184313_create_posts.rb
      create    app/models/post.rb
      invoke    test_unit
      create      test/unit/post_test.rb
      create      test/fixtures/posts.yml
       route  resources :posts
      invoke  scaffold_controller
      create    app/controllers/posts_controller.rb
      invoke    erb
      create      app/views/posts
      create      app/views/posts/index.html.erb
      create      app/views/posts/edit.html.erb
      create      app/views/posts/show.html.erb
      create      app/views/posts/new.html.erb
      create      app/views/posts/_form.html.erb
      create      app/views/layouts/posts.html.erb
      invoke    test_unit
      create      test/functional/posts_controller_test.rb
      invoke    helper
      create      app/helpers/posts_helper.rb
      invoke      test_unit
      create        test/unit/helpers/posts_helper_test.rb
      invoke  stylesheets
      create    public/stylesheets/scaffold.css

Na tym etapie usuwamy zbędne pliki:

mac:testapp andrzejsliwa$ rm public/index.html 
mac:testapp andrzejsliwa$ rm public/favicon.ico

Migrujemy bazę danych:

mac:testapp andrzejsliwa$ rake db:migrate
(in /Users/andrzejsliwa/work/testapp)
==  CreatePosts: migrating ====================================================
-- create_table(:posts)
NOTICE:  CREATE TABLE will create implicit sequence "posts_id_seq" for serial column "posts.id"
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "posts_pkey" for table "posts"
   -> 0.0073s
==  CreatePosts: migrated (0.0074s) ===========================================

Oraz definiujemy routing:

Testapp::Application.routes.draw do |map|
  resources :posts
  # ...
  
  root :to => "posts#index"
end

Tak przygotowaną aplikacje, po przetestowaniu możemy dodać do repozytorium gita:

mac:testapp andrzejsliwa$ git init
Initialized empty Git repository in /Users/andrzejsliwa/work/testapp/.git/
mac:testapp andrzejsliwa$ git add .
mac:testapp andrzejsliwa$ git status
# On branch master
#
# Initial commit
#
# Changes to be committed:
#   (use "git rm --cached <file>..." to unstage)
#
#   new file:   .gitignore
#   new file:   Gemfile
#   new file:   README
#   new file:   Rakefile
#   new file:   app/controllers/application_controller.rb
#   new file:   app/controllers/posts_controller.rb
#   new file:   app/helpers/application_helper.rb
#   new file:   app/helpers/posts_helper.rb
#   new file:   app/models/post.rb
#   new file:   app/views/layouts/posts.html.erb
#   new file:   app/views/posts/_form.html.erb
#   new file:   app/views/posts/edit.html.erb
#   new file:   app/views/posts/index.html.erb
#   new file:   app/views/posts/new.html.erb
#   new file:   app/views/posts/show.html.erb
#   new file:   config.ru
#   new file:   config/application.rb
#   new file:   config/boot.rb
#   new file:   config/database.yml.example
#   new file:   config/environment.rb
#   new file:   config/environments/development.rb
#   new file:   config/environments/production.rb
#   new file:   config/environments/test.rb
#   new file:   config/initializers/backtrace_silencers.rb
#   new file:   config/initializers/cookie_verification_secret.rb
#   new file:   config/initializers/inflections.rb
#   new file:   config/initializers/mime_types.rb
#   new file:   config/initializers/session_store.rb
#   new file:   config/locales/en.yml
#   new file:   config/routes.rb
#   new file:   db/migrate/20100226184313_create_posts.rb
#   new file:   db/schema.rb
#   new file:   db/seeds.rb
#   new file:   doc/README_FOR_APP
#   new file:   lib/tasks/.gitkeep
#   new file:   public/404.html
#   new file:   public/422.html
#   new file:   public/500.html
#   new file:   public/images/rails.png
#   new file:   public/javascripts/application.js
#   new file:   public/javascripts/controls.js
#   new file:   public/javascripts/dragdrop.js
#   new file:   public/javascripts/effects.js
#   new file:   public/javascripts/prototype.js
#   new file:   public/javascripts/rails.js
#   new file:   public/robots.txt
#   new file:   public/stylesheets/.gitkeep
#   new file:   public/stylesheets/scaffold.css
#   new file:   script/rails
#   new file:   test/fixtures/posts.yml
#   new file:   test/functional/posts_controller_test.rb
#   new file:   test/performance/browsing_test.rb
#   new file:   test/test_helper.rb
#   new file:   test/unit/helpers/posts_helper_test.rb
#   new file:   test/unit/post_test.rb
#   new file:   vendor/plugins/.gitkeep
#

mac:testapp andrzejsliwa$ git commit -m "initial import"
[master (root-commit) 54399d1] initial import
 53 files changed, 8540 insertions(+), 0 deletions(-)
 create mode 100644 .gitignore
 create mode 100644 Gemfile
 create mode 100644 README
 create mode 100644 Rakefile
 create mode 100644 app/controllers/application_controller.rb
 create mode 100644 app/controllers/posts_controller.rb
 create mode 100644 app/helpers/application_helper.rb
 create mode 100644 app/helpers/posts_helper.rb
 create mode 100644 app/models/post.rb
 create mode 100644 app/views/layouts/posts.html.erb
 create mode 100644 app/views/posts/_form.html.erb
 create mode 100644 app/views/posts/edit.html.erb
 create mode 100644 app/views/posts/index.html.erb
 create mode 100644 app/views/posts/new.html.erb
 create mode 100644 app/views/posts/show.html.erb
 create mode 100644 config.ru
 create mode 100644 config/application.rb
 create mode 100644 config/boot.rb
 create mode 100644 config/database.yml.example
 create mode 100644 config/environment.rb
 create mode 100644 config/environments/development.rb
 create mode 100644 config/environments/production.rb
 create mode 100644 config/environments/test.rb
 create mode 100644 config/initializers/backtrace_silencers.rb
 create mode 100644 config/initializers/cookie_verification_secret.rb
 create mode 100644 config/initializers/inflections.rb
 create mode 100644 config/initializers/mime_types.rb
 create mode 100644 config/initializers/session_store.rb
 create mode 100644 config/locales/en.yml
 create mode 100644 config/routes.rb
 create mode 100644 db/migrate/20100226184313_create_posts.rb
 create mode 100644 db/schema.rb
 create mode 100644 db/seeds.rb
 create mode 100644 doc/README_FOR_APP
 create mode 100644 lib/tasks/.gitkeep
 create mode 100644 public/404.html
 create mode 100644 public/422.html
 create mode 100644 public/500.html
 create mode 100644 public/images/rails.png
 create mode 100644 public/javascripts/application.js
 create mode 100644 public/javascripts/controls.js
 create mode 100644 public/javascripts/dragdrop.js
 create mode 100644 public/javascripts/effects.js
 create mode 100644 public/javascripts/prototype.js
 create mode 100644 public/javascripts/rails.js
 create mode 100644 public/robots.txt
 create mode 100644 public/stylesheets/.gitkeep
 create mode 100644 public/stylesheets/scaffold.css
 create mode 100755 script/rails
 create mode 100644 test/fixtures/posts.yml
 create mode 100644 test/functional/posts_controller_test.rb
 create mode 100644 test/performance/browsing_test.rb
 create mode 100644 test/test_helper.rb
 create mode 100644 test/unit/helpers/posts_helper_test.rb
 create mode 100644 test/unit/post_test.rb
 create mode 100644 vendor/plugins/.gitkeep
 
mac-2:testapp andrzejsliwa$ git remote add origin git@github.com:andrzejsliwa/testapp.git
mac-2:testapp andrzejsliwa$ git push origin master
Counting objects: 84, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (68/68), done.
Writing objects: 100% (84/84), 81.08 KiB, done.
Total 84 (delta 2), reused 0 (delta 0)
To git@github.com:andrzejsliwa/testapp.git
* [new branch]      master -> master

Drukowanie bezpośrednio do PDF

W aplikacjach webowych format PDF ugruntował już swoją pozycję. W większości przypadków jest formatem w którym "drukowane" są zarówno faktury, jak i wszelkiej maści dokumenty informacyjne. W przypadku frameworka Ruby on Rails do tej pory korzystałem z biblioteki Prawn.

Niestety możliwości tej biblioteki są dość ubogie jeśli chodzi o tworzenie dokumentów mocno customizowanych, wymuszając wręcz rysowanie co bardziej skomplikowanych elementów wizualnych. W powiązaniu z wymaganiami klienta powodowało to ciągłą, syzyfową pracę, by zapewnić poprawne wyświetlanie dokumentów gdzie treść oraz jej rozkład mógł się zmieniać.

Zrażony tymi problemami postanowiłem znaleźć rozwiązanie bazujące na htmlu jako formacie źródłowym dla PDF. Skierowałem swoje pierwsze kroki w kierunku GitHuba i tam też znalazłem gotowe rozwiązanie moich problemów w postaci plugina.

Plugin nazywa się Wicket PDF i jest tak naprawdę prostym wrapperem dla programu uruchamianego z linii poleceń wkhtmltopdf (bazujący na webkit).

Instalacja rozwiązania polega na zainstalowaniu wkhtmltopdf (ze źródeł, bądź z prekompilowanych binarek)

w przypadku mojego systemu operacyjnego(Mac OSX) wygląda to następująco:

$ wget http://wkhtmltopdf.googlecode.com/files/wkhtmltopdf-0.9.1-OS-X.i368
$ sudo mv wkhtmltopdf-0.9.1-OS-X.i368 /opt/local/bin/wkhtmltopdf
$ sudo chmod +x wkhtmltopdf

możemy oczywiście przetestować funkcjonowanie tego programu:

$ wkhtmltopdf www.google.pl google.pdf

lub

$ wkhtmltopdf file:///Users/andrzejsliwa/Desktop/test.html test.pdf

następnie instalujemy sam plugin:

$ script/plugin install git://github.com/mileszs/wicked_pdf.git
$ script/generate wicked_pdf

tak zainstalowany plugin można bez problemu wykorzystać w następujący sposób:

# GET /pages/1
# GET /pages/1.xml
def show
  @page = Page.find(params[:id])

  respond_to do |format|
    format.html # show.html.erb
    format.xml  { render :xml => @page }
    format.pdf do
      render :Pdf => "#{@page.id}",
        :template => 'pages/show.html.erb',
        :wkhtmltopdf => '/opt/local/bin/wkhtmlopdf'
    end
  end
end

generowanie linków dla dokumentów pdf może wyglądać następująco:

<%= link_to "PDF", page_path(@page, :format => 'pdf'), :target => "_blank"%>

Lektura obowiązkowa: http://github.com/mileszs/wicked_pdf http://code.google.com/p/wkhtmltopdf/