Flask Web Development Developing
Flask Web Development Developing
Miguel Grinberg
Machine Translated by Google
Publicado por O'Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA 95472.
Os livros da O'Reilly podem ser adquiridos para uso educacional, comercial ou promocional de vendas. Edições online também estão disponíveis
para a maioria dos títulos (http:// my.safaribooksonline.com). Para obter mais informações, entre em contato com nosso departamento de vendas
corporativo/institucional: 800-998-9938 ou [email protected].
Nutshell Handbook, o logotipo do Nutshell Handbook e o logotipo da O'Reilly são marcas registradas da O'Reilly Media, Inc. Flask Web
Development, a imagem de um Pyrenean Mastiff e a identidade visual relacionada são marcas comerciais da O'Reilly Media, Inc.
Muitas das designações usadas por fabricantes e vendedores para distinguir seus produtos são reivindicadas como marcas registradas. Onde
essas designações aparecem neste livro, e a O'Reilly Media, Inc. estava ciente de uma reivindicação de marca registrada, as designações foram
impressas em letras maiúsculas ou iniciais maiúsculas.
Embora tenham sido tomadas todas as precauções na preparação deste livro, a editora e os autores não assumem nenhuma responsabilidade
por erros ou omissões, ou por danos resultantes do uso das informações aqui contidas.
ISBN: 978-1-449-37262-0
[LSI]
Machine Translated by Google
Para Alícia.
Machine Translated by Google
Machine Translated by Google
Índice
Prefácio. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XI
1. Instalação. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Usando Ambientes Virtuais 4
Instalando pacotes Python com pip 6
3. Modelos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
O mecanismo de modelo Jinja2 22
Modelos de renderização 22
Variáveis 23
Estruturas de controle 24
Integração do Twitter Bootstrap com Flask-Bootstrap 26
Páginas de erro personalizadas 29
links 31
em
Machine Translated by Google
Arquivos Estáticos 32
Localização de Datas e Horas com Flask-Moment 33
4. Formulários da Web. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Mensagem piscando 46
5. Bancos de dados. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Bancos de dados 49
definição de 56
modelo Operações de 57
linhas Consultar 60
linhas 60
Uso de banco de dados em funções de exibição 62
6. E-mail. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
de opções de configuração da 76
estrutura do projeto 78
nós
| Índice
Machine Translated by Google
Script de inicialização 81
Arquivo de Requisitos 82
Testes de unidade 83
8. Autenticação do usuário. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
Extensões de autenticação para Flask Password 89
Security Hashing de 90
usuários Testando 97
usuário Adicionando 99
Índice | vii
Machine Translated by Google
versão de 178
serviços da Web RESTful com Flask 179
viii | Índice
Machine Translated by Google
Índice. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
Índice | ix
Machine Translated by Google
Machine Translated by Google
Prefácio
O Flask se destaca de outras estruturas porque permite que os desenvolvedores assumam o controle e
tenham total controle criativo de seus aplicativos. Talvez você já tenha ouvido a frase “combater a estrutura”
antes. Isso acontece com a maioria dos frameworks quando você decide resolver um problema com uma
solução que não é a oficial. Pode ser que você queira usar um mecanismo de banco de dados diferente ou
talvez um método diferente de autenticação de usuários.
Desviar-se do caminho definido pelos desenvolvedores do framework lhe dará muitas dores de cabeça.
Frasco não é assim. Você gosta de bancos de dados relacionais? Ótimo. Flask suporta todos eles.
Talvez você prefira um banco de dados NoSQL? Não há problema algum. Flask também trabalha com eles.
Quer usar seu próprio mecanismo de banco de dados caseiro? Não precisa de um banco de dados?
Continua tudo bem. Com o Flask você pode escolher os componentes do seu aplicativo ou até mesmo
escrever o seu próprio, se assim o desejar. Sem perguntas!
A chave para essa liberdade é que o Flask foi projetado desde o início para ser estendido. Ele vem com um
núcleo robusto que inclui a funcionalidade básica de que todos os aplicativos da Web precisam e espera
que o restante seja fornecido por algumas das muitas extensões de terceiros no ecossistema e, é claro, por
você.
Neste livro, apresento meu fluxo de trabalho para desenvolver aplicativos da Web com Flask. Não pretendo
ter a única maneira verdadeira de criar aplicativos com essa estrutura. Você deve considerar minhas
escolhas como recomendações e não como evangelho.
A maioria dos livros de desenvolvimento de software fornece exemplos de código pequenos e focados que
demonstram os diferentes recursos da tecnologia-alvo isoladamente, deixando o código “cola” necessário
para transformar esses diferentes recursos em aplicativos totalmente funcionais para serem descobertos
pelo leitor . Eu tenho uma abordagem completamente diferente. Todos os exemplos que apresento fazem
parte de um único aplicativo que começa muito simples e é expandido em cada capítulo sucessivo. Este
aplicativo começa com apenas algumas linhas de código e termina como um aplicativo de blog e rede social
bem caracterizado.
XI
Machine Translated by Google
Você deve ter algum nível de experiência em codificação Python para aproveitar ao máximo este livro.
Embora o livro não assuma nenhum conhecimento prévio do Flask, os conceitos do Python, como
pacotes, módulos, funções, decoradores e programação orientada a objetos, devem ser bem
compreendidos. Alguma familiaridade com exceções e diagnóstico de problemas de rastreamentos
de pilha será muito útil.
Ao trabalhar com os exemplos deste livro, você passará muito tempo na linha de comando. Você deve
se sentir confortável usando a linha de comando do seu sistema operacional.
Aplicações web modernas não podem evitar o uso de HTML, CSS e JavaScript. O aplicativo de
exemplo desenvolvido ao longo do livro obviamente faz uso delas, mas o livro em si não entra em
muitos detalhes sobre essas tecnologias e como elas são usadas. Algum grau de familiaridade com
essas linguagens é recomendado se você pretende desenvolver aplicativos completos sem a ajuda
de um desenvolvedor versado em técnicas do lado do cliente.
Lancei o aplicativo complementar deste livro como código-fonte aberto no GitHub. Embora o GitHub
possibilite o download de aplicativos como arquivos ZIP ou TAR comuns, recomendo fortemente que
você instale um cliente Git e se familiarize com o controle de versão do código-fonte, pelo menos com
os comandos básicos para clonar e verificar as diferentes versões do aplicativo diretamente do
repositório. A pequena lista de comandos necessários é mostrada em “Como trabalhar com o código
de exemplo” na página xiii. Você também vai querer usar o controle de versão para seus próprios
projetos, então use este livro como uma desculpa para aprender Git!
Finalmente, este livro não é uma referência completa e exaustiva sobre o framework Flask.
A maioria dos recursos é abordada, mas você deve complementar este livro com a documentação
oficial do Flask .
xii | Prefácio
Machine Translated by Google
Capítulo 7 apresenta uma estrutura de aplicativo apropriada para aplicativos de médio e grande porte.
Parte II, Exemplo: um aplicativo de blog social, cria o Flasky, o aplicativo de blog e rede social de código aberto
que desenvolvi para este livro:
A Parte III, The Last Mile, descreve algumas tarefas importantes não diretamente relacionadas à codificação do
aplicativo que precisam ser consideradas antes de publicar um aplicativo:
O histórico de commits neste repositório foi cuidadosamente criado para corresponder à ordem em que os
conceitos são apresentados no livro. A maneira recomendada de trabalhar com o código é verificar os commits
começando pelo mais antigo e, em seguida, avançar pela lista de commits à medida que avança no livro. Como
alternativa, o GitHub também permitirá que você baixe cada commit como um arquivo ZIP ou TAR.
Se você decidir usar o Git para trabalhar com o código-fonte, precisará instalar o cliente Git, que pode ser baixado
em http:// git-scm.com. O comando a seguir baixa o código de exemplo usando o Git:
Prefácio | xiii
Machine Translated by Google
O comando git clone instala o código-fonte do GitHub em uma pasta flasky criada no diretório atual. Esta
pasta não contém apenas o código-fonte; também está incluída uma cópia do repositório Git com todo o
histórico de alterações feitas no aplicativo.
No primeiro capítulo você será solicitado a verificar o lançamento inicial do aplicativo e, em seguida, nos
locais apropriados, será instruído a avançar no histórico.
O comando Git que permite que você percorra o histórico de alterações é git checkout. Aqui está um
exemplo:
$ git checkout 1a
Além de verificar os arquivos de origem para uma versão do aplicativo, pode ser necessário realizar
algumas configurações. Por exemplo, em alguns casos, você precisará instalar pacotes Python adicionais
ou aplicar atualizações no banco de dados. Você será informado quando estes
são necessários.
Normalmente, você não modificará os arquivos de origem do aplicativo, mas se o fizer, o Git não permitirá
que você verifique uma revisão diferente, pois isso causaria a perda de suas alterações locais. Antes de
verificar uma revisão diferente, você precisará reverter os arquivos ao seu estado original. A maneira mais
fácil de fazer isso é com o comando git reset :
Este comando destruirá suas alterações locais, então você deve salvar qualquer coisa que não queira
perder antes de usar este comando.
De tempos em tempos, você pode querer atualizar seu repositório local daquele no GitHub, onde correções
de bugs e melhorias podem ter sido aplicadas. Os comandos que conseguem isso são:
Os comandos git fetch são usados para atualizar o histórico de confirmação e as tags em seu repositório
local a partir do remoto no GitHub, mas nada disso afeta os arquivos de origem reais, que são atualizados
com o comando git reset a seguir . Mais uma vez, esteja ciente de que sempre que git reset for usado,
você perderá todas as alterações locais feitas.
Outra operação útil é visualizar todas as diferenças entre duas versões do aplicativo. Isso pode ser muito
útil para entender uma mudança em detalhes. Do comando
xiv | Prefácio
Machine Translated by Google
linha, o comando git diff pode fazer isso. Por exemplo, para ver a diferença entre as revisões 2a e 2b, use:
$ git diff 2a 2b
As diferenças são mostradas como um patch, que não é um formato muito intuitivo para revisar as
alterações se você não estiver acostumado a trabalhar com arquivos de patch. Você pode achar que as
comparações gráficas mostradas pelo GitHub são muito mais fáceis de ler. Por exemplo, as diferenças
entre as revisões 2a e 2b podem ser visualizadas no GitHub em https:// github.com/ miguelgrin berg/ flasky/
compare/ 2a...2b
Agradecemos, mas não exigimos, atribuição. Uma atribuição geralmente inclui o título, autor, editora e
ISBN. Por exemplo: “Flask Web Development por Miguel Grinberg (O'Reilly). Copyright 2014 Miguel
Grinberg, 978-1-449-3726-2.”
Se você acha que o uso de exemplos de código está fora do uso justo ou da permissão dada acima, sinta-
se à vontade para nos contatar em [email protected].
Largura constante
Usado para listagens de programas, bem como dentro de parágrafos para se referir a elementos de
programas como nomes de variáveis ou funções, bancos de dados, tipos de dados, variáveis de
ambiente, instruções e palavras-chave.
Largura constante em
negrito Mostra comandos ou outro texto que deve ser digitado literalmente pelo usuário.
Prefácio | xv
Machine Translated by Google
XVI | Prefácio
Machine Translated by Google
Temos uma página web para este livro, onde listamos errata, exemplos e qualquer informação
adicional. Você pode acessar esta página em http:// www.bit.ly/ flask-web-dev.
Para comentar ou fazer perguntas técnicas sobre este livro, envie um e-mail para
[email protected] .
Para obter mais informações sobre nossos livros, cursos, conferências e notícias, consulte nosso
site em http:// www.oreilly.com.
Agradecimentos
Eu não poderia ter escrito este livro sozinho. Recebi muita ajuda da família, colegas de trabalho,
velhos amigos e novos amigos que fiz ao longo do caminho.
Gostaria de agradecer a Brendan Kohler por sua revisão técnica detalhada e por sua ajuda para
dar forma ao capítulo sobre Interfaces de Programação de Aplicativos. Também devo muito a
David Baumgold, Todd Brunhoff, Cecil Rock e Matthew Hugues, que revisaram o manuscrito em
diferentes estágios de conclusão e me deram conselhos muito úteis sobre o que cobrir e como
organizar o material.
Escrever os exemplos de código para este livro foi um esforço considerável. Agradeço a ajuda de
Daniel Hofmann, que fez uma revisão completa do código do aplicativo e apontou várias melhorias.
Também sou grato a meu filho adolescente, Dylan Grinberg, que suspendeu seu vício em Minecraft
por alguns fins de semana e me ajudou a testar o código em várias plataformas.
A O'Reilly tem um programa maravilhoso chamado Early Release que permite que leitores
impacientes tenham acesso aos livros enquanto eles estão sendo escritos. Alguns dos meus
leitores do Early Release foram além e se envolveram em conversas úteis sobre sua experiência
de trabalho no livro, levando a melhorias significativas. eu gostaria de reconhecer
fingir | xvii
Machine Translated by Google
Sundeep Gupta, Dan Caron, Brian Wisti e Cody Scott, em particular, pelas contribuições que
fizeram a este livro.
A equipe da O'Reilly Media sempre esteve ao meu lado. Acima de tudo, gostaria de agradecer
à minha maravilhosa editora, Meghan Blanchette, por seu apoio, conselhos e assistência
desde o primeiro dia em que nos conhecemos. Meg tornou memorável a experiência de
escrever meu primeiro livro.
xviii | Prefácio
Machine Translated by Google
PARTE I
Introdução ao Flask
Machine Translated by Google
Machine Translated by Google
CAPÍTULO 1
Instalação
Frasco é um pequeno framework para a maioria dos padrões, pequeno o suficiente para ser chamado
de “micro framework”. É pequeno o suficiente para que, assim que você se familiarizar com ele,
provavelmente seja capaz de ler e entender todo o seu código-fonte.
Mas ser pequeno não significa que ele faça menos que outros frameworks. O Flask foi projetado
como uma estrutura extensível desde o início; ele fornece um núcleo sólido com os serviços
básicos, enquanto as extensões fornecem o resto. Como você pode escolher os pacotes de
extensão que deseja, você acaba com uma pilha enxuta que não tem inchaço e faz exatamente
o que você precisa.
Não há suporte nativo no Flask para acessar bancos de dados, validar formulários da web,
autenticar usuários ou outras tarefas de alto nível. Esses e muitos outros serviços importantes
que a maioria dos aplicativos da Web precisam estão disponíveis por meio de extensões que se
integram aos pacotes principais. Como desenvolvedor, você tem o poder de escolher as extensões
que funcionam melhor para o seu projeto ou até mesmo escrever as suas próprias, se desejar.
Isso contrasta com uma estrutura maior, em que a maioria das escolhas foram feitas para você e
são difíceis ou às vezes impossíveis de mudar.
Neste capítulo, você aprenderá como instalar o Flask. O único requisito necessário é um
computador com Python instalado.
3
Machine Translated by Google
Os ambientes virtuais são muito úteis porque evitam confusão de pacotes e conflitos de versão no
interpretador Python do sistema. A criação de um ambiente virtual para cada aplicativo garante que os
aplicativos tenham acesso apenas aos pacotes que usam, enquanto o interpretador global permanece
organizado e limpo e serve apenas como uma fonte a partir da qual mais ambientes virtuais podem ser
criados. Como benefício adicional, os ambientes virtuais não exigem direitos de administrador.
Os ambientes virtuais são criados com o utilitário virtualenv de terceiros . Para verificar se você o possui
instalado em seu sistema, digite o seguinte comando:
$ virtualenv --version
A maioria das distribuições Linux fornece um pacote para virtualenv. Por exemplo, os usuários do
Ubuntu podem instalá-lo com este comando:
Se você estiver usando o Microsoft Windows ou qualquer sistema operacional que não forneça um
pacote virtualenv oficial, terá um procedimento de instalação um pouco mais complicado.
Usando seu navegador da Web, navegue até https:// bitbucket.org/ pypa/ setuptools, a página inicial do
instalador do setuptools . Nessa página, procure um link para baixar o script do instalador.
Este é um script chamado ez_setup.py. Salve este arquivo em uma pasta temporária em seu
computador e execute os seguintes comandos nessa pasta:
$ python ez_setup.py $
easy_install virtualenv
4 | Capítulo 1: Instalação
Machine Translated by Google
Agora você precisa criar a pasta que hospedará o código de exemplo, disponível em um
repositório do GitHub. Conforme discutido na página xiii “Como trabalhar com o código de sobre
O próximo passo é criar o ambiente virtual Python dentro da pasta flasky usando o comando
virtualenv. Este comando possui um único argumento obrigatório: o nome do ambiente virtual.
Uma pasta com o nome escolhido será criada no diretório atual e todos os arquivos associados
ao ambiente virtual estarão dentro dela. Uma convenção de nomenclatura comumente usada
para ambientes virtuais é chamá-los de venv:
$ virtualenv venv
Novo executável python em venv/bin/python2.7
Também criando executável em venv/bin/python
Instalando setuptools............concluído.
Instalando o pip...............concluído.
Agora você tem uma pasta venv dentro da pasta flasky com um novo ambiente virtual que
contém um interpretador Python privado. Para começar a usar o ambiente virtual, você deve
“ativá-lo”. Se você estiver usando uma linha de comando bash (usuários de Linux e Mac OS X),
poderá ativar o ambiente virtual com este comando:
$ fonte venv/bin/ativar
$ venv\Scripts\ativar
(venv) $
Com este comando, o Flask e suas dependências são instalados no ambiente virtual.
Você pode verificar se o Flask foi instalado corretamente iniciando o interpretador Python e
tentando importá-lo:
Se nenhum erro aparecer, você pode se parabenizar: você está pronto para o próximo
capítulo, onde escreverá sua primeira aplicação web.
6 | Capítulo 1: Instalação
Machine Translated by Google
CAPÍTULO 2
Neste capítulo, você aprenderá sobre as diferentes partes de um aplicativo Flask. Você também
escreverá e executará seu primeiro aplicativo web Flask.
Inicialização
Todos os aplicativos Flask devem criar uma instância do aplicativo. O servidor web passa todas as
requisições que recebe dos clientes para este objeto para manipulação, usando um protocolo chamado
Web Server Gateway Interface (WSGI). A instância da aplicação é um objeto da classe Flask,
normalmente criado da seguinte forma:
O único argumento necessário para o construtor da classe Flask é o nome do módulo principal ou
pacote do aplicativo. Para a maioria dos aplicativos, a variável __name__ do Python é o valor correto.
Posteriormente, você verá exemplos mais complexos de inicialização de aplicativos, mas para
aplicativos simples isso é tudo o que é necessário.
7
Machine Translated by Google
Clientes como navegadores da web enviam solicitações para o servidor da web, que por sua vez as envia para a
instância do aplicativo Flask. A instância do aplicativo precisa saber qual código precisa ser executado para cada
URL solicitado, para manter um mapeamento de URLs para funções Python.
A associação entre uma URL e a função que a manipula é chamada de rota.
A maneira mais conveniente de definir uma rota em um aplicativo Flask é por meio do decorador app.route exposto
pela instância do aplicativo, que registra a função decorada como uma rota. O exemplo a seguir mostra como uma
rota é declarada usando este decorador:
@app.route('/')
def index():
return '<h1>Hello World!</h1>'
O exemplo anterior registra a função index() como o manipulador da URL raiz do aplicativo. Se esse aplicativo
fosse implantado em um servidor associado ao nome de domínio www.example.com , navegar para http://
www.example.com em seu navegador acionaria index() para ser executado no servidor. O valor de retorno dessa
função, chamado de resposta, é o que o cliente recebe. Se o cliente for um navegador da Web, a resposta será o
documento exibido ao usuário.
Funções como index() são chamadas de funções de visualização. Uma resposta retornada por uma função de
exibição pode ser uma string simples com conteúdo HTML, mas também pode assumir formas mais complexas,
como você verá mais adiante.
Se você prestar atenção em como são formadas algumas URLs de serviços que você usa todos os dias, notará
que muitas têm seções variáveis. Por exemplo, a URL da sua página de perfil do Facebook é http://
www.facebook.com/ <seu-nome>, portanto, seu nome de usuário faz parte dela. O Flask oferece suporte a esses
tipos de URLs usando uma sintaxe especial no decorador de rota .
O exemplo a seguir define uma rota que possui um componente de nome dinâmico:
@app.route('/usuário/<nome>')
def usuário(nome):
return '<h1>Olá, %s!</h1>' % nome
A parte entre colchetes angulares é a parte dinâmica, portanto, qualquer URL que corresponda às
partes estáticas será mapeada para esta rota. Quando a função view é invocada, Flask envia o
componente dinâmico como um argumento. Na função de exibição de exemplo anterior, esse
argumento é usado para gerar uma saudação personalizada como resposta.
Os componentes dinâmicos nas rotas são strings por padrão, mas também podem ser definidos
com um tipo. Por exemplo, route /user/<int:id> corresponderia apenas a URLs que tivessem um
número inteiro no segmento dinâmico id . Flask suporta tipos int, float e path para rotas. O tipo de
caminho também representa uma string, mas não considera as barras como separadores e, em
vez disso, as considera parte do componente dinâmico.
Inicialização do servidor
A instância do aplicativo tem um método run que inicia o servidor web de desenvolvimento
integrado do Flask:
if __name__ == '__main__':
app.run(debug=True)
O idioma Python __name__ == '__main__' é usado aqui para garantir que o servidor da Web de
desenvolvimento seja iniciado apenas quando o script for executado diretamente. Quando o script
é importado por outro script, supõe-se que o script pai iniciará um servidor diferente, portanto, a
chamada app.run() é ignorada.
Depois que o servidor é inicializado, ele entra em um loop que espera por solicitações e as atende.
Esse loop continua até que o aplicativo seja interrompido, por exemplo, pressionando Ctrl-C.
Existem vários argumentos opcionais que podem ser fornecidos ao app.run() para configurar o
modo de operação do servidor web. Durante o desenvolvimento, é conveniente ativar o modo de
depuração, que entre outras coisas ativa o depurador e o recarregador. Isso é feito passando o
argumento debug definido como True.
Inicialização do servidor | 9
Machine Translated by Google
@app.route('/')
def index():
return '<h1>Hello World!</h1>'
if __name__ == '__main__':
app.run(debug=True)
Para executar o aplicativo, certifique-se de que o ambiente virtual criado anteriormente esteja ativado
e tenha o Flask instalado. Agora abra seu navegador e digite http:// 127.0.0.1:5000/ na barra de
endereços. A Figura 2-1 mostra o navegador da Web após a conexão com o aplicativo.
Se você digitar qualquer outro URL, o aplicativo não saberá como lidar com ele e retornará um
código de erro 404 para o navegador - o erro familiar que você recebe quando navega para
uma página da Web que não existe.
A versão aprimorada do aplicativo mostrada no Exemplo 2-2 adiciona uma segunda rota que é
dinâmica. Ao visitar esse URL, você recebe uma saudação personalizada.
@app.route('/') def
index(): return
'<h1>Hello World!</h1>'
@app.route('/usuário/<nome>')
def usuário(nome):
return '<h1>Olá, %s!</h1>' % nome
if __name__ == '__main__':
app.run(debug=True)
Para testar a rota dinâmica, verifique se o servidor está em execução e navegue até http://
localhost:5000/user/Dave. O aplicativo responderá com uma saudação personalizada, gerada
usando o argumento dinâmico de nome . Tente nomes diferentes para ver como a função view
sempre gera a resposta com base no nome fornecido. Um exemplo é mostrado na Figura 2-2.
O ciclo de solicitação-resposta
Agora que você jogou com um aplicativo Flask básico, talvez queira saber mais sobre como o Flask faz
sua mágica. As seções a seguir descrevem alguns dos aspectos de design da estrutura.
A maneira óbvia pela qual o Flask poderia dar acesso a uma função de exibição ao objeto de solicitação
é enviando-o como um argumento, mas isso exigiria que cada função de exibição no aplicativo tivesse
um argumento extra. As coisas ficam mais complicadas se você considerar que o objeto de solicitação
não é o único objeto que as funções de exibição podem precisar acessar para atender a uma solicitação.
Para evitar sobrecarregar as funções de exibição com muitos argumentos que podem ou não ser
necessários, o Flask usa contextos para tornar temporariamente certos objetos globalmente acessíveis.
Graças aos contextos, funções de visualização como a seguinte podem ser escritas:
@app.route('/')
def index():
Observe como nesta exibição a função request é usada como se fosse uma variável global. Na
realidade, a requisição não pode ser uma variável global se você considerar que em um servidor
multithread as threads estão trabalhando em diferentes requisições de diferentes clientes ao mesmo
tempo, então cada thread precisa ver um objeto diferente na requisição . Os contextos permitem que o
Flask torne certas variáveis globalmente acessíveis a um thread sem interferir nos outros threads.
Existem dois contextos no Flask: o contexto do aplicativo e o contexto da solicitação. A Tabela 2-1
mostra as variáveis expostas por cada um desses contextos.
g Contexto do aplicativo Um objeto que o aplicativo pode usar para armazenamento temporário durante a manipulação de
um pedido. Essa variável é redefinida a cada solicitação.
solicitar Solicitar contexto O objeto de solicitação, que encapsula o conteúdo de uma solicitação HTTP enviada pelo
cliente.
sessão Solicitar contexto A sessão do usuário, um dicionário que o aplicativo pode usar para armazenar valores que
são “lembrados” entre as solicitações.
O Flask ativa (ou envia) o aplicativo e os contextos de solicitação antes de despachar uma solicitação
e, em seguida, os remove quando a solicitação é tratada. Quando o contexto do aplicativo é enviado,
as variáveis current_app eg ficam disponíveis para o encadeamento; da mesma forma, quando o
contexto de solicitação é enviado, a solicitação e a sessão também ficam disponíveis. Se alguma
dessas variáveis for acessada sem um aplicativo ativo ou contexto de solicitação, um erro será gerado.
As quatro variáveis de contexto serão revisitadas em capítulos posteriores em detalhes, então não se
preocupe se você ainda não entender por que elas são úteis.
O ciclo de solicitação-resposta | 13
Machine Translated by Google
Neste exemplo, current_app.name falha quando não há nenhum contexto de aplicativo ativo, mas
torna-se válido assim que um contexto é enviado. Observe como um contexto de aplicativo é obtido
invocando app.app_context() na instância do aplicativo.
Despacho de solicitação
Quando o aplicativo recebe uma solicitação de um cliente, ele precisa descobrir qual função de
visualização invocar para atendê-la. Para esta tarefa, o Flask procura o URL fornecido na solicitação
no mapa de URL do aplicativo, que contém um mapeamento de URLs para as funções de exibição
que os manipulam. Flask constrói este mapa usando os decorators app.route ou a versão não
decorador equivalente app.add_url_rule().
Para ver como é o mapa de URL em um aplicativo Flask, você pode inspecionar o mapa criado para
hello.py no shell do Python. Para este teste, certifique-se de que seu ambiente virtual esteja ativado:
(venv) $ python
>>> from hello import app >>>
app.url_map
Map([<Rule '/' (HEAD, OPTIONS, GET) -> index>, <Rule '/
static/<filename>' (HEAD , OPTIONS, GET) -> static>, <Regra '/user/
<nome>' (HEAD, OPTIONS, GET) -> user>])
As rotas / e /user/ <nome> foram definidas pelos decoradores app.route no aplicativo. A rota /static/
<filename> é uma rota especial adicionada pelo Flask para dar acesso a arquivos estáticos. Você
aprenderá mais sobre arquivos estáticos no Capítulo 3.
Os elementos HEAD, OPTIONS, GET mostrados no mapa de URL são os métodos de solicitação
tratados pela rota. O Flask anexa métodos a cada rota para que diferentes métodos de solicitação
enviados para a mesma URL possam ser manipulados por diferentes funções de visualização. Os
métodos HEAD e OPTIONS são gerenciados automaticamente pelo Flask, então na prática pode-se
dizer que nesta aplicação as três rotas do mapa de URL são anexadas ao método GET .
Você aprenderá como especificar diferentes métodos de solicitação para rotas no Capítulo 4.
Ganchos de
solicitação Às vezes, é útil executar o código antes ou depois de cada solicitação ser processada.
Por exemplo, no início de cada solicitação pode ser necessário criar uma conexão com o banco de
dados ou autenticar o usuário que está fazendo a solicitação. Em vez de duplicar o código que faz
isso em todas as funções de exibição, o Flask oferece a opção de registrar funções comuns a serem
invocadas antes ou depois de uma solicitação ser despachada para uma função de exibição.
Os ganchos de solicitação são implementados como decoradores. Estes são os quatro ganchos suportados
pelo Flask:
• before_first_request: Registre uma função para executar antes que a primeira solicitação seja
manipulado.
after_request: Registre uma função para executar após cada solicitação, se nenhuma exceção não tratada
ocorreu. •
teardown_request: Registre uma função para executar após cada solicitação, mesmo que não seja tratada
ocorreram exceções.
Um padrão comum para compartilhar dados entre funções de gancho de solicitação e funções de exibição é
usar o g context global. Por exemplo, um manipulador before_request pode carregar o usuário logado do
banco de dados e armazená-lo em g.user. Mais tarde, quando a função view é invocada, ela pode acessar o
usuário a partir daí.
Exemplos de ganchos de solicitação serão mostrados em capítulos futuros, então não se preocupe se isso
ainda não fizer sentido.
Respostas
Quando o Flask invoca uma função de exibição, ele espera que seu valor de retorno seja a resposta à
solicitação. Na maioria dos casos, a resposta é uma string simples que é enviada de volta ao cliente como uma
página HTML.
Mas o protocolo HTTP requer mais do que uma string como resposta a uma solicitação. Uma parte muito
importante da resposta HTTP é o código de status, que Flask define por padrão como 200, o código que indica
que a solicitação foi realizada com sucesso.
Quando uma função de exibição precisa responder com um código de status diferente, ela pode adicionar o
código numérico como um segundo valor de retorno após o texto de resposta. Por exemplo, a função de
exibição a seguir retorna um código de status 400, o código para um erro de solicitação inválida:
@app.route('/') def
index(): return
'<h1>Solicitação incorreta</h1>', 400
As respostas retornadas pelas funções de exibição também podem receber um terceiro argumento, um dicionário
de cabeçalhos que são adicionados à resposta HTTP. Isso raramente é necessário, mas você verá um exemplo
no Capítulo 14.
Em vez de retornar um, dois ou três valores como uma tupla, as funções de exibição do Flask têm a opção de
retornar um objeto Response . A função make_response() recebe um, dois ou três argumentos, os mesmos
valores que podem ser retornados de uma função view e retorna um objeto Response . Algumas vezes é útil
realizar esta conversão dentro do
O ciclo de solicitação-resposta | 15
Machine Translated by Google
view e, em seguida, use os métodos do objeto de resposta para configurar ainda mais a resposta. O exemplo a seguir
cria um objeto de resposta e define um cookie nele:
@app.route('/') def
index(): response
= make_response('<h1>Este documento contém um cookie!</h1>') response.set_cookie('answer',
'42') return response
Existe um tipo especial de resposta chamado redirecionamento. Esta resposta não inclui um documento de página;
apenas fornece ao navegador um novo URL a partir do qual carregar uma nova página.
Redirecionamentos são comumente usados com formulários da web, como você aprenderá no Capítulo 4.
Um redirecionamento é normalmente indicado com um código de status de resposta 302 e o URL para redirecionar
fornecido em um cabeçalho de localização . Uma resposta de redirecionamento pode ser gerada usando um retorno
de três valores ou também com um objeto Response , mas devido ao seu uso frequente, o Flask fornece uma função
auxiliar redirect() que cria esta resposta:
@app.route('/') def
index():
return redirecionamento('http://www.example.com')
Outra resposta especial é emitida com a função abort , que é usada para tratamento de erros. O exemplo a seguir
retorna o código de status 404 se o argumento id dinâmico fornecido na URL não representa um usuário válido:
@app.route('/user/<id>') def
get_user(id): user =
load_user(id) if not user:
abort(404)
return
'<h1>Hello, %s</h1>' % user .nome
Observe que abort não retorna o controle para a função que o chama, mas devolve o controle para o servidor da web
levantando uma exceção.
Extensões de balão
Flask é projetado para ser estendido. Ele fica intencionalmente fora de áreas de funcionalidade importante, como
banco de dados e autenticação de usuário, dando a você a liberdade de selecionar os pacotes que melhor se adaptam
ao seu aplicativo ou de escrever o seu próprio, se desejar.
Existe uma grande variedade de extensões para muitos propósitos diferentes que foram criadas pela
comunidade e, se isso não for suficiente, qualquer pacote ou biblioteca padrão do Python também
pode ser usado. Para lhe dar uma ideia de como uma extensão é incorporada a um aplicativo, a
seção a seguir adiciona uma extensão a hello.py que aprimora o aplicativo com argumentos de linha
de comando.
Flask-Script é uma extensão para Flask que adiciona um analisador de linha de comando ao seu
aplicativo Flask. Ele vem com um conjunto de opções de uso geral e também oferece suporte a
comandos personalizados.
O exemplo 2-3 mostra as alterações necessárias para adicionar análise de linha de comando ao
aplicativo hello.py .
# ...
if __name__ == '__main__':
manager.run()
Extensões desenvolvidas especificamente para Flask são expostas no namespace flask.ext . Flask-
Script exporta uma classe chamada Manager, que é importada de flask.ext.script.
O método de inicialização desta extensão é comum a muitas extensões: uma instância da classe
principal é inicializada passando a instância da aplicação como um argumento para o construtor. O
objeto criado é então usado conforme apropriado para cada extensão. Nesse caso, a inicialização do
servidor é roteada por meio de manager.run(), onde a linha de comando é analisada.
Extensões de Frascos | 17
Machine Translated by Google
Com essas alterações, o aplicativo adquire um conjunto básico de opções de linha de comando. A
execução de hello.py agora mostra uma mensagem de uso:
Uso de $ python
hello.py: hello.py [-h] {shell,runserver} ...
argumentos posicionais:
shell {shell,runserver}
Executa um shell Python dentro do contexto do aplicativo Flask.
servidor de execução Executa o servidor de desenvolvimento do Flask, ou seja, app.run()
O comando shell é usado para iniciar uma sessão de shell Python no contexto do aplicativo. Você pode
usar esta sessão para executar tarefas ou testes de manutenção ou para depurar problemas.
O comando runserver , como o próprio nome indica, inicia o servidor web. A execução de python hello.py
runserver inicia o servidor da Web no modo de depuração, mas há muito mais opções disponíveis:
O argumento --host é uma opção útil porque informa ao servidor da Web qual interface de rede escutar
para conexões de clientes. Por padrão, o servidor web de desenvolvimento do Flask escuta as conexões
no host local, portanto, apenas as conexões originadas no computador que executa o servidor são aceitas.
O seguinte comando faz com que o servidor web escute conexões na interface de rede pública, permitindo
que outros computadores da rede também se conectem:
O servidor web agora deve estar acessível a partir de qualquer computador na rede em http://
abcd:5000, onde “abcd” é o endereço IP externo do computador que executa o
servidor.
Este capítulo introduziu o conceito de respostas a solicitações, mas há muito mais a dizer
sobre respostas. O Flask fornece um suporte muito bom para gerar respostas usando modelos,
e esse é um tópico tão importante que o próximo capítulo é dedicado a ele.
Extensões de Frascos | 19
Machine Translated by Google
Machine Translated by Google
CAPÍTULO 3
Modelos
A chave para escrever aplicativos fáceis de manter é escrever um código limpo e bem estruturado.
Os exemplos que você viu até agora são muito simples para demonstrar isso, mas as funções de
exibição do Flask têm dois propósitos completamente independentes disfarçados de um, o que cria
um problema.
A tarefa óbvia de uma função view é gerar uma resposta a uma requisição, como você viu nos
exemplos mostrados no Capítulo 2. Para as requisições mais simples isso é suficiente, mas em geral
uma requisição aciona uma mudança no estado da aplicação , e a função de exibição também é
onde essa alteração é gerada.
Por exemplo, considere um usuário que está registrando uma nova conta em um site. O usuário
digita um endereço de e-mail e uma senha em um formulário da Web e clica no botão Enviar. No
servidor, chega uma requisição que inclui os dados do usuário e o Flask a despacha para a função
view que trata das requisições de cadastro. Essa função de exibição precisa conversar com o banco
de dados para adicionar o novo usuário e, em seguida, gerar uma resposta para enviar de volta ao
navegador. Esses dois tipos de tarefas são formalmente chamados de lógica de negócios e lógica
de apresentação, respectivamente.
Um modelo é um arquivo que contém o texto de uma resposta, com variáveis de espaço reservado
para as partes dinâmicas que serão conhecidas apenas no contexto de uma solicitação. O processo
que substitui as variáveis por valores reais e retorna uma string de resposta final é chamado de
renderização. Para a tarefa de renderizar modelos, o Flask usa um poderoso mecanismo de modelo
chamado Jinja2.
21
Machine Translated by Google
A resposta retornada pela função view user() do Exemplo 2-2 tem um componente dinâmico, que é
representado por uma variável. O exemplo 3-2 mostra o modelo que implementa esta resposta.
padrão, o Flask procura modelos em uma subpasta de modelos localizada dentro da pasta do
aplicativo. Para a próxima versão do hello.py, você precisa armazenar os modelos definidos
anteriormente em uma nova pasta de modelos como index.html e user.html.
As funções de exibição no aplicativo precisam ser modificadas para renderizar esses modelos.
O Exemplo 3-3 mostra essas mudanças.
# ...
@app.route('/index') def
index():
return render_template('index.html')
@app.route('/user/<nome>') def
user(name):
return render_template('user.html', name=name)
A função render_template fornecida pelo Flask integra o mecanismo de modelo Jinja2 com o aplicativo.
Esta função usa o nome do arquivo do modelo como seu primeiro argumento. Quaisquer argumentos
adicionais são pares chave/valor que representam valores reais para variáveis referenciadas no
modelo. Neste exemplo, o segundo modelo está recebendo
uma variável de nome .
Argumentos de palavra-chave como nome=nome no exemplo anterior são bastante comuns, mas
podem parecer confusos e difíceis de entender se você não estiver acostumado com eles. O “nome” no
22 | Capítulo 3: Modelos
Machine Translated by Google
o lado esquerdo representa o nome do argumento, que é usado no espaço reservado escrito no
modelo. O “nome” do lado direito é uma variável no escopo atual que fornece o valor para o argumento
de mesmo nome.
Variáveis
A construção {{ name }} usada no modelo mostrado no Exemplo 3-2 faz referência a uma variável, um
espaço reservado especial que informa ao mecanismo de modelo que o valor que vai naquele lugar
deve ser obtido dos dados fornecidos no momento em que o modelo é renderizado .
Jinja2 reconhece variáveis de qualquer tipo, até mesmo tipos complexos como listas, dicionários e
objetos. A seguir estão mais alguns exemplos de variáveis usadas em modelos:
As variáveis podem ser modificadas com filtros, que são adicionados após o nome da variável com
uma barra vertical como separador. Por exemplo, o modelo a seguir mostra a variável de nome em
letras maiúsculas:
Olá, {{ nome|maiúsculo }}
A Tabela 3-1 lista alguns dos filtros comumente usados que acompanham o Jinja2.
O filtro seguro é interessante destacar. Por padrão, Jinja2 escapa de todas as variáveis para fins de
segurança. Por exemplo, se uma variável for definida com o valor '<h1>Olá</h1>', Jinja2
irá renderizar a string como '<h1>Hello</h1>', o que fará com que o elemento h1 seja exibido e
não interpretado pelo navegador. Muitas vezes é necessário exibir o código HTML armazenado em
variáveis, e para esses casos é utilizado o filtro seguro .
Nunca use o filtro seguro em valores não confiáveis, como texto inserido
por usuários em formulários da web.
Estruturas de controle
Jinja2 oferece várias estruturas de controle que podem ser usadas para alterar o fluxo do modelo.
Esta seção apresenta alguns dos mais úteis com exemplos simples.
O exemplo a seguir mostra como declarações condicionais podem ser inseridas em um modelo:
{% se usuário %}
Olá, {{ usuário }}! {%
outro %}
Olá estranho! {% fim
se %}
Outra necessidade comum em modelos é renderizar uma lista de elementos. Este exemplo mostra como
isso pode ser feito com um loop for :
<ul>
{% for comment in comments %}
<li>{{ comment }}</li> {%
endfor %} </ul>
Jinja2 também oferece suporte a macros, que são semelhantes a funções no código Python. Por exemplo:
{% macro render_comment(comentário) %}
<li>{{ comentário }}</li> {%
endmacro %}
<ul>
{% para comentar nos comentários %}
{{ render_comment(comentário) }} {%
endfor %} </ul>
Para tornar as macros mais reutilizáveis, elas podem ser armazenadas em arquivos autônomos que são
importados de todos os modelos que precisam delas:
24 | Capítulo 3: Modelos
Machine Translated by Google
Partes do código do modelo que precisam ser repetidas em vários lugares podem ser armazenadas em
um arquivo separado e incluídas em todos os modelos para evitar repetição:
{% incluem 'common.html' %}
Outra maneira poderosa de reutilizar é por meio da herança de modelo, que é semelhante à herança de
classe no código Python. Primeiro, um modelo base é criado com o nome base.html:
<html>
<head>
{% block head %}
<title>{% block title %}{% endblock %} - Meu aplicativo</title> {% endblock %} </head>
<body> {% block
body % }
{%
endblock %} </body> </
html>
Aqui, as tags de bloco definem os elementos que um modelo derivado pode alterar. Neste exemplo,
existem blocos chamados head, title e body; observe que o título está contido na cabeça. O exemplo a
seguir é um modelo derivado do modelo base:
A diretiva extends declara que este template deriva de base.html. Esta diretiva é acompanhada de novas
definições para os três blocos definidos no template base, que são inseridos nos devidos lugares. Observe
que a nova definição do bloco head , que não está vazio no modelo base, usa super() para reter o
conteúdo original.
O uso no mundo real de todas as estruturas de controle apresentadas nesta seção será mostrado
posteriormente, para que você tenha a oportunidade de ver como elas funcionam.
O Bootstrap é uma estrutura do lado do cliente, portanto, o servidor não está diretamente envolvido com
ela. Tudo o que o servidor precisa fazer é fornecer respostas HTML que façam referência às folhas de
estilo em cascata (CSS) e aos arquivos JavaScript do Bootstrap e instanciar os componentes desejados
por meio do código HTML, CSS e JavaScript. O lugar ideal para fazer tudo isso é nos templates.
A maneira óbvia de integrar o Bootstrap com o aplicativo é fazer todas as alterações necessárias nos
modelos. Uma abordagem mais simples é usar uma extensão Flask chamada Flask Bootstrap para
simplificar o esforço de integração. Flask-Bootstrap pode ser instalado com pip:
As extensões Flask geralmente são inicializadas ao mesmo tempo em que a instância do aplicativo é
criada. O Exemplo 3-4 mostra a inicialização do Flask-Bootstrap.
Depois que o Flask-Bootstrap é inicializado, um modelo base que inclui todos os arquivos do Bootstrap
fica disponível para o aplicativo. Este modelo tira proveito da herança de modelo de Jinja2; a aplicação
estende um template base que possui a estrutura geral da página incluindo os elementos que importam
o Bootstrap. O exemplo 3-5 mostra uma nova versão de user.html como um modelo derivado.
{% estende "bootstrap/base.html" %}
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation"> <div
class="container"> <div
class="navbar-header">
<button type="button" class="navbar-toggle" data-
toggle="collapse" data-target=".navbar-collapse"> <span class="sr-
only">Alternar navegação <span class="icon-bar"> <span
class="icon-bar"> <span class="icon-
bar">
26 | Capítulo 3: Modelos
Machine Translated by Google
</button>
<a class="navbar-brand" href="/"> Flasky</a> </div>
<div
class="navbar-collapselapse">
<ul class="nenhum navbar-nenhum">
<li> <a href="/"> Página inicial</a></li>
</ul>
</div>
</div>
</div>
{% endblock %}
Os modelos base definem blocos que podem ser substituídos por modelos derivados. As diretivas
block e endblock definem blocos de conteúdo que são adicionados ao modelo base.
O modelo user.html acima define três blocos chamados de título, barra de navegação e conteúdo.
Estes são todos os blocos que o template base exporta para templates derivados definirem. O
bloco de título é direto; seu conteúdo aparecerá entre as tags <title> no cabeçalho do documento
HTML renderizado. A barra de navegação e os blocos de conteúdo são reservados para a barra
de navegação da página e o conteúdo principal.
Neste modelo, o bloco navbar define uma barra de navegação simples usando componentes
Bootstrap. O bloco de conteúdo possui um contêiner <div> com um cabeçalho de página dentro.
A linha de saudação que estava na versão anterior do modelo agora está dentro do cabeçalho da página.
A Figura 3-1 mostra a aparência do aplicativo com essas alterações.
O modelo base.html do Flask-Bootstrap define vários outros blocos que podem ser usados em
modelos derivados. A Tabela 3-2 mostra a lista completa de blocos disponíveis.
documento
Todo o documento HTML
Muitos dos blocos na Tabela 3-2 são usados pelo próprio Flask-Bootstrap, portanto, substituí-los
diretamente causaria problemas. Por exemplo, os blocos de estilos e scripts são onde
os arquivos Bootstrap são declarados. Se o aplicativo precisar adicionar seu próprio conteúdo a um bloco
que já possui algum conteúdo, então a função super() de Jinja2 deve ser usada. Por exemplo,
28 | Capítulo 3: Modelos
Machine Translated by Google
é assim que o bloco de scripts precisaria ser escrito no modelo derivado para adicionar um novo arquivo
JavaScript ao documento:
{% block scripts %}
{{ super() }}
<script type="text/javascript" src="my-script.js"></script> {% endblock %}
Quando você insere uma rota inválida na barra de endereços do navegador, obtém uma página de erro de
código 404. A página de erro agora é muito simples e pouco atraente e não tem consistência com a página que
usa o Bootstrap.
O Flask permite que um aplicativo defina páginas de erro personalizadas que podem ser baseadas em modelos,
como rotas regulares. Os dois códigos de erro mais comuns são 404, acionado quando o cliente solicita uma
página ou rota desconhecida, e 500, acionado quando há uma exceção não tratada. O exemplo 3-6 mostra
como fornecer manipuladores personalizados para esses dois erros.
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_server_error(e): return
render_template('500.html'), 500
Os manipuladores de erro retornam uma resposta, como as funções de visualização. Eles também retornam o
código de status numérico que corresponde ao erro.
Os modelos referenciados nos manipuladores de erro precisam ser escritos. Esses templates devem seguir o
mesmo layout das páginas normais, então neste caso eles terão uma barra de navegação e um cabeçalho de
página que mostra a mensagem de erro.
A maneira direta de escrever esses modelos é copiar templates/ user.html para templates/ 404.html e templates/
500.html e, em seguida, alterar o elemento de cabeçalho da página nesses dois novos arquivos para a
mensagem de erro apropriada, mas isso gerará um muita duplicação.
A herança de modelo de Jinja2 pode ajudar com isso. Da mesma forma que o Flask-Bootstrap fornece um
template base com o layout básico da página, a aplicação pode definir seu próprio template base com um layout
de página mais completo que inclui a barra de navegação e deixa o conteúdo da página para ser definido em
derivado modelos. O exemplo 3-7 mostra templates/ base.html, um novo template que herda de bootstrap/
base.html e define a barra de navegação, mas é um template base para outros templates como templates/
user.html, templates/ 404.html e templates /500.html.
Exemplo 3-7. templates/ base.html: Modelo base de aplicativo com barra de navegação
{% estende "bootstrap/base.html" %}
No bloco de conteúdo deste template há apenas um elemento container <div> que envolve um novo
bloco vazio chamado page_content, que templates derivados podem definir.
Exemplo 3-8. templates/ 404.html: Página de erro 404 de código personalizado usando modelo herdado
tância
{% estende "base.html" %}
30 | Capítulo 3: Modelos
Machine Translated by Google
</div>
{% endblock %}
O modelo templates/ user.html agora pode ser simplificado tornando-o herdado do modelo base, conforme mostrado no
Exemplo 3-9.
Exemplo 3-9. templates/ user.html: modelo de página simplificado usando herança de modelo
{% estende "base.html" %}
{% block page_content %}
<div class="page-header">
<h1>Olá, {{ name }}!</h1> </div>
{%
endblock %}
links
Qualquer aplicativo que tenha mais de uma rota, invariavelmente precisará incluir links que conectem as diferentes páginas,
como em uma barra de navegação.
Links | 31
Machine Translated by Google
Escrever as URLs como links diretamente no template é trivial para rotas simples, mas para rotas
dinâmicas com porções variáveis pode ser mais complicado construir as URLs diretamente no
template. Além disso, as URLs escritas explicitamente criam uma dependência indesejada nas
rotas definidas no código. Se as rotas forem reorganizadas, os links nos modelos podem quebrar.
Para evitar esses problemas, o Flask fornece a função auxiliar url_for() , que gera URLs a partir
das informações armazenadas no mapa de URLs do aplicativo.
Em seu uso mais simples, essa função usa o nome da função de exibição (ou nome do endpoint
para rotas definidas com app.add_url_route()) como seu único argumento e retorna sua URL.
Por exemplo, na versão atual de hello.py , a chamada url_for('index') retornaria /. Chamar
url_for('index', _external=True) retornaria uma URL absoluta, que neste exemplo é http://
localhost:5000/.
URLs dinâmicos podem ser gerados com url_for() passando as partes dinâmicas como
argumentos de palavra-chave. Por exemplo, url_for('user', name='john', _external=True) retornaria
http:// localhost:5000/ user/ john.
Arquivos Estáticos
Os aplicativos da Web não são feitos apenas de código e modelos Python. A maioria dos
aplicativos também usa arquivos estáticos, como imagens, arquivos de origem JavaScript e CSS
referenciados no código HTML.
Você deve se lembrar que quando o mapa de URL do aplicativo hello.py foi inspecionado no
Capítulo 2, uma entrada estática apareceu nele. Isso ocorre porque as referências a arquivos
estáticos são tratadas como uma rota especial definida como /static/ <filename>. Por exemplo,
uma chamada para url_for('static', filename='css/styles.css', _external=True) retornaria http://
localhost:5000/ static/ css/ styles.css.
Em sua configuração padrão, o Flask procura por arquivos estáticos em um subdiretório chamado
static localizado na pasta raiz do aplicativo. Os arquivos podem ser organizados em subdiretórios
dentro desta pasta, se desejado. Quando o servidor recebe a URL do exemplo anterior, ele
32 | Capítulo 3: Modelos
Machine Translated by Google
gera uma resposta que inclui o conteúdo de um arquivo no sistema de arquivos localizado em static/ css/
styles.css.
O exemplo 3-10 mostra como o aplicativo pode incluir um ícone favicon.ico no modelo base para os
navegadores mostrarem na barra de endereço.
{% block head %}
{{ super() }}
<link rel="shortcut icon" href="{{ url_for('static', filename = 'favicon.ico') }}"
type="image/x-icon">
<link rel="icon" href="{{ url_for('static', filename = 'favicon.ico') }}" type="image/x-icon"> { %
bloco final %}
A declaração do ícone é inserida no final do bloco principal . Observe como super() é usado para preservar o
conteúdo original do bloco definido nos modelos base.
O servidor precisa de unidades de tempo uniformes que sejam independentes da localização de cada usuário,
então normalmente o Tempo Universal Coordenado (UTC) é usado. Para os usuários, no entanto, ver os
horários expressos em UTC pode ser confuso, pois os usuários sempre esperam ver as datas e horários
apresentados em seu horário local e formatados de acordo com os costumes locais de sua região.
Uma solução elegante que permite que o servidor funcione exclusivamente em UTC é enviar essas unidades
de tempo para o navegador da Web, onde são convertidas para a hora local e renderizadas.
Os navegadores da Web podem fazer um trabalho muito melhor nessa tarefa porque têm acesso às
configurações de fuso horário e localidade no computador do usuário.
Existe uma excelente biblioteca de código aberto do lado do cliente escrita em JavaScript que renderiza datas
e horas no navegador chamada moment.js. Flask-Moment é uma extensão para aplicativos Flask que integra
moment.js em modelos Jinja2. Flask-Moment é instalado com pip:
Flask-Moment depende de jquery.js além de moment.js. Essas duas bibliotecas precisam ser incluídas em algum
lugar do documento HTML - diretamente, caso em que você pode escolher quais versões usar, ou por meio das
funções auxiliares fornecidas pela extensão, que fazem referência a versões testadas dessas bibliotecas de uma
Entrega de conteúdo Rede (CDN). Como o Bootstrap já inclui jquery.js, apenas moment.js precisa ser adicionado
neste caso. O Exemplo 3-12 mostra como esta biblioteca é carregada nos scripts do template base.
{% scripts de bloqueio
%} {{ super() }}
{{ moment.include_moment() }} {%
endblock %}
Para trabalhar com carimbos de data e hora, o Flask-Moment disponibiliza uma classe de momento para modelos.
O exemplo no Exemplo 3-13 passa uma variável chamada current_time para o template para renderização.
@app.route('/')
def index():
return render_template('index.html',
current_time=datetime.utcnow())
O formato format('LLL') renderiza a data e a hora de acordo com o fuso horário e as configurações de localidade
no computador cliente. O argumento determina o estilo de renderização, de 'L' a 'LLLL' para diferentes níveis de
verbosidade. A função format() também pode aceitar especificadores de formato personalizados.
O estilo de renderização fromNow() mostrado na segunda linha renderiza um carimbo de data/hora relativo e o
atualiza automaticamente com o passar do tempo. Inicialmente, esse registro de data e hora será mostrado como
“alguns segundos atrás”, mas a opção de atualização o manterá atualizado com o passar do tempo; portanto, se
você deixar a página aberta por alguns minutos, verá o texto mudando para “um minuto atrás”. depois “2 minutos
atrás” e assim por diante.
34 | Capítulo 3: Modelos
Machine Translated by Google
{{ moment.long('it') }}
Com todas as técnicas discutidas neste capítulo, você será capaz de construir páginas da
Web modernas e fáceis de usar para seu aplicativo. O próximo capítulo aborda um aspecto
dos templates ainda não discutido: como interagir com o usuário através de formulários web.
CAPÍTULO 4
Formulários da Web
O objeto request, apresentado no Capítulo 2, expõe todas as informações enviadas pelo cliente com
uma requisição. Em particular, request.form fornece acesso a dados de formulário enviados em
solicitações POST .
Embora o suporte fornecido no objeto de requisição do Flask seja suficiente para a manipulação de
formulários web, há uma série de tarefas que podem se tornar tediosas e repetitivas. Dois bons
exemplos são a geração de código HTML para formulários e a validação dos dados do formulário
enviado.
O Frasco-WTF extensão torna o trabalho com formulários da web uma experiência muito mais
agradável. Esta extensão é um wrapper de integração do Flask em torno dos WTForms independentes
de estrutura pacote.
Por padrão, o Flask-WTF protege todos os formulários contra ataques Cross-Site Request Forgery
(CSRF). Um ataque CSRF ocorre quando um site malicioso envia solicitações para um site diferente
no qual a vítima está logada.
Para implementar a proteção CSRF, o Flask-WTF precisa que o aplicativo configure uma chave de
criptografia. Flask-WTF usa essa chave para gerar tokens criptografados que são usados para
verificar a autenticidade de solicitações com dados de formulário. O Exemplo 4-1 mostra como
configurar uma chave de criptografia.
app = Flask(__name__)
app.config['SECRET_KEY'] = 'string difícil de adivinhar'
37
Machine Translated by Google
O dicionário app.config é um local de uso geral para armazenar variáveis de configuração usadas pela estrutura,
pelas extensões ou pelo próprio aplicativo. Os valores de configuração podem ser adicionados ao objeto app.config
usando a sintaxe de dicionário padrão. O objeto de configuração também possui métodos para importar valores de
configuração de arquivos ou do ambiente
ambiente.
A variável de configuração SECRET_KEY é usada como uma chave de criptografia de uso geral pelo Flask e várias
extensões de terceiros. Como o próprio nome indica, a força da criptografia depende do valor dessa variável ser
secreta. Escolha uma chave secreta diferente em cada aplicativo que você criar e certifique-se de que essa string
não seja conhecida por ninguém.
Classes de formulário
Ao usar Flask-WTF, cada formulário da web é representado por uma classe que herda da classe Form. A classe
define a lista de campos no formulário, cada um representado por um objeto. Cada objeto de campo pode ter um
ou mais validadores anexados; validadores são funções que verificam se a entrada enviada pelo usuário é válida.
O Exemplo 4-2 mostra um formulário da Web simples que possui um campo de texto e um botão de envio.
class NameForm(Form):
name = StringField('Qual é o seu nome?', validators=[Required()]) submit =
SubmitField('Enviar')
Os campos no formulário são definidos como variáveis de classe e cada variável de classe recebe um objeto
associado ao tipo de campo. No exemplo anterior, o formulário NameForm tem um campo de texto chamado name
e um botão de envio chamado submit. A classe StringField representa um elemento <input> com um atributo
type="text" . A classe SubmitField representa um elemento <input> com um atributo type="submit" . O primeiro
argumento para os construtores de campo é o rótulo que será usado ao renderizar o formulário para HTML.
O argumento opcional validators incluído no construtor StringField define uma lista de verificadores que serão
aplicados aos dados enviados pelo usuário antes de serem aceitos. O validador Required() garante que o campo
não seja enviado vazio.
A lista de campos HTML padrão suportados pelo WTForms é mostrada na Tabela 4-1.
Campo de texto
StringField
TextAreaField Campo de texto de várias linhas
Aulas de formulário | 39
Machine Translated by Google
Validador Descrição
EqualTo Compara os valores de dois campos; útil ao solicitar uma senha para ser digitada duas vezes para confirmação
Campos de formulário são chamadas que, quando invocadas, de um modelo se renderizam para
HTML. Supondo que a função view passe uma instância NameForm para o template como
um argumento chamado form, o modelo pode gerar um formulário HTML simples da seguinte forma:
<form method="POST">
{{ form.name.label }} {{ form.name() }}
{{ form.submit() }}
</form>
Claro, o resultado é extremamente nu. Para melhorar a aparência do formulário, quaisquer argumentos
enviados para as chamadas que renderizam os campos são convertidos em atributos HTML para o campo;
assim, por exemplo, você pode fornecer os atributos id ou class do campo e definir estilos CSS:
<form method="POST">
{{ form.name.label }} {{ form.name(id='meu-campo-de-texto') }}
{{ form.submit() }}
</form>
Mas mesmo com atributos HTML, o esforço necessário para renderizar um formulário dessa maneira é
significativo, por isso é melhor aproveitar o próprio conjunto de estilos de formulário do Bootstrap sempre que possível.
O Flask-Bootstrap fornece uma função auxiliar de alto nível que renderiza um formulário Flask WTF inteiro usando os
estilos de formulário predefinidos do Bootstrap, tudo com uma única chamada. Usando o Flask Bootstrap, o formulário
anterior pode ser renderizado da seguinte forma:
A diretiva de importação funciona da mesma forma que os scripts Python regulares e permite que
os elementos do modelo sejam importados e usados em muitos modelos. O arquivo bootstrap/
wtf.html importado define funções auxiliares que renderizam formulários Flask-WTF usando Bootstrap.
A função wtf.quick_form() pega um objeto de formulário Flask-WTF e o renderiza usando estilos
Bootstrap padrão. O modelo completo para hello.py é mostrado no Exemplo 4-3.
{% extends "base.html" %} {%
import "bootstrap/wtf.html" como wtf %}
{% block page_content %}
<div class="page-header">
<h1>Olá, {% if name %}{{ name }}{% else %}Estranho{% endif %}!</h1> </div>
{{ wtf.quick_form(form) }} {%
endblock % }
A área de conteúdo do modelo agora tem duas seções. A primeira seção é um cabeçalho de
página que mostra uma saudação. Aqui, uma condicional de modelo é usada. Condicionais em
Jinja2 têm o formato {% if variable %}...{% else %}...{% endif %}. Se a condição for avaliada como
True, o que aparece entre as diretivas if e else é renderizado no modelo. Se a condição for
avaliada como False, o que estiver entre else e endif será renderizado. O modelo de exemplo
renderizará a string “Hello, Stranger!” quando o argumento do modelo de nome é indefinido. A
segunda seção do conteúdo renderiza o objeto NameForm usando a função wtf.quick_form() .
Na nova versão do hello.py, a função index() view renderizará o formulário e também receberá
seus dados. O exemplo 4-4 mostra a função de visualização index() atualizada .
O argumento de métodos adicionado ao decorador app.route informa ao Flask para registrar a função de
exibição como um manipulador para solicitações GET e POST no mapa de URL. Quando os métodos não
são fornecidos, a função de exibição é registrada para lidar apenas com solicitações GET .
Adicionar POST à lista de métodos é necessário porque os envios de formulário são muito mais
convenientemente tratados como solicitações POST . É possível enviar um formulário como uma solicitação
GET , mas como as solicitações GET não têm corpo, os dados são anexados à URL como uma string de
consulta e ficam visíveis na barra de endereço do navegador. Por esse e vários outros motivos, os envios de
formulários são quase universalmente feitos como solicitações POST .
A variável local name é usada para armazenar o nome recebido do formulário quando disponível; quando o
nome não é conhecido, a variável é inicializada como None. A função view cria uma instância da classe
Quando um usuário navega para o aplicativo pela primeira vez, o servidor receberá uma solicitação GET sem
dados de formulário, portanto, valida_on_submit() retornará Falso. O corpo da instrução if será ignorado e a
solicitação será tratada pela renderização do modelo, que obtém o objeto de formulário e a variável de nome
definida como None como argumentos. Os usuários agora verão o formulário exibido no navegador.
Quando o formulário é enviado pelo usuário, o servidor recebe uma solicitação POST com os dados.
A chamada para validar_on_submit() chama o validador Required() anexado ao campo de nome. Se o nome
não estiver vazio, o validador o aceita e valid_on_submit() retorna True. Agora o nome digitado pelo usuário
está acessível como o atributo de dados do campo. Dentro do corpo da instrução if , esse nome é atribuído à
variável de nome local e o campo do formulário é limpo definindo esse atributo de dados como uma string
vazia. A chamada render_template() na última linha renderiza o modelo, mas desta vez o argumento name
contém o nome do formulário, então a saudação será personalizada.
A Figura 4-1 mostra a aparência do formulário na janela do navegador quando um usuário entra inicialmente
no site. Quando o usuário envia um nome, o aplicativo responde com uma mensagem personalizada
saudações. O formulário ainda aparece abaixo dele, para que um usuário possa enviá-lo com um novo
nome, se desejar. A Figura 4-2 mostra o aplicativo nesse estado.
Se o usuário enviar o formulário com um nome vazio, o validador Required() detectará o erro, conforme
mostrado na Figura 4-3. Observe quanta funcionalidade está sendo fornecida automaticamente. Este é
um ótimo exemplo do poder que extensões bem projetadas como Flask-WTF e Flask-Bootstrap podem
dar ao seu aplicativo.
A última versão do hello.py tem um problema de usabilidade. Se você inserir seu nome e enviá-lo e clicar no
botão de atualização em seu navegador, provavelmente receberá um aviso obscuro que solicita confirmação
antes de enviar o formulário novamente. Isso acontece porque os navegadores repetem a última solicitação
enviada quando são solicitados a atualizar a página. Quando a última solicitação enviada é uma solicitação
POST com dados de formulário, uma atualização causaria um envio de formulário duplicado, que em quase
todos os casos não é a ação desejada.
Muitos usuários não entendem o aviso do navegador. Por este motivo, é considerado uma boa prática para
aplicações web nunca deixar uma requisição POST como última requisição enviada pelo navegador.
Essa prática pode ser obtida respondendo a solicitações POST com um redirecionamento em vez de uma
resposta normal. Um redirecionamento é um tipo especial de resposta que possui uma URL em vez de uma
string com código HTML. Quando o navegador recebe essa resposta, ele emite uma solicitação GET para a
URL de redirecionamento e essa é a página exibida. A página pode demorar mais alguns milissegundos para
carregar por causa da segunda solicitação que deve ser enviada ao servidor, mas fora isso, o usuário não verá
nenhuma diferença. Agora, a última solicitação é um GET, portanto, o comando de atualização funciona
conforme o esperado. Esse truque é conhecido como padrão Post/ Redirect/ Get.
Mas esta abordagem traz um segundo problema. Quando a aplicação atende a requisição POST , ela tem
acesso ao nome digitado pelo usuário em form.name.data, mas assim que a requisição termina os dados do
formulário são perdidos. Como a solicitação POST é tratada com um
redirecionar, o aplicativo precisa armazenar o nome para que a solicitação redirecionada possa tê-lo e usá-lo
para criar a resposta real.
Os aplicativos podem “lembrar” coisas de uma solicitação para a seguinte, armazenando-as na sessão do
usuário, armazenamento privado que está disponível para cada cliente conectado. A sessão do usuário foi
apresentada no Capítulo 2 como uma das variáveis associadas ao contexto da requisição.
É chamado de sessão e é acessado como um dicionário Python padrão.
O exemplo 4-5 mostra uma nova versão da função de visualização index() que implementa redirecionamentos e
sessões de usuário.
Na versão anterior do aplicativo, uma variável de nome local era utilizada para armazenar o nome digitado pelo
usuário no formulário. Essa variável agora é colocada na sessão do usuário como session['name'] para que seja
lembrada além da solicitação.
As solicitações que vêm com dados de formulário válidos agora terminarão com uma chamada para redirect(),
uma função auxiliar que gera a resposta de redirecionamento HTTP. A função redirect() usa a URL para
redirecionar como um argumento. A URL de redirecionamento usada neste caso é a URL raiz, portanto, a
resposta poderia ter sido escrita de forma mais concisa como redirect('/'), mas, em vez disso, a função geradora
de URL do Flask url_for() é usada. O uso de url_for() para gerar URLs é encorajado porque esta função gera
URLs usando o mapa de URLs, então URLs são garantidos para serem compatíveis com rotas definidas e
quaisquer alterações feitas nos nomes das rotas estarão disponíveis automaticamente ao usar esta função.
O primeiro e único argumento necessário para url_for() é o nome do endpoint , o nome interno de cada rota. Por
padrão, o ponto final de uma rota é o nome da função de visualização anexada a ela. Neste exemplo, a função
view que lida com a URL raiz é index(), então o nome dado a url_for() é index.
A última mudança está na função render_template() , que agora obtém o argumento name diretamente
da sessão usando session.get('name'). Como acontece com os dicionários regulares, usar get() para
solicitar uma chave de dicionário evita uma exceção para chaves que não foram encontradas, porque
get() retorna um valor padrão de None para uma chave ausente.
Com esta versão do aplicativo, você pode ver que atualizar a página em seu navegador resulta no
comportamento esperado.
Mensagem piscando
Às vezes, é útil fornecer ao usuário uma atualização de status após a conclusão de uma solicitação.
Pode ser uma mensagem de confirmação, um aviso ou um erro. Um exemplo típico é quando você
envia um formulário de login para um site com um erro e o servidor responde renderizando o formulário
de login novamente com uma mensagem acima informando que seu nome de usuário ou senha é
inválido.
O Flask inclui essa funcionalidade como um recurso principal. O Exemplo 4-6 mostra como a função
flash() pode ser usada para esse propósito.
Neste exemplo, cada vez que um nome é enviado, ele é comparado com o nome armazenado na
sessão do usuário, que teria sido colocado lá durante um envio anterior do mesmo formulário. Se os
dois nomes forem diferentes, a função flash() será invocada com uma mensagem a ser exibida na
próxima resposta enviada de volta ao cliente.
Chamar flash() não é suficiente para exibir as mensagens; os modelos usados pelo aplicativo precisam
renderizar essas mensagens. O melhor lugar para renderizar as mensagens em flash é o modelo base,
porque isso habilitará essas mensagens em todas as páginas. O Flask disponibiliza uma função
get_flashed_messages() para modelos recuperarem as mensagens e renderizá-las, conforme mostrado no
Exemplo 4-7.
{% bloco de conteúdo
%} <div class="container">
{% para mensagem em get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">×</button> {{ message }}
</div> {%
endfor
%}
Neste exemplo, as mensagens são renderizadas usando estilos CSS de alerta do Bootstrap para mensagens
de aviso (uma delas é mostrada na Figura 4-4).
Um loop é usado porque pode haver várias mensagens enfileiradas para exibição, uma para cada vez que
flash() foi chamado no ciclo de solicitação anterior. Mensagens recuperadas
Mensagem piscando | 47
Machine Translated by Google
from get_flashed_messages() não será retornado na próxima vez que esta função for chamada, então as
mensagens flash aparecerão apenas uma vez e serão descartadas.
Ser capaz de aceitar dados do usuário por meio de formulários da web é um recurso exigido pela maioria dos
aplicativos, assim como a capacidade de armazenar esses dados em armazenamento permanente. Usar
bancos de dados com Flask é o tópico do próximo capítulo.
CAPÍTULO 5
bancos de dados
Bancos de dados relacionais armazenam dados em tabelas, que modelam as diferentes entidades no
domínio do aplicativo. Por exemplo, um banco de dados para um aplicativo de gerenciamento de pedidos
provavelmente terá tabelas de clientes, produtos e pedidos .
Uma tabela tem um número fixo de colunas e um número variável de linhas. As colunas definem os
atributos de dados da entidade representada pela tabela. Por exemplo, uma tabela de clientes terá
colunas como nome, endereço, telefone e assim por diante. Cada linha em uma tabela define um
elemento de dados real que consiste em valores para todas as colunas.
As tabelas têm uma coluna especial chamada chave primária, que contém um identificador exclusivo
para cada linha armazenada na tabela. As tabelas também podem ter colunas chamadas chaves
estrangeiras, que fazem referência à chave primária de outra linha da mesma ou de outra tabela. Esses
links entre as linhas são chamados de relacionamentos e são a base do modelo de banco de dados
relacional.
A Figura 5-1 mostra um diagrama de um banco de dados simples com duas tabelas que armazenam
usuários e funções de usuário. A linha que conecta as duas tabelas representa um relacionamento entre
as tabelas.
49
Machine Translated by Google
Nesse diagrama de banco de dados, a tabela de funções armazena a lista de todas as funções de usuário possíveis, cada
uma identificada por um valor de id exclusivo — a chave primária da tabela. A tabela de usuários contém a lista de usuários,
cada um com seu próprio ID exclusivo . Além das chaves primárias de id , a tabela de funções possui uma coluna de nome
e a tabela de usuários possui colunas de nome de usuário e senha . A coluna role_id na tabela users é uma chave
estrangeira que referencia o id de um papel, e desta forma o papel atribuído a cada usuário é estabelecido.
Conforme visto no exemplo, os bancos de dados relacionais armazenam dados de forma eficiente e evitam a duplicação.
Renomear uma função de usuário neste banco de dados é simples porque os nomes de funções existem em um único local.
Imediatamente após a alteração do nome de uma função na tabela de funções , todos os usuários que tiverem um role_id
que faça referência à função alterada verão a atualização.
Por outro lado, ter os dados divididos em várias tabelas pode ser uma complicação.
Produzir uma listagem de usuários com suas funções apresenta um pequeno problema, porque usuários e funções de
usuário precisam ser lidos de duas tabelas e unidos antes que possam ser apresentados juntos. Os mecanismos de banco
de dados relacional fornecem suporte para realizar operações de junção entre tabelas quando necessário.
Os bancos de dados que não seguem o modelo relacional descrito na seção anterior são referidos coletivamente como
bancos de dados NoSQL . Uma organização comum para bancos de dados NoSQL usa coleções em vez de tabelas e
documentos em vez de registros. Os bancos de dados NoSQL são projetados de forma a dificultar as junções, portanto, a
maioria deles não oferece suporte a essa operação. Para um banco de dados NoSQL estruturado como na Figura 5-1, listar
os usuários com suas funções requer que o próprio aplicativo execute a operação de junção lendo o campo role_id de cada
usuário e, em seguida, procurando- o na tabela de funções .
Um projeto mais apropriado para um banco de dados NoSQL é mostrado na Figura 5-2. Isso é resultado da aplicação de
uma operação chamada desnormalização, que reduz o número de tabelas em detrimento da duplicação de dados.
Um banco de dados com essa estrutura tem o nome da função explicitamente armazenado com cada usuário. Renomear
uma função pode se tornar uma operação cara que pode exigir a atualização de um grande número de documentos.
Mas nem tudo são más notícias com bancos de dados NoSQL. Ter os dados duplicados permite consultas mais rápidas.
A listagem de usuários e suas funções é direta porque nenhuma junção é necessária.
SQL ou NoSQL?
Os bancos de dados SQL são excelentes para armazenar dados estruturados de forma eficiente e compacta. Esses
bancos de dados fazem grandes esforços para preservar a consistência. Os bancos de dados NoSQL relaxam alguns
dos requisitos de consistência e, como resultado, às vezes podem obter uma vantagem de desempenho.
Uma análise completa e comparação dos tipos de banco de dados está fora do escopo deste livro. Para aplicativos de
pequeno a médio porte, os bancos de dados SQL e NoSQL são perfeitamente capazes e têm desempenho praticamente
equivalente.
Como se essas escolhas não fossem suficientes, também há vários pacotes de camada de abstração de banco de
dados, como SQLAlchemy ou MongoEngine, que permitem trabalhar em um nível superior com objetos Python regulares
em vez de entidades de banco de dados, como tabelas, documentos ou linguagens de consulta .
Há vários fatores a serem avaliados ao escolher uma estrutura de banco de dados: Facilidade de uso Ao
comparar
mecanismos de banco de dados diretos com camadas de abstração de banco de dados, o segundo grupo
claramente vence. Camadas de abstração, também chamadas de mapeadores relacionais de objeto (ORMs) ou
mapeadores de documento de objeto (ODMs), fornecem conversão transparente de operações orientadas a
objetos de alto nível em instruções de banco de dados de baixo nível.
SQL ou NoSQL? | 51
Machine Translated by Google
Desempenho As
conversões que os ORMs e ODMs precisam fazer para traduzir do domínio do objeto para o domínio do
banco de dados têm uma sobrecarga. Na maioria dos casos, a penalidade de desempenho é insignificante,
mas nem sempre. Em geral, o ganho de produtividade obtido com ORMs e ODMs supera em muito uma
degradação mínima de desempenho, portanto, este não é um argumento válido para descartar completamente
os ORMs e ODMs. O que faz sentido é escolher uma camada de abstração de banco de dados que forneça
acesso opcional ao banco de dados subjacente no caso de operações específicas precisarem ser otimizadas,
implementando-as diretamente como instruções nativas do banco de dados.
Portabilidade
As opções de banco de dados disponíveis em suas plataformas de desenvolvimento e produção devem ser
consideradas. Por exemplo, se você planeja hospedar seu aplicativo em uma plataforma de nuvem, deve
descobrir quais opções de banco de dados esse serviço oferece.
Outro aspecto de portabilidade se aplica a ORMs e ODMs. Embora algumas dessas estruturas forneçam
uma camada de abstração para um único mecanismo de banco de dados, outras abstraem ainda mais e
fornecem uma escolha de mecanismos de banco de dados – todos acessíveis com a mesma interface
orientada a objetos. O melhor exemplo disso é o SQLAlchemy ORM, que oferece suporte a uma lista de
mecanismos de banco de dados relacionais, incluindo os populares MySQL, Postgres e SQLite.
Integração com o
Flask A escolha de um framework que tenha integração com o Flask não é absolutamente necessária, mas
evitará que você mesmo tenha que escrever o código de integração. A integração do Flask pode simplificar
a configuração e a operação, portanto, deve-se dar preferência ao uso de um pacote projetado especificamente
como uma extensão do Flask.
Com base nesses objetivos, o framework de banco de dados escolhido para os exemplos deste livro será o Flask-
SQLAlchemy, o wrapper de extensão Flask para SQLAlchemy.
No Flask-SQLAlchemy, um banco de dados é especificado como uma URL. A Tabela 5-1 lista o formato de URLs
de banco de dados para os três mecanismos de banco de dados mais populares.
MySQL mysql://username:password@hostname/database
Postgre postgresql://username:password@hostname/database
Nessas URLs, hostname refere-se ao servidor que hospeda o serviço MySQL, que pode ser localhost ou um
servidor remoto. Os servidores de banco de dados podem hospedar vários bancos de dados, portanto, o banco
de dados indica o nome do banco de dados a ser usado. Para bancos de dados que precisam de autenticação,
nome de usuário e senha são as credenciais do usuário do banco de dados.
Os bancos de dados SQLite não possuem um servidor, portanto, o nome do host, o nome do
usuário e a senha são omitidos e o banco de dados é o nome de arquivo de um arquivo em disco.
A URL do banco de dados do aplicativo deve ser configurada como a chave SQLALCHEMY_DATABASE_URI
no objeto de configuração do Flask. Outra opção útil é a chave de configuração
SQLALCHEMY_COMMIT_ON_TEARDOWN, que pode ser definida como True para permitir confirmações
automáticas de alterações no banco de dados ao final de cada solicitação. Consulte a documentação do Flask-
SQLAlchemy para obter informações sobre outras opções de configuração.
O Exemplo 5-1 mostra como inicializar e configurar um banco de dados SQLite simples.
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] =\ 'sqlite:///'
+ os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = Verdadeiro
db = SQLAlquimia(aplicativo)
O objeto db instanciado da classe SQLAlchemy representa o banco de dados e fornece acesso a todas as
funcionalidades do Flask-SQLAlchemy.
Definição do modelo
O termo modelo é usado para se referir às entidades persistentes usadas pelo aplicativo. No
contexto de um ORM, um modelo é tipicamente uma classe Python com atributos que correspondem ao
colunas de uma tabela de banco de dados correspondente.
A instância do banco de dados Flask-SQLAlchemy também fornece uma classe base para modelos
como um conjunto de classes auxiliares e funções usadas para definir sua estrutura. os papéis
e as tabelas de usuários da Figura 5-1 podem ser definidas como modelos Role e User , conforme mostrado em
Exemplo 5-2.
classe Role(db.Model):
__tablename__ = 'funções'
id = db.Column(db.Integer, primary_key=True)
nome = db.Column(db.String(64), exclusivo=Verdadeiro)
def __repr__(auto):
return '<Função %r>' % self.name
classe Usuário(db.Model):
__tablename__ = 'usuários'
id = db.Column(db.Integer, primary_key=True)
nome de usuário = db.Column(db.String(64), exclusivo=Verdadeiro, índice=Verdadeiro)
def __repr__(auto):
return '<Usuário %r>' % self.username
A variável de classe __tablename__ define o nome da tabela no banco de dados. O Flask SQLAlchemy atribui um
Numérico
decimal.Decimal número de ponto fixo
Os argumentos restantes para db.Column especificam opções de configuração para cada atÿ
tributo. A Tabela 5-3 lista algumas das opções disponíveis.
Se definido como True, não permite valores duplicados para esta coluna.
exclusivo
índice Se definido como True, crie um índice para esta coluna, para que as consultas sejam mais eficientes.
anulável Se definido como True, permite valores vazios para esta coluna. Se definido como Falso, a coluna não permitirá nulo
valores.
Flask-SQLAlchemy requer que todos os modelos definam uma coluna de chave primária
umn, que normalmente é denominado id.
Embora não seja estritamente necessário, os dois modelos incluem um método __repr__() para
dê a eles uma representação de string legível que pode ser usada para depuração e teste
propósitos.
Definição do modelo | 55
Machine Translated by Google
Relacionamentos
Bancos de dados relacionais estabelecem conexões entre linhas em tabelas diferentes por meio do
uso de relacionamentos. O diagrama relacional na Figura 5-1 expressa um relacionamento simples
entre os usuários e suas funções. Este é um relacionamento um-para-muitos de funções para usuários,
porque uma função pertence a muitos usuários e os usuários têm apenas uma função.
O Exemplo 5-3 mostra como o relacionamento um-para-muitos na Figura 5-1 é representado nas
classes de modelo.
classe Role(db.Model):
# ...
users = db.relationship('User', backref='role')
class User(db.Model): #
...
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
Conforme visto na Figura 5-1, um relacionamento conecta duas linhas por meio do usuário de uma
chave estrangeira. A coluna role_id adicionada ao modelo User é definida como uma chave estrangeira
e isso estabelece o relacionamento. O argumento 'roles.id' para db.ForeignKey() especifica que a
coluna deve ser interpretada como tendo valores id de linhas na tabela de funções .
Na maioria dos casos, db.relationship() pode localizar a chave estrangeira do relacionamento por
conta própria, mas às vezes não pode determinar qual coluna usar como chave estrangeira. Por
exemplo, se o modelo de usuário tiver duas ou mais colunas definidas como chaves estrangeiras de
função , o SQLAlchemy não saberá qual das duas usar. Sempre que a configuração da chave
estrangeira for ambígua, argumentos adicionais para db.relationship() precisam ser fornecidos. A
Tabela 5-4 lista algumas das opções de configuração comuns que podem ser usadas para definir um
relacionamento.
primaryjoin Especifique explicitamente a condição de junção entre os dois modelos. Isso é necessário apenas para relacionamentos ambíguos.
preguiçoso Especifique como os itens relacionados devem ser carregados. Os valores possíveis são select (itens são carregados sob demanda
na primeira vez em que são acessados), imediato (itens são carregados quando o objeto de origem é carregado), join (itens são
carregados imediatamente, mas como uma junção), subconsulta (itens são carregados imediatamente , mas como uma subconsulta),
noload (os itens nunca são carregados) e dynamic (em vez de carregar os itens, é fornecida a consulta que pode carregá-los).
Lista de usuários Se definido como Falso, use um escalar em vez de uma lista.
secondjoin Especifique a condição de junção secundária para relacionamentos muitos-para-muitos quando o SQLAlchemy não pode
determiná-lo por conta própria.
Os modelos agora estão totalmente configurados de acordo com o diagrama de banco de dados da
Figura 5-1 e estão prontos para serem usados. A melhor maneira de aprender a trabalhar com esses
modelos é em um shell Python. As seções a seguir o guiarão pelas operações de banco de dados mais
comuns.
coisa a fazer é instruir o Flask-SQLAlchemy a criar um banco de dados baseado nas classes do modelo. A função
db.create_all() faz isso:
Se você verificar o diretório do aplicativo, verá agora um novo arquivo chamado data.sqlite, o nome que foi dado
ao banco de dados SQLite na configuração. A função db.create_all() não recriará ou atualizará uma tabela de
banco de dados se ela já existir no banco de dados. Isso pode ser inconveniente quando os modelos são
modificados e o
alterações precisam ser aplicadas a um banco de dados existente. A solução de força bruta para atualizar as
tabelas de banco de dados existentes é remover primeiro as tabelas antigas:
>>> db.drop_all()
>>> db.create_all()
Infelizmente, esse método tem o efeito colateral indesejado de destruir todos os dados do banco de dados antigo.
Uma solução melhor para o problema de atualização de bancos de dados é apresentada no final do capítulo.
Inserindo Linhas
O exemplo a seguir cria algumas funções e usuários:
Os construtores de modelos aceitam valores iniciais para os atributos de modelo como argumentos de palavra-
chave. Observe que até mesmo o atributo role pode ser usado, mesmo que não seja uma coluna de banco de
dados real, mas uma representação de alto nível do relacionamento um-para-muitos. O atributo id desses novos
objetos não é definido explicitamente: as chaves primárias são gerenciadas por Flask-SQLAlchemy. Os objetos
existem apenas no lado do Python até agora; eles ainda não foram gravados no banco de dados. Por causa
disso, seu valor id ainda não foi atribuído:
>>> print(admin_role.id)
Nenhum
>>> print(mod_role.id)
As alterações no banco de dados são gerenciadas por meio de uma sessão de banco de dados, que o Flask
SQLAlchemy fornece como db.session. Para preparar os objetos a serem gravados no banco de dados, eles devem ser
adicionados à sessão:
>>> db.session.add(admin_role)
>>> db.session.add(mod_role)
>>> db.session.add(user_role) >>>
db.session.add(user_john) >>>
db.session .add(user_susan) >>>
db.session.add(user_david)
Para gravar os objetos no banco de dados, a sessão precisa ser confirmada chamando seu método commit() :
>>> db.session.commit()
>>> print(admin_role.id) 1
>>> print(mod_role.id)
2
>>> print(user_role.id) 3
As sessões de banco de dados são extremamente úteis para manter o banco de dados consistente. A operação de
confirmação grava todos os objetos que foram adicionados à sessão atomicamente. Se ocorrer um erro durante a
gravação da sessão, toda a sessão será descartada. Se você sempre confirmar as alterações relacionadas em conjunto
em uma sessão, terá a garantia de evitar inconsistências no banco de dados devido a atualizações parciais.
Modificando Linhas
O método add() da sessão do banco de dados também pode ser usado para atualizar modelos. Continuando
na mesma sessão de shell, o exemplo a seguir renomeia a função "Admin" para "Administrador":
Excluindo linhas
A sessão do banco de dados também possui um método delete() . O exemplo a seguir exclui a função
"Moderador" do banco de dados:
Observe que exclusões, como inserções e atualizações, são executadas apenas quando a sessão do
banco de dados é confirmada.
Consultando Linhas
Flask-SQLAlchemy disponibiliza um objeto de consulta em cada classe de modelo. A consulta mais básica
para um modelo é aquela que retorna todo o conteúdo da tabela correspondente:
>>> Role.query.all()
[<Função u'Administrador'>, <Função u'Usuário'>]
>>> User.query.all()
[<Usuário u'john'>, <Usuário u'susan'>, <Usuário u'david'>]
Um objeto de consulta pode ser configurado para emitir pesquisas de banco de dados mais específicas por
meio do uso de filtros. O exemplo a seguir localiza todos os usuários aos quais foi atribuída a função
"Usuário" :
>>> User.query.filter_by(role=user_role).all()
[<Usuário u'susan'>, <Usuário u'david'>]
Também é possível inspecionar a consulta SQL nativa que o SQLAlchemy gera para uma determinada
consulta convertendo o objeto de consulta em uma string:
>>> str(User.query.filter_by(role=user_role))
'SELECT users.id AS users_id, users.username AS users_username,
users.role_id AS users_role_id FROM users WHERE :param_1 = users.role_id'
Se você sair da sessão do shell, os objetos criados no exemplo anterior deixarão de existir como objetos
Python, mas continuarão existindo como linhas em suas respectivas tabelas de banco de dados. Se você
iniciar uma nova sessão de shell, precisará recriar objetos Python a partir de suas linhas de banco de
dados. O exemplo a seguir emite uma consulta que carrega a função de usuário com o nome "User":
Filtros como filter_by() são invocados em um objeto de consulta e retornam um novo valor refinado
consulta. Vários filtros podem ser chamados em sequência até que a consulta seja configurada conforme necessário.
A Tabela 5-5 mostra alguns dos filtros mais comuns disponíveis para consultas. O completo
Opção Descrição
filter() filter_by() Retorna uma nova consulta que adiciona um filtro adicional à consulta original
Retorna uma nova consulta que adiciona um filtro de igualdade adicional à consulta original
limit() offset() Retorna uma nova consulta que limita o número de resultados da consulta original ao número fornecido
order_by() Retorna uma nova consulta que aplica um deslocamento na lista de resultados da consulta original
Retorna uma nova consulta que classifica os resultados da consulta original de acordo com os critérios fornecidos
group_by() Retorna uma nova consulta que agrupa os resultados da consulta original de acordo com os critérios fornecidos
Depois que os filtros desejados forem aplicados à consulta, uma chamada para all() fará com que o
consulta para executar e retornar os resultados como uma lista, mas existem outras maneiras de acionar o
execução de uma consulta além de all(). A Tabela 5-6 mostra outros métodos de execução de consulta.
Opção Descrição
primeiro() Retorna o primeiro resultado de uma consulta ou None se não houver resultados
first_or_404() Retorna o primeiro resultado de uma consulta, ou aborta a requisição e envia um erro 404 como resposta se houver
não há resultados
get() Retorna a linha que corresponde à chave primária fornecida ou None se nenhuma linha correspondente for encontrada
get_or_404() Retorna a linha que corresponde à chave primária fornecida. Se a chave não for encontrada, ele aborta a solicitação e
envia um erro 404 como resposta
A consulta user_role.users aqui tem um pequeno problema. A consulta implícita executada quando a expressão
user_role.users é emitida internamente chama all() para retornar a lista de usuários. Como o objeto de consulta está
oculto, não é possível refiná-lo com filtros de consulta adicionais. Neste exemplo particular, pode ter sido útil solicitar que
a lista de usuários seja retornada em ordem alfabética. No Exemplo 5-4, a configuração do relacionamento é modificada
com um argumento lazy = 'dynamic' para solicitar que a consulta não seja executada automaticamente.
class Role(db.Model): #
...
users = db.relationship('User', backref='role', lazy='dynamic') #
...
Com o relacionamento configurado desta forma, user_role.users retorna uma query que ainda não foi executada, então
filtros podem ser adicionados a ela:
>>> user_role.users.order_by(User.username).all()
[<User u'david'>, <User u'susan'>] >>>
user_role.users.count() 2
As operações de banco de dados descritas nas seções anteriores podem ser usadas diretamente dentro das funções de
visualização. O exemplo 5-5 mostra uma nova versão da rota da página inicial que registra os nomes inseridos pelos
usuários no banco de dados.
Nesta versão modificada do aplicativo, cada vez que um nome é enviado, o aplicativo o verifica no banco de
dados usando o filtro de consulta filter_by() . Uma variável conhecida é escrita na sessão do usuário para
que após o redirecionamento a informação possa ser enviada para o template, onde é utilizada para
customizar a saudação. Observe que, para que o aplicativo funcione, as tabelas do banco de dados devem
ser criadas em um shell Python, conforme mostrado anteriormente.
A nova versão do modelo associado é mostrada no Exemplo 5-6. Este modelo usa o argumento conhecido
para adicionar uma segunda linha à saudação que é diferente para conhecido e
Novos usuários.
{% extends "base.html" %} {%
import "bootstrap/wtf.html" como wtf %}
{% block page_content %}
<div class="page-header">
<h1>Olá, {% if name %}{{ name }}{% else %}Estranho{% endif %}!</h1> {% if not
known %} <p>Prazer
em conhecê- lo!</p > {% else %}
<p>Feliz
em vê-lo novamente!</p> {% endif
%} </div>
{{ wtf.quick_form(form) }} {%
endblock %}
Para adicionar objetos à lista de importação, o comando shell precisa ser registrado com uma função de
callback make_context . Isso é mostrado no Exemplo 5-7.
def make_shell_context():
A função make_shell_context() registra as instâncias do aplicativo e do banco de dados e os modelos para que sejam
importados automaticamente para o shell:
<class 'app.User'>
Flask-SQLAlchemy cria tabelas de banco de dados a partir de modelos apenas quando eles ainda não existem,
portanto, a única maneira de atualizar as tabelas é destruindo as tabelas antigas primeiro, mas é claro que isso faz
com que todos os dados do banco de dados sejam perdidos.
Uma solução melhor é usar uma estrutura de migração de banco de dados . Da mesma forma, as ferramentas de
controle de versão do código-fonte acompanham as alterações nos arquivos de código-fonte, uma estrutura de migração
de banco de dados acompanha as alterações em um esquema de banco de dados e, em seguida, alterações
incrementais podem ser aplicadas ao banco de dados.
O principal desenvolvedor do SQLAlchemy escreveu uma estrutura de migração chamada Alembic , mas em vez de
usar o Alembic diretamente, os aplicativos Flask podem usar o Flask Migrate extension, um wrapper Alembic leve que
se integra ao Flask-Script para fornecer todas as operações por meio de comandos do Flask-Script.
# ...
Para expor os comandos de migração do banco de dados, o Flask-Migrate expõe uma classe
MigrateCommand que é anexada ao objeto gerenciador do Flask-Script . Neste exemplo, o
comando é anexado usando db.
Antes que as migrações do banco de dados possam ser mantidas, é necessário criar um repositório
de migração com o subcomando init :
Este comando cria uma pasta migrations , onde todos os scripts de migração serão armazenados.
Alembic, uma migração de banco de dados é representada por um script de migração. Este script
tem duas funções chamadas upgrade() e downgrade(). A função upgrade() aplica as alterações do
banco de dados que fazem parte da migração e a função downgrade() as remove. Ao ter a
capacidade de adicionar e remover as alterações, o Alembic pode reconfigurar um banco de dados
para qualquer ponto do histórico de alterações.
Para uma primeira migração, isso é efetivamente equivalente a chamar db.create_all(), mas em
migrações sucessivas o comando upgrade aplica atualizações às tabelas sem afetar seu
conteúdo.
O tópico de design e uso de banco de dados é muito importante; livros inteiros foram escritos
sobre o assunto. Você deve considerar este capítulo como uma visão geral; tópicos mais
avançados serão discutidos em capítulos posteriores. O próximo capítulo é dedicado ao envio de e-mail.
CAPÍTULO 6
Muitos tipos de aplicativos precisam notificar os usuários quando determinados eventos ocorrem, e o
método usual de comunicação é o e-mail. Embora o pacote smtplib da biblioteca padrão do Python possa
ser usado para enviar e-mail dentro de um aplicativo Flask, a extensão Flask-Mail envolve o smtplib e o
integra perfeitamente ao Flask.
A extensão se conecta a um servidor SMTP (Simple Mail Transfer Protocol) e envia e-mails a ele para
entrega. Se nenhuma configuração for fornecida, o Flask-Mail se conecta ao localhost na porta 25 e envia
e-mail sem autenticação. A Tabela 6-1 mostra a lista de chaves de configuração que podem ser usadas
para configurar o servidor SMTP.
Durante o desenvolvimento, pode ser mais conveniente conectar-se a um servidor SMTP externo.
Como exemplo, o Exemplo 6-1 mostra como configurar o aplicativo para enviar e-mail por meio de uma
conta do Google Gmail.
69
Machine Translated by Google
import os
...
# app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os. environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
As duas variáveis de ambiente que contêm o nome de usuário e a senha do servidor de e-mail precisam ser
definidas no ambiente. Se você estiver no Linux ou Mac OS X usando o bash, poderá definir essas variáveis
da seguinte maneira:
70 | Capítulo 6: E-mail
Machine Translated by Google
Observe que a função send() do Flask-Mail usa current_app, então ela precisa ser executada com um
contexto de aplicativo ativado.
criar mensagens de e-mail manualmente todas as vezes, é uma boa ideia abstrair as partes comuns da
funcionalidade de envio de e-mail do aplicativo em uma função. Como um benefício adicional, esta
função pode renderizar corpos de e-mail de modelos Jinja2 para ter mais flexibilidade. A implementação
é mostrada no Exemplo 6-3.
app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin <[email protected]>'
A função depende de duas chaves de configuração específicas do aplicativo que definem uma string de
prefixo para o assunto e o endereço que será usado como remetente. A função send_email recebe o
endereço de destino, uma linha de assunto, um modelo para o corpo do email e uma lista de argumentos
de palavra-chave. O nome do modelo deve ser fornecido sem a extensão, para que duas versões do
modelo possam ser usadas para os corpos de texto simples e rich text. Os argumentos de palavra-
chave passados pelo chamador são fornecidos às chamadas render_template() para que possam ser
usados pelos modelos que geram o corpo do e-mail.
A função de visualização index() pode ser facilmente expandida para enviar um e-mail ao administrador
sempre que um novo nome for recebido com o formulário. O Exemplo 6-4 mostra essa mudança.
subpasta mail dentro dos modelos para mantê-los separados dos modelos normais. Os modelos de e-mail
esperam que o usuário receba um argumento de modelo, portanto, a chamada para send_email() o inclui
como um argumento de palavra-chave.
Com essas variáveis de ambiente definidas, você pode testar o aplicativo e receber um e-mail toda vez que
inserir um novo nome no formulário.
72 | Capítulo 6: E-mail
Machine Translated by Google
mail.send(msg)
Se você executar o aplicativo agora, notará que ele é muito mais responsivo, mas lembre-se
de que, para aplicativos que enviam um grande volume de e-mail, ter um trabalho dedicado ao
envio de e-mail é mais adequado do que iniciar uma nova thread para cada e-mail. . Por
exemplo, a execução da função send_async_email() pode ser enviada para um Celery fila de
tarefas.
Este capítulo completa a visão geral dos recursos que são obrigatórios para a maioria dos
aplicativos da web. O problema agora é que o script hello.py está começando a ficar grande e
isso torna mais difícil trabalhar com ele. No próximo capítulo, você aprenderá como estruturar
um aplicativo maior.
CAPÍTULO 7
Embora ter pequenos aplicativos da Web armazenados em um único script possa ser muito conveniente,
essa abordagem não é bem dimensionada. À medida que o aplicativo cresce em complexidade, trabalhar
com um único arquivo de origem grande torna-se problemático.
Ao contrário da maioria dos outros frameworks web, o Flask não impõe uma organização específica para
grandes projetos; a maneira de estruturar o aplicativo é deixada inteiramente para o desenvolvedor. Neste
capítulo, é apresentada uma possível forma de organizar uma grande aplicação em pacotes e módulos.
Essa estrutura será usada nos demais exemplos do livro.
Estrutura do Projeto
O Exemplo 7-1 mostra o layout básico de um aplicativo Flask.
|-flasky |-
app/ |-
templates/ |-
static/ |-
main/ |-
__init__.py |-
errors.py |-
forms.py |-
views.py |-
__init__.py |-
email.py |
-models.py |-
migrations/ |-
tests/ |-
__init__.py |-
test*.py |-
venv/ |-
requirements.txt
75
Machine Translated by Google
|-config.py |-
manage.py
migrations contém os scripts de migração do banco de dados, como antes. • Os testes de unidade
• requirements.txt lista as dependências do pacote para que seja fácil gerar novamente um ambiente virtual
idêntico em um computador diferente.
Para ajudá-lo a entender totalmente essa estrutura, as seções a seguir descrevem o processo para converter o
aplicativo hello.py nela.
Opções de configuração
Os aplicativos geralmente precisam de vários conjuntos de configuração. O melhor exemplo disso é a necessidade
de usar diferentes bancos de dados durante o desenvolvimento, teste e produção para que não interfiram entre si.
Em vez da configuração de estrutura semelhante a um dicionário simples usada por hello.py, uma hierarquia de
classes de configuração pode ser usada. O Exemplo 7-2 mostra o arquivo config.py .
import os
basedir = os.path.abspath(os.path.dirname(__file__))
configuração de
classe : SECRET_KEY = os.environ.get('SECRET_KEY') ou 'string difícil de adivinhar'
SQLALCHEMY_COMMIT_ON_TEARDOWN =
Verdadeiro FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]'
FLASKY_MAIL_SENDER = 'Flasky Admin <[email protected]>'
FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN')
@staticmethod
def init_app(aplicativo):
passar
class DevelopmentConfig(Config):
DEBUGAR = Verdadeiro
MAIL_SERVER = 'smtp.googlemail.com'
MAIL_PORT = 587
MAIL_USE_TLS =
Verdadeiro MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') ou \
'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') ou \
'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')
classe ProduçãoConfig(Config):
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') ou \
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
config =
{ 'desenvolvimento': DevelopmentConfig,
'testing': TestingConfig,
'produção': ProductionConfig,
'padrão': DevelopmentConfig
}
A classe base Config contém configurações que são comuns a todas as configurações; as diferentes
subclasses definem configurações que são específicas para uma configuração. Configurações adicionais
podem ser adicionadas conforme necessário.
Para tornar a configuração mais flexível e segura, algumas configurações podem ser opcionalmente
importadas de variáveis de ambiente. Por exemplo, o valor de SECRET_KEY, devido à sua natureza
sensível, pode ser definido no ambiente, mas um valor padrão é fornecido caso o ambiente não o defina.
A variável SQLALCHEMY_DATABASE_URI recebe diferentes valores em cada uma das três configurações.
Isso permite que o aplicativo seja executado em diferentes configurações, cada uma usando um banco de
dados diferente.
As classes de configuração podem definir um método de classe init_app() que usa uma instância do
aplicativo como um argumento. Aqui, a inicialização específica da configuração pode ser executada. Por
enquanto, a classe base Config implementa um método init_app() vazio .
Opções de configuração | 77
Machine Translated by Google
Pacote de aplicativos
O pacote do aplicativo é onde residem todos os códigos, modelos e arquivos estáticos do aplicativo.
Ele é chamado simplesmente de aplicativo, embora possa receber um nome específico do aplicativo, se desejado.
Os modelos e as pastas estáticas fazem parte do pacote do aplicativo, portanto, essas duas pastas são movidas
para dentro do app. Os modelos de banco de dados e as funções de suporte de e-mail também são movidos
dentro deste pacote, cada um em seu próprio módulo como app/ models.py e app/ email.py.
como o aplicativo é criado na versão de arquivo único é muito conveniente, mas tem uma grande desvantagem.
Como o aplicativo é criado no escopo global, não há como aplicar as alterações de configuração dinamicamente:
no momento em que o script está sendo executado, a instância do aplicativo já foi criada, portanto, já é tarde
demais para fazer alterações na configuração. Isso é particularmente importante para testes de unidade porque
às vezes é necessário executar o aplicativo em configurações diferentes para uma melhor cobertura de teste.
A solução para esse problema é atrasar a criação do aplicativo movendo-o para uma função de fábrica que pode
ser invocada explicitamente a partir do script. Isso não apenas dá ao script tempo para definir a configuração, mas
também a capacidade de criar várias instâncias do aplicativo - algo que também pode ser muito útil durante o
teste. A função de fábrica do aplicativo, mostrada no Exemplo 7-3, é definida no construtor do pacote do aplicativo .
Este construtor importa a maioria das extensões do Flask atualmente em uso, mas como não há nenhuma
instância do aplicativo para inicializá-las, ele as cria não inicializadas ao não passar nenhum argumento para seus
construtores. A função create_app() é a fábrica de aplicativos, que recebe como argumento o nome de uma
configuração a ser usada para o aplicativo. As definições de configuração armazenadas em uma das classes
definidas em config.py podem ser importadas diretamente para o aplicativo usando o método from_object()
disponível no objeto de configuração app.config do Flask. O objeto de configuração é selecionado pelo nome no
dicionário de configuração . Depois que um aplicativo é criado e configurado, as extensões podem ser inicializadas.
Chamar init_app() nas extensões que foram criadas anteriormente conclui sua inicialização.
bootstrap.init_app(app)
mail.init_app(app)
moment.init_app(app)
db.init_app(app)
aplicativo de retorno
A função de fábrica retorna a instância do aplicativo criada, mas observe que os aplicativos criados
com a função de fábrica em seu estado atual estão incompletos, pois faltam rotas e manipuladores
de página de erro personalizados. Este é o tema da próxima seção.
conversão para uma fábrica de aplicativos apresenta uma complicação para as rotas. Em aplicativos
de script único, a instância do aplicativo existe no escopo global, portanto, as rotas podem ser
facilmente definidas usando o decorador app.route . Mas agora que o aplicativo foi criado em tempo
de execução, o decorador app.route começa a existir somente depois que create_app() é invocado,
o que é tarde demais. Assim como as rotas, os manipuladores de página de erro personalizados
apresentam o mesmo problema, pois são definidos com o decorador app.errorhandler .
Felizmente, o Flask oferece uma solução melhor usando blueprints. Um blueprint é semelhante a um
aplicativo, pois também pode definir rotas. A diferença é que as rotas associadas a um blueprint
ficam em um estado inativo até que o blueprint seja registrado em um aplicativo, momento em que
as rotas se tornam parte dele. Usando um blueprint definido no escopo global, as rotas do aplicativo
podem ser definidas quase da mesma forma que no aplicativo de script único.
Assim como os aplicativos, os blueprints podem ser definidos em um único arquivo ou podem ser
criados de forma mais estruturada com vários módulos dentro de um pacote. Para permitir a maior
flexibilidade, um subpacote dentro do pacote do aplicativo será criado para hospedar o blueprint.
O Exemplo 7-4 mostra o construtor de pacote, que cria o projeto.
Pacote de aplicativos | 79
Machine Translated by Google
Blueprints são criados instanciando um objeto da classe Blueprint. O construtor dessa classe usa dois
argumentos obrigatórios: o nome do blueprint e o módulo ou pacote no qual o blueprint está localizado. Assim
como nos aplicativos, a variável __name__ do Python é, na maioria dos casos, o valor correto para o segundo
argumento.
As rotas do aplicativo são armazenadas em um módulo app/ main/ views.py dentro do pacote e os
manipuladores de erro estão em app/ main/ errors.py. A importação desses módulos faz com que as rotas e
manipuladores de erros sejam associados ao blueprint. É importante observar que os módulos são importados
na parte inferior do script app/ __init__.py para evitar dependências circulares, porque views.py e errors.py
precisam importar o blueprint principal .
O blueprint é registrado com o aplicativo dentro da função de fábrica create_app() , conforme mostrado no
Exemplo 7-5.
def create_app(config_name):
# ...
aplicativo de retorno
@main.app_errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@main.app_errorhandler(500)
def internal_server_error(e): return
render_template('500.html'), 500
Uma diferença ao escrever manipuladores de erro dentro de um blueprint é que, se o decorador errorhandler
for usado, o manipulador só será invocado para erros originados no blueprint. Para instalar manipuladores de
erro em todo o aplicativo, o app_errorhandler deve ser usado.
de . import main
from .forms import NameForm
from .. import db
from ..models import User
Existem duas diferenças principais ao escrever uma função de visualização dentro de um blueprint.
Primeiro, como foi feito anteriormente para manipuladores de erros, o decorador de rota vem do projeto.
A segunda diferença está no uso da função url_for() . Como você deve se lembrar, o primeiro
argumento para esta função é o nome do ponto de extremidade da rota, que para rotas baseadas em
aplicativos assume como padrão o nome da função de exibição. Por exemplo, em um aplicativo de
script único, a URL para uma função de visualização index() pode ser obtida com url_for('index').
A diferença com os blueprints é que o Flask aplica um namespace a todos os endpoints provenientes
de um blueprint para que vários blueprints possam definir funções de exibição com os mesmos nomes
de endpoint sem colisões. O namespace é o nome do blueprint (o primeiro argumento para o construtor
Blueprint ), portanto, a função de visualização index() é registrada com o nome do endpoint main.index
e sua URL pode ser obtida com url_for('main.index').
A função url_for() também oferece suporte a um formato mais curto para terminais em blueprints nos
quais o nome do blueprint é omitido, como url_for('.index'). Com essa notação, o blueprint da solicitação
atual é usado. Isso significa efetivamente que os redirecionamentos dentro do mesmo blueprint podem
usar o formato mais curto, enquanto os redirecionamentos entre blueprints devem usar o nome do
endpoint com namespace.
Para concluir as alterações na página do aplicativo, os objetos de formulário também são armazenados
dentro do blueprint em um módulo app/ main/ forms.py .
Script de inicialização
O arquivo manage.py na pasta de nível superior é usado para iniciar o aplicativo. Esse script é
mostrado no Exemplo 7-8.
Script de lançamento | 81
Machine Translated by Google
def make_shell_context():
return dict(app=app, db=db, User=User, Role=Role)
manager.add_command("shell", Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)
if __name__ == '__main__':
manager.run()
Como uma conveniência, uma linha shebang é adicionada, para que em sistemas operacionais baseados
em Unix o script possa ser executado como ./manage.py em vez do python mais detalhado manage.py.
Arquivo de Requisitos
Os aplicativos devem incluir um arquivo requirements.txt que registra todas as dependências do pacote,
com os números de versão exatos. Isso é importante caso o ambiente virtual precise ser regenerado em
uma máquina diferente, como a máquina na qual o aplicativo será implantado para uso em produção.
Este arquivo pode ser gerado automaticamente pelo pip com o seguinte comando:
É uma boa ideia atualizar esse arquivo sempre que um pacote for instalado ou atualizado. Um arquivo
de requisitos de exemplo é mostrado aqui:
Frasco==0.10.1
Flask-Bootstrap==3.0.3.1
Flask-Mail==0.9.0
Flask-Migrate==1.1.0
Flask-Moment==0.2.0
Flask-SQLAlchemy==1.0
Flask-Script==0.6.6
Flask-WTF= =0,9,4
Jinja2==2.7.1
Mako==0.9.1
MarkupSafe==0.18
SQLAlchemy==0.8.4
WTForms==1.0.5
Ferramenta==0.9.4
alambique==0.6.2
pisca-pisca==1.3
é perigoso==0.23
Quando você precisar construir uma réplica perfeita do ambiente virtual, você pode criar um novo ambiente
virtual e executar o seguinte comando nele:
Testes de unidade
Este aplicativo é muito pequeno, então não há muito o que testar ainda, mas como exemplo, dois testes
simples podem ser definidos conforme mostrado no Exemplo 7-9.
import unittest
from flask import current_app from
app import create_app, db
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
def test_app_exists(self):
self.assertFalse(current_app is None)
def test_app_is_testing(self):
self.assertTrue(current_app.config['TESTING'])
Testes Unitários | 83
Machine Translated by Google
Os testes são escritos usando o pacote unittest padrão da biblioteca padrão do Python. Os métodos setUp()
e tearDown() são executados antes e depois de cada teste, e quaisquer métodos que tenham um nome que
comece com test_ são executados como testes.
Se você quiser aprender mais sobre como escrever testes de unidade com
o pacote unittest do Python , leia a documentação oficial.
O método setUp() tenta criar um ambiente para o teste que seja próximo ao de um aplicativo em execução.
Ele primeiro cria um aplicativo configurado para teste e ativa seu contexto. Esta etapa garante que os testes
tenham acesso a current_app, como solicitações regulares.
Em seguida, ele cria um novo banco de dados que o teste pode usar quando necessário. O banco de dados
e o contexto do aplicativo são removidos no método tearDown() .
O primeiro teste garante que a instância do aplicativo existe. O segundo teste garante que o aplicativo
esteja em execução na configuração de teste. Para tornar a pasta de testes um pacote adequado, um
arquivo tests/ __init__.py precisa ser adicionado, mas pode ser um arquivo vazio, pois o pacote unittest
pode escanear todos os módulos e localizar os testes.
Para executar os testes de unidade, um comando personalizado pode ser adicionado ao script manage.py .
O Exemplo 7-10 mostra como adicionar um comando de teste .
@manager.command
def test():
"""Execute os testes de unidade."""
importar testes
unittest = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)
.------------------------------------------------- ---------------------
Realizou 2 testes em 0,001s
OK
A URL do banco de dados é obtida de uma variável de ambiente como primeira escolha, com um banco de
dados SQLite padrão como alternativa. As variáveis de ambiente e os nomes dos arquivos do banco de
dados SQLite são diferentes para cada uma das três configurações. Por exemplo, na configuração de
desenvolvimento, a URL é obtida da variável de ambiente DEV_DATABASE_URL e, se ela não for definida,
será usado um banco de dados SQLite com o nome data-dev.sqlite .
Independentemente da origem da URL do banco de dados, as tabelas do banco de dados devem ser criadas
para o novo banco de dados. Ao trabalhar com o Flask-Migrate para acompanhar as migrações, as tabelas
de banco de dados podem ser criadas ou atualizadas para a revisão mais recente com um único comando:
Acredite ou não, você chegou ao final da Parte I. Agora você aprendeu os elementos básicos necessários
para construir um aplicativo da Web com o Flask, mas provavelmente não tem certeza de como todas essas
peças se encaixam para formar um aplicativo real. O objetivo da Parte II é ajudar com isso, orientando você
no desenvolvimento de um aplicativo completo.
PARTE II
Aplicativo de blog
Machine Translated by Google
Machine Translated by Google
CAPÍTULO 8
Autenticação de usuário
A maioria dos aplicativos precisa acompanhar quem são seus usuários. Quando os usuários se conectam com
o aplicativo, eles se autenticam com ele, um processo pelo qual tornam sua identidade conhecida. Uma vez
que o aplicativo saiba quem é o usuário, ele pode oferecer uma experiência personalizada.
O método de autenticação mais comumente usado exige que os usuários forneçam uma identificação (seja
seu e-mail ou nome de usuário) e uma senha secreta. Neste capítulo, o sistema de autenticação completo para
Flasky é criado.
Além dos pacotes específicos de autenticação, as seguintes extensões de uso geral serão usadas:
89
Machine Translated by Google
Senha de Segurança
A segurança das informações do usuário armazenadas em bancos de dados geralmente é negligenciada
durante o design de aplicativos da web. Se um invasor conseguir invadir seu servidor e acessar seu banco
de dados de usuários, você arriscará a segurança de seus usuários, e o risco é maior do que você pensa.
É um fato conhecido que a maioria dos usuários usa a mesma senha em vários sites, portanto, mesmo
que você não armazene nenhuma informação confidencial, o acesso às senhas armazenadas em seu
banco de dados pode dar ao invasor acesso às contas que seus usuários têm em outros sites. sites.
A chave para armazenar senhas de usuários com segurança em um banco de dados não depende do
armazenamento da senha em si, mas de um hash dela. Uma função de hash de senha usa uma senha
como entrada e aplica uma ou mais transformações criptográficas a ela. O resultado é uma nova sequência
de caracteres que não tem nenhuma semelhança com a senha original. Os hashes de senha podem ser
verificados no lugar das senhas reais porque as funções de hash são repetíveis: dadas as mesmas
entradas, o resultado é sempre o mesmo.
O Exemplo 8-1 mostra as mudanças no modelo User criado no Capítulo 5 para acomodar o hash de senha.
classe User(db.Model): #
...
password_hash = db.Column(db.String(128))
@property
def password(self):
raise AttributeError('senha não é um atributo legível')
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(senha)
A função hash de senha é implementada por meio de uma propriedade somente gravação
chamada senha. Quando esta propriedade é definida, o método setter chamará a função
generate_password_hash() de Werkzeug e gravará o resultado no campo password_hash .
A tentativa de ler a propriedade da senha retornará um erro, pois claramente a senha original
não pode ser recuperada após o hash.
A funcionalidade de hash de senha agora está completa e pode ser testada no shell:
>>> u.verify_password('cachorro')
Falso
>>> u2 = User()
>>> u2.password = 'cat' >>>
u2.password_hash
'pbkdf2:sha1:1000$UjvnGeTP$875e28eb0874f44101d6b332442218f66975ee89'
Segurança de senha | 91
Machine Translated by Google
No futuro, os testes acima podem ser escritos como testes de unidade que podem ser repetidos
facilmente. No Exemplo 8-2, um novo módulo dentro do pacote de testes é mostrado com três novos
testes que exercitam as alterações recentes no modelo User .
import unittest
from app.models import User
def test_no_password_getter(self): u =
User(password = 'cat') with
self.assertRaises(AttributeError): u.password
def test_password_verification(self):
u = User(senha = 'gato')
self.assertTrue(u.verify_password('gato'))
self.assertFalse(u.verify_password('cachorro'))
def test_password_salts_are_random(self): u =
User(password='cat') u2 =
User(password='cat')
self.assertTrue(u.password_hash != u2.password_hash)
O esquema de autenticação será hospedado em um pacote Python com o mesmo nome. O construtor
de pacote do blueprint cria o objeto blueprint e importa rotas de um módulo views.py . Isso é mostrado
no Exemplo 8-3.
de . importar visualizações
O módulo app/ auth/ views.py , mostrado no Exemplo 8-4, importa o blueprint e define as rotas associadas
à autenticação usando seu decorador de rota . Por enquanto, uma rota /login foi adicionada, o que
renderiza um modelo de espaço reservado com o mesmo nome.
@auth.route('/login') def
login():
return render_template('auth/login.html')
Observe que o arquivo de modelo fornecido para render_template() é armazenado dentro da pasta auth .
Esta pasta deve ser criada dentro de app/ templates, pois o Flask espera que os templates sejam
relativos à pasta de templates da aplicação. Ao armazenar os modelos de blueprint em uma pasta
própria, não há risco de colisões de nomes com o blueprint principal ou quaisquer outros blueprints que
venham a ser adicionados no futuro.
def create_app(config_name):
...
# de .auth import auth como auth_blueprint
app.register_blueprint(auth_blueprint, url_prefix='/auth')
aplicativo de retorno
O argumento url_prefix no registro do blueprint é opcional. Ao serem utilizadas, todas as rotas definidas
no blueprint serão cadastradas com o prefixo informado, neste caso /auth. Por exemplo, a rota /login
será registrada como /auth/ login e a URL totalmente qualificada no servidor da Web de desenvolvimento
se tornará http:// localhost:5000/ auth/ login.
poder trabalhar com o modelo de usuário do aplicativo , a extensão Flask-Login requer que alguns métodos sejam
implementados por ela. Os métodos necessários são mostrados na Tabela 8-1.
Método Descrição
is_authenticated() Deve retornar True se o usuário tiver credenciais de login ou False caso
contrário. está ativo() Deve retornar True se o usuário tiver permissão para efetuar login ou False caso contrário. Um valor de retorno
Falso pode ser usado para contas desativadas.
get_id() Deve retornar um identificador exclusivo para o usuário, codificado como uma string Unicode.
Esses quatro métodos podem ser implementados diretamente como métodos na classe de modelo, mas como
uma alternativa mais fácil, o Flask-Login fornece uma classe UserMixin que possui implementações padrão
apropriadas para a maioria dos casos. O modelo de usuário atualizado é mostrado no Exemplo 8-6.
Exemplo 8-6. app/ models.py: Atualizações no modelo de usuário para dar suporte a logins de usuários
Observe que um campo de e-mail também foi adicionado. Nesse aplicativo, os usuários farão login com seu e-
mail, pois é menos provável que esqueçam seus endereços de e-mail do que seus nomes de usuário.
login_manager = LoginManager()
login_manager.session_protection = 'forte'
login_manager.login_view = 'auth.login'
def create_app(config_name):
# ...
login_manager.init_app(aplicativo)
# ...
O atributo session_protection do objeto LoginManager pode ser definido como None, 'basic' ou 'strong' para
fornecer diferentes níveis de segurança contra adulteração de sessão do usuário. Com a configuração 'forte' ,
o Flask-Login acompanhará o endereço IP do cliente e o agente do navegador e desconectará o usuário se
detectar uma alteração. O atributo login_view define o endpoint para a página de login. Lembre-se de que, como
a rota de login está dentro de um blueprint, ela precisa ser prefixada com o nome do blueprint.
Por fim, Flask-Login exige que o aplicativo configure uma função de retorno de chamada que carregue um
usuário, dado o identificador. Essa função é mostrada no Exemplo 8-8.
de . importar login_manager
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
A função de retorno de chamada do carregador de usuário recebe um identificador de usuário como uma string
Unicode. O valor de retorno da função deve ser o objeto do usuário, se disponível, ou None, caso contrário.
proteger uma rota para que ela só possa ser acessada por usuários autenticados, Flask-Login fornece um
decorador login_required . Segue um exemplo de seu uso:
@app.route('/secret')
@login_required
def secret():
return 'Apenas usuários autenticados são permitidos!'
Se essa rota for acessada por um usuário não autenticado, o Flask-Login interceptará a solicitação e enviará
o usuário para a página de login.
Adicionando um formulário
de login O formulário de login que será apresentado aos usuários tem um campo de texto para o endereço
de e-mail, um campo de senha, uma caixa de seleção “lembrar-me” e um botão de envio. A classe de
formulário Flask-WTF é mostrada no Exemplo 8-9.
class LoginForm(Form):
email = StringField('Email', validators=[Required(), Length(1, 64),
Email()])
password = PasswordField('Password', validators=[Required()])
Remember_me = BooleanField('Mantenha- me conectado')
submit = SubmitField('Log In')
O campo email aproveita os validadores Length() e Email() fornecidos pelo WTForms. A classe PasswordField
representa um elemento <input> com type="password". A classe BooleanField representa uma caixa de
seleção.
O modelo associado à página de login é armazenado em auth/ login.html. Este modelo só precisa renderizar
o formulário usando a macro wtf.quick_form() do Flask-Bootstrap .
A Figura 8-1 mostra o formulário de login renderizado pelo navegador da web.
A barra de navegação no modelo base.html usa uma condicional Jinja2 para exibir os links “Sign In” ou “Sign
Out” dependendo do estado de login do usuário atual. A condicional é mostrada no Exemplo 8-10.
Exemplo 8-10. app/ templates/ base.html: links da barra de navegação para entrar e sair
A variável current_user usada na condicional é definida por Flask-Login e está automaticamente disponível
para visualizar funções e modelos. Essa variável contém o usuário conectado no momento ou um objeto de
usuário anônimo proxy se o usuário não estiver conectado.
Objetos de usuário anônimo respondem ao método is_authenticated() com False, portanto, essa é uma
maneira conveniente de saber se o usuário atual está conectado.
Conectando usuários
A função view cria um objeto LoginForm e o usa como o formulário simples do Capítulo 4. Quando a
solicitação é do tipo GET, a função view apenas renderiza o modelo, que
por sua vez exibe o formulário. Quando o formulário é enviado em uma solicitação POST , a função
valid_on_submit() do Flask-WTF valida as variáveis do formulário e, em seguida, tenta fazer o login do usuário.
Para fazer login de um usuário, a função começa carregando o usuário do banco de dados usando o e-mail
fornecido com o formulário. Se existir um usuário com o endereço de e-mail fornecido, seu método
verify_password() será chamado com a senha que também veio com o formulário.
Se a senha for válida, a função login_user() do Flask-Login é invocada para registrar o usuário como logado para
a sessão do usuário. A função login_user() leva o usuário a fazer login e um booleano opcional “lembre-se de
mim”, que também foi enviado com o formulário.
Um valor False para este argumento faz com que a sessão do usuário expire quando a janela do navegador for
fechada, portanto, o usuário terá que efetuar login novamente na próxima vez. Um valor True faz com que um
cookie de longo prazo seja definido no navegador do usuário e com isso a sessão do usuário possa ser
restaurada.
De acordo com o padrão Post/Redirect/Get discutido no Capítulo 4, a solicitação POST que enviou as credenciais
de login termina com um redirecionamento, mas há dois possíveis destinos de URL. Se o formulário de login foi
apresentado ao usuário para impedir o acesso não autorizado a um URL protegido, o Flask-Login salvou o URL
original no próximo argumento de string de consulta, que pode ser acessado no dicionário request.args .
Se o próximo argumento da string de consulta não estiver disponível, será emitido um redirecionamento para a
página inicial. Se o e-mail ou a senha fornecidos pelo usuário forem inválidos, uma mensagem flash é definida e
o formulário é renderizado novamente para que o usuário tente novamente.
O modelo de login precisa ser atualizado para renderizar o formulário. Essas mudanças são mostradas no
Exemplo 8-12.
{% extends "base.html" %} {%
import "bootstrap/wtf.html" como wtf %}
{% block page_content %}
<div class="page-header">
<h1>Login</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
Desconectando usuários
@auth.route('/logout')
@login_required
def logout():
logout_user()
flash('Você foi desconectado.') return
redirect(url_for('main.index'))
Testando logins
Para verificar se a funcionalidade de login está funcionando, a página inicial pode ser atualizada para
saudar o usuário conectado pelo nome. A seção do modelo que gera a saudação é mostrada no
Exemplo 8-14.
Neste modelo, mais uma vez, current_user.is_authenticated() é usado para determinar se o usuário
está logado.
Como nenhuma funcionalidade de registro de usuário foi criada, um novo usuário pode ser registrado
a partir do shell:
O usuário criado anteriormente agora pode fazer login. A Figura 8-2 mostra a página inicial do aplicativo
com o usuário logado.
usuário O formulário que será usado na página de registro solicita que o usuário insira um endereço
de e-mail, nome de usuário e senha. Essa forma é mostrada no Exemplo 8-15.
class RegistrationForm(Form):
email = StringField('Email', validators=[Required(), Length(1, 64), Email()]) username
=
StringField('Username', validators=[ Required(), Length( 1,
64), Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,
'Nomes de usuário devem ter apenas letras,
' 'números, pontos ou sublinhados')])
password = PasswordField('Password', validators=[ Required(),
EqualTo('password2', message='As senhas devem corresponder.')]) password2 =
PasswordField('Confirmar senha', validators=[Required()]) enviar = EnviarCampo('Registrar')
Este formulário usa o validador Regexp de WTForms para garantir que o campo de nome de usuário
contenha apenas letras, números, sublinhados e pontos. Os dois argumentos para o validador que
seguem a expressão regular são os sinalizadores de expressão regular e a mensagem de erro a ser
exibida em caso de falha.
A senha é digitada duas vezes como medida de segurança, mas nesta etapa é necessário validar
se os dois campos de senha possuem o mesmo conteúdo, o que é feito com outro validador da
WTForms chamado EqualTo. Este validador é anexado a um dos campos de senha com o nome do
outro campo dado como um argumento.
Este formulário também possui dois validadores personalizados implementados como métodos.
Quando um formulário define um método com o prefixo valid_ seguido pelo nome de um campo, o
método é invocado além de quaisquer validadores definidos regularmente. Nesse caso, os
validadores personalizados para e-mail e nome de usuário garantem que os valores fornecidos não
sejam duplicados. Os validadores personalizados indicam um erro de validação lançando uma
exceção ValidationError com o texto da mensagem de erro como argumento.
O modelo que apresenta este formato é chamado /templates/ auth/ register.html. Como o modelo de
login, este também renderiza o formulário com wtf.quick_form(). A página de registro é mostrada na
Figura 8-3.
A página de registro precisa estar vinculada à página de login para que os usuários que não possuem uma conta
possam encontrá-la facilmente. Essa mudança é mostrada no Exemplo 8-16.
Exemplo 8-16. app/ templates/ auth/ login.html: Link para a página de registro
<p>
Novo usuário?
<a href="{{ url_for('auth.register') }}"> Clique aqui
para se registrar </a> </p>
gestão do registo de utilizadores não apresenta grandes surpresas. Quando o formulário de registro é enviado e
validado, um novo usuário é adicionado ao banco de dados usando as informações fornecidas pelo usuário. A
função view que executa esta tarefa é mostrada no Exemplo 8-17.
Confirmação de conta
Para determinados tipos de aplicativos, é importante garantir que as informações do usuário fornecidas
durante o registro sejam válidas. Um requisito comum é garantir que o usuário possa ser contatado por
meio do endereço de e-mail fornecido.
Para validar o endereço de e-mail, os aplicativos enviam um e-mail de confirmação aos usuários
imediatamente após o registro. A nova conta é inicialmente marcada como não confirmada até que as
instruções no e-mail sejam seguidas, o que prova que o usuário pode ser contatado. O procedimento de
confirmação da conta geralmente envolve clicar em um link de URL especialmente criado que inclui um
token de confirmação.
de conta mais simples seria um URL com o formato http:// www.example.com/ auth/ confirm/ <id> incluído
no e-mail de confirmação, onde id é o id numérico atribuído ao usuário no banco de dados. Quando o
usuário clica no link, a função view que manipula essa rota recebe o ID do usuário para confirmar como
um argumento e pode facilmente atualizar o status confirmado do usuário.
Mas obviamente não é uma implementação segura, pois qualquer usuário que descobrir o formato dos
links de confirmação poderá confirmar contas arbitrárias apenas enviando números aleatórios na URL. A
ideia é substituir o id na URL por um token que contenha as mesmas informações criptografadas com
segurança.
Se você se lembra da discussão sobre as sessões do usuário no Capítulo 4, o Flask usa cookies
assinados criptograficamente para proteger o conteúdo das sessões do usuário contra adulteração. estes seguros
os cookies são assinados por um pacote chamado itsdangerous. A mesma ideia pode ser aplicada
aos tokens de confirmação.
O seguinte é uma sessão de shell curta que mostra como seu perigoso pode gerar um seguro
token que contém um id de usuário dentro:
O método dumps() gera uma assinatura criptográfica para os dados fornecidos como um argumento
e então serializa os dados mais a assinatura como uma string de token conveniente. O argumento
expires_in define um tempo de expiração para o token expresso em segundos.
Para decodificar o token, o objeto serializador fornece um método load() que usa o token como seu
único argumento. A função verifica a assinatura e o prazo de validade e, se for válida, retorna os
dados originais. Quando o método load() recebe um token inválido ou um token válido que expirou,
uma exceção é lançada.
A geração e verificação de token usando essa funcionalidade podem ser adicionadas ao modelo de
usuário . As mudanças são mostradas no Exemplo 8-18.
data = s.loads(token)
exceto:
return False
if data.get('confirm') != self.id:
return False
self.confirmed = True
db.session.add(self)
retornar Verdadeiro
O método generate_confirmation_token() gera um token com um tempo de validade padrão de uma hora.
O método confirm() verifica o token e, se válido, define o novo atributo confirmado como True.
Além de verificar o token, a função confirm() verifica se o id do token corresponde ao usuário logado, que
é armazenado em current_user. Isso garante que, mesmo que um usuário mal-intencionado descubra
como gerar tokens assinados, ele não poderá confirmar a conta de outra pessoa.
Como uma nova coluna foi adicionada ao modelo para rastrear o estado
confirmado de cada conta, uma nova migração de banco de dados
precisa ser gerada e aplicada.
Os dois novos métodos adicionados ao modelo User são facilmente testados em testes de unidade. Você
pode encontrar os testes de unidade no repositório GitHub para o aplicativo.
atual /register redireciona para /index depois de adicionar o novo usuário ao banco de dados.
Antes de redirecionar, esta rota agora precisa enviar o e-mail de confirmação. Essa mudança é mostrada
no Exemplo 8-19.
Exemplo 8-19. app/ auth/ views.py: rota de registro com e-mail de confirmação
Observe que uma chamada db.session.commit() teve que ser adicionada, mesmo que o banco de
dados automático configurado pelo aplicativo confirme no final da solicitação. O problema é que
novos usuários recebem um id quando são confirmados no banco de dados. Como o id é
necessário para o token de confirmação, a confirmação não pode ser adiada.
Os modelos de e-mail usados pelo blueprint de autenticação serão adicionados na pasta templates/
auth/ email para mantê-los separados dos modelos HTML. Conforme discutido no Capítulo 6, para
cada e-mail são necessários dois modelos para as versões de texto simples e rich text do corpo.
Como exemplo, o Exemplo 8-20 mostra a versão em texto simples do modelo de e-mail de
confirmação e você pode encontrar a versão HTML equivalente no repositório GitHub.
Exemplo 8-20. app/ auth/ templates/ auth/ email/ confirm.txt: corpo do texto do e-mail de confirmação
Bem-vindo a Flasky!
Sinceramente,
A Equipe Flasky
Por padrão, url_for() gera URLs relativos, então, por exemplo, url_for('auth.confirm', token='abc')
retorna a string '/auth/confirm/abc'.
Isso, é claro, não é um URL válido que pode ser enviado em um e-mail. Os URLs relativos
funcionam bem quando são usados no contexto de uma página da Web porque o navegador os
converte em absolutos adicionando o nome do host e o número da porta da página atual, mas ao
enviar um URL por e-mail, esse contexto não existe. O argumento _external=True é adicionado à
chamada url_for() para solicitar uma URL totalmente qualificada que inclua o esquema (http:// ou
https://), nome do host e porta.
@auth.route('/confirm/<token>')
@login_required
def confirm(token): if
current_user.confirmed: return
redirect(url_for('main.index')) if
current_user.confirm(token): flash('Você
confirmou sua conta. Obrigado!') else: flash('The o link de
Essa rota é protegida com o decorador login_required do Flask-Login, de modo que, quando os
usuários clicarem no link do e-mail de confirmação, eles serão solicitados a fazer login antes de
acessarem essa função de visualização.
A função primeiro verifica se o usuário logado já está confirmado, e nesse caso redireciona para a
página inicial, pois obviamente não há o que fazer. Isso pode evitar trabalho desnecessário se um
usuário clicar no token de confirmação várias vezes por engano.
Como a confirmação real do token é feita inteiramente no modelo User , tudo o que a função view
precisa fazer é chamar o método confirm() e, em seguida, piscar uma mensagem de acordo com
o resultado. Quando a confirmação é bem-sucedida, o atributo confirmado do model User é
alterado e adicionado à sessão, que será confirmada quando a solicitação terminar.
Cada aplicativo pode decidir o que os usuários não confirmados podem fazer antes de confirmarem
sua conta. Uma possibilidade é permitir que usuários não confirmados façam login, mas apenas
mostrar a eles uma página que solicita que confirmem suas contas antes de obter acesso.
Essa etapa pode ser executada usando o hook before_request do Flask , que foi brevemente
descrito no Capítulo 2. A partir de um blueprint, o hook before_request aplica-se apenas às
requisições que pertencem ao blueprint. Para instalar um gancho para todas as solicitações de
aplicativo de um blueprint, o decorador before_app_request deve ser usado. O Exemplo 8-22
mostra como esse manipulador é implementado.
Exemplo 8-22. app/ auth/ views.py: Filtre contas não confirmadas no manipulador before_app_request
@auth.before_app_request
def before_request(): se
current_user.is_authenticated() \ e não
current_user.confirmed \ e
request.endpoint[:5] != 'auth.': return
redirect(url_for('auth.unconfirmed'))
@auth.route('/unconfirmed') def
unconfirmed(): se
current_user.is_anonymous() ou current_user.confirmed:
return redirect('main.index') return
render_template('auth/unconfirmed.html')
Se as três condições forem atendidas, um redirecionamento será emitido para uma nova rota /auth/
unconfirmed que mostra uma página com informações sobre a confirmação da conta.
A página que é apresentada aos usuários não confirmados (mostrada na Figura 8-4) apenas renderiza
um modelo que fornece instruções aos usuários sobre como confirmar sua conta e oferece um link
para solicitar um novo e-mail de confirmação, caso o e-mail original tenha sido perdido. A rota que
reenvia o e-mail de confirmação é mostrada no Exemplo 8-23.
Esta rota repete o que foi feito na rota de cadastro usando current_user, o usuário que está logado,
como usuário alvo. Essa rota também é protegida com login_required para garantir que, ao ser
acessada, seja conhecido o usuário que está fazendo a solicitação.
Gerenciamento de contas
Os usuários que possuem contas com o aplicativo podem precisar fazer alterações em suas contas de
tempos em tempos. As seguintes tarefas podem ser adicionadas ao plano de autenticação usando as
técnicas apresentadas neste capítulo: Atualizações de senha
Usuários preocupados
com a segurança podem querer alterar suas senhas periodicamente. Este é um recurso fácil de
implementar, pois desde que o usuário esteja logado, é seguro apresentar um formulário que
solicita a senha antiga e uma nova senha para substituí-la.
(Esse recurso é implementado como commit 8f no repositório GitHub.)
Redefinições de
senha Para evitar que os usuários fiquem fora do aplicativo quando eles esquecem suas senhas,
uma opção de redefinição de senha pode ser oferecida. Para implementar redefinições de senha
de maneira segura, é necessário usar tokens semelhantes aos usados para confirmar contas.
Quando um usuário solicita uma redefinição de senha, um e-mail com um token de redefinição é
enviado para o endereço de e-mail registrado. O usuário então clica no link do e-mail e, após a
verificação do token, é apresentado um formulário onde uma nova senha pode ser inserida. (Esse
recurso é implementado como commit 8g no repositório GitHub.)
Alterações de endereço
de e-mail Os usuários podem ter a opção de alterar o endereço de e-mail registrado, mas antes
que o novo endereço seja aceito, ele deve ser verificado com um e-mail de confirmação. Para usar isso
recurso, o usuário insere o novo endereço de e-mail em um formulário. Para confirmar o endereço
de e-mail, um token é enviado por e-mail para esse endereço. Quando o servidor recebe o token de
volta, ele pode atualizar o objeto do usuário. Enquanto o servidor espera para receber o token, ele
pode armazenar o novo endereço de e-mail em um novo campo de banco de dados reservado para
endereços de e-mail pendentes ou pode armazenar o endereço no token junto com o id . (Esse
recurso é implementado como commit 8h no repositório GitHub.)
No próximo capítulo, o subsistema de usuário do Flasky será estendido por meio do uso de funções de
usuário.
CAPÍTULO 9
Funções do usuário
Nem todos os usuários de aplicativos da web são criados iguais. Na maioria dos aplicativos, uma pequena
porcentagem de usuários recebe poderes extras para ajudar a manter o aplicativo funcionando sem
problemas. Os administradores são o melhor exemplo, mas em muitos casos também existem usuários
avançados de nível médio, como moderadores de conteúdo.
Uma tabela de funções simples foi criada no Capítulo 5 como um veículo para demonstrar relacionamentos
um-para-muitos. O Exemplo 9-1 mostra um modelo de função aprimorado com algumas adições.
class Role(db.Model):
__tablename__ = 'roles' id
= db.Column(db.Integer, primary_key=True) name =
db.Column(db.String(64), unique=True) default =
db.Column( db.Boolean, default=False, index=True)
111
Machine Translated by Google
permissions = db.Column(db.Integer)
users = db.relationship('User', backref='role', lazy='dynamic')
O campo padrão deve ser definido como True para apenas uma função e False para todas as outras.
A função marcada como padrão será aquela atribuída aos novos usuários no momento do registro.
A lista de tarefas para as quais são necessárias permissões obviamente é específica do aplicativo. Para
Flasky, a lista de tarefas é mostrada na Tabela 9-1.
Comente sobre postagens feitas por outros 0b00000010 (0x02) Comente sobre artigos escritos por outros
Moderar comentários feitos por outros 0b00001000 (0x08) Suprimir comentários ofensivos feitos por outros
Observe que um total de oito bits foi alocado para as tarefas e, até agora, apenas cinco foram usados.
Os três restantes são deixados para expansão futura.
Permissão de classe :
SEGUE = 0x01
COMENTÁRIO = 0x02
WRITE_ARTICLES = 0x04
MODERATE_COMMENTS = 0x08
ADMINISTRADOR = 0x80
A Tabela 9-2 mostra a lista de funções de usuário que serão suportadas, junto com a permissão
bits que o definem.
Anônimo 0b00000000 (0x00) Usuário que não está conectado. Acesso somente leitura ao aplicativo.
Do utilizador
0b00000111 (0x07) Permissões básicas para escrever artigos e comentários e seguir outros usuários. Isto é o
padrão para novos usuários.
Moderador 0b00001111 (0x0f) Adiciona permissão para suprimir comentários considerados ofensivos ou inapropriados.
Administrador 0b11111111 (0xff) Acesso total, que inclui permissão para alterar as funções de outros usuários.
Organizar as funções com permissões permite adicionar novas funções no futuro que usam diferentes combinações
de permissões.
Adicionar as funções ao banco de dados manualmente é demorado e sujeito a erros. Em vez disso, um método de
classe será adicionado à classe Role para esse propósito, conforme mostrado no Exemplo 9-3.
class Role(db.Model): #
...
@staticmethod
def insert_roles():
funções =
{ 'User': (Permission.FOLLOW |
Permission.COMMENT |
Permission.WRITE_ARTICLES, True),
'Moderator': (Permission.FOLLOW |
Permission. COMMENT
| Permission.WRITE_ARTICLES |
Permission.MODERATE_COMMENTS, False),
'Administrador': (0xff, False)
} para r em funções:
função = Role.query.filter_by(name=r).first() se a
função for Nenhuma:
função = Função(nome=r)
função.permissões = funções[r][0]
função.default = funções[r][1 ]
db.session.add(role)
db.session.commit()
A função insert_roles() não cria diretamente novos objetos de função. Em vez disso, ele tenta localizar funções
existentes por nome e atualizá-las. Um novo objeto de função é criado apenas para nomes de funções que ainda
não estão no banco de dados. Isso é feito para que a lista de funções possa ser atualizada no futuro, quando as
alterações precisarem ser feitas. Para adicionar uma nova função ou alterar as atribuições de permissão para uma
função, altere a matriz de funções e execute novamente a função. Observe que a função “Anonymous” não precisa
ser representada no banco de dados, pois foi projetada para representar usuários que não estão no banco de dados.
Para aplicar essas funções ao banco de dados, uma sessão de shell pode ser usada:
Atribuição de função
Quando os usuários registram uma conta no aplicativo, a função correta deve ser atribuída a eles. Para a maioria
dos usuários, o papel atribuído no momento do registro será o papel de “Usuário”, conforme
essa é a função marcada como padrão. A única exceção é feita para o administrador, que precisa receber a
função de “Administrador” desde o início. Este usuário é identificado por um endereço de e-mail armazenado
na variável de configuração FLASKY_ADMIN , portanto, assim que esse endereço de e-mail aparecer em
uma solicitação de registro, ele poderá receber a função correta. O Exemplo 9-4 mostra como isso é feito no
construtor do modelo User .
Exemplo 9-4. app/ models.py: Defina uma função padrão para os usuários
O construtor User primeiro invoca os construtores das classes base, e se depois disso o objeto não tiver um
papel definido, ele define o administrador ou funções padrão dependendo do endereço de e-mail.
Verificação de função
Para simplificar a implementação de funções e permissões, um método auxiliar pode ser adicionado ao
modelo User que verifica se uma determinada permissão está presente, conforme mostrado no Exemplo 9-5.
Exemplo 9-5. app/ models.py: avalia se um usuário tem uma determinada permissão
class AnonymousUser(AnonymousUserMixin):
def can(self, permissions): return
False
login_manager.anonymous_user = AnonymousUser
O método can() adicionado ao modelo User executa uma operação bit a bit e entre as permissões solicitadas e
as permissões da função atribuída. O método retorna True se todos os bits solicitados estiverem presentes no
papel, o que significa que o usuário deve ter permissão para executar a tarefa. A verificação de permissões de
administração é tão comum que também é implementada como um método autônomo is_administrator() .
Para consistência, uma classe AnonymousUser customizada que implementa os métodos can() e
is_administrator() é criada. Este objeto herda da classe AnonymousUserMixin do Flask-Login e é registrado
como a classe do objeto que é atribuído a current_user quando o usuário não está logado. Isso permitirá que o
aplicativo chame livremente current_user.can() e current_user.is_administrator() sem ter que verificar primeiro
se o usuário está logado.
Para os casos em que toda uma função de exibição precisa ser disponibilizada apenas para usuários com
determinadas permissões, um decorador personalizado pode ser usado. O Exemplo 9-6 mostra a implementação
de dois decoradores, um para verificações de permissão genérica e outro que verifica especificamente a
permissão do administrador.
Exemplo 9-6. app/ decorators.py: decoradores personalizados que verificam as permissões do usuário
def admin_required(f):
return permission_required(Permission.ADMINISTER)(f)
Esses decoradores são construídos com a ajuda do pacote functools da biblioteca padrão Python e retornam
um código de erro 403, o erro HTTP “Proibido”, quando o usuário atual não possui as permissões solicitadas.
No Capítulo 3, páginas de erro personalizadas foram criadas para os erros 404 e 500, então agora uma página
para o erro 403 também precisa ser adicionada.
@main.route('/admin')
@login_required
@admin_required
def for_admins_only():
return "Para administradores!"
@main.route('/moderator')
@login_required
@permission_required(Permission.MODERATE_COMMENTS)
def for_moderators_only():
return "Para moderadores de comentários!"
As permissões também podem precisar ser verificadas nos modelos, portanto, a classe Permission com todas
as constantes de bit precisa estar acessível a eles. Para evitar ter que adicionar um argumento de modelo em
cada chamada render_template() , um processador de contexto pode ser usado. Os processadores de contexto
disponibilizam variáveis globalmente para todos os modelos. Essa mudança é mostrada no Exemplo 9-7.
Exemplo 9-7. app/ main/ __init__.py: Adicionando a classe Permission ao template conÿ
texto
@main.app_context_processor
def inject_permissions(): return
dict(Permission=Permission)
As novas funções e permissões podem ser exercidas em testes de unidade. O Exemplo 9-8 mostra dois testes
simples que também servem como demonstração de uso.
classe UserModelTestCase(unittest.TestCase):
# ...
def test_roles_and_permissions(self):
Role.insert_roles() u =
User(email='[email protected]', password='cat')
self.assertTrue(u.can(Permission.WRITE_ARTICLES))
self.assertFalse(u. can(Permissão.MODERATE_COMMENTS))
def test_anonymous_user(self): u =
AnonymousUser()
self.assertFalse(u.can(Permission.FOLLOW))
Antes de passar para o próximo capítulo, é uma boa ideia recriar ou atualizar o banco de dados de
desenvolvimento para que todas as contas de usuário que foram criadas antes da existência de
funções e permissões tenham uma função atribuída.
O sistema do usuário agora está bastante completo. O próximo capítulo fará uso dele para criar
páginas de perfil de usuário.
CAPÍTULO 10
Perfis de usuário
Neste capítulo, os perfis de usuário para Flasky são implementados. Todos os sites socialmente
conscientes fornecem aos seus usuários uma página de perfil, onde é apresentado um resumo da
participação do usuário no site. Os usuários podem anunciar sua presença no site compartilhando a URL
em sua página de perfil, por isso é importante que as URLs sejam curtas e fáceis de lembrar.
Informação do Perfil
Para tornar as páginas de perfil do usuário mais interessantes, algumas informações adicionais sobre os
usuários podem ser registradas. No Exemplo 10-1, o modelo User é estendido com vários novos campos.
Os novos campos armazenam o nome real do usuário, localização, descrição escrita pelo próprio usuário,
data de registro e data da última visita. O campo about_me é atribuído ao tipo db.Text().
A diferença entre db.String e db.Text é que db.Text não precisa de um comprimento máximo.
Os dois timestamps recebem um valor padrão da hora atual. Observe que datetime.utcnow está sem () no
final. Isso ocorre porque o argumento padrão para db.Column() pode assumir uma função como valor
padrão, portanto, cada vez que um valor padrão precisa ser gerado, a função é invocada para produzi-lo.
Esse valor padrão é tudo o que é necessário para gerenciar o campo member_since .
119
Machine Translated by Google
O campo last_seen também é inicializado com a hora atual na criação, mas precisa ser atualizado toda vez que
o usuário acessa o site. Um método na classe User pode ser adicionado para executar esta atualização. Isso é
def ping(self):
self.last_seen = datetime.utcnow()
db.session.add(self)
O método ping() deve ser chamado toda vez que uma solicitação do usuário for recebida. Como o manipulador
before_app_request no esquema de autenticação é executado antes de cada solicitação, ele pode fazer isso
facilmente, conforme mostrado no Exemplo 10-3.
@auth.before_app_request
def before_request(): if
current_user.is_authenticated():
current_user.ping() if
not current_user.confirmed \ and
request.endpoint[:5] != 'auth.': return
redirect(url_for('auth. não confirmado'))
Criar uma página de perfil para cada usuário não apresenta novos desafios. O Exemplo 10-4 mostra a definição
da rota.
@main.route('/user/<username>') def
user(username): user
= User.query.filter_by(username=username).first() if user is None:
abort(404) return
Esta rota é adicionada no blueprint principal . Para um usuário chamado john, a página de perfil estará em http://
localhost:5000/ user/ john. O nome de usuário fornecido na URL é pesquisado no banco de dados e, se
encontrado, o modelo user.html é renderizado com ele como argumento. Um nome de usuário inválido enviado
para esta rota fará com que um erro 404 seja retornado. O usuário.html
template deve renderizar as informações armazenadas no objeto de usuário. Uma versão inicial desse modelo
é mostrada no Exemplo 10-5.
{% block page_content %}
<div class="page-header">
<h1>{{ user.username }}</h1> {%
if user.name or user.location %}
<p>
{% if user.name %}{{ user.name }}{% endif %} {% if
user.location %}
De <a href="http://maps.google.com/?q={{ user.location }}">
{{ user.location }} </a>
{%
endif %} </p>
{%
endif %} {% if
current_user.is_administrator() %} <p><a
href="mailto:{{ user.email }}">{{ user.email }}</a></p> {% endif %} {% if
user.about_me
%}<p>{{ user.about_me }}</p>{% endif % }
<p>
Membro desde {{ moment(user.member_since).format('L') }}.
Visto pela última vez {{ moment(user.last_seen).fromNow() }}.
</p>
</div>
{% endblock %}
• Os campos de nome e localização são renderizados dentro de um único elemento <p> . Somente quando pelo
menos um dos campos é definido é que o elemento <p> é criado.
• O campo de localização do usuário é renderizado como um link para uma consulta do Google
Maps. • Se o usuário conectado for um administrador, os endereços de e-mail serão exibidos, renderizados
como um link mailto .
Como a maioria dos usuários deseja acesso fácil à sua própria página de perfil, um link para ela pode ser adicionado
à barra de navegação. As alterações relevantes no modelo base.html são mostradas no Exemplo 10-6.
O uso de uma condicional para o link da página de perfil é necessário porque a barra de navegação também é
renderizada para usuários não autenticados, caso em que o link do perfil é ignorado.
A Figura 10-1 mostra a aparência da página de perfil no navegador. O novo link do perfil na barra de navegação
também é mostrado.
Editor de perfil
Existem dois casos de uso diferentes relacionados à edição de perfis de usuário. A mais óbvia é que os usuários
precisam ter acesso a uma página onde possam inserir informações sobre si mesmos para apresentar em suas
páginas de perfil. Um requisito menos óbvio, mas também importante, é permitir que os administradores editem o
perfil de qualquer usuário – não apenas os itens de informações pessoais, mas também outros campos no modelo
de usuário aos quais os usuários não têm acesso direto, como a função do usuário. Como os dois requisitos de
edição de perfil são substancialmente diferentes, serão criados dois formulários diferentes.
classe EditProfileForm(Form):
name = StringField(' Nome real', validators=[Comprimento(0, 64)]) location
= StringField('Localização', validators=[Comprimento(0, 64)]) about_me =
TextAreaField('Sobre mim') submit =
EnviarCampo('Enviar')
Observe que, como todos os campos deste formulário são opcionais, o validador de comprimento permite um
comprimento de zero. A definição de rota que usa este formulário é mostrada no Exemplo 10-8.
Esta função de visualização define valores iniciais para todos os campos antes de apresentar o formulário.
Para qualquer campo, isso é feito atribuindo-se o valor inicial a form.<nome do campo>.data.
Quando form.validate_on_submit() é False, os três campos neste formulário são inicializados a partir dos
campos correspondentes em current_user. Então, quando o formulário é enviado, os atributos de dados dos
campos do formulário contêm os valores atualizados, então eles são movidos de volta para os campos do
objeto do usuário e o objeto é adicionado à sessão do banco de dados.
A Figura 10-2 mostra a página Editar perfil.
Para facilitar o acesso dos usuários a essa página, um link direto pode ser adicionado à página de perfil,
conforme mostrado no Exemplo 10-9.
A condicional que inclui o link fará com que o link apareça apenas quando os usuários estiverem visualizando
seus próprios perfis.
O formulário de edição de perfil para administradores é mais complexo do que o de usuários comuns.
Além dos três campos de informações de perfil, este formulário permite que os administradores
editem o e-mail, nome de usuário, status confirmado e função de um usuário. A forma é mostrada no
Exemplo 10-10.
Exemplo 10-10. app/ main/ forms.py: Formulário de edição de perfil para administradores
classe EditProfileAdminForm(Form):
email = StringField('Email', validators=[Required(), Length(1, 64), Email()]) username
=
StringField('Username', validators=[ Required(), Length(1,
64), Regexp ('^[A-Za-z][A-Za-z0-9_.]*$', 0,
'Nomes de usuário devem ter apenas letras,
' 'números, pontos ou sublinhados')])
confirmado = BooleanField('Confirmado') role
= SelectField('Role', coerce=int) name =
StringField('Real name', validators=[Length(0, 64)]) location =
StringField('Location', validators= [Length(0, 64)]) about_me =
TextAreaField('Sobre mim') submit =
SubmitField('Enviar')
Os campos de e-mail e nome de usuário são construídos da mesma forma que nos formulários
de autenticação, mas sua validação requer algum cuidado. A condição de validação usada para
ambos os campos deve primeiro verificar se houve uma alteração no campo e somente quando
houver uma alteração deve garantir que o novo valor não duplique o de outro usuário. Quando
esses campos não são alterados, a validação deve passar. Para implementar essa lógica, o
construtor do formulário recebe o objeto do usuário como um argumento e o salva como uma
variável de membro, que é posteriormente utilizada nos métodos de validação personalizados.
Exemplo 10-11. app/ main/ views.py: rota de edição de perfil para administradores
@main.route('/edit-profile/<int:id>', methods=['GET', 'POST']) @login_required
@admin_required
def
edit_profile_admin(id): user =
User.query.get_or_404(id) form =
EditProfileAdminForm(user=user) if
form.validate_on_submit():
user.email = form.email.data
user.username = form.username.data
user.confirmed = form.confirmed.data
Esta rota tem basicamente a mesma estrutura que a mais simples para usuários regulares. Nesta função
de exibição, o usuário é fornecido por seu id, então a função de conveniência get_or_404() do Flask-
SQLAlchemy pode ser usada, sabendo que se o id for inválido, a solicitação retornará um código de erro
404.
O SelectField usado para o papel do usuário também merece ser estudado. Ao definir o valor inicial para
o campo, o role_id é atribuído a field.role.data porque a lista de tuplas definida no atributo choice usa os
identificadores numéricos para fazer referência a cada opção. Quando o formulário é enviado, o id é
extraído do atributo data do campo e usado em uma consulta para carregar o objeto role por seu id. O
argumento coerce=int usado na declaração SelectField no formulário garante que o atributo data desse
campo seja
um inteiro.
Para vincular a esta página, outro botão é adicionado na página de perfil do usuário, conforme mostrado
no Exemplo 10-12.
Exemplo 10-12. app/ templates/ user.html: link de edição de perfil para administrador
{% if current_user.is_administrator() %} <a
class="btn btn-danger"
href="{{ url_for('.edit_profile_admin', id=user.id) }}"> Editar perfil
[Admin] </a> {% endif
%}
Este botão é renderizado com um estilo Bootstrap diferente para chamar a atenção para ele. A
condicional neste caso faz com que o botão apareça nas páginas de perfil se o usuário conectado for
um administrador.
Avatares de usuário
A aparência das páginas de perfil pode ser melhorada mostrando imagens de avatar dos usuários. Nesta seção,
você aprenderá como adicionar avatares de usuários fornecidos pelo Gravatar, o principal serviço de avatar. O
Gravatar associa imagens de avatar a endereços de e-mail. Os usuários criam uma conta em http:// gravatar.com e
depois carregam suas imagens. Para gerar a URL do avatar para um determinado endereço de e-mail, seu hash
MD5 é calculado:
Os URLs do avatar são gerados anexando o hash MD5 ao URL http:// www.gravatar.com/ avatar/ ou https://
secure.gravatar.com/ avatar/. Por exemplo, você pode digitar http:// www.gravatar.com/ avatar/
d4c74594d841139328695756648b6bd6 na barra de endereço do seu navegador para obter a imagem do avatar
para o endereço de e-mail [email protected] ou uma imagem gerada padrão se esse endereço de e-mail não ter
um avatar cadastrado. A string de consulta da URL pode incluir vários argumentos que configuram as características
da imagem do avatar, listadas na Tabela 10-1.
d O gerador de imagens padrão para usuários que não possuem avatares cadastrados no serviço Gravatar.
As opções são "404" para retornar um erro 404, um URL que aponta para uma imagem padrão ou um
dos seguintes geradores de imagem: "mm", "identicon", "monsterid", "wavatar", "retro" ou "blank".
Forçar o uso de avatares padrão.
fd
O conhecimento de como construir uma URL do Gravatar pode ser adicionado ao modelo User . A implementação
é mostrada no Exemplo 10-13.
importar hashlib
da solicitação de importação do balão
# ...
def gravatar(self, size=100, default='identicon', rating='g'): if request.is_secure:
url = 'https://
secure.gravatar.com/avatar' else: url = 'http:/ /
www.gravatar.com/avatar'
hash = hashlib.md5(self.email.encode('utf-8')).hexdigest() return '{url}/
{hash}?s={size}&d={default}&r={rating}'. formatar(
url=url, hash=hash, tamanho=tamanho, padrão=padrão, classificação=avaliação)
Essa implementação seleciona a URL base padrão ou segura do Gravatar para corresponder à
segurança da solicitação do cliente. A URL do avatar é gerada a partir da URL base, o hash MD5 do
endereço de e-mail do usuário e os argumentos, todos com valores padrão.
Com esta implementação, é fácil gerar URLs de avatar no shell do Python:
O método gravatar() também pode ser invocado a partir dos templates Jinja2. O Exemplo 10-14 mostra
como um avatar de 256 pixels pode ser adicionado à página de perfil.
...
<img class="img-rounded profile-thumbnail" src="{{ user.gravatar(size=256) }}">
...
Usando uma abordagem semelhante, o modelo base adiciona uma pequena imagem em miniatura do
usuário conectado na barra de navegação. Para formatar melhor as imagens do avatar na página, são
usadas classes CSS personalizadas. Você pode encontrá-los no repositório de código-fonte em um
arquivo styles.css adicionado à pasta de arquivos estáticos do aplicativo e referenciado no modelo base.html .
A Figura 10-3 mostra a página de perfil do usuário com avatar.
A geração de avatares requer a geração de um hash MD5, que é uma operação intensiva da CPU. Se
for necessário gerar um grande número de avatares para uma página, o trabalho computacional pode
ser significativo. Como o hash MD5 para um usuário permanecerá constante, ele pode ser armazenado
em cache no modelo User . O Exemplo 10-15 mostra as mudanças no modelo User para armazenar os
hashes MD5 no banco de dados.
Exemplo 10-15. app/ models.py: Geração de URL do Gravatar com cache de hashes MD5
www.gravatar.com/avatar'
hash = self.avatar_hash ou hashlib.md5(
self.email.encode('utf-8')).hexdigest()
return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(
url=url, hash=hash, tamanho=tamanho, padrão=padrão, classificação=avaliação)
No próximo capítulo, será criado o mecanismo de blog que alimenta esse aplicativo.
CAPÍTULO 11
Postagens no blog
Este capítulo é dedicado à implementação do principal recurso do Flasky, que é permitir que os usuários leiam
e escrevam postagens de blog. Aqui você aprenderá algumas técnicas novas para reutilização de modelos,
paginação de longas listas de itens e trabalho com rich text.
class Post(db.Model):
__tablename__ = 'posts' id
= db.Column(db.Integer, primary_key=True) body =
db.Column(db.Text) timestamp
= db.Column(db.DateTime, index=True , default=datetime.utcnow) author_id =
db.Column(db.Integer, db.ForeignKey('users.id'))
Uma postagem de blog é representada por um corpo, um carimbo de data/hora e um relacionamento um-para-
muitos do modelo User . O campo do corpo é definido com o tipo db.Text para que não haja limitação de
comprimento.
O formulário que será mostrado na página principal do aplicativo permite que os usuários escrevam uma
postagem no blog. Este formulário é muito simples; ele contém apenas uma área de texto onde a postagem
do blog pode ser digitada e um botão de envio. A definição de formulário é mostrada no Exemplo 11-2.
131
Machine Translated by Google
class PostForm(Form):
body = TextAreaField("No que você está pensando?", validators=[Required()]) submit
= SubmitField('Enviar')
A função de exibição index() manipula o formulário e passa a lista de postagens antigas do blog para o modelo,
conforme mostrado no Exemplo 11-3.
Exemplo 11-3. app/ main/ views.py: rota da página inicial com uma postagem no blog
Essa função de exibição passa o formulário e a lista completa de postagens do blog para o modelo.
A lista de postagens é ordenada por seu carimbo de data/hora em ordem decrescente. O formulário de postagem do
blog é tratado da maneira usual, com a criação de uma nova instância Post quando um envio válido é recebido. A
permissão do usuário atual para escrever artigos é verificada antes de permitir a nova postagem.
Observe a maneira como o atributo author do novo objeto post é definido para a expressão
current_user._get_current_object(). A variável current_user do Flask Login, como todas as variáveis de contexto, é
implementada como um objeto de proxy local de thread. Este objeto se comporta como um objeto de usuário, mas é
realmente um invólucro fino que contém o objeto de usuário real dentro dele. O banco de dados precisa de um objeto
de usuário real, que é obtido chamando _get_current_object().
O formulário é renderizado abaixo da saudação no modelo index.html , seguido pelas postagens do blog. A lista de
postagens de blog é uma primeira tentativa de criar uma linha do tempo de postagem de blog, com todas as
postagens de blog no banco de dados listadas em ordem cronológica, da mais recente para a mais antiga. As
alterações no modelo são mostradas no Exemplo 11-4.
Exemplo 11-4. app/ templates/ index.html: modelo de página inicial com postagens de blog
{% extends "base.html" %} {%
import "bootstrap/wtf.html" como wtf %}
...
<div>
{% if current_user.can(Permission.WRITE_ARTICLES) %}
{{ wtf.quick_form(form) }} {%
endif %}
</div>
<ul class="posts"> {%
para postagem em postagens
%} <li class="post">
<div class="profile-thumbnail">
<a href="{{ url_for('.user', username=post.author.username) }}">
<img class="img-rounded profile-thumbnail"
src="{{ post.author.gravatar(size=40) }}">
</a>
</div>
<div class="post-date">{{ moment(post.timestamp).fromNow() }}</div> <div
class="post-author">
<a href="{{ url_for('.user', username=post.author.username) }}">
{{ post.author.username }} </a>
</
div>
<div class="post- body">{{ post.body }}</div> </li> {%
endfor
%} </ul>
...
Observe que o método User.can() é usado para ignorar o formulário de postagem do blog para usuários que
não têm a permissão WRITE_ARTICLES em sua função. A lista de postagens do blog é implementada como
uma lista não ordenada em HTML, com classes CSS proporcionando uma formatação mais agradável. Um
pequeno avatar do autor é renderizado no lado esquerdo, e tanto o avatar quanto o nome de usuário do autor
são renderizados como links para a página de perfil do usuário. Os estilos CSS usados são armazenados em
um arquivo styles.css na pasta estática do aplicativo . Você pode revisar este arquivo no repositório do GitHub.
A Figura 11-1 mostra a página inicial com o formulário de envio e a lista de postagens do blog.
Figura 11-1. Página inicial com formulário de envio de blog e lista de postagens de blog
A página de perfil do usuário pode ser aprimorada mostrando uma lista de postagens de blog de autoria do
usuário. O Exemplo 11-5 mostra as mudanças na função view para obter a lista de postagens.
Exemplo 11-5. app/ main/ views.py: rota da página de perfil com postagens de blog
@main.route('/user/<username>') def
user(username): user
= User.query.filter_by(username=username).first() if user is None:
abort(404) posts =
A lista de postagens de blog para um usuário é obtida do relacionamento User.posts , que é um objeto de
consulta, portanto, filtros como order_by() também podem ser usados nela.
O modelo user.html requer a árvore HTML <ul> que renderiza uma lista de postagens de blog como aquela em
index.html. Ter que manter duas cópias idênticas de um pedaço de HTML não é o ideal, então para casos
como este, a diretiva include() de Jinja2 é muito útil. O modelo user.html inclui a lista de um arquivo externo,
conforme mostrado no Exemplo 11-6.
Exemplo 11-6. app/ templates/ user.html: modelo de página de perfil com postagens de blog
...
<h3>Postagens de {{ user.username }}</h3>
{% include '_posts.html' %}
...
Para completar esta reorganização, a árvore <ul> de index.html é movida para o novo template
_posts.html, e substituída por outra diretiva include() . Observe que o uso de um prefixo de sublinhado
no nome do modelo _posts.html não é um requisito; isso é apenas uma convenção para distinguir
modelos autônomos e parciais.
À medida que o site cresce e o número de postagens do blog aumenta, torna-se lento e impraticável
mostrar a lista completa de postagens nas páginas inicial e de perfil. Páginas grandes levam mais
tempo para serem geradas, baixadas e renderizadas no navegador da Web, portanto, a qualidade
da experiência do usuário diminui à medida que as páginas ficam maiores. A solução é paginar os
dados e renderizá-los em blocos.
blog Para poder trabalhar com várias páginas de postagens de blog, é necessário ter um banco de
dados de teste com um grande volume de dados. Adicionar manualmente novas entradas de banco de
dados é demorado e tedioso; uma solução automatizada é mais apropriada. Existem vários pacotes
Python que podem ser usados para gerar informações falsas. Um bastante completo é o ForgeryPy,
que é instalado com o pip:
O pacote ForgeryPy não é, estritamente falando, uma dependência do aplicativo, pois é necessário
apenas durante o desenvolvimento. Para separar as dependências de produção das dependências de
desenvolvimento, o arquivo requirements.txt pode ser substituído por uma pasta de requisitos que
armazena diferentes conjuntos de dependências. Dentro desta nova pasta, um arquivo dev.txt pode
listar as dependências necessárias para o desenvolvimento e um arquivo prod.txt pode listar as
dependências necessárias na produção. Como há um grande número de dependências que estarão
em ambas as listas, um arquivo common.txt é adicionado para elas e, em seguida, as listas dev.txt e
prod.txt usam o prefixo -r para incluí-lo. O Exemplo 11-7 mostra o arquivo dev.txt .
O Exemplo 11-8 mostra métodos de classe adicionados aos modelos User e Post que podem gerar
dados falsos.
seed()
para i in range(count): u =
User(email=forgery_py.internet.email_address(),
nome de usuário=forgery_py.internet.user_name(Verdadeiro),
senha=forgery_py.lorem_ipsum.word(),
confirmado=Verdadeiro,
nome=forgery_py.name.full_name(),
localização=forgery_py.address.city(),
about_me=forgery_py.lorem_ipsum .sentence(),
classe Post(db.Model):
# ...
@staticmethod
def generate_fake(count=100): da
semente de importação aleatória ,
randint import forgery_py
seed()
user_count = User.query.count() for i in
range(count):
u = User.query.offset(randint(0, user_count - 1)).first() p =
Post(body=forgery_py.lorem_ipsum.sentences(randint(1, 3)),
timestamp=forgery_py.date.date(True ), autor=u)
db.session.add(p)
db.session.commit()
Os atributos desses objetos falsos são gerados com geradores de informações aleatórias ForgeryPy,
que podem gerar nomes, e-mails, frases e muitos outros atributos de aparência real.
Os endereços de e-mail e nomes de usuário dos usuários devem ser únicos, mas como o ForgeryPy
os gera de maneira completamente aleatória, existe o risco de haver duplicatas. Nesse evento
improvável, a confirmação da sessão do banco de dados lançará uma exceção IntegrityError .
Essa exceção é tratada revertendo a sessão antes de continuar. As iterações de loop que produzem
uma duplicata não gravarão um usuário no banco de dados, portanto, o número total de usuários
falsos adicionados pode ser menor que o número solicitado.
A geração de postagem aleatória deve atribuir um usuário aleatório a cada postagem. Para isso, o
filtro de consulta offset() é usado. Este filtro descarta o número de resultados fornecidos como
argumento. Ao definir um deslocamento aleatório e, em seguida, chamar first(), um usuário aleatório
diferente é obtido a cada vez.
Os novos métodos facilitam a criação de um grande número de usuários e postagens falsos a partir
do shell do Python:
Se você executar o aplicativo agora, verá uma longa lista de postagens de blog aleatórias na página
inicial.
O Exemplo 11-9 mostra as mudanças na rota da página inicial para suportar a paginação.
O número da página a renderizar é obtido da query string da requisição, que está disponível
como request.args. Quando uma página explícita não é fornecida, uma página padrão de 1 (a primeira página)
é usado. O argumento type=int garante que, se o argumento não puder ser convertido em
um número inteiro, o valor padrão é retornado.
Para carregar uma única página de registros, a chamada para all() é substituída por Flask-SQLAlchemy's
paginar(). O método paginate() leva o número da página como o primeiro e único
argumento requerido. Um argumento per_page opcional pode ser dado para indicar o tamanho de
cada página, em número de itens. Se este argumento não for especificado, o padrão é 20 itens
por página. Outro argumento opcional chamado error_out pode ser definido como True (o padrão)
para emitir um erro de código 404 quando uma página fora do intervalo válido é solicitada. Se error_out
for False, as páginas fora do intervalo válido serão retornadas com uma lista vazia de itens. Para
tornar os tamanhos de página configuráveis, o valor do argumento per_page é lido de um
variável de configuração específica do aplicativo chamada FLASKY_POSTS_PER_PAGE.
Com essas alterações, a lista de postagens do blog na página inicial mostrará um número limitado de
Unid. Para ver a segunda página de postagens, adicione uma string de consulta ?page=2 ao URL no
barra de endereço do navegador.
O valor de retorno de paginate() é um objeto da classe Pagination, uma classe definida por Flask SQLAlchemy.
Este objeto contém várias propriedades que são úteis para gerar páginas
links em um modelo, então ele é passado para o modelo como um argumento. Um resumo do
atributos do objeto de paginação são mostrados na Tabela 11-1.
Atributo Descrição
iter_pages(left_edge=2, Um iterador que retorna a sequência de números de página para exibição em um widget
right_edge=2) direita da página atual e páginas right_edge no lado direito. Por exemplo,
para a página 50 de 100, este iterador configurado com valores padrão retornará as
seguintes páginas: 1, 2, None, 48, 49, 50, 51, 52, 53, 54, 55, None, 99, 100. A None
Armado com este objeto poderoso e as classes CSS de paginação do Bootstrap, é muito fácil
construir um rodapé de paginação no modelo. A implementação mostrada no Exemplo 11-10 é
feita como uma macro Jinja2 reutilizável.
</ul>
{% endmacro %}
A macro cria um elemento de paginação Bootstrap, que é uma lista não ordenada estilizada. Ele define os
seguintes links de página dentro dele:
• Um link de “página anterior”. Este link obtém a classe desabilitada se a página atual for a
primeira página.
• Links para todas as páginas retornadas pelo iterador iter_pages() do objeto de paginação .
Essas páginas são renderizadas como links com um número de página explícito, dado como um
argumento para url_for(). A página atualmente exibida é destacada usando a classe CSS ativa . As
lacunas na sequência de páginas são renderizadas com o caractere de reticências. • Um link de
“próxima página”. Este link aparecerá desabilitado se a página atual for a última página.
As macros Jinja2 sempre recebem argumentos de palavra-chave sem precisar incluir **kwargs na lista de
argumentos. A macro de paginação passa todos os argumentos de palavra-chave que recebe para a
chamada url_for() que gera os links de paginação. Essa abordagem pode ser usada com rotas como a página
de perfil que possui uma parte dinâmica.
A macro pagination_widget pode ser adicionada abaixo do modelo _posts.html incluído por index.html e
user.html. O Exemplo 11-11 mostra como ele é usado na página inicial do aplicativo.
Exemplo 11-11. app/ templates/ index.html: Rodapé de paginação para listas de postagens de blog
{% extends "base.html" %} {%
import "bootstrap/wtf.html" como wtf %} {% import
"_macros.html" como macros %}
...
{% include '_posts.html' %} <div
class="pagination">
{{ macros.pagination_widget(pagination, '.index') }} </div> {%
endif
%}
• Flask-PageDown, um wrapper PageDown para Flask que integra PageDown com formulários Flask-WTF.
• Markdown, um conversor de Markdown para HTML do lado do servidor implementado em Python. • Bleach, um
Flask-PageDown define uma classe PageDownField que tem a mesma interface que o TextAreaField de WTForms.
Antes que este campo possa ser usado, a extensão precisa ser inicializada conforme mostrado no Exemplo 11-12.
pagedown = PageDown()
# ...
def create_app(config_name):
# ...
pagedown.init_app(aplicativo)
# ...
Para converter o controle de área de texto na página inicial em um editor de rich text Markdown, o campo do
corpo do PostForm deve ser alterado para um PageDownField , conforme mostrado no Exemplo 11-13.
Exemplo 11-13. app/ main/ forms.py: formulário de postagem habilitado para Markdown
class PostForm(Form):
body = PageDownField("No que você está pensando?", validators=[Required()])
submit = SubmitField('Enviar')
A pré-visualização do Markdown é gerada com a ajuda das bibliotecas PageDown, pelo que estas devem ser
adicionadas ao template. Flask-PageDown simplifica essa tarefa fornecendo uma macro de modelo que inclui os
arquivos necessários de um CDN, conforme mostrado no Exemplo 11-14.
{% scripts de bloco %}
{{ super() }}
{{ pagedown.include_pagedown() }} {%
endblock %}
Com essas alterações, o texto formatado em Markdown digitado no campo da área de texto será renderizado
imediatamente como HTML na área de visualização abaixo. A Figura 11-3 mostra o formulário de envio de blog
com rich text.
o formulário é enviado, apenas o texto Markdown bruto é enviado com a solicitação POST ; a
visualização HTML que foi mostrada na página é descartada. Enviar a visualização HTML gerada com
o formulário pode ser considerado um risco de segurança, pois seria bastante fácil para um invasor
construir sequências HTML que não correspondem à fonte do Markdown e enviá-las. Para evitar
qualquer risco, apenas o texto fonte do Markdown é enviado e, uma vez no servidor, ele é convertido
novamente para HTML usando o Markdown, um conversor Markdown para HTML do Python. O HTML
resultante será limpo com Bleach para garantir que apenas uma pequena lista de tags HTML permitidas
seja usada.
A conversão das postagens do blog Markdown para HTML pode ser emitida no modelo _posts.html ,
mas isso é ineficiente, pois as postagens terão que ser convertidas toda vez que forem renderizadas
em uma página. Para evitar essa repetição, a conversão pode ser feita uma vez quando o post do blog
for criado. O código HTML para a postagem de blog renderizada é armazenado em cache em um novo
campo adicionado ao modelo de postagem que o modelo pode acessar diretamente. A fonte original do
Markdown também é mantida no banco de dados caso a postagem precise ser editada. O Exemplo
11-15 mostra as mudanças no modelo Post .
Exemplo 11-15. app/ models/ post.py: manipulação de texto Markdown no modelo Post
classe Post(db.Model): #
...
body_html = db.Column(db.Text)
...
# @staticmethod
def on_changed_body(target, value, oldvalue, initiator):
permitido_tags = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'código', 'em', 'i', 'li', ' ol',
'pré', 'forte' , 'ul', 'h1', 'h2', 'h3', 'p']
A conversão real é feita em três etapas. Primeiro, a função markdown() faz uma conversão inicial
para HTML. O resultado é passado para clean(), juntamente com a lista de tags HTML aprovadas.
A função clean() remove todas as tags que não estão na lista branca. A conversão final é feita
com linkify(), outra função fornecida pelo Bleach que converte qualquer URL escrita em texto
simples em links <a> apropriados. Esta última etapa é necessária porque a geração automática
de links não está oficialmente na especificação Markdown. PageDown o suporta como uma
extensão, então linkify() é usado no servidor para corresponder.
Exemplo 11-16. app/ templates/ _posts.html: Use a versão HTML dos corpos dos posts no template
...
<div class="post-body">
{% if post.body_html %}
{{ post.body_html | safe }} {% else
%}
{{ post.body }} {%
endif %} </
div>
...
O | sufixo seguro ao renderizar o corpo HTML está lá para dizer a Jinja2 para não escapar dos
elementos HTML. Jinja2 escapa de todas as variáveis de modelo por padrão como uma medida
de segurança. O HTML gerado pelo Markdown foi gerado no servidor, portanto, é seguro renderizá-lo.
Os usuários podem querer compartilhar links para postagens de blog específicas com amigos nas redes
sociais. Para isso, a cada postagem será atribuída uma página com uma URL exclusiva que a referencia.
A rota e a função de visualização que suportam links permanentes são mostradas no Exemplo 11-17.
@main.route('/post/<int:id>') def
post(id): post =
Post.query.get_or_404(id) return
render_template('post.html', posts=[post])
As URLs que serão atribuídas às postagens do blog são construídas com o campo de id exclusivo
atribuído quando a postagem é inserida no banco de dados.
Para alguns tipos de aplicativos, pode ser preferível criar links permanentes
que usam URLs legíveis em vez de IDs numéricos. Uma alternativa para
IDs numéricos é atribuir a cada postagem do blog um slug, que é uma
string única relacionada à postagem.
Observe que o modelo post.html recebe uma lista apenas com o post a ser renderizado. O envio de uma
lista é necessário para que o modelo _posts.html referenciado por index.html e user.html também possa
ser utilizado nesta página.
Os links permanentes são adicionados na parte inferior de cada postagem no modelo genérico
_posts.html , conforme mostrado no Exemplo 11-18.
<ul class="posts"> {%
para postagem em postagens
%} <li class="post">
...
<div class="post-content">
...
<div class="post-footer">
<a href="{{ url_for('.post', id=post.id) }}">
<span class="label label-default">Link permanente </a>
</div>
</div>
</li>
{% endfor %}
</ul>
O novo modelo post.html que renderiza a página de link permanente é mostrado no Exemplo 11-19.
Inclui o modelo de exemplo.
{% estende "base.html" %}
{% block page_content %}
{% include '_posts.html' %} {%
endblock %}
O último recurso relacionado às postagens do blog é um editor de postagens que permite aos usuários
editar suas próprias postagens. O editor de postagem do blog viverá em uma página independente.
No topo da página, a versão atual da postagem será mostrada para referência, seguida por um editor
Markdown onde o Markdown de origem pode ser modificado. O editor será baseado no Flask
PageDown, portanto, uma visualização da versão editada da postagem do blog será exibida na parte
inferior da página. O modelo edit_post.html é mostrado no Exemplo 11-20.
{% extends "base.html" %} {%
import "bootstrap/wtf.html" como wtf %}
{% block page_content %}
<div class="page-header">
<h1>Editar postagem</
h1> </
div>
<div> {{ wtf.quick_form(form) }}
</div>
{% endblock % }
{% scripts de bloco %}
{{ super() }}
{{ pagedown.include_pagedown() }} {%
endblock %}
Esta função de exibição é codificada para permitir que apenas o autor de uma postagem de blog a edite, exceto
para administradores, que podem editar postagens de todos os usuários. Se um usuário tentar editar uma
postagem de outro usuário, a função de exibição responderá com um código 403. A classe de formulário
PostFormweb usada aqui é a mesma usada na página inicial.
Para completar o recurso, um link para o editor de postagem do blog pode ser adicionado abaixo de cada
postagem do blog, ao lado do link permanente, conforme mostrado no Exemplo 11-22.
<ul class="posts"> {%
para postagem em postagens
%} <li class="post">
...
<div class="post-content">
...
<div class="post-footer">
...
{% if current_user == post.author %} <a
href="{{ url_for('.edit', id=post.id) }}">
<span class="label label-primary">Editar </a> {% elif
current_user.is_administrator() %} <a
href="{{ url_for('.edit', id=post.id) }}">
<span class="label label-danger">Editar [Admin] </a> {% endif
%}
</div>
</div>
</li>
{% endfor %}
</ul>
Essa alteração adiciona um link "Editar" a qualquer postagem de blog de autoria do usuário atual.
Para administradores, o link é adicionado a todas as postagens. O link do administrador tem um estilo
diferente como uma indicação visual de que este é um recurso de administração. A Figura 11-4 mostra
a aparência dos links Edit e Permalink no navegador da web.
CAPÍTULO 12
Seguidores
Aplicativos da Web socialmente conscientes permitem que os usuários se conectem com outros usuários.
Os aplicativos chamam esses relacionamentos de seguidores, amigos, contatos, conexões ou camaradas,
mas o recurso é o mesmo independentemente do nome e, em todos os casos, envolve o acompanhamento
de links direcionais entre pares de usuários e o uso desses links no banco de dados consultas.
Neste capítulo, você aprenderá como implementar um recurso de seguidor para Flasky. Os usuários
poderão “seguir” outros usuários e optar por filtrar a lista de postagens do blog na página inicial para incluir
apenas as dos usuários que seguem.
Conforme discutimos no Capítulo 5, bancos de dados estabelecem links entre registros usando
relacionamentos. O relacionamento um-para-muitos é o tipo mais comum de relacionamento, no qual um
registro é vinculado a uma lista de registros relacionados. Para implementar esse tipo de relacionamento,
os elementos do lado “muitos” possuem uma chave estrangeira que aponta para o elemento vinculado do
lado “um”. O aplicativo de exemplo em seu estado atual inclui dois relacionamentos um-para-muitos: um
que vincula funções de usuário a listas de usuários e outro que vincula usuários às postagens de blog que
eles criaram.
A maioria dos outros tipos de relacionamento pode ser derivada do tipo um-para-muitos. O relacionamento
muitos-para-um é um-para-muitos visto do ponto de vista do lado “muitos”. O tipo de relacionamento um-
para-um é uma simplificação do um-para-muitos, onde o lado “muitos” é restrito a ter no máximo um
elemento. O único tipo de relacionamento que não pode ser implementado como uma simples variação do
modelo um-para-muitos é o muitos-para-muitos, que possui listas de elementos em ambos os lados. Essa
relação é descrita em detalhes na seção a seguir.
149
Machine Translated by Google
Relacionamentos muitos-para-muitos
Os relacionamentos um-para-muitos, muitos-para-um e um-para-um têm pelo menos um lado com uma
única entidade, portanto, os links entre os registros relacionados são implementados com chaves
estrangeiras apontando para aquele único elemento. Mas como você implementa um relacionamento em
que ambos os lados são “muitos” lados?
A solução é adicionar uma terceira tabela ao banco de dados, chamada tabela de associação. Agora, o
relacionamento muitos-para-muitos pode ser decomposto em dois relacionamentos um-para-muitos de cada uma
das duas tabelas originais para a tabela de associação. A Figura 12-1 mostra como o relacionamento muitos-para-
muitos entre alunos e classes é representado.
A tabela de associação neste exemplo é chamada de registros. Cada linha desta tabela representa uma matrícula
individual de um aluno em uma turma.
Consultar um relacionamento muitos-para-muitos é um processo de duas etapas. Para obter a lista de aulas que
um aluno está fazendo, você começa na relação um-para-muitos entre alunos e matrículas e obtém a lista de
matrículas do aluno desejado. Em seguida, o relacionamento um-para-muitos entre classes e registros é percorrido
na direção muitos-para-um para obter todas as classes associadas aos registros recuperados para o aluno. Da
mesma forma, para encontrar todos os alunos de uma turma, você começa na turma e obtém uma lista de
matrículas e, em seguida, obtém os alunos vinculados a essas matrículas.
Atravessar dois relacionamentos para obter resultados de consulta parece difícil, mas para um relacionamento
simples como o do exemplo anterior, SQLAlchemy faz a maior parte do trabalho.
A seguir está o código que representa o relacionamento muitos-para-muitos na Figura 12-1:
registros = db.Table('registrations',
db.Column('student_id', db.Integer, db.ForeignKey('students.id')),
db.Column('class_id', db.Integer, db.ForeignKey( 'classes.id'))
)
classe Aluno(db.Modelo):
id = db.Column(db.Integer, primary_key=True) name
= db.Column(db.String) classes
= db.relationship('Class',
secundário=registros,
backref=db.backref('alunos', preguiçoso= 'dinâmico'),
preguiçoso='dinâmico')
class Class(db.Model): id
= db.Column(db.Integer, primary_key = True) name =
db.Column(db.String)
O relacionamento de classes usa semântica de lista, o que torna extremamente fácil trabalhar
com relacionamentos muitos-para-muitos configurados dessa maneira. Dado um aluno s e uma
turma c, o código que cadastra o aluno na turma é:
>>> s.classes.append(c)
>>> db.session.add(s)
As consultas que listam as turmas em que o aluno s está matriculado e a lista de alunos
matriculados na turma c também são muito simples:
Se posteriormente o aluno decidir abandonar a classe c, você poderá atualizar o banco de dados da seguinte maneira:
>>> s.classes.remove(c)
Relacionamentos autorreferenciais Um
relacionamento muitos-para-muitos pode ser usado para modelar usuários seguindo outros
usuários, mas há um problema. No exemplo de alunos e turmas, havia duas entidades claramente
definidas ligadas entre si pela tabela de associação. No entanto, para representar usuários
seguindo outros usuários, são apenas usuários - não há uma segunda entidade.
Uma relação em que ambos os lados pertencem à mesma tabela é chamada de auto-referencial.
Neste caso, as entidades do lado esquerdo do relacionamento são os usuários, que
podem ser chamados de “seguidores”. As entidades do lado direito também são usuários, mas são os
usuários “seguidos”. Conceitualmente, os relacionamentos autorreferenciais não são diferentes dos
relacionamentos regulares, mas são mais difíceis de se pensar. A Figura 12-2 mostra um diagrama de
banco de dados para um relacionamento autorreferencial que representa usuários seguindo outros usuários.
A tabela de associação neste caso é chamada de segue. Cada linha nesta tabela representa um usuário
seguindo outro usuário. A relação um-para-muitos ilustrada no lado esquerdo associa os usuários à lista
de linhas “follows” na qual eles são os seguidores. A relação um-para-muitos ilustrada no lado direito
associa os usuários à lista de linhas “follows” na qual eles são o usuário seguido.
Para poder trabalhar com dados personalizados no relacionamento, a tabela de associação deve ser
promovida a um modelo adequado que o aplicativo possa acessar. O Exemplo 12-1 mostra a nova
tabela de associação, representada pelo modelo Follow .
Exemplo 12-1. app/ models/ user.py: A seguinte tabela de associação como um modelo
class Follow(db.Model):
__tablename__ = 'follows'
follower_id = db.Column(db.Integer, db.ForeignKey('users.id'),
primary_key=True)
seguido_id = db.Column(db.Integer, db.ForeignKey('users.id'),
primary_key=Verdadeiro)
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
SQLAlchemy não pode usar a tabela de associação de forma transparente porque isso não dará ao
aplicativo acesso aos campos personalizados nela. Em vez disso, o relacionamento muitos-para-muitos
deve ser decomposto em dois relacionamentos básicos um-para-muitos para os lados esquerdo e
direito, e estes devem ser definidos como relacionamentos padrão. Isso é mostrado no Exemplo 12-2.
Exemplo 12-2. app/ models/ user.py: Um relacionamento muitos-para-muitos implementado como dois
relacionamentos um-para-muitos
O argumento lento para as referências anteriores é especificado como associado. Esse modo lento faz
com que o objeto relacionado seja carregado imediatamente da consulta de junção. Por exemplo, se
um usuário está seguindo 100 outros usuários, chamar user.followed.all() retornará uma lista de 100
instâncias Follow , onde cada uma tem as propriedades de referência de seguidor e seguido de volta
definidas para os respectivos usuários. O modo lazy='joined' permite que tudo isso aconteça a partir de
uma única consulta ao banco de dados. Se lazy for definido como o valor padrão de select, o seguidor
e os usuários seguidos serão carregados lentamente quando forem acessados pela primeira vez e
cada atributo exigirá uma consulta individual, o que significa que obter a lista completa de usuários
seguidos exigiria 100 consultas adicionais ao banco de dados .
O argumento cascade configura como as ações executadas em um objeto pai se propagam para
objetos relacionados. Um exemplo de opção em cascata é a regra que diz que quando um objeto é
adicionado à sessão do banco de dados, todos os objetos associados a ele por meio de
relacionamentos também devem ser automaticamente adicionados à sessão. As opções de cascata
padrão são apropriadas para a maioria das situações, mas há um caso em que as opções de cascata
padrão não funcionam bem para esse relacionamento muitos-para-muitos. O comportamento em
cascata padrão quando um objeto é excluído é definir a chave estrangeira em qualquer objeto
relacionado que se vincule a ela para um valor nulo. Mas para uma tabela de associação, o
comportamento correto é excluir as entradas que apontam para um registro que foi excluído, pois
isso destrói efetivamente o link. Isso é o que a opção delete-orphan cascade faz.
O aplicativo agora precisa trabalhar com os dois relacionamentos um-para-muitos para implementar
a funcionalidade muitos-para-muitos. Como essas são operações que precisarão ser repetidas
frequentemente, é uma boa ideia criar métodos auxiliares no modelo User para todas as operações
possíveis. Os quatro novos métodos que controlam essa relação são mostrados no Exemplo 12-3.
db.session.delete(f)
return
self.followers.filter_by( follower_id=user.id).first() não é nenhum
O método follow() insere manualmente uma instância Follow na tabela de associação que vincula um seguidor
a um usuário seguido, dando ao aplicativo a oportunidade de definir o campo personalizado. Os dois usuários
que estão se conectando são atribuídos manualmente à nova instância Follow em seu construtor e, em seguida,
o objeto é adicionado à sessão do banco de dados como de costume.
Observe que não há necessidade de definir manualmente o campo timestamp porque ele foi definido com um
valor padrão que define a data e hora atuais. O método unfollow() usa o relacionamento Follow para localizar a
instância Follow que vincula o usuário ao usuário seguido que precisa ser desconectado. Para destruir o link
entre os dois usuários, o objeto Follow é simplesmente deletado. Os métodos is_following() e is_followed_by()
pesquisam os relacionamentos um-para-muitos do lado esquerdo e direito, respectivamente, para o usuário
fornecido e retornam True se o usuário for encontrado.
A parte do banco de dados do recurso agora está concluída. Você pode encontrar um teste de unidade que
exercita o relacionamento com o banco de dados no repositório de código-fonte no GitHub.
Exemplo 12-4. app/ templates/ user.html: Melhorias do seguidor no cabeçalho do perfil do usuário
Há quatro novos endpoints definidos nessas alterações de modelo. A rota /follow/ <nome
do usuário> é invocada quando um usuário clica no botão “Seguir” na página de perfil de
outro usuário. A implementação é mostrada no Exemplo 12-5.
Essa função de exibição carrega o usuário solicitado, verifica se ele é válido e se ainda não foi
seguido pelo usuário conectado e, em seguida, chama a função auxiliar follow() no modelo User
para estabelecer o link. A rota /unfollow/ <username> é implementada de maneira semelhante.
Essa função carrega e valida o usuário solicitado e, em seguida, pagina seu relacionamento de
seguidores usando as mesmas técnicas aprendidas no Capítulo 11. Como a consulta de seguidores
retorna instâncias Follow , a lista é convertida em outra lista que possui campos de usuário e
carimbo de data/hora em cada entrada para que renderização é mais simples.
O modelo que renderiza a lista de seguidores pode ser escrito genericamente para que possa ser
usado para listas de seguidores e usuários seguidos. O modelo recebe o usuário, um título para a
página, o endpoint a ser usado nos links de paginação, o objeto de paginação e a lista de resultados.
O endpoint Follow_by é quase idêntico. A única diferença é que a lista de usuários é obtida do
relacionamento user.followed . Os argumentos do modelo também são ajustados de acordo.
O modelo seguidores.html é implementado com uma tabela de duas colunas que mostra nomes de
usuário e seus avatares à esquerda e timestamps Flask-Moment à direita. Você pode consultar o
repositório de código-fonte no GitHub para estudar a implementação em detalhes.
A página inicial do aplicativo atualmente mostra todas as postagens no banco de dados em ordem cronológica decrescente. Com o
recurso de seguidores agora completo, seria uma boa adição dar aos usuários a opção de visualizar postagens de blog apenas dos
A maneira óbvia de carregar todas as postagens de autoria de usuários seguidos é primeiro obter a lista desses usuários e, em
seguida, obter as postagens de cada um e classificá-las em uma única lista. É claro que essa abordagem não escala bem; o esforço
para obter essa lista combinada aumentará à medida que o banco de dados cresce e operações como paginação não podem ser
feitas com eficiência. A chave para obter os posts do blog com bom desempenho é fazê-lo com uma única consulta.
A operação de banco de dados que pode fazer isso é chamada de junção. Uma operação de junção pega duas ou mais tabelas e
localiza todas as combinações de linhas que satisfazem uma determinada condição. As linhas combinadas resultantes são inseridas
id de usuário
1 joão
2 susana
3 david
A Tabela 12-2 mostra a tabela de postagens correspondentes , com algumas postagens de blog.
id autor_id corpo
12 Postagem no blog por susan
Finalmente, a Tabela 12-3 mostra quem está seguindo quem. Nesta tabela, você pode ver que john está seguindo david, susan está
seguidor_id seguido_id
1 3
2 1
2 3
Para obter a lista de postagens seguidas pelo usuário susan, as tabelas de postagens e seguidores devem
ser combinado. Primeiro a tabela a seguir é filtrada para manter apenas as linhas que possuem susan como
o seguidor, que neste exemplo são as duas últimas linhas. Em seguida, uma tabela de junção temporária
é criado a partir de todas as combinações possíveis de linhas das postagens e filtrado
segue tabelas em que o author_id do post é o mesmo que o Follow_id de
a seguir, selecionando efetivamente todas as postagens que aparecem na lista de usuários que susan está seguindo.
A Tabela 12-4 mostra o resultado da operação de junção. As colunas que foram usadas para executar
a junção são marcadas com * nesta tabela.
Esta tabela contém exatamente a lista de postagens de blog de autoria de usuários que susan está seguindo.
A consulta Flask-SQLAlchemy que literalmente executa a operação de junção conforme descrito é
bastante complexo:
return db.session.query(Post).select_from(Follow).\
filter_by(follower_id=self.id).\
join(Post, Follow.followed_id == Post.author_id)
Todas as consultas que você viu até agora começam no atributo de consulta do modelo que
é consultado. Esse formato não funciona bem para esta consulta, porque a consulta precisa
retorna linhas de postagens , mas a primeira operação que precisa ser feita é aplicar um filtro ao
segue tabela. Portanto, uma forma mais básica da consulta é usada. Para entender completamente
esta consulta, cada parte deve ser analisada individualmente:
• db.session.query(Post) especifica que esta será uma consulta que retornará Post
objetos.
Emitindo a operação de junção primeiro, a consulta pode ser iniciada a partir de Post.query, então agora os
únicos dois filtros que precisam ser aplicados são join() e filter(). Mas isso é o mesmo? Pode parecer que fazer
a junção primeiro e depois a filtragem daria mais trabalho, mas na realidade essas duas consultas são
equivalentes. O SQLAlchemy primeiro coleta todos os filtros e depois gera a consulta da maneira mais
eficiente. As instruções SQL nativas para essas duas consultas são idênticas. A versão final dessa consulta é
adicionada ao Postmodel, conforme mostrado no Exemplo 12-7.
classe User(db.Model):
# ...
@property
def seguido_posts(auto):
return Post.query.join(Follow, Follow.followed_id == Post.author_id)\
.filter(Follow.follower_id == self.id)
Observe que o método following_posts() é definido como uma propriedade para que não precise do (). Dessa
forma, todos os relacionamentos têm uma sintaxe consistente.
As junções são extremamente difíceis de entender; você pode precisar experimentar o código de exemplo em
um shell antes que tudo seja assimilado.
# ...
show_followed = Falso
if current_user.is_authenticated():
show_followed = bool(request.cookies.get('show_followed', '')) if show_followed:
query =
current_user.followed_posts else:
query = Post.query
paginação = query.order_by(Post.timestamp.desc()).paginate(
página, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],
error_out=False)
posts = pagination.items return
render_template('index.html', form=form, posts=posts,
show_followed=show_followed, paginação=paginação)
@main.route('/all')
@login_required
def show_all():
resp = make_response(redirect(url_for('.index')))
resp.set_cookie('show_followed', '', max_age=30*24*60 *60) retorno resp
@main.route('/followed')
@login_required
def show_followed(): resp
= make_response(redirect(url_for('.index')))
resp.set_cookie('show_followed', '1', max_age=30*24* 60*60) retorno resp
Os links para essas rotas são adicionados ao modelo de página inicial. Quando eles são invocados, o cookie
show_followed é definido com o valor adequado e um redirecionamento de volta à página inicial é emitido.
Os cookies podem ser definidos apenas em um objeto de resposta, portanto, essas rotas precisam criar um objeto
de resposta por meio de make_response() em vez de permitir que o Flask faça isso.
A função set_cookie() usa o nome do cookie e o valor como os dois primeiros argumentos. O argumento opcional
max_age define o número de segundos até que o cookie
expira. A não inclusão deste argumento faz com que o cookie expire quando a janela do navegador for
fechada. Nesse caso, é definida uma idade de 30 dias para que a configuração seja lembrada mesmo que
o usuário não retorne ao aplicativo por vários dias.
As alterações no modelo adicionam duas guias de navegação na parte superior da página que invocam
as rotas /all ou /followed para definir as configurações corretas na sessão. Você pode inspecionar as
alterações do modelo em detalhes no repositório de código-fonte no GitHub. A Figura 12-4 mostra a
aparência da página inicial com essas alterações.
Se você tentar o aplicativo neste ponto e mudar para a lista de postagens seguidas, notará que suas
próprias postagens não aparecem na lista. Claro que isso é correto, porque os usuários não são seguidores
de si mesmos.
Mesmo que as consultas estejam funcionando conforme o planejado, a maioria dos usuários espera ver
suas próprias postagens quando estão olhando as de seus amigos. A maneira mais fácil de resolver esse
problema é registrar todos os usuários como seus próprios seguidores no momento em que são criados.
Esse truque é mostrado no Exemplo 12-10.
Exemplo 12-10. app/ models/ user.py: Tornar os usuários seus próprios seguidores na construção
Infelizmente, você provavelmente tem vários usuários no banco de dados que já foram criados e não
estão seguindo a si mesmos. Se o banco de dados for pequeno e fácil de regenerar, ele poderá ser
excluído e recriado, mas se isso não for uma opção, adicionar uma função de atualização que conserte
os usuários existentes é a solução adequada. Isso é mostrado no Exemplo 12-11.
Exemplo 12-11. app/ models/ user.py: torne os usuários seus próprios seguidores
Agora o banco de dados pode ser atualizado executando a função de exemplo anterior no shell:
A criação de funções que introduzem atualizações no banco de dados é uma técnica comum usada para
atualizar os aplicativos implantados, pois a execução de uma atualização com script é menos propensa a
erros do que a atualização manual dos bancos de dados. No Capítulo 17, você verá como essa função e
outras semelhantes podem ser incorporadas a um script de implantação.
Tornar todos os usuários auto-seguidores torna o aplicativo mais utilizável, mas essa alteração apresenta
algumas complicações. As contagens de seguidores e usuários seguidos mostradas na página de perfil
do usuário agora são aumentadas em um devido aos links de auto-seguidor. Os números precisam ser
diminuídos em um para serem precisos, o que é fácil de fazer diretamente no modelo renderizando
{{ user.followers.count() - 1 }} e {{ user.followed.count() - 1 }} . As listas de seguidores e usuários seguidos
também devem ser ajustadas para não mostrar o mesmo usuário, outra tarefa simples de se fazer no
template com condicional. Por fim, todos os testes de unidade que verificam a contagem de seguidores
também são afetados pelos links de autoseguidores e devem ser ajustados para levar em conta os
autoseguidores.
CAPÍTULO 13
Comentários do usuário
Permitir que os usuários interajam é a chave para o sucesso de uma plataforma de blog social. Neste capítulo, você
aprenderá como implementar os comentários do usuário. As técnicas apresentadas são genéricas o suficiente para serem
diretamente aplicáveis a um grande número de aplicações socialmente habilitadas.
Os comentários aplicam postagens de blog específicas, portanto, é definida uma relação um-para-muitos da tabela de
postagens . Esse relacionamento pode ser usado para obter a lista de comentários associados a uma postagem de blog
específica.
165
Machine Translated by Google
class Comment(db.Model):
__tablename__ = 'comentários'
id = db.Column(db.Integer, primary_key=True) body =
db.Column(db.Text) body_html
= db.Column(db.Text) timestamp =
db .Column(db.DateTime, index=True, default=datetime.utcnow) desabilitado =
db.Column(db.Boolean) author_id =
db.Column(db.Integer, db.ForeignKey('users.id')) post_id =
db.Column(db.Integer, db.ForeignKey('posts.id'))
@staticmethod
def on_changed_body(alvo, valor, valor antigo, iniciador):
permitido_tags = ['a', 'abbr', 'acronym', 'b', 'código', 'em', 'i', 'forte']
Para concluir as alterações no banco de dados, os modelos User e Post devem definir os relacionamentos
um-para-muitos com a tabela de comentários , conforme mostrado no Exemplo 13-2.
Exemplo 13-2. app/ models/ user.py: relacionamentos um-para-muitos de usuários e postagens para
comentários
classe Usuário(db.Model):
# ...
comentários = db.relationship('Comentário', backref='autor', preguiçoso='dinâmico')
classe Post(db.Model): #
...
comentários = db.relationship('Comentário', backref='post', lazy='dinâmico')
classe ComentárioForm(Formulário):
body = StringField('', validators=[Required()]) submit =
SubmitField('Enviar')
O exemplo 13-4 mostra a rota /post/ <int:id> atualizada com suporte para comentários.
Essa função de exibição instancia o formulário de comentário e o envia para o modelo post.html para
renderização. A lógica que insere um novo comentário quando o formulário é enviado é semelhante ao
tratamento de postagens de blog. Como no caso Post , o autor do comentário não pode ser definido diretamente
como current_user porque este é um objeto proxy de variável de contexto.
A expressão current_user._get_current_object() retorna o objeto User real .
Os comentários são classificados por seu carimbo de data/hora em ordem cronológica, portanto, novos
comentários são sempre adicionados no final da lista. Quando um novo comentário é inserido, o redirecionamento
que finaliza a solicitação volta para a mesma URL, mas a função url_for() define a página como -1, um número
de página especial que é usado para solicitar a última página de comentários para que o comentário recém
inserido é visto na página. Quando o número da página é obtido
da string de consulta e encontrado como -1, um cálculo com o número de comentários e o tamanho da página
é feito para obter o número real da página a ser usada.
Para completar esse recurso, as postagens de blog exibidas nas páginas inicial e de perfil precisam de um link
para a página com os comentários. Isso é mostrado no Exemplo 13-5.
Exemplo 13-5. _app/ templates/ _posts.html: Link para os comentários da postagem do blog
Observe como o texto do link inclui o número de comentários, que é facilmente obtido a partir do relacionamento
um-para-muitos entre as postagens e as tabelas de comentários usando o filtro count() do SQLAlchemy .
Também é interessante a estrutura do link para a página de comentários, que é construída como um link
permanente para a postagem com um sufixo #comments adicionado. Esta última parte é chamada de fragmento
de URL e é usada para indicar uma posição inicial de rolagem da página. O navegador da Web procura um
elemento com o id fornecido e rola a página para que o elemento apareça no topo da página. Essa posição
inicial é definida para o cabeçalho de comentários no modelo post.html , que é escrito como <h4
id="comments">Comentários<h4>.
A Figura 13-2 mostra como os comentários aparecem na página.
Uma alteração adicional foi feita na macro de paginação. Os links de paginação para comentários também
precisam do fragmento #comments adicionado, portanto, um argumento de fragmento foi adicionado à macro
e passado na invocação da macro do modelo post.html .
No Capítulo 9 foi definida uma lista de funções de usuário, cada uma com uma lista de permissões. Uma das permissões era
Permission.MODERATE_COMMENTS, que dá aos usuários que a possuem em suas funções o poder de moderar comentários
feitos por outras pessoas.
Esse recurso será exposto como um link na barra de navegação que aparece apenas para os usuários que têm permissão
para usá-lo. Isso é feito no modelo base.html usando uma condicional, conforme mostrado no Exemplo 13-6.
Exemplo 13-6. app/ templates/ base.html: Link para moderar comentários na barra de navegação
...
{% if current_user.can(Permission.MODERATE_COMMENTS) %}
<li> <a href="{{ url_for('main.moderate') }}"> Moderar comentários</a></li> {%
endif %}
...
Esta é uma função muito simples que lê uma página de comentários do banco de dados e os
passa para um modelo para renderização. Junto com os comentários, o template recebe o objeto
de paginação e o número da página atual.
{% block page_content %}
<div class="page-header">
<h1>Moderação de comentários</
h1> </
div> {% set mode = True %}
{% include '_comments.html' %} {% if
paginação %} <div
class="pagination">
{{ macros.pagination_widget(pagination, '.moderate') }} </div> {%
endif
%} {%
endblock %}
Este modelo adia a renderização dos comentários para o modelo _comments.html , mas antes
de passar o controle para o modelo subordinado, ele usa a diretiva set de Jinja2 para definir uma
variável de modelo moderada definida como True. Essa variável é usada pelo modelo
_comments.html para determinar se os recursos de moderação precisam ser renderizados.
A parte do modelo _comments.html que processa o corpo de cada comentário precisa ser modificada
de duas maneiras. Para usuários regulares (quando a variável moderada não está definida), todos os
comentários marcados como desativados devem ser suprimidos. Para moderadores (quando
moderado está definido como True), o corpo do comentário deve ser renderizado independentemente
do estado desativado e, abaixo do corpo, um botão deve ser incluído para alternar o estado.
O Exemplo 13-9 mostra essas mudanças.
Exemplo 13-9. app/ templates/ _comments.html: renderização dos corpos dos comentários
...
<div class="comentário-corpo">
{% if comment.disabled %}
<p></p><i>Este comentário foi desativado por um moderador.</i></p> {% endif
%} {% se
moderado ou não comment.disabled % } {% if
comment.body_html %}
{{ comment.body_html | safe }} {%
else %}
{{ comment.body }} {%
endif %}
{% endif %}
</div>
{% se moderado %}
<br>
{% se comentário.disabled %}
<a class="btn btn-default btn-xs" href="{{ url_for('.moderate_enable' ,
id=comment.id, page=page) }}">Ativar</a> {%
else %} <a
class="btn btn-danger btn-xs" href="{{ url_for('.moderate_disable', id =comment.id,
page=page) }}">Desativar</a>
{% fim se %}
{% fim se %}
...
Com essas alterações, os usuários verão um breve aviso para comentários desabilitados. Os
moderadores verão o aviso e o corpo do comentário. Os moderadores também verão um botão para
alternar o estado desativado abaixo de cada comentário. O botão invoca uma das duas novas rotas,
dependendo de qual dos dois estados possíveis o comentário está mudando.
O Exemplo 13-10 mostra como essas rotas são definidas.
@main.route('/moderate/disable/<int:id>')
@login_required
@permission_required(Permission.MODERATE_COMMENTS)
def modere_disable(id):
comment = Comment.query.get_or_404(id)
comment.disabled = True
db.session.add(comentário)
return redirect(url_for('.moderate',
page=request.args.get('page', 1, type=int )))
O tópico de recursos sociais é concluído com este capítulo. No próximo capítulo, você aprenderá
como expor a funcionalidade do aplicativo como uma API que clientes que não sejam navegadores
da Web podem usar.
CAPÍTULO 14
Nos últimos anos, tem havido uma tendência nas aplicações web de mover cada vez mais a lógica de
negócios para o lado do cliente, produzindo uma arquitetura conhecida como Rich Internet Application
(RIA). Em RIAs, a função principal (e às vezes única) do servidor é fornecer ao aplicativo cliente
serviços de armazenamento e recuperação de dados. Neste modelo, o servidor torna-se um serviço
web ou Application Programming Interface (API).
Existem vários protocolos pelos quais os RIAs podem se comunicar com um serviço da web. Protocolos
Remote Procedure Call (RPC) como XML-RPC ou seu derivado Simplified Object Access Protocol
(SOAP) eram escolhas populares alguns anos atrás. Mais recentemente, a arquitetura Representational
State Transfer (REST) emergiu como a favorita para aplicações web devido ao fato de ser construída
no modelo familiar da World Wide Web.
O Flask é uma estrutura ideal para construir serviços da Web RESTful devido à sua natureza leve.
Neste capítulo, você aprenderá como implementar uma API RESTful baseada em Flask.
Introdução ao REST
O Ph.D. de Roy Fielding dissertação apresenta o estilo arquitetural REST para serviços web listando
suas seis características definidoras:
Servidor cliente
Deve haver uma separação clara entre os clientes e o servidor.
Stateless
Uma solicitação de cliente deve conter todas as informações necessárias para realizá-la.
O servidor não deve armazenar nenhum estado sobre o cliente que persista de uma solicitação
para a seguinte.
175
Machine Translated by Google
Cache
As respostas do servidor podem ser rotuladas como cacheable ou noncacheable para que os clientes
(ou intermediários entre clientes e servidores) possam usar um cache para fins de otimização.
Interface Uniforme O
protocolo pelo qual os clientes acessam os recursos do servidor deve ser consistente, bem definido e
padronizado. A interface uniforme comumente usada dos serviços da Web REST é o protocolo HTTP.
Servidores, caches
ou gateways do System Proxy em camadas podem ser inseridos entre clientes e servidores conforme
necessário para melhorar o desempenho, a confiabilidade e a escalabilidade.
Os clientes Code-on-
Demand podem, opcionalmente, baixar o código do servidor para executar em seu contexto.
recursos é fundamental para o estilo arquitetônico REST. Neste contexto, um recurso é um item de interesse
no domínio da aplicação. Por exemplo, no aplicativo de blog, usuários, postagens de blog e comentários
são todos recursos.
Cada recurso deve ter uma URL exclusiva que o represente. Continuando com o exemplo de blog, uma
postagem de blog pode ser representada pela URL /api/ posts/ 12345, em que 12345 é um identificador
exclusivo da postagem, como a chave primária do banco de dados da postagem. O formato ou conteúdo da
URL realmente não importa; tudo o que importa é que cada URL de recurso identifique exclusivamente um
recurso.
Uma coleção de todos os recursos em uma classe também possui um URL atribuído. A URL para a coleção
de posts do blog pode ser /api/ posts/ e a URL para a coleção de todos os comentários pode ser /api/
comments/.
Uma API também pode definir URLs de coleção que representam subconjuntos lógicos de todos os recursos
em uma classe. Por exemplo, a coleção de todos os comentários na postagem do blog 12345 pode ser
representada pela URL /api/ posts/ 12345/ comments/. É uma prática comum definir URLs que representam
coleções de recursos com uma barra final, pois isso dá a eles uma representação de “pasta”.
Esteja ciente de que Flask aplica tratamento especial a rotas que terminam
com uma barra. Se um cliente solicitar uma URL sem uma barra final e a única
rota correspondente tiver uma barra no final, o Flask responderá automaticamente
com um redirecionamento para a URL da barra final. Nenhum redirecionamento
é emitido para o caso inverso.
Métodos de solicitação
O aplicativo cliente envia solicitações ao servidor nas URLs de recursos estabelecidas e usa o método
de solicitação para indicar a operação desejada. Para obter a lista de postagens de blog disponíveis
na API de blogging, o cliente enviaria uma solicitação GET para http:// www.example.com/ api/ posts/
e, para inserir uma nova postagem de blog, enviaria uma solicitação POST para a mesma URL, com
o conteúdo da postagem do blog no corpo da solicitação. Para recuperar a postagem de blog 12345,
o cliente enviaria uma solicitação GET para http:// www.example.com/ api/ posts/ 12345. A Tabela 14-1
lista os métodos de solicitação comumente usados em APIs RESTful com seus significados.
URL
PEGAR Coleta de recursos Obtenha a coleção de recursos (ou uma página dela se o servidor implementar 200
URL paginação).
PUBLICAR Coleta de recursos Crie um novo recurso e adicione-o à coleção. O servidor escolhe 201
na resposta.
COLOCAR recurso individual Modifique um recurso existente. Alternativamente, este método também pode 200
URL ser usado para criar um novo recurso quando o cliente pode escolher o recurso
URL.
URL
URL
recursos são enviados e recebidos entre o cliente e o servidor nos corpos de solicitações e respostas,
mas o REST não especifica o formato a ser usado para codificar os recursos. O cabeçalho Content-
Type em solicitações e respostas é usado para indicar o formato no qual um recurso é codificado no
corpo. Os mecanismos de negociação de conteúdo padrão no protocolo HTTP podem ser usados
entre o cliente e o servidor para concordar com um formato compatível com ambos.
Os dois formatos comumente usados com serviços web RESTful são JavaScript Object Notation (JSON) e
Extensible Markup Language (XML). Para RIAs baseados na web, o JSON é atraente por causa de seus
laços estreitos com o JavaScript, a linguagem de script do lado do cliente usada pelos navegadores da web.
Voltando à API de exemplo do blog, um recurso de postagem de blog pode ser representado em JSON da
seguinte maneira:
{
"url": "http://www.example.com/api/posts/12345", "title":
"Escrevendo APIs RESTful em Python", "autor":
"http://www.example.com/api /users/2", "body": "... texto
do artigo aqui...", "comentários": "http://
www.example.com/api/posts/12345/comments"
}
Observe como os campos URL, autor e comentários na postagem do blog acima são URLs de recursos
totalmente qualificados. Isso é importante porque essas URLs permitem que o cliente descubra novos
recursos.
Em uma API RESTful bem projetada, o cliente conhece apenas uma pequena lista de URLs de recursos de
nível superior e, em seguida, descobre o restante a partir de links incluídos nas respostas, semelhante a
como você pode descobrir novas páginas da Web enquanto navega na Web clicando em links que aparecem
em páginas que você conhece.
Controle de
versão Em um aplicativo da Web tradicional centrado no servidor, o servidor tem controle total do aplicativo.
Quando um aplicativo é atualizado, a instalação da nova versão no servidor é suficiente para atualizar todos
os usuários, pois até mesmo as partes do aplicativo que rodam no navegador do usuário são baixadas do
servidor.
A situação com RIAs e serviços da web é mais complicada, porque frequentemente os clientes são
desenvolvidos independentemente do servidor – talvez até por pessoas diferentes. Considere o caso de um
aplicativo em que o serviço da Web RESTful é usado por uma variedade de clientes, incluindo navegadores
da Web e clientes de smartphones nativos. O cliente do navegador da web pode ser atualizado no servidor
a qualquer momento, mas os aplicativos do smartphone não podem ser atualizados à força; o proprietário
do smartphone precisa permitir que a atualização aconteça. Mesmo que o proprietário do smartphone esteja
disposto a atualizar, não é possível programar a implantação dos aplicativos atualizados do smartphone em
todas as lojas de aplicativos para coincidir exatamente com a implantação do novo servidor.
Por esses motivos, os serviços da Web precisam ser mais tolerantes do que os aplicativos da Web comuns
e ser capazes de trabalhar com versões antigas de seus clientes. Uma maneira comum de resolver esse
problema é criar uma versão das URLs manipuladas pelo serviço da Web. Por exemplo, o primeiro
lançamento do serviço da Web de blog pode expor a coleção de postagens de blog em /api/ v1.0/ posts/.
Incluir a versão do serviço da Web na URL ajuda a manter os recursos novos e antigos organizados para
que o servidor possa fornecer novos recursos a novos clientes enquanto continua a oferecer suporte
antigos clientes. Uma atualização no serviço de blogging pode alterar o formato JSON das postagens
do blog e agora expor as postagens do blog como /api/ v1.1/ posts/, mantendo o formato JSON antigo
para clientes que se conectam a /api/ v1.0/ Postagens/. Por um período de tempo, o servidor manipula
todas as URLs em suas variações v1.1 e v1.0 .
Embora o suporte a várias versões do servidor possa se tornar um fardo de manutenção, há situações
em que essa é a única maneira de permitir que o aplicativo cresça sem causar problemas às implantações
existentes.
As seções a seguir mostram como o Flasky pode ser estendido com um serviço da Web RESTful que
fornece aos clientes acesso a postagens de blog e recursos relacionados.
rotas associadas a uma API RESTful formam um subconjunto independente do aplicativo, portanto,
colocá-las em seu próprio esquema é a melhor maneira de mantê-las bem organizadas. A estrutura
geral do esquema da API dentro do aplicativo é mostrada no Exemplo 14-1.
|-flasky |-
app/ |-
api_1_0 |-
__init__.py |-
user.py |-
post.py |-
comment.py |-
authentication.py |-
errors.py |-
decorators.py
Observe como o pacote usado para a API inclui um número de versão em seu nome. Quando uma
versão incompatível com versões anteriores da API precisa ser introduzida, ela pode ser adicionada
como outro pacote com um número de versão diferente e ambas as APIs podem ser atendidas ao
mesmo tempo.
Este esquema de API implementa cada recurso em um módulo separado. Módulos para levar
cuidados com a autenticação, tratamento de erros e fornecer decoradores personalizados também estão incluídos
incluído. O construtor do blueprint é mostrado no Exemplo 14-2.
def create_app(config_name):
...
# de .api_1_0 importar api como api_1_0_blueprint
app.register_blueprint(api_1_0_blueprint, url_prefix='/api/v1.0')
# ...
Tratamento de erros
Um serviço da Web RESTful informa ao cliente sobre o status de uma solicitação enviando o aplicativo
código de status HTTP apropriado na resposta mais qualquer informação adicional no
corpo de resposta. Os códigos de status típicos que um cliente pode esperar ver em um serviço da web
estão listados na Tabela 14-2.
Tabela 14-2. Códigos de status de resposta HTTP normalmente retornados por APIs
201 Criada A solicitação foi concluída com sucesso e um novo recurso foi criado como resultado.
403 Proibido As credenciais de autenticação enviadas com a solicitação são insuficientes para a solicitação.
405 Método não permitido O método de solicitação solicitado não é compatível com o recurso fornecido.
A manipulação dos códigos de status 404 e 500 apresenta uma pequena complicação, pois esses
erros são gerados pelo Flask por conta própria e geralmente retornam uma resposta HTML,
o que provavelmente confundirá um cliente de API.
Uma forma de gerar respostas apropriadas para todos os clientes é fazer com que os manipuladores de erros
adaptem suas respostas com base no formato solicitado pelo cliente, técnica denominada negociação de
conteúdo. O Exemplo 14-4 mostra um manipulador de erro 404 aprimorado que responde com JSON para
clientes de serviços da Web e com HTML para outros. O manipulador de erros 500 é escrito de maneira
semelhante.
Exemplo 14-4. app/ main/ errors.py: Manipuladores de erros com negociação de conteúdo HTTP
@main.app_errorhandler(404)
def page_not_found(e):
se request.accept_mimetypes.accept_json e \
não request.accept_mimetypes.accept_html:
response = jsonify({'error': 'not found'})
response.status_code = 404
return response
return render_template('404.html'), 404
Esta nova versão do manipulador de erros verifica o cabeçalho de solicitação Accept , que Werkzeug decodifica
em request.accept_mimetypes, para determinar em qual formato o cliente deseja a resposta. gerado apenas
para clientes que aceitam JSON e não aceitam HTML.
Os códigos de status restantes são gerados explicitamente pelo serviço da Web, para que possam ser
implementados como funções auxiliares dentro do projeto no módulo errors.py .
O Exemplo 14-5 mostra a implementação do erro 403; os outros são semelhantes.
Exemplo 14-5. app/ api/ errors.py: manipulador de erros da API para o código de status 403
def proibido(mensagem):
response = jsonify({'error': 'forbidden', 'message': message})
response.status_code = 403
return response
Agora as funções de exibição no serviço da Web podem invocar essas funções auxiliares para gerar respostas
de erro.
Os serviços da Web, como os aplicativos da Web comuns, precisam proteger as informações e garantir que
não sejam fornecidas a terceiros não autorizados. Por esse motivo, os RIAs devem solicitar credenciais de login
de seus usuários e passá-las ao servidor para verificação.
Foi mencionado anteriormente que uma das características dos serviços da Web RESTful é que eles não têm
estado, o que significa que o servidor não tem permissão para “lembrar” nada sobre o cliente entre as
solicitações. Os clientes precisam fornecer todas as informações necessárias para realizar uma solicitação na
própria solicitação, portanto, todas as solicitações devem incluir credenciais de usuário.
A funcionalidade de login atual implementada com a ajuda do Flask-Login armazena dados na sessão do
usuário, que o Flask armazena por padrão em um cookie do lado do cliente, para que o servidor não
armazene nenhuma informação relacionada ao usuário; ele pede ao cliente para armazená-lo. Parece que
essa implementação está em conformidade com o requisito sem estado do REST, mas o uso de cookies
em serviços da Web RESTful cai em uma área cinzenta, pois pode ser complicado para clientes que não
são navegadores da Web implementá-los. Por esse motivo, geralmente é visto como uma má escolha de
design usá-los.
O requisito sem estado do REST pode parecer excessivamente rígido, mas não é
arbitrário. Servidores sem estado podem escalar com muita facilidade. Quando os
servidores armazenam informações sobre os clientes, é necessário ter um cache
compartilhado acessível a todos os servidores para garantir que o mesmo servidor
sempre receba solicitações de um determinado cliente. Ambos são problemas complexos de resolver.
Como a arquitetura RESTful é baseada no protocolo HTTP, a autenticação HTTP é o método preferencial
usado para enviar credenciais, seja em suas versões Basic ou Digest.
Com a autenticação HTTP, as credenciais do usuário são incluídas em um cabeçalho de autorização com
todas as solicitações.
O protocolo de autenticação HTTP é simples o suficiente para ser implementado diretamente, mas a
extensão Flask-HTTPAuth fornece um wrapper conveniente que oculta os detalhes do protocolo em um
decorador semelhante ao login_required do Flask-Login .
Para inicializar a extensão para autenticação HTTP Basic, deve-se criar um objeto da classe
HTTPBasicAuth . Como Flask-Login, Flask-HTTPAuth não faz suposições sobre o procedimento necessário
para verificar as credenciais do usuário, portanto, essas informações são fornecidas em uma função de
retorno de chamada. O Exemplo 14-6 mostra como o ramal é inicializado e fornecido com um retorno de
chamada de verificação.
@auth.verify_password
def verify_password(email, password): if email
== '': g.current_user
= AnonymousUser() return True
Como esse tipo de autenticação do usuário será usado apenas no esquema da API, a
extensão Flask HTTPAuth é inicializada no pacote do esquema e não no pacote do aplicativo
como outras extensões.
O e-mail e a senha são verificados usando o suporte existente no modelo Usuário . O callback
de verificação retorna True quando o login é válido ou False caso contrário. Logins anônimos
são suportados, para os quais o cliente deve enviar um campo de e-mail em branco.
Quando as credenciais de autenticação são inválidas, o servidor retorna um erro 401 para o
cliente. Flask-HTTPAuth gera uma resposta com esse código de status por padrão, mas para
garantir que a resposta seja consistente com outros erros retornados pela API, a resposta de
erro pode ser personalizada conforme mostrado no Exemplo 14-7.
@api.route('/posts/')
@auth.login_required
def get_posts():
passar
Mas como todas as rotas no blueprint precisam ser protegidas da mesma maneira, o decorator
login_required pode ser incluído uma vez em um manipulador before_request para o blueprint,
conforme mostrado no Exemplo 14-8.
@api.before_request
@auth.login_required
def before_request():
Agora as verificações de autenticação serão feitas automaticamente para todas as rotas no blueprint.
Como verificação adicional, o manipulador before_request também rejeita usuários autenticados que
não confirmaram suas contas.
Os clientes devem enviar credenciais de autenticação a cada solicitação. Para evitar a transferência
constante de informações confidenciais, pode ser oferecida uma solução de autenticação baseada
em token.
Na autenticação baseada em token, o cliente envia as credenciais de login para uma URL especial
que gera tokens de autenticação. Depois que o cliente tiver um token, ele poderá usá-lo no lugar das
credenciais de login para autenticar solicitações. Por motivos de segurança, os tokens são emitidos
com uma expiração associada. Quando um token expira, o cliente deve autenticar novamente para
obter um novo. O risco de um token cair em mãos erradas é limitado devido à sua curta vida útil. O
Exemplo 14-9 mostra os dois novos métodos adicionados ao modelo User que oferecem suporte à
geração e verificação de tokens de autenticação usando itsdangerous.
@staticmethod
def verify_auth_token(token): s =
Serializer(current_app.config['SECRET_KEY']) try: data =
return User.query.get(data['id'])
Para autenticar solicitações que vêm com um token, o retorno de chamada Verify_password para
Flask-HTTPAuth deve ser modificado para aceitar tokens, bem como credenciais regulares. O
callback atualizado é mostrado no Exemplo 14-10.
retornar falso
g.current_user = usuário
g.token_used = Falso
return user.verify_password(senha)
Nesta nova versão, o primeiro argumento de autenticação pode ser o endereço de e-mail ou um
token de autenticação. Se este campo estiver em branco, será considerado um usuário anônimo,
como antes. Se a senha estiver em branco, o campo email_or_token será considerado um token
e validado como tal. Se ambos os campos não estiverem vazios, a autenticação regular de e-mail
e senha será assumida. Com essa implementação, a autenticação baseada em token é opcional;
fica a critério de cada cliente usar ou não. Para dar às funções de exibição a capacidade de
distinguir entre os dois métodos de autenticação, uma variável g.token_used é adicionada.
A rota que retorna os tokens de autenticação para o cliente também é adicionada ao projeto da
API. A implementação é mostrada no Exemplo 14-11.
Exemplo 14-12. app/ models.py: converte uma postagem em um dicionário JSON serializável
classe Post(db.Model): #
...
def to_json(self):
json_post = {
'url': url_for('api.get_post', id=self.id, _external=True), 'body': self.body,
'body_html':
self.body_html, 'timestamp':
self.timestamp, 'autor' :
url_for('api.get_user', id=self.author_id, _external=True),
} return json_post
Os campos url, author e comments precisam retornar as URLs dos respectivos recursos, então estes
são gerados com chamadas url_for() para rotas que serão definidas no blueprint da API. Observe
que _external=True é adicionado a todas as chamadas url_for() para que as URLs totalmente
qualificadas sejam retornadas em vez das URLs relativas que normalmente são usadas no contexto
de um aplicativo da web tradicional.
Este exemplo também mostra como é possível retornar atributos “inventados” na representação de
um recurso. O campo comment_count retorna o número de comentários existentes para a postagem
do blog. Embora este não seja um atributo real do modelo, ele é incluído na representação do recurso
como uma conveniência para o cliente.
O método to_json() para modelos User pode ser construído de forma semelhante ao Post.
Esse método é mostrado no Exemplo 14-13.
'post_count': self.posts.count()
} return json_user
Observe como neste método alguns dos atributos do usuário, como e-mail e função, são omitidos da resposta
por motivos de privacidade. Este exemplo novamente demonstra que a representação de um recurso
oferecido aos clientes não precisa ser idêntica à representação interna do modelo de banco de dados
correspondente.
A conversão de uma estrutura JSON em um modelo apresenta o desafio de que alguns dos dados
provenientes do cliente podem ser inválidos, errados ou desnecessários. O Exemplo 14-14 mostra o método
que cria um modelo Post a partir de JSON.
Exemplo 14-14. app/ models.py: Crie uma postagem de blog a partir de JSON
class Post(db.Model): #
...
@staticmethod
def from_json(json_post): body
= json_post.get('body') if body is
None or body == '': raise
ValidationError('post does not have a body') return
Post(corpo=corpo)
Como você pode ver, esta implementação opta por usar apenas o atributo body do dicionário JSON. O
atributo body_html é ignorado, pois a renderização Markdown do lado do servidor é acionada automaticamente
por um evento SQLAlchemy sempre que o atributo body é modificado. O atributo carimbo de data/hora não
precisa ser fornecido, a menos que o cliente tenha permissão para retroceder postagens, o que não é um
recurso deste aplicativo. O campo autor não é usado porque o cliente não tem autoridade para selecionar o
autor de uma postagem de blog; o único valor possível para o campo autor é o do usuário autenticado. Os
atributos comments e comment_count são gerados automaticamente a partir de um relacionamento de banco
de dados, portanto, não há nenhuma informação útil neles necessária para criar um modelo. Por fim, o campo
url é ignorado porque nessa implementação as URLs dos recursos são definidas pelo servidor, não pelo
cliente.
Observe como a verificação de erros é feita. Se o campo do corpo estiver ausente ou vazio, uma exceção
ValidationError será gerada. Lançar uma exceção é neste caso a maneira apropriada de lidar com o erro
porque este método não tem conhecimento suficiente para lidar adequadamente com a condição. A exceção
efetivamente passa o erro para o chamador, permitindo que o código de nível superior faça o tratamento do
erro. A classe ValidationError é implementada como uma subclasse simples de ValueError do Python. Essa
implementação é mostrada no Exemplo 14-15.
classe ValidationError(ValueError):
passar
O aplicativo agora precisa lidar com essa exceção fornecendo a resposta apropriada ao cliente. Para evitar
a adição de código de captura de exceção nas funções de exibição, um manipulador de exceção global
pode ser instalado. Um manipulador para a exceção ValidationError é mostrado no Exemplo 14-16.
Exemplo 14-16. app/ api_1_0/ errors.py: manipulador de erro de API para ValidationError exceto
ções
@api.errorhandler(ValidationError) def
validation_error(e): return
bad_request(e.args[0])
O decorator errorhandler é o mesmo que é usado para registrar manipuladores para códigos de status
HTTP, mas neste uso ele usa uma classe Exception como argumento. A função decorada será invocada
sempre que uma exceção da classe especificada for levantada. Observe que o decorador é obtido do
esquema da API, portanto, esse manipulador será invocado apenas quando a exceção for gerada enquanto
uma rota do esquema estiver sendo processada.
Usando esta técnica, o código nas funções de visualização pode ser escrito de forma muito clara e concisa,
sem a necessidade de incluir a verificação de erros. Por exemplo:
resta é implementar as rotas que lidam com os diferentes recursos. As solicitações GET são geralmente as
mais fáceis porque apenas retornam informações e não precisam fazer nenhuma alteração. O exemplo
14-17 mostra os dois manipuladores GET para postagens de blog.
Exemplo 14-17. app/ api_1_0/ posts.py: GET manipuladores de recursos para postagens
@api.route('/posts/')
@auth.login_required
def get_posts():
posts = Post.query.all() return
jsonify({ 'posts': [post.to_json() for post in posts] })
@api.route('/posts/<int:id>')
@auth.login_required
def get_post(id):
post = Post.query.get_or_404(id) return
jsonify(post.to_json())
A primeira rota trata da requisição da coleção de posts. Essa função usa uma compreensão de lista para gerar
a versão JSON de todas as postagens. A segunda rota retorna uma única postagem de blog e responde com
um erro de código 404 quando o id fornecido não é encontrado no banco de dados.
O manipulador POST para recursos de postagem de blog insere uma nova postagem de blog no banco de
dados. Essa rota é mostrada no Exemplo 14-18.
Exemplo 14-18. app/ api_1_0/ posts.py: manipulador de recursos POST para postagens
@api.route('/posts/', method=['POST'])
@permission_required(Permission.WRITE_ARTICLES)
def new_post():
post = Post.from_json(request.json)
post.author = g.current_user db.
session.add(post)
db.session.commit()
return jsonify(post.to_json()), 201, \
{'Localização': url_for('api.get_post', id=post.id, _external=True)}
Observe que, para conveniência do cliente, o corpo da resposta inclui o novo recurso. Isso evitará que o cliente
tenha que emitir um recurso GET para ele imediatamente após a criação do recurso.
O decorador permission_required usado para impedir que usuários não autorizados criem novas postagens de
blog é semelhante ao usado no aplicativo, mas é personalizado para o esquema da API. A implementação é
mostrada no Exemplo 14-19.
O manipulador PUT para postagens de blog, usado para editar recursos existentes, é mostrado no Exemplo
14-20.
Exemplo 14-20. app/ api_1_0/ posts.py: manipulador de recursos PUT para postagens
@api.route('/posts/<int:id>', methods=['PUT'])
@permission_required(Permission.WRITE_ARTICLES)
def edit_post(id): post
= Post.query.get_or_404(id) if g.
current_user != post.author e \ not
g.current_user.can(Permission.ADMINISTER):
return proibido('Permissões insuficientes')
post.body = request.json.get('body', post.body)
db.session.add(post)
return jsonify(post.to_json())
As verificações de permissão são mais complexas neste caso. A verificação padrão de permissão para
escrever postagens de blog é feita com o decorador, mas para permitir que um usuário edite uma postagem
de blog, a função também deve garantir que o usuário seja o autor da postagem ou então um administrador.
Essa verificação é adicionada explicitamente à função de visualização. Se essa verificação tiver que ser
adicionada em muitas funções de exibição, criar um decorador para ela seria uma boa maneira de evitar a
repetição de código.
Como o aplicativo não permite a exclusão de postagens, o manipulador para o método de solicitação
DELETE não precisa ser implementado.
/users/<int:id> Um usuário
PEGAR
/Postagens/
GET, POST Todas as postagens do blog
O Exemplo 14-21 mostra uma possível implementação de paginação para a lista de postagens de blog.
@api.route('/posts/') def
get_posts(): page =
request.args.get('page', 1, type=int) pagination =
Post.query.paginate( page,
per_page=current_app.config[ 'FLASKY_POSTS_PER_PAGE'],
error_out=False)
posts = pagination.items prev
= Nenhum se
pagination.has_prev:
prev = url_for('api.get_posts', page=page-1, _external=True)
next =
Nenhum se pagination.has_next:
next = url_for('api.get_posts', page=page+1, _external=True) return
jsonify({ 'posts':
[post.to_json() for post in posts], 'prev': prev, 'next':
próximo,
'contagem':
paginação.total
})
O campo posts na resposta JSON contém os itens de dados como antes, mas agora é apenas uma
parte do conjunto completo. Os itens anterior e seguinte contêm os URLs de recursos para as páginas
anterior e seguinte, quando disponíveis. O valor de contagem é o número total de itens na coleção.
Essa técnica pode ser aplicada a todas as rotas que retornam coleções.
um serviço da Web, um cliente HTTP deve ser usado. Os dois clientes mais usados para testar serviços da Web a
partir da linha de comando são curl e HTTPie. O último tem uma linha de comando muito mais concisa e legível.
{
"postagens": [
...
],
"anterior": nulo
"próximo": "http://127.0.0.1:5000/api/v1.0/posts/?page=2", "contagem":
150
}
Observe os links de paginação incluídos na resposta. Como esta é a primeira página, uma página anterior não é
definida, mas uma URL para obter a próxima página e uma contagem total foram retornadas.
A mesma solicitação pode ser emitida por um usuário anônimo enviando um e-mail e senha vazios:
O comando a seguir envia uma solicitação POST para adicionar uma nova postagem de blog:
{
"autor": "http://127.0.0.1:5000/api/v1.0/users/1", "body": "Estou
adicionando uma postagem da *linha de comando*.", "body_html":
"<p>Estou adicionando uma postagem da <em>linha de comando</em>.</p>",
"comments": "http://127.0.0.1:5000/api/v1.0/posts /111/comments",
"comment_count": 0,
"timestamp": "Dom, 22 de dezembro de 2013 08:30:27
GMT", "url": "http://127.0.0.1:5000/api/v1.0 /postagens/111"
}
Para usar tokens de autenticação, uma solicitação para /api/ v1.0/ token é enviada:
{
"expiration": 3600,
"token": "eyJpYXQiOjEzODg4MjQ3MjcsImV4cCI6MTM4ODgyODMyNywiYWxnIjoiSFMy..."
}
E agora o token retornado pode ser usado para fazer chamadas na API pela próxima hora, passando-o junto
Quando o token expirar, as solicitações serão retornadas com um erro de código 401, indicando que um
novo token precisa ser obtido.
Parabéns! Este capítulo conclui a Parte II e, com isso, a fase de desenvolvimento de recursos do Flasky
está concluída. O próximo passo é obviamente implantá-lo, e isso traz um novo conjunto de desafios que
são o assunto da Parte III.
PARTE III
a última milha
Machine Translated by Google
Machine Translated by Google
CAPÍTULO 15
teste
Há duas razões muito boas para escrever testes de unidade. Ao implementar uma nova funcionalidade, os testes de
unidade são usados para confirmar que o novo código está funcionando da maneira esperada. O mesmo resultado
pode ser obtido testando manualmente, mas é claro que os testes automatizados economizam tempo e esforço.
Uma segunda razão, mais importante, é que cada vez que o aplicativo é modificado, todos os testes de unidade
construídos em torno dele podem ser executados para garantir que não haja regressões no código existente; em
outras palavras, que as novas alterações não afetaram a maneira como o código antigo funciona.
Os testes de unidade fazem parte do Flasky desde o início, com testes projetados para exercitar recursos específicos
do aplicativo implementados nas classes de modelo de banco de dados.
Essas classes são fáceis de testar fora do contexto de um aplicativo em execução, portanto, como exige pouco
esforço, implementar testes de unidade para todos os recursos implementados nos modelos de banco de dados é a
melhor maneira de garantir que pelo menos parte do aplicativo comece robusto e fica assim.
Python tem uma excelente ferramenta de cobertura de código apropriadamente chamada de cobertura. Você pode
instalá-lo com pip:
197
Machine Translated by Google
Essa ferramenta vem como um script de linha de comando que pode iniciar qualquer aplicativo
Python com cobertura de código habilitada, mas também fornece acesso de script mais
conveniente para iniciar o mecanismo de cobertura programaticamente. Para ter métricas de
cobertura bem integradas ao script iniciador manage.py , o comando de teste personalizado
adicionado no Capítulo 7 pode ser expandido com um argumento opcional --coverage . A
implementação desta opção é mostrada no Exemplo 15-1.
# ...
@manager.command
def test(coverage=False):
"""Execute os testes de
unidade.""" se cobertura e não os.environ.get('FLASK_COVERAGE'):
import sys
os.environ['FLASK_COVERAGE'] = '1'
os.execvp(sys.executable, [sys .executable] + sys.argv) import
unittest tests =
unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests) if COV:
COV.stop()
COV.save()
print('Resumo da Cobertura:')
COV.report()
basedir = os.path.abspath(os.path.dirname(__file__)) covdir =
os.path.join(basedir, 'tmp/coverage')
COV.html_report(directory=covdir) print('
Versão HTML: file://%s/index.html' % covdir)
COV.erase()
# ...
O Flask-Script facilita muito a definição de comandos personalizados. Para adicionar uma opção
booleana ao comando de teste , adicione um argumento booleano à função test() . Flask-Script
deriva o nome da opção do nome do argumento e passa True ou False para a função de acordo.
Mas integrar a cobertura de código com o script manage.py apresenta um pequeno problema. No
momento em que a opção --coverage é recebida na função test() , já é tarde demais para ativar
as métricas de cobertura; a essa altura, todo o código no escopo global já foi
OK
Resumo da cobertura:
Nome Stmts Miss Branch BrMiss Cover Missing
...
.------------------------------------------------- ----------------------
app/__init__ app/ 33 0 0 0 100%
api_1_0/__init__ app/api_1_0/ 3 0 0 0 100%
authentication app/api_1_0/comments 30 19 11 11 27%
app/api_1_0/decorators app/ 40 30 12 12 19%
api_1_0/errors app/api_1_0/posts 11 3 2 2 62%
app/api_1_0/users app/ 17 10 0 0 41%
auth/__init__ app/auth/ 35 23 9 9 27%
forms app/ aplicativo de 30 24 12 12 14%
autenticação/visualizações/ 3 0 0 0 100%
decoradores 45 8 8 8 70%
109 84 41 41 17%
14 3 2 2 69%
app/email 15 9 0 0 40%
app/exceptions app/ 2 0 0 0 100%
main/__init__ app/main/ 6 1 0 0 83%
errors app/main/forms 20 15 9 9 17%
app/main/views app/ 39 7 8 8 68%
models 169 131 36 36 19%
243 62 44 17 72%
.------------------------------------------------- ----------------------
429
TOTAL 864 194 versão HTML: file:///home/flask/flasky/tmp/coverage/ 167 44%
index.html
O relatório mostra uma cobertura geral de 44%, o que não é péssimo, mas não é muito bom
qualquer. As classes de modelo, que receberam toda a atenção dos testes de unidade até agora,
constituem um total de 243 afirmações, das quais 72% são contempladas em provas. Obviamente, as views.py
arquivos nos blueprints principal e de autenticação e as rotas no blueprint api_1_0 todos têm
abrangência muito baixa, uma vez que não são exercidos em nenhum dos testes unitários existentes.
Armado com este relatório, é fácil determinar quais testes precisam ser adicionados ao teste
suite para melhorar a cobertura, mas infelizmente nem todas as partes do aplicativo podem ser testadas
tão facilmente quanto os modelos de banco de dados. As próximas duas seções discutem testes mais avançados
estratégias que podem ser aplicadas para exibir funções, formulários e modelos.
tion para testá-lo, pois a função pode precisar acessar contextos globais do Flask, como request
ou sessão, pode estar esperando dados de formulário em uma solicitação POST e algumas funções de exibição
também pode exigir um usuário conectado. Resumindo, as funções de exibição podem ser executadas apenas dentro do
contexto de uma solicitação e um aplicativo em execução.
O Flask vem equipado com um cliente de teste para tentar resolver esse problema, pelo menos parcialmente.
O cliente de teste replica o ambiente que existe quando um aplicativo está em execução
dentro de um servidor web, permitindo que os testes atuem como clientes e enviem requisições.
As funções de visualização não apresentam grandes diferenças quando executadas no cliente de teste;
as solicitações são recebidas e encaminhadas para as funções de exibição apropriadas, das quais
as respostas são geradas e retornadas. Depois que uma função de exibição é executada, sua resposta é
passou para o teste, que pode verificar se está correto.
15-2 mostra uma estrutura de teste de unidade que usa o cliente de teste.
Exemplo 15-2. testes/ test_client.py: Framework para testes usando o cliente de teste Flask
import unittest
from app import create_app, db
from app.models import User, Role
Role.insert_roles()
self.client = self.app.test_client(use_cookies=True)
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
def test_home_page(self):
resposta = self.client.get(url_for('main.index'))
self.assertTrue('Stranger' in response.get_data(as_text=True))
A variável de instância self.client adicionada ao caso de teste é o objeto cliente de teste Flask.
Este objeto expõe métodos que emitem solicitações para o aplicativo. Quando o cliente de teste é
criado com a opção use_cookies ativada, ele aceitará e enviará cookies da mesma forma que os
navegadores, portanto, a funcionalidade que depende de cookies para recuperar o contexto entre as
solicitações pode ser usada. Em particular, essa abordagem permite o uso de sessões de usuário,
portanto, é necessário fazer logon e logout dos usuários.
O teste test_home_page() é um exemplo simples do que o cliente de teste pode fazer. Neste exemplo,
uma solicitação para a página inicial é emitida. O valor de retorno do método get() do cliente de teste
é um objeto Flask Response , contendo a resposta retornada pela função de visualização invocada.
Para verificar se o teste foi bem-sucedido, o corpo da resposta, obtido de response.get_data(), é
pesquisado pela palavra "Stranger", que faz parte do “Hello, Stranger!” saudação mostrada a usuários
anônimos. Observe que get_data() retorna o corpo da resposta como uma matriz de bytes por padrão;
passando as_text=True retorna uma string Unicode que é muito mais fácil de trabalhar.
O cliente de teste também pode enviar solicitações POST que incluem dados de formulário usando o
método post() , mas enviar formulários apresenta uma pequena complicação. Os formulários gerados
pelo Flask-WTF possuem um campo oculto com um token CSRF que precisa ser enviado junto com o
formulário. Para replicar essa funcionalidade, um teste precisaria solicitar a página que inclui o
formulário, analisar o texto HTML retornado na resposta e extrair o token para que possa enviar o
token com os dados do formulário. Para evitar o incômodo de negociar
com tokens CSRF em testes, é melhor desabilitar a proteção CSRF na configuração de teste. Isso é
mostrado no Exemplo 15-3.
classe TestingConfig(Config):
#...
WTF_CSRF_ENABLED = Falso
O exemplo 15-4 mostra um teste de unidade mais avançado que simula um novo usuário registrando
uma conta, efetuando login, confirmando a conta com um token de confirmação e, finalmente, efetuando
logout.
Exemplo 15-4. testes/ test_client.py: Simulação de um novo fluxo de trabalho do usuário com o cliente
de teste Flask
classe FlaskClientTestCase(unittest.TestCase):
...
# def test_register_and_login(self): #
registrar uma nova conta
response = self.client.post(url_for('auth.register'), data={
'email': '[email protected]',
'username': 'john',
'password': 'cat',
'password2': 'cat'
})
self.assertTrue(response.status_code == 302)
# sair
response = self.client.get(url_for('auth.logout'),
follow_redirects=True) data
= response.get_data(as_text=True)
self.assertTrue('Você foi desconectado' em data)
O teste começa com o envio de um formulário para a via de inscrição. O argumento data para post()
é um dicionário com os campos do formulário, que devem corresponder exatamente aos nomes dos
campos definidos no formulário. Como a proteção CSRF agora está desativada na configuração de
teste, não há necessidade de enviar o token CSRF com o formulário.
A rota /auth/ register pode responder de duas maneiras. Se os dados cadastrais forem válidos, um
redirecionamento envia o usuário para a página de login. No caso de um registro inválido, a resposta
renderiza a página com o formulário de registro novamente, incluindo quaisquer mensagens de erro
apropriadas. Para validar se o cadastro foi aceito, o teste verifica se o código de status da resposta é
302, que é o código para um redirecionamento.
A segunda seção do teste emite um login para o aplicativo usando o e-mail e a senha recém-
cadastrados. Isso é feito com uma solicitação POST para a rota /auth/ login . Desta vez, um argumento
follow_redirects=True é incluído na chamada post() para fazer o cliente de teste funcionar como um
navegador e emitir automaticamente uma solicitação GET para a URL redirecionada. Com esta
opção, o código de status 302 não será retornado; em vez disso, a resposta da URL redirecionada é
retornada.
Uma resposta bem-sucedida ao envio de login agora teria uma página que cumprimenta o usuário
pelo nome de usuário e indica que a conta precisa ser confirmada para obter acesso. Duas instruções
assert verificam se esta é a página, e aqui é interessante observar que uma pesquisa pela string
'Hello, john!' não funcionaria porque esta string é montada a partir de partes estáticas e dinâmicas,
com espaço em branco extra entre as partes.
Para evitar um erro neste teste devido ao espaço em branco, uma expressão regular mais flexível é
usada.
O próximo passo é confirmar a conta, que apresenta outro pequeno obstáculo. A URL de confirmação
é enviada ao usuário por e-mail durante o registro, portanto, não há uma maneira fácil de acessá-la a
partir do teste. A solução apresentada no exemplo ignora o token que foi gerado como parte do
cadastro e gera outro diretamente da instância User . Outra possibilidade seria extrair o token
analisando o corpo do e-mail, que o Flask-Mail salva ao executar em uma configuração de teste.
Com o token em mãos, a terceira parte do teste é simular o usuário clicando na URL do token de
confirmação. Isso é obtido enviando uma solicitação GET para a URL de confirmação com o token
anexado. A resposta a esta solicitação é um redirecionamento para a página inicial, mas mais uma
vez follow_redirects=True foi especificado, portanto, o cliente de teste solicita a página redirecionada
automaticamente. A resposta é verificada para a saudação e uma mensagem instantânea que informa
ao usuário que a confirmação foi bem-sucedida.
A etapa final desse teste é enviar uma solicitação GET para a rota de logoff; para confirmar se isso
funcionou, o teste procura uma mensagem piscante na resposta.
cliente de teste Flask também pode ser usado para testar serviços da Web RESTful. O Exemplo 15-5 mostra um
exemplo de classe de teste de unidade com dois testes.
Exemplo 15-5. testes/ test_api.py: teste de API RESTful com o cliente de teste Flask
classe APITestCase(unittest.TestCase):
# ...
def get_api_headers(self, username, password): return
def test_no_auth(self):
resposta = self.client.get(url_for('api.get_posts'),
content_type='application/json')
self.assertTrue(response.status_code == 401)
def test_posts(self): #
adiciona um usuário
r = Role.query.filter_by(name='User').first()
self.assertIsNotNone(r) u =
User(email='[email protected]', password='cat', confirmado=True, role= r)
db.session.add(u)
db.session.commit()
# escreva uma
resposta de postagem =
self.client.post( url_for('api.new_post'),
headers=self.get_auth_header('[email protected]', 'cat'), data=json.dumps({'body' : 'corpo da postagem do blog
self.assertTrue(response.status_code == 201) url =
response.headers.get('Location')
self.assertIsNotNone(url)
# obtenha a nova
resposta da postagem =
self.assertTrue(response.status_code == 200)
json_response = json.loads(response.data.decode('utf-8'))
self.assertTrue(json_response['url'] == url)
self.assertTrue(json_response[ 'body'] == 'corpo da postagem do *blog*')
self.assertTrue(json_response['body_html'] ==
'<p>corpo da postagem do <em>blog</em></p>')
Os métodos setUp() e tearDown() para testar a API são os mesmos do aplicativo regular, mas o suporte
a cookies não precisa ser configurado porque a API não o utiliza. O método get_api_headers() é um
método auxiliar que retorna os cabeçalhos comuns que precisam ser enviados com todas as solicitações.
Isso inclui as credenciais de autenticação e os cabeçalhos relacionados ao tipo MIME. A maioria dos
testes precisa enviar esses cabeçalhos.
O teste test_no_auth() é um teste simples que garante que uma solicitação que não inclua credenciais
de autenticação seja rejeitada com o código de erro 401. O teste test_posts() adiciona um usuário ao
banco de dados e usa a API RESTful para inserir uma postagem de blog e então leia de volta. Quaisquer
solicitações que enviem dados no corpo devem codificá-los com json.dumps(), porque o cliente de teste
Flask não codifica automaticamente para JSON. Da mesma forma, os corpos de resposta também são
retornados no formato JSON e devem ser decodificados com json.loads() antes de serem inspecionados.
Quando os testes exigem o ambiente completo, não há outra opção a não ser usar um navegador da
Web real conectado ao aplicativo em execução em um servidor da Web real. Felizmente, a maioria dos
navegadores da web pode ser automatizada. Selênio é uma ferramenta de automação de navegador da
web que oferece suporte aos navegadores da web mais populares nos três principais sistemas operacionais.
O teste com o Selenium requer que o aplicativo esteja em execução em um servidor da Web que esteja
ouvindo solicitações HTTP reais. O método que será mostrado nesta seção inicia a aplicação com o
servidor de desenvolvimento em segundo plano enquanto os testes são executados
no fio principal. Sob o controle dos testes, o Selenium inicia um navegador da Web e faz com que ele
se conecte ao aplicativo para executar as operações necessárias. Um problema com
essa abordagem é que, após a conclusão de todos os testes, o servidor Flask precisa ser interrompido
forma, para que as tarefas em segundo plano, como o mecanismo de cobertura de código, possam
concluir seu trabalho de forma limpa. O servidor da Web Werkzeug tem uma opção de desligamento,
mas como o servidor está sendo executado isolado em seu próprio thread, a única maneira de pedir
ao servidor para desligar é enviando uma solicitação HTTP regular. O Exemplo 15-6 mostra a
implementação de uma rota de desligamento do servidor.
abort(500)
shutdown()
return 'Desligando...'
A rota de desligamento funcionará apenas quando o aplicativo estiver sendo executado no modo de
teste; invocá-lo em outras configurações falhará. O procedimento de desligamento real envolve a
chamada de uma função de desligamento que Werkzeug expõe no ambiente. Depois de chamar essa
função e retornar da solicitação, o servidor da Web de desenvolvimento saberá que precisa sair
normalmente.
O Exemplo 15-7 mostra o layout de um caso de teste configurado para executar testes com Selenium.
@classmethod
def setUpClass(cls): #
inicia o Firefox try:
@classmethod
def tearDownClass(cls): if
cls.client: # pare
o servidor flask e o navegador cls.client.get('http://
localhost:5000/shutdown') cls.client.close()
# destrói o banco de
dados
db.drop_all() db.session.remove()
def setUp(self): se
não for self.client:
self.skipTest(' Navegador da Web não disponível')
O método setUp() executado antes de cada teste ignora os testes se o Selenium não puder iniciar o
navegador da web no método startUpClass() . No Exemplo 15-8, você pode ver um exemplo de teste
construído com Selenium.
class SeleniumTestCase(unittest.TestCase):
# ...
def test_admin_home_page(self): #
navega para a página inicial
self.client.get('http://localhost:5000/')
self.assertTrue(re.search('Hello,\s+Stranger!',
self.client.page_source))
# login
self.client.find_element_by_name('email').\
send_keys('[email protected]')
self.client.find_element_by_name('senha').send_keys('cat')
self.client.find_element_by_name('enviar').click()
self.assertTrue(re.search('Olá,\s+john!', self .client.page_source))
Este teste efetua login no aplicativo usando a conta de administrador que foi criada em setUpClass() e,
em seguida, abre a página de perfil. Observe como a metodologia de teste é diferente do cliente de teste
Flask. Ao testar com o Selenium, os testes enviam comandos para o navegador da Web e nunca
interagem diretamente com o aplicativo. Os comandos correspondem às ações que um usuário real
executaria com o mouse ou teclado.
O teste começa com uma chamada para get() com a página inicial do aplicativo. No navegador, isso faz
com que a URL seja inserida na barra de endereço. Para verificar esta etapa, a origem da página é
verificada quanto à mensagem “Hello, Stranger!” saudações.
Para ir para a página de login, o teste procura o link “Log In” usando find_element_by_link_text() e, em
seguida, chama click() nele para acionar um clique real em
Para efetuar login no aplicativo, o teste localiza os campos de formulário de e-mail e senha por
seus nomes usando find_element_by_name() e, em seguida, escreve texto neles com
send_keys(). O formulário é enviado chamando click() no botão enviar. A saudação personalizada
é verificada para garantir que o login foi bem-sucedido e o navegador agora está na página
inicial.
A parte final do teste localiza o link “Perfil” na barra de navegação e clica nele. Para verificar se
a página de perfil foi carregada, o cabeçalho com o nome de usuário é pesquisado na origem
da página.
Vale a pena?
Até agora você deve estar se perguntando se o teste usando o cliente de teste Flask ou
Selenium realmente vale a pena. É uma pergunta válida e não tem uma resposta simples.
Quer você goste ou não, seu aplicativo será testado. Se você não testar você mesmo, seus
usuários se tornarão os testadores relutantes que encontrarão os bugs e, então, você terá que
corrigir os bugs sob pressão. Testes simples e focados como os que exercitam modelos de
banco de dados e outras partes da aplicação que podem ser executadas fora do contexto de
uma aplicação devem ser sempre utilizados, pois possuem um custo baixíssimo e garantem o
bom funcionamento das peças centrais do lógica do aplicativo.
Às vezes são necessários testes end-to-end do tipo que o cliente de teste Flask e o Selenium
podem realizar, mas devido à maior complexidade para escrevê-los, eles devem ser usados
apenas para funcionalidades que não podem ser testadas isoladamente. O código do aplicativo
deve ser organizado de forma que seja possível inserir o máximo possível da lógica de negócios
em modelos de banco de dados ou outras classes auxiliares que sejam independentes do
contexto do aplicativo e, portanto, possam ser facilmente testadas. O código que existe nas
funções de exibição deve ser simples e apenas agir como uma camada fina que aceita
solicitações e invoca as ações correspondentes em outras classes ou funções que encapsulam a lógica do aplicativ
Então, sim, o teste vale a pena. Mas é importante projetar uma estratégia de teste eficiente e
escrever um código que possa aproveitá-la.
CAPÍTULO 16
Desempenho
Ninguém gosta de aplicativos lentos. Longas esperas para carregar as páginas frustram os usuários, por isso é
importante detectar e corrigir problemas de desempenho assim que eles aparecem. Neste capítulo, dois aspectos
importantes de desempenho de aplicativos da Web são considerados.
Quando o desempenho do aplicativo degenera lentamente com o tempo, provavelmente é devido a consultas de
banco de dados lentas, que pioram à medida que o tamanho do banco de dados aumenta. A otimização de consultas
ao banco de dados pode ser tão simples quanto adicionar mais índices ou tão complexa quanto adicionar um cache
entre o aplicativo e o banco de dados. A instrução de explicação , disponível na maioria das linguagens de consulta
de banco de dados, mostra as etapas que o banco de dados executa para executar uma determinada consulta,
geralmente expondo ineficiências no design do banco de dados ou do índice.
Mas antes de começar a otimizar as consultas, é necessário determinar quais consultas valem a pena otimizar.
Durante uma solicitação típica, várias consultas ao banco de dados podem ser emitidas, portanto, muitas vezes é
difícil identificar quais de todas as consultas são as mais lentas.
Flask-SQLAlchemy tem a opção de registrar estatísticas sobre consultas de banco de dados emitidas durante uma
solicitação. No Exemplo 16-1, você pode ver como esse recurso pode ser usado para registrar consultas que são
mais lentas do que um limite configurado.
Exemplo 16-1. app/ main/ views.py: Relata consultas lentas ao banco de dados
@main.after_app_request
def after_request(resposta): para
consulta em get_debug_queries():
if query.duration >= current_app.config['FLASKY_SLOW_DB_QUERY_TIME']:
current_app.logger.warning( 'Slow
query: %s\nParameters: %s\nDuration: %fs\nContext: %s\n' % (query.statement ,
query.parameters, query.duration,
211
Machine Translated by Google
consulta.contexto))
resposta de retorno
Nesse caso, o manipulador after_app_request não modifica a resposta; ele apenas obtém os tempos de
consulta registrados pelo Flask-SQLAlchemy e registra qualquer um dos lentos.
A função get_debug_queries() retorna as consultas emitidas durante a requisição como uma lista. As
informações fornecidas para cada consulta são mostradas na Tabela 16-1.
Nome Descrição
contexto Uma string que indica o local do código-fonte onde a consulta foi emitida
O manipulador after_app_request percorre a lista e registra todas as consultas que duraram mais do que um
limite fornecido na configuração. O log é emitido no nível de aviso.
Alterar o nível para "erro" faria com que todas as ocorrências de consulta lenta também fossem enviadas por
e-mail.
configuração de classe :
# ...
SQLALCHEMY_RECORD_QUERIES =
Verdadeiro FLASKY_DB_QUERY_TIMEOUT
...#
= 0,5
Sempre que uma consulta lenta for detectada, uma entrada será gravada no registrador de aplicativos do Flask.
Para poder armazenar essas entradas de log, o logger deve ser configurado. A configuração de registro
depende em grande parte da plataforma que hospeda o aplicativo. Alguns exemplos são mostrados no Capítulo
17.
Se você clonou o repositório Git do aplicativo no GitHub, pode executar git checkout
16a para verificar esta versão do aplicativo.
Outra possível fonte de problemas de desempenho é o alto consumo de CPU, causado por funções que
executam computação pesada. Os criadores de perfil de código-fonte são úteis para localizar as partes mais
lentas de um aplicativo. Um criador de perfil observa um aplicativo em execução e registra as funções que são
chamadas e quanto tempo cada uma leva para ser executada. Em seguida, produz um relatório detalhado
mostrando as funções mais lentas.
O servidor web de desenvolvimento do Flask, que vem de Werkzeug, pode habilitar opcionalmente o criador de
perfil do Python para cada solicitação. O Exemplo 16-3 adiciona uma nova opção de linha de comando ao
aplicativo que inicia o criador de perfil.
@manager.command
def profile(length=25, profile_dir=None): """Inicie o
aplicativo sob o code profiler.""" from werkzeug.contrib.profiler
import ProfilerMiddleware app.wsgi_app = ProfilerMiddleware(app.wsgi_app,
restrições= [comprimento], profile_dir=profile_dir)
app.run()
Se você clonou o repositório Git do aplicativo no GitHub, pode executar git checkout
16b para verificar esta versão do aplicativo.
Quando o aplicativo for iniciado com o perfil python manage.py, o console mostrará as estatísticas do criador
de perfil para cada solicitação, que incluirá as 25 funções mais lentas. A opção --length pode ser usada para
alterar o número de funções mostradas no relatório.
Se a opção --profile-dir for fornecida, os dados do perfil para cada solicitação serão salvos em um arquivo no
diretório fornecido. Os arquivos de dados do criador de perfil podem ser usados para gerar relatórios mais
detalhados que incluem um gráfico de chamadas. Para obter mais informações sobre o criador de perfil do
Python, consulte a documentação oficial.
As preparações para implantação estão concluídas. O próximo capítulo lhe dará uma visão geral do que
esperar ao implantar seu aplicativo.
CAPÍTULO 17
Implantação
O servidor de desenvolvimento web que acompanha o Flask não é robusto, seguro ou eficiente o
suficiente para funcionar em um ambiente de produção. Neste capítulo, as opções de implantação para
aplicativos Flask são examinadas.
Independente do método de hospedagem utilizado, há uma série de tarefas que devem ser realizadas
quando a aplicação é instalada em um servidor de produção. O melhor exemplo é a criação ou
atualização das tabelas do banco de dados.
Ter que executar essas tarefas manualmente sempre que o aplicativo é instalado ou atualizado é
propenso a erros e demorado, portanto, em vez disso, um comando que executa todas as tarefas
necessárias pode ser adicionado ao manage.py.
O Exemplo 17-1 mostra uma implementação de comando de implantação que é apropriada para Flasky.
@manager.command
def deploy():
"""Executar tarefas de implantação."""
from flask.ext.migrate import upgrade from app.models
import Role, User
215
Machine Translated by Google
As funções invocadas por este comando foram todas criadas antes; eles são apenas invocados todos juntos.
Essas funções são todas projetadas de forma que não causem problemas se forem executadas várias vezes.
Projetar funções de atualização dessa maneira torna possível executar apenas esse comando de implantação
toda vez que uma instalação ou atualização é feita.
O depurador é uma excelente ferramenta para depurar problemas de aplicativos durante o desenvolvimento,
mas obviamente não pode ser usado em uma implantação de produção. Os erros que ocorrem na produção
são silenciados e, em vez disso, o usuário recebe uma página de erro de código 500. Mas, felizmente, os
rastreamentos de pilha desses erros não são completamente perdidos, pois o Flask os grava em um arquivo de log.
Durante a inicialização, o Flask cria uma instância da classe logging.Logger do Python e a anexa à instância
do aplicativo como app.logger. No modo de depuração, esse logger grava no console, mas no modo de
produção não há manipuladores configurados para ele por padrão, portanto, a menos que um manipulador
seja adicionado, os logs não são armazenados. As alterações no Exemplo 17-2 configuram um manipulador
de registro que envia os erros que ocorrem durante a execução no modo de produção para os e-mails do
administrador da lista configurados na configuração FLASKY_ADMIN .
classe ProduçãoConfig(Config):
# ...
@classmethod
def init_app(cls, app):
Config.init_app(aplicativo)
Lembre-se de que todas as instâncias de configuração têm um método estático init_app() que é invocado por
create_app(). Na implementação desse método para a classe ProductionConfig , o criador de logs do aplicativo é
configurado para registrar erros em um criador de logs de e-mail.
O nível de registro do registrador de e-mail é definido como logging.ERROR, portanto, apenas problemas graves
são enviados por e-mail. As mensagens registradas em níveis inferiores podem ser registradas em um arquivo,
syslog ou qualquer outro método suportado adicionando os manipuladores de registro apropriados. O método de
registro a ser usado para essas mensagens depende muito da plataforma de hospedagem.
Implantação em Nuvem
A última tendência em hospedagem de aplicativos é hospedar na “nuvem”. Essa tecnologia, formalmente
conhecida como Platform as a Service (PaaS), libera o desenvolvedor de aplicativos das tarefas mundanas de
instalação e manutenção das plataformas de hardware e software nas quais o aplicativo é executado. No modelo
PaaS, um provedor de serviços oferece uma plataforma totalmente gerenciada na qual os aplicativos podem ser
executados. O desenvolvedor do aplicativo usa ferramentas e bibliotecas do provedor para integrar o aplicativo à
plataforma. O aplicativo é então carregado nos servidores mantidos pelo provedor e geralmente é implantado em
segundos. A maioria dos provedores de PaaS oferece maneiras de “dimensionar” dinamicamente o aplicativo
adicionando ou removendo servidores conforme necessário para acompanhar o número de solicitações recebidas.
As implantações em nuvem oferecem grande flexibilidade e são relativamente simples de configurar, mas é claro
que toda essa qualidade tem um preço. O Heroku, um dos provedores de PaaS mais populares que oferece
suporte muito bom para Python, é estudado em detalhes na seção a seguir.
A Plataforma Heroku
Heroku foi um dos primeiros provedores de PaaS, estando no mercado desde 2007. A plataforma Heroku
é muito flexível e suporta uma longa lista de linguagens de programação. Para implantar um aplicativo no
Heroku, o desenvolvedor usa o Git para enviar o aplicativo para o próprio servidor Git do Heroku. No
servidor, o comando git push aciona automaticamente a instalação, configuração e implantação.
O Heroku usa unidades de computação chamadas dynos para medir o uso e cobrar pelo serviço. O tipo
mais comum de dinamômetro é o dinamômetro da web, que representa uma instância do servidor da web.
Um aplicativo pode aumentar sua capacidade de manipulação de solicitações usando mais dynos da web.
O outro tipo de dinamômetro é o dinamômetro de trabalho, que é usado para executar trabalhos em
segundo plano ou outras tarefas de suporte.
A plataforma fornece um grande número de plug-ins e complementos para bancos de dados, suporte por e-
mail e muitos outros serviços. As seções a seguir expandem alguns dos detalhes envolvidos na implantação
do Flasky no Heroku.
com o Heroku, o aplicativo deve estar hospedado em um repositório Git. Se você estiver trabalhando com
um aplicativo hospedado em um servidor Git remoto, como GitHub ou BitBucket, a clonagem do aplicativo
criará um repositório Git local perfeito para usar com o Heroku. Se o aplicativo ainda não estiver hospedado
em um repositório Git, será necessário criar um para ele em sua máquina de desenvolvimento.
Heroku Você deve criar uma conta no Heroku antes de poder usar o serviço. Você pode se inscrever e
hospedar aplicativos no nível de serviço mais baixo sem nenhum custo, portanto, essa é uma ótima
plataforma para experimentar.
maneira mais conveniente de gerenciar seus aplicativos Heroku é por meio do Heroku Toolbelt utilitários
de linha de comando. O Toolbelt é composto por dois aplicativos Heroku:
• heroku: O cliente Heroku, usado para criar e gerenciar aplicativos • capataz: Uma
Observe que, se você ainda não tiver um cliente Git instalado, o instalador do Toolbelt também instalará o
Git para você.
O utilitário do cliente Heroku precisa ter as credenciais da sua conta Heroku antes de se conectar ao
serviço. O comando heroku login cuida disso:
$ heroku login
Digite suas credenciais do Heroku.
E-mail: <seu-endereço-de-e-mail>
Senha (a digitação será oculta): <sua-senha> Carregando
chave pública ssh .../id_rsa.pub
É importante que sua chave pública SSH seja carregada no Heroku, pois
é isso que habilita o comando git push . Normalmente, o comando login
cria e carrega uma chave pública SSH automaticamente, mas o comando
heroku keys:add pode ser usado para carregar sua chave pública
separadamente do comando login ou se você precisar carregar chaves
adicionais.
Criando um aplicativo
A próxima etapa é criar um aplicativo usando o cliente Heroku. Para fazer isso, primeiro certifique-se de
que seu aplicativo está sob controle de origem do Git e, em seguida, execute o seguinte comando no
diretório de nível superior:
Os nomes de aplicativos Heroku devem ser exclusivos, portanto, encontre um nome que não seja usado
por nenhum outro aplicativo. Conforme indicado pela saída do comando create , uma vez implantado, o
aplicativo estará disponível em http:// <appname>.herokuapp.com. Nomes de domínio personalizados
também podem ser anexados ao aplicativo.
Provisionando um banco de
dados O Heroku oferece suporte a bancos de dados Postgres como um complemento. Um pequeno banco de dados de até
10.000 linhas pode ser adicionado a um aplicativo sem nenhum custo:
Configurando o
registro O registro de erros fatais por e-mail foi adicionado anteriormente, mas, além disso, é muito
importante configurar o registro de categorias de mensagem menores. Um bom exemplo desse tipo
de mensagem são os avisos para consultas lentas ao banco de dados adicionados no Capítulo 16.
Com o Heroku, os logs devem ser gravados em stdout ou stderr. A saída de registro é capturada e
tornada acessível por meio do cliente Heroku com o comando heroku logs .
A configuração de log pode ser adicionada à classe ProductionConfig em seu método estático
init_app() , mas como esse tipo de log é específico do Heroku, uma nova configuração pode ser criada
especificamente para essa plataforma, deixando o ProductionConfig como configuração base para
diferentes tipos de plataformas de produção. A classe HerokuConfig é mostrada no Exemplo 17-3.
Quando o aplicativo é executado pelo Heroku, ele precisa saber que esta é a configuração que precisa ser
usada. A instância do aplicativo criada em manage.py usa a variável de ambiente FLASK_CONFIG para
saber qual configuração usar, então essa variável precisa ser definida no ambiente Heroku. As variáveis de
ambiente são definidas usando o comando config:set do cliente Heroku :
Configurando o e-
mail O Heroku não fornece um servidor SMTP, então um servidor externo deve ser configurado.
Existem vários complementos de terceiros que integram suporte de envio de e-mail pronto para produção
com o Heroku, mas para fins de teste e avaliação é suficiente usar a configuração padrão do Gmail herdada
da classe Config base .
Como pode ser um risco de segurança incorporar credenciais de login diretamente no script, o nome de
usuário e a senha para acessar o servidor SMTP do Gmail são fornecidos como variáveis de ambiente:
de produção Heroku não fornece um servidor da web para os aplicativos que ele hospeda. Em vez disso, ele
espera que os aplicativos iniciem seus próprios servidores e escutem no número da porta definido na variável
de ambiente PORT.
O servidor web de desenvolvimento que acompanha o Flask terá um desempenho muito ruim porque não foi
projetado para ser executado em um ambiente de produção. Dois servidores web prontos para produção que
funcionam bem com aplicativos Flask são Gunicorn e uWSGI.
Para testar a configuração do Heroku localmente, é uma boa ideia instalar o servidor web no ambiente virtual.
Por exemplo, o Gunicorn é instalado da seguinte maneira:
O argumento manage:app indica o pacote ou módulo que define o aplicativo à esquerda dos dois pontos e o
nome da instância do aplicativo dentro desse pacote à direita. Observe que o Gunicorn usa a porta 8000 por
padrão, não 5000 como o Flask.
Adicionando um arquivo
O arquivo de requisitos do Heroku deve incluir todos os requisitos comuns para a versão de produção
do aplicativo, o pacote psycopg2 para habilitar o suporte ao banco de dados Postgres e o servidor web
Gunicorn.
-r requisitos/prod.txt gunicorn==18.0
psycopg2==2.5.1
Adicionando um
Procfile Heroku precisa saber qual comando usar para iniciar o aplicativo. Este comando é dado em um
arquivo especial chamado Procfile. Este arquivo deve ser incluído na pasta de nível superior do aplicativo.
O formato do Procfile é muito simples: em cada linha é dado o nome da tarefa, seguido de dois pontos e
o comando que executa a tarefa. O nome da tarefa web é especial; ela é reconhecida pelo Heroku como
a tarefa que inicia o servidor web. O Heroku fornecerá a essa tarefa uma variável de ambiente PORT
definida para a porta na qual o aplicativo precisa atender às solicitações. Por padrão, o Gunicorn respeita
a variável PORT se ela estiver definida, portanto, não há necessidade de incluí-la no comando de
inicialização.
Heroku Toolbelt inclui um segundo utilitário chamado Foreman, usado para executar o aplicativo
localmente por meio do Procfile para fins de teste. As variáveis de ambiente, como FLASK_CONFIG ,
definidas por meio do cliente Heroku, estão disponíveis apenas no Heroku
servidores, portanto, eles também devem ser definidos localmente para que o ambiente de
teste no Foreman seja semelhante. Foreman procura essas variáveis de ambiente em um arquivo
chamado .env no diretório de nível superior do aplicativo. Por exemplo, o arquivo .env pode
conter as seguintes variáveis:
FLASK_CONFIG=heroku
MAIL_USERNAME=<seu-nome de usuário>
MAIL_PASSWORD=<sua-senha>
Foreman tem várias opções, mas as duas principais são Foreman Run e Foreman Start.
O comando run pode ser usado para executar comandos arbitrários no ambiente do aplicativo
e é perfeito para executar o comando deploy que o aplicativo usa para criar o banco de dados:
É possível simular vários dinamômetros usando a opção -c . Por exemplo, o seguinte comando
inicia três web workers, cada um escutando em uma porta diferente:
(venv) $ capataz start -c web=3
cate. A única ação necessária é que o aplicativo intercepte todas as requisições enviadas para a interface
http:// e as redirecione para https://, e é isso que a extensão Flask SSLify faz.
A extensão precisa ser adicionada ao arquivo requirements.txt . O código no Exemplo 17-6 é usado para
ativar a extensão.
Exemplo 17-6. app/ __init__.py: Redirecionar todas as solicitações para HTTP seguro
def create_app(config_name):
...
# se não for app.debug e não app.testing e não app.config['SSL_DISABLE']:
de flask.ext.sslify importar SSLify sslify =
SSLify(aplicativo)
# ...
O suporte para SSL precisa ser ativado apenas no modo de produção e somente quando a plataforma for
compatível. Para facilitar a ativação e desativação do SSL, uma nova variável de configuração chamada
SSL_DISABLE foi adicionada. A classe base Config o define como True, para que o SSL não seja usado
por padrão, e a classe HerokuConfig o substitui. A implementação dessa variável de configuração é
mostrada no Exemplo 17-7.
configuração de classe :
# ...
SSL_DISABLE = Verdadeiro
classe HerokuConfig(ProductionConfig):
...
# SSL_DISABLE = bool(os.environ.get('SSL_DISABLE'))
O valor de SSL_DISABLE em HerokuConfig é obtido de uma variável de ambiente com o mesmo nome. Se
a variável de ambiente for definida para algo diferente de uma string vazia, a conversão para Boolean
retornará True, desativando o SSL. Se a variável de ambiente não existir ou estiver definida como uma
string vazia, a conversão para Boolean fornecerá um valor False . Para evitar que o SSL seja ativado ao
usar o Foreman, é necessário adicionar SSL_DISABLE=1 ao arquivo .env .
Com essas alterações, os usuários serão obrigados a usar o servidor SSL, mas há mais um detalhe que
precisa ser tratado para que o suporte seja completo. Ao usar o Heroku, os clientes não se conectam
diretamente aos aplicativos hospedados, mas a um servidor proxy reverso que redireciona as solicitações
para os aplicativos. Nesse tipo de configuração, apenas o servidor proxy é executado no modo SSL; os
aplicativos recebem todas as solicitações do servidor proxy sem SSL, pois não há necessidade de usar
segurança forte para solicitações internas à rede Heroku. Isso é um problema quando o aplicativo precisa
gerar URLs absolutas que correspondam à segurança da solicitação, porque request.is_secure sempre
será False quando um servidor proxy reverso for usado.
Os servidores proxy passam informações que descrevem a solicitação original do cliente para
os servidores da Web redirecionados por meio de cabeçalhos HTTP personalizados, portanto, é
possível determinar se o usuário está se comunicando com o aplicativo por SSL observando-os.
Werkzeug fornece um middleware WSGI que verifica os cabeçalhos personalizados do servidor
proxy e atualiza o objeto de solicitação de forma que, por exemplo, request.is_secure reflita a
segurança da solicitação que o cliente enviou ao servidor proxy reverso e não a solicitação que
o servidor proxy enviado para o aplicativo.
O Exemplo 17-8 mostra como adicionar o middleware ProxyFix ao aplicativo.
classe HerokuConfig(ProductionConfig):
...
# @classmethod
def init_app(cls, app):
# ...
etapa final do processo é carregar o aplicativo nos servidores Heroku. Certifique-se de que todas
as alterações sejam confirmadas no repositório Git local e, em seguida, use git push heroku
master para carregar o aplicativo no controle remoto do heroku :
Para [email protected]:<appname>.git *
[nova ramificação] mestre -> mestre
O aplicativo agora está implantado e em execução, mas não funcionará corretamente porque o comando
de implantação não foi executado. O cliente Heroku pode executar este comando da seguinte forma:
Depois que as tabelas do banco de dados são criadas e configuradas, o aplicativo pode ser reiniciado
para iniciar corretamente:
$ heroku restart
Reiniciando dynos... feito
Revisando logs A
saída de log gerada pelo aplicativo é capturada pelo Heroku. Para visualizar o conteúdo do log, use o
comando logs :
$ logs heroku
Durante o teste, também pode ser conveniente seguir o arquivo de log, o que pode ser feito da seguinte maneira:
$ heroku logs -t
Quando um aplicativo Heroku precisa ser atualizado, o mesmo processo precisa ser repetido.
Após todas as alterações terem sido confirmadas no repositório Git, os seguintes comandos executam uma
atualização:
$ heroku maintenance:on $
git push heroku master $
heroku run python manage.py deploy $ heroku
restart $ heroku
maintenance:off
A opção de manutenção disponível no cliente Heroku colocará o aplicativo offline durante a atualização e
mostrará uma página estática que informa aos usuários que o site voltará em breve.
Hospedagem Tradicional
A opção de hospedagem tradicional envolve a compra ou aluguel de um servidor, físico ou virtual, e a configuração
de todos os componentes necessários. Isso normalmente é mais barato do que hospedar na nuvem, mas
obviamente muito mais trabalhoso. As seções a seguir lhe darão uma ideia do trabalho envolvido.
Configuração do servidor
Existem várias tarefas de administração que devem ser executadas no servidor antes que ele possa hospedar
aplicativos:
• Instale um servidor de banco de dados como MySQL ou Postgres. Usar um banco de dados SQLite também
é possível, mas não é recomendado para um servidor de produção devido às suas muitas limitações.
• Instale um Mail Transport Agent (MTA), como o Sendmail , para enviar e-mail aos usuários. • Instale um
servidor web pronto para produção, como Gunicorn ou uWSGI. • Adquira, instale e
configure um certificado SSL para habilitar HTTP seguro. • (Opcional, mas altamente
recomendado) Instale um servidor Web de proxy reverso front-end, como nginx ou Apache. Esse processo
servirá arquivos estáticos diretamente e encaminhará quaisquer outras solicitações para o servidor da Web
do aplicativo, ouvindo em uma porta privada no host local.
• Endurecimento do servidor. Isso agrupa várias tarefas que têm como objetivo reduzir
vulnerabilidades no servidor, como instalação de firewalls, remoção de software e serviços não
utilizados, etc.
Como não há Heroku ou Foreman para importar essas variáveis, essa tarefa precisa ser feita pelo
próprio aplicativo durante a inicialização. O bloco de código curto no Exemplo 17-9 carrega e analisa
um arquivo .env semelhante ao usado com Foreman. Esse código pode ser adicionado ao script de
ativação manage.py antes que a instância do aplicativo seja criada.
if os.path.exists('.env'):
print('Importando ambiente de .env...') for line em
open('.env'): var =
line.strip().split('=' ) if len(var) == 2:
os.environ[var[0]] =
var[1]
O arquivo .env deve conter pelo menos a variável FLASK_CONFIG que seleciona a configuração
ração a ser utilizada.
Configurando o registro
Para servidores baseados em Unix, o registro pode ser enviado ao daemon syslog . Uma nova
configuração específica para Unix pode ser criada como uma subclasse de ProductionConfig, conforme
mostrado no Exemplo 17-10.
classe UnixConfig(ProductionConfig):
@classmethod
def init_app(cls, app):
ProductionConfig.init_app(app)
# log to syslog
import logging
from logging.handlers import SysLogHandler
syslog_handler = SysLogHandler()
syslog_handler.setLevel(logging.WARNING)
app.logger.addHandler(syslog_handler)
Com esta configuração, os logs do aplicativo serão gravados em /var/ log/ messages. O serviço
syslog pode ser configurado para gravar um arquivo de log separado ou enviar os logs para uma
máquina diferente, se necessário.
CAPÍTULO 18
Recursos adicionais
Você acabou de ler este livro. Parabéns! Espero que os tópicos que abordei tenham fornecido a
você uma base sólida para começar a criar seus próprios aplicativos com o Flask.
Os exemplos de código são de código aberto e têm uma licença permissiva, então você pode usar
tanto do meu código quanto quiser para semear seus projetos, mesmo que sejam de natureza
comercial. Neste breve capítulo final, quero fornecer uma lista de dicas e recursos adicionais que
podem ser úteis à medida que você continua trabalhando com o Flask.
• PyCharm: IDE comercial da JetBrains com edições Community (gratuita) e Professional (paga),
ambas compatíveis com aplicações Flask. Disponível em Linux, Mac OS X e Windows.
• PyDev: IDE de código aberto baseado no Eclipse. Disponível para Linux, Mac OS X e
Janelas.
• Ferramentas Python para Visual Studio: IDE gratuito criado como uma extensão do ambiente
Visual Studio da Microsoft. Somente para Microsoft Windows.
231
Machine Translated by Google
• Sessão Flask-KV: Implementação alternativa de sessões de usuário que usam o lado do servidor
armazenar
Se a funcionalidade de que você precisa para o seu projeto não estiver coberta por nenhuma das
extensões e pacotes mencionados neste livro, seu primeiro destino para procurar por extensões
adicionais deve ser o Flask Extension Registry oficial. Outros bons lugares para pesquisar são o
Python Package Index, GitHub, e BitBucket.
Fale sobre seu trabalho com seus colegas em reuniões ou conferências de grupos de usuários. •
Contribuir com correções de bugs ou melhorias nos pacotes que você usa. • Escrever
novas extensões Flask e lançá-las como código aberto. • Libere seus aplicativos
Espero que você decida ser voluntário de uma dessas maneiras ou de qualquer outra que seja significativa
para você. Se você fizer isso, obrigado!
Índice
decoradores, 115
Símbolos
arquivo .env, 222, 228
E
e-mail, 221
de versão, 178 F
autenticação, 181, 184 Flask, 3
funções abort, 16, 180
add_url_route function, 14
C
after_app_request hook, 211 application
cloud,
factory function, 78 app_errorhandler
cobertura de código 217 ,
decorator, 80, 180 before_app_request hook, 107
configuração 197 , 211, 216, 228
before_request hook, 15, 183 blueprints,
79, 92, 179 configuration object, 78
D context processadores, 63,
tabela de 116 contextos, 12, 84 cookies,
associação de banco de 161 variável de contexto current_app,
13, 84 argumento de
dados, 150 filtro de consulta
depuração, 9
filter_by, 159 filtro de
consulta de rotas dinâmicas, 8 decorador de tratamento de
junção, 159 junções, 158 migrações, 64 erro, 29, 79, 80, 188
depuração, 216
Gostaríamos de ouvir suas sugestões para melhorar nossos índices. Envie um e-mail para [email protected].
235
Machine Translated by Google
envio assíncrono, 72 G
Configuração do Gmail, 69
Git, xiii, 218, 225
Frasco-Migrar, 64
Gunicórnio, 221, 222
Flask-Moment, 33
método de formato, 34
método fromNow, 34 método H
lang, 35 Heroku, 218
Flask-Script, 17 Cliente Heroku, 218
Flask-SQLAlchemy, 52 Cinto de ferramentas Heroku, 218
adicionar método de sessão, 58, 60 Códigos de status HTTP, 180
opções de coluna, 55 HTTPie, 192
tipos de coluna, 54
método create_all, 58
236 | Índice
Machine Translated by Google
pip, 6
plataforma como serviço (PaaS), 217 postar/
Integrei ambientes de desenvolvimento (IDEs),
redirecionar/obter padrão, 44
231
Procfile, 222
é perigoso, 104, 184
código-fonte de criação de perfil,
213 servidores proxy, 225
J
EM
M
teste de unidade, 83
P
EM
paginação, 191
Interface de Gateway do Servidor Web (WSGI), 7
segurança de senha, hash, 90
Ferramenta, 3, 90, 213, 216
desempenho, 213
Middleware ProxyFix WSGI, 225
permissões, 112
Índice | 237
Machine Translated by Google
Sobre o autor
Miguel Grinberg tem mais de 25 anos de experiência como engenheiro de software. No trabalho, ele lidera uma
equipe de engenheiros que desenvolve software de vídeo para a indústria de transmissão. Ele tem um blog (http://
blog.miguelgrinberg.com) onde escreve sobre uma variedade de tópicos, incluindo desenvolvimento web, robótica,
fotografia e críticas ocasionais de filmes. Ele mora em Portland, Oregon com sua esposa, quatro filhos, dois
cachorros e um gato.
Colophon O
animal na capa da Flask Web Development é um Mastim dos Pirinéus (uma raça de Canis lupus familiaris). Esses
cães espanhóis gigantes são descendentes de um antigo cão guardião de gado chamado Molossus, que foi criado
pelos gregos e romanos e agora está extinto. No entanto, sabe-se que esse ancestral desempenhou um papel na
criação de muitas raças comuns hoje em dia, como Rottweiler, Dogue Alemão, Terra Nova e Cane Corso. Os
Mastins dos Pirineus só foram reconhecidos como uma raça pura desde 1977, e o Pyrenean Mastiff Club of
America está trabalhando para promover esses cães como animais de estimação nos Estados Unidos.
Após a Guerra Civil Espanhola, a população de Mastins dos Pirineus em sua terra natal despencou, e a raça só
sobreviveu devido ao trabalho dedicado de alguns criadores dispersos por todo o país. O pool genético moderno
dos Pirinéus deriva dessa população do pós-guerra, tornando-os propensos a doenças genéticas como a displasia
da anca.
Hoje, proprietários responsáveis certificam-se de que seus cães sejam testados para doenças e radiografados
para procurar anormalidades no quadril antes de serem criados.
Mastins dos Pirinéus adultos podem atingir mais de 200 libras quando totalmente crescidos, portanto, possuir este
cão requer um compromisso com um bom treinamento e muito tempo ao ar livre.
Apesar do seu tamanho e história como caçadores de ursos e lobos, o Pirinéu tem um temperamento muito calmo
e é um excelente cão de família. Eles podem ser confiados para cuidar das crianças e proteger a casa, ao mesmo
tempo em que são dóceis com outros cães.
Com uma socialização adequada e uma forte liderança, os Mastins dos Pirineus prosperam em um ambiente
doméstico e são excelentes guardiões e companheiros.
A imagem da capa é da Wood's Animate Creation. As fontes da capa são URW Typewriter e Guardian Sans. A
fonte do texto é Adobe Minion Pro; a fonte do cabeçalho é Adobe Myriad Condensed; e a fonte do código é Ubuntu
Mono de Dalton Maag.