Drukowanie bezposrednio do PDF

W aplikacjach webowych format PDF ugruntował już swoją pozycję. W wiekszoś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. Wymusząją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 mogł 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 naprawde prostym wrapperem dla programu uruchamianego z lini 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

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/

Oszukiwanie jest fajne

W codziennej pracy często zdarza się sytuacja kiedy potrzebujemy informacji odnosnie jakiegos narzędzia, bibiloteki itp. Wtedy ruszamy często do Googla. Lecz można również… oszukiwać (cheat - oszustwo) instalując taki oto przydatny wynalazek ułatwiający nam życie:

$ gem install cheat

i odrazu możemy z niego korzystać, powiedzmy że potrzebuję informacji jak używać git’a:

$ cheat git

I dostaniemy taki oto cheatsheet:

git:
  Setup
  -----
  
  git clone <repo>
    clone the repository specified by <repo>; this is similar to "checkout" in
    some other version control systems such as Subversion and CVS
  
  Add colors to your ~/.gitconfig file:
  
    [color]
      ui = auto
    [color "branch"]
      current = yellow reverse
      local = yellow
      remote = green
    [color "diff"]
      meta = yellow bold
      frag = magenta bold
      old = red bold
      new = green bold
    [color "status"]
      added = yellow
      changed = green
      untracked = cyan
  
  Highlight whitespace in diffs
  
    [color]
      ui = true
    [color "diff"]
      whitespace = red reverse
    [core]
      whitespace=fix,-indent-with-non-tab,trailing-space,cr-at-eol
  
  Add aliases to your ~/.gitconfig file:
  
    [alias]
      st = status
      ci = commit
      br = branch
      co = checkout
      df = diff
      lg = log -p
  
  
  Configuration
  -------------
  
  git config -e [--global]
    edit the .git/config [or ~/.gitconfig] file in your $EDITOR
  
  git config --global user.name 'John Doe'
  git config --global user.email johndoe@example.com
    sets your name and email for commit messages
  
  ...
  

Albo informacji odnośnie np. rspec’a:

$ cheat rspec

I dostaniemy taki oto cheatsheet:

rspec:
  INSTALL
  =======
    INSTALL rspec
    =============
  $ sudo gem install rspec
    OR
  $ ./script/plugin install git://github.com/dchelimsky/rspec.git
  
    INSTALL rspec_on_rails plugin
    =============================
  $ ./script/plugin install git://github.com/dchelimsky/rspec-rails.git
  
  
  BOOTSTRAP THE APP
  =================
  $ ./script/generate rspec
        create  spec
        create  spec/spec_helper.rb
        create  spec/spec.opts
        create  previous_failures.txt
        create  script/spec_server
        create  script/spec
  
  
  
  HOW TO USE
  ==========
  
    COMMAND LINE
    =============
  spec --color --format specdoc user.rspec
  
    RAILS
    =============
  ./script/generate rspec_model User
  rake doc:plugins # generates local docs for your app's plugins
  rake -T spec # lists all rspec rake tasks
  rake spec # run all specs
  rake spec SPEC=spec/models/mymodel_spec.rb SPEC_OPTS="-e \"should do
  something\"" #run a single spec
  
  
  
  
  module UserSpecHelper
    def valid_user_attributes
      { :email => "joe@bloggs.com",
        :username => "joebloggs",
        :password => "abcdefg"}
    end
  end
  
  
  describe "A User (in general)" do
    include UserSpecHelper
  
    before(:each) do
      @user = User.new
    end
  
    it "should be invalid without a username" do
      pending "some other thing we depend on"
      @user.attributes = valid_user_attributes.except(:username)
      @user.should_not be_valid
      @user.should have(1).error_on(:username)
      @user.errors.on(:username).should == "is required"
      @user.username = "someusername"
      @user.should be_valid
    end
  end
  
  SHOULDA COULDA WOULDA
  =====================
  target.should satisfy {|arg| ...}
  target.should_not satisfy {|arg| ...}
  
  target.should equal <value>
  target.should not_equal <value>
  
  target.should be_close <value>, <tolerance>
  target.should_not be_close <value>, <tolerance>
  
  target.should be <value>
  target.should_not be <value>
  
  target.should predicate [optional args]
  target.should be_predicate [optional args]
  target.should_not predicate [optional args]
  target.should_not be_predicate [optional args]
  
  target.should be < 6
  target.should == 5
  target.should_not == 'Samantha'
  
  target.should match <regex>
  target.should_not match <regex>
  
  target.should be_an_instance_of <class>
  target.should_not be_an_instance_of <class>
  
  target.should be_a_kind_of <class>
  target.should_not be_a_kind_of <class>
  
  target.should respond_to <symbol>
  target.should_not respond_to <symbol>
  
  *OLD:*
  proc.should raise <exception>
  proc.should_not raise <exception>
  proc.should_raise <exception> # not available anymore
  *NEW:*
  lambda {a_call}.should raise_error
  lambda {a_call}.should raise_error(<exception> [, message])
  lambda {a_call}.should_not raise_error
  lambda {a_call}.should_not raise_error(<exception> [, message])
  
  proc.should throw <symbol>
  proc.should_not throw <symbol>
  
  target.should include <object>
  target.should_not include <object>
  
  target.should have(<number>).things
  target.should have_at_least(<number>).things
  target.should have_at_most(<number>).things
  
  target.should have(<number>).errors_on(:field)
  
  proc { thing.approve! }.should change(thing, :status).
      from(Status::AWAITING_APPROVAL).
      to(Status::APPROVED)
  
  proc { thing.destroy }.should change(Thing, :count).by(-1)
  
  Mocks and Stubs
  ===============
  
  user_mock = mock "User"
  user_mock.should_receive(:authenticate).with("password").and_return(true)
  user_mock.should_receive(:coffee).exactly(3).times.and_return(:americano)
 
  user_mock.should_receive(:coffee).exactly(5).times.and_raise(NotEnoughCoffeeExc
  ption)
  
  people_stub = mock "people"
  people_stub.stub!(:each).and_yield(mock_user)
  people_stub.stub!(:bad_method).and_raise(RuntimeError)
  
  user_stub = mock_model("User", :id => 23, :username => "pat", :email =>
  "pat@example.com")
  

Prawda że fajnie jest oszukiwać ! ;)

Lektura obowiązkowa:
http://cheat.errtheblog.com/

App Templates - szybki start tworzenia aplikacji

Ruby on Rails z wersji na wersję podlega naturalnej ewolucji. Jest to proces ciągły, oparty na reagowaniu jego użytkowników na nowe wymagania stawiane przed frameworkiem. Aktualna gałąź 2.3 wprowadza szerego udogodnień między innymi z związanych z automatyzacją procesu tworzenia aplikacji. Wprowadzono prosty język DSL za pomocą którego jesteśmy w stanie projektować własne szablony. Szablony te mogą na wzajem się wywoływać co pozwala na modułowe wykorzystanie.

Przykłąd szablonu automatyzującego dodawanie aplikacji do repozytorium GIT:

log 'GIT template', 'Starting template'

unless self.respond_to?(:already_using)
  template = ENV['TEMPLATE_BASE'] || 'http://github.com/andrzejsliwa/config/raw/master/templates'
  load_template("#{template}/helpers_methods.rb")
  @stand_alone = true
end

already_using('.git/config', 'GIT')

# Remove temp directories.
%w[cache pids sessions sockets].each do |dir|
  rmdir "tmp/#{dir}"
end

# Remove unnecessary files.
%w[index.html favicon.ico robots.txt].each do |file|
  rm "public/#{file}"
end
rm 'doc/README_FOR_APP'

# Hold empty directories touching a .gitignore file
run("find . \\( -type d -empty \\) -and \\( -not -regex ./\\.git.* \\) -exec touch {}/.gitignore \\;", false)

# Copy database.yml for reuse
cp 'config/database.yml', 'config/database.yml.example'

# Create root .gitignore file from template
config_file '.gitignore'

git :init

