Skip to content

01 - Get-it

Parte 4: Fazendo um formulário de criação de anotação

Vamos implementar agora a funcionalidade de adicionar anotações. O objetivo é que você aprenda como receber dados no servidor enviados pelo navegador.

Atenção

É possível que nas próximas etapas o servidor apresente erros inesperados. Tente acessar o servidor de um navegador com aba anônima.

Para começar, modifique o template index.html para adicionar o <form>:

<!-- DOCTYPE, HTML, HEAD DEVEM CONTINUAR AQUI -->
<body>
  <img src="img/logo-getit.png">
  <p>Como o Post-it, mas com outro verbo</p>

  <form method="post">
    <label for="titulo">Título</label>
    <input id="titulo" type="text" name="titulo" />
    <label for="detalhes">Detalhes</label>
    <input id="detalhes" name="detalhes" />
    <input type="submit" />
  </form>
  <!-- O RESTO DO HTML A PARTIR DAQUI -->

Seu código do programa principal não precisa ser modificado ainda.

EXERCÍCIO

Execute o servidor e teste a página.

Importante

Quando você for testar a página, ao clicar em submit a página simplesmente vai recarregar. É isso mesmo que deve ocorrer. Nos próximos passos nós vamos usar essa informação.

Usando os dados recebidos do formulário

Talvez você tenha notado que no formulário (<form>) existe um atributo method="post". Isso quer dizer que os dados do formulário serão enviados utilizando o método HTTP POST (veremos mais detalhes sobre ele no futuro). O que você precisa saber por enquanto é que até o momento nós sempre enviamos requisições do tipo GET para o servidor. Para entender melhor o que está acontecendo, observe a saída do seu terminal. Deve haver uma requisição parecida com essa:

POST / HTTP/1.1
Host: 0.0.0.0:8080
Connection: keep-alive
Content-Length: 25
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://0.0.0.0:8080
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://0.0.0.0:8080/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,pt;q=0.8

titulo=Sorvete+de+banana&detalhes=Coloque+uma+banana+no+congelador+e+espere.+Pronto%21+1%2B1%3D2.

Há dois pontos importantes:

  1. A primeira linha da requisição está diferente. Existe um POST ao invés de GET, mas a rota é a mesma (/).
  2. O corpo da requisição (última linha) possui os dados que o usuário digitou no formulário html.

Queremos pegar o corpo da requisição, titulo=Sorvete+de+banana&detalhes=Coloque+uma+banana+no+congelador+e+espere.+Pronto%21+1%2B1%3D2. e extrair o título e os detalhes da anotação.

Note que o que vem após titulo= é o que o usuário digitou no campo referente ao título. Porém o texto está codificado. O texto Sorvete+de+banana deveria ser Sorvete de banana.

Além disso, há o símbolo & indicando que um outro valor será apresentado em seguido.

Em seguida, nos deparamos com o texto detalhes= que representa o texto digitado pelo usuário no campo input referente aos detalhes da anotação.

O mesmo ocorre com o campo detalhes. O texto Coloque+uma+banana+no+congelador+e+espere.+Pronto%21+1%2B1%3D2. deveria ser Coloque uma banana no congelador e espere. Pronto!.

Para remover esses caracteres especiais, vamos utilizar a função urllib.parse.unquote_plus (veja a documentação aqui).

EXERCÍCIO

Vamos utilizar o começo da string de requisição para saber o seu tipo (GET ou POST).

Modifique a sua função index no arquivo views.py para conter o seguinte conteúdo:

def index(request):
    # A string de request sempre começa com o tipo da requisição (ex: GET, POST)
    if request.startswith('POST'):
        request = request.replace('\r', '')  # Remove caracteres indesejados
        # Cabeçalho e corpo estão sempre separados por duas quebras de linha
        partes = request.split('\n\n')
        corpo = partes[1]
        params = {}
        # Preencha o dicionário params com as informações do corpo da requisição
        # O dicionário conterá dois valores, o título e a descrição.
        # Posteriormente pode ser interessante criar uma função que recebe a
        # requisição e devolve os parâmetros para desacoplar esta lógica.
        # Dica: use o método split da string e a função unquote_plus
        for chave_valor in corpo.split('&'):
            # AQUI É COM VOCÊ

    # O RESTO DO CÓDIGO DA FUNÇÃO index CONTINUA DAQUI PARA BAIXO...

Você deverá escrever algum código, onde está escrito # AQUI É COM VOCÊ.

Seu objetivo é preencher o dicionário params com os valores recebidos do formulário html.

