Skip to content

Latest commit

 

History

History
1168 lines (862 loc) · 48.1 KB

viikko6.md

File metadata and controls

1168 lines (862 loc) · 48.1 KB

Jatkamme sovelluksen rakentamista siitä, mihin jäimme viikon 5 lopussa. Allaoleva materiaali olettaa, että olet tehnyt kaikki edellisen viikon tehtävät. Jos et tehnyt kaikkia tehtäviä, voit ottaa kurssin repositorioista edellisen viikon mallivastauksen. Jos sait suurimman osan edellisen viikon tehtävistä tehtyä, saattaa olla helpointa, että täydennät vastaustasi mallivastauksen avulla.

Jos otat edellisen viikon mallivastauksen tämän viikon pohjaksi, kopioi hakemisto muualle kurssirepositorion alta (olettaen että olet kloonannut sen) ja tee sovelluksen sisältämästä hakemistosta uusi repositorio.

Testeistä

Osa tämän viikon tehtävistä saattaa hajottaa jotain edellisinä viikkoina tehtyjä testejä. Voit merkitä tehtävät testien hajoamisesta huolimatta, eli testien pitäminen kunnossa on vapaaehtoista.

Muistutus debuggerista

Viikolla 2 tutustuimme byebug-debuggeriin. Valitettavasti debuggeri ei ole vielä löytänyt tietänsä jokaisen kurssilaisen työkaluvalikoimaan.

Debuggerin käyttö on erittäin helppoa. Riittää kirjoittaa komento byebug mihin tahansa kohtaan sovelluksen koodia. Seuraavassa esimerkki:

class PlacesController < ApplicationController
   # ...

  def search
    byebug
    @places = BeermappingApi.places_in(params[:city])
    session[:previous_city] = params[:city]
    if @places.empty?
      redirect_to places_path, notice: "No locations in #{params[:city]}"
    else
      render :index
    end
  end
end

Tarkastelemme siis debuggerilla BeermappingApia käyttävää osaa sovelluksesta. Kun nyt sovelluksella haetaan jotain olutravintolaa avaa debuggeri konsolisession koodiin merkittyyn kohtaan:

[6, 15] in /Users/mluukkai/kurssirepot/wadror/ratebeer/app/controllers/places_controller.rb
    6:     @place = BeermappingApi.find(params[:id],session[:previous_city] )
    7:   end
    8:
    9:   def search
   10:     byebug
=> 11:     @places = BeermappingApi.places_in(params[:city])
   12:     session[:previous_city] = params[:city]
   13:     if @places.empty?
   14:       redirect_to places_path, notice: "No locations in #{params[:city]}"
   15:     else

(byebug) params
{"utf8"=>"✓", "authenticity_token"=>"KA26q0QTqbFhnyuql7k3W4iwGlgUky3wwz1PwypUy+4=", "city"=>"helsinki", "commit"=>"Search", "action"=>"search", "controller"=>"places"}
(byebug) params[:city]
"helsinki"
(byebug)

eli pystymme mm. tarkastamaan että params hashin sisältö on sellainen kuin oletamme sen olevan.

Suoritetaan sitten seuraava komento ja katsotaan että tulos on odotetun kaltainen:

(byebug) next

[7, 16] in /Users/mluukkai/kurssirepot/wadror/ratebeer/app/controllers/places_controller.rb
    7:   end
    8:
    9:   def search
   10:     byebug
   11:     @places = BeermappingApi.places_in(params[:city])
=> 12:     session[:previous_city] = params[:city]
   13:     if @places.empty?
   14:       redirect_to places_path, notice: "No locations in #{params[:city]}"
   15:     else
   16:       render :index

(byebug) @places.size
7
(byebug) @places.first
#<Place:0x007fc9d7491878 @id="6742", @name="Pullman Bar", @status="Beer Bar", @reviewlink="http://beermapping.com/maps/reviews/reviews.php?locid=6742", @proxylink="http://beermapping.com/maps/proxymaps.php?locid=6742&d=5", @blogmap="http://beermapping.com/maps/blogproxy.php?locid=6742&d=1&type=norm", @street="Kaivokatu 1", @city="Helsinki", @state=nil, @zip="00100", @country="Finland", @phone="+358 9 0307 22", @overall="72.500025", @imagecount="0">
(byebug) @places.first.name
"Pullman Bar"
(byebug) continue

viimeinen komento jatkaa ohjelman normaalia suorittamista.

Debuggerin voi siis käynnistää mistä tahansa kohtaa sovelluksen koodia, myös testeistä tai jopa näkymistä. Kokeillaan debuggerin käynnistämistä uuden oluen luomislomakkeen renderöinnin aikana:

[10, 19] in /Users/mluukkai/kurssirepot/wadror/ratebeer/app/views/beers/_form.html.erb
   10:       </ul>
   11:     </div>
   12:   <% end %>
   13:
   14:   <% byebug %>
=> 15:
   16:   <div class="field">
   17:     <%= f.label :name %><br>
   18:     <%= f.text_field :name %>
   19:   </div>

(byebug) @styles.size
5
(byebug) @styles.first
#<Style id: 1, name: "European pale lager", description: "Similar to the Munich Helles story, many European c...", created_at: "2016-02-05 17:44:15", updated_at: "2015-02-05 18:21:13">
(byebug) options_from_collection_for_select(@styles, :id, :name, selected: @beer.style_id)
"<option value=\"1\">European pale lager</option>\n<option value=\"2\">American pale ale</option>\n<option value=\"3\">Baltic porter</option>\n<option value=\"4\">Weizen</option>\n<option value=\"5\">Sahti</option>"
(byebug)

Näkymätemplateen on siis lisätty <% byebug %>. Kuten huomaamme, on jopa näkymän apumetodin options_from_collection_for_select kutsuminen mahdollista debuggerista käsin!

Eli vielä kertauksena kun kohtaat ongelman, turvaudu arvailun sijaan byebugiin!

Rails-konsolin käytön tärkeyttä sovelluskehityksen välineenä on yritetty korostaa läpi kurssin. Eli kun teet jotain vähänkin epätriviaalia, testaa asia ensin konsolissa. Joissain tilanteissa voi olla jopa parempi tehdä kokeilut debuggerin avulla avautuvassa konsolissa, sillä tällöin on mahdollista avata konsolisessio juuri siihen kontekstiin, mihin koodia ollaan kirjoittamassa. Näin ollen päästään käsiksi esim. muuttujiin params, sessions ym. suorituskontekstista riippuvaan dataan.

Bootstrap

Toistaiseksi emme ole kiinnittäneet ollenkaan huomiota sovelluksiemme ulkoasuun. Modernin ajattelun mukaan HTML-koodi määrittelee ainoastaan sivujen tietosisällön ja ulkoasu määritellään erillisissä CSS-tiedostoissa.

