Python Panoráma 9: Dinamikus webalkalmazások PostgreSQL + Flask + Heroku19 perc olvasás

A negyedik és nyolcadik részben egy statikus weboldalt készítettünk. Most azt nézzük meg, hogyan lehet egy dinamikus weboldalt létrehozni és abba különféle funkciókat beépíteni. Manapság a legtöbb weboldal rendelkezik valamilyen dinamikus jellemvonással. Például a felhasználói fiókok kezelése, személyes adatok tárolása is erre az útra vezet. Nézzük hát meg hogyan is lehet egy adatbázist hozzárendelni weboldalunkhoz.

CÉL

A sorozat egyes részeiben már futólag megismerkedtünk mind adatbázisokkal a könyvtár programunk során, mind weboldalak létrehozásával a Flask és a Heroku felhőszolgáltatása révén. Ezt a két tudást most ötvözni szeretnénk, úgy hogy közben azt megfűszerezzük egy kis HTML-el illetve CSS-el. Célunk tehát ebben a részben az, hogy felállítsunk egy olyan webalkalmazást, amelynek kezdőoldalán ha a felhasználó megadja az email címét, testmagasságát, illetve testtömegét, akkor ezek alapján az adott személy BMI-je (testtömegindex) kiszámításra kerüljön. Ezután egy email formájában ezt az információt továbbítani szeretnénk a kitöltő email címére. Az emailben szeretnénk, hogy a kalkulált BMI, valamint a kitöltők BMI átlaga is szerepeljen.

MEGVALÓSÍTÁS

A webalkalmazásunk elkészítését a következő szakaszokra bonthatjuk:

1. Front-end

– Honlap
– Továbbítás
– CSS

2. Back-end

– Virtuális környezet
– Flask
– PostgreSQL
– email küldés

3. Heroku

– Heroku app és adatbázis
– requirements.txt, Procfile, runtime.txt
– Git

1. Front-end

Kezdésként két html fájlt kell létrehoznunk. Az egyik a honlap lesz, ahol a felhasználó meg tudja majd adni az adatait, pontosabban az email címét, testmagasságát és testtömegét. A másik pedig az az oldal lesz, amelyre továbbítja programunk a látogatót, abban az esetben, ha a beírt adatok megfelelnek. Ebből eredendően az első html fájlnak alapvetően az alkalmazásunk célját és megnevezését, egy rövid útmutatást, a három adatbeviteli rublikát és egy “küldés” gombot kell tartalmaznia. A második fájl tartalma egy rövid szöveges visszajelzés lesz a felhasználónak, hogy adatai sikeresen eljutottak hozzánk és hamarosan választ kap tőlünk egy email formájában. A két html fájlhoz rendeljünk egy CSS fájlt is, amely azokat szépen megformázza.

A front-end igazán lényeges része a honlap html kódja (index.html) lesz, ezért ezt nézzük meg kicsit részletesebben is.

<!DOCTYPE html>
<html lang="en">
  <title>Data Collector App</title>
  <head>
    <link href="../static/main.css" rel="stylesheet"
  </head>
  <body>
    
<div class="container">
      
<h1>Biological Data Collection</h1>

      
<h3>Fill out the form to recieve your BMI.</h3>

        
<div class="message"> {{text | safe}} </div>

        
<form action="{{url_for('success')}}" method="POST">
          <input title="email address" placeholder="email address" type="email" name="email_name" required> 
          <input title="Height" placeholder="Height in cm" type="number" min="50", max="300" name="height_name" required> 
          <input title="Mass" placeholder="Bodymass in kg" type"number" name="mass_name" min="10" max="700" required> 
          <button type="submit"> Submit </button>
        </form>

      </div>

    </body>
</html>

Láthatjuk, hogy a kódunk lényegét egy form tartalmazza. Ebben az egyes input objektumokat elnevezzük, megadjuk a típusukat, valamint az alsó, felső korlátjukat is. Azt is láthatjuk, hogy a form-hoz egy eseményt is rendeltünk. Ez azt jelenti, hogy ha minden paraméter megfelel, akkor a felhasználót továbbítja a success.html-re. Az input objektumoknál a name paraméter, illetve a form objektumnál a method=”POST” elem a Flask applikációnkhoz szükséges. Ennek köszönhetően tudjuk majd a beírt input értékeket egy-egy változóban elmenteni.

A success.html és a CSS fájl tartalma inkább formai tényező. Az én verzióm itt érhető el. Miután megvannak a front-end-hez szükséges fájlok, a negyedik részben megismert módon rendezzük el őket a static és templates mappákba.

2. Back-end