if @stand_alone
  # Initial commit
  git :add => "."
  git :commit => "-a -m 'initial import.'"
end

log 'GIT template', 'Successfully applied template'

Użycie przykładowego szablonu może odbywać się na 2 sposoby:

w trakcie tworzenia aplikacji:

$ rails application_name  
-m  http://github.com/andrzejsliwa/config/raw/master/templates/git.rb

w dowolnym momencie wykorzystując wbudowany task Rake’a:

$ rake rails:template  
LOCATION=http://github.com/andrzejsliwa/config/raw/master/templates/git.rb

Zapraszam do zapoznania się z tym mechanizmem, umiejętnie wykorzystany może zaoszczędzić nam mnóstwo czasu związanego z konfiguracją aplikacji.

Na koniec przykład pliku szablonu nad którym obecnie jeszcze pracuje a który ma zautomatyzwać konfiguracje aplikacji pod Cucumber / Rspec oraz Authologic. Skrypt ten wykorzystuje wyżej prezentowany git.rb:

log 'BASE template', 'Starting base template'

template = ENV['TEMPLATE_BASE'] || 'http://github.com/andrzejsliwa/config/raw/master/templates'

unless self.respond_to?(:already_using)
  load_template("#{template}/helpers_methods.rb")
end



gem 'authlogic',          :version => '>=2.1.3', :lib => false
gem 'gemcutter',          :version => '>=0.2.1', :lib => false

gem 'rcov',               :version => '>=0.9.7', :lib => false, :env => 'test'
gem 'database_cleaner',   :version => '>=0.4.0', :lib => false, :env => 'test'
gem 'webrat',             :version => '>=0.6.0', :lib => false, :env => 'test'
gem 'rspec',              :version => '>=1.2.9', :lib => false, :env => 'test'
gem 'rspec-rails',        :version => '>=1.2.9', :lib => false, :env => 'test'
gem 'cucumber',           :version => '>=0.5.3', :lib => false, :env => 'test'
gem 'cucumber-rails',     :version => '>=0.2.2', :lib => false, :env => 'test'
gem 'autotest',           :version => '>=4.1.4', :lib => false, :env => 'test'
gem 'autotest-rails',     :version => '>=4.1.0', :lib => false, :env => 'test'
gem 'autotest-fsevent',   :version => '>=0.1.3', :lib => false, :env => 'test'
gem 'autotest-growl',     :version => '>=0.1.7', :lib => false, :env => 'test'
gem 'ZenTest',            :version => '>=4.2.1', :lib => false, :env => 'test'
gem 'factory_girl',       :version => '>=1.2.3', :lib => false, :env => 'test'
gem 'email_spec',         :version => '>=0.3.8', :lib => "email_spec", :env => 'test'

rake 'gems:install', :env => 'test'
rake 'gems:install'

# We don't like unittest ;)
rmdir_r "test"

# Get database.yml file
database_yml(:postgresql)

# Run required generators
generate :cucumber
generate :rspec
generate :email_spec

gem 'email_spec', :version => '>=0.3.8', :lib => "email_spec", :env => 'cucumber'

# Get .autotest config file
config_file '.autotest'

rakefile 'rcov.rake', get_source('lib/tasks/rcov.rake')

spec 'rcov.opts'
spec 'spec_helper.rb'

features 'support/env.rb'

rake 'db:create:all'
rake 'db:migrate'
rake 'db:test:load'

load_template("#{template}/haml.rb") if yes?("enable HAML?")

@is_git = yes?("enable GIT?")
load_template("#{template}/git.rb") if @is_git

if @is_git
  # Initial commit
  git :add => "."
  git :commit => "-a -m 'initial import.'"
end

log 'BASE template', 'Successfully applied template'

Wszystkie aktualne źródła znajdują się tutaj: http://github.com/andrzejsliwa/config/tree/master/templates/

Lektura obowiązkowa:
http://asciicasts.com/episodes/148-app-templates-in-rails-2-3
http://m.onkey.org/2008/12/4/rails-templates
http://guides.rubyonrails.org/2_3_release_notes.html