HTML:ssä merkataan elementtejä luokilla (class), ja id:illä, jotta tyylitiedostojen määrittelemiä tyylejä saadaan ohjattua halutuille kohtiin sivua.

Määrittelimme jo muutama viikko sitten, että application layoutiin sijoittamamme navigointipalkki sijaitsee div-elementisssä jolle on asetettu luokka "navibar":

<div class="navibar">
  <%= link_to 'breweries', breweries_path %>
  <%= link_to 'beers', beers_path %>
  <%= link_to 'styles', styles_path %>
  <%= link_to 'ratings', ratings_path %>
  <%= link_to 'users', users_path %>
  <%= link_to 'clubs', beer_clubs_path %>
  <%= link_to 'places', places_path %>
  |
  <% if not current_user.nil? %>
    <%= link_to current_user.username, current_user %>
    <%= link_to 'rate a beer', new_rating_path %>
    <%= link_to 'join a club', new_membership_path %>
    <%= link_to 'signout', signout_path, method: :delete %>
  <% else %>
    <%= link_to 'signin', signin_path %>
    <%= link_to 'signup', signup_path %>
  <% end %>
</div>

Määrittelimme viikolla 2 navigointipalkille tyylin lisäämällä hakemistossa app/assets/stylesheats/ sijaitsevaan tiedostoon application.css seuraavat:

.navibar {
    padding: 10px;
    background: #EFEFEF;
}

CSS:ää käyttämällä koko sivuston ulkoasu voitaisiin muotoilla sivuston suunnittelijan haluamalla tavalla, jos silmää ja kykyä muotoiluun löytyy.

Sivuston muotoilunkaan suhteen ei onneksi ole enää tarvetta keksiä pyörää uudelleen. Bootstrap (vanhalta nimeltään Twitter Bootstrap) http://getbootstrap.com/ on "kehys", joka sisältää suuren määrän web-sivujen ulkoasun muotoiluun tarkoitettuja CSS-tyylitiedostoja ja javascriptiä. Bootstrap onkin noussut nopeasti suureen suosioon web-sivujen ulkoasun muotoilussa.

Aloitetaan sitten sovelluksemme bootstrappaaminen gemin https://github.com/twbs/bootstrap-sass. Lisätään Gemfileen seuraavat:

gem 'bootstrap-sass'
group :development do
  gem 'rails_layout'
end

Otetaan gemit käyttöön komennolla bundle install.

Generoidaan seuraavaksi sovellukselle Bootstrapin tarvitsemat tiedostot. Ota kuitenkin ensin talteen sovelluksen navigaatiopalkin generoiva koodi. Suoritetaan sitten bootstrapin tarvitsemien tiedostojen generointi (mm. tiedoston application.html.erb ylikirjottavalla) komennolla

rails generate layout:install bootstrap3 --force

Käynnistetään rails server uudelleen. Kun nyt avaamme sovelluksen selaimella, huomaamme jo pienen muutoksen esim. fonteissa. Myös navigointipalkki on hävinnyt.

HUOM: jos hakemistoon app/assets/stylesheets jäi vielä tiedosto application.css, saatat joutua poistamaan sen sillä yo. skripti on luonut korvaavan tiedoston application.css.scss

Edellä suorittamamme komento on luonut navigointipalkkia varten hakemistoon app/views/layout tiedostot _navigation.html.erb ja _navigation_links.html.erb

Kuten arvata saattaa, navigointipalkkiin tulevat linkit sijoitetaan tiedostoon _navigation_links.html.erb. Jokainen linkki tulee sijoittaa li-tagin sisälle. Lisää tiedostoon seuraavat:

<li><%= link_to 'breweries', breweries_path %></li>
<li><%= link_to 'beers', beers_path %></li>
<li><%= link_to 'styles', styles_path %></li>
<li><%= link_to 'ratings', ratings_path %></li>
<li><%= link_to 'users', users_path %></li>
<li><%= link_to 'clubs', beer_clubs_path %></li>
<li><%= link_to 'places', places_path %></li>
<% if not current_user.nil? %>
    <li><%= link_to current_user.username, current_user %></li>
    <li><%= link_to 'rate a beer', new_rating_path %></li>
    <li><%= link_to 'join a club', new_membership_path %></li>
    <li><%= link_to 'signout', signout_path, method: :delete %></li>
<% else %>
    <li><%= link_to 'signin', signin_path %></li>
    <li><%= link_to 'signup', signup_path %></li>
<% end %>

Sen lisäksi että Bootstrapilla voi helposti muodostaa navigointipalkin, joka pysyy jatkuvasti sivun ylälaidassa, voidaan Bootstrapin grid-järjestelmän avulla jakaa sivu erillisiin osiin, ks. http://getbootstrap.com/css/#grid

Muutetaan sovelluksen layoutin eli tiedoston application.html.erb sivupohjan renderöivää osaa seuraavasti:

  <body>
    <header>
      <%= render 'layouts/navigation' %>
    </header>

    <main role="main" class=".container">
      <%= render 'layouts/messages' %>

      <div class="row">
        <div class="col-md-8">
          <%= yield %>
        </div>
        <div class="col-md-4">
          <img src="http://www.cs.helsinki.fi/u/mluukkai/wadror/pint.jpg" width="200">
        </div>
      </div>

    </main>
  </body>

Eli sijoitamme bootstrapin containeriin yhden rivin, jonka jaamme kahteen sarakkeeseen: 8:n levyiseen johon kunkin sivun tiedot upotetaan ja 4:n levyiseen osaan jossa näytämme kuvan.

Sivun pohja on nyt kunnossa ja voimme hyödyntää bootstrapin tyylejä ja komponentteja sivuillamme.

Navigaatio määriteltiin jo navbar-komponentin ks. http://getbootstrap.com/components/#navbar avulla.

Muotoillaan seuraavaksi hieman sivulla käyttämiämme taulukoita. Bootstrapin sivulta http://getbootstrap.com/css/#tables näemme, että taulukon normaali bootstrap-muotoilu saadaan käyttöön lisäämällä taulukon HTML-koodille luokka table, seuraavasti:

<table class="table">
  ...
</table>

Lisätään luokkamäärittely esim. oluiden sivulle ja kokeillaan. Näyttää jo paljon professionaalimmalta. Päätetään vielä lisätä luokka table-hover, jonka ansioista se rivi jonka kohdalla hiiri on muuttuu korostetuksi, eli taulukon luokkamäärittelyksi tulee

<table class="table table-hover">
  ...
</table>

Tehtävä 1

Muuta ainakin muutama sovelluksen taulukoista käyttämään bootstrapin tyylejä.

