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.
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.
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.
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>
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' %>
Lisää sovelluksen ainakin muutamille painikkeille ja painikkeen tapaan toimiville linkeille valitut tyylit. Poisto-operaatioissa tyyliksi kannattaa laittaa
btn btn-danger
.
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
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ä.
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")
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ä!
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
jaUser
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 endMetodia käytetään nyt kontrollerista seuraavasti:
@top_breweries = Brewery.top 3Huom: 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.
Lisää reittausten sivulle myös parhaat kolme oluttyyliä
Reittausten sivu voi näyttää tehtävävien jälkeen esim. seuraavalta:
Sivun muotoiluun voi olla apua seuraavasta: http://getbootstrap.com/css/#grid-nesting
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.
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
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
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 => falseYksittä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ä
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
Administraattori näkee käyttäjien näkymästä jäädytetyt käyttäjätunnukset
Jos käyttjätunnus on jäädytetty kirjautuminen ei onnistu
Administraattori voi uudelleenaktivoida jäädytetyn käyttäjätunnuksen käyttäjän sivulta
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.
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.
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ä':
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:
<script>alert('Evil XSS attack');</script>
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ä:
Lisätietoa http://www.railsdispatch.com/posts/security ja http://railscasts.com/episodes/204-xss-protection-in-rails-3
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
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:
- http://rubymonk.com/learning/books/5-metaprogramming-ruby-ascent
- http://rubymonk.com/learning/books/2-metaprogramming-ruby
- https://github.com/sathish316/metaprogramming_koans
- myös kirja Eloquent Ruby käsittelee aihepiiriä varsin hyvin
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/