3. Weather app rakendus

OpenWeatherMap on ilmaennustust ja hetkese ilma andmeid pakkuv veebileht. Sellel lehel on väga hea ja lihtne API, mille kaudu on võimalik igal arendajal luua oma ilmarakendus.

Käesolevas juhendis loome samm-sammult algelise ilmaandmeid kuvava rakenduse "skeleti". Koodinäidete põhjal loodud toimiv, kuid kohati poolik versioon weather appist on kättesaadav 4. peatükis. Parema lõpptulemuse saavutamiseks tasub tähele panna punasega välja toodud soovitusi ning nende põhjal iseseisvalt rakendust täiendada.

Juhendist aitavad paremini aru saada algteadmised järgnevates teemades:

  • API

  • HTTP request

  • Python

  • HTML, CSS

  • SQL (väga algeline)

3.1. API võtmed ja config

API võtmeid kasutatakse näiteks päringute arvu piiramiseks või tasuliste teenuste ligipääsu haldamiseks. OpenWeatherMapist ilmaandmete saamiseks on vaja konkreetse kasutajaga seotud API võtit, mis tuleb iga päringuga kaasa anda. Tasuta pakett võimaldab ühel kasutajal teha kuni 1000 API päringut päevas.

  1. Loo tasuta konto OpenWeatherMapis.

  2. Vali ülemiselt menüüribalt "API keys" või kasuta seda linki, et kontoga seotud API võtmeid hallata.

  3. Vaikimisi peaks olema igale kasutajale genereeritud Default nimeline API key, mis näeb välja umbes selline: "5g6a09fa42d2285ed29f78bf7842aai7". Soovi korral on võimalik rohkem API võtmeid genereerida, kuid nende aktiveerumine võib veidi aega võtta.

  4. Lisa API võti oma projekti config faili. Nii on kõige lihtsam salajast infot muust projektist eraldi hoida, kuid vajadusel siiski mugavalt kasutada.

Loo projekti juurkausta config.py fail.

Mall config.py

class Config:
    OPEN_WEATHER_API_KEY = "<sinu API võti>"

Lisa config.py ka .gitignore faili

# Private API keys
config.py

# Other .gitignore rules
# ...

Soovitus: Miks me lisasime äsja loodud faili .gitignorei? Üldjuhul on ükskõik milliste privaatsete API võtmete või muu salajase info Giti versioonihalduses hoiustamine halb tava. Sellega kaasneb uus probleem. Kuidas teavad teised meeskonnas, et milline peaks olema config.py fail? Selle probleemi lahendamiseks loome uue faili nimega config.example.py, millesse kopeerime algse faili sisu ning eemaldame kõik võtmed. Näitefaili saame koos muu koodiga giti panna, vältides turvaaukude tekitamist.

  1. Muudame app.py faili, et Flask saaks aru, millist config faili lugeda

from flask import Flask
from flask import render_template

app = Flask(__name__)
app.config.from_object('config:Config')

@app.route('/')
def hello_world():
    api_key = app.config['OPEN_WEATHER_API_KEY']
    app.logger.info(api_key)
    return render_template('index.html')


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

Soovitus: Pane rakendus käima ning võid leida, et su konsoolis on API võti välja logitud. Proovi API võti teha nähtavaks ka veebilehel. Selle probleemi lahendamiseks võid vastuseid leida Googlest.

Soovitus: Rakenduse config on igal pool kättesaadav. Selleks, et rakenduse configi lugeda mõnes muus failis peale app.py:

  • Impordi from flask import current_app

  • Kasuta current_app.config["<YOUR-CONFIG-FIELD-NAME>"]

3.2. API kasutamine

OpenWeatherMap API dokumentatsioon on kättesaadav siit.

Soovime saada praeguse hetke ilmaandmeid mingi kindla asukoha kohta. Selleks kasutame dokumentatsioonist leitud endpointi:

https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={api_key}

Nagu näha siis endpoint nõuab kolme parameetrit (lat, lon, API key). Soovitus: Sa võid nendest parameetritest mõelda nagu funktsiooni parameetritest Pythonis.

Loome uue faili openweather.py ning proovime seal selle funktsiooni realiseerida.

Mall openweather.py

import requests