Bootstrap tarjoaa valmiit tyylit myös painikkeille http://getbootstrap.com/css/#buttons

Päätetään käyttää luokkaparin btn btn-primary määrittelemää sinistä painiketta. Seuraavassa esimerkki, missä luokka on lisätty oluen reittauksen tekevälle painikkeelle:

  <h4>give a rating:</h4>

  <%= form_for(@rating) do |f| %>
    <%= f.hidden_field :beer_id %>
    score: <%= f.number_field :score %>
    <%= f.submit class:"btn btn-primary" %>
  <% end %>

Luokka voidaan lisätä myös niihin linkkeihin, jotka halutaan napin painikkeen näköisiksi:

<%= link_to 'New Beer', new_beer_path, class:'btn btn-primary' %>

Tehtävä 2

Lisää sovelluksen ainakin muutamille painikkeille ja painikkeen tapaan toimiville linkeille valitut tyylit. Poisto-operaatioissa tyyliksi kannattaa laittaa btn btn-danger.

Tehtävä 3

Muuta navigointipalkkia siten, että käyttäjän kirjautuessa kirjautunutta käyttäjää koskevat toiminnot tulevat menupalkin dropdowniksi alla olevan kuvan tapaan.

Ohjeita löydät dokumentista http://getbootstrap.com/components/#nav-dropdowns

kuva

Tehtävä 4

Tee jostain sivustosi osasta tyylikkäämpi käyttämällä jotain Bootstrapin komponenttia. Saat merkitä rastin jos käytät aikaa sivustosi ulkoasun parantamiseen vähintään 15 minuuttia. Saat rastin myös jos muutat loputkin sovelluksen taulukoista ja napeista käyttämään bootstrapin tyylejä.

Scopet

Osa panimoista on jo lopettanut toimintansa ja haluaisimme eriyttää lopettaneet panimot aktiivisten panimoiden listalta. Lisätään painimotietokantaan aktiivisuuden merkkaava boolean-arvoinen sarake. Luodaan migraatio:

rails g migration AddActivityToBrewery active:boolean

Huom: koska migraation nimi alkaa sanalla Add ja loppuu olion nimeen Brewery, ja sisältää tiedon lisättävästä sarakkeesta, generoituu juuri oikea migraatiokoodi automaattisesti.

Suoritetaan migraatio ja käydään konsolista käsin merkkaamassa kaikki tietokannassa olevat panimot aktiiviseksi:

irb(main):020:0> Brewery.all.each{ |b| b.active=true; b.save }

Käydään luomassa uusi panimo, jotta saamme tietokantaamme myös yhden epäaktiivisen panimon.

Muutetaan sitten panimon sivua siten, että se kertoo panimon mahdollisen epäaktiivisuuden panimon nimen vieressä:

<h2><%= @brewery.name %>
  <% if not @brewery.active  %>
      <span class="label label-info">retired</span>
  <% end %>
</h2>

<p>
  <em>Established year:</em>
  <%= @brewery.year %>
</p>

<p>Number of beers <%= @brewery.beers.count%> </p>

<ul>
 <% @brewery.beers.each do |beer| %>
   <li><%= link_to beer.name, beer %></li>
 <% end %>
</ul>

<% if @brewery.ratings.empty? %>
    <p>beers of the brewery have not yet been rated! </p>
<% else %>
    <p>Has <%= pluralize(@brewery.ratings.count,'rating') %>, average <%= @brewery.average_rating %> </p>
<% end %>

<% if current_user %>
  <%= link_to 'Edit', edit_brewery_path(@brewery), class:"btn btn-primary"  %>
  <%= link_to 'Destroy', @brewery, method: :delete, data: { confirm: 'Are you sure?' }, class:"btn btn-danger"  %>
<% end %>

Panimon luomis- ja editointilomakkeeseen on syytä lisätä mahdollisuus panimon aktiivisuuden asettamiseen. Lisätään views/breweries/_form.html.erb:iin checkbox aktiivisuuden säätelyä varten:

  <div class="field">
    <%= f.label :active %>
    <%= f.check_box :active %>
  </div>

Kokeillaan. Huomaamme kuitenkin että aktiivisuuden muuttaminen ei toimi.

Syynä tälle on se, että attribuuttia active ei ole lueteltu massasijoitettavaksi sallittujen attribuuttien joukossa.

Tutkitaan hieman panimokontrolleria. Sekä uuden panimon luominen, että panimon tietojen muuttaminen hakevat panimoon liittyvät tiedot metodin brewery_params avulla:

  def create
    @brewery = Brewery.new(brewery_params)

    # ...
  end

  def update
    # ...
    if @brewery.update(brewery_params)
    # ...
  end

  def brewery_params
    params.require(:brewery).permit(:name, :year)
  end

Kuten viikolla 2 totesimme on jokainen massasijoitettavaksi tarkoitettu attribuutti eksplisiittisesti sallittava permit metodin avulla. Muutetaan metodia brewery_params seuraavasti:

  def brewery_params
    params.require(:brewery).permit(:name, :year, :active)
  end

Päätetään, että haluamme näyttää panimoiden listalla erikseen aktiiviset ja epäaktiiviset panimot. Suoraviivainen ratkaisu on seuraava. Talletetaan kontrollerissa aktiiviset ja passiiviset omiin muuttujiinsa:

  def index
    @active_breweries = Brewery.where(active:true)
    @retired_breweries = Brewery.where(active:[nil, false])
  end

Kentän active-arvo voi olla joko eksplisiittisesti asetettu false tai nil jotka molemmat tarkoittavat eläköitynyttä panimoa, olemme joutuneet lisäämään jälkimmäiseen where-lauseeseen molemmat vaihtoehdot.

Copypastetaan näkymään taulukko kahteen kertaan, erikseen aktiivisille ja eläköityneille:

<h1>Breweries</h1>

<h2>Active</h2>

<p> Number of active breweries: <%= @active_breweries.count %> </p>

<table class="table table-hover">
  <thead>
    <tr>
    <th>Name</th>
    <th>Year</th>
    </tr>
  </thead>

  <tbody>
    <% @active_breweries.each do |brewery| %>
      <tr>
        <td><%= link_to brewery.name, brewery %></td>
        <td><%= brewery.year %></td>
        <td></td>
      </tr>
    <% end %>
  </tbody>
</table>

<h2>Retired</h2>

<p> Number of retired breweries: <%= @retired_breweries.count %> </p>

<table class="table table-hover">
  <thead>
  <tr>
    <th>Name</th>
    <th>Year</th>
  </tr>
  </thead>

  <tbody>
  <% @retired_breweries.each do |brewery| %>
      <tr>
        <td><%= link_to brewery.name, brewery %></td>
        <td><%= brewery.year %></td>
        <td></td>
      </tr>
  <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New Brewery', new_brewery_path, class:"btn btn-primary"  %>

