Jatkamme sovelluksen rakentamista siitä, mihin jäimme viikon 1 lopussa. Allaoleva materiaali olettaa, että olet tehnyt kaikki edellisen viikon tehtävät. Jos et tehnyt kaikkia tehtäviä, voit täydentää ratkaisusi tehtävien palautusjärjestelmän kautta näkyvän esimerkivastauksen avulla.
Huom: muutamilla Macin käyttäjillä oli ongelmia Herokun tarvitseman pg-gemin kanssa. Paikallisesti gemiä ei tarvita ja se määriteltiinkin asennettavaksi ainoastaan tuotantoympäristöön. Jos ongelmia ilmenee, voit asentaa gemit antamalla bundle install
-komentoon seuraavan lisämääreen:
bundle install --without production
Tämä asetus muistetaan jatkossa, joten pelkkä bundle install
riittää kun haluat asentaa uusia riippuvuuksia.
Käytäthän jo järkevää editoria, eli jotain muuta kun nanoa, geditiä tai notepadia? Suositeltavia editoreja ovat esim. RubyMine, Visual Studio Code ks lisää täältä
Itse käytän nykyään Visual Studio Codea. Suosittelen! Jos käytät VSC:tä, kannattaa ehdottamasti asentaa Ruby-plugin
Käytimme viime viikolla rubyn oletusarvoista konsolia irbiä. On myös olemassa hieman kehittyneempi konsoli Pry, otetaan se käyttöön.
Lisää tiedostoon Gemfile rivi gem 'pry-rails'
seuraavaan kohtaan:
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
gem 'pry-rails' # lisää siis tämä rivi!
end
Suorita komentoriviltä komento bundle install
Kun nyt avaat rails konsolin, eli suoritat komentoriviltä komennon rails c
avautuu viime viikolla käyttämme irbin sijaan Pry. Perustoiminnoiltaan Pry on täsmälleen samanlainen kuin irb. Tulostusasu on hieman ihmisystävällisempi:
[59] pry(main)> Beer.first
Beer Load (0.4ms) SELECT "beers".* FROM "beers" ORDER BY "beers"."id" ASC LIMIT 1
=> #<Beer:0x00007fc430b910f8
id: 1,
name: "Iso 3",
style: "Lager",
brewery_id: 1,
created_at: Sat, 01 Sep 2018 16:41:53 UTC +00:00,
updated_at: Sat, 01 Sep 2018 16:41:53 UTC +00:00>
[60] pry(main)>
Jos operaation tulos on pidempi kuin ruudulle mahtuu, kontrolli ei palaa konsoliin, vaan alimpana rivinä on kaksoispiste
[61] pry(main)> Beer.all
Beer Load (0.3ms) SELECT "beers".* FROM "beers"
=> [#<Beer:0x00007fc4318bee10
id: 1,
name: "Iso 3",
style: "Lager",
brewery_id: 1,
created_at: Sat, 01 Sep 2018 16:41:53 UTC +00:00,
updated_at: Sat, 01 Sep 2018 16:41:53 UTC +00:00>,
#<Beer:0x00007fc4318beaa0
id: 2,
name: "Tuplahumala",
style: "Lager",
brewery_id: 1,
created_at: Sat, 01 Sep 2018 16:41:53 UTC +00:00,
updated_at: Sat, 01 Sep 2018 16:41:53 UTC +00:00>,
#<Beer:0x00007fc4318be910
id: 4,
name: "Huvila Pale Ale",
:
Voit selata tulosta nuolinäppäimillä ja pääset takaisin konsoliin painamalla q:ta.
Jos haluat oppia käyttämään Pryn edistyneempiä ominaisuuksia kannattaa katsoa aiheesta kertova Rails cast
Haluamme laittaa sivulle modernien web-sivustojen tyyliin navigointipalkin eli sijoittaa sovelluksen kaikkien sivujen ylälaitaan linkit oluiden ja panimoiden listoihin.
Navigointipalkki saadaan generoitua helposti metodin link_to
ja polkuapumetodien avulla lisäämällä jokaiselle sivulle seuraavat linkit:
<%= link_to 'breweries', breweries_path %>
<%= link_to 'beers', beers_path %>
Tarkkasilmäisimmät saattoivat jo viime viikolla huomata, että näkymätemplatet eivät sisällä kaikkea sivulle tulevaa HTML-koodia. Esim. yksittäisen oluen näkymätemplate /app/views/beers/show.html.erb on seuraava:
<p id="notice"><%= notice %></p>
<p>
<strong>Name:</strong>
<%= @beer.name %>
</p>
<p>
<strong>Style:</strong>
<%= @beer.style %>
</p>
<p>
<strong>Brewery:</strong>
<%= @beer.brewery_id %>
</p>
<%= link_to 'Edit', edit_beer_path(@beer) %> |
<%= link_to 'Back', beers_path %>
Jos katsomme yksittäisen oluen sivun HTML-koodia selaimen view source code -toiminnolla, huomaamme, että sivulla on paljon muutakin kuin templatessa määritelty HTML (osa headin sisällöstä on poistettu):
<!DOCTYPE html>
<html>
<head>
<title>Ratebeer</title>
<link data-turbolinks-track="true" href="/assets/application.css?body=1" media="all" rel="stylesheet" />
<script data-turbolinks-track="true" src="/assets/jquery.js?body=1"></script>
<meta content="authenticity_token" name="csrf-param" />
<meta content="hZaC8o95xUbekA3PTsVZ+JmkVj9CCn5a4Kw8tF96WOU=" name="csrf-token" />
</head>
<body>
<p id="notice"></p>
<p>
<strong>Name:</strong>
Iso 3
</p>
<p>
<strong>Style:</strong>
Lager
</p>
<p>
<strong>Brewery:</strong>
1
</p>
<a href="/beers/1/edit">Edit</a> |
<a href="/beers">Back</a>
</body>
</html>
Sivu sisältää siis dokumentin tyypin määrittelyn, käytettävät tyylitiedostot ja javascript-tiedostot määrittelevän head-elementin ja sivun sisällön määrittelevän body-elementin (ks. lisää http://www.w3.org/community/webed/wiki/HTML/Training).
Oluen sivun näkymätemplate siis sisältää ainoastaan body-elementin sisälle tulevan HTML-koodin.
On tyypillistä, että sovelluksen kaikki sivut ovat body-elementin sisältöä lukuun ottamatta samat. Railsissa saadaankin määriteltyä kaikille sivuille yhteiset osat sovelluksen layoutiin, eli tiedostoon app/views/layouts/application.html.erb. Oletusarvoisesti tiedoston sisältö on seuraavanlainen:
<!DOCTYPE html>
<html>
<head>
<title>Ratebeer</title>
<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
<%= javascript_include_tag "application", "data-turbolinks-track" => true %>
<%= csrf_meta_tags %>
</head>
<body>
<%= yield %>
</body>
</html>
Head-elementin sisällä olevat apumetodit määrittelevät sovelluksen käyttämät tyyli- ja javascript-tiedostot, apumetodi csrf_meta_tags
lisää sivulle CSRF-hyökkäykset eliminoivan logiikan
(ks. tarkemmin esim. täältä). Kuten arvata saattaa, body-elementin sisällä olevan komennon yield
-kohdalle renderöityy kunkin sivun oman näkymätemplaten määrittelemä sisältö.
Saamme navigointipalkin näkyville kaikille sivuille muuttamalla sovelluksen layoutin body-elementtiä seuraavasti:
<body>
<div class="navibar">
<%= link_to 'breweries', breweries_path %>
<%= link_to 'beers', beers_path %>
</div>
<%= yield %>
</body>
Navigointipalkki on laitettu luokan navibar sisältävän div-elementin sisällä, joten sen ulkoasua voidaan halutessa muotoilla css:n avulla.
Lisää tiedostoon app/assets/stylesheets/application.css seuraava:
.navibar {
padding: 10px;
background: #EFEFEF;
}
Kun reloadaat sivun, huomaat, että sovelluksesi antama vaikutelma on jo melko professionaali.
Railsin Routing-komponentin (ks. http://api.rubyonrails.org/classes/ActionDispatch/Routing.html, http://guides.rubyonrails.org/routing.html) vastuulla on ohjata eli reitittää sovellukselle tulevien HTTP-pyyntöjen käsittely sopivan kontrollerin metodille.
Tieto siitä miten eri URLeihin tulevat pyynnöt tulee reitittää, konfiguroidaan tiedostoon config/routes.rb
. Tässä vaiheessa tiedoston sisältö on seuraavanlainen:
Rails.application.routes.draw do
resources :beers
resources :breweries
end
Tutustumme myöhemmin resources
-metodin lisäämiin reitteihin.
Aloitetaan sillä, että tehdään panimoiden listasta sovelluksen oletusarvoinen kotisivu. Tämä tapahtuu lisäämällä routes-tiedostoon rivi
root 'breweries#index'
Nyt osoite http://localhost:3000/ ohjautuu kaikki panimot näyttävälle sivulle.
Edellinen on oikeastaan hieman tyylikkäämpi tapa sanoa:
get '/', to: 'breweries#index'
eli reititä polulle '/' tuleva HTTP GET -pyyntö käsiteltäväksi luokan BreweriesController
metodille index
.
Englanninkielistä kirjallisuutta lukiessa kannattaa huomata, että Railsin terminologiassa kontrollereiden metodeja nimitetään usein actioneiksi. Käytämme kuitenkin kurssilla nimitystä kontrollerimetodi tai kontrollerin metodi.
Voisimme vastaavasti lisätä routes.rb:hen rivin
get 'kaikki_bisset', to: 'beers#index'
jolloin URLiin http://localhost:3000/kaikki_bisset tulevat GET-pyynnöt vievät kaikkien oluiden sivulle. Kokeile että tämä toimii.
Mielenkiintoinen yksityiskohta routes.rb-tiedostossa on se, että vaikka tiedosto näyttää tekstimuotoiselta konfiguraatiotiedostolta, on koko tiedoston sisältö Rubya. Tiedoston rivit ovat metodikutsuja. Esim. rivi
get 'kaikki_bisset', to: 'beers#index'
kutsuu get-metodia parametreinaan merkkijono '/kaikki_bisset' ja hash to: 'beers#index'
. Hashin yhteydessä on käytetty uudempaa syntaksia, eli vanhaa syntaksia käyttäen reitityksen kohteen määrittelevä hash kirjoitettaisiin :to => 'beers#index'
, ja routes.rb:n rivi olisi:
get 'kaikki_bisset', :to => 'beers#index'
voisimme käyttää metodikutsussa myös sulkuja, ja määritellä hashin käyttäen aaltosulkuja, eli kömpelöimmässä muodossa reitti voitaisiin määritellä seuraavasti:
get( 'kaikki_bisset', { :to => 'beers#index' } )
Rubyn joustava syntaksi (yhdessä kielen muutamien muiden piirteiden kanssa) mahdollistaakin luonnollisen kielen sujuvuutta tavoittelevan ilmaisutavan sovelluksen konfigurointiin ja ohjelmointiin. Tyyli tunnetaan englanninkielisellä termillä Internal DSL ks. http://martinfowler.com/bliki/InternalDslStyle.html
Lisätään seuraavaksi ohjelmaan mahdollisuus antaa oluille "reittauksia" eli pisteytyksiä skaalalla 0-50. Emme käytä viime viikolta tuttua generaattoria (rails generate scaffold...
) vaan teemme kaiken itse.
Haluamme että kaikki reittaukset ovat osoitteessa http://localhost:3000/ratings. Kokeillaan nyt selaimella mitä tapahtuu kun urliin yritetään mennä.
Seurauksena on virheilmoitus No route matches [GET] "/ratings"
eli osoitteeseen tehtyä HTTP GET -pyyntöä ei vastannut mikään määritelty "reitti".
Lisätään reitti kirjoittamalla routes-tiedostoon seuraava:
get 'ratings', to: 'ratings#index'
Määrittelemme siis Rails-konventiota mukaillen, että kaikkien reittausten sivun 'ratings' hoitaa RatingsController-luokan metodi index.
Huom: suunnilleen samaa tarkoittaisi myös match 'ratings' => 'ratings#index'
. Kuten niin tyypillistä Railsille, voi routes.rb:ssäkin käyttää saman asian määrittelemiseen monia erilaisia tapoja.
Kokeile nyt sivua uudelleen selaimella.
Virheilmoitus muuttuu muotoon uninitialized constant RatingsController
eli määritelty reitti yrittää ohjata ratings-osoitteeseen tulevan GET-kutsun RatingsController
-luokassa määritellyn kontrollerin metodin index
-käsiteltäväksi.
Määritellään kontrolleri tiedostoon /app/controllers/ratings_controller.rb.
class RatingsController < ApplicationController
def index
end
end
Huomioi nimeämiskäytännöt ja tiedoston sijainti, Rails etsii kontrolleria nimenomaan hakemistosta /app/controllers. Jos sijoitat kontrollerin muualle, ei Rails löydä sitä.
Kokeile nyt sivua selaimella vielä kerran.
Seurauksena on uusi virheilmoitus
Missing template ratings/index, application/index with {:locale=>[:en], :formats=>[:html], :handlers=>[:erb, :builder, :raw, :ruby, :jbuilder, :coffee]}. Searched in: * "/Users/mluukkai/kurssirepot/wadror/ratebeer/app/views"
joka taas johtuu siitä, että Rails yrittää renderöidä kontrollerin metodia vastaavan oletusarvoisen, hakemistossa /app/views/ratings/index.html.erb olevan näkymätemplaten, mutta sellaista ei löydy.
Luodaan tiedosto /app/views/ratings/index.html.erb jolla on seuraava sisältö (joudut myös luomaan hakemiston /app/views/ratings):
<h2>List of ratings</h2>
<p>To be completed...</p>
ja nyt sivu toimii!
Huomaa taas Railsin konventiot, tiedoston sijainti on tarkasti määritelty, eli koska kyseessä on näkymätemplate jota kutsutaan ratings-kontrollerista (joka siis on täydelliseltä nimeltään RatingsController), sijoitetaan se hakemistoon /views/ratings.
Muistutuksena vielä viime viikosta: kontrollerimetodi index
renderöi oletusarvoisesti suorituksensa lopuksi (oikeassa hakemistossa olevan) index-nimisen näkymän. Eli koodi
class RatingsController < ApplicationController
def index
end
end
tekee oikeastaan siis saman asian kuin seuraava:
class RatingsController < ApplicationController
def index
render :index # renderöin näkymätemplate /app/views/ratings/index.html
end
end
Eksplisiittinen render-metodin kutsu jätetään kuitenkin yleensä pois jos renderöidään oletusarvoinen, eli kontrollerimetodin kanssa samanniminen template.
Yhteen olueeseen liittyy useita reittauksia, eli oliomalli pitää päivittää seuraavanlaiseksi:
Tarvitsemme siis tietokantataulun ja vastaavan model-olion.
Railsissa muutokset tietokantaan, esim. uuden taulun lisääminen, kannattaa tehdä aina migraatioiden avulla. Migraatiot ovat siis hakemistoon db/migrate sijoitettavia tiedostoja, joihin kirjoitetaan Rubyllä tietokantaa muokkaavat operaatiot. Tutustumme migraatioihin tarkemmin vasta myöhemmin ja käytämme modelin luomiseen nyt Railsin valmista model-generaattoria, joka luo model-olion lisäksi automaattisesti tarvittavan migraation.
Reittauksella on kokonaislukuarvoinen score
sekä vierasavain, joka linkittää sen reitattuun olueeseen. Railsin konvention mukaan vierasavaimen nimen tulee olla beer_id
.
Model ja tietokannan generoiva migraatio saadaan luotua antamalla komentoriviltä komento:
rails g model Rating score:integer beer_id:integer
ja luodaan tietokantataulu suorittamalla komentoriviltä migraatio
rails db:migrate
Toisin kuin viime viikolla käyttämämme scaffold-generaattori, model-generaattori ei luo ollenkaan kontrolleria eikä näkymätemplateja.
Muistutuksena viime viikolta: railsin generaattorien (scaffold, model, ...) luomat tiedostot on mahdollista poistaa komennolla destroy:
rails destroy model Rating
Jos olet suorittanut jo migraation ja huomaat että generaattorin luoma koodi onkin tuohottava, on erittäin tärkeää ensin perua migraatio komennolla
rails db:rollback
Jotta yhteydet saadaan myös oliotasolle (muistutuksena viime viikon materiaali), tulee luokkia päivittää seuraavasti
class Beer < ApplicationRecord
belongs_to :brewery
has_many :ratings
end
class Rating < ApplicationRecord
belongs_to :beer
end
Eli jokaiseen olueeseen liittyy useita reittauksia ja reittaus kuuluu aina täsmälleen yhteen olueeseen.
Käynnistetään Rails-konsoli antamalla komentoriviltä komento rails c
. Huomaa, että jos konsolisi oli jo auki, saat lisätyn koodin konsolin käyttöön komennolla reload!
. Luodaan muutama reittaus:
> b = Beer.first
> b.ratings.create score: 10
> b.ratings.create score: 21
> b.ratings.create score: 17
Reittaukset siis lisätään ensimmäisenä kannasta löytyvälle oluelle. Huomaa luontitapa, saman asian olisi ajanut monimutkaisempi tapa
b.ratings << Rating.create(score:15)
Yritetään luoda olut ilman panimoa:
> b = Beer.create name: 'anonymous', style: 'watery'
(0.1ms) begin transaction
(0.1ms) rollback transaction
=> #<Beer:0x00007fb9d5b7f958 id: nil, name: "anonymous", style: "watery", brewery_id: nil, created_at: nil, updated_at: nil>
[3] pry(main)>
id ja aikaleimakentät eivät saa arvoja ollenkaan, näyttääkin siltä että olut ei talletu ollenkaan tietokantaan.
Jos kutsumme oluen metodia errors, kertoo olut syyn tallettumisen epäonnistumiselle
> b.errors
=> #<ActiveModel::Errors:0x00007fb9d54f8b98
@base=
#<Beer:0x00007fb9d5b7f958 id: nil, name: "anonymous", style: "watery", brewery_id: nil, created_at: nil, updated_at: nil>,
@details={:brewery=>[{:error=>:blank}]},
@messages={:brewery=>["must exist"]}>
eli olut ei suostu tallettumaan kantaan ilman tietoa panimosta. Voimme korjata tilanteen antamalla arvon panimolle ja kutsumalla oluelle metodia save:
> b.brewery = Brewery.find_by(name: 'Koff')
> b.save
(0.1ms) begin transaction
Beer Create (1.9ms) INSERT INTO "beers" ("name", "style", "brewery_id", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["name", "anonymous"], ["style", "watery"], ["brewery_id", 1], ["created_at", "2018-09-11 18:21:40.830949"], ["updated_at", "2018-09-11 18:21:40.830949"]]
(0.8ms) commit transaction
Syynä talletuksen epäonnistumiselle on se, että Rails vaatii oletusarvoisesti, että tilanteissa, joissa olio viittaa vierasavaimen avulla toiseen olioon ja koodissa käytetään belongs_to määrettä liitoksen tekemiseen, kuten oluiden tapauksessa tehdään
class Beer < ApplicationRecord
belongs_to :brewery
# ...
end
vierasavaimen arvo ei saa olla alustamaton kun olio talletetaan.
Konsolin käyttörutiini on Rails-kehittäjälle äärimmäisen tärkeää. Tee seuraavat asiat konsolista käsin:
luo uusi panimo "BrewDog", perustamisvuosi 2007
lisää panimolle kaksi olutta
- Punk IPA (tyyli IPA)
- Nanny State (tyyli lowalcohol)
lisää molemmille oluille muutama reittaus
Kertaa tarvittaessa edellisen viikon materiaalista konsolia käsittelevät osuudet.
Palauta tämä tehtävä lisäämällä sovelluksellesi hakemisto exercises ja sinne tiedosto exercise1, joka sisältää copypasten konsolisessiosta
Nyt tietokannassamme on reittauksia, ja haluamme saada ne listattua kaikkien reittausten sivulle.
Listataan kaikki reittaukset ratings-sivulla. Ota mallia esim. panimokontrollerin
index
-metodista ja sitä vastaavasta templatesta. Tee reittauksen lista ensin esim. seuraavaan tyyliin<ul> <% @ratings.each do |rating| %> <li> <%= rating %> </li> <% end %> </ul>Lisää sivulle myös tieto reittausten yhteenlasketusta lukumäärästä
Tässä vaiheessa sivun pitäisi näyttää suunnilleen seuraavalta
Reittaus renderöityy hiukan ikävässä muodossa. Tämä johtuu siitä, että li-elementin sisällä on pelkkä olion nimi, ja koska emme ole määritelleet Ratingille olion merkkijonomuotoa määrittelevää to_s
-metodia, käytössä on kaikkien luokkien yliluokalta Objectilta peritty oletusarvoinen to_s
.
Määrittelemme hetken kuluttua reittauksille metodin to_s
, tutkitaan ensin kuitenkin muutamaa asiaa liittyen olion metodien määrittelyyn.
Tutkitaan hetki luokkaa Brewery
:
class Brewery < ApplicationRecord
has_many :beers
end
Panimoilla on nimi name
ja perustamisvuosi year
. Konsolista käsin pääsemme näihin käsiksi tuttuun tyyliin:
> b = Brewery.first
> b.name
=> "Koff"
> b.year
=> 1897
>
Teknisesti ottaen esim. b.year
on metodikutsu. Rails luo model-olioon jokaiselle vastaavan tietokantataulun skeeman määrittelemälle sarakkeelle kentän eli attribuutin ja metodit attribuutin arvon lukemista ja arvon muuttamista varten. Nämä automaattisesti generoidut metodit ovat sisällöltään suunnilleen seuraavat:
class Brewery < ApplicationRecord
# ..
def year
read_attribute(:year)
end
def year=(value)
write_attribute(:year, value)
end
end
Metodit siis mahdollistavat olion attribuutin arvon lukemisen ja muuttamisen. Arvoa muuttava metodi ei kuitenkaan vielä tee muutosta tietokantaan, muutos tapahtuu vasta kutsuttaessa metodia save
, kyseessä ovatkin siis automaattisesti generoituvat 'getterit ja setterit'.
Olion ulkopuolelta olion attribuutteihin päästään käsiksi 'pistenotaatiolla':
b.year
entä olion sisältä? Tehdään panimolle metodi, joka demonstroi panimon attribuuttien käsittelyä panimon sisältä:
class Brewery < ApplicationRecord
has_many :beers
def print_report
puts name
puts "established at year #{year}"
puts "number of beers #{beers.count}"
end
end
eli olion sisältä metodeja (myös beers
on metodi!) voidaan kutsua kuten esim. Javassa, metodin nimellä.
Ja esimerkki metodin käytöstä:
> b = Brewery.first
> b.print_report
Koff
established at year 1897
number of beers 2
Metodeja olisi voitu kutsua olion sisältä myös käyttäen Rubyn 'thissiä' eli olion self
-viitettä:
def print_report
puts self.name
puts "established at year #{self.year}"
puts "number of beers #{self.beers.count}"
end
Tehdään sitten panimolle metodi, jonka avulla panimon voi 'uudelleenkäynnistää', tällöin panimon perustamisvuosi muuttuu vuodeksi 2018:
def restart
year = 2018
puts "changed year to #{year}"
end
kokeillaan
> b = Brewery.first
> b.year
=> 1897
> b.restart
changed year to 2018
> b.year
=> 1897
>
eli huomaamme, että vuoden muuttaminen ei toimikaan odotetulla tavalla! Syynä tähän on se, että year = 2018
metodin restart
sisällä ei kutsukaan metodia
def year=(value)
joka sijoittaisi attribuutille uuden arvon, vaan luo metodille paikallisen muuttujan nimeltään year
johon arvo 2018 sijoitetaan.
Jotta sijoitus onnistuu, on metodia kutsuttava self
-viitteen kautta:
def restart
self.year = 2018
puts "changed year to #{year}"
end
ja nyt toiminnallisuus on odotetun kaltainen:
> b = Brewery.first
> b.year
=> 1897
> b.restart
changed year to 2018
> b.year
=> 2018
>
HUOM: Rubyssä olioiden instanssimuuttujat määritellään @
-alkuisina. Instanssimuuttujat eivät kuitenkaan ole sama asia kuin ActiveRecordin avulla tietokantaan talletettavat olioiden attribuutit. Eli seuraavakaan metodi ei toimisi odotetulla tavalla:
def restart
@year = 2018
puts "changed year to #{@year}"
end
Panimon sisällä year
siis on ActiveRecordin tietokantaan tallentama attribuutti, kun taas @year
on olion instanssimuuttuja. Railsin modeleissa instanssimuuttujia ei juurikaan käytetä. Instanssimuuttujia käytetään Railsissa lähinnä tiedonvälitykseen kontrollereilta näkymille.
Tee sitten luokalle Rating metodi
to_s
, joka palauttaa oliosta paremman merkkijonoesityksen, esim. muodossa "karhu 35", eli ensin reitatun oluen nimi ja sen jälkeen reittauksen pistemäärä.Merkkijonon muodostamisessa myös seuraavasta voi olla apua https://github.com/mluukkai/WebPalvelinohjelmointi2018/blob/master/web/rubyn_perusteita.md#merkkijonot
Tehtävän jälkeen reittausten sivujen tulisi näyttää suunnilleen seuraavalta:
Huom: kun kirjoitat sovelluksellesi uutta koodia, useimmiten on järkevämpää tehdä kokeiluja konsolista käsin. Seuraavassa kokeillaan reittauksen oletusarvoista to_s
-metodin palauttamaa arvoa:
> r = Rating.last
> r.to_s
=> "#<Rating:0x007f8054b1cb10>"
>
Määritellään reittaukselle to_s
-metodi:
class Rating < ApplicationRecord
belongs_to :beer
def to_s
"tekstiesitys"
end
end
ja kokeillaan uudelleen konsolista:
> r.to_s
=> "#<Rating:0x007f8054b1cb10>"
Muutos ei kuitenkaan vaikuta tulleen voimaan, missä vika?
Jotta muutettu koodi tulisi voimaan, on uusi koodi ladattava konsolin käyttöön komennolla reload!
ja käytettävä uudestaan kannasta haettua olioa:
> reload!
Reloading...
=> true
> r.to_s
=> "#<Rating:0x007f8054b1cb10>"
> r = Rating.last
> r.to_s
=> "tekstiesitys"
>
Eli kuten yllä näemme, ei pelkkä koodin uudelleenlataaminen vielä riitä, sillä muuttujassa r
olevassa oliossa on käytössä edelleen vanha koodi.
Lisää luokalle
Beer
metodiaverage_rating
, joka laskee oluen ratingien keskiarvon. Lisää keskiarvo yksittäisen oluen sivulle jos oluella on ratingejaNäkymätemplatessa voi tehdä tuotettavasta sisällöstä ehdollisen seuraavasti
<% if @beer.ratings.empty? %> beer has not yet been rated! <% else %> beer has some ratings <% end %>
Tehtävän jälkeen oluen sivun tulisi näyttää suunnilleen seuraavalta (huom: edellisen viikon jäljiltä sivullasi saattaa näkyä panimon nimen sijaan panimon id. Jos näin on, muuta näkymäsi vastaamaan kuvaa):
Moduuli enumerable (ks. https://ruby-doc.org/core-2.5.1/Enumerable.html) sisältää runsaasti oliokokoelmien läpikäyntiin tarkoitettuja apumetodeja.
Oliokokoelmamaiset luokat voivat sisällyttää moduulin enumerable toiminnallisuuden itselleen, ja tällöin ne perivät moduulin tarjoaman toiminnallisuuden.
Tutustu nyt
map
- jareduce
-metodeihin (ks. esim. reduce map ja etsi googlella lisää ohjeita) ja muuta (tarvittaessa) oluen reittausten keskiarvon laskeva metodi käyttämään reducea tai mapia ja sumia.Keskiarvon laskeminen onnistuu tässä tapauksessa myös helpommin hyödyntämällä ActiveRecordin metodeja, ks. http://api.rubyonrails.org/classes/ActiveRecord/Calculations.html
Lisätään konsolista jollekin vielä reittaamattomalle oluelle yksi reittaus. Oluen sivu näyttää nyt seuraavalta:
Sivulla on pieni, mutta ikävä kielioppivirhe:
beer has 1 ratings
Tutustu Railsissa valmiina olevaan
pluralize
-apumetodiin http://apidock.com/rails/ActionView/Helpers/TextHelper/pluralize ja tee oluen sivusta metodin avulla kieliopillisesti oikeaoppinen (eli yhden reittauksen tapauksessa tulee tulostua 'beer has 1 rating')
Tehdään nyt sovellukseen mahdollisuus reittausten luomiseen www-sivulta käsin.
Railsin konventioiden mukaan Rating-olion luontiin tarkoitetun lomakkeen tulee löytyä osoitteesta ratings/new, ja lomakkeeseen pääsyn hoitaa ratings-kontrollerin metodi new
.
Luodaan vastaava reitti routes.rb:hen
get 'ratings/new', to:'ratings#new'
Lisäämme siis ratings-kontrolleriin (joka siis täydelliseltä nimeltään on RatingsController) metodin new
, joka huolehtii lomakkeen renderöinnistä. Metodi on yksinkertainen:
def new
@rating = Rating.new
end
Metodi ainoastaan luo uuden Rating-olion ja välittää sen @rating
-muuttujan avulla oletusarvoisesti renderöitävälle näkymätemplatelle new.html.erb. Olio luodaan new
-komennolla eli sitä ei talleteta tietokantaan.
Luodaan nyt seuraava näkymä eli tiedosto /app/views/ratings/new.html.erb:
<h2>Create new rating</h2>
<%= form_for(@rating) do |f| %>
beer id: <%= f.number_field :beer_id %>
score: <%= f.number_field :score %>
<%= f.submit %>
<% end %>
Mene nyt lomakkeen sisältävälle sivulle eli osoitteeseen http://localhost:3000/ratings/new
Näkymän avulla muodostuva HTML-koodi näyttää (suunnilleen) seuraavalta (näet koodin menemällä sivulle ja valitsemalla selaimesta view page source):
<form action="/ratings" method="post">
beer id: <input name="rating[beer_id]" type="number" />
score: <input name="rating[score]" type="number" />
<input name="commit" type="submit" value="Create Rating" />
</form>
eli generoituu normaali HTML-lomake (ks. tarkemmin http://www.w3.org/community/webed/wiki/HTML/Training#Forms).
Lomakkeen lähetystapahtuman kohdeosoite on /ratings ja käytettävä HTTP-metodi GET:in sijasta POST. Lomakkeessa on kaksi numeromuotoista kenttää ja niiden arvot lähetetään vastaanottajalle POST-kutsun mukana "muuttujien" rating[beer_id]
ja rating[score]
arvoina.
Railsin metodi form_for
siis muodostaa automaattisesti oikeaan osoitteeseen lähetettävän, oikeanlaisen lomakkeen, jossa on syöttökentät kaikille parametrina olevan tyyppisen olion attribuuteille.
Lisää lomakkeiden muodostamisesta form_for
-metodilla osoitteessa
http://guides.rubyonrails.org/form_helpers.html#dealing-with-model-objects
Jos yritämme luoda reittauksen aiheutuu virheilmoitus No route matches [POST] "/ratings"
eli joudumme luomaan tiedostoon config/routes.rb reitin:
post 'ratings', to: 'ratings#create'
Uuden olion luonnista vastaava metodi on Railsin konvention mukaan nimeltään create
, luodaan sen pohja:
def create
raise
end
Tässä vaiheessa metodi ei tee muuta kuin aiheuttaa poikkeuksen (metodikutsu raise
).
Kokeillaan nyt lähettää lomakkeella tietoa. Kontrollerin metodissa heittämä poikkeus aiheuttaa virheilmoituksen. Rails lisää virhesivulle erilaista diagnostiikkaa, mm. HTTP-pyynnön parametrit sisältävän hashin, joka näyttää seuraavalta:
{"utf8"=>"✓",
"authenticity_token"=>"1OfMRb9BTZzTnM5PfpFUupImkdIbLbwWi0FB90XBSqs=",
"rating"=>{"beer_id"=>"1", "score"=>"2"},
"commit"=>"Create Rating"}
Hashin sisällä on siis välittynyt lomakkeen avulla lähetetty tieto.
Parametrit sisältävä hash on kontrollerin sisällä talletettu muuttujaan params
.
Uuden ratingin tiedot ovat hashissa avaimen :rating
arvona, eli pääsemme niihin käsiksi komennolla params[:rating]
joka taas on hash jonka arvo on {"beer_id"=>"1", "score"=>"2"}
. Eli esim. pistemäärään päästäisiin käsiksi komennolla params[:rating][:score]
.
Tutkitaan hieman asiaa kontrollerista käsin Railsin debuggeria hyödyntäen
Rails on jo konfiguroinut sovelluksesi käyttöön byebug-debuggerin (ja railsin web-konsolin, jota tarkastelemme hieman myöhemmin).
Lisätään kontrollerin alkuun, eli sille kohtaan koodia jota haluamme tarkkailla, komento byebug
def create
byebug
end
Kun luot lomakkeella uuden reittauksen, sovellus pysähtyy komennon byebug
kohdalle. Terminaaliin josta Rails on käynnistetty, avautuu nyt interaktiivinen konsolinäkymä:
Started POST "/ratings" for 127.0.0.1 at 2018-09-07 18:45:05 +0300
Processing by RatingsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"ILMJj9dNNuAV6ivJaqN2LMyazdVCg3CYscTzHiU9n3MfP07ry27rZq6r/Lq+m/9d/r2cr47r95XWO1ZN/8/ZUw==", "rating"=>{"beer_id"=>"1", "score"=>"20"}, "commit"=>"Create Rating"}
Return value is: nil
[4, 13] in /Users/mluukkai/opetus/ratebeer/app/controllers/ratings_controller.rb
4: end
5:
6: def new
7: @rating = Rating.new
8: end
9:
10: def create
11: byebug
=> 12: end
13: end
(byebug)
Nuoli kertoo seuraavana vuorossa olevan komennon. Tutkitaan nyt params
-muuttujan sisältöä:
(byebug) params
<ActionController::Parameters {"utf8"=>"✓", "authenticity_token"=>"ILMJj9dNNuAV6ivJaqN2LMyazdVCg3CYscTzHiU9n3MfP07ry27rZq6r/Lq+m/9d/r2cr47r95XWO1ZN/8/ZUw==", "rating"=>{"beer_id"=>"1", "score"=>"20"}, "commit"=>"Create Rating", "controller"=>"ratings", "action"=>"create"} permitted: false>
(byebug) params[:rating][:beer_id]
"1"
(byebug) params[:rating][:score]
"20"
Debuggerin konsolissa voi tarpeen vaatiessa suorittaa mitä tahansa koodia Rails-konsolin tavoin.
Debuggerin tärkeimmät komennot lienevät step, next, continue ja help. Step suorittaa koodista seuraavan askeleen, edeten mahdollisiin metodikutsuihin. Next suorittaa seuraavan rivin kokonaisuudessaan. Continue jatkaa ohjelman suorittamista normaaliin tapaan.
Lisätietoa byebugista seuraavassa http://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-byebug-gem
Jos lisäät koodiin komennon binding.pry
voit käyttää Pry:tä debuggerina:
def create
binding.pry
end
kun koodirivi suoritetaan, suoritus pysähtyy ja Pry-sessio aukeaa koodirivin kohdalle. Voit jatkaa suoritusta komennolla exit
. On makuasia kumpaa käytät debuggaukseen byebugia vai Prytä.
Kontrollerin sisällä params[:rating]
siis sisältää kaiken tiedon, joka uuden reittauksen luomiseen tarvitaan. Ja koska kyseessä on hash, joka on muotoa {"beer_id"=>"1", "score"=>"30"}
, voi sen antaa suoraan metodin create
parametriksi, eli reittauksen luonnin pitäisi periaatteessa onnistua komennolla:
Rating.create params[:rating] # joka siis tarkoittaa samaa kuin Rating.create beer_id:"1", score:"30"
Muuta siis kontrollerisi koodi seuraavanlaiseksi:
def create
Rating.create params[:rating]
end
Kokeile nyt luoda reittaus. Vastoin kaikkia odotuksia, luomisoperaatio epäonnistuu ja seurauksena on virheilmoitus
ActiveModel::ForbiddenAttributesError
Mistä on kyse?
Jos olisimme tehneet reittauksen luovan komennon muodossa
Rating.create beer_id: params[:rating][:beer_id], score: params[:rating][:score]
joka siis periaatteessa tarkoittaa täysin samaa kuin ylläoleva muoto (sillä params[:rating]
on sisällöltään täysin sama hash kuin beer_id:params[:rating][:beer_id], score:params[:rating][:score]
), ei virheilmoitusta olisi tullut. Tietoturvasyistä Rails ei kuitenkaan salli mielivaltaista params
-muuttujasta tapahtuvaa "massasijoitusta" (engl. mass assignment eli kaikkien parametrien antamista hashina) olion luomisen yhteydessä.
Rails 4:stä lähtien kontrollerin on lueteltava eksplisiittisesti mitä hashin params
sisällöstä voidaan massasijoittaa olioiden luonnin yhteydessä. Tähän kontrolleri käyttää params
:in metodeja require
ja permit
.
Periaatteena on, että ensin requirella otetaan paramsin sisältä luotavan olion tiedot sisältävä hash:
params.require(:rating)
tämän jälkeen luetellaan permitillä ne kentät, joiden arvon massasijoitus sallitaan:
params.require(:rating).permit(:score, :beer_id)
Kontrollerimme on siis seuraava:
def create
Rating.create params.require(:rating).permit(:score, :beer_id)
end
Lisää tietoa lomakkeiden parametrien käsittelystä seuraavassa https://edgeguides.rubyonrails.org/action_controller_overview.html#strong-parameters
Kokeile nyt reittauksen luomista. HUOM: kun luot lomakkeella reittausta, tarkista, että lomakkeelle syöttämä oluen id vastaa jonkun tietokannassa olevan oluen id:tä!
Reittausten luominen onnistuu jo, voit tarkastaa tilanne konsolista tai kaikkien reittausten sivulta. Ainakin chromella reittauksen luominen sellaisen tilanteen että selain näyttää pysyvän samalla sivulla, mutta sivu "jäätyy". Syy tälle paljastuu sovelluksen konsoliin kirjoittamasta lokiviestistä:
app/controllers/ratings_controller.rb:11
No template found for RatingsController#create, rendering head :no_content
Completed 204 No Content in 137ms (ActiveRecord: 1.7ms)
eli koska sovellukseen ei ole määritelty näkymätemplatea create-operaatiolle, lähettää selain tyhjän vastauksen, eli vastauksen, mikä ei sisällä ollenkaan HTML-koodia. Chrome näyttää kuitenkin jättävän edellisen sivun näkyviin saadessaan tyhjän vastauksen.
Voisimme luoda näkymätemplaten create:lle, mutta päätämmekin, että uuden reittauksen luomisen jälkeen käyttäjän selain uudelleenohjataan kaikki reittaukset sisältävälle sivulle, eli muutetaan kontrollerin koodi muodoon:
def create
Rating.create params.require(:rating).permit(:score, :beer_id)
redirect_to ratings_path
end
ratings_path
on Railsin tarjoama polkuapumetodi, joka tarkoittaa samaa kuin "/ratings"
Jos olet luonut reittauksia joihin liittyvä beer_id
ei vastaa olemassa olevan oluen id:tä, saat nyt todennäköisesti virheilmoituksen. Voit tuhota railsin konsolista (käsin nämä ratingit seuraavasti
Rating.last # näyttää viimeksi luodun ratingin, tarkasta onko siinä oleva beer_id virheellinen
Rating.last.delete # poistaa viimeksi luodun ratingin
Saat tuhottua oluettomat ratingit myös seuraavalla "onelinerilla":
Rating.all.select{ |r| r.beer.nil? }.each{ |r| r.delete }
Select luo taulukon, johon sisältyy ne läpikäydyn kokoelman alkiot, joille koodilohkossa oleva ehto on tosi. r.beer.nil?
palauttaa true
jos olio r.beer
on nil
.
Edellisen komennon voi kirjottaa myös hieman lyhemmässä muodossa
Rating.all.select{ |r| r.beer.nil? }.each(&:delete)
Mitä kontrollerissa käytetty komento redirect_to ratings_path
oikeastaan tekee? Normaalistihan kontrolleri renderöi sopivan näkymätemplaten ja näin aikaansaatu HTML-koodi palautetaan selaimelle, joka renderöi sivun näytölle.
Uudelleenohjauksessa palvelin lähettää selaimelle statuskoodilla 302 varustetun vastauksen, joka ei sisällä ollenkaan HTML:ää. Vastaus sisältää ainoastaan osoitteen, mihin selaimen tulee automaattisesti tehdä uusi HTTP GET -pyyntö. Uudelleenohjautuminen on huomaamatonta selaimen käyttäjän kannalta.
Kokeile mitä tapahtuu kun laitat uuden reittauksen luomisen jälkeiseksi uudelleenohjaukseksi esim. redirect_to "http://www.cs.helsinki.fi"
!
Olisi ollut teknisesti mahdollista olla käyttämättä uudelleenohjausta ja renderöidä kaikkien reittausten sivu suoraan uuden reittauksen luovasta kontrollerista:
def create
Rating.create params.require(:rating).permit(:score, :beer_id)
@ratings = Rating.all
render :index
end
Vaikka aikaansaannos näyttää sivuston käyttäjälle täsmälleen samalta, tämä ei ole kuitenkaan järkevää muutamastakaan syystä. Ensinnäkin kaikki metodissa index
oleva koodi, joka tarvitaan näkymän muodostamiseen on kopioitava create
-metodiin (nyt kopioitavaa koodia ei ole paljon, mutta tilanne ei ole aina yhtä yksinkertainen).
Toinen syy liittyy selaimen käyttäytymiseen. Jos kontrollerimme käyttäisi sivun renderöintiä ja selaimen käyttäjä refreshaisi sivun uuden oluen luomisen jälkeen, jotkut vanhat selaimet lähettäisivät lomakkeen tiedot uudelleen, sillä edellinen selaimen toiminto jonka refreshaus suorittaa on nimenomaan lomakkeen tietojen lähetyksen hoitanut HTTP POST. Redirectauksen yhteydessä vastaavaa ongelmaa ei ole, sillä POST-komennon jälkeen seuraava käyttäjälle näkyvä sivu saadaan aikaan redirectauksen aikaansaamalla HTTP GET:illä.
Nyrkkisääntönä (ei vaan Railsissa vaan Web-ohjelmoinnissa yleensäkin, ks. http://en.wikipedia.org/wiki/Post/Redirect/Get) onkin käyttää lomakkeista huolehtivien HTTP POST -metodien käsittelevässä kontrollerissa aina uudelleenohjausta (ellei kontrollerin suorittama operaatio epäonnistu esim. lomakkeella lähetetyn tiedon virheellisyyden vuoksi).
Nostetaan vielä esiin tämä tärkeä ero:
- kun kontrollerimetodi päättyy komentoon
render :jotain
(joka siis tapahtuu usein implisiittisesti) generoi Rails-sovellus HTML-sivun, jonka palvelin lähettää selaimelle renderöitäväksi - kun kontrollerimetodi päättyy komentoon
redirect_to osoite
lähettää palvelin selaimelle statuskoodissa 302 varustetun uudelleenohjauspyynnön, jossa se pyytää selainta tekemään automaattisesti HTTP GET -pyynnön kontrollerimetodin määrittelemään osoitteeseen, selaimen käyttäjän kannalta uudelleenohjaus on huomaamaton toimenpide
Jokaisen Web-ohjelmoijan on syytä ymmärtää edellinen!
Rails on sisältänyt versiosta 4.2 alkaen oletusarvoisesti debuggerin tapaan toimivan web-konsolin. Konsolinäkymä avautuu automaattisesti jos ohjelmassa syntyy poikkeus.
Poikkeuksen voi "aiheuttaa" esim. kirjoittamalla mihin tahansa kohtaan koodia raise
kuten teimme jo hieman aiemmin.
Palautetaan raise reittauskontrollerin metodiin create:
class RatingsController < ApplicationController
def create
raise
Rating.create params.require(:rating).permit(:score, :beer_id)
redirect_to ratings_path
end
end
Kun nyt luot reittauksen, renderöityy tuttu virhesivu. Virhesivun alalaidassa olevassa konsolinäkymässä voi nyt suorittaa ruby-komentoja täsmälleen samalla tavalla kuin debuggeria käytettäessä:
Aivan kuten debuggeria käytettäessä, web-konsolin näkymä avautuu siihen kontekstiin, jossa virhe tapahtuu, eli esim. muuttuja params
on viitattavissa, samoin voidaan suorittaa kaikkia komentoja, joita konrollerimetodista käsin voitaisiin suorittaa, esim. hakea reittauksia tietokannasta modelin Rating
avulla.
Rails luo automaattisesti kaikille tiedostoon routes.rb määritellyille reiteille ns. polkuapumetodit (engl. path helper), joita hyödyntämällä sovelluksessa ei ole tarvetta kovakoodata eri sivujen osoitteita.
Esim. uuden reittauksen jälkeisen uudelleenohjauksen osoite olisi voitu ratings_path
-apufunktion sijaan kovakoodata:
def create
Rating.create params.require(:rating).permit(:score, :beer_id)
redirect_to 'ratings'
end
Kuten yleensäkin, kovakoodaus ei ole järkevää osoitteidenkaan suhteen.
Tarjolla olevia automaattisesti generoituja polkuja pääsee tarkastelemaan komentoriviltä komennolla rails routes
mluukkai@melkki.~/ratebeer$ rails routes
Prefix Verb URI Pattern Controller#Action
beers GET /beers(.:format) beers#index
POST /beers(.:format) beers#create
new_beer GET /beers/new(.:format) beers#new
edit_beer GET /beers/:id/edit(.:format) beers#edit
beer GET /beers/:id(.:format) beers#show
PATCH /beers/:id(.:format) beers#update
PUT /beers/:id(.:format) beers#update
DELETE /beers/:id(.:format) beers#destroy
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
PATCH /breweries/:id(.:format) breweries#update
PUT /breweries/:id(.:format) breweries#update
DELETE /breweries/:id(.:format) breweries#destroy
root GET / breweries#index
ratings GET /ratings(.:format) ratings#index
ratings_new GET /ratings/new(.:format) ratings#new
POST /ratings(.:format) ratings#create
Esim alimmat 3 reittiä kertovat seuraavaa:
- metodikutsu
ratings_path
generoi linkin, joka vie osoitteeseen "ratings" ja ohjautuu ratings-kontrollerin metodilleindex
. - metodikutsu
ratings_new_path
generoi linkin, joka vie osoitteeseen "ratings/new" ja ohjautuu ratings-kontrollerin metodillenew
. Tämä taas renderöi reittauksentekoformin- huom. kuten ylempänä olevia reittejä vertailemalla huomaamme, ei
ratings_new_path
ole samanlainen kuin esim uusien oluiden luontipolku, asia korjataan myöhemmin
- huom. kuten ylempänä olevia reittejä vertailemalla huomaamme, ei
- POST-kutsu osoitteeseen "ratings" ohjataan ratings-kontrollerin metodille
create
Kuten olemme jo huomanneet komennon rails routes
informaatio tulee myös virhetilanteissa renderöityvälle web-sivulle. Sivu jopa tarjoaa interaktiivisen työkalun, jonka avulla voi kokeilla miten sovellus reitittää syötetyn esimerkkipolun:
Lisää kaikkien reittausten sivulle linkki uuden reittauksen tekemiseen. Lisää sovelluksen navigointipalkkiin linkki kaikkien reittausten listalle
Uuden reittauksen luominen on nyt hieman ikävää, sillä reittaajan pitää tietää oluen id. Muutetaan reittaamista siten, että käyttäjä voi valita reitattavan oluen listalta.
Jotta uuden reittauksen luontilomake pystyisi muodostamaan listan, on lomakkeen näyttämisestä huolehtivan kontrollerin haettava lista kannasta ja talletettava se muuttujaan, eli laajennetaan kontrolleria seuraavasti:
class RatingsController < ApplicationController
def new
@rating = Rating.new
@beers = Beer.all
end
# ...
end
Sivua http://guides.rubyonrails.org/form_helpers.html#making-select-boxes-with-ease konsultoimalla ja hieman kokeiluja tekemällä päädytään siihen että reittauksen luovaa lomaketta tulee muuttaa seuraavasti:
<%= form_for(@rating) do |f| %>
<%= f.select :beer_id, options_from_collection_for_select(@beers, :id, :name) %>
score: <%= f.number_field :score %>
<%= f.submit %>
<% end %>
eli lomakkeen beer_id
:n arvo generoidaan HTML lomakkeen select-elementillä, jonka valintavaihtoehdot muodostetaan näkymäapumetodilla options_from_collection_for_select
@beers
-muuttujassa olevasta oluiden listasta (ensimmäinen parametri @beers) siten, että arvoksi otetaan oluen id (toinen parametri :id) ja lomakkeen käyttäjälle näytetään oluen nimi (kolmas parametri :name).
Kolmas parametri siis määrittelee miten yksittäiset valinnat näytetään lomakkeella. Nyt siis näytetään kunkin oluen metodin name tulos. Rubyssä viittaukset metodeiden nimiin määritellään symboleina, eli kaksoispisteellä alkavina merkkijonoina.
Huom: näkymäapumetodeja on mahdollista testata myös konsolista. Metodeja voi kutsua helper
-olion kautta:
> b = Beer.all
> helper.options_from_collection_for_select(b, :id, :name)
=> "<option value=\"1\">Iso 3</option>\n<option value=\"2\">Karhu</option>\n<option value=\"3\">Tuplahumala</option>\n<option value=\"4\">Huvila Pale Ale</option>\n<option value=\"5\">X Porter</option>\n<option value=\"6\">Hefeweizen</option>\n<option value=\"7\">Helles</option>\n<option value=\"8\">Lite</option>\n<option value=\"9\">IVB</option>\n<option value=\"10\">Extra Light Triple Brewed</option>\n<option value=\"13\">Punk IPA</option>\n<option value=\"14\">Nanny State</option>"
>
Tee oluelle
to_s
-metodi, jonka muodostamassa tekstuaalisessa esityksessä on sekä oluen, että sen panimon nimiMuuta reittauksen luovaa lomaketta siten, että valittavista oluista näytetään nimikentän arvon sijaan olion
to_s
-metodin palauttama tekstuaalinen esitys
Tee vastaava muutos oluiden luomisesta huolehtivaan lomakkeeseen (tiedostossa views/beers/_form.html.erb) ja sen näyttämisestä vastaavaan kontrolleriin (beers#new), eli sen sijaan että luotavan oluen panimo määritellään antamalla id käsin, valitsee käyttäjä panimon listalta.
Muuta uuden oluen luomisen hoitavaa kontrolleria (beers#create) siten, että uuden oluen luomisen jälkeen selain uudelleenohjataan kaikkien oluiden listan sisältävälle sivulle (jonka osoite kannattaa generoida polkuapumetodilla). Oletusarvoisesti uudelleenohjaus tapahtuu luodun oluen sivulle komennolla
redirect_to @beer
, eli muutos tulee tähän.Scaffoldingin automaattisesti luoma lomake sisältää mm. virheiden raportointiin tarkoitettua koodia, johon tutustumme tarkemmin myöhemmin.
Tällä hetkellä luotavan oluen tyyli annetaan merkkijonona. Tulemme myöhemmin muokkaamaan sovellusta siten, että myös oluttyylit talletetaan tietokantaan.
Tehdään ensin välivaiheen ratkaisu, eli muuta sovellustasi siten, että luotavan oluen tyyli valitaan listalta, joka muodostetaan kontrollerin välittämän taulukon perusteella. Olutkontrollerin
new
-metodin koodi muuttuu siis seuraavasti:Kontrolleri
def new @beer = Beer.new @breweries = Brewery.all @styles = ["Weizen", "Lager", "Pale ale", "IPA", "Porter"] endNäkymän tulee siis generoida lomakkeeseen valintavaihtoehdot taulukon
@styles
perusteella. Vaihtoehtojen generointiin kannattaa nyt metodinoptions_from_collection_for_select
sijaan käyttää metodiaoptions_for_select
, ks. http://api.rubyonrails.org/classes/ActionView/Helpers/FormOptionsHelper.html#method-i-options_for_select
Näiden muutosten jälkeen oluen tietojen editointi ei yllättäen enää toimi. Syynä tälle on se, että uuden oluen luominen ja oluen tietojen editointi käyttävät molemmat samaa lomakkeen generoivaa näkymätemplatea (app/views/beers/_form.html.erb) ja muutosten jälkeen näkymän toiminta edellyttää, että muuttuja @breweries
sisältää panimoiden listan ja muuttuja @styles
sisältää oluiden tyylit. Oluen tietojen muutossivulle mennään kontrollerimetodin edit
suorituksen jälkeen, ja joudummekin muuttamaan kontrolleria seuraavasti korjataksemme virheen:
def edit
@breweries = Brewery.all
@styles = ["Weizen", "Lager", "Pale ale", "IPA", "Porter"]
end
Onkin hyvin tyypillistä, että kontrollerimetodit new
ja edit
sisältävät paljon samaa koodia. Olisikin ehkä järkevä ekstraktoida yhteinen koodi omaan metodiinsa.
REST (representational state transfer) on HTTP-protokollaan perustuva arkkitehtuurimalli erityisesti web-pohjaisten sovellusten toteuttamiseen. Taustaidea on periaatteessa yksinkertainen: osoitteilla määritellään haettavat ja muokattavat resurssit, pyyntömetodit kuvaavat resurssiin kohdistuvaa operaatiota, ja pyynnön rungossa on tarvittaessa resurssiin liittyvää dataa.
Lue nyt http://guides.rubyonrails.org/routing.html kohtaan 2.5 asti. Rails siis tekee helpoksi REST-tyylisen rakenteen noudattamisen. Jos kiinnostaa, RESTistä voi lukea lisää esim. täältä
Muutetaan reittauksen polut tiedostoon routes.rb siten, että käytetään valmista resources
-määrittelyä:
# kommentoi tai poista entiset määrittelyt
#get 'ratings', to: 'ratings#index'
#get 'ratings/new', to: 'ratings#new'
#post 'ratings', to: 'ratings#create'
resources :ratings, only: [:index, :new, :create]
Koska emme tarvitse reittejä delete, edit ja update, käytämme :only
-tarkennetta, jolla valitsemme vain tarvitsemamme reitit. Katsotaan nyt komentoriviltä rails routes
-komennolla (tai virheellisen urlin omaavalta web-sivulta) sovellukseen määriteltyjä polkuja:
ratings GET /ratings(.:format) ratings#index
POST /ratings(.:format) ratings#create
new_rating GET /ratings/new(.:format) ratings#new
Tulos on muuten sama kuin edellä, mutta apumetodin ratings_new_path
nimi on nyt Railsin konvention mukainen new_rating_path
.
Korvaa vielä templatessa app/views/ratings/index.erb.html käytetty vanha polkuapumetodikutsu uudella.
Lisätään ohjelmaan vielä mahdollisuus poistaa reittauksia. Lisätään ensin vastaava reitti muokkaamalla routes.rb:tä:
resources :ratings, only: [:index, :new, :create, :destroy]
Lisätään sitten reittauksien listalle linkki, jonka avulla kunkin reittauksen voi poistaa, eli muutetaan reittauksin listaa seuraavasti
<ul>
<% @ratings.each do |rating| %>
<li> <%= rating %> <%= link_to 'delete', rating_path(rating.id), method: :delete %> </li>
<% end %>
</ul>
Railsin noudattaman REST-konvention mukaan olion tuhoaminen tehdään HTTP:n DELETE-metodilla. Esim. jos tuhottavana on rating, jonka id on 5, tapahtuu nyt linkkiä klikkaamalla HTTP DELETE -kutsu osoitteeseen ratings/5.
Kuten jo aiemmin mainittiin, voi rating_path(rating.id)
-kutsun sijaan link_to
:n parametrina olla suoraan olio, jolle kutsu kohdistuu, eli edellinen hieman lyhemmässä muodossa:
<ul>
<% @ratings.each do |rating| %>
<li> <%= rating %> <%= link_to 'delete', rating, method: :delete %> </li>
<% end %>
</ul>
Jotta saamme poiston toimimaan, tulee vielä määritellä kontrollerille poiston suorittava metodi destroy
.
Metodiin johtava url on muotoa ratings/[tuhottavan olion id]. Metodi pääsee Railsin konvention mukaan käsiksi tuhottavan olion id:hen params
-olion kautta. Tuhoaminen tapahtuu hakemalla olio tietokannasta ja kutsumalla sen metodia delete
:
def destroy
rating = Rating.find(params[:id])
rating.delete
redirect_to ratings_path
end
Lopussa suoritetaan uudelleenohjaus takaisin kaikkien reittausten sivulle. Uudelleenohjaus siis aiheuttaa sen, että selain lähettää sovellukselle uudelleen GET-pyynnön osoitteeseen /ratings, ja ratings#index-metodi suoritetaan tämän takia uudelleen.
Reittauksen poisto on nyt siinä mielessä ikävä, että herkkäsorminen sivuston käyttäjä saattaa vahinkoklikkauksella tuhota reittauksia.
Katso esim. kaikki oluet listaavan sivun templatesta /app/views/beers/index.html.erb mallia ja tee ratingin tuhoamisesta sellainen, että käyttäjältä kysytään varmistus reittauksen tuhoamisen yhteydessä.
Jos sovelluksesta poistetaan olut, jolla on reittauksia, käy niin että poistettuun olueeseen liittyvät reittaukset jäävät tietokantaan, todennäköisesti tämä aiheuttaa virheen reittausten sivun renderöinnissä.
Poista jokin olut, jolla on reittauksia ja mene reittausten sivulle. Seurauksena on virheilmoitus
undefined method `name' for nil:NilClass
Virhe taas aiheutuu siitä, että reittaus-olion
to_s
-metodissa kutsutaanbeer.name
Poista orvoksi jääneet reittaukset konsolista käsin. Yritä keksiä ensin itse komento/komennot, joiden avulla saat muodostettua orpojen reittauksen listan. Jos et keksi vastausta, ylempänä tällä sivulla on tehtävään valmis vastaus.
Olueeseen liittyvät reittaukset saadaan helposti poistettua automaattisesti. Merkitään oluen modelin koodiin has_many :ratings
yhteyteen että reittaukset ovat oluesta riippuvaisia, ja että ne tuhotaan oluen tuhoutuessa:
class Beer < ApplicationRecord
belongs_to :brewery
has_many :ratings, dependent: :destroy
# ...
end
Nyt orpojen ongelma poistuu.
Tee vastaava muutos panimoihin, eli kun panimo poistetaan, tulee panimoon liittyvien oluiden poistua.
Tee panimo jolla on vähintään yksi olut jolla on reittauksia. Poista panimo ja varmista, että panimoon liittyvät oluet ja niihin liittyvät reittaukset poistuvat.
Sovelluksessamme panimoon liittyy oluita ja oluisiin liittyy reittauksia. Kuhunkin panimoon siis liittyy epäsuorasti joukko reittauksia. Rails tarjoaa helpon keinon päästä panimoista suoraan käsiksi reittauksiin:
class Brewery < ApplicationRecord
has_many :beers
has_many :ratings, through: :beers
end
eli yhteys määritellään kuten "tietokantatasolla" oleva yhteys, mutta yhteyteen lisätään tarkenne, että se muodostuu toisten oluiden kautta. Nyt panimoilla on reittaukset palauttava metodi ratings
Lisää yhteys koodiisi ja kokeile seuraavaa konsolista (muista ensin reload!
):
> k = Brewery.find_by name:"Koff"
> k.ratings.count
=> 5
Lisää yksittäisen panimon tiedot näyttävälle sivulle tieto panimon oluiden reittausten määrästä sekä keskiarvosta. Lisää tätä varten panimolle metodi
average_rating
reittausten keskiarvon laskemista varten.Tee reittausten yhteenlasketun määrän "kieliopillisesti moitteeton" tehtävän 6 tyyliin. Jos reittauksia ei ole, älä näytä keskiarvoa.
Panimon sivun tulisi näyttää muutoksen jälkeen suunnilleen seuraavalta:
Huomaamme, että oluella ja panimolla on täsmälleen samalla tavalla toimiva ja vieläpä saman niminen metodi average_rating
. Ei ole hyväksyttävää jättää koodia tähän tilaan.
Ruby tarjoaa keinon jakaa metodeja kahden luokan välillä moduulien avulla, ks. https://github.com/mluukkai/WebPalvelinohjelmointi2018/blob/master/web/rubyn_perusteita.md#moduuli
Moduleilla on useampia käyttötarkoituksia, niiden avulla voidaan mm. muodostaa nimiavaruuksia. Nyt olemme kuitenkin kiinnostuneita modulien avulla toteutettavasta mixin-perinnästä.
Tutustu nyt riittävällä tasolla moduleihin ja refaktoroi koodisi siten, että metodi
average_rating
siirretään moduuliin, jonka luokatBeer
jaBrewery
sisällyttävät.Koska nyt tehtävää moduulia käytetään ainoastaan modeleista on järkevintä määritellä se ns. concernina ja sijoittaa moduulin määrittelevä tiedosto hakemistoon app/models/concerns
module RatingAverage extend ActiveSupport::Concern # ... end
- HUOM: jos moduulisi nimi on ao. esimerkin tapaan
RatingAverage
tulee se Rubyn nimentäkonvention takia sijaita tiedostossaapp/models/concerns/rating_average.rb
, eli vaikka luokkien nimet ovat Rubyssä isolla alkavia CamelCase-nimiä, noudattavat niiden tiedostojen nimet snake_case.rb-tyyliä.
Tehtävän jälkeen esim. luokan Brewery tulisi siis näyttää suunnilleen seuraavalta (olettaen että tekemäsi moduulin nimi on RatingAverage):
class Brewery < ApplicationRecord
include RatingAverage
has_many :beers
has_many :ratings, through: :beers
end
ja metodin average_rating
tulisi edelleen toimia entiseen tyyliin:
> b = Beer.first
> b.average_rating
=> #<BigDecimal:7fa4bbde7aa8,'0.17E2',9(45)>
> b = Brewery.first
> b.average_rating
=> #<BigDecimal:7fa4bfbf7410,'0.16E2',9(45)>
>
Haluamme viikon lopuksi tehdä sovelluksesta sellaisen, että ainoastaan ylläpitäjä pystyy poistamaan panimoita. Toteutamme viikolla 3 kattavamman tavan autentikointiin, teemme nyt nopean ratkaisun http basic -autentikaatiota hyödyntäen. Ks. http://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic.html
Tutustumme samalla nopeasti Railsin kontrollerien filtterimetodeihin ks. http://guides.rubyonrails.org/action_controller_overview.html#filters, joiden avulla voidaan helposti määritellä toiminnallisuutta, mikä suoritetaan esim. ennen (before_action) tietyn kontrollerin joidenkin metodien suorittamista.
Määrittelemme ensin panimokontrolleriin (private
-näkyvyydellä varustetun) filtterimetodin nimeltään authenticate
, joka suoritetaan ennen jokaista panimokontrollerin metodia:
class BreweriesController < ApplicationController
before_action :set_brewery, only: [:show, :edit, :update, :destroy]
before_action :authenticate
# HUOM: älä kirjoita private-määrettä tiedostoon ennen kontrollerimetodeja (index, new, ...)
private
def authenticate
raise "toteuta autentikointi"
end
end
Filtterimetodi aiheuttaa poikkeuksen, joten mennessä minne tahansa panimoita käsitteleville sivuille aiheutuu poikkeus. Varmista tämä selaimella.
Rajoitetaan sitten filtterimetodin suoritus koskemaan ainoastaan panimon poistoa:
class BreweriesController < ApplicationController
before_action :set_brewery, only: [:show, :edit, :update, :destroy]
before_filter :authenticate, only: [:destroy]
# ...
private
def authenticate
raise "toteuta autentikointi"
end
end
Varmistetaan jälleen selaimella muut sivut toimivat, mutta panimon poisto aiheuttaa virheen.
Toteutetaan sitten http-basicauth-autentikointi (ks. tarvittaessa lisää esim. täältä)
Kovakoodataan käyttäjätunnukseksi "admin" ja salasanaksi "secret":
class BreweriesController < ApplicationController
before_action :set_brewery, only: [:show, :edit, :update, :destroy]
before_filter :authenticate, only: [:destroy]
# ...
private
def authenticate
authenticate_or_request_with_http_basic do |username, password|
if username == "admin" and password == "secret"
login_ok = true
else
login_ok = false # käyttäjätunnus/salasana oli väärä
end
# koodilohkon arvo on sen viimeisen komennon arvo eli true/false riippuen kirjautumisen onnistumisesta
login_ok
end
end
end
Ja sovellus toimii haluamallamme tavalla!
HUOM: kun olet kerran antanut oikean käyttäjätunnus-salasanaparin, ei selain kysy uusia tunnuksia mennessäsi sivulle uudelleen. Avaa uusi incognito-ikkuna jos haluat testata kirjautumista uudelleen!
Toimintaperiaatteena metodissa authenticate_or_request_with_http_basic
on se, että sovellus pyytää selainta lähettämään käyttäjätunnuksen ja salasanan, jotka sitten välitetään do
:n ja end
:in välissä olevalle koodilohkolle parametrien username
ja password
avulla. Jos koodilohkon arvo on tosi, näytetään sivu käyttäjälle.
Koska koodilohko saa saman arvon kuin if:n ehto, voidaan se yksinkertaistaa seuraavaan muotoon
def authenticate
authenticate_or_request_with_http_basic do |username, password|
username == "admin" and password == "secret"
end
end
HTTP Basic -autentikaatio on kätevä tapa yksinkertaisiin sivujen suojaamistarpeisiin, mutta monimutkaisemmissa tilanteissa ja parempaa tietoturvaa edellytettäessä kannattaa käyttää muita ratkaisuja.
Kannattaa huomata, että HTTP Basic -autentikaatiota ei tule käyttää kuin suojatun HTTPS-protokollan yli sillä käyttäjätunnus ja salasana lähetetään Base64-enkoodattuna, eli käytännössä kuka tahansa voi headereihin käsiksi päästyään selvittää salasanan. Hieman parempi vaihtoehto on Digest-autentikaatio, jossa käyttäjätunnuksen ja salasanan sijaan tunnistautuminen tapahtuu yksisuuntaisella funktiolla laskettavan tunnisteen avulla. Digest-autentikaation käyttäminen Railsissa on helppoa, ks. http://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Digest.html
Laajenna ratkaisua siten, että ohjelma hyväksyy myös muita kovakoodattuja käyttäjätunnus-salasana-pareja. Käytössä olevat tunnukset on kovakoodattu metodissa määriteltyyn hashiin. Metodin tulee toimia mielivaltaisen kokoisilla tunnukset sisältävillä hasheilla.
def authenticate admin_accounts = { "pekka" => "beer", "arto" => "foobar", "matti" => "ittam", "vilma" => "kangas" } authenticate_or_request_with_http_basic do |username, password| # do something here end endTestatessasi toiminnallisuutta, muista että joudut käyttämän incognito-selainta jos haluat kirjautua uudelleen annettuasi kertaalleen oikean käyttäjätunnus/salasanaparin.
VIHJE: oikean koodin kirjoittaminen saattaa olla helpointa debuggerin avulla, pysäytä ohjelman suorutus byebugilla:
authenticate_or_request_with_http_basic do |username, password| byebug end
ja kokeile mitä muuttujissa admin_accounts, username ja password on arvoina ja kehittele oikea komento.
VIHJE2: koodilohkon pitää siis saada arvokseen tosi/epätosi riipuen siitä onko salasana oikein. Arvon ei kuitenkaan tarvitse välttämättä olla true tai false, sillä ruby tulkitsee myös muut arvot joko todeksi (truthy) tai epätodeksi (falsy), esim. nil tulkitaan epätodeksi katso tarkemmin esim. seuraavasta https://learn.co/lessons/truthiness-in-ruby-readme
Viikon lopuksi on taas aika deployata sovellus herokuun.
Navigoitaessa reittausten sivulle syntyy pahaenteinen virheilmoitus:
Tuotantomoodissa pyörivän sovelluksen virheiden jäljittäminen on aina hiukan vaikeampaa kuin kehitysmoodissa, jossa Rails tarjoaa sovellusohjelmoijalle monia mahdollisuuksia virheiden selvittämiseen.
Tuotantomoodissa virheiden syy täytyykin kaivaa sovelluksen lokista. Kuten viime viikolla jo mainittiin, herokussa olevan sovelluksen lokiin pääsee käsiksi komennolla heroku logs
.
Tälläkin kertaa virheen syy paljastuu:
> heroku logs
2018-09-08T13:34:55.379420+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5] Processing by RatingsController#index as HTML
2018-09-08T13:34:55.381470+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5] Rendering ratings/index.html.erb within layouts/application
2018-09-08T13:34:55.384735+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5] Rating Load (1.2ms) SELECT "ratings".* FROM "ratings"
2018-09-08T13:34:55.385523+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5] Rendered ratings/index.html.erb within layouts/application (3.9ms)
2018-09-08T13:34:55.385780+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5] Completed 500 Internal Server Error in 6ms (ActiveRecord: 1.2ms)
2018-09-08T13:34:55.386820+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]
2018-09-08T13:34:55.386846+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5] ActionView::Template::Error (PG::UndefinedTable: ERROR: relation "ratings" does not exist
2018-09-08T13:34:55.386848+00:00 app[web.1]: LINE 1: SELECT "ratings".* FROM "ratings"
2018-09-08T13:34:55.386849+00:00 app[web.1]: ^
2018-09-08T13:34:55.386850+00:00 app[web.1]: : SELECT "ratings".* FROM "ratings"):
2018-09-08T13:34:55.386958+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5] 1: <h2>List of ratings</h2>
2018-09-08T13:34:55.386960+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5] 2:
2018-09-08T13:34:55.386966+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5] 3: <ul>
2018-09-08T13:34:55.386968+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5] 4: <% @ratings.each do |rating| %>
2018-09-08T13:34:55.386970+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5] 5: <li> <%= rating %> <%= link_to 'delete', rating_path(rating.id), method: :delete, data: { confirm: 'Are you sure?' } %> </li>
2018-09-08T13:34:55.386972+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5] 6: <% end %>
2018-09-08T13:34:55.386973+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5] 7: </ul>
2018-09-08T13:34:55.386977+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]
2018-09-08T13:34:55.387016+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5] app/views/ratings/index.html.erb:4:in `_app_views_ratings_index_html_erb___3457620989041177195_70202650345860'
Tietokantataulua ratings siis ei ole olemassa. Ongelma korjaantuu suorittamalla migratiot:
heroku run rails db:migrate
Generoidaan seuraavaksi tilanne, jossa tietokanta joutuu hieman epäkonsistenttiin tilaan.
Käynnistä heroku-konsoli komennolla heroku run console
ja luo sovellukseen olut johon ei liity mitään panimoa
> b = Beer.new name:"crap beer", style:"lager"
> b.save(validate: false)
ja olut johon liittyvää panimoa ei ole olemassa (eli viiteavaimena oleva panimon id on virheellinen):
> b = Beer.new name:"shitty beer", style:"lager", brewery_id: 123
> b.save(validate: false)
Kun menet nyt kaikkien oluiden sivulle on seurauksena jälleen ikävä ilmoitus "We're sorry, but something went wrong.". Jälleen kerran ongelmaa on etsittävä lokeista:
2018-09-08T13:39:17.133318+00:00 app[web.1]: [c355e369-86e1-4a48-9bca-b7bda15ddc97] Rendered beers/index.html.erb within layouts/application (14.1ms)
2018-09-08T13:39:17.133472+00:00 app[web.1]: [c355e369-86e1-4a48-9bca-b7bda15ddc97] Completed 500 Internal Server Error in 15ms (ActiveRecord: 3.1ms)
2018-09-08T13:39:17.134072+00:00 app[web.1]: [c355e369-86e1-4a48-9bca-b7bda15ddc97]
2018-09-08T13:39:17.134111+00:00 app[web.1]: [c355e369-86e1-4a48-9bca-b7bda15ddc97] ActionView::Template::Error (undefined method `name' for nil:NilClass):
2018-09-08T13:39:17.134992+00:00 app[web.1]: [c355e369-86e1-4a48-9bca-b7bda15ddc97] 17: <tr>
2018-09-08T13:39:17.134995+00:00 app[web.1]: [c355e369-86e1-4a48-9bca-b7bda15ddc97] 18: <td><%= link_to beer.name, beer %></td>
2018-09-08T13:39:17.134998+00:00 app[web.1]: [c355e369-86e1-4a48-9bca-b7bda15ddc97] 20: <td><%= link_to beer.brewery.name, beer.brewery %></td>
2018-09-08T13:39:17.134996+00:00 app[web.1]: [c355e369-86e1-4a48-9bca-b7bda15ddc97] 19: <td><%= beer.style %></td>
2018-09-08T13:39:17.135001+00:00 app[web.1]: [c355e369-86e1-4a48-9bca-b7bda15ddc97] 22: <td><%= link_to 'Destroy', beer, method: :delete, data: { confirm: 'Are you sure?' } %></td>
2018-09-08T13:39:17.135003+00:00 app[web.1]: [c355e369-86e1-4a48-9bca-b7bda15ddc97] 23: </tr>
2018-09-08T13:39:17.135000+00:00 app[web.1]: [c355e369-86e1-4a48-9bca-b7bda15ddc97] 21: <td><%= link_to 'Edit', edit_beer_path(beer) %></td>
2018-09-08T13:39:17.135009+00:00 app[web.1]: [c355e369-86e1-4a48-9bca-b7bda15ddc97]
2018-09-08T13:39:17.135058+00:00 app[web.1]: [c355e369-86e1-4a48-9bca-b7bda15ddc97] app/views/beers/index.html.erb:20:in `block in _app_views_beers_index_html_erb___3429094900426111748_70202515664160'
2018-09-08T13:39:17.135060+00:00 app[web.1]: [c355e369-86e1-4a48-9bca-b7bda15ddc97] app/views/beers/index.html.erb:16:in `_app_views_beers_index_html_erb___3429094900426111748_70202515664160'
Syy löytyy:
undefined method `name' for nil:NilClass
virheen aiheuttanut rivi on
<td><%= link_to beer.brewery.name, beer.brewery %></td>
eli on olemassa olut, jonka kentässä brewery
on arvona nil
. Tämä voi johtua joko siitä, että oluen brewery_id
on nil
tai brewery_id
:n arvona on virheellinen (esim. poistetun panimon) id.
Kun virheen syy paljastuu, on etsittävä syylliset. Eli avataan heroku-konsoli komennolla heroku run console
ja haetaan panimottomat oluet:
> Beer.all.select{ |b| b.brewery.nil? }
=> [#<Beer id: 8, name: "crap beer", style: "lager", brewery_id: nil, created_at: "2018-09-08 13:37:21", updated_at: "2018-09-08 13:37:21">, #<Beer id: 9, name: "shitty beer", style: "lager", brewery_id: 123, created_at: "2018-09-08 13:38:51", updated_at: "2018-09-08 13:38:51">]
>
Seuraavana toimenpiteenä on virheen aiheuttavien olioiden korjaaminen. Koska loimme ne nyt itse testaamista varten, poistamme oliot (otamme ensin _
-muuttujassa olevat edellisen operaation palauttamat oliot talteen muuttujaan):
> bad_beer = _
=> [#<Beer id: 8, name: "crap beer", style: "lager", brewery_id: nil, created_at: "2018-09-08 13:37:21", updated_at: "2018-09-08 13:37:21">, #<Beer id: 9, name: "shitty beer", style: "lager", brewery_id: 123, created_at: "2018-09-08 13:38:51", updated_at: "2018-09-08 13:38:51">]
> bad_beer.each{ |bad| bad.delete }
> Beer.all.select{ |b| b.brewery.nil? }
=> []
>
Useimmiten tuotannossa vastaan tulevat ongelmat johtuvat siitä, että tietokantaskeeman muutosten takia jotkut oliot ovat joutuneet epäkonsistenttiin tilaan, eli ne esim. viittaavat olioihin joita ei ole tai viitteet puuttuvat. Sovellus kannattaakin deployata tuotantoon mahdollisimman usein, näin tiedetään että mahdolliset ongelmat ovat juuri tehtyjen muutosten aiheuttamia ja korjaus on helpompaa.
Koska kyseessä on tuotannossa oleva ohjelma, tietokannan resetointi (rails db:drop
) ei ole missään tapauksessa hyväksyttävä keino "korjata" epäkonsistenttia tietokantaa sillä tuotannossa olevaa dataa ei saa hävittää. Opettele siis heti alusta asti lukemaan lokeja ja selvittämään ongelmat kunnolla.
Commitoi kaikki tekemäsi muutokset ja pushaa koodi Githubiin. Deployaa myös uusin versio Herokuun.
Tehtävät kirjataan palautetuksi osoitteeseen https://studies.cs.helsinki.fi/courses/#/rails2018