Flask, Sessiot, autentikointi ja tietokannat
- Hello World Flask-sovelluksena
- Flask ja templatet
- Laskuri sessioilla
- Tietokannat
- Tietokannan luominen
- Tietokantayhteys
- Kyselyt
- Lisääminen
- Poistaminen
- Päivittäminen
- Monimutkaisemmat tulostukset
- Transaktiot
- Automaattiset avainkentät
- Lisätietoa
- Tietojen validointi
- Autentikointi
- Vanhoja malleja
Harjoitellaan Flask-sovelluskehyksen ja sessioiden käyttöä WWW-sovelluksen tekemisessä. Toteutaan sessioilla yksinkertainen lomake, joka laskee lukuja yhteen. Kokeillaan tietokantojen käyttämistä.
mallivastaus ( lähdekoodi oma.py, lähdekoodi flask.py, kirjaudu, laskuri, lomake, reseptit, )
Hello World Flask-sovelluksena
- Tutustu Flaskia käsittelevään materiaaliin ja avaa avuksesi seuraavat sivut:
Flask on rakennettu Werkzeug-WSGI-kirjaston päälle joten osa tarpeellisesta dokumentaatiosta on suoraan Werkzeugin dokumentaatiossa.
- Luo W-aseman cgi-bin-hakemistoon uusi alihakemisto ohjaus4
-
Luo kansioon uusi tiedosto flask.cgi ja kirjoita siihen seuraava ohjelmakoodi:
#!/usr/bin/python # -*- coding: utf-8 -*- from wsgiref.handlers import CGIHandler from oma import app as application if __name__ == '__main__': CGIHandler().run(application)
- Luo samaan kansioon myös oma.py-tiedosto ja kirjoita siihen seuraava ohjelmakoodi:
#!/usr/bin/python # -*- coding: utf-8 -*- from flask import Flask, session, redirect, url_for, escape, request, Response, render_template import os app = Flask(__name__) # @app.route määrää mille osoitteille tämä funktio suoritetaan @app.route('/') def hello_world(): return Response("Hello World", content_type="text/plain; charset=UTF-8") # yleensä riittää: # return "Hello World" if __name__ == '__main__': app.debug = True app.run(debug=True)
- Varmista, että tiedostojen ja kansioiden oikeudet ovat samat kuin aiemmin tehdyissä cgi-ohjelmissa.
- Kokeile sovellustasi osoitteessa http://users.jyu.fi/~omatunnus/cgi-bin/ohjaus4/flask.cgi/. Viimeinen /-merkki on olennainen!
- Jos sovelluksesi ei toimi niin lue ohjetta eteenpäin ja kokeile omalla / mikroluokan koneelle asennetulla Flaskilla
- WWW-palvelin suorittaa Flask-kehyksen ja oma.py-tiedostosta löytyy hello_world-funktio, joka on määrätty suoritettavaksi kun pyydetään sovelluksen juurikansiota (/). HTTP-protokollaan liittyviä asioita ei tarvitse itse tehdä vaan Flask hoitaa ne. Riittää, että funktio palauttaa merkkijonon jossa on sivulle haluttu sisältö.
Flask omalle koneelle
Asenna Anaconda (Python 2.7), jonka mukana tulee myös Flask. Anaconda on jo valmiiksi asennettuna Agoran mikroluokkiin.
Tämän jälkeen riittää, kun käynnistää oman sovelluksen Windowsin CMD-promptissa komennolla
python oma.py
ja voi kokeilla selaimella osoitteessa:
http://127.0.0.1:5000/tai
http://localhost:5000/
Nyt saa myös selkeät virheilmoitukset suoraan selaimeen, koska Flaskin debug-tila toimii omalla koneella.
Halavassa / jalavassa tämä ei onnistu, koska Flaskia ei niistä löydy eikä tule ennen kuin koneet päivitetään samaan käyttisversioon kuin users.jyu.fi. Yliopiston mikroluokkien koneisiin on osaan asennettuna Anaconda.
Lopullinen sovellus pitää yleensä saada toimimaan muualla kuin omalla koneella ja toiminnallisuudessa voi olla eroja esim. tiedostopolut voivat olla erilaisia. Debuggaus on kuitenkin huomattavasti helpompaa omalla koneella. Jos on W:-asema verkkolevynä niin voi suoraan sieltä ajaa sovellustaan oman koneen Flaskilla ja testailla samaan aikaan myös users.jyu.fin kautta.
Debuggaus
Sitten kun flask-sovellus lähtee toimimaan niin virheiden debuggaus on haastavaa, koska users.jyu.fi:ssä ei toimi flaskin oma debug-tila. Ohjelman kaatuessa syntaksivirheeseen tai ajonaikaiseen virheeseen on tuloksena vain internal server error. cgitb-kirjasto ei auta.
Täytyy itse logata virheet. Seuraava tekee lokitiedoston kansioon '../piilo/'. Korjaa polku omaan kansiorakenteeseesi sopivaksi:
import logging logging.basicConfig(filename=os.path.abspath('../piilo/flask.log'),level=logging.DEBUG) logging.debug("Tänne mitä haluaa lokiin kirjoittaa")
Älä kirjoita lokia cgi-bin-kansioon tai sen alikansioihin.. Älä myöskään kirjoita lokia w:\hidden-kansioon, koska se on users.jyu.fi-palvelimen asetuksissa määritelty erikoiskansioksi. Jostain syystä sinne kirjoittaminen ei toimi users.jyu.fi:ssä.
Älä luo flask-sovelluksen käyttämiä kansioita Windowsin kautta tai esim. WinSCP:llä vaan vain ja ainoastaan Unix-komentoriviltä. Valitettavasti users.jyu.fissä käytetty SELinux sotkee kansioiden ja tiedostojen oikeuksia ja tiedostokontekstiä jos ne tehdään Windows-levyjaon kautta.
Jotta lokista olisi iloa on käytettävä try..exceptiä. Nappaa exceptillä kiinni mahdolliset virheet ja dumppaa virheilmoitus lokiin:
try: # sqlite haluaa absoluuttisen polun con = sqlite3.connect(os.path.abspath('../piilo/resepti')) except Exception as e: logging.debug("Kanta ei aukea") # sqliten antama virheilmoitus: logging.debug(str(e))
Lokin sisältöä voi seurailla helpoiten ottamalla pääteyhteyden halavaan ja käyttämällä tail-komentoa:
tail -f flask.log
Tämä kyttää koko ajan tiedoston sisältöä ja näyttää heti sinne tulevat uudet rivit.
Flask ja templatet
Luodaan seuraavan kuvan esittämä lomake käyttäen jinja-templatea. Kopioi itsellesi valmis pohja ja tallenna se ohjaus4/templates-kansioon laskuri.html-nimellä.
- Lisää oma.py-tiedoston alkuun rivi:
from flask import render_template
- Nyt voit käyttää Jinja-templatea aivan samaan tapaan kuin aiemmin jo opit. Kokeile muuttaa hello_world-funktiota:
return render_template('laskuri.html')
- Muuta lomakkeen käsittelijäksi sivu itse. Vaihda templateen seuraava rivi:
<form action="{{request.base_url}}" method="get">
Template saa automaattisesti käyttöönsä Flaskin request-, session- ja g-objektit. Näiden lisäksi tulevat tietysti objektit, jotka annat render_template-kutsussa.
Laskuri sessioilla
- Kokeile edellä luomaasi lomaketta. Toimiiko vai saatko virheilmoituksen? Kokeile vaihtaa lomakkeen lähetysmetodia POST- ja GET-metodien välillä.
- Hyväksytyt metodit pitää erikseen luetella:
# method-parametrissa luetellaan metodit joita tämä funktio kelpuuttaa. @app.route('/laskuri', methods=['POST','GET']) def laskuri(): return render_template('laskuri.html')
- Varsinaisessa laskurissa tarvitset request-objektin
form-, args- tai values -ominaisuutta. Vrt. CGI-ohjelmassa käytetty cgi.fieldstorage.
- request.form sisältää POST-metodilla lähetetyt tiedot
- request.args sisältää query stringin sisältämät tiedot eli GET-metodilla lähetetyt tiedot
- request.values sisältää molempien edellämainittujen sisällöt
Kts. myös Werkzeug BaseRequest
Toinen vaihtoehto on käyttää get- ja getlist-metodeja samaan tapaan kuin cgi.fieldstoragen kanssa. Kts. Werkzeug datastructures.
avain = request.form.get("avain", "oletusarvo") avain = request.form.getlist("avain")
- Muuta ohjelmaa siten, että tulostat ennen lomaketta näkyville mahdollisesti syötetyn luvun:
@app.route('/laskuri', methods=['POST','GET']) def laskuri(): try: luku = request.form['luku'] except: luku = 0 # sama lyhyemmin. voidaan antaa myös oletusarvo: luku = request.form.get("luku", 0) # jos lukuja olisi useampi niin pitää käyttää getlist-metodia # luvut = request.form.getlist("luku") return render_template('laskuri.html', luku=luku)
- Muutetaan ohjelmaa siten, että voidaan laskea yhteen syötettyjä lukuja. Ohjelman pitää siis
muistaa aiemmin saatu summa. Tämä onnistuu session avulla.
Sessioon tallennetut muuttujat säilyttävät arvonsa koko istunnon ajan. Lisää ohjelmakooditiedostosi alkuun rivi:
from flask import Flask, session, redirect, url_for, escape, request, Response, render_template
Tämän lisäksi tarvit salaisen avaimen jota käytetään sessioon liittyvän evästeen käsittelyssä. Lisää seuraava rivi app-objektin määrittelyn jälkeen:app.secret_key = '"\xf9$T\x88\xefT8[\xf1\xc4Y-r@\t\xec!5d\xf9\xcc\xa2\xaa'
Sinun pitäisi muodostaa itse oma salainen avain. Se onnistuu esimerkiksi käynnistämällä python-tulkki komentoriviltä ja antamalla komennot:
import os os.urandom(24)
- Luo uusi sessiomuuttuja:
session['summa'] = 10
sessiomuuttujia käytetään samaan tapaan kuin dictejä - sessio-muuttujien pitäisi näkyä suoraan templateen. Kokeile tulostaa session['summa']-muuttujan arvo www-sivulle
- Yritä kasvattaa sessiomuuttujasi arvoa ohjelman jokaisella suorituskerralla.
Ensimmäisellä käyttökerralla sessio-muuttujaa ei ole määritelty joten varminta on testata pythonin poikkeuskäsittelyn (try...except) avulla onko sessiomuuttujalla jo arvo vai ei. Jos muuttuja on alustamaton niin yritys lisätä muuttujan arvoa aiheuttaa poikkeuksen jolloin voidaan tehdä muuttujan alustaminen.
- Ota lomakkeelle annettu luku talteen. Tarkista, että lomakkeelle on varmasti syötetty kokonaisluku. Lisää luku sessiomuuttujaan. Tulosta yhteenlaskettu summa sivulle.
-
Istunnon ja siihen liittyvien muuttujien ylläpitäminen edellyttää, että
käytetty selain tukee evästeitä. Evästeessä kuljetetaan selaimen ja WWW-palvelimen
välillä istuntoon liittyvää tunnistetta.
Katso Firebugin Network-välilehdeltä minkälaisia HTTP-otsakkeita liikkuu selaimesi ja WWW-palvelimen välillä.
Sieltä pitäisi löytyä Set-Cookie-alkuisia rivej. Kokeile myös Web Developer Toolbarin
Cookies|View Cookie Information-valinnan avulla millaisen evästeen flask asettaa käyttäessäsi sessioita.
Seuraavassa kuvassa hahmotetaan sessioon liittyvää toimintaa PHP-sovelluksessa. Toiminta on täysin vastaavaa flask-sovelluksilla.
Testauksessa kannattaa käyttää myös Web Developer Toolbarista löytyvää valintaa Cookies | Delete Domain Cookies. Tämä poistaa näkyvillä olevaan sivuun liittyvät evästeet. Poistamalla evästeet palaa laskuri alkutilaan.
- Tee uusi sivu joka poistaa laskurisi session.pop('summa',None)-metodilla. Kokeile nollautuuko laskuri.
@app.route('/nollaa') def nollaa(): session.pop('summa',None) # url_for-metodilla voidaan muodostaa osoite haluttuun funktioon. redirect taas ohjaa suoraan tälle sivulle joten # nollaa-osoite ei tarvitse omaa sisältöä return redirect(url_for('laskuri'))
Tietokannat
Tehtävien aiheena on tietokantojen käsittely Python-kielellä. Katso ennen näiden tehtävien tekemistä luennot: Tietokannat ja Python ja Tietokannat ja python jatkoa.
Mallia voi katsoa myös valmiista esimerkistä, joka on tehty ilman html:ää. (lähdekoodi).
Tietokantatehtävät voi tehdä Flaskin sijaan myös tavallisella CGI-ohjelmalla tai ajamalla sovellusta suoraan halavassa/jalavassa komentoriviltä.
Tietokannan luominen
Tehtävissä käytetään SQLite3-tietokantaa, jota voidaan hallinnoida suoraan komentoriviltä käynnistyvällä työkalulla. Samaan tapaan onnistuu myös hienompien tietokannan hallintajärjestelmien hallinnointi (PostgreSQL, MySQL). SQLite-tietokannan kanssa mahdollisesti vastaantulevista ongelmista on oma FAQ.
- Ota Puttylla SSH-yhteys halava.cc.jyu.fi:hin.
- Siirry htmlkansioon eli symbolisen linkin kautta W:-asemaa vastaavaan paikkaan. Luo tänne alihakemisto hidden ellet luonut sitä jo aiemmin.
- Kopioi itsellesi resepti.sql-tiedosto wget-komennolla:
wget http://appro.mit.jyu.fi/web-sovellukset/ohjaus/ohjaus4/resepti.sql
Tietokannan rakenne:
- Avaa hidden-kansiossa sqlite3 komennolla:
sqlite3 resepti
- Luo tietokannan rakenne komennolla:
.read resepti.sql
SQLite lukee resepti.sql-tiedoston ja suorittaa sen sisältämät SQL-komennot. Jos SQLite kaatuu niin käynnistä se uudelleen.
SQLite luo nyt uuden resepti-tiedoston ja tekee siitä tietokannan. SQLite saattaa kaatua mutta sen pitäisi silti luoda tietokanta. Kokeile käynnistämällä SQlite uudelleen ja kirjoittamalla .tables. Jos saat listauksen tietokannan tauluista niin kaikki on ok.
Jos kannan luominen sql-lauseilla ei mitenkään onnistu voit yrittää kopioida valmiin kantatiedoston ja avata sen komennolla sqlite3 resepti.sqlite.
- Voit tutustua sqlite3-työkalun osaamiin komentoihin kirjoittamalla .help.
- Tutki tietokantasi rakennetta seuraavilla komennoilla: .tables, .schema resepti ja .schema ruokalaji.
- Tutki mitä taulut sisältävät yksinkertaisilla SQL-kyselyillä:
SELECT * FROM Resepti;
SELECT * FROM Ruokalaji;
- .quit-komennolla pääset takaisin unix-shelliin.
Tietokantayhteys
Sovelluksen alussa pitää luoda tietokantaan yhteys, jota käytetään koko suorituksen ajan. Yhteyden avaaminen on aina raskas prosessi, joten turhaan yhteyttä ei pidä aukoa ja sulkea. Suurissa sovelluksissa tietokantayhteys pidetään auki pitempään ja samaa yhteyttä käytetään uudelleen ja uudelleen.
- Lisää Flask-ohjelmaasi uusi sivu /reseptit.
Avaa ensimmäisenä tietokantayhteys:
import sqlite3 import logging import os # oletetaan että sovellus on kansiossa /~omatunnus/cgi-bin/ohjaus4/ # ja tietokanta on kansiossa /~omatunnus/hidden/ # os.path.abspath muuntaa suhteellisen polun absoluuttiseksi, joka taasen kelpaa sqlitelle con = sqlite3.connect(os.path.abspath('../../hidden/resepti'))
-
Aseta tietokantayhteyteen seuraavat asetukset jotta voidaan kursoreissa viitata kenttiin niiden nimillä:
# voidaan käsitellä palautettuja tietueita niiden kenttien nimillä con.row_factory = sqlite3.Row
- Anna tietokantaan ja kansioon, jossa tietokantatiedosto sijaitsee, kaikille kirjoitusoikeus (chmod a+rw resepti ja chmod a+rwx ./), niin tietokantaan kohdistuvat muutosoperaatiot toimivat jatkossa myös WWW:n kautta.
- Kokeile selaimella toimiiko ohjelmasi ilman virheilmoituksia
- Kun tietokantayhteyttä ei enää tarvita niin sulje yhteys:
con.close()
Kyselyt
Tietokantaan kohdistuvat kyselyt ovat jokaisen sovelluksen perusta. Jos SQL-kyselyiden kirjoittaminen tuottaa suuria hankaluuksia voit harjoitella niitä Henkilökohtaisen tiedonhallinnan perusteet -kurssin demotehtävillä.
-
Voit esimerkiksi kirjoittaa poikkeusten virheilmoitukset lokiin, koska Flask itse piilottaa ne server error -virheilmoituksen taakse:
try: # sqlite haluaa absoluuttisen polun # varmista, että polku on oikein! con = sqlite3.connect(os.path.abspath('../../hidden/resepti')) except Exception as e: logging.debug("Kanta ei aukea") # sqliten antama virheilmoitus: logging.debug(str(e))
-
Valmistele yksinkertainen kysely:
sql = """ SELECT * FROM resepti """
- Luo Cursor-objekti ja suorita (execute) kysely. Ota kiinni
mahdolliset virhetilanteet:
cur = con.cursor() try: cur.execute(sql) except: # vaatii koodin alkuun rivin: import sys logging.debug(sys.exc_info()[0])
-
Luo tuloksena saaduista tietueista templatelle sopiva tietorakenne. Seuraavassa on muutamana eri versiona esimerkki:
con = sqlite3.connect(os.path.abspath('../../hidden/resepti')) sql = """ SELECT nimi as Nimi, kuvaus as Kuvaus, henkilomaara as Henkilomaara FROM resepti """ cur = con.cursor() try: cur.execute(sql) except: # vaatii koodin alkuun rivin: import sys logging.debug(sys.exc_info()[0]) reseptit = [] for row in cur.fetchall(): # luo lennosta aina uuden dictin, joka sisältää yhden tietueen tiedot reseptit.append( dict(nimi=row[0], kuvaus=row[1], maara=row[2]) ) # sama kuin edellä mutta käytetään kenttien nimiä eikä indeksejä # voit käyttää kumpaa versiota tahansa # for row in cur.fetchall(): # reseptit.append( dict(nimi=row['Nimi'], kuvaus=row['Kuvaus'], maara=row['Henkilomaara']) ) # sama kuin edellä mutta toisenlaisena versiona # Vastaava kuin cur.execute(sql) mutta kysely on suoraan executen sisällä ja suoritettuna yhteyden (con) kautta # cur = con.execute('select nimi, kuvaus, henkilomaara from resepti') # tyhjätään lista # reseptit = [] # lyhin versio # reseptit = [dict(nimi=row[0], kuvaus=row[1], maara=row[2]) for row in cur.fetchall()]
- Vie reseptit templatelle. Voit ensimmäisessä versiossa suoraan tulostaa tietorakenteen templatessa näkyville.
{{ reseptit }}
Kokeile suorittaa ohjelmasi selaimessa. Tee tämän jälkeen siistimpi tulostusversio eli käy näkymässä silmukassa läpi reseptit-dict ja tulosta sen sisältö kauniisti. Esim.{% for p in reseptit %} <p>{{ p['nimi'] }} {{ p['maara'] }} {{ p['kuvaus'] }}</p> {% endfor %}
Avainten pitää olla samat (isot ja pienet kirjaimet ovat merkitseviä) kuin mitä olet käyttänyt reseptit-listaa kasaillessa (nimi=row...)
- Yleensä kyselyjä pitää rajoittaa sopivilla ehdoilla, jotka muuttuvat ohjelman suorituksen mukana. Tällöin kyselystä
tulee dynaaminen. Muuta edellistä kyselyä seuraavanlaiseksi:
# valmistellaan kysely ja sijoitetaan dynaamisen arvon tilalle :lkm-muuttuja sql = '''SELECT Nimi, Kuvaus, Henkilomaara, ReseptiID, RuokaLajiID FROM Resepti WHERE henkilomaara = :lukumaara ''' lkm = 4 cur.execute( sql, {"lukumaara":lkm})
Kokeile sivun toimintaa Kokeile muuttaa lkm-muuttujan arvoa.
lukumaara-placeholderin nimessä on merkitystä isoilla ja pienillä kirjaimilla. :lukumaara on eri asia kuin :Lukumaara. - Yritä käydä sama kursori läpi silmukalla kahteen kertaan. Jälkimmäisellä kerralla ei pitäisi löytyä mitään. Kursori antaa kertaalleen tietokantakyselyn tulokset ja jos haluaa ne uudelleen on suoritettava execute uudelleen. Samaa kyselyä ei kannata suorittaa useaan kertaan vaan kannattaa ottaa heti talteen mitä tarvitsee.
- Hae tietokannasta kaikki reseptit ja vie ne templatelle. Hae myös kaikki ruokalajit. Tulosta nämä kaikki siististi näkyville www-sivullesi niin voit helpommin testata seuraavia tehtäviä kun sivua uudelleen ladattaessa näet heti mitä muutoksia sisältöön on tullut.
Muuttujia saa sijoittaa kyselyihin vain edellämainitulla tavalla tai käyttäen ?-merkki placeholdereita.! Missään tapauksessa sijoittamista ei saa tehdä normaaleilla merkkijono-operaatioilla. Oikein toimiessa tietokanta pitää itse huolen siitä, että sijoitettavat arvot ovat tarpeen mukaan heittomerkkien sisällä tai ilman. Tietokanta pitää myös huolen, että kenttiin ei tule vääräntyyppistä tietoa. Kyselyjen valmistelu parantaa tietoturvaa SQL Injection -tyyppisiä hyökkäyksiä vastaan.
Lisääminen
Tietojen lisääminen tapahtuu valmisteltujen kyselyjen avulla aivan samaan tapaan kuin kyselyjenkin tekeminen:
- Tee edellisen kohdan koodeista kopio kommentteihin.
- Lisätään tietokantaan uusi resepti. Muuta valmisteltava kysely muotoon:
INSERT INTO resepti (Nimi, Kuvaus, Henkilomaara, RuokalajiID, ReseptiID) VALUES (:nimi, :kuvaus, :henkilomaara, :ruokalaji, :reseptiid)
- Kehittele jokin reseptin nimi ja lyhyt kuvaus (esim. makaronilaatikko) ja luo muuttujat kaikille parametreille. resepti.sql:stä voit katsoa millaisilla ID-arvoilla ruokalajeja on olemassa. Anna ReseptiID:ksi joku sellainen numero mitä ei vielä ole tietokannassa (esim. 10).
- Sido muuttujat kyselyyn ja suorita sovelluksesi lataamalla sivu uudelleen.
- Kokeile suorittaa saman reseptin lisääminen uudelleen. Onnistuuko vai saatko virheilmoituksen?
Ota try..exceptillä kiinni mahdolliset virheet ja tulosta ne lokitiedostoon.
Muistathan, että lisäys ei jää voimaan jollet hyväksy transaktiota:
con.commit()
Muistathan myös, että kahta reseptiä ei voi lisätä samalla reseptiID:llä (perusavaimen eheys!).
Poistaminen
- Tee edellisen kohdan koodeista kopio kommentteihin.
- Poistetaan edellä lisätty resepti. Muuta kysely muotoon
DELETE FROM resepti WHERE reseptiID = :id
- Luo id-muuttuja ja sille arvo, joka löytyy tietokannasta (kts. resepti.sql). Sido muuttuja kyselyyn. Kokeile onnistuuko poistaminen.
- Yritä poistaa ruokalajeja. Onnistuuko se ilman ongelmia? Voitko vapaasti poistaa minkä tahansa ruokalajin?
- Kokeile poistaa kerralla useampia tietueita. Tämä onnistuu kyselyllä:
DELETE FROM taulu WHERE tunniste IN (1, 2, 3)
- Pohdi miten tekisit poistamisen hakusanan perusteella (ei tarvitse tässä toteuttaa).
Päivittäminen
- Kopioi edellinen osuus kommentteihin.
- Muuta kysely muotoon
UPDATE resepti SET henkilomaara = :maara WHERE reseptiID = :reseptiID
- Muuta lisäämäsi tietueen henkilömäärä toiseksi luvuksi.
- Aja kysely napauttamalla selaimen Refresh-painiketta.
- Tarkista, että muutos tapahtui tietokannan komentorivikäyttöliittymästä antamalla SQL-kysely:
SELECT * FROM resepti WHERE reseptiid = 10;
Monimutkaisemmat tulostukset
Kyselyjen kohdistuessa useampaan tauluun voi tulla kiusaus kirjoittaa silmukka, joka tekee useita hyvin samankaltaisia kyselyjä tietokantaan. Tietokanta menee kuitenkin helposti jumiin, jos siihen kohdistuu kyselyjä hirvittävän nopealla tahdilla. Hyvänä perussääntönä voi pitää: minimoi tietokantaan kohdistuvien erillisten operaatioiden määrä. Yksittäisessä kyselyssä kannattaa kuitenkin tietokanta laittaa tekemään kaikki mahdolliset laskutoimitukset ja järjestämiset.
- Listataan kaikki ruokalajit otsikkoina ja otsikon alle ruokalajiin liittyvät reseptit välilyönneillä eroteltuna.
Haetaan ensin kaikki ruokalajit ja niihin liittyvät reseptit.
SELECT ruokalaji.nimi AS ruokalaji, resepti.nimi AS resepti FROM resepti, ruokalaji WHERE ruokalaji.ruokalajiID = resepti.ruokalajiID ORDER BY ruokalaji.nimi, resepti.nimi
- Aja kysely ja käy for-silmukalla se läpi ja tulosta aluksi kultakin riviltä löytyvä ruokalaji h2-elementin sisälle.
- Nyt ruokalaji toistuu turhan moneen kertaan. Tutki onko ruokalaji
sama kuin edellisellä silmukan kierroksella ja tee tulostus vain ensimmäisellä kerralla.
- Huom! Jos meitä kiinnostaisi ylipäätään tietää mille ruokalajeille on
olemassa reseptejä, niin tähän sopisi kysely
SELECT DISTINCT ruokalaji.nimi as ruokalaji FROM ruokalaji, resepti WHERE ruokalaji.ruokalajiID = resepti.ruokalajiID
- Huom! Jos meitä kiinnostaisi ylipäätään tietää mille ruokalajeille on
olemassa reseptejä, niin tähän sopisi kysely
- Nyt halutaan tulostaa vielä reseptit ruokalajin alle. Tulosta reseptin nimi siinä tapauksessa, jos edellisellä kierroksella oli sama ruokalaji.
- Testaa. Huomaa, että myös sillä rivillä, jossa ruokalaji muuttuu on yksi resepti. Tulosta siis myös se.
- Tarkista, että
h2
jap
-elementit tulostuvat järkevästi niin, että koodi pystyy validina.- Tämä voi vaatia hieman erityisjärjestelyjä silmukan ensimmäisen tai viimeisen alkion tapauksessa.
- Tulostus voidaan tehdä myös sisäkkäisillä for-silmukoilla, jolloin erityisjärjestelyjä ei tarvita. Ulommassa tulostetaan ruokalajeja ja sisemmässä reseptejä niin kauan kuin ruokalaji pysyy samana.
- Entäs jos halutaankin listata kaikki ruokalajit? Nythän listattiin
vain sellaiset, joille on määritelty reseptejä. Kyselyä pitää muuttaa seuraavasti:
SELECT ruokalaji.nimi AS ruokalaji, resepti.nimi AS resepti FROM ruokalaji LEFT OUTER JOIN resepti ON ruokalaji.ruokalajiID = resepti.ruokalajiID ORDER BY ruokalaji.nimi, resepti.nimi
- Lisäksi on tarkistettava onko rivillä ruokalaji ja resepti vai pelkkä ruokalaji ja tehtävä tulostus sen mukaisesti.
- Kokeile miten uusittu tulostus toimii.
Transaktiot
Transaktioiden avulla pystytään hallitsemaan tietokantaoperaatiokokonaisuuksia. Esim. tietokantaan pitää syöttää useita tietueita peräkkäin ja jos yksikin lisäys epäonnistuu, niin kaikki lisäykset pitää pystyä peruuttamaan. Transaktio varmistaa, että keskeneräisen transaktion vaikutukset tietokantaan eivät heijastu muille tietokannan käyttäjille kuin vasta transaktion hyväksymisen jälkeen.
- Harjoitellaan transaktioiden toimintaa lisäämällä samalla kerralla uusi ruokalaji ja siihen liittyviä reseptejä. Valmistele kaksi kyselyä, joista toinen lisää ruokalajin ja toinen reseptin.
- Tee lisäys kuten yllä eli luo muuttuja ja sido ne kyselyihin. Aseta reseptin ruokalajiID:ksi sama kuin mikä keksimälläsi ruokalajilla on. Laita reseptin reseptiID:ksi jokin sellainen numero mikä on jo olemassa tietokannassa.
- Kokeile suorittaa kysely. Muista tehdä tarkistukset kyselyiden onnistumiselle (try...except). Nyt pitäisi tulla virheilmoitus reseptin lisäyksen
epäonnistumisesta. virheen tapahtuessa tee rollback eli peruuta jo tehdyt muutokset:
con.rollback()
Tarkista tietokannan komentorivikäyttöliittymästä onnistuiko ruokalajin lisäys.sqlite-kirjastossa on oletuksena transaktiot käytössä koko ajan. Esim. PHP:lla toimittaessa transaktio pitäisi käynnistää erikseen koska oletuksena PHP:n PDO-rajapinnassa jokainen SQL-lause commitoidaan heti.
- Lisää laskuri virheiden määrälle, joka on aluksi nolla.
- Jos SQL-lauseiden suorituksessa tulee virhe, niin lisää virhelaskurin määrää.
- Kaikkien SQL-lauseiden lopussa tee tarkistus onko tullut virheitä. Jos virheitä ei ole tullut, niin suorita con.commit() ja muussa tapauksessa con.rollback().
- Poista viime testissä lisätty ruokalaji komentoriviltä SQL-lauseella
DELETE FROM Ruokalaji WHERE ruokalajiid = 123; # keksimäsi ruokalaji-id numeroksi
- Kokeile nyt ohjelmaa. Tuliko ruokalaji takaisin vai peruuntuiko koko operaatio?
Automaattiset avainkentät
Usein käytetään avaimina kenttiä, joiden arvon keksiminen jätetään tietokannan harteille. Esim. reseptiID ja ruokalajiID ovat tällaisia kenttiä. Tähän saakka ne on keksitty itse, mutta entäs jos tietokanta keksii ne?
- Muutetaan edellistä lisäystä hieman. Poista kyselyistä reseptin INSERT-kyselystä reseptiID ja ruokalajin lisäyksestä ruokalajiID.
- Mistä nyt tiedämme mikä on reseptin ruokalajin id-numero?
Tämän saa selville pyytämällä cursorilta:
ruokalajiID = cur.lastrowid
saman voi myös kysyä kyselynä:
SELECT last_insert_rowid()
- Kokeile ohjelmaa. Saat luultavasti virheen, miksi? Korjaa ruokalajin tietoja niin, että lisäys onnistuu.
Lisätietoa
Kannattaa ehdottomasti tutustua Philip Greenspunin kirjaan: SQL for Web Nerds
Tietojen validointi
Luo edellä olevaa kuvaa vastaava Flask-sivu. Voit käyttää valmista pohjaa.
Lisää tarkistus jossa varmistetaan, että etunimi ja sukunimi on täytetty ja tiedekunta on valittu. Tee tarkistus vain jos lomake on täytetty eli tarkistusta ei pidä tehdä sivulle ensimmäistä kertaa tultaessa.
- Lisää virheilmoitukset yhteen virhesanakirjaan, jossa avaimena käytetään virheellisen kentän nimeä ja arvona virheilmoitustekstiä.
-
# luodaan dict kentat = {"etunimi":"","sukunimi":"","tdk":""} errors = dict(kentat) #tekee kopion samoilla avaimilla if request.method == 'POST': #varmistetaan, että lomake on lähetetty for k in errors: try: kentat[k] = request.form[k] except KeyError: errors[k] = "!"
- Muodosta python-sanakirja (dict) tiedekunnista ja vie se templatelle ja luo sen perusteella näkymässä olevan alasvetovalikon sisältö.
Käytä avaimena tiedekunnan numeroa.
tiedekunnat = {0:"Valitse tiedekunta", 1:"Humanistinen tiedekunta",2:"Informaatioteknologian tiedekunta", 3:"Kasvatustieteiden tiedekunta", 4:"Liikunta- ja terveystieteiden tiedekunta", 5:"Matemaattis-luonnontieteellinen tiedekunta", 6:"Taloustieteiden tiedekunta", 7:"Yhteiskuntatieteellinen tiedekunta"}
Huomaa, että dictissä olevat avaimet ovat kokonaislukuja ja lomakkeelta saadut tiedot ovat aina merkkijonoja.
- Jos tiedoissa oli virheitä, niin tulosta virhe-ilmoitus seuraavilla tavoilla:
- Ensimmäinen versio: Jos virheitä on niin tulosta yleinen virheilmoitus.
- Toinen versio: Käy läpi kaikki virhetaulukon virheet ja tulosta ne lomakkeen alussa.
- Kolmas versio: Tulosta kunkin lomake-elementin viereen siihen liittyvä virheilmoitus.
- Aseta kuhunkin lomakkeen kenttään oletusarvoksi edellisellä kerralla syötetty teksti tai valinta
- Tekstikentissä, joihin pitäisi syöttää viikkotehtävien pisteet, ei ole vielä
name
-ominaisuutta asetettu. Mitäname
-ominaisuuksiin pitäisi laittaa, että saat kaikki viikkotehtäväpisteet varmasti oikein ja kätevästi käsiteltäväksesi? Huomaathan, että selain voi lähettää lomakkeen kenttien arvot satunnaisessa järjestyksessä. - Tarkista, että viikkotehtäviin on laitettu numero. Tarkista, että numero on väliltä 0-5. Älä kuitenkaan herjaa tyhjästä tekstilaatikosta. Kokeile tehdä tarkistus siten, että käyt yhdessä silmukassa läpi kaikki viikkotehtävien syötteet. Koko ryhmää koskeva virheilmoitus riittää toistaiseksi.
Autentikointi
Käyttäjän kirjautuminen ja autentikointi järjestelmään tehdään sessioiden avulla seuraavalla tavalla:
- Täytyy ensimmäisenä luoda sivu jolla voi kirjautua sisään. Tee uusi sivu (/kirjaudu) ja sijoita sivulle kirjautumislomake jolla kysytään tunnus ja salasana
- Tarkista sivulla, että käyttäjätunnus on syötetty ja salasanaksi on syötetty "testi". Salasanan vertailu on tehtävä seuraavalla tavalla:
import hashlib m = hashlib.sha512() avain = "omasalainenavain" m.update(avain) m.update(salasana) if len(tunnus) and m.digest() == "6n\x90\xb5\xfe)\xa9\xd9\xc1B\n\xfa3LK\x19\xc4\xd6=\xcd \x0fBKz\x9f\xe32\x8a5-\xa5\x81\x8f\xc0<\xff\xa4c\xc26-\xb3S[a-\xf4\xeb'\xdf3\xd4r\x0f\xbfY)dW\x1a\xd7W.": # jos kaikki ok niin asetetaan sessioon tieto kirjautumisesta ja ohjataan laskurisivulle session['kirjautunut'] = "ok" return redirect(url_for('laskuri')) # jos ei ollut oikea salasana niin pysytään kirjautumissivulla. return render_template('kirjaudu.html')
Selkokielistä salasanaa ei siis vertailla vaan salasanasta jollakin algoritmilla laskettua merkkijonoa. Testi-salasanaa vastaava merkkijono on tuotettu seuraavalla tavalla. Oikean salasanan vertailu pitää tehdä samalla tavalla jolloin voi verrata onko m.digest-funktion palauttama merkkijono sama kuin haluttu.
>>> import hashlib >>> m = hashlib.sha512() >>> m.update("omasalainenavain") >>> m.update("testi") >>> m.digest() "6n\x90\xb5\xfe)\xa9\xd9\xc1B\n\xfa3LK\x19\xc4\xd6=\xcd \x0fBKz\x9f\xe32\x8a5-\xa5\x81\x8f\xc0<\xff\xa4c\xc26-\xb3S[a-\xf4\xeb'\xdf3\xd4r\x0f\xbfY)dW\x1a\xd7W."
- Kokeile toimiiko kirjautuminen eli ohjaudutko laskurisivulle jos syötät oikean salasanan
- Tee myös logout-sivu, joka poistaa sessiosta kirjautunut-avaimen ja ohjaa sen jälkeen kirjaudu-sivulle
- Vielä tarvitaan oma decorator, jolla saadaan helposti yhdistetty kirjautumisvaatimus halutuille sivuille. Vrt. app.routes.
Lisää sivun alkuun:
from functools import wraps
Kirjoita seuraavanlainen funktio:def auth(f): ''' Tämä decorator hoitaa kirjautumisen tarkistamisen ja ohjaa tarvittaessa kirjautumissivulle ''' @wraps(f) def decorated(*args, **kwargs): # tässä voisi olla monimutkaisempiakin tarkistuksia mutta yleensä tämä riittää if not 'kirjautunut' in session: return redirect(url_for('kirjaudu')) return f(*args, **kwargs) return decorated
- Kokeile nyt käyttää omaa @auth-decoratoria. Lisää se esim. laskuri-funktion eteen:
@app.route('/laskuri', methods=['POST','GET']) @auth def laskuri():
- Kokeile mennä selaimella laskurisivulle. Ohjaudutko kirjautumiseen? Jos et niin muista poistaa vanha kirjautuminen logout-sivulla ja kokeile sen jälkeen uudelleen.
Käyttäjien kommentit