Ratkaisu on toimiva, mutta siinä on parillakin tapaa parantamisen varaa. Parannellaan ensin kontrolleria.

Kontrolleri siis haluaa listan sekä aktiivisista että jo lopettaneista panimoista. Kontrolleri myös kertoo kuinka nuo listat haetaan tietokannasta.

Voisimme tehdä kontrollerista siistimmän, jos luokka Brewery tarjoaisi mukavamman rajapinnan panimoiden listan hakuun. ActiveRecord tarjoaa tähän mukavan ratkaisun, scopet, ks. http://guides.rubyonrails.org/active_record_querying.html#scopes

Määritellään nyt panimoille kaksi scopea, aktiiviset ja eläköityneet:

class Brewery < ActiveRecord::Base
  include RatingAverage

  validates :name, presence: true
  validates :year, numericality: { less_than_or_equal_to: ->(_) { Time.now.year} }

  scope :active, -> { where active:true }
  scope :retired, -> { where active:[nil,false] }

  has_many :beers, :dependent => :destroy
  has_many :ratings, :through => :beers
end

Scope määrittelee luokalle metodin, joka palauttaa kaikki scopen määrittelevän kyselyn palauttamat oliot.

Nyt Brewery-luokalta saadaan pyydettyä kaikkien panimoiden lisäksi mukavan rajapinnan avulla aktiiviset ja lopettaneet panimot:

Brewery.all      # kaikki panimot
Brewery.active   # aktiiviset
Brewery.retired  # lopettaneet

Kontrollerista tulee nyt elegantti:

  def index
    @active_breweries = Brewery.active
    @retired_breweries = Brewery.retired
  end

Ratkaisu on luettavuuden lisäksi parempi myös olioiden vastuujaon kannalta. Ei ole järkevää laittaa kontrollerin vastuulle sen kertomista miten aktiiviset ja eläköityneet panimot tulee hakea kannasta, sen sijaan tämä on hyvin luontevaa antaa modelin vastuulle, sillä modelin rooli on nimenomaan toimia abstraktiokerroksena muun sovelluksen ja tietokannan välillä.

Kannattaa huomioida, että ActiveRecord mahdollistaa operaatioiden ketjuttamisen. Voitaisiin kirjoittaa esim:

  Brewery.where(active:true).where("year>2000")

ja tuloksena olisi SQL-kysely

  SELECT "breweries".* FROM "breweries" WHERE "breweries"."active" = ? AND (year>2000)

ActiveRecord osaa siis optimoida ketjutetut metodikutsut yhdeksi SQL-operaatioksi. Myös scope toimii osana ketjutusta, eli vuoden 2000 jälkeen persutetut, edelleen aktiiviset panimot saataisiin selville myös seuraavalla 'onlinerilla':

  Brewery.active.where("year>2000")

Partiaalit

Siistitään seuraavaksi panimolistan näyttötemplatea. Templatessa on nyt käytännössä sama taulukko kopioituna kahteen kertaan peräkkäin. Eristämme taulukon omaksi partiaaliksi, eli näyttötemplateen upotettavaksi tarkoitetuksi näyttötemplaten palaksi, ks. http://guides.rubyonrails.org/layouts_and_rendering.html#using-partials.

Annetaan partialille nimi views/breweries/_list.html.erb (Huom: partialien nimet ovat aina alaviiva-alkuisia!). Sisältö on seuraava:

<table class="table table-hover">
  <thead>
  <tr>
    <th>Name</th>
    <th>Year</th>
  </tr>
  </thead>

  <tbody>
  <% breweries.each do |brewery| %>
      <tr>
        <td><%= link_to brewery.name, brewery %></td>
        <td><%= brewery.year %></td>
        <td></td>
      </tr>
  <% end %>
  </tbody>
</table>

Partiaali viittaa nyt taulukkoon sijoitettavien panimoiden listaan nimellä breweries.

Kaikki panimot renderöivä template ainoastaan renderöi partiaalin ja lähettää sille renderöitävän panimolistan parametriksi:

<h1>Breweries</h1>

<h2>Active</h2>

<p> Number of active breweries: <%= @active_breweries.count %> </p>

<%= render 'list', breweries: @active_breweries %>

<h2>Retired</h2>

<p> Number of retired breweries: <%= @retired_breweries.count %> </p>

<%= render 'list', breweries: @retired_breweries %>

<br>

<%= link_to 'New Brewery', new_brewery_path, class:"btn btn-primary"  %>

Panimoiden sivun template on nyt lähes silmiä hivelevä!

Tehtävä 5-6 (kahden tehtävän arvoinen)

Ratings-sivumme on tällä hetkellä hieman tylsä. Muuta sivua siten, että sillä näytetään reittausten sijaan:

  • kolme reittausten keskiarvon perusteella parasta olutta ja panimoa
  • kolme eniten reittauksia tehnyttä käyttäjää
  • viisi viimeksi tehtyä reittausta.

Vihjeitä: Tee luokalle Rating scope :recent, joka palauttaa viisi viimeisintä reittausta. Scopen vaatimaan tietokantakyselyyn löydät apuja linkistä http://guides.rubyonrails.org/active_record_querying.html, ks. order ja limit. Kokeile ensin kyselyn tekoa konsolista!

Parhaiden oluet ja panimot sekä innokkaimmat reittaajat kertovien scopejen teko ei onnistu yhtä helposti, sillä scopen palauttamat oliot pitäisi selvittää tietokantatasolla eli tarvittaisiin monimutkaista SQL:ää.

Scopejen sijaan voit tehdä luokille Brewery, Beer ja User luokkametodit (eli Javan terminologiassa staattiset metodit), joiden avulla kontrolleri saa haluamansa panimot, oluet ja käyttäjät. Esim. panimolla metodi olisi suunilleen seuraavanlainen:

class Brewery
 # ...

 def self.top(n)
   sorted_by_rating_in_desc_order = Brewery.all.sort_by{ |b| -(b.average_rating||0) }
   # palauta listalta parhaat n kappaletta
   # miten? ks. http://www.ruby-doc.org/core-2.1.0/Array.html
 end
end

Metodia käytetään nyt kontrollerista seuraavasti:

 @top_breweries = Brewery.top 3

Huom: oluiden, tyylien ja panimoiden top-metodit ovat oikeastaan copypastea ja moduuleja käyttämällä olisi mahdollista saada koodin määrittely siirrettyä yhteen paikkaan. Kun olet tehnyt viikon kaikki tehtävät voit yrittää siistiä koodisi!

Älä copypastaa näyttöjen koodia vaan käytä tarvittaessa partiaaleja.

Tehtävä 7