A back-end építése során a Flask-et fogjuk használni. A Flask-el a negyedik Python Panoráma részben ismerkedhettünk meg, mikor egy webalkalmazást állítottunk fel egy Heroku szerveren. Ha nem tiszta valamilyen részlete a folyamatnak, akkor csupán fusd át a negyedik részt.

Virtuális környezet

A back-end elkészítését indítsuk azzal, hogy létrehozunk egy virtuális környezetet a Python-nak. Ezt a “python -m venv virtual” paranccsal érhetjük el a Command Line-ban. Ezután telepítsük a virtuális környezetbe a szükséges package-eket: flask, gunicorn, psycopg2, SQLAlchemy.

Flask

Ezt követően készítsünk el egy egyszerű flask applikációt, amely az webalkalmazásunk gerincét fogja képezni. Ennek a következőképpen kell kinéznie az első körben:

from flask import Flask, render_template, request

app=Flask(__name__)

@app.route('/')
def index():
    return render_template("index.html")

@app.route('/success', methods=['POST'])
def success():
    if request.method=='POST':
        email=request.form["email_name"]
        height=request.form["height_name"]
        mass=request.form["mass_name"]
        bmi=float(mass)/(float(height)/100)**2
        bmi_rounded=round(bmi,2)
        print(email, height, mass, bmi, bmi_rounded)
        return render_template("success.html")

if __name__ == '__main__':
    app.debug=True
    app.run()

Ha ezt a Flask applikációt futtatjuk, akkor ha adatokat viszünk be az index.html-en található input rubrikákba, majd azt elküldjük, akkor átugrunk a success.html-re. Azonban ez nem olyan egyértelmű, mint az hangzik, ugyanis ehhez szükséges a “POST” method-öt deklarálni. (alapértelmezetten get method-ként értelmezi a flask az app.route-ot, így csak akkor tudnánk, meglátogatni azt, ha beírnánk annak URL-jét a böngésző címsávjába)

Tehát a scriptünkben azt látjuk, hogy csak akkor kapjuk vissza a success.html-t és akkor tárolódnak el a bevitt adatok, ha a “request.method” “POST”. A “request.form” egy olyan method, amely egy szótárszerű objektumot eredményez, ahol a html fájl input elemeinek neve a kulcsok, az értékek pedig a beírt adatok. A bmi változót a képletének megfelelően kiszámoljuk, majd két tizedesjegyre felkerekítjük. (a képletben szereplő mértékegységek kilogramm és méter)

PostgreSQL

Flask applikációnkat most egy adatbázissal is ki fogjuk bővíteni, hogy a felhasználók által megadott adatokat valahol megfelelően el tudjuk tárolni. Ehhez a PostgreSQL-t fogjuk használni, amely innen tölthető le. Telepítése után indítsuk el a pgadmin 4-et, majd jelentkezzünk be és hozzunk létre egy új adatbázist.

Ezután egy táblát is létre kell hoznunk az adatbázisban, de ezt már a Python-on keresztül tesszük meg. Ehhez elsőként importáljuk az SQLAlchemy-t. Ahhoz, hogy az alkalmazásunk egy adatbázishoz tudjon kapcsolódni először létre kell hoznunk egy változóban (db) egy SQLAlchemy objektumot, amely a flask applikációnkat tartalmazza az “app” változóként. Emellett viszont azt is meg kell adnunk, hogy ez az adatbázis címen érhető el. Ezt az “app.config” és az adatbázis URI-jének megadásával tehetjük meg.

Ha ezzel megvagyunk akkor egy class-ba foglalva megadhatjuk a programunknak, hogy hogyan épüljön fel az adatbázisunk új táblája, tehát például milyen mezők szerepeljenek benne. Ezt követően aktiváljuk a python környezetünket a Command Line-ban a “virtual\Scripts\activate” paranccsal, majd futtassuk a python-t. Ha minden rendben van és gond nélkül fut a python, akkor importáljuk a “db”-t a python scriptünkből (mywebapp.py). Majd alkalmazzuk a db.create_all() method-öt. Ezzel elvileg létre is hoztuk a táblánkat az adatbázisban. Ezt mindenképpen ellenőrizzük is le a pgadmin 4-ben.

from flask import Flask, render_template, request
from flask_sqlalchemy import SQLAlchemy


app=Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI']='postgresql://yourusername:yourpassword@localhost/yourdatabasename'
db=SQLAlchemy(app)

class Data(db.Model):
    __tablename__="data"
    id=db.Column(db.Integer, primary_key=True)
    email_=db.Column(db.String(120), unique=True)
    height_=db.Column(db.Integer)
    mass_=db.Column(db.Integer)
    bmi_=db.Column(db.Float)

    def __init__(self, email_, height_,mass_,bmi_):
        self.email_=email_
        self.height_=height_
        self.mass_=mass_
        self.bmi_=bmi_