Por exemplo, se o corpo da requisição for titulo=Sorvete+de+banana&detalhes=Coloque+uma+banana+no+congelador+e+espere.+Pronto%21+1%2B1%3D2., o dicionário params deve ser preenchido com {'titulo': 'Sorvete de banana', 'detalhes': 'Coloque uma banana no congelador e espere. Pronto! 1+1=2.'}.

IMPORTANTE

Note que a função index agora está recebendo o request como argumento (index(request)).

Será necessário alterar o arquivo servidor.py para passar o request.

if filepath.is_file():
    response = read_file(filepath)
elif route == '':
    response = index(request)
else:
    response = bytes()

EXERCÍCIO

Ainda na função index(request) do arquivo views.py, adicione a nova anotação (que deverá estar armazenada em params['titulo'] e params['detalhes']) ao arquivo notes.json.

Dica: crie uma função no arquivo utils.py que recebe a nova anotação e a adiciona à lista do arquivo notes.json.

Última refatoração

Envie novamente uma nova anotação e recarregue a página. O navegador deve perguntar se você quer reenviar o formulário. Se você confirmar, a anotação deve ser adicionada mais uma vez, ficando duplicada no arquivo notes.json. Por que isso acontece? Como podemos corrigir? Para isso vamos precisar entender um pouco sobre códigos de status de respostas HTTP.

Desde a parte 1 deste handout, nós começamos o cabeçalho das nossas respostas com 'HTTP/1.1 200 OK'. O código 200 é um dos possíveis códigos de status resposta. Ele diz para o navegador que a requisição foi processada com sucesso. Para avisarmos para o navegador que depois de enviar a anotação ele deve recarregar a lista de anotações, vamos precisar do código 303, que pede para o navegador se redirecionar para outra página após receber a resposta. No nosso caso, vamos redirecionar para a mesma página, mas a nova requisição será novamente do tipo GET (para saber um pouco mais leia este artigo).

Até o momento o nosso servidor sempre utiliza o cabeçalho fixo com código 200. Precisamos refatorá-lo para que possamos responder com códigos diferentes.

EXERCÍCIO

Implemente a função build_response no arquivo utils.py. Ele deve receber os seguintes argumentos: build_response(body='', code=200, reason='OK', headers='') (talvez você queira ler isso: https://docs.python.org/3/tutorial/controlflow.html#default-argument-values).

Acesse o link a seguir para ver um exemplo de response: https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview#responses

Lembre-se de testar a sua função com python test_utils.py.

Agora que você implementou a função build_response, vamos refatorar o código para utilizá-la.

Modifique seu programa principal da seguinte maneira:

Atenção

O código abaixo funcionará somente após finalizar o exercício abaixo.

import socket
from pathlib import Path
from utils import extract_route, read_file, build_response
from views import index

CUR_DIR = Path(__file__).parent
SERVER_HOST = '0.0.0.0'
SERVER_PORT = 8080

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((SERVER_HOST, SERVER_PORT))
server_socket.listen()

print(f'Servidor escutando em (ctrl+click): http://{SERVER_HOST}:{SERVER_PORT}')

while True:
    client_connection, client_address = server_socket.accept()

    request = client_connection.recv(1024).decode()
    print('*'*100)
    print(request)

    route = extract_route(request)

    filepath = CUR_DIR / route
    if filepath.is_file():
        response = build_response() + read_file(filepath)
    elif route == '':
        response = index(request)
    else:
        response = build_response()

    client_connection.sendall(response)

    client_connection.close()

server_socket.close()

EXERCÍCIO

Modifique a função index no arquivo views.py para que ela utilize a função build_response.

Será necessário fazer duas alterações na função index:

  1. No retorno principal, será necessário alterar a última linha return load_template('index.html').format(notes=notes).encode() para utilizar a função build_response. Certifique-se de não estar chamando a função encode duas vezes.
  2. No caso de requisição com o método POST, você deve devolver o resultado de build_response(code=303, reason='See Other', headers='Location: /').

Se tudo estiver correto você pode preencher o formulário e enviar. A lista de anotações deve ser atualizada e, ao recarregar a página, o navegador não deve perguntar novamente se você quer reenviar o formulário.

Desafio

O handout acabou, mas se quiser praticar um pouco mais você pode fazer o servidor devolver uma resposta com o código 404 quando a requisição é feita a uma página/recurso que não existe (dica: você só vai precisar modificar uma linha).

Ufa, cansei

Parabéns! Agora você pode tentar fazer alguma das receitas da nossa lista de anotações. Depois disso, se ainda tiver pique, é um bom momento para dar aquela relembrada em CSS para a próxima aula com essa lista de jogos: