Web Development

Strona internetowa vs aplikacja internetowa

strona internetowa
  • wyłącznie treść

  • statyczne pliki HTML i CSS

aplikacja
  • interakcja

  • narzędzia umożliwiające bardziej złożoną komunikację z serwerem:
    • po stronie serwera np. Python, PHP, Node.js

    • po stronie klienta np. Javascript, AJAX

Jeśli strona pełni funkcję wyłącznie informacyjną to nie należy jej nazywać aplikacją.

HTTP

Protokół obsługiwany przez przeglądarkę internetową. Określa zasady wymiany informacji pomiędzy serwerem a klientem (przeglądarką internetową). Klient wysyła żądanie do serwera na podstawie którego generowania jest odpowiedź.

HTTPS

Komunikacja pomiędzy klientem a serwerem jest szyfrowana.

URL (Uniform Resource Locator) vs URI (Uniform Resource Identifier)

URL to URI, ale URI to nie URL.

Główne metody HTTP

  • GET - pobieranie zasobu

  • POST - wysyłanie danych (np. formularzy, plików)

  • PUT - nadpisywanie istniejącego zasobu (lub tworzenie nowego jeżeli nie istnieje)

  • PATCH - częściowa aktualizacja zasobu

  • DELETE - usuwanie zasobu

Czym jest framework?

Szkielet służący do budowania aplikacji. Bardzo często wymusza stosowanie dobrych praktyk (np. KISS, DRY).

  • wyposaża programistę w zestaw gotowych narzędzi i bibliotek

  • wyznacza strukturę aplikacji

  • często definiuje domyślną konfigurację, która jest użyteczna dla większości projektów

Czym jest Flask?

Microframework napisany w języku Python. Pozwala na łatwą rozbudowę aplikacji dzięki licznym rozszerzeniom. Oparty m.in. na Jinja2 - silniku szablonów służącemu do renderowania dynamicznych stron internetowych.

SQL

Język zapytań pozwalający na tworzenie, edycję oraz zarządzanie relacyjnymi bazami danych. Dane w bazie przechowywane są w postaci powiązanych ze sobą tabel. Typy relacji pomiędzy tabelami:

  • jeden-do-wielu

  • wiele-do-wielu

  • jeden-do-jednego

ORM (Object-Relational Mapping)

Mapowanie obiektowo-relacyjne pozwala na odwzorowanie tabel bazodanowych wraz z ich relacjami przy użyciu obiektów w aplikacji. Systemy ORM pozwalają m.in. na:

  • nawiązanie połączenia z bazą

  • wysyłanie zapytań SQL

  • zarządzanie sesją, transakcjami bazodanowymi

  • cache’owanie wyników zapytań

SQL vs ORM

SQL

posts

id

INTEGER PRIMARY KEY

NOT NULL

title

VARCHAR (128)

NOT NULL

content

TEXT

NOT NULL

author

VARCHAR (64)

NOT NULL

SELECT id, title, content, author FROM posts WHERE id = 5;

ORM

class Post(db.Model):
    __tablename__ = 'posts'

    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(128), nullable=False)
    content = db.Column(db.Text(), nullable=False)
    author = db.Column(db.String(64), nullable=False)
Post.query.get(5)

Tworzenie aplikacji

main.py

from flask import Flask


app = Flask('blog')


@app.route('/')
def hello_world():
    return 'Hello World'

Dekorator app.route pozwala na powiązanie funkcji hello_world z adresem URL /. Funkcja hello_world zwraca treść, która będzie dostępna pod adresem URL http://localhost:5000.

Uruchamianie aplikacji

export FLASK_APP=main.py
flask run

Aplikacja dostępna pod adresem: http://127.0.0.1:5000/

Prezentacja danych - pierwszy szablon

templates/index.html

<ul>
    {% for post in posts %}
        <li>{{ post.title }}</li>
    {% endfor %}
</ul>

Tagi, będące częścią Jinja2, pozwalają na wykonywanie kodu w szablonie HTML. Tagi, którymi będziemy się posługiwać podczas kursu:

  • for: pętla

  • if/elif/else: instrukcje warunkowe

  • extends: służy do zasygnalizowania silnikowi szablonów, że dany szablon rozszerza inny szablon (np. korzysta z jego kodu HTML)

  • block: służy do tworzenia bloków, których treść może być nadpisywana w szablonach korzystających z innych szablonów poprzez użycie tagu extends

W szablonach HTML można korzystać ze zmiennych, które zostały przekazane do funkcji render_template np. {{ posts }}.

Pobieranie postów z pliku

main.py

from flask import Flask, render_template


@app.route('/')
def index():
    posts = get_all_posts()
    return render_template('index.html', posts=posts)

Pobieranie pojedyńczego postu z pliku