Lisää reittausten sivulle myös parhaat kolme oluttyyliä

Reittausten sivu voi näyttää tehtävävien jälkeen esim. seuraavalta:

kuva

Sivun muotoiluun voi olla apua seuraavasta: http://getbootstrap.com/css/#grid-nesting

Näyttöjen koodin siistiminen helpereillä

Viikolla 3 lisäsimme luokkaan ApplicationController metodin current_user jonka määrittelimme myös ns. helper-metodiksi

class ApplicationController < ActionController::Base
  # ...
  helper_method :current_user

 end

näin sekä kontrollerit että näkymät voivat tarvittaessa käyttää metodia kirjaantuneena olevan käyttäjän identiteetin tarkastamiseen. Koska metodi on määritelty luokkaan ApplicationController on se automaattisesti kaikkien kontrollerien käytössä. Helper-metodiksi määrittely tuo metodin myös näkymien käyttöön.

Sovelluksissa on usein tarve kirjoittaa apumetodeja (eli Railsin terminologian mukaan helper-metodeja) pelkästään näyttötemplateja varten. Tällöin niitä ei kannata sijoittaa ApplicationController-luokkaan vaan hakemiston app/helpers/ alla oleviin moduuleihin. Jos apumetodia on tarkoitus käyttää useammasta näytöstä, on oikea sijoituspaikka application_helper, jos taas apumetodit ovat tarpeen ainoastaan yhden kontrollerin alaisuudessa olevilla sivuilla, kannattaa ne määritellä ko. kontrolleria vastaavaan helper-moduliin.

Huomaamme, että näyttöjemme koodissa on joitain toistuvia osia. Esim. oluen, tyylin ja panimon show.html.erb-templateissa on kaikissa hyvin samantapainen koodi, jolla sivulle luodaan tarvittaessa linkit editointia ja poistamista varten:

<%= if current_user %>
  <%= link_to 'Edit', edit_beer_path(@beer), class:"btn btn-primary" %>

  <%= link_to 'Delete', @beer, method: :delete, data: { confirm: 'Are you sure?' }, class:"btn btn-danger"  %>
<% end %>

Eriytetään nämä omaksi helperiksi, moduliin application_helper.rb

module ApplicationHelper
  def edit_and_destroy_buttons(item)
    unless current_user.nil?
      edit = link_to('Edit', url_for([:edit, item]), class:"btn btn-primary")
      del = link_to('Destroy', item, method: :delete,
                    data: {confirm: 'Are you sure?' }, class:"btn btn-danger")
      raw("#{edit} #{del}")
    end
  end

end

