No tópico passado nós paramos com nosso web app com este formato:
#app.py
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
app.run()
E tínhamos uma página html com o seguinte formato:
<!-- index.html -->
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link
rel="stylesheet"
href="{{ url_for('static', filename='style.css') }}"
/>
<title>Minha primeira página web</title>
</head>
<body>
<h1>Hello world</h1>
</body>
</html>
E brincamos adicionando uma linha de CSS à página:
/* style.css */
h1 {
color: green;
}
E nossa estrutura de diretórios era esta:

Agora vamos entrar um pouco mais a fundo no back-end da aplicação. Vamos fazer um formulário simples de upload de imagens e fazer com que essas imagens sejam renderizadas no html.
Para isso vou precisar instalar mais uma lib que é a flask-wtf, usada para criação de formulários:
(venv)$ pip install flask-wtf
Com a lib instalada, vou mudar um pouco nosso arquivo app.py. Primeiro vou criar uma classe que irá ser usada para criar nossos formulários. Eu sei que não abordei classes ainda nem quase nada de orientação a objetos mas se ficar confuso relevem um pouco essa parte. Quando vierem os tópicos de orientação a objetos isso vai ficar mais claro:
Importando tudo que vou precisar no app.py, teremos estes imports:
#app.py
from flask import Flask, render_template, redirect, url_for
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired
from werkzeug import secure_filename
import os
FlaskForm será usada para criar o formulário. FileField será usada para o upload dos arquivos. FileRequired será usada para assegurar que o botão de enviar no html só irá enviar informaçãose realmente um arquivo tiver sido inserido. Secure_filename será usada para fazer com que o arquivo baixado tenha um filename sem caracteres proibidos e outros problemas, e a lib "os" simula o sistema operacional e eu irei usá-la para pegar os arquivos baixados e renderizar no html.
A classe responsável por criar o formulário terá esta cara:
#app.py
from flask import Flask, render_template, redirect, url_for
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired
from werkzeug import secure_filename
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = 'whatever'
class UploadForm(FlaskForm):
file = FileField(validators=[FileRequired()])
A linha "app.config['SECRET_KEY'] é um parâmetro necessário de configuração em aplicações web, que é a chave secreta da aplicação. Não vou entrar em detalhes sobre secret keys agora, mas saibam que é algo necessário e importante em aplicações web, e não deve estar escrita no código e sim como variável de ambiente, mas justamente pra não adentrar nessa seara agora vou me limitar a deixar do jeito que está.
Vejam a classe UploadForm, que herda de FlaskForm. A variável file irá receber uma instância de FileField que terá como validador a FileRequired(). No momento isso tudo pode estar parecendo bem confuso mas vai ficar mais claro mais lá pra frente.
Agora vou mudar um pouco meu app.route inicial. Ele vai receber uma instância da classe recém-criada, a UploadForm, e essa instância será responsável por renderizar o formulário de upload de arquivos no meu html.
...
class UploadForm(FlaskForm):
file = FileField(validators=[FileRequired()])
@app.route('/', methods=['GET', 'POST'])
def index():
form = UploadForm()
if form.validate_on_submit():
filename = secure_filename(form.file.data.filename)
form.file.data.save(f'static/images/{filename}')
return redirect(url_for('upload'))
return render_template('index.html', form=form)
O que acontece acima é o seguinte:
Meu decorador app.route, que é o responsável por definir que na url "/" a função "index" irá ser executada, deve receber quais métodos HTTP eu quero que essa página possa realizar. Basicamente o GET é responsável por receber dados e o POST por mandar dados. Não é tão simples assim mas por enquanto entendam que é assim. Então meu html que vai ser renderizado na url "/" vai poder tanto receber dados quanto enviar dados, e esses dados enviados serão as imagens que eu irei subir por meio do meu formulário.
A variável "form", como dito, é uma instância da classe UploadForm e será no fim das contas nosso formulário no HTML. E nossa função index retornará de cara a página "index.html" mandando como variável o "form" criado dentro da função index.
A condicional "if form.validate_on_submit()" serve para validar o envio dos dados. Como eu pedi na classe UploadForm que a instância de FileField tivesse como validador o FileRequired, a condicional vai entrar em ação devido a isso. Ou seja, se na página quando eu apertar o botão de enviar sem ter selecionado nenhum arquivo, nada vai acontecer. Já se o submit for validado, eu irei salvar o arquivo na variável filename na pasta "static/images/" e por fim minha função vai me redirecionar para a rota definida para a função upload, que será criada mais pra frente.
Antes de criar a rota para os arquivos de upload vou mexer no index.html:
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link
rel="stylesheet"
href="{{ url_for('static', filename='style.css') }}"
/>
<title>Minha primeira página web</title>
</head>
<body>
<h1>Meu primeiro web app</h1>
<form enctype="multipart/form-data" method="post">
{{ form.hidden_tag() }} {{ form.file }}
<input type="submit" />
</form>
</body>
</html>
Notem que o que mudou pra versão anterior foi o <form>, que recebe os atributos enctype e method, indicando que estou mandando um método HTTP Post ao submeter o formulário, com meu arquivo criado.
Dentro do form eu inseri dois dados vindos do Flask que é o form.hidden_tag() e o form.file. O .hidden_tag() é um método de segurança que não vou comentar agora. O form.file é a variável file criada na nossa classe UploadForm:
class UploadForm(FlaskForm):
file = FileField(validators=[FileRequired()])
Ou seja, é aqui que meu formulário vai ser renderizado no HTML.
Eu poderia rodar a aplicação agora pra mostrar a cara do index.html mas vou logo criar a outra rota e o outro arquivo html para rodar a aplicação toda de uma vez.
Voltando ao arquivo app.py, vamos criar a rota que irá renderizar as imagens subidas:
#app.py
...
@app.route('/uploaded', methods=['GET'])
def upload():
files = os.listdir('static/images/')
return render_template('upload.html', files=files)
...
Acima, temos uma nova rota, na url "/uploaded", que só poderá executar método GET, ou seja, só irá receber dados, e dentro dessa rota a função upload irá ser executada, com nossa variável "files" listando todos os arquivos que estão dentro da pasta "static/images/" e renderizando o template "upload.html", que receberá como variável "files".
Nosso "upload.html" terá esta cara:
<!-- upload.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<h1>Arquivos na pasta de uploads</h1>
<div id="images">
{% for file in files %}
<img src="static/images/{{ file }}" width="400" height="400" />
{% endfor %}
</div>
</body>
</html>
Notem que eu estou executando um laço "for" dentro de um template HTML usando o Flask. Pra cada arquivo na pasta de imagens o html vai renderizar essa imagem usando a tag <img>, recebendo como "source" cada imagem da pasta de arquivos de imagens. E cada imagem terá uma largura e uma altura de 400px.
Agora, falta criar a pasta de imagens dentro da pasta static:

Com tudo pronto, podemos rodar nosso app.py:

E olhando agora nossa pasta de imagens:

Ou seja, conseguimos usar nosso front-end para subir arquivos para nosso back-end. Agora eu posso criar um programinha pra fazer qualquer coisa com essas imagens, como redimensioná-las, algo que já foi feito aqui, etc.
Nosso app.py até o momento está assim:
#app.py
from flask import Flask, render_template, redirect, url_for
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired
from werkzeug import secure_filename
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = 'whatever'
class UploadForm(FlaskForm):
file = FileField(validators=[FileRequired()])
@app.route('/', methods=['GET', 'POST'])
def index():
form = UploadForm()
if form.validate_on_submit():
filename = secure_filename(form.file.data.filename)
form.file.data.save(f'static/images/{filename}')
return redirect(url_for('upload'))
return render_template('index.phtml', form=form)
@app.route('/uploaded', methods=['GET'])
def upload():
files = os.listdir('static/images/')
return render_template('upload.html', files=files)
app.run()
Nosso index.html:

E nosso upload.html

No próximo tópico iremos deixar nosso HTML mais bonitinho com CSS e criar mais validações pra impedir que arquivos que não sejam imagens não sejam enviados para o servidor.