Skip to content

Latest commit

 

History

History
1521 lines (1098 loc) · 70.1 KB

viikko2.md

File metadata and controls

1521 lines (1098 loc) · 70.1 KB

Kisko afterparty pe 16.12. klo 16-18

Suomen johtava Rails-talo Kisko järjestää kurssilaisille illanvieton pe 16.12. klo 16-18

Jos haluat mukaan, kysy ilmoittautumislinkkiä matti.luukkainen@helsinki.fi tai Discordissa kurssikanavalla tai @mluukkai

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.

Järkevä editori

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ä.

Nykyään hyvin yleisesti käytössä on Visual Studio Codea. Jos käytät VSC:tä, kannattaa ehdottamasti asentaa Ruby-plugin

Tärkeintä editorin valinnassa on kuitenkin loppuviimeeksi se, että käyttäjälle sen käyttäminen on mieluisaa.

Sovelluksen layout

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 style="color: green"><%= notice %></p>

<%= render @beer %>

<div>
  <%= link_to "Edit this beer", edit_beer_path(@beer) %> |
  <%= link_to "Back to beers", beers_path %>

  <%= button_to "Destroy this beer", @beer, method: :delete %>
</div>

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>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_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.

routes.rb

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-tiedostoon 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-tiedoston 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.

Oluiden pisteytys

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-tiedostossakin 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:

RatingsController#index is missing a template for request formats: text/html

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.

Modelin teko käsin, melkein...

Yhteen olueeseen liittyy useita reittauksia, eli oliomalli pitää päivittää seuraavanlaiseksi:

olueeseen liittyy reittauksia

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)

Puuttuva viiteavain

Yritetään luoda olut ilman panimoa:

irb(main)> b = Beer.create name:"anonymous", style: "watery"
=> #<Beer:0x00007f4444abc8b0 id: nil, name: "anonymous", style: "watery", brewery_id: nil, created_at: nil, updated_at: nil>
irb(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

irb(main)> b.errors
=> #<ActiveModel::Errors [#<ActiveModel::Error attribute=brewery, type=blank, options={:message=>:required}>]>

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", "2022-09-11 18:21:40.830949"], ["updated_at", "2022-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.

Tehtävä 1

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.

Tehtävä 2

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:

kuva

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.

Luomme hetken kuluttua reittauksille partials-tiedoston, joka pilkkoo koodia osiin ja jonka avulla voimme tuottaa helposti luettavan muodon arvosteluille. Tutkitaan ensin kuitenkin muutamaa asiaa liittyen olion metodien määrittelyyn.

Muutamia selvennyksiä Railsin model-olioista

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 2022:

def restart
  year = 2022
  puts "changed year to #{year}"
end

kokeillaan

> b = Brewery.first
> b.year
=> 1897
> b.restart
changed year to 2022
> b.year
=> 1897
>

eli huomaamme, että vuoden muuttaminen ei toimikaan odotetulla tavalla! Syynä tähän on se, että year = 2022 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 2022 sijoitetaan.

Jotta sijoitus onnistuu, on metodia kutsuttava self-viitteen kautta:

def restart
  self.year = 2022
  puts "changed year to #{year}"
end

ja nyt toiminnallisuus on odotetun kaltainen:

> b = Brewery.first
> b.year
=> 1897
> b.restart
changed year to 2022
> b.year
=> 2022
>

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 = 2022
  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.

Tehtävä 3

Muuta sivun ratings-näkymä sellaiseksi, että arvosteluoliosta tehdään parempi merkkijonoesitys 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/WebPalvelinohjelmointi2022/blob/main/web/rubyn_perusteita.md#merkkijonot

Voit tehdä kaiken suoraan tiedostoon views/partials/index.html.erb tai voit halutessasi myös tehdä luokalle Rating partials-tiedoston, joka hoitaa yhden ratingin muotoilemisen

Apua partials-tiedoston tekemiseen ja renderöimiseen voi katsoa esim. _beer.html.erb ja vastaavasta index.html.erb tiedostosta. Muista partials-tiedostojen nimeämiskäytäntö!

Tehtävän jälkeen reittausten sivujen tulisi näyttää suunnilleen seuraavalta:

kuva

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.

Tehtävä 4

Lisää luokalle Beer metodi average_rating, joka laskee oluen ratingien keskiarvon. Lisää keskiarvo yksittäisen oluen sivulle jos oluella on ratingeja.

Nä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 %>

Muista palauttaa keskiarvo liukulukuna, tässä voi käyttää apuna to_f-metodia.

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):