def weather_data(lat: int, lon: int, api_key: str) -> dict:
    """
    Retrieves weather data from the OpenWeatherMap API based on latitude and longitude coordinates.

    Args:
        lat (int): The latitude coordinate.
        lon (int): The longitude coordinate.
        api_key (str): The API key for accessing the OpenWeatherMap API.

    Returns:
        dict: A dictionary containing the weather data.

    """
    url = f"https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={api_key}"
    response = requests.get(url)
    return response.json()

Mall app.py:

from flask import Flask
from flask import render_template
from openweather import weather_data

app = Flask(__name__)
app.config.from_object('config:Config')

@app.route('/')
def hello_world():
    data = weather_data(40.7128, -74.0060, app.config['OPEN_WEATHER_API_KEY'])
    return render_template('index.html', data=data)


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

Mall index.html

{% extends 'base.html' %}

{% block title %}
Home
{% endblock %}

{% block content %}
{{ data }}
{% endblock %}

Soovitus: Ilmselt ei lähe see rakendus enam tööle ning näitab errorit. Põhjuseks on pip package requests, mis on puudu sinu projektis. Lae see endale alla ning ära unusta ka pakke freezida. Loe selle kohta täpsemalt siit.

Nüüd peaks su koduleht sisaldama andmeid New Yorki ilma kohta.

3.3. Vaated

Veebilehtedel on üldjuhul mitu erinevat vaadet, kuhu navigeerida saab. Selles juhendis näitame, kuidas seda Flaskiga teha ning loome eraldi lehe ilmaandmete jaoks.

  1. Loome uue faili weather.html templates kausta ning kasutame sama sisu nagu index.html kasutab.

  2. Muudame index.html faili ning lisame lingi, mis viitab meie ilmaandmete lehele, mis asub URLil /weather

Mall index.html

{% extends 'base.html' %}

{% block title %}
Home
{% endblock %}

{% block content %}
<h1>Welcome to weather app!</h1>
<a href="{{ url_for('weather') }}">Click to see weather raport</a>
{% endblock %}

Soovitus: Genereerisime href jaoks URLi kasutades Flaski url_for() meetodit. Uuri selle kohta lähemalt siit.

  1. Muudame app.py faili ning lisame sinna uue vaate jaoks vajaliku URLi /weather

Mall app.py

from flask import Flask
from flask import render_template
from openweather import weather_data

app = Flask(__name__)
app.config.from_object('config:Config')

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

@app.route('/weather')
def weather():
    data = weather_data(40.7128, -74.0060, app.config['OPEN_WEATHER_API_KEY'])
    return render_template('weather.html', data=data)

if __name__ == '__main__':
    app.run()
  1. Nüüd saame navigeeruda kodulehelt ilmaandmete lehele, kuid ilmaandmete lehel on alati ühe ja sama asukoha andmed (New York). Selle probleemi lahendame järgmises peatükis.

Soovitus: Proovi luua veel vaateid erinevate linnade jaoks. Selleks loo veel erinevaid funktsioone app.py failis. Ära unusta ka neile vaadetele linke lisada oma index.html failis.

3.4. Otsinguparameetrid

Soovime lisada veebilehele otsingumootori, mille abil on võimalik ilmaennustust saada ükskõik millise asukoha jaoks. Selleks kasutame HTML <form> tagi, ning URL otsinguparameetreid.

  1. Muudame index.html ning lisame sinna otsimisfunktsionaalsuse kasutades <form> elementi.

Mall index.html

{% extends 'base.html' %}

{% block title %}
Home
{% endblock %}

{% block content %}
<h1>Welcome to weather app!</h1>
<form action="{{ url_for('weather')}}" method="GET">
    <label for="lat">Latitude</label>
    <input type="text" name="lat" required>
    <label for="lon">Longitude</label>
    <input type="text" name="lon" required>
    <button type="submit">Get weather</button>
</form>
{% endblock %}

Soovitus: Loe <form> elemendi kohta lähemalt siit.

Nüüd tekkisid su veebilehele otsinguväljad, kuhu saad sisestada andmeid. Pane tähele - kui andmed on sisestatud ning vajutad “Get weather”, siis kanduvad sisestatud väärtused ka uude URLi.

Näiteks kui sisestad (1, 1):

http://localhost:5000/weather?lat=1&lon=1
  1. Kasutame URLis olevaid otsinguparameetreid, et asukohapõhist ilma näidata. Selleks peame muutma weather funktsiooni app.py failis.

Mall app.py

from flask import Flask
from flask import render_template
from flask import request
from openweather import weather_data

app = Flask(__name__)
app.config.from_object('config:Config')

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