main.py

@app.route('/posts/<id_>')
def post_detail(id_):
    post = get_post_by_id(id_)
    return render_template('post_detail.html', post=post)

Szablon dla pojedyńczego postu

templates/post_detail.html

<div>
    <p>Autor: {{ post.author }}</p>
    <strong>{{ post.title }}</strong>
    <p>{{ post.content }}</p>
</div>

Aktualizacja szablonu zawierającego listę postów

templates/index.html

<ul>
    {% for post in posts %}
        <li><a href="{{ url_for('post_detail', id_=post.id) }}">{{ post.title }}</a></li>
    {% endfor %}
</ul>

Funkcja url_for pozwala na utworzenie adresu URL na podstawie nazwy funkcji (widoku) oraz jej argumentów.

Reużywalność szablonów

templates/base.html

<html>
    <head>
        <title>PyPiła 2020</title>
    </head>
    <body>
        <h1>Najlepszy blog w internecie :)</h1>
        {% block content %}{% endblock %}
    </body>
</html>

templates/index.html

{% extends 'base.html' %}

{% block content %}
    <ul>
        {% for post in posts %}
            <li>
                <a href="{{ url_for('post_detail', id_=post.id) }}">{{ post.title }}</a>
            </li>
        {% endfor %}
    </ul>
{% endblock %}

templates/post_detail.html

{% extends 'base.html' %}

{% block content %}
    <div>
        <p>Autor: {{ post.author }}</p>
        <strong>{{ post.title }}</strong>
        <p>{{ post.content }}</p>
    </div>
    <a href="{{ url_for('index') }}">Powrót</a>
{% endblock %}

Dodawanie postów do pliku

main.py

from flask import Flask, redirect, render_template, request, url_for


@app.route('/add-post', methods=('GET', 'POST'))
def add_post():
    if request.method == 'POST':
        form = request.form
        post = save_post_to_file(form['title'], content=form['content'], author=form['author'])
        return redirect(url_for('post_detail', id_=post.id))
    return render_template('add_post.html')

Obiekt request reprezentuje żadanie wysłane do serwera HTTP. Wysyłanie danych z formularza odbywa się przy pomocy metody POST. Wspomniany obiekt request zawiera treść formularza, którą możemy wykorzystać do zapisu postu.

templates/add_post.html

{% extends 'base.html' %}

{% block content %}
    <form method="post">
        <div>
            <label for="title">Tytuł:</label>
            <input name="title" id="title" required>
        </div>
        <div>
            <label for="content">Treść:</label>
            <textarea name="content" id="content" rows="4" cols="30" required></textarea>
        </div>
        <div>
            <label for="author">Autor:</label>
            <input name="author" id="author" required>
        </div>
        <input type="submit" value="Dodaj">
    </form>
    <a href="{{ url_for('index') }}">Powrót</a>
{% endblock %}

templates/index.html

{% extends 'base.html' %}

{% block content %}
    <a href="{{ url_for('add_post') }}">Dodaj post</a>
    <ul>
        {% for post in posts %}
            <li>
                <a href="{{ url_for('post_detail', id_=post.id) }}">{{ post.title }}</a>
            </li>
        {% endfor %}
    </ul>
{% endblock %}

Usuwanie postów z pliku

main.py

@app.route('/delete-post/<id_>', methods=('GET', 'POST'))
def delete_post(id_):
    post = get_post_by_id(id_)
    if request.method == 'POST':
        delete_post_by_id(post.id)
        return redirect(url_for('index'))
    return render_template('delete_post.html', post=post)

templates/delete_post.html

{% extends 'base.html' %}

{% block content %}
    <p>Czy na pewno chcesz usunąć post zatytułowany <strong>{{ post.title }}</strong>?</p>
    <form method="post">
        <button type="submit">Potwierdź</button>
    </form>
    <a href="{{ url_for('index') }}">Powrót</a>
{% endblock %}

templates/index.html

{% extends 'base.html' %}

{% block content %}
    <a href="{{ url_for('add_post') }}">Dodaj post</a>
    <ul>
        {% for post in posts %}
            <li>
                <a href="{{ url_for('post_detail', id_=post.id) }}">{{ post.title }}</a>
                <a href="{{ url_for('delete_post', id_=post.id) }}">Usuń</a>
            </li>
        {% endfor %}
    </ul>
{% endblock %}

Pierwszy model

models.py

from flask_sqlalchemy import SQLAlchemy


db = SQLAlchemy()


class Post(db.Model):
    __tablename__ = 'posts'

    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(128), nullable=False)
    content = db.Column(db.Text(), nullable=False)
    author = db.Column(db.String(64), nullable=False)

Integracja bazy danych z obiektem aplikacji

main.py

from flask import Flask