@app.route('/')
def index():
    return render_template("index.html")

@app.route('/success', methods=['POST'])
def success():
    if request.method=='POST':
        email=request.form["email_name"]
        height=request.form["height_name"]
        mass=request.form["mass_name"]
        bmi=float(mass)/(float(height)/100)**2
        bmi_rounded=round(bmi,2)
        print(email, height, mass, bmi, bmi_rounded)
        data=Data(email,height,mass,bmi)
        db.session.add(data)
        db.session.commit()
        return render_template("success.html")

if __name__ == '__main__':
    app.debug=True
    app.run()

Most, hogy van egy adatbázisunk és benne egy táblánk, már csak azt kell megadnunk a programunknak, hogy mikor a felhasználó elküldi a beírt adatait, akkor az a táblánk egy új soraként jelenjen meg. Ekkor használjuk fel a Data class-t, amelynek az inicializációja során megtörténnek az értékadások, tehát az input rubrikákból a request method-ökön keresztül kiolvasott értékek átkerülnek egy rekordba. Végül pedig ezt az új rekordot bevisszük a “data” táblánkba.

previous arrow
next arrow
Slider

Folytatásként azt próbáljuk megakadályozni, hogy adatbázisunk több ugyanahhoz az email címhez tartozó adatokat is tartalmazhasson. Azaz azt szeretnénk, hogy egy email címmel csak egy adatmegadás történhessen. Ehhez egy feltételes blokkot kell betűznünk a kódunkba, amely egy lekérdezés (query) alapján megvizsgálja, hogy a táblánk tartalmaz-e olyan email címet, amelyet a felhasználó megadott. Ha nem, akkor a “data” táblánk kibővül egy sorral, ha viszont igen, akkor pedig alkalmazásunknak tájékoztatnia kell a felhasználót arról, hogy a megadott email címet már felhasználták egyszer. Ezeknek megfelelően a flask applikációnk “success” függvénye így módosul:

def success():
    if request.method=='POST':
        email=request.form["email_name"]
        height=request.form["height_name"]
        mass=request.form["mass_name"]
        bmi=float(mass)/(float(height)/100)**2
        bmi_rounded=round(bmi,2)
        print(email, height, mass, bmi, bmi_rounded) #This is just for helping us to see that the program is running correctly.
        if db.session.query(Data).filter(Data.email_==email).count()==0:
            data=Data(email,height,mass,bmi)
            db.session.add(data)
            db.session.commit()
            return render_template("success.html")
    return render_template("index.html", text="This email address was used already.")

Ha jobban megnézzük akkor láthatjuk, hogy az utolsó sorban a text paraméter megjelent az index.html fájlnál egy Jinja helyőrzőben is: {{text | safe}}. Így ha a felhasználó már egy előzőleg felhasznált email címmel akar adatokat megadni, akkor nem a success.html-re fog eljutni, hanem az ezzel a “text” elemmel kibővített index.html honlapra.

Email küldés

Most, hogy idáig eljutottunk nézzük meg, hogyan lehet emaileket küldeni egy python script segítségével. Ehhez készítsünk egy új scriptet (email_sender.py). Ennél a scriptnél nincs más dolgunk, mint egy külön erre a célra létrehozott emailfiókhoz csatlakozni és abból automatikus üzeneteket továbbítani azokra az email címekre, amelyek bekerültek az adatbázisunkba.

Az üzenetünkben alapvetően három adatot szeretnénk megosztani a felhasználóval. Egyszer a testmagassága és tömege alapján kiszámított BMI értékét, másodszor a kitöltők átlagos BMI értékét, harmadszor pedig azt, hogy hány ember töltötte ki ezt a form-ot. Azonban ezekből még csak az elsőt tudjuk megadni, ezért módosítsuk a success függvényünket és bővítsük ki azt a következő sorokkal:

def success():
    if request.method=='POST':
        email=request.form["email_name"]
        height=request.form["height_name"]
        mass=request.form["mass_name"]
        bmi=float(mass)/(float(height)/100)**2
        bmi_rounded=round(bmi,2)
        if db.session.query(Data).filter(Data.email_==email).count()==0:
            data=Data(email,height,mass,bmi)
            db.session.add(data)
            db.session.commit()
            avarage_bmi=db.session.query(func.avg(Data.bmi_)).scalar()
            avarage_bmi=round(avarage_bmi,2)
            count=db.session.query(Data.bmi_).count()
            send_email(email,bmi_rounded, avarage_bmi, count)
            return render_template("success.html")
    return render_template("index.html", text="This email address was used already.")