@app.route('/weather')
def weather():
    lat = request.args.get('lat')
    lon = request.args.get('lon')

    data = weather_data(lat, lon, app.config['OPEN_WEATHER_API_KEY'])
    return render_template('weather.html', data=data)

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

Nüüd saab ilmaandmeid küsida ükskõik millise asukoha kohta meie planeedil.

Soovitus: Võib-olla oled tähele pannud, et Latitude ja Longitude väljadele saab sisestada ükskõik millist infot. Siiski ainult väga väike hulk sisendeid annavad meile päris infot ilma kohta. Loo sisendite validaator, mis võtab sisse lat ja lon väärtused ning tagastab boolean väärtuse selle kohta, et kas andmed on sobivad. Kui ei ole sobivad, siis muuda muutuja data väärtuseks “ilmaandmed puuduvad”.

3.5. Andmete visualiseerimine

Hetkel on otsingutulemused inimese jaoks halvasti loetavad. Muudame need paremaks!

  1. Paneme andmed tabelisse, et neid oleks parem visualiseerida.

Mall weather.html

{% extends 'base.html' %}

{% block title %}
Weather data
{% endblock %}

{% block content %}
<h1>Weather data</h1>
<table>
    <tr>
        <th>Location</th>
        <th>Temperature</th>
        <th>Weather</th>
        <th>Icon</th>
    </tr>
    <tr>
        <td>
            {{ location["county"] }}
            <br>
            {{ location["state"] }}
            <br>
            {{ location["city"] }}
        </td>
        <td>
            <!-- Converting K to C -->
            {{ (data["main"]["temp"] - 273.15) // 1 }}°C
        </td>
        <td>
            {{ data["weather"][0]["main"] }}
            <br>
            {{ data["weather"][0]["description"] }}
        </td>
        <td>
            <img src="https://openweathermap.org/img/wn/{{ data['weather'][0]['icon'] }}@2x.png" alt="Weather Icon">
        </td>
    </tr>
</table>
{% endblock %}
  1. Muudame koordinaatide info asukoha infoks, inimloetaval kujul.

Mall app.py

from flask import Flask
from flask import render_template
from flask import request
from openweather import weather_data
import reverse_geocode

app = Flask(__name__)
app.config.from_object('config:Config')

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

@app.route('/weather')
def weather():
    lat = request.args.get('lat')
    lon = request.args.get('lon')

    # get location name data from lat and lon
    location = reverse_geocode.get((lat, lon))
    data = weather_data(lat, lon, app.config['OPEN_WEATHER_API_KEY'])
    return render_template('weather.html', data=data, location=location)

if __name__ == '__main__':
    app.run()
  1. Laeme alla reverse_geocode paki.

Soovitus: Aktiveeri venv enne allalaadimist, pärast ära unusta pakke freezida.

pip install reverse_geocode
  1. Käivitame rakenduse

flask --debug run

3.6. BONUS Interaktiivusus

Praegune lahendus koosneb kahest vaatest. Esiteks vaade, kus kasutaja saab andmeid sisestada ning teiseks vaade, kus kasutaja saab ilmaennustust näha. Tegelikult pole selle jaoks kahte vaadet vaja. Piisab ühest, kus on nii otsinguväljad kui ka ilmaennustuse tulemused. Loome sellise lahenduse kasutades HTMXi. HTMX on lihtne töörist, millega saab oma veebilehte peale esmast laadimist uuendada uute andmetega.

  1. Lisame HTMXi võimekuse oma veebilehele. Selleks lisame <script> tagi oma veebilehe päisesse. Seda teeme failis base.html.

Mall base.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="{{ url_for('static', filename='normalize.css') }}">
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
    <!--for HTMX support-->
    <script src="https://unpkg.com/htmx.org@2.0.1"
        integrity="sha384-QWGpdj554B4ETpJJC9z+ZHJcA/i59TyjxEPXiiUgN2WmTyV5OEZWCD6gQhgkdpB/"
        crossorigin="anonymous"></script>
    <title>{% block title %}{% endblock %}</title>
</head>

<body>
    {% block content %}{% endblock %}
</body>

</html>
  1. Muudame otsinguvälja selliselt, et vajutades nupule “Get weather” ei navigeeru kasutaja uuele lehele, vaid ilmaandmed ilmuvad samale lehele, kus otsing teostati.

Mall index.html

{% extends 'base.html' %}

{% block title %}
Home
{% endblock %}

{% block content %}
<h1>Welcome to weather app!</h1>
<form hx-get="{{ url_for('weather') }}" hx-target="#results">
    <label for="lat">Latitude</label>
    <input type="text" name="lat" required>
    <label for="lon">Longitude</label>
    <input type="text" name="lon" required>
    <button type="submit">Get weather</button>
</form>
<div id="results">Input latitude and longitude to search for weather.</div>
{% endblock %}

Mis tagataustal toimub:

  1. hx-get teeb GET requesti aadressile http://localhost:5000/weather.

  2. Request tuleb tagasi ilmainfo lehe andmetega.

  3. hx-target asendab elemendi (mille id="result") sisemise teksti response andmetega, milleks hetkel on ilmainfo.

Soovitus: Proovi luua lahendus, kus peale igat otsingut vana tulemust ei kustutata, vaid vana tulemus liigutatakse allapoole ja uus ilmub ülemisse otsa. Mõtle sellest kui listist, mille algusesse andmeid juurde lisatakse.

3.7. Andmebaas

Soovitus: Enne selle osaga alustamist tutvu juhendiga: PostgreSQL setup. Paigalda näiteks pgAdmin4 abil andmebaasi server ning uuenda config faili.

Andmebaasiga saab suhelda kahte moodi:

Suhtluse tüüp

Pros

Cons

Otsene suhtlus (SQL)

Lihtsam mõista

Raskem kirjutada

Lihtsam üles seada

Rohkem ülalpidamist

Turvaaugud

Kaudne suhtlus (ORM)

Lihtsam kirjutada

Raskem mõista

Ühildub hästi keelega

Raskem üles seada

Vähem ülalpidamist

Selles juhendis kirjeldame kuidas kasutada otsest suhtlust andmebaasi ja programmi vahel, kuna sellega on lihtsam algust teha.

Soovitus: Kui sul on soov minna teist teed ja kasutada ORM lahendust, siis uuri Flask-SQLAlchemy.

  1. Lisame projektile psycopg[binary] paki, mis lubab meil lihtsasti ühenduda PostgreSQL andmebaasiga.

pip install "psycopg[binary]"
  1. Lisame config.py faili andmebaasi ühenduseks vajaliku connection stringi. Mallis on välja toodud näidis, mis tuleks asendada enda andmebaasi andmetega.

Mall config.py klass

class Config:
    # Enter your OpenWeather API key here
    OPEN_WEATHER_API_KEY = "<your-open-weather-api-key>"
    # Connection string used for postgres database access.
    POSTGRES_CONNECTION_STRING = "host=localhost user=user password=pass port=5432 dbname=app"

Soovitus: Ära unusta ka config.example.py faili uuendada, et ka teised sinu meeskonnas teaksid, milline peaks see fail välja nägema.

  1. Loo uus kaust database. Seal saad hoiustada kõiki andmebaasiga seotud tegevusi.

  2. Loo sinna kausta fail schema.sql, kus saad kirjeldada milline su andmebaas välja peaks nägema. Selle rakenduse raames soovime hoiustada andmeid kasutajate ja kasutajate ajaloo kohta. SQLi kohta lähemalt loe siit juhendist.

Mall schema.sql

DROP TABLE IF EXISTS history_entries;
DROP TABLE IF EXISTS users;

CREATE TABLE users (
    id INT GENERATED ALWAYS AS IDENTITY,
    name VARCHAR(20) NOT NULL UNIQUE,
    password VARCHAR(100) NOT NULL,
    PRIMARY KEY(id)
);

CREATE TABLE history_entries(
    id INT GENERATED ALWAYS AS IDENTITY,
    lat FLOAT NOT NULL,
    lon FLOAT NOT NULL,
    user_id INT,
    PRIMARY KEY(id),
    FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
);
  1. Soovime seda SQLi rakendada iga kord kui Flask käivitatakse. Selleks loome uue faili database.py ning lisame sinna funktsiooni create_database.

Mall database.py

import os.path

import psycopg
from flask import current_app


def create_database():
    # load schema.sql file into variable
    schema: str
    with open(os.path.join(os.path.abspath("."), "database", "schema.sql"), "r") as file:
        schema = file.read()

    # Connect to an existing database
    with psycopg.connect(current_app.config["POSTGRES_CONNECTION_STRING"]) as conn:
        # Open a cursor to perform database operations
        with conn.cursor() as cur:
            # Execute the schema script to initialize the database
            cur.execute(schema)
            # Commit the changes to the database
            # When this is not called, no changes will be made to database.
            conn.commit()
  1. Kutsume create_database funktsiooni välja app.py failis

Mall app.py

import reverse_geocode
from flask import Flask
from flask import render_template
from flask import request

from database.database import create_database
from openweather import weather_data

app = Flask(__name__)
app.config.from_object('config:Config')
with app.app_context(): # must be in application context to execute
    create_database()
  1. Flaski rakendust käima pannes võid näha muudatusi oma andmebaasis. Muudatuste paremaks vaatlemiseks võid kasutada näiteks pgAdmin4 või DBeaver.

3.8. Kasutaja lisamine

Süsteemi peaks olema võimalik lisada kasutajaid. Selleks tuleb luua veebilehele registreerimise vorm, kus kasutaja saab sisestada oma andmed. Samuti tuleb neid andmeid hoiustada andmebaasis.

  1. Loome kasutajate registreerimise vormi kodulehele. Selleks täiendame index.html faili. Kasutaja poolt sisestatud andmed suuname /signup endpointile. Muudame veebilehte ka paremini loetavaks, lisades paar style atribuuti.

Soovitus: Malli täielik kopeerimine eeldab ka BONUS osa.

Mall index.html

{% extends 'base.html' %}

{% block title %}
Home
{% endblock %}

{% block content %}
<div style="display:flex; justify-content:space-between;">
    <div>
        <h1>Welcome to weather app!</h1>
        <form hx-get="{{ url_for('weather') }}" hx-target="#results" style="display:flex; flex-direction:column;">
            <label for="lat" id="lat">Latitude</label>
            <input type="text" name="lat" required>
            <label for="lon" id="lon">Longitude</label>
            <input type="text" name="lon" required>
            <button type="submit">Get weather</button>
        </form>
        <div id="results">Input latitude and longitude to search for weather.</div>
    </div>
    <div>
        <h1>Become a user</h1>
        <form action="{{ url_for('signup') }}" method="POST" style="display:flex; flex-direction:column;">
            <label for="name" id="name">Name</label>
            <input type="text" name="name" required>
            <label for="password" id="password">Password</label>
            <input type="password" name="password" required>
            <button type="submit">Register</button>
        </form>
    </div>
</div>
{% endblock %}

Soovitus: Pane tähele, et seekord kasutasime form tagis POST meetodit.

  1. Täiendame ka app.py faili, lisades sinna signup funktsiooni.

Mall signup

@app.route('/signup', methods=["POST"])
def signup():
    return render_template('index.html')
  1. Kui panna hetkel rakendus käima, ei tee andmete sisestamine mitte midagi. Lisame kasutaja andmed andmebaasi. Selleks lisame database kausta uue faili nimega user.py, kuhu lisame kasutaja andmete andmebaasi lisamise loogika.

Mall user.py

import psycopg
from flask import current_app


def create_user(name: str, password: str):
    with psycopg.connect(current_app.config["POSTGRES_CONNECTION_STRING"]) as conn:
        with conn.cursor() as cur:
            cur.execute(
                "INSERT INTO users (name, password) VALUES (%s, %s)",
                (name, password)
            )
            conn.commit()

Soovitus: Kui lisada 2 sama nimega kasutajat, tuleb veebilehel error. Proovi enne kasutaja lisamist teada saada, kas sellise nimega kasutaja on juba olemas, ja kui on, siis ära lisa uut kasutajat.

  1. Täiendame viimast korda signup funktsiooni ning kutsume create_user funktsiooni seal välja.

Mall signup

@app.route('/signup', methods=["POST"])
def signup():
    name = request.form.get('name')
    password = request.form.get('password')
    create_user(name, password)
    return redirect("/")

Soovitus: Tavaliselt on enamustel veebilehtedel parooli osas nõudmised, näiteks parool peab olema 8 tähte pikk, peab sisaldama numbreid jms. Proovi luua parooli validaator, mis ei loo kasutajat, kui parool on liiga nõrk.

Soovitus: Hetkel lisame parooli andmebaasi sellisel kujul nagu kasutaja selle meile andis. SEE EI OLE HEA TAVA. Hea tava ei ole see sellepärast, et kui mõni häkker pääseb andmebaasile ligi, siis on tal võimalik teisi kasutajaid jäljendada, kuna ta teab nende paroole. Lahendus sellele probleemile on paroolide hashimine. Proovi luua funktsioon, mis enne parooli andmebaasi panemist selle ära hashib.

3.8.1. Sessioonid

Hetkel lisatakse kastaja andmed küll andmebaasi, kuid veebilehe kasutamise osas see otseselt midagi ei muuda. Kasutaja olemasolu rakendamiseks ja kontrollimiseks on lihtsaim lahendus Flaski sisseehitatud sessioonid.

  1. Seadistame SECRET_KEY, mida kasutatakse kasutaja sessiooni hashimiseks.

Mall app.py

app.secret_key = 'your_secret_key'

Soovitus: Hea praktika järgi tuleks ‘your_secret_key’ placeholderi asemele kirjutada midagi muud. Lisaks võib secret_key config faili tõsta.

  1. Sessioonid põhinevad kasutajanimel ja user_idl. Seega lisame user.py faili funktsiooni, mille parameetriteks on kasutajanimi ja parool. Juhul kui kasutaja on olemas ja parool õige, tagastab funktsioon user_id, mida kasutame sessiooni alustamiseks.

Mall user.py

def check_user(name: str, password: str):
    with psycopg.connect(current_app.config["POSTGRES_CONNECTION_STRING"]) as conn:
        with conn.cursor() as cur:
            cur.execute(
                "SELECT id, password FROM users WHERE name = %s",
                (name,)
            )
            user_data = cur.fetchone()
            if user_data and password == user_data[1]:
                return user_data[0]
            return None
  1. Täiendame signup funktsiooni nii, et peale kasutaja loomist algaks kohe uus sessioon.

Mall signup

from flask import session

@app.route('/signup', methods=["POST"])
def signup():
    name = request.form.get('name')
    password = request.form.get('password')
    create_user(name, password)
    user_id = check_user(name, password)
    if user_id:
        session['user_id'] = user_id
        session['username'] = name
        return redirect("/")
    return redirect("/")
  1. Lisame veel login välja, endpointi ja funktsiooni, et juba olemasoleva kasutajaga sessiooni alustada.

Mall index.html

<h1>Login</h1>
        <form action="{{ url_for('login') }}" method="POST" style="display:flex; flex-direction:column;">
            <label for="name" id="login-name">Name</label>
            <input type="text" name="name" required>
            <label for="password" id="login-password">Password</label>
            <input type="password" name="password" required>
            <button type="submit">Login</button>
        </form>

Mall user.py

@app.route('/login', methods=["POST"])
def login():
    name = request.form.get('name')
    password = request.form.get('password')
    user_id = check_user(name, password)
    if user_id:
        session['user_id'] = user_id
        session['username'] = name
        return redirect("/")
    return redirect("/")

Tähelepanek: Backendis toimub küll palju toiminguid (kasutaja loomine, sisselogimine, parooli ja kasutajanime kontrollimine), kuid kasutaja ei saa nende toimingute kohta tagasisidet. Selle probleemi lahendab peatükk 3.9.

3.8.2. Conditional rendering

Soovime oma rakendusse lisada logout nupu, kuid seda oleks mõistlik kuvada ainult siis, kui kasutaja on juba sisse logitud. Samuti oleks mõistlik login ja signup väljasid kuvada ainult enne sisselogimist.

  1. Lisame avalehele logout nupu, mida kuvatakse ainult sisselogitud kasutajale. session on justkui muutuja, mille väljade väärtusele pääseb ligi nii backendis kui ka html faili sees.

Mall index.html

{% if session.get('username') %}
    <h1>Welcome, {{ session.get('username') }}!</h1>
    <button>
        <a href="{{ url_for('logout') }}">Logout</a>
    </button>
{% endif %}

Märkus: {% if %} ja {% endif %} vahele jäävad HTML elemendid kuvatakse juhul kui session.get('username') tõeväärtuseks on True.

Mall user.py

@app.route('/logout')
def logout():
    session.pop('user_id', None)
    session.pop('username', None)
    flash('Logged out')
    return redirect("/")
  1. Muudame {% if %} ja {% endif %} plokkide abil signup ja login väljade kuvamist.

Mall index.html

<div>
    {% if not session.get('username') %}
    <h1>Become a user</h1>
    <form action="{{ url_for('signup') }}" method="POST" style="display:flex; flex-direction:column;">
        <label for="name" id="name">Name</label>
        <input type="text" name="name" required>
        <label for="password" id="password">Password</label>
        <input type="password" name="password" required>
        <button type="submit">Register</button>
    </form>
    {% endif %}
</div>
<div>
    {% if not session.get('username') %}
    <h1>Login</h1>
    <form action="{{ url_for('login') }}" method="POST" style="display:flex; flex-direction:column;">
        <label for="name" id="login-name">Name</label>
        <input type="text" name="name" required>
        <label for="password" id="login-password">Password</label>
        <input type="password" name="password" required>
        <button type="submit">Login</button>
    </form>
    {% endif %}
</div>

Märkus: Erinevalt logout väljast kasutame not session.get('username').

3.8.3. Otsinguajalugu

Lisame rakendusele funktsionaalsuse, mille jaoks on varasemalt loodud kasutajate süsteem päriselt vajalik - otsinguajalugu. Iga ilmaandmete päring seotakse sisselogitud kasutajaga (selle olemasolul) ja salvestatakse andmebaasi.

  1. history_entries tabel on juba varasemalt schema.sql failis defineeritud. Seega loome database kausta search_history.py faili koos funktsioonidega, mille abil andmeid salvestada ja pärida.

Mall search_history.py

import psycopg
from flask import current_app


def log_search_query(user_id, lat, lon):
    with psycopg.connect(current_app.config["POSTGRES_CONNECTION_STRING"]) as conn:
        with conn.cursor() as cur:
            cur.execute(
                "INSERT INTO history_entries (lat, lon, user_id) VALUES (%s, %s, %s)",
                (lat, lon, user_id)
            )
            conn.commit()


def get_user_search_histroy(user_id):
    with psycopg.connect(current_app.config["POSTGRES_CONNECTION_STRING"]) as conn:
        with conn.cursor() as cur:
            cur.execute(
                "SELECT lat, lon FROM history_entries WHERE user_id = %s",
                (user_id,)
            )
            user_search_history = cur.fetchall()
            return user_search_history

log_search_query salvestab andmebaasi koordinaadid ning päringu sooritanud kasutaja id. get_user_search_history võtab argumendiks kasutaja id ning tagastab kõik seotud koordinaadid.

  1. Täiendame weather funktsiooni nii, et sisselogitud kasutaja päring salvestataks andmebaasi.

Mall app.py

@app.route('/weather')
def weather():
    lat = request.args.get('lat')
    lon = request.args.get('lon')
    if 'user_id' in session:
        log_search_query(session['user_id'], lat, lon)
    location = reverse_geocode.get((lat, lon))
    data = weather_data(lat, lon, app.config['OPEN_WEATHER_API_KEY'])
    return render_template('weather.html', data=data, location=location)
  1. BONUS: Lisa app.py faili endpoint /history, mis kutsub välja funktsiooni get_user_search_history ning tagastab sisselogitud kasutaja otsinguajaloo.

3.9. Teavitused

Rakenduse kasutajakogemust aitaks parandada teavitused, mis kuvaksid infot näiteks vale parooli sisestamise korral.

  1. Lisame HTML faili elemendi, milles backendist tulevaid sõnumeid kuvada.

Mall index.html

<div style="display:flex; justify-content:center;">
    {% for msg in get_flashed_messages() %}
      <h1>{{ msg }}</h1>
    {% endfor %}
</div>
  1. Flashi kasutades on backendist teavituste saatmine väga lihtne, piisab sobivasse kohta ühe koodirea lisamisest.

from flask import flash

Mall app.py

@app.route('/signup', methods=["POST"])
def signup():
    name = request.form.get('name')
    password = request.form.get('password')
    create_user(name, password)
    user_id = check_user(name, password)
    if user_id:
        session['user_id'] = user_id
        session['username'] = name
        flash("User created")
        return redirect("/")
    flash("Couldn't create user")
    return redirect("/")


@app.route('/login', methods=["POST"])
def login():
    name = request.form.get('name')
    password = request.form.get('password')
    user_id = check_user(name, password)
    if user_id:
        session['user_id'] = user_id
        session['username'] = name
        flash("Login successful")
        return redirect("/")
    flash('Invalid credentials')
    return redirect("/")

Soovitus: Hea praktika kohaselt tuleks API route sisaldav fail võimalikult tühjana hoida ehk importida muudes failides defineeritud funktsioone. Proovi signup ja login loogika koos sõnumite kuvamisega näiteks user.py faili tõsta.