from models import db


app = Flask('blog')
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///./blog.db'
db.init_app(app)

Tworzenie tabel

flask shell
from models import db


db.create_all()

Pobieranie postów z bazy danych

main.py

from flask import Flask, render_template

from models import db, Post


@app.route('/')
def index():
    posts = Post.query.all()
    return render_template('index.html', posts=posts)

Sprawdzenie wyświetlania postów

Brak wyników?

flask shell
from models import db


db.session.execute("INSERT INTO posts VALUES(null, 'Pierwszy post', 'Super interesująca treść', 'Jan Kowalski');")
db.session.commit()

Pobieranie pojedyńczego postu z bazy danych

main.py

@app.route('/posts/<id_>')
def post_detail(id_):
    post = Post.query.get(id_)
    return render_template('post_detail.html', post=post)

Dodawanie postów do bazy danych

main.py

from flask import Flask, redirect, render_template, request, url_for


@app.route('/add-post', methods=('GET', 'POST'))
def add_post():
    if request.method == 'POST':
        form = request.form
        post = Post(title=form['title'], content=form['content'], author=form['author'])
        db.session.add(post)
        db.session.commit()
        return redirect(url_for('post_detail', id_=post.id))
    return render_template('add_post.html')

Usuwanie postów z bazy danych

main.py

@app.route('/delete-post/<id>', methods=('GET', 'POST'))
def delete_post(id):
    post = Post.query.get(id_)
    if request.method == 'POST':
        db.session.delete(post)
        db.session.commit()
        return redirect(url_for('index'))
    return render_template('delete_post.html', post=post)

Instrukcje warunkowe w szablonach

templates/index.html

{% extends 'base.html' %}

{% block content %}
    <a href="{{ url_for('add_post') }}">Dodaj post</a>
    {% if posts %}
        <ul>
            {% for post in posts %}
                <li>
                    <a href="{{ url_for('post_detail', id_=post.id) }}">{{ post.title }}</a>
                    <a href="{{ url_for('delete_post', id_=post.id) }}">Usuń</a>
                </li>
            {% endfor %}
        </ul>
    {% else %}
        <p>Nie dodałeś jeszcze żadnego posta!</p>
    {% endif %}
{% endblock %}

Stylowanie

templates/base.html

<html>
    <head>
        <title>PyPiła 2020</title>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
    </head>
    <body class="container">
        <h1 class="my-3">Najlepszy blog w internecie :)</h1>
        {% block content %}{% endblock %}
    </body>
</html>

templates/index.html

{% extends 'base.html' %}

{% block content %}
    <a class="btn btn-primary btn-lg my-3" href="{{ url_for('add_post') }}">Dodaj post</a>
    {% if posts %}
        <ul class="list-group">
            {% for post in posts %}
                <li class="list-group-item">
                    <a href="{{ url_for('post_detail', id_=post.id) }}">{{ post.title }}</a>
                    <a class="btn btn-danger float-right" href="{{ url_for('delete_post', id_=post.id) }}">Usuń</a>
                </li>
            {% endfor %}
        </ul>
    {% else %}
        <p class="alert alert-info">Nie dodałeś jeszcze żadnego posta!</p>
    {% endif %}
{% endblock %}

templates/post_detail.html

{% extends 'base.html' %}

{% block content %}
    <div class="card p-3 mb-3">
        <p>Autor: {{ post.author }}</p>
        <strong>{{ post.title }}</strong>
        <p>{{ post.content }}</p>
    </div>
    <a href="{{ url_for('index') }}">Powrót</a>
{% endblock %}

templates/add_post.html

{% extends 'base.html' %}

{% block content %}
    <form class="card p-3" method="post">
        <div class="form-group">
            <label for="title">Tytuł:</label>
            <input class="form-control" name="title" id="title" required>
        </div>
        <div class="form-group">
            <label for="content">Treść:</label>
            <textarea class="form-control" name="content" id="content" rows="4" cols="30" required></textarea>
        </div>
        <div>
            <label for="author">Autor:</label>
            <input class="form-control" name="author" id="author" required>
        </div>
        <input class="btn btn-primary btn-block mt-3" type="submit" value="Dodaj">
    </form>
    <a href="{{ url_for('index') }}">Powrót</a>
{% endblock %}

templates/delete_post.html

{% extends 'base.html' %}

{% block content %}
    <p>Czy na pewno chcesz usunąć post zatytułowany <strong>{{ post.title }}</strong>?</p>
    <form method="post">
        <button class="btn btn-primary" type="submit">Potwierdź</button>
    </form>
    <a href="{{ url_for('index') }}">Powrót</a>
{% endblock %}

Zadanie domowe

Dodaj do aplikacji widok pozwalający na edycję wybranego postu.