Ahhoz, hogy ezek a sorok működőképesek is legyenek importálnunk kell egy SQLAlchemy modult.  Így hát a script elejére írjuk be a “from sqlalchemy.sql import func” sort. Most már ismert minden az üzenetünkhöz szükséges adat, így elkezdhetjük írni magát az üzenetküldő scriptünket.

Első lépésként importáljuk a szükséges könyvtárakat. Ezt követően definiáljuk a send_email függvényünket, amelyet a flask applikációnkban majd meg szeretnénk hívni. A függvény definiálása során meg kell adnunk azt az email címet, amelyről küldeni szeretnénk az üzeneteket, annak jelszavát, azt az email címet, amelyre küldeni szeretnénk az üzenetet (ez mindig az “email” változó aktuális értéke lesz), a tárgyat, magát az üzenetet az adatokkal végül pedig az emailfiókhoz kapcsolódás módját. A mi esetünkben egy gmail fiókhoz szeretnénk kapcsolódni, így itt arra kell figyelnünk, hogy mikor létrehozzuk az emailfiókot, akkor a biztonsági beállításoknál kapcsoljuk be az “Allow less-secure apps” opciót.
A végső üzenetküldő scriptünknek pedig valahogy így kell kinéznie:

from email.mime.text import MIMEText
import smtplib

def send_email(email, bmi_rounded, avarage_bmi, count):
    from_email="youraccountname@gmail.com"
    from_password="yourpasswordhere"
    to_email=email

    subject="Your Calculated BMI. Thank you for participating!"
    message="Hello! Your BMI is &amp;amp;amp;amp;lt;strong&amp;amp;amp;amp;gt;%s&amp;amp;amp;amp;lt;/strong&amp;amp;amp;amp;gt;. 
 Avarage BMI of %s people participating in this survey is &amp;amp;amp;amp;lt;strong&amp;amp;amp;amp;gt;%s&amp;amp;amp;amp;lt;/strong&amp;amp;amp;amp;gt;." % (bmi_rounded, count, avarage_bmi)

    msg=MIMEText(message,'html')
    msg['Subject']= subject
    msg['To']=to_email
    msg['From']=from_email

    gmail=smtplib.SMTP('smtp.gmail.com',587)
    gmail.ehlo()
    gmail.starttls()
    gmail.login(from_email,from_password)
    gmail.send_message(msg)

Ezután csak importálnunk kell ezt a függvényt a flask applikációnkba, hogy azt meg tudjuk hívni, mikor új adatokat küld egy felhasználó. Tehát akkor ez a sor megy a mywebapp.py elejére: “from email_sender import send_email”

3. Heroku

Heroku app és adatbázis

Ahhoz, hogy alkalmazásunkat fel tudjuk tenni az internetre szükségünk van a Heroku felhőszolgáltatására, amellyel részletesebben megismerkedtünk a negyedik részben. Első lépésként lépjünk be a Heroku fiókunkba a Command Line-on keresztül a Heroku Toolbelt segítségével, majd hozzunk létre egy új app-et.

heroku login
heroku create yourappnamehere

Bár a flask applikációnk jól működik, azonban van vele egy kis probléma, ugyanis mikor mi futtattuk azt, akkor végig egy olyan adatbázishoz kapcsolódtunk, amely helyileg elérhető volt. Ahhoz viszont, hogy ebből egy webalkalmazást tudjuk létrehozni szükségünk van egy Heroku adatbázisra. Szerencsénkre ezt egy addonként hozzá is tudjuk adni a Heroku app-ünkhöz.

heroku addons:create heroku-postgresql:hobby-dev --app yourappnamehere

Most, hogy van egy adatbázisunk már csak annak az útvonalát kell megadnunk a flask alkalmazásunkban. Ehhez írjuk be továbbra is a Command Line-ba a “heroku config –app yourappnamehere” sort, majd a megkapott adatbázis URL-t másoljuk át a flask applikációnkba, úgy hogy az URL végéhez illesztjük csak simán azt, hogy “?sslmode=require”. Ennek valahogy így kell kinéznie:

heroku config --app yourappnamehere
app.config['SQLALCHEMY_DATABASE_URI']='postgres://username:password@address:portnumber/databasename?sslmode=require'

requirements.txt, Procfile, runtime.txt

A negyedik részhez hasonlóan itt is létre kell hoznunk a weboldalunk felállításához szükséges három fájlt. A requirements.txt-be kerülnek a szükséges package-ek, a Procfile-ba a gunicorn (webszerver), valamint a flask applikációnk, a runtime.txt-be pedig az aktuális runtime érték, amely a Heroku oldalán érhető el.

