#226 Upgrading to Rails 3 Part 2
Nell’ultimo episodio vi abbiamo mostrato come aggiornare un’applicazione da Rails 2 a Rails 3, usando il sito Railscasts come esempio. Siamo arrivati fino al punto di far partire senza errori l’applicazione, ma ci sono ancora delle cose che devono essere sistemate o aggiornate. In questo episodio ci occuperemo proprio di queste cose.
Un metodo ovvio per capire cosa resta da sistemare è quello di lanciare i test o le specs dell’applicazione, ma prima di fare questa operazione, vale la pena fare un rapido giro sull’applicazione col browser, in modo tale da rendersi conto "manualmente" degli errori più macroscopici. Facendo ciò per l’applicazione Railscast, scopriamo abbastanza in fretta che, sebbene la pagina di indice degli episodi funzioni a dovere, le pagine di dettaglio del singolo episodio non funzionano e lanciano il seguente errore:
uninitialized constant ApplicationHelper::Textilizer
In questa applicazione Textilizer
è una classe personalizzata presente nella cartella /lib
ed è proprio questo posizionamento a creare dei problemi, poichè per default, in Rails 3 i file presenti nella cartella /lib
non vengono più inclusi automaticamente nel percorso di caricamento dell’applicazione, come invece avveniva in Rails 2. Per risolvere il problema possiamo o imporre manualmente un require
del file nella cartella /lib
, oppure aggiungere la cartella al percorso di caricamento.
Optiamo per la seconda strada e aggiungiamo la cartella all’autoload_paths
. C’è una linea di codice commentata nel file /config/application.rb
che possiamo decommentare e personalizzare per lo scopo:
# Custom directories with classes and modules you want to be autoloadable. config.autoload_paths += %W(#{Rails.root}/lib)
Il team di sviluppo del core Rails sta ancora lavorandoci su per trovare la migliore soluzione per gestire il caricamento dei file dalla cartella /lib
, per cui il tutto potrebbe cambiare nella versione definitiva di Rails 3.
Quando ricarichiamo la pagina, ora otteniamo un errore diverso:
La pagina ora solleva un errore relativo ad una costante APP_CONFIG
mancante. Questa costante è definita in un pre-initializer in /config/preinitializer.rb
:
# load app_config.yml require 'yaml' APP_CONFIG = YAML.load(File.read("#{Rails.root}/config/app_config.yml"))
In un’applicazione Rails 2 questo file sarebbe stato quello in cui avremmo messo tutto ciò che andava definito prima del caricamento dell’applicazione e in questo caso era usato per caricare un file YAML personalizzato. In Rails 3 il file preinitializer.rb
non è più suppportato, per cui qualsiasi codice che vi si trovi al suo interno andrebbe, al passaggio a Rails 3, spostato nel file config/application.rb
. In particolare, il codice proveniente dal file preinitializer deve essere posizionato vicino alla cima del file, subito prima della riga require 'rails/all'
.
Non possiamo incollare pari pari il codice così com’era, perchè usa Rails.root
che non è disponibile prima del require rails/all
. Dobbiamo invece riferire i file, relativamente alla posizione del file in cui ci troviamo, ossia application.rb
:
require File.expand_path('../boot', __FILE__) # load app_config.yml require 'yaml' APP_CONFIG = YAML.load(File.read(File.expand_path('../app_config.yml', __FILE__))) require 'rails/all'
Quando ricarichiamo nuovamente la pagina, ora si carica, ma non sembra ancora a posto. L’HTML nelle note ha subito un escaping e la barra laterale destra non si vede. Comunque la pagina bene o male si vede e ciò per ora può renderci soddisfatti. Tutto quello che faremo ora, sarà di navigare tutte le pagine del sito per individuare eventuali eccezioni. Una volta sistemate le pagine che generavano errori, possiamo fare un secondo giro su ciascuna pagina per sistemare gli errori simili al seguente:
Lanciamo i test dell’applicazione
Se lanciamo nuovamente il task rake rails:upgrade:check
, vedremo che ci sono ancora un po’ di questioni aperte da sistemare. Generalmente si tratta di cose legate a deprecazioni, per cui appariranno come warning, dal momento che continuano a funzionare anche con Rails 3.0, anche se da Rails 3.1 verosimilmente smetteranno di andare. Dal momento che le modifiche che dobbiamo fare riguardano delle fix a deprecazioni piuttosto che errori veri e propri, diamo subito uno sguardo al test suite dell’applicazione ed assicuriamoci che tutti i test passino prima di fare una qualunque modifica al codice.
Per lanciare i test, dobbiamo innanzitutto aggiungere i gem opportuni al nostro Gemfile
. Vogliamo avere disponibili questi gem solo in ambiente di test, perchè serviranno solo lì, per cui useremo il metodo group. Potrà sembrare che abbiamo voluto installare i gem solo per l’ambiente di test, ma i gem che includono task Rake, come ad esempio RSpec, dovranno essere inclusi anche nell’ambiente di sviluppo.
L’applicazione Railscasts usa Mocha per fare del mocking, insieme a RSpec e a Factory Girl, per cui dovremo aggiungere il seguente codice al Gemfile
:
group :development, :test do gem "mocha" gem "rspec-rails", ">= 2.0.0.beta.19" gem "factory_girl_rails" end
Per assicurarci che questi gem vengano installati, lanciamo nuovamente bundle install
e poi, per avere pronto all’uso RSpec, lanceremo i suoi generatori. Il generatore sovrascriverà i file già installati di RSpec, per cui sarà il caso di farne un backup prima di procedere:
$ rails g rspec:install <span class="passed">create</span> .rspec <span class="info">exist</span> spec <span class="failed">conflict</span> spec/spec_helper.rb Overwrite /Users/asalicetti/rails/apps_for_asciicasts/ep226/railscasts/spec/spec_helper.rb? (enter "h" for help) [Ynaqdh] Y <span class="forced">force</span> spec/spec_helper.rb <span class="passed">create</span> autotest <span class="passed">create</span> autotest/discover.rb
Il file spec_helper.rb
rigenerato dovrà essere sistemato prima di andare avanti. Per default userà Rspec per il mocking, per cui aggiorneremo la linea config.mock_with
affinchè usi Mocha.
L’applicazione inoltre ha un po’ di macro personalizzate per RSpec nel file /spec/controller_macros.rb
e anche queste dovranno essere incluse. Più su nel file spec_helper
c’è una linea di codice che include tutto ciò che si trova sotto la cartella support
, per cui tutto quello che dovremo fare, sarà di creare questa cartella sotto la cartella spec
e spostare lì il file. Poi, per includere le macro in RSpec, dobbiamo aggiungere una linea config.include
, in modo che il nostro modulo personalizzato sia incluso. Fatte queste modifiche, il file apparirà così:
# This file is copied to ~/spec when you run 'ruby script/generate rspec' # from the project root directory. ENV["RAILS_ENV"] ||= 'test' require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' # Requires supporting files with custom matchers and macros, etc, # in ./support/ and its subdirectories. Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} RSpec.configure do |config| config.mock_with :mocha config.fixture_path = "#{::Rails.root}/spec/fixtures" config.use_transactional_fixtures = true config.include ControllerMacros end
Siamo ora pronti per lanciare le specs e vedere quante di queste passano. Sappiamo già che ci saranno un po’ di warning di deprecazione, ma per ora ci occuperemo solo dei test falliti.
$ rake spec
Questo comando produrrà un bel po’ di messaggi in output ed infine mostrerà che i test eseguiti hanno sollevato un errore. La riga significativa è:
/Users/asalicetti/rails/apps_for_asciicasts/ep226/railscasts/spec/controllers/episodes_controller_spec.rb:5:in `block in <top (required)>': undefined local variable or method `integrate_views' for :Class (NameError)
Se vedete messaggi di errore simili a questo e non siete sicuri di come sistemarli, può essere utile cercare risposte all’errore su Google, per vedere se qualcun’altro ha avuto in precedenza lo stesso problema e ha trovato una soluzione. In questo caso la soluzione è semplice. Il metodo integrate_views
è stato rinominato in render_views
, per cui dobbiamo cambiare tale occorrenza, ovunque si presenti nella nostra applicazione.
Una volta fatto ciò, rilanciamo nuovamente le specs. Di nuovo, vedremo molti messaggi di deprecazione sull’output, ma fra loro vedremo anche che passeranno tutti i test, tranne quattro di essi:
Finished in 3.53 seconds <span class="failed">152 examples, 4 failures</span> 1) CommentsController as guest index action should render index template for rss with xml Failure/Error: response.should have_tag('title', :text => 'Railscasts Comments') <span class="failed">undefined method `has_tag?' for #<ActionController::TestResponse:0x000001040f2dc8></span> # ./spec/controllers/comments_controller_spec.rb:16:in `block (2 levels) in <main>' 2) EpisodesController as guest index action should render index template for rss with xml Failure/Error: response.should have_tag('title', :text => 'Railscasts') <span class="failed">undefined method `has_tag?' for #<ActionController::TestResponse:0x00000104007710></span> # ./spec/controllers/episodes_controller_spec.rb:26:in `block (2 levels) in <top (required)>' 3) EpisodesController as guest index action should render index template for rss with xml for iPod Failure/Error: response.should have_tag('title', :text => /Railscasts.+iPod/) <span class="failed">undefined method `has_tag?' for #<ActionController::TestResponse:0x00000102e2dd00></span> # ./spec/controllers/episodes_controller_spec.rb:33:in `block (2 levels) in <top (required)>' 4) EpisodesController as guest show action should render show template for rss with xml Failure/Error: response.should have_tag('title', :text => /Comments/) <span class="failed">undefined method `has_tag?' for #<ActionController::TestResponse:0x000001041f03b0></span> # ./spec/controllers/episodes_controller_spec.rb:65:in `block (2 levels) in <top (required)>'
Tutti questi fallimenti derivano da un’unica causa comune: il metodo have_tag
. Ancora una volta, una rapida ricerca su Google ci aiuta a capire che il problema è legato a Webrat ed al fatto che have_tag
è stato rimosso. Ora esiste un metodo simile, have_selector
, che possiamo usare. Le opzioni per have_selector
sono lievemente diverse, dovremo sostituire l’opzione :text
con un’opzione :content
, per cui per esempio:
response.should have_tag('title', :text => 'Railscasts')
diverrà:
response.should have_selector('title', :content => 'Railscasts')
Fatte anche queste modifiche e rilanciando i test ora, dovrebbe andare tutto bene:
Finished in 3.44 seconds <span class="passed">152 examples, 0 failures</span>
Rimozione del codice deprecato
Ora che tutti i test sono passati, possiamo cominciare a lavorare sulla lista dei messaggi generati dal task rake rails:upgrade:check
per ridurre il numero di deprecazioni. Il primo elemento della lista riguarda una chiamata ad ActiveRecord:
<span class="failed">Soon-to-be-deprecated ActiveRecord calls</span> Methods such as find(:all), find(:first), finds with conditions, and the :joins option will soon be deprecated. More information: <span class="url">http://m.onkey.org/2010/1/22/active-record-query-interface</span>
Si riferisce a un pezzo di codice che continua ad usare la vecchia sintassi find
, come ad esempio questo metodo presente nella classe di modello Episode, che accetta un hash di condizioni:
def self.primitive_search(query) find(:all, :conditions => primitive_search_conditions(query)) end
Possiamo aggiornare codice come questo, adottando semplicemente il nuovo metodo where
:
def self.primitive_search(query) where(primitive_search_conditions(query)) end
La nuovo sintassi per le query ActiveRecord è stata trattata nell’episodio 202 [guardalo, leggilo], per cui per maggiori dettagli vi rimandiamo là.
Il successivo errore in lista è questo:
<span class="failed">named_scope is now just scope</span> The named_scope method has been renamed to just scope. More information: <span class="url">http://github.com/rails/rails/commit/d60bb0a9e4be2ac0a9de9a69041a4ddc2e0cc914</span>
Anche questo è semplice da sistemare. Dobbiamo solo sostituire nel modello ciascuna chiamata a named_scope
con scope
. Anche gli argomenti dovranno essere aggiornati alla nuova sintassi di Rails 3. Per cui, per esempio, nel modello Comment
:
named_scope :recent, :order => "created_at DESC"
diventa:
scope :recent, order("created_at DESC")
Si noti che man mano che sistemiamo queste cose fra le varie classi dell’applicazione, dovremmo avere l’accortezza di rilanciare ogni volta la suite dei test per assicurarci di non introdurre regressioni.
Il problema successivo ai named scopes è quello riguardante il routing. Il plugin rails upgrade include un task rake per aggiornare un file di route, ma è consigliabile fare questa operazione a mano, in modo tale da cogliere l’occasione per fare un po’ di pulizia.
Al momento, il file degli instradamenti appare così:
Railscasts::Application.routes.draw do |map| map.resources :spam_questions map.resources :spam_checks map.with_options :controller => 'info' do |info| info.about 'about', :action => 'about' info.contest 'contest', :action => 'contest' info.feeds 'feeds', :action => 'feeds' info.give_back 'give_back', :action => 'give_back' end map.login 'login', :controller => 'sessions', :action => 'new' map.logout 'logout', :controller => 'sessions', :action => 'destroy' map.resources :sponsors map.resources :comments map.resources :tags map.resources :episodes, :collection => { :archive => :get } map.resources :sessions map.resources :spam_reports, :member => { :confirm => :post }, :collection => { :confirm => :post } map.root :episodes end
La nuova sintassi per gli instradamenti è stata trattata in dettaglio nell’episodio 203 [guardalo, leggilo] per cui non ritratteremo l’argomento anche qui. Dopo le opportune modifiche, il file degli instradamenti dovrebbe apparire così:
Railscasts::Application.routes.draw do root :to => "episodes#index" match "about" => "info#about", :as => "about" match "contest" => "info#contest", :as => "contest" match "feeds" => "info#feeds", :as => "feeds" match "give_back" => "info#give_back", :as => "give_back" match "login" => "sessions#new", :as => "login" match "logout" => "sessions#destroy", :as => "logout" resources :sponsors resources :comments resources :tags resources :episodes do collection do get :archive end end resources :sessions resources :spam_questions resources :spam_checks resources :spam_reports do member do post :confirm end collection do post :confirm end end end
Il nuovo file è leggermente più lungo, ma appare più chiaro.
L’ultimo elemento rimasto da sistemare sono le chiamate agli helper ERb:
<span class="failed">Deprecated ERb helper calls</span> Block helpers that use concat (e.g., form_for) should use <%= instead of <%. The current form will continue to work for now, but you will get deprecation warnings since this form will go away in the future. More information: <span class="url">http://weblog.rubyonrails.org/</span>
Nel codice delle viste, in Rails 3, talvolta occorre usare <%=
anzichè <%
all’inizio di un blocco che genera dei contenuti in output, tipo il form_for
.
Un esempio di questo tipo di codice lo si ritrova nel file archive.html.erb:
<% form_tag archive_episodes_path, :method => 'get' do %> <p> <%= text_field_tag :search, params[:search] %> <%= submit_tag "Search", :name => nil %> </p> <% end %>
Il form_tag
in questo codice inserirà del contenuto attorno a ciò che si trova all’interno del suo blocco, per cui dovremo cambiare la sintassi del tag e usare anche il simbolo =:
<%= form_tag archive_episodes_path, :method => 'get' do %>
Non tutti i blocchi nel codice delle viste debbono essere cambiati, comunque, sebbene il task rake rails:upgrade:check
dica diversamente. Per esempio, nel codice qui riportato, in cui iteriamo su ciascun elemento in un hash, non aggiungeremo il simbolo =, dal momento che questo codice non aggiunge alcun tag attorno al codice presente all’interno del blocco stesso:
<% @episode_months.each do |month, episodes| %> <h2><%=h month.strftime('%B %Y') %></h2> <% for episode in episodes %> <div> <%= episode.position %>. <%= link_to episode.name, episode %> </div> <% end %> <% end %>
Potrete avere maggiori dettagli in merito, consultando l’episodio 208 [guardalo, leggilo].
Finito di aggiornare tuttoo il codice delle viste, il controllo sull’upgrade mostrerà ancora degli errori, ma questi ultimi sono da considerarsi falsi, dal momento che considerano tutti i blocchi in tutte le viste. Si tenga presente che si dovranno aggiornare solo i tag dei blocchi che generano effettivamente del contenuto, come form_for
, form_tag
, div_for
e così via. Se non siete sicuri sul da farsi, non mettetelo e poi controllate la presenza di errori di deprecazione nei vostri logdi test o di sviluppo.
Ora che abbiamo finito di sfruttare il plugin di upgrade, possiamo rimuoverlo lanciando:
$ rails plugin remove rails_upgrade
Abbiamo fatto un bel po’ di passi avanti nell’aggiornamento dell’applicazione a Rails 3. I test ora passano tutti e abbiamo rimosso tutto il codice deprecato. Abbiamo ancora i problemi che avevamo visto poco fa, comunque, a causa dei quali veniva erroneamente fatto l’escape di parti dell’HTML della pagina e poi c’era anche il problema della barra laterale mancante. Ci occuperemo di queste cose nel prossimo episodio.