kuva

Tehtävä 5

Moduuli enumerable (ks. https://ruby-doc.org/core-3.1.2/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- ja reduce-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:

kuva

Sivulla on pieni, mutta ikävä kielioppivirhe:

beer has 1 ratings

Tehtävä 6

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')

Lomake ja post

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-tiedostoon:

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, ei mitään näytä tapahtuvan. Selaimen developer-konsoli paljastaa kuitenkin, että selain on tehnyt POST-pyynnön osoitteeseen http://localhost:3000/ratings mutta palvelin on vastannut siihen 404

kuva

Joudumme siis luomaan tiedostoon config/routes.rb reitin lomakkeen lähetyksen käsittelyyn:

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:

{"authenticity_token"=>"[FILTERED]",
 "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].

Debuggeri

Tutkitaan hieman asiaa kontrollerista käsin Railsin debuggeria hyödyntäen

Rails on jo konfiguroinut sovelluksesi käyttöön debuggerin. Railsin oletusarvoinen debuggeri ei kuitenkaan tällä hetkellä käyttäydy kaikissa tilanteissa hyvin, joten asennetaan vaihtoehtoinen pry-byebug lisäämällä tiedostoon Gemfile seuraava

group :development, :test do
  gem 'pry-byebug'
end

Lisäyksen jälkeen tulee suorita komentoriviltä komento bundle install ja käynnistää sovellus uudelleen.

Lisätään kontrollerin alkuun, eli sille kohtaan koodia jota haluamme tarkkailla, komento binding.pry

def create
  binding.pry
end

Kun luot lomakkeella uuden reittauksen, sovellus pysähtyy komennon binding.pry kohdalle. Terminaaliin josta Rails on käynnistetty, avautuu nyt interaktiivinen konsolinäkymä:

Started POST "/ratings" for ::1 at 2022-07-20 14:02:51 +0300
Processing by RatingsController#create as TURBO_STREAM
  Parameters: {"authenticity_token"=>"[FILTERED]", "rating"=>{"beer_id"=>"12", "score"=>"12"}, "commit"=>"Create Rating"}
[7, 15] in ~/ratebeer/app/controllers/ratings_controller.rb
     7|     def new
     8|       @rating = Rating.new
     9|     end
    10|
    11|     def create
=>  12|       binding.pry
    13|     end
    14|
    15| end
=>#0    RatingsController#create at ~/ratebeer/app/controllers/ratings_controller.rb:12
  #1    ActionController::BasicImplicitRender#send_action(method="create", args=[]) at ~/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/actionpack-7.0.3/lib/action_controller/metal/basic_implicit_render.rb:6
  # and 73 frames (use `bt' command for all frames)
(ruby)

Nuoli kertoo kohdan jossa suoritus keskeytettiin. Tutkitaan nyt params-muuttujan sisältöä:

(rdbg) params
#<ActionController::Parameters {"authenticity_token"=>"2pGKvP6I-RYAoEbZr6eJltrNZt_T0YlQvO4K7EOyMFrF1W_OzJoPTKd39LBQoMyG5u_ScQrLjztIcB8TyWpDTw", "rating"=>#<ActionController::Parameters {"beer_id"=>"12", "score"=>"12"} permitted: false>, "commit"=>"Create Rating", "controller"=>"ratings", "action"=>"create"} permitted: false>
(ruby) params[:rating][:beer_id]
"12"
(ruby) params[:rating][:score]
"12"

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 debuggerista seuraavassa https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem

Reittauksen talletus

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 arvojen 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:12:in `create'
No template found for RatingsController#create, rendering head :no_content
Completed 204 No Content in 55ms (ActiveRecord: 16.4ms | Allocations: 12093)

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.

Uudelleenohjaus

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"!

redirect_to vs render

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!

Polkuapumetodit

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 metodille index.
  • metodikutsu ratings_new_path generoi linkin, joka vie osoitteeseen "ratings/new" ja ohjautuu ratings-kontrollerin metodille new. 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
  • 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:

kuva

Tehtävä 7

Lisää kaikkien reittausten sivulle linkki uuden reittauksen tekemiseen. Lisää sovelluksen navigointipalkkiin linkki kaikkien reittausten listalle

Oluiden valinta listalta

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>"
>

Tehtävä 8

Tee oluelle to_s-metodi, jonka muodostamassa tekstuaalisessa esityksessä on sekä oluen, että sen panimon nimi

Muuta reittauksen luovaa lomaketta siten, että valittavista oluista näytetään nimikentän arvon sijaan olion to_s-metodin palauttama tekstuaalinen esitys

Tehtävä 9

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.

Tehtävä 10

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", "Lowalcohol"]
end

Näkymän tulee siis generoida lomakkeeseen valintavaihtoehdot taulukon @styles perusteella. Vaihtoehtojen generointiin kannattaa nyt metodin options_from_collection_for_select sijaan käyttää metodia options_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", "Lowalcohol"]
  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 ja reititys

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.

Ratingin poisto

Lisätään ohjelmaan vielä mahdollisuus poistaa reittauksia. Lisätään ensin vastaava reitti muokkaamalla routes.rb-tiedostoa:

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> <%= render rating %> <%= button_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> <%= render rating %> <%= button_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.

Tehtävä 11

Reittauksen poisto on nyt siinä mielessä ikävä, että herkkäsorminen sivuston käyttäjä saattaa vahinkoklikkauksella tuhota reittauksia.

Katso esim. täältä mallia ja tee ratingin tuhoamisesta sellainen, että käyttäjältä kysytään varmistus reittauksen tuhoamisen yhteydessä.

Orvot oliot

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ä.

Tehtävä 12

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 kutsutaan beer.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.

Tehtävä 13

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.

Jos kaikkien panimoiden sivulta ei vielä ratkaisussasi pääse yksittäisten panimoiden sivuille, korjaa tilanne!

Olioiden epäsuora yhteys

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

Tehtävä 14

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:

kuva

Yhteisen koodin siirto moduuliin

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.

Tehtävä 15

Ruby tarjoaa keinon jakaa metodeja kahden luokan välillä moduulien avulla, ks. https://github.com/mluukkai/WebPalvelinohjelmointi2022/blob/main/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 luokat Beer ja Brewery 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 tiedostossa app/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)>
>

Yksinkertainen suojaus

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: %i[ show edit update destroy ]
  before_action :authenticate

  # ...

  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: %i[ show edit update destroy ]
  before_action :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_action :authenticate, only: [:destroy]

  # ...

  private

  # ...

  def authenticate
    authenticate_or_request_with_http_basic do |username, password|
      if username == "admin" and password == "secret"
        return true
      else
        raise "Wrong username or password" # käyttäjätunnus/salasana oli väärä
      end
    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|
    raise "Wrong username or password" unless username == "admin" and password == "secret"
    
    return true
  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

Tehtävä 16

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
 end

Testatessasi 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 suoritus:

authenticate_or_request_with_http_basic do |username, password| binding.pry 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

Sovellus internetiin

Viikon lopuksi on taas aika deployata sovellus Fly.io:n Herokuun. Deployment Fly.io:n onnistuu ehkä ongelmitta, sillä Fly.io suorittaa automaattisesti sovellukseen määritellyt tietokantamigraatiot. Herokun suhteen tilanne on toisin.

Ongelmia Herokussa

Navigoitaessa reittausten sivulle syntyy pahaenteinen virheilmoitus:

kuva

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
2020-08-20T13:34:55.379420+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5] Processing by RatingsController#index as HTML
2020-08-20T13:34:55.381470+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]   Rendering ratings/index.html.erb within layouts/application
2020-08-20T13:34:55.384735+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]   Rating Load (1.2ms)  SELECT "ratings".* FROM "ratings"
2020-08-20T13:34:55.385523+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]   Rendered ratings/index.html.erb within layouts/application (3.9ms)
2020-08-20T13:34:55.385780+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5] Completed 500 Internal Server Error in 6ms (ActiveRecord: 1.2ms)
2020-08-20T13:34:55.386820+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]
2020-08-20T13:34:55.386846+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5] ActionView::Template::Error (PG::UndefinedTable: ERROR:  relation "ratings" does not exist
2020-08-20T13:34:55.386848+00:00 app[web.1]: LINE 1: SELECT "ratings".* FROM "ratings"
2020-08-20T13:34:55.386849+00:00 app[web.1]: ^
2020-08-20T13:34:55.386850+00:00 app[web.1]: : SELECT "ratings".* FROM "ratings"):
2020-08-20T13:34:55.386958+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]     1: <h2>List of ratings</h2>
2020-08-20T13:34:55.386960+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]     2:
2020-08-20T13:34:55.386966+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]     3: <ul>
2020-08-20T13:34:55.386968+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]     4:  <% @ratings.each do |rating| %>
2020-08-20T13: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>
2020-08-20T13:34:55.386972+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]     6:  <% end %>
2020-08-20T13:34:55.386973+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]     7: </ul>
2020-08-20T13:34:55.386977+00:00 app[web.1]: [fc20f584-1aef-4d93-8bff-c1a55e5cb6f5]
2020-08-20T13: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:

2022-08-20T10:56:01.307817+00:00 app[web.1]: F, [2022-08-20T10:56:01.307761 #4] FATAL -- : [22db4647-3122-419e-8e83-e2e99bfe3606]
2022-08-20T10:56:01.307818+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606] ActionView::Template::Error (undefined method `name' for nil:NilClass):
2022-08-20T10:56:01.307818+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606]     10:
2022-08-20T10:56:01.307819+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606]     11:   <p>
2022-08-20T10:56:01.307819+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606]     12:     <strong>Brewery:</strong>
2022-08-20T10:56:01.307820+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606]     13:     <%= link_to beer.brewery.name, beer.brewery %>
2022-08-20T10:56:01.307820+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606]     14:   </p>
2022-08-20T10:56:01.307821+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606]     15:
2022-08-20T10:56:01.307821+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606]     16:   <% if beer.ratings.empty? %>
2022-08-20T10:56:01.307821+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606]
2022-08-20T10:56:01.307822+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606] app/views/beers/_beer.html.erb:13
2022-08-20T10:56:01.307822+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606] app/views/beers/index.html.erb:7
2022-08-20T10:56:01.307822+00:00 app[web.1]: [22db4647-3122-419e-8e83-e2e99bfe3606] app/views/beers/index.html.erb:6

Syy löytyy:

undefined method `name' for nil:NilClass

Virheen aiheuttanut rivi on:

<%= link_to beer.brewery.name, beer.brewery %>

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: "2020-08-20 13:37:21", updated_at: "2020-08-20 13:37:21">, #<Beer id: 9, name: "shitty beer", style: "lager", brewery_id: 123, created_at: "2020-08-20 13:38:51", updated_at: "2020-08-20 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: "2020-08-20 13:37:21", updated_at: "2020-08-20 13:37:21">, #<Beer id: 9, name: "shitty beer", style: "lager", brewery_id: 123, created_at: "2020-08-20 13:38:51", updated_at: "2020-08-20 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.

Tehtävien palautus

Commitoi kaikki tekemäsi muutokset ja pushaa koodi GitHubiin. Deployaa myös uusin versio Fly.io:n tai Herokuun.

Tehtävät kirjataan palautetuksi osoitteeseen https://studies.cs.helsinki.fi/stats/courses/rails2022

Ja ei kun eteenpäin: viikko 3.