Metodi muodostaa link_to:n avulla kaksi HTML-linkkielementtiä ja palauttaa molemmat "raakana" (ks. http://apidock.com/rails/ActionView/Helpers/RawOutputHelper/raw), eli käytännössä HTML-koodina, joka voidaan upottaa sivulle.

Painikkeet lisätään esim. oluttyylin sivulle seuraavasti:

<h2>
  <%= @style.name %>
</h2>

<quote>
  <%= @style.description %>
</quote>

...

<%= edit_and_destroy_buttons(@style) %>

Näytön muodostava template siistiytyykin huomattavasti.

Painikkeet muodostava koodi olisi pystytty myös eristämään omaan partialiin, ja onkin hiukan makuasia kumpi on tässä tilanteessa parempi ratkaisu, helper-metodi vai partiali.

Tehtävä 8

Usealla sovelluksen sivulla näytetään reittausten keskiarvoja. Keskiarvot ovat Decimal-tyyppiä, joten ne tulostuvat välillä hieman liiankin monen desimaalin tarkkuudella. Määrittele reittausten keskiarvon renderöintiä varten apumetodi round(param), joka tulostaa aina parametrinsa yhden desimaalin tarkkuudella, ja ota apumetodi käyttöön (ainakin joissakin) näyttötemplateissa.

Voit käyttää helpperissäsi esim. Railsista löytyvää number_with_precision-metodia, ks. http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_with_precision

Reitti panimon statuksen muuttamiselle

Lisäsimme hetki sitten panimoille tiedon niiden aktiivisuudesta ja mahdollisuuden muuttaa panimon aktiivisuusstatusta panimon tietojen editointilomakkeesta. Kuvitellaan hieman epärealistisesti, että panimot voisivat vähän väliä lopettaa ja aloittaa jälleen toimintansa. Tällöin aktiivisuusstatuksen muuttaminen panimon tietojen editointilomakkeelta olisi hieman vaivalloista. Tälläisessä tilanteessa olisikin kätevämpää, jos esim. kaikkien panimoiden listalla olisi panimon vieressä nappi, jota painamalla panimon aktiivisuusstatuksen muuttaminen onnistuisi. Voisimme toteuttaa tälläisen napin upottamalla panimoiden listalle jokaisen panimon kohdalle sopivan lomakkeen. Teemme kuitenkin nyt toisenlaisen ratkaisun. Lisäämme panimoille Railsin oletusarvoisen kuuden reitin lisäksi uuden reitin toggle_activity, johon tehdyn HTTP POST -kutsun avulla panimon aktiivisuusstatusta voi muuttaa.

Tehdään tiedostoon routes.rb seuraava muutos panimon osalta:

  resources :breweries do
    post 'toggle_activity', on: :member
  end

Kun nyt teemme komennon rake routes huomaamme panimolle ilmestyneen uuden reitin:

toggle_activity_brewery POST   /breweries/:id/toggle_activity(.:format) breweries#toggle_activity
              breweries GET    /breweries(.:format)                     breweries#index
                        POST   /breweries(.:format)                     breweries#create
            new_brewery GET    /breweries/new(.:format)                 breweries#new
           edit_brewery GET    /breweries/:id/edit(.:format)            breweries#edit
                brewery GET    /breweries/:id(.:format)                 breweries#show
                        PUT    /breweries/:id(.:format)                 breweries#update
                        DELETE /breweries/:id(.:format)                 breweries#destroy

Päätämme lisätä aktiivisuusstatuksen muutostoiminnon yksittäisen panimon sivulle. Eli lisätään panimon sivulle app/views/breweries/show.html.erb seuraava:

<%= link_to "change activity", toggle_activity_brewery_path(@brewery.id), method: :post, class: "btn btn-primary" %>

Kun nyt klikkaamme painiketta, tekee selain HTTP POST -pyynnön osoitteeseen /breweries/:id/toggle_activity, missä :id on sen panimon id, jolla linkkiä klikattiin. Railsin reititysmekanismi yrittää kutsua breweries-kontrollerin metodia toggle_activity jota ei ole, joten seurauksena on virheilmoitus. Metodi voidaan toteuttaa esim. seuraavasti:

  def toggle_activity
    brewery = Brewery.find(params[:id])
    brewery.update_attribute :active, (not brewery.active)

    new_status = brewery.active? ? "active" : "retired"

    redirect_to :back, notice:"brewery activity status changed to #{new_status}"
  end

Tominnallisuuden toteuttaminen oli varsin helppoa, mutta onko reitin toggle_activity lisääminen järkevää? RESTful-ideologian mukaan puhdasoppisempaa olisi ollut hoitaa asia lomakkeen avulla, eli polkuun breweries/:id kohdistuneella PUT-pyynnöllä. Jokatapauksessa tulee välttää tilanteita, joissa resurssin tilaa muutettaisiin GET-pyynnöllä, ja tästä syystä määrittelimmekin polun toggle_activity ainoastaan POST-pyynnöille.

Lisää custom routeista sivulla http://guides.rubyonrails.org/routing.html#adding-more-restful-actions

Admin-käyttäjä ja pääsynhallintaa

Tehtävä 9

Tällä hetkellä kuka tahansa kirjautunut käyttäjä voi poistaa panimoja, oluita ja olutseuroja. Laajennetaan järjestelmää siten, että osa käyttäjistä on administraattoreja, ja poisto-operaatioit ovat vain sallittuja vain heille

  • luo User-modelille uusi boolean-muotoinen kenttä admin, jonka avulla merkataan ne käyttäjät joilla on ylläpitäjän oikeudet järjestelmään
  • riittää, että käyttäjän voi tehdä ylläpitäjäksi ainoastaan konsolista
  • tee panimoiden, oluiden, olutseurojen ja tyylien poisto-operaatioista ainoastaan ylläpitäjälle mahdollinen toimenpide

Huom: salasanan validoinnin takia käyttäjän tekeminen adminiksi konsolista ei onnistu, jos salasanakenttiin ei ole asetettu arvoja:

irb(main):001:0> u = User.first
irb(main):002:0> u.admin = true
irb(main):003:0> u.save
  (0.1ms)  rollback transaction
=> false

Yksittäisten attribuuttien arvon muuttaminen on kuitenkin mahdollista validaation kiertävällä metodilla update_attr:

irb(main):005:0> u.update_attribute(:admin, true)

Validointien suorittamisen voi ohittaa myös tallentamalla olion komennolla u.save(validate: false)

HUOM: toteutuksessa kannattanee hyödyntää esifiltteriä

Tehtävä 10-11 (kahden tehtävän arvoinen)

Toteuta toiminnallisuus, jonka avulla administraattorit voivat jäädyttää jonkin käyttäjätunnuksen. Jäädyttäminen voi tapahtua esim. napilla, jonka vain administraattorit näkevät käyttäjän sivulla. Jäädytetyn tunnuksen omaava käyttäjä ei saa päästä kirjautumaan järjestelmään. Yrittäessään kirjautumista, sovellus huomauttaa käyttäjälle että hänen tunnus on jäädytetty ja kehoittaa ottamaan yhteyttä ylläpitäjiin. Administraattorien tulee pystyä palauttamaan jäädytetty käyttäjätunnus ennalleen.

HUOM: älä määrittele Userille attribuuttia nimeltä frozen, kyseessä on kielletty attribuutin nimi!

Voit toiteuttaa toiminnallisuuden esim. allaolevien vihjaamaan kuvien tapaan

Administraattori voi jäädyttää käyttäjätunnuksen käyttäjän sivulta

kuva

Administraattori näkee käyttäjien näkymästä jäädytetyt käyttäjätunnukset

kuva

Jos käyttjätunnus on jäädytetty kirjautuminen ei onnistu

kuva

Administraattori voi uudelleenaktivoida jäädytetyn käyttäjätunnuksen käyttäjän sivulta

kuva

Monimutkaisempi pääsynhallinta

Jos sovelluksessa on tarvetta monipuolisempaan pääsynhallintaan (engl. authorization), kannattanee asia hoitaa esim. cancan-gemin avulla ks. https://github.com/CanCanCommunity/cancancan ja http://railscasts.com/episodes/192-authorization-with-cancan

Aihetta esittelevä Rails cast on jo aika ikääntynyt, eli tarkemmat ohjeet kannattaa katsoa projektin Github-sivulta. Rails castit tarjoavat todella hyviä esittelyjä monista aihepiireistä, eli vaikka castit eivät enää olisi täysin ajantasalla kaikkien detaljien suhteen, kannattaa ne usein silti katsoa läpi.

Rails-sovellusten tietoturvasta

Emme ole vielä toistaiseksi puhuneet mitään Rails-sovellusten tietoturvasta. Nyt on aika puuttua asiaan. Rails-guideissa on tarjolla erinomainen katsaus tyypillisimmistä web-sovellusten tietoturvauhista ja siitä miten Rails-sovelluksissa voi uhkiin varautua.

Tehtävät 12-14 (kolmen tehtävän arvoinen)

Lue http://guides.rubyonrails.org/security.html

Teksti on pitkä mutta asia on tärkeä. Jos haluat optimoida ajankäyttöä, jätä luvut 4, 5 ja 7.4-7.8 lukematta.

Voit merkata tehtävät tehdyksi kun seuraavat asiat selvillä

  • SQL-injektio
  • CSRF
  • XSS
  • järkevä sessioiden käyttö

Tietoturvaan liittyen kannattaa katsoa myös seuraavat

Yo. dokumentista ei käy täysin selväksi se, että Rails sanitoi (eli escapettaa kaikki script- ja html-tagit yms) oletusarvoisesti sivuilla renderöitävän syötteen, eli esim. jos yrittäisimme syöttää javascript-pätkän <script>alert('Evil XSS attack');</script> oluttyylin kuvaukseen, koodia ei suoriteta, vaan koodi renderöityy sivulle 'tekstinä':

kuva

Jos katsomme sivun lähdekoodia, huomaamme, että Rails on korvannut HTML-tägit aloittavat ja sulkevat < -ja > -merkit niitä vastaavilla tulostuvilla merkeillä, jolloin syöte muuttuu selaimen kannalta normaaliksi tekstiksi:

 &lt;script&gt;alert(&#39;Evil XSS attack&#39;);&lt;/script&gt;

Oletusarvoisen sanitoinnin saa 'kytkettyä pois' pyytämällä eksplisiittisesti metodin raw avulla, että renderöitävä sisältö sijoitetaan sivulle sellaisenaan. Jos muuttaisimme tyylin kuvauksen renderöintiä seuraavasti

<p>
  <%= raw(@style.description) %>
</p>

suoritetaan javascript-koodi sivun renderöinnion yhteydessä:

kuva

Lisätietoa http://www.railsdispatch.com/posts/security ja http://railscasts.com/episodes/204-xss-protection-in-rails-3

Metaohjelmointia: mielipanimoiden ja tyylin refaktorointi

Viikon 4 [tehtävissä 3 ja 4](ks. https://github.com/mluukkai/WebPalvelinohjelmointi2016/blob/master/web/viikko4.md#teht%C3%A4v%C3%A4-3) toteutettiin metodit henkilön suosikkipanimon ja oluttyylin selvittämiseen. Seuraavassa on eräs melko suoraviivainen ratkaisu metodien favorite_style ja favorite_brewery toteuttamiseen:

class User
  # ...
  def favorite_brewery
    return nil if ratings.empty?

    rated = ratings.map{ |r| r.beer.brewery }.uniq
    rated.sort_by { |brewery| -rating_of_brewery(brewery) }.first
  end

  def rating_of_brewery(brewery)
    ratings_of = ratings.select{ |r| r.beer.brewery==brewery }
    ratings_of.map(&:score).inject(&:+) / ratings_of.count.to_f
  end

  def favorite_style
    return nil if ratings.empty?

    rated = ratings.map{ |r| r.beer.style }.uniq
    rated.sort_by { |style| -rating_of_style(style) }.first
  end

  def rating_of_style(style)
    ratings_of = ratings.select{ |r| r.beer.style==style }
    ratings_of.map(&:score).inject(&:+) / ratings_of.count.to_f
  end

end

Tutkitaan mielipanimon selvittävää metodia:

  def favorite_brewery
    return nil if ratings.empty?

    rated = ratings.map{ |r| r.beer.brewery }.uniq
    rated.sort_by { |brewery| -rating_of_brewery(brewery) }.first
  end

Erikoistapauksen (ei yhtään reittausta) tarkastamisen jälkeen metodi selvittää minkä panimoiden oluita käyttäjä on reitannut:

  rated = ratings.map{ |r| r.beer.brewery }.uniq

Komento siis palauttaa listan käyttäjän tekemiin reittauksiin liittyvistä panimoista, komennon uniq ansiosta sama panimo ei esiinny listalla kuin kerran (jos et tiedä miten komento map toimii, googlaa).

Tämän jälkeen metodi järjestää reitattujen panimoiden listan käyttäen järjestämiskriteerinä panimon saamien reittausten keskiarvoa ja palauttaa listan ensimmäisen, eli parhaan keskiarvoreittauksen saaneen panimon.

  rated.sort_by { |brewery| -rating_of_brewery(brewery) }.first

Kunkin panimon reittausten keskiarvo lasketaan apumetodilla:

  def rating_of_brewery(brewery)
    ratings_of = ratings.select{ |r| r.beer.brewery==brewery }
    ratings_of.map(&:score).inject(&:+) / ratings_of.count.to_f
  end

Apumetodi valitsee aluksi käyttäjän tekemistä reittauksista ne, jotka koskevat parametriksi annettua panimoa. Tämän jälkeen lasketaan valittujen reittausten pistemäärien keskiarvo.

Huomaamme, että favorite_style toimii täsmälleen saman periaatteen mukaan ja metodi itse sekä sen käyttämä apumetodi ovatkin oikeastaan copypastea mielipanimon selvittämiseen käytettävästä koodista.

Koska ohjelmistossamme on kattavat testit, on copypaste helppo refaktoroida pois. Tutkitaan ensin apumetodeja, jotka laskevat yhden panimon ja yhden tyylin reittausten keskiarvon:

  def rating_of_style(style)
    ratings_of = ratings.select{ |r| r.beer.style==style }
    ratings_of.map(&:score).inject(&:+) / ratings_of.count.to_f
  end

  def rating_of_brewery(brewery)
    ratings_of = ratings.select{ |r| r.beer.brewery==brewery }
    ratings_of.map(&:score).inject(&:+) / ratings_of.count.to_f
  end

Uudelleennimetään metodien parametri:

  def rating_of_style(item)
    ratings_of = ratings.select{ |r| r.beer.style==item }
    ratings_of.map(&:score).inject(&:+) / ratings_of.count.to_f
  end

  def rating_of_brewery(item)
    ratings_of = ratings.select{ |r| r.beer.brewery==item }
    ratings_of.map(&:score).inject(&:+) / ratings_of.count.to_f
  end

Huomaamme, että erona metodeissa on ainoastaan select-metodin koodilohkossa reittaukseen liittyvälle olut-oliolle kutsuttava metodi.

Kutsuttava metodi voidaan antaa myös parametrina. Tällöin eksplisiittisen kutsun sijaan metodia kutsutaan olion send-metodin avulla:

  def rating_of(category, item)
    ratings_of = ratings.select{ |r| r.beer.send(category)==item }
    ratings_of.map(&:score).inject(&:+) / ratings_of.count.to_f
  end

Metodia voidaan nyt käyttää seuraavasti:

2.2.1 :037 > u = User.first
2.2.1 :038 > u.rating_of :brewery , Brewery.find_by(name:"BrewDog")
 => 30.2
2.2.1 :031 > u.rating_of :style , Style.find_by(name:"American IPA")
 => 36.666666666666664

Eli voimme luopua alkuperäisistä apumetodeista ja käyttää uutta paramterisoitua metodia:

  def favorite_style
    return nil if ratings.empty?

    rated = ratings.map{ |r| r.beer.style }.uniq
    rated.sort_by { |style| -rating_of(:style, style) }.first
  end

  def favorite_brewery
    return nil if ratings.empty?

    rated = ratings.map{ |r| r.beer.brewery }.uniq
    rated.sort_by { |brewery| -rating_of(:brewery, brewery) }.first
  end

Nimetään uudelleen metodien komennossa sort_by käyttämä apumuuttuja:

  def favorite_style
    return nil if ratings.empty?

    rated = ratings.map{ |r| r.beer.style }.uniq
    rated.sort_by { |item| -rating_of(:style, item) }.first
  end

  def favorite_brewery
    return nil if ratings.empty?

    rated = ratings.map{ |r| r.beer.brewery }.uniq
    rated.sort_by { |item| -rating_of(:brewery, item) }.first
  end

Muutetaan map:issa käytettävä metodikutsu muotoon send

  def favorite_style
    return nil if ratings.empty?

    rated = ratings.map{ |r| r.beer.send(:style) }.uniq
    rated.sort_by { |item| -rating_of(:style, item) }.first
  end

  def favorite_brewery
    return nil if ratings.empty?

    rated = ratings.map{ |r| r.beer.send(:brewery) }.uniq
    rated.sort_by { |item| -rating_of(:brewery, item) }.first
  end

Määritellään parametrina oleva metodin nimi muuttujan category avulla:

  def favorite_style
    category = :style
    return nil if ratings.empty?

    rated = ratings.map{ |r| r.beer.send(category) }.uniq
    rated.sort_by { |item| -rating_of(category, item) }.first
  end

  def favorite_brewery
    category = :brewery
    return nil if ratings.empty?

    rated = ratings.map{ |r| r.beer.send(category) }.uniq
    rated.sort_by { |item| -rating_of(category, item) }.first
  end

Nyt molempien metodien koodi on täysin sama. Yhteinen osa voidaankin eriyttää omaksi metodiksi:

  def favorite_style
    favorite :style
  end

  def favorite_brewery
    favorite :brewery
  end

  def favorite(category)
    return nil if ratings.empty?

    rated = ratings.map{ |r| r.beer.send(category) }.uniq
    rated.sort_by { |item| -rating_of(category, item) }.first
  end

Uuden ratkaisumme etu on copypasten poiston lisäksi se, että jos oluelle määritellään jokun uusi "attribuutti", esim. väri, saamme samalla hinnalla mielivärin selvittävän metodin:

  def favorite_color
    favorite :color
  end

method_missing

Metodit favorite_style ja favorite_brewery olisi oikeastaan mahdollista saada toimimaan ilman niiden eksplisiittistä määrittelemistä.

Kommentoidaan metodit hetkeksi pois koodistamme.

Jos oliolle kutsutaan metodia, jota ei ole olemassa (määriteltynä luokassa itsessään, sen yliluokissa eikä missään luokan tai yliluokkien sisällyttämässä moduulissa), esim.

2.2.1 :069 > u.paras_bisse
NoMethodError: undefined method `paras_bisse' for #<User:0x000001059cb0c0>
  from /Users/mluukkai/.rvm/gems/ruby-2.2.1/gems/activemodel-4.1.5/lib/active_model/attribute_methods.rb:435:in `method_missing'
  from /Users/mluukkai/.rvm/gems/ruby-2.2.1/gems/activerecord-4.1.5/lib/active_record/attribute_methods.rb:208:in `method_missing'
  from (irb):69
  from /Users/mluukkai/.rvm/gems/ruby-2.2.1/gems/railties-4.1.5/lib/rails/commands/console.rb:90:in `start'
  from /Users/mluukkai/.rvm/gems/ruby-2.2.1/gems/railties-4.1.5/lib/rails/commands/console.rb:9:in `start'
  from /Users/mluukkai/.rvm/gems/ruby-2.2.1/gems/railties-4.1.5/lib/rails/commands/commands_tasks.rb:69:in `console'
2.2.1 :070 >

on tästä seurauksena se, että Ruby-tulkki kutsuu olion method_missing-metodia parametrinaan tuntemattoman metodin nimi. Rubyssä kaikki luokat perivät Object-luokan, joka määrittelee method_missing-metodin. Luokkien on sitten tarvittaessa mahdollista ylikirjoittaa tämä metodi ja saada näinollen aikaan "metodeja" joita ei ole olemassa, mutta jotka kutsujan kannalta toimivat aivan kuten normaalit metodit.

Rails käyttää sisäisesti metodia method_missing moniin tarkoituksiin. Emme voikaan suoraviivaisesti ylikirjoittaa sitä, meidän on muistettava delegoida method_missing-kutsut yliluokalle jollemme halua käsitellä niitä itse.

Määritellään luokalle User kokeeksi seuraavanlainen method_missing:

  def method_missing(method_name, *args, &block)
    puts "nonexisting method #{method_name} was called with parameters: #{args}"
    return super
  end

kokeillaan:

2.2.1 :072 > u.paras_bisse
nonexisting method paras_bisse was called with parameters: []
NoMethodError: undefined method `paras_bisse' for #<User:0x000001016af8e0>
  from /Users/mluukkai/.rvm/gems/ruby-2.2.1/gems/activemodel-4.1.5/lib/active_model/attribute_methods.rb:435:in `method_missing'
  from /Users/mluukkai/.rvm/gems/ruby-2.2.1/gems/activerecord-4.1.5/lib/active_record/attribute_methods.rb:208:in `method_missing'
  from /Users/mluukkai/kurssirepot/ratebeer/app/models/user.rb:30:in `method_missing'
2.2.1 :073 >

Eli kuten ylimmältä riviltä huomataan, suoritettiin määrittelemämme method_missing-metodi. Voimmekin ylikirjoittaa method_missingin seuraavasti:

  def method_missing(method_name, *args, &block)
    if method_name =~ /^favorite_/
      category = method_name[9..-1].to_sym
      self.favorite category
    else
      return super
    end
  end

Nyt kaikki favorite_-alkuiset metodikutsut joita ei tunneta tulkitaan siten, että alaviivan jälkeinen osa eristetään ja kutsutaan oliolle metodia favorite, siten että alaviivan jälkiosa on kategorian määrittelevänä parametrina.

Nyt metodit favorite_brewery ja favorite_style "ovat olemassa" ja toimivat:

2.2.1 :076 > u = User.first
2.2.1 :077 > u.favorite_brewery.name
 => "Malmgard"
2.2.1 :078 > u.favorite_style.name
  => "Baltic porter"

Ikävänä sivuvaikutuksena metodien määrittelystä method_missing:in avulla on se, että mikä tahansa favorite_-alkuinen metodi "toimisi", mutta aiheuttaisi kenties epäoptimaalisen virheen.

2.2.1 :079 > u.favorite_movie
NoMethodError: undefined method `movie' for #<Beer:0x00000105a18690>
  from /Users/mluukkai/.rvm/gems/ruby-2.2.1/gems/activemodel-4.1.5/lib/active_model/attribute_methods.rb:435:in `method_missing'

Ruby tarjoaa erilaisia mahdollisuuksia mm. sen määrittelemiseen, mitkä favorite_-alkuiset metodit hyväksyttäisiin. Voisimme esim. toteuttaa seuraavan rubymäisen tavan asian määrittelemiselle:

class User < ActiveRecord::Base
  include RatingAverage

  favorite_available_by :style, :brewery

  # ...
end

Emme kuitenkaan lähde nyt tälle tielle. Hyöty tulisi näkyviin vasta jos favorite_-alkuisia metodeja voitaisiin hyödyntää muissakin luokissa.

Poistetaan kuitenkin nyt tässä tekemämme method_missing:iin perustuva toteutus ja palautetaan luvun alussa poiskommentoidut versiot.

Jos tässä luvussa esitellyn tyyliset temput kiinnostavat, voit jatkaa esim. seuraavista:

Tehtävien palautus

Commitoi kaikki tekemäsi muutokset ja pushaa koodi Githubiin. Deployaa myös uusin versio Herokuun.

Tehtävät kirjataan palautetuksi osoitteeseen http://wadrorstats2016.herokuapp.com/