Git

Következő lépésként a git segítségével egy repository-hoz hozzáadjuk a fájljainkat és azt feltöltjük egy Heroku szerverre. Ezelőtt azonban létre kell hoznunk egy “.gitignore” fájlt, amellyel meg tudjuk adni azoknak a fájloknak a nevét a projektmappánkban, amelyeket nem szeretnénk feltölteni. Ilyen például __pycache__ és a virtual, amely a virtuális python környezetünket tartalmazza. A “.gitignore” kiterjesztésű fájlt csak trükkösen tudjuk létrehozni. Nyissunk meg egy  Command Line ablakot és egy szövegszerkesztővel (például notepad) nyissuk meg a “.gitignore” nevű fájlt. Mivel nem létezik ilyen nevű fájl, ezért az megkérdez minket, hogy létrehozzon-e egy ilyen fájlt. Mondjuk, hogy igen és írjuk bele azoknak a fájloknak a nevét, amelyeket nem szeretnénk feltölteni.

Ezután a Command Line-ban a szokásos heroku és git parancsokkal végezzük el a fájlok feltöltését a szerverre:

heroku login
git init - a projektmappában kell megnyitunk a Command Line-t és ezt a parancsot futtatnunk
git add . - hozzáadja az aktuális könyvtár tartalmát egy git repository-hoz
git commit -m "Deployment of webapplication"
heroku git:remote --app yourappnamehere
git push heroku master

Az utolsó parancsot követően a Heroku szerver telepíti a megadott package-eket és megpróbálja futtatni a webalkalmazásunkat. Ha ez sikeres, akkor látogassuk meg a weboldalunkat a heroku open paranccsal. (URL: yourappnamehere.herokuapp.com) Ha kipróbáljuk, azt tapasztalhatjuk, hogy hiába küldjük el adatainkat, nem tudjuk elérni a success.html-t.

Ez azért van, mert a Heroku adatbázisunkban, még nem hoztuk létre a “data” táblánkat. Ehhez elsőként vissza kell mennünk a Command Line-ba és a Heroku szerveren futtatni a python-t a “heroku run python” parancs segítségével. Majd az előzőkhez hasonlóan itt is importálnunk kell a “db”-t a python scriptünkből és a db.create_all() sorral létrehoznunk a Heroku adatbázisban a “data” táblát. Ezután kiléphetünk az interaktív python shell-ből az “exit()”-el.

Ha ezeket mind sikerült kiviteleznünk, akkor alkalmazásunk már készen is áll, hogy adatokat gyűjtsünk azoktól és adatokat szolgáltassunk azoknak, akik meglátogatják webalkalmazásunkat. A python scriptünk végső verziója itt érhető el.

TOVÁBBFEJLESZTÉS ÉS ÜZLETI FELHASZNÁLÁS

Ebben a részben egy igen tanulságos alkalmazást hoztunk létre, azonban aki szeretné az még tovább tudja fejleszteni. Egyrészt lehet külsőleg is csinosítani alkalmazásunkat, másrészt viszont kibővíthetjük azt további funkciókkal. Például érdemes lenne, arról is visszajelzést adnunk, hogy a kiszámított BMI érték az milyen kategóriába esik(enyhe soványság, normál testsúly, túlsúly, stb.).

Egy mégérdekesebb projekt lehetne mondjuk, ha ezt alkalmazást úgy tudnánk átalakítani, hogy a megadott biológia és életviteli adatok alapján az adott személynek egy étrendet vagy igény esetén edzéstervet tudnánk javasolni. Ehhez mindenképpen érdemes lenne egy dietetikust is bevonni a projektünkbe. Szemmel láthatóan már ebbe az irányba is sok lehetőséget tudunk felltárni, így ez az alkalmazás is egy jó alapnak bizonyulhat az üzleti felhasználás szempontjából.
A részhez tartozó fájlok innen érhetők el.

Köszönöm, hogy végigolvastad a posztomat és ha érdekesnek találtad, akkor nézz bele a sorozat többi részébe is.

 

Gulácsy Dominik

About Dominik Gulácsy

Sophomore at Corvinus University of Budapest studying International Business who is motivated to use relevant academic knowledge to solve problems through optimisation. Dedicated to fully support the development of new business solutions in close collaboration with team members by IoT and data science applications. Gained experience in SQL and VBA but looking forward to learning more. A keen supporter of the circular economy.

View all posts by Dominik Gulácsy →

Leave a Reply

Your email address will not be published. Required fields are marked *