0% acharam este documento útil (0 voto)
24 visualizações258 páginas

Flask Web Development Developing

Enviado por

Dayane Rodrigues
Direitos autorais
© © All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
0% acharam este documento útil (0 voto)
24 visualizações258 páginas

Flask Web Development Developing

Enviado por

Dayane Rodrigues
Direitos autorais
© © All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
Você está na página 1/ 258

Machine Translated by Google

Machine Translated by Google


Machine Translated by Google

Desenvolvimento Web em Flask

Miguel Grinberg
Machine Translated by Google

Flask Web Development


por Miguel Grinberg

Copyright © 2014 Miguel Grinberg. Todos os direitos reservados.

Impresso nos Estados Unidos da América.

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].

Editores: Meghan Blanchette e Rachel Roumeliotis Designer de capa: Randy Comer


Editor de Produção: Nicole Shelby Designer de Interiores: David Futato

Editora de texto: Nancy Kotary Ilustradora: Rebeca Demarest


Revisor: Charles Roumeliotis

Maio de 2014: Primeira edição

Histórico de revisão para a primeira edição:

25/04/2014: Primeiro lançamento

Consulte http:// oreilly.com/ catalog/ errata.csp?isbn=9781449372620 para detalhes do lançamento.

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

Parte I. Introdução ao Flask

1. Instalação. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Usando Ambientes Virtuais 4
Instalando pacotes Python com pip 6

2. Estrutura básica do aplicativo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7


Inicialização 7
Rotas e Funções de Visualização 8
Inicialização do servidor 9
Uma Aplicação Completa 9
O ciclo de solicitação-resposta 12
Contextos de aplicativos e solicitações 12
Pedido de Despacho 14
Ganchos de solicitação 14
Respostas 15
Extensões de balão 16
Opções de linha de comando com Flask-Script 17

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

Proteção contra falsificação de solicitação entre sites (CSRF) 37


Classes de formulário 38

Renderização HTML de Formulários 40

Manipulação de formulários em funções de visualização 41


Redirecionamentos e Sessões de Usuário 44

Mensagem piscando 46

5. Bancos de dados. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49

Bancos de dados 49

SQL Bancos de dados 50

NoSQL SQL ou NoSQL? 51

Estruturas de banco de dados Python 51

Gerenciamento de banco de dados com Flask-SQLAlchemy 52


Relacionamentos de 54

definição de 56

modelo Operações de 57

banco de dados Criar as 58

tabelas Inserir linhas 58

Modificar linhas Excluir 60

linhas Consultar 60

linhas 60
Uso de banco de dados em funções de exibição 62

Integração com Python Shell 63

Migrações de banco de dados com Flask-Migrate 64

Criando um repositório de migração 64

Criando um script de migração 65

Atualizando o banco de dados 66

6. E-mail. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69

Suporte por e-mail com Flask-Mail 69

Enviando e-mail do Python Shell 70

Integração de e-mails com o aplicativo 71

Envio de e-mail assíncrono 72

7. Grande estrutura de aplicativos. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75


Pacote de aplicativos 75

de opções de configuração da 76

estrutura do projeto 78

nós
| Índice
Machine Translated by Google

Usando uma fábrica de aplicativos 78

Implementando a Funcionalidade do Aplicativo em um Blueprint 79

Script de inicialização 81

Arquivo de Requisitos 82
Testes de unidade 83

Configuração do banco de dados


85

Parte II. Exemplo: um aplicativo de blog social

8. Autenticação do usuário. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
Extensões de autenticação para Flask Password 89

Security Hashing de 90

senhas com Werkzeug Criando um blueprint de 90

autenticação Autenticação de usuário com Flask- 92

Login Preparando o modelo de usuário para logins 94

Protegendo rotas Adicionando um formulário de 94

login Fazendo login de 95

usuários Fazendo login de 96

usuários Testando 97

logins Novo registro de 99

usuário Adicionando 99

um formulário de registro de 100

usuário Cadastrando um novo Usuários 100


102
Confirmação de conta 103

Gerando tokens de confirmação com seus perigosos 103

Envio de e-mails de confirmação 105

Gerenciamento de contas 109

9. Funções do usuário. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111

Banco de Dados Representação de Funções 111

Atribuição de Funções 113


Verificação de Funções 114

10. Perfis de usuário. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119


Informação do Perfil 119

Página de perfil do usuário 120


Editor de perfil 122
Editor de perfil de nível de usuário 122
Editor de perfil de nível de administrador 124

Índice | vii
Machine Translated by Google

Avatares de usuário 127

11. Postagens de blog. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131


Envio de postagem de blog e exibição de 131

postagens de blog em páginas de 134

perfil Paginação de longas listas de postagem 135

de blog Criação de dados falsos de 135

postagem de blog Renderização de 137

dados em páginas Adicionar um widget de paginação 138

Postagens Rich-Text com Markdown e Flask-PageDown 141

Usando Flask-PageDown 141

Manipulando Rich Text no Servidor 143

Links permanentes para postagens de blog 145

Editor de postagens do blog 146

12. Seguidores. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149


Relacionamentos de banco de dados revisitados 149

Relacionamentos muitos-para-muitos 150

Relacionamentos autorreferenciais 151

Relacionamentos avançados de muitos para muitos 152

Seguidores na página de perfil 155

Consultar postagens seguidas usando uma junção de banco de dados 158

Mostrar postagens seguidas na página inicial 160

13. Comentários do usuário. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165


Representação de banco de dados de comentários 165

Envio de comentários e exibição Moderação 167


de comentários 169

14. Interfaces de Programação de Aplicativos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175


Introdução aos recursos 175

REST são tudo Métodos de 176

solicitação Corpos de 177

solicitação e resposta Criação de 177

versão de 178
serviços da Web RESTful com Flask 179

Criação de um esquema de API 179

Tratamento de erros 180


Autenticação do usuário com Flask-HTTPAuth 181
Autenticação baseada em token 184

Serialização de recursos de e para JSON 186

Implementação de endpoints de recursos 188

viii | Índice
Machine Translated by Google

Paginação de grandes coleções de recursos 191


Testando Web Services com HTTPie 192

Parte III. a última milha

15. Teste. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197


Obtendo Relatórios de Cobertura de 197
Código O Flask Test 200
Client Testando Aplicações 200
Web Testando Serviços 204
Web Testando End-to-End com Selenium 205
Vale a pena? 209

16. Desempenho. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211


Registro de desempenho lento do banco de dados 211
Criação de perfil do código-fonte 213

17. Implantação. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215


Fluxo de trabalho de 215
implantação Registro de erros durante a 216
implantação da nuvem 217
de produção A plataforma 218
Heroku Preparação do 218
aplicativo Teste com 222
Foreman Habilitação de HTTP seguro com Flask- 223
SSLify Implantação com git 225
push Revisão de 226
logs Implantação de um 227
upgrade Configuração 227
tradicional do 227
servidor de hospedagem Importação de 228
variáveis de ambiente Configuração do registro 228

18. Recursos Adicionais. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231


Usando um Ambiente de Desenvolvimento Integrado (IDE) 231
Encontrando Extensões Flask 232
Envolva-se com o Flask 232

Í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

Para quem é este livro

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 .

Como Este Livro Está Organizado


Este livro está dividido em três partes:

A Parte I, Introdução ao Flask, explora os fundamentos do desenvolvimento de aplicativos da Web


com a estrutura Flask e algumas de suas extensões:

• O Capítulo 1 descreve a instalação e configuração do framework Flask. • O Capítulo

2 mergulha direto no Flask com um aplicativo básico. • O Capítulo 3

apresenta o uso de modelos em aplicativos Flask. • O Capítulo 4 apresenta

os formulários da web. • O Capítulo 5

apresenta os bancos de dados.

xii | Prefácio
Machine Translated by Google

• O Capítulo 6 apresenta o suporte por e-mail. • O

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:

• O Capítulo 8 implementa um sistema de autenticação do usuário. • O

Capítulo 9 implementa as funções e permissões do usuário. • O

Capítulo 10 implementa as páginas de perfil do usuário. •

O Capítulo 11 cria a interface de blogging. • O Capítulo

12 implementa seguidores. • O Capítulo 13

implementa comentários de usuários para postagens de blog. • O Capítulo

14 implementa uma interface de programação de aplicativo (API).

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 Capítulo 15 descreve em detalhes diferentes estratégias de teste de unidade. •

O Capítulo 16 oferece uma visão geral das técnicas de análise de desempenho. • O

Capítulo 17 descreve as opções de implantação para aplicativos Flask, tanto tradicionais


e baseado em nuvem.

• O Capítulo 18 lista recursos adicionais.

Como trabalhar com o código de exemplo


Os exemplos de código apresentados neste livro estão disponíveis no GitHub em https:// github.com/ miguelgrinberg/
flasky.

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:

$ git clone https://github.com/miguelgrinberg/flasky.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

O 1a referenciado no comando é um tag, um ponto nomeado no histórico do projeto.


Este repositório é marcado de acordo com os capítulos do livro, então a tag 1a usada no exemplo define
os arquivos do aplicativo para a versão inicial usada no Capítulo 1. A maioria dos capítulos tem mais de
uma tag associada a eles, então, por exemplo, tags 5a, 5b e assim por diante são versões incrementais
apresentadas no Capítulo 5.

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 :

$ git reset --hard

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:

$ git fetch --all $ git


fetch --tags $ git reset
--hard origin/master

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

Usando exemplos de código


Este livro está aqui para ajudá-lo a realizar seu trabalho. Em geral, se um código de exemplo for oferecido
com este livro, você poderá usá-lo em seus programas e documentação. Você não precisa entrar em
contato conosco para obter permissão, a menos que esteja reproduzindo uma parte significativa do código.
Por exemplo, escrever um programa que usa vários blocos de código deste livro não requer permissão. A
venda ou distribuição de um CD-ROM com exemplos dos livros da O'Reilly requer permissão. Responder
a uma pergunta citando este livro e citando um código de exemplo não requer permissão. Incorporar uma
quantidade significativa de código de exemplo deste livro na documentação do seu produto requer
permissão.

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].

Convenções utilizadas neste livro

As seguintes convenções tipográficas são usadas neste livro:


Itálico
Indica novos termos, URLs, endereços de e-mail, nomes de arquivo e extensões de arquivo.

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

Itálico de largura constante


Mostra o texto que deve ser substituído por valores fornecidos pelo usuário ou por valores
determinados pelo contexto.

Este elemento significa uma dica ou sugestão.

Este elemento significa uma nota geral.

Este elemento indica um aviso ou cuidado.

Safari® Livros Online


Safari Livros On-line é uma biblioteca digital sob demanda que
oferece conteúdo especializado em forma de livro e vídeo dos
principais autores do mundo em tecnologia e negócios.

Profissionais de tecnologia, desenvolvedores de software, web designers e profissionais de


negócios e criativos usam o Safari Books Online como seu principal recurso para pesquisa,
resolução de problemas, aprendizado e treinamento de certificação.

Safari Books Online oferece uma variedade de combinações de produtos e programas de


precificação para organizações , agências governamentais, e indivíduos. Os assinantes têm acesso
a milhares de livros, vídeos de treinamento e manuscritos de pré-publicação em um banco de
dados totalmente pesquisável de editoras como O'Reilly Media, Prentice Hall Professional, Addison-
Wesley Professional, Microsoft Press, Sams, Que, Peachpit Press, Focal Press , Cisco Press,
John Wiley & Sons, Syngress, Morgan Kaufmann, IBM Redbooks, Packt, Adobe Press, FT Press,
Apress, Manning, New Riders, McGraw-Hill, Jones & Bartlett, Course Technology e dezenas de
outros . Para obter mais informações sobre o Safari Books Online, visite-nos online.

XVI | Prefácio
Machine Translated by Google

Como entrar em contato conosco

Envie comentários e perguntas sobre este livro para o editor:

O'Reilly Media, Inc.


1005 Gravenstein Highway North
Sebastopol, CA 95472
800-998-9938 (nos Estados Unidos ou Canadá)
707-829-0515 (internacional ou local)
707-829-0104 (fax)

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.

Encontre-nos no Facebook: http:// facebook.com/ oreilly

Siga-nos no Twitter: http:// twitter.com/ oreillymedia Assista-

nos no YouTube: http:// www.youtube.com/ oreillymedia

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.

Para concluir, gostaria de agradecer imensamente à incrível comunidade Flask.

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.

Flask tem duas dependências principais. Os subsistemas de roteamento, depuração e Web


Server Gateway Interface (WSGI) vêm de Werkzeug, enquanto o suporte a modelos é fornecido
por Jinja2. Werkzeug e Jinja2 são de autoria do desenvolvedor principal do Flask.

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.

Os exemplos de código neste livro foram verificados para funcionar


com Python 2.7 e Python 3.3, então usar uma dessas duas versões
é altamente recomendado.

3
Machine Translated by Google

Usando Ambientes Virtuais


A maneira mais conveniente de instalar o Flask é usar um ambiente virtual. Um ambiente virtual é uma
cópia privada do interpretador Python na qual você pode instalar pacotes de forma privada, sem afetar
o interpretador Python global instalado em seu sistema.

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

Se você receber um erro, terá que instalar o utilitário.

O Python 3.3 adiciona suporte nativo de ambientes virtuais por meio


do módulo venv e do comando pyvenv. pyvenv pode ser usado em
vez de virtualenv, mas observe que ambientes virtuais criados com
pyvenv no Python 3.3 não incluem pip, que precisa ser instalado
manualmente. Essa limitação foi removida no Python 3.4, onde o
pyvenv pode ser usado como um substituto completo do virtualenv.

A maioria das distribuições Linux fornece um pacote para virtualenv. Por exemplo, os usuários do
Ubuntu podem instalá-lo com este comando:

$ sudo apt-get install python-virtualenv

Se você estiver usando o Mac OS X, poderá instalar o virtualenv usando o easy_install:

$ sudo easy_install virtualenv

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

Os comandos anteriores devem ser emitidos de uma conta com direitos de


administrador. No Microsoft Windows, inicie a janela do prompt de comando
usando a opção “Executar como administrador”. Em sistemas baseados
em Unix, os dois comandos de instalação devem ser precedidos por sudo
ou executados como usuário root. Uma vez instalado, o utilitário virtualenv
pode ser invocado a partir de contas normais.

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

exemplo”, a maneira mais conveniente de fazer isso é verificando o código diretamente do


GitHub usando um cliente Git. Os comandos a seguir baixam o código de exemplo do GitHub e
inicializam a pasta do aplicativo na versão “1a”, a versão inicial do aplicativo:

$ git clone https://github.com/miguelgrinberg/flasky.git $ cd flasky $ git


checkout 1a

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

Se você estiver usando o Microsoft Windows, o comando de ativação é:

$ venv\Scripts\ativar

Quando um ambiente virtual é ativado, a localização de seu interpretador Python é adicionada


ao PATH, mas essa alteração não é permanente; afeta apenas sua sessão de comando atual.
Para lembrá-lo de que você ativou um ambiente virtual, o comando de ativação modifica o
prompt de comando para incluir o nome do ambiente:

(venv) $

Usando Ambientes Virtuais | 5


Machine Translated by Google

Quando terminar de trabalhar com o ambiente virtual e quiser retornar ao interpretador


Python global, digite deactivate no prompt de comando.

Instalando pacotes Python com pip


A maioria dos pacotes Python é instalada com o utilitário pip , que o virtualenv adiciona
automaticamente a todos os ambientes virtuais na criação. Quando um ambiente virtual é
ativado, o local do utilitário pip é adicionado ao PATH.

Se você criou o ambiente virtual com pyvenv no Python 3.3, o pip


deve ser instalado manualmente. As instruções de instalação estão
disponíveis no site do pip. No Python 3.4, o pyvenv instala o pip
automaticamente.

Para instalar o Flask no ambiente virtual, use o seguinte comando:

(venv) frasco de instalação $ pip

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:

(venv) $ python >>>


frasco de importação
>>>

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

Estrutura básica do aplicativo

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:

from flask import Flask


app = Flask(__name__)

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.

O argumento name que é passado para o construtor da aplicação Flask é


uma fonte de confusão entre os novos desenvolvedores Flask. Flask usa
esse argumento para determinar o caminho raiz do aplicativo para que mais
tarde ele possa encontrar arquivos de recursos relativos ao local do aplicativo.

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

Rotas e Funções de Visualização

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>'

Os decoradores são um recurso padrão da linguagem Python; eles podem


modificar o comportamento de uma função de maneiras diferentes. Um padrão
comum é usar decoradores para registrar funções como manipuladores para um evento.

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.

As strings de resposta embutidas no código Python levam a um código que é


difícil de manter, e isso é feito aqui apenas para introduzir o conceito de
respostas. Você aprenderá a maneira correta de gerar respostas no Capítulo 3.

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:

8 | Capítulo 2: Estrutura básica do aplicativo


Machine Translated by Google

@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.

O servidor web fornecido pela Flask não se destina ao uso em produção.


Você aprenderá sobre servidores Web de produção no Capítulo 17.

Uma Aplicação Completa


Nas seções anteriores, você aprendeu sobre as diferentes partes de um aplicativo web Flask e
agora é hora de escrever um. Todo o script do aplicativo hello.py nada mais é do que

Inicialização do servidor | 9
Machine Translated by Google

do que as três partes descritas anteriormente combinadas em um único arquivo. A aplicação é


mostrada no Exemplo 2-1.

Exemplo 2-1. hello.py: Um aplicativo Flask completo

from flask import Flask app


= Flask(__name__)

@app.route('/')
def index():
return '<h1>Hello World!</h1>'

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

Se você clonou o repositório Git do aplicativo no GitHub, agora


pode executar git checkout 2a para verificar esta versão do aplicativo.

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.

Figura 2-1. aplicativo de balão hello.py

Em seguida, inicie o aplicativo com o seguinte comando:

10 | Capítulo 2: Estrutura básica do aplicativo


Machine Translated by Google

(venv) $ python hello.py *


Executando em http://127.0.0.1:5000/ *
Reiniciando com o recarregador

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.

Exemplo 2-2. hello.py: Aplicação Flask com uma rota dinâmica


from flask import Flask app
= Flask(__name__)

@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)

Se você clonou o repositório Git do aplicativo no GitHub, agora


pode executar git checkout 2b para verificar esta versão do aplicativo.

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.

Uma Aplicação Completa | 11


Machine Translated by Google

Figura 2-2. Rota Dinâmica

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.

Contextos de aplicativos e solicitações


Quando o Flask recebe uma requisição de um cliente, ele precisa disponibilizar alguns objetos para a
função view que irá tratá-la. Um bom exemplo é o objeto request, que encapsula a requisição HTTP
enviada pelo cliente.

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:

da solicitação de importação de frasco

@app.route('/')
def index():

12 | Capítulo 2: Estrutura básica do aplicativo


Machine Translated by Google

user_agent = request.headers.get('User-Agent') return


'<p>Seu navegador é %s</p>' % user_agent

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.

Uma thread é a menor sequência de instruções que podem ser gerenciadas


independentemente. É comum que um processo tenha vários threads
ativos, às vezes compartilhando recursos como memória ou manipuladores
de arquivos. Servidores web multithread iniciam um pool de threads e
selecionam um thread do pool para lidar com cada requisição recebida.

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.

Tabela 2-1. Contexto global do balão

Nome variável Contexto Descrição

current_app Contexto do aplicativo A instância do aplicativo para o aplicativo ativo.

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.

A seguinte sessão de shell do Python demonstra como o contexto do aplicativo funciona:

>>> from hello import app


>>> from flask import current_app >>>
current_app.name
Traceback (última chamada mais recente):
...
RuntimeError: trabalhando fora do contexto do aplicativo

O ciclo de solicitação-resposta | 13
Machine Translated by Google

>>> app_ctx = app.app_context() >>>


app_ctx.push() >>>
current_app.name 'olá'
>>>
app_ctx.pop()

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.

14 | Capítulo 2: Estrutura básica do aplicativo


Machine Translated by Google

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.

• before_request: Registre uma função para executar antes de cada solicitação. •

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:

da importação de frasco make_response

@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:

do redirecionamento de importação de frasco

@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:

da importação do frasco abortar

@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.

16 | Capítulo 2: Estrutura básica do aplicativo


Machine Translated by Google

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.

Opções de linha de comando com Flask-Script O

servidor web de desenvolvimento do Flask oferece suporte a várias opções de configuração de


inicialização, mas a única maneira de especificá-las é passando-as como argumentos para a chamada
app.run() no script. Isso não é muito conveniente; a maneira ideal de passar opções de configuração
é por meio de 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.

A extensão é instalada com pip:

(venv) $ pip install flask-script

O exemplo 2-3 mostra as alterações necessárias para adicionar análise de linha de comando ao
aplicativo hello.py .

Exemplo 2-3. hello.py: Usando Flask-Script


from flask.ext.script import Manager
manager = Manager(app)

# ...

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

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 2c para verificar esta versão do aplicativo.

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()

argumentos opcionais: -h,


--help mostrar esta mensagem de ajuda e sair

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:

(venv) $ python hello.py runserver --help using: hello.py


runserver [-h] [-t HOST] [-p PORT] [--threaded]
[--processos PROCESSOS] [--passthrough-errors] [-d] [-r]

Executa o servidor de desenvolvimento do Flask, ou seja, app.run()

argumentos opcionais: -h,


--help -t mostrar esta mensagem de ajuda e sair
HOST, --host HOST -p
PORTA, --port PORTA --
threaded --
processes PROCESSOS --
passthrough-errors -d, --no-
debug -r, -- sem
recarga

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:

18 | Capítulo 2: Estrutura básica do aplicativo


Machine Translated by Google

(venv) $ python hello.py runserver --host 0.0.0.0 * Executando


em http://0.0.0.0:5000/ * Reiniciando com
o recarregador

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.

A mistura de lógica de negócios e apresentação leva a um código difícil de entender e manter.


Imagine ter que construir o código HTML para uma tabela grande concatenando dados obtidos do
banco de dados com os literais de string HTML necessários. Mover a lógica de apresentação para
modelos ajuda a melhorar a capacidade de manutenção do aplicativo.

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

O mecanismo de modelo Jinja2


Em sua forma mais simples, um modelo Jinja2 é um arquivo que contém o texto de uma resposta.
O Exemplo 3-1 mostra um modelo Jinja2 que corresponde à resposta da função de visualização
index() do Exemplo 2-1.

Exemplo 3-1. templates/ index.html: modelo Jinja2


<h1>Olá mundo!</h1>

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.

Exemplo 3-2. templates/ user.html: template Jinja2


<h1>Olá, {{ nome }}!</h1>

Modelos de renderização Por

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.

Exemplo 3-3. hello.py: renderizando um modelo


from flask import Flask, render_template

# ...

@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.

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 3a para verificar esta versão do aplicativo.

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:

<p>Um valor de um dicionário: {{ mydict['key'] } }.</p> <p>Um


valor de uma lista: {{ mylist[3] }}.</p> <p>Um valor de
uma lista, com um índice variável: {{ mylist[myintvar] }}.</p> <p>Um valor do método de
um objeto: {{ myobj.somemethod() }}.</p>

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.

Tabela 3-1. Filtros variáveis Jinja2


Nome do filtro Descrição

seguro Renderiza o valor sem aplicar escape

capitalize Converte o primeiro caractere do valor em maiúsculo e o restante em minúsculo

mais baixo Converte o valor em caracteres minúsculos

superior Converte o valor em caracteres maiúsculos

título Capitaliza cada palavra no valor

aparar Remove espaços em branco iniciais e finais do valor

striptags Remove quaisquer tags HTML do valor antes de renderizar

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

O mecanismo de modelo Jinja2 | 23


Machine Translated by Google

irá renderizar a string como '&lt;h1&gt;Hello&lt;/h1&gt;', 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.

A lista completa de filtros pode ser obtida na documentação oficial do Jinja2.

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:

{% importar 'macros.html' como macros %}


<ul>

24 | Capítulo 3: Modelos
Machine Translated by Google

{% for comment in comments %}


{{ macros.render_comment(comentário) }} {% endfor
%} </ul>

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:

{% extends "base.html" %} {% block


title %}Índice{% endblock %} {% block head %}
{{ super() }} <style> </
style> {% endblock
%} {%
block body
%} <h1>Olá, mundo!
</h1> {% endblock %}

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 mecanismo de modelo Jinja2 | 25


Machine Translated by Google

Integração do Twitter Bootstrap com Flask-Bootstrap


Bootstrap é uma estrutura de código aberto do Twitter que fornece componentes de interface do
usuário para criar páginas da web limpas e atraentes que são compatíveis com todos os navegadores
da web modernos.

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:

(venv) $ pip install flask-bootstrap

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.

Exemplo 3-4. hello.py: Inicialização Flask-Bootstrap

from flask.ext.bootstrap import Bootstrap #


...
bootstrap = Bootstrap(app)

Assim como Flask-Script no Capítulo 2, Flask-Bootstrap é importado do namespace flask.ext e inicializado


passando a instância do aplicativo no construtor.

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.

Exemplo 3-5. templates/ user.html: Template que usa Flask-Bootstrap

{% estende "bootstrap/base.html" %}

{% block title %}Flasky{% endblock %}

{% 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 %}

{% block content %} <div


class="container"> <div
class="page-header"> <h1>Olá,
{{ name }}!</h1> </div> </div> {%
endblock
%}

A diretiva de extensões Jinja2 implementa a herança do modelo referenciando bootstrap/ base.html


de Flask-Bootstrap. O modelo base do Flask-Bootstrap fornece uma página da Web de esqueleto
que inclui todos os arquivos CSS e JavaScript do Bootstrap.

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.

Se você clonou o repositório Git do aplicativo no GitHub, pode executar git


checkout 3b para verificar esta versão do aplicativo.
A documentação oficial do Bootstrap é um ótimo recurso de aprendizado cheio de
exemplos prontos para copiar/colar.

Integração do Twitter Bootstrap com Flask-Bootstrap | 27


Machine Translated by Google

Figura 3-1. Modelos do Twitter Bootstrap

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.

Tabela 3-2. Blocos de modelo base do Flask-Bootstrap

Nome do bloco Descrição

documento
Todo o documento HTML

Atributos html_attribs dentro da tag <html>

html O conteúdo da tag <html>

cabeça O conteúdo da tag <head>

título O conteúdo da tag <title>

metas A lista de tags <meta>

estilos Definições de folha de estilo em cascata

body_attribs Atributos dentro da tag <body>

corpo O conteúdo da tag <body>

barra de navegação Barra de navegação definida pelo usuário

contente Conteúdo da página definido pelo usuário

scripts Declarações de JavaScript na parte inferior do documento

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 %}

Páginas de erro personalizadas

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.

Exemplo 3-6. hello.py: páginas de erro personalizadas

@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.

Páginas de erro personalizadas | 29


Machine Translated by Google

Exemplo 3-7. templates/ base.html: Modelo base de aplicativo com barra de navegação
{% estende "bootstrap/base.html" %}

{% block title %}Flasky{% endblock %}

{% 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"> </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 %}

{% block content %} <div


class="container"> {% block
page_content %}{% endblock %} </div> {% endblock
%}

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.

Os modelos do aplicativo agora herdarão deste modelo em vez de diretamente do Flask-Bootstrap. O


exemplo 3-8 mostra como é simples construir uma página de erro 404 de código personalizado herdada
de templates/ base.html.

Exemplo 3-8. templates/ 404.html: Página de erro 404 de código personalizado usando modelo herdado
tância

{% estende "base.html" %}

{% block title %}Flasky - Página não encontrada{% endblock %}

{% block page_content %} <div


class="page-header"> <h1>Não
encontrado</h1>

30 | Capítulo 3: Modelos
Machine Translated by Google

</div>
{% endblock %}

A Figura 3-2 mostra a aparência da página de erro no navegador.

Figura 3-2. Página de erro de código personalizado 404

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 title %}Flasky{% endblock %}

{% block page_content %}
<div class="page-header">
<h1>Olá, {{ name }}!</h1> </div>
{%
endblock %}

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 3c para verificar esta versão do aplicativo.

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 relativas são suficientes para gerar links que conectam as


diferentes rotas do aplicativo. URLs absolutos são necessários
apenas para links que serão usados fora do navegador da web,
como ao enviar links por e-mail.

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.

Os argumentos de palavra-chave enviados para url_for() não estão limitados a argumentos


usados por rotas dinâmicas. A função adicionará quaisquer argumentos extras à string de
consulta. Por exemplo, url_for('index', page=2) retornaria /?page=2.

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.

Exemplo 3-10. templates/ base.html: definição de favicon

{% 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.

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 3d para verificar esta versão do aplicativo.

Localização de Datas e Horas com Flask-Moment


A manipulação de datas e horas em um aplicativo da Web não é um problema trivial quando os usuários
trabalham em diferentes partes do mundo.

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:

(venv) $ pip install flask-moment

A extensão é inicializada conforme mostrado no Exemplo 3-11.

Localização de Datas e Horários com Flask-Moment | 33


Machine Translated by Google

Exemplo 3-11. hello.py: Inicializar Flask-Moment

from flask.ext.moment import Moment


moment = Moment(app)

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.

Exemplo 3-12. templates/ base.html: Importar biblioteca moment.js

{% 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.

Exemplo 3-13. hello.py: Adicione uma variável de data e hora

de data e hora importar data e hora

@app.route('/')
def index():
return render_template('index.html',
current_time=datetime.utcnow())

O exemplo 3-14 mostra como current_time é renderizado no modelo.

Exemplo 3-14. templates/ index.html: Renderização de timestamp com Flask-Moment

<p>A data e hora locais são {{ moment(current_time).format('LLL') }}.</p> <p>Isso foi


{{ moment(current_time).fromNow(refresh=True) }}< /p>

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

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 3e para verificar esta versão do aplicativo.

Flask-Moment implementa os métodos format(), fromNow(), fromTime(), calendar(), valueOf()


e unix() de moment.js. Consulte a documentação para conhecer todas as opções de
formatação oferecidas.

Flask-Moment assume que os registros de data e hora manipulados pelo


aplicativo do lado do servidor são objetos de data e hora “ingênuos” expressos
em UTC. Veja a documentação para o datetime pacote na biblioteca padrão
para obter informações sobre objetos de data e hora ingênuos e cientes.

Os timestamps renderizados pelo Flask-Moment podem ser localizados em vários idiomas.


Um idioma pode ser selecionado no modelo passando o código do idioma para a função
lang():

{{ 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.

Localização de Datas e Horários com Flask-Moment | 35


Machine Translated by Google
Machine Translated by Google

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.

Flask-WTF e suas dependências podem ser instaladas com pip:

(venv) $ pip install flask-wtf

Proteção contra falsificação de solicitação entre sites (CSRF)

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.

Exemplo 4-1. hello.py: configuração Flask-WTF

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.

Para maior segurança, a chave secreta deve ser armazenada em


uma variável de ambiente em vez de ser incorporada ao código. Esta
técnica é descrita no Capítulo 7.

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.

Exemplo 4-2. hello.py: Definição de classe de formulário

de flask.ext.wtf import Form de


wtforms import StringField, SubmitField de
wtforms.validators import Obrigatório

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.

38 | Capítulo 4: Formulários da Web


Machine Translated by Google

A classe base Form é definida pela extensão Flask-WTF, portanto é


importado de flask.ext.wtf. Os campos e validadores, no entanto,
são importados diretamente do pacote WTForms.

A lista de campos HTML padrão suportados pelo WTForms é mostrada na Tabela 4-1.

Tabela 4-1. Campos HTML padrão WTForms


Tipo de campo Descrição

Campo de texto
StringField
TextAreaField Campo de texto de várias linhas

Campo de texto da senha


PasswordField
Campo de texto oculto
Campo oculto

DataCampo Campo de texto que aceita um valor datetime.date em um determinado formato

DateTimeField Campo de texto que aceita um valor datetime.datetime em um determinado formato

Campo de texto que aceita um valor inteiro


IntegerField
DecimalField Campo de texto que aceita um valor decimal.Decimal

FloatField Campo de texto que aceita um valor de ponto flutuante

BooleanField Caixa de seleção com valores True e False

Lista de botões de rádio


RadioField

SelectField Lista suspensa de opções

SelectMultipleField Lista suspensa de opções com seleção múltipla

FileField Campo de upload de arquivo

Botão de envio de formulário


Enviar campo
Incorporar um formulário como um campo em um formulário contêiner
FormField

FieldList Lista de campos de um determinado tipo

A lista de validadores internos do WTForms é mostrada na Tabela 4-2.

Aulas de formulário | 39
Machine Translated by Google

Tabela 4-2. validadores WTForms

Validador Descrição

E-mail Valida um endereço de e-mail

EqualTo Compara os valores de dois campos; útil ao solicitar uma senha para ser digitada duas vezes para confirmação

IPAddress Valida um endereço de rede IPv4

Comprimento Valida o comprimento da string inserida

NumberRange Valida se o valor digitado está dentro de um intervalo numérico

Opcional Permite entrada vazia no campo, ignorando validadores adicionais

Obrigatório Valida se o campo contém dados

Regexp Valida a entrada em uma expressão regular

URL Valida um URL

AnyOf Valida se a entrada é uma de uma lista de valores possíveis

Nenhum Valida se a entrada não pertence a uma lista de valores possíveis

Renderização HTML de Formulários

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:

{% importa "bootstrap/wtf.html" como wtf %}


{{ wtf.quick_form(form) }}

40 | Capítulo 4: Formulários da Web


Machine Translated by Google

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.

Exemplo 4-3. templates/ index.html: Usando Flask-WTF e Flask-Bootstrap para renderizar um


formulário

{% extends "base.html" %} {%
import "bootstrap/wtf.html" como wtf %}

{% block title %}Flasky{% endblock %}

{% 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() .

Manipulação de formulários em funções de visualização

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 .

Exemplo 4-4. hello.py: Métodos de rota

@app.route('/', method=['GET', 'POST']) def


index():
nome = Nenhum
form = NameForm()
if form.validate_on_submit(): name
= form.name.data
form.name.data = ''
return render_template('index.html', form=form, name=name)

Manipulação de formulários em funções de exibição | 41


Machine Translated by Google

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

NameForm mostrada anteriormente para representar o formulário. O método valid_on_submit() do formulário


retorna True quando o formulário foi enviado e os dados foram aceitos por todos os validadores de campo.
Em todos os outros casos, valid_on_submit() retorna False. O valor de retorno desse método serve
efetivamente para decidir se o formulário precisa ser renderizado ou processado.

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.

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 4a para verificar esta versão do aplicativo.

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

42 | Capítulo 4: Formulários da Web


Machine Translated by Google

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.

Figura 4-1. Formulário web Flask-WTF

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.

Figura 4-2. Formulário da Web após o envio

Manipulação de formulários em funções de exibição | 43


Machine Translated by Google

Figura 4-3. Formulário da Web após falha no validador

Redirecionamentos e Sessões de Usuário

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

44 | Capítulo 4: Formulários da Web


Machine Translated by Google

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.

Por padrão, as sessões do usuário são armazenadas em cookies do lado do


cliente que são assinados criptograficamente usando a SECRET_KEY
configurada. Qualquer adulteração do conteúdo do cookie tornaria a
assinatura inválida, invalidando assim a sessã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.

Exemplo 4-5. hello.py: Redirecionamentos e sessões de usuário

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

@app.route('/', method=['GET', 'POST']) def


index(): form
= NameForm() if
form.validate_on_submit():
session['name'] = form.name.data return
redirect(url_for('index')) return
render_template('index.html', form=form, name=session.get('name'))

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.

Redirecionamentos e Sessões de Usuário | 45


Machine Translated by Google

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.

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 4b para verificar esta versão do aplicativo.

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.

Exemplo 4-6. hello.py: mensagens instantâneas


from flask import Flask, render_template, session, redirect, url_for, flash

@app.route('/', method=['GET', 'POST']) def


index(): form
= NameForm() if
form.validate_on_submit():
old_name = session.get('name') se
old_name não for None e old_name != form.name.data: flash('Parece
que você mudou seu nome!') session['name'] =
form.name.data form.name.data =
return ''
redirect(url_for('index'))
return render_template('index.html',
formulário = formulário, nome = sessão.get('nome'))

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.

46 | Capítulo 4: Formulários da Web


Machine Translated by Google

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.

Exemplo 4-7. templates/ base.html: renderização de mensagem Flash

{% 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">&times;</button> {{ message }}
</div> {%
endfor
%}

{% block page_content %}{% endblock %} </


div>
{% endblock %}

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).

Figura 4-4. mensagem piscada

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.

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 4c para verificar esta versão do aplicativo.

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.

48 | Capítulo 4: Formulários da Web


Machine Translated by Google

CAPÍTULO 5

bancos de dados

Um banco de dados armazena os dados do aplicativo de maneira organizada. O aplicativo emite


consultas para recuperar partes específicas conforme necessário. Os bancos de dados mais utilizados
para aplicações web são aqueles baseados no modelo relacional , também chamados de bancos de
dados SQL em referência à Linguagem de Consulta Estruturada que utilizam. Mas, nos últimos anos, os
bancos de dados orientados a documentos e de valor-chave , informalmente conhecidos como bancos
de dados NoSQL, tornaram-se alternativas populares.

Bancos de Dados SQL

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

Figura 5-1. Exemplo de banco de dados relacional

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.

Bancos de dados NoSQL

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.

50 | Capítulo 5: Bancos de dados


Machine Translated by Google

Figura 5-2. Exemplo de banco de dados NoSQL

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.

Estruturas de banco de dados Python


O Python possui pacotes para a maioria dos mecanismos de banco de dados, tanto de código aberto quanto comerciais.
O Flask não impõe restrições sobre quais pacotes de banco de dados podem ser usados, portanto, você pode trabalhar
com MySQL, Postgres, SQLite, Redis, MongoDB ou CouchDB, se algum deles for o seu favorito.

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.

Gerenciamento de banco de dados com Flask-SQLAlchemy


Flask-SQLAlchemy é uma extensão Flask que simplifica o uso de SQLAlchemy dentro de aplicativos Flask.
SQLAlchemy é uma poderosa estrutura de banco de dados relacional que suporta vários back-ends de banco de
dados. Ele oferece um ORM de alto nível e acesso de baixo nível à funcionalidade SQL nativa do banco de dados.

Como a maioria das outras extensões, Flask-SQLAlchemy é instalado com pip:

(venv) $ pip install flask-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.

52 | Capítulo 5: Bancos de dados


Machine Translated by Google

Tabela 5-1. URLs do banco de dados Flask-SQLAlchemy

URL do mecanismo de banco de dados

MySQL mysql://username:password@hostname/database

Postgre postgresql://username:password@hostname/database

SQLite (Unix) sqlite:////absolute/path/to/database

SQLite (Windows) sqlite:///c:/absolute/path/to/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.

Exemplo 5-1. hello.py: Configuração do banco de dados

from flask.ext.sqlalchemy import SQLAlchemy

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.

Gerenciamento de banco de dados com Flask-SQLAlchemy | 53


Machine Translated by Google

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.

Exemplo 5-2. hello.py: Definição de função e modelo de usuário

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

nome de tabela padrão se __tablename__ for omitido, mas o padrão


nomes não seguem a convenção de usar plurais para nomes de tabelas, então é melhor
tabelas de nomes explicitamente. As variáveis de classe restantes são os atributos do modelo,
definidos como instâncias da classe db.Column .

O primeiro argumento fornecido ao construtor db.Column é o tipo da coluna do banco de dados


e atributo do modelo. A Tabela 5-2 lista alguns dos tipos de colunas disponíveis, juntamente
com o tipo Python usado no modelo.

Tabela 5-2. Tipos de coluna SQLAlchemy mais comuns

Nome do tipo Python type Descrição

inteiro int Inteiro regular, normalmente 32 bits

SmallInteger int Inteiro de curto alcance, normalmente 16 bits

Número inteiro de precisão ilimitada


BigInteger inteiro ou longo
Flutuador
flutuador Número de ponto flutuante

Numérico
decimal.Decimal número de ponto fixo

54 | Capítulo 5: Bancos de dados


Machine Translated by Google

Nome do tipo Python type Descrição

Corda String de comprimento variável


str
Texto Cadeia de comprimento variável, otimizada para comprimento grande ou não vinculado
str
Unicode String Unicode de comprimento variável
unicode
UnicodeTexto unicode Cadeia de caracteres Unicode de comprimento variável, otimizada para comprimento grande ou ilimitado

boleano valor booleano


bool
Data Valor da data
datahora.data
Tempo Valor do tempo
datahora.hora
DateTime datetime.datetime Valor de data e hora

Intervalo datetime.timedelta Intervalo de tempo

Enum Lista de valores de string


str
PickleType Qualquer objeto Python Serialização automática do Pickle

LargeBinary str blob binário

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.

Tabela 5-3. Opções de coluna SQLAlchemy mais comuns


Nome da opção Descrição

primary_key Se definido como True, a coluna é a chave primária da tabela.

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.

Defina um valor padrão para a coluna.


padrão

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.

Exemplo 5-3. hello.py: Relacionamentos

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 .

O atributo de usuários adicionado ao modelo Role representa a visão orientada a objetos do


relacionamento. Dada uma instância da classe Role, o atributo users retornará a lista de usuários
associados a esse papel. O primeiro argumento para db.relationship() indica qual modelo está do outro
lado do relacionamento. Este modelo pode ser fornecido como uma string se a classe ainda não estiver
definida.

O argumento backref para db.relationship() define a direção reversa do relacionamento adicionando


um atributo role ao modelo User . Este atributo pode ser usado em vez de role_id para acessar o
modelo de função como um objeto em vez de uma chave estrangeira.

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.

56 | Capítulo 5: Bancos de dados


Machine Translated by Google

Tabela 5-4. Opções comuns de relacionamento do SQLAlchemy

Nome da opção Descrição

referência anterior Adicione uma referência anterior no outro modelo no 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.

Especifique a ordem usada para os itens no relacionamento.


ordenar por

Especifique o nome da tabela de associação a ser usada em relacionamentos muitos-para-muitos.


secundário

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.

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 5a para verificar esta versão do aplicativo.

Existem outros tipos de relacionamento além do um-para-muitos. O relacionamento um-para-um pode


ser expresso como o um-para-muitos descrito anteriormente, mas com a opção uselist definida como
False na definição db.relationship() para que o lado “muitos” se torne um lado “um”. A relação muitos-
para-um também pode ser expressa como um-para-muitos se as tabelas forem invertidas, ou pode ser
expressa com a chave estrangeira e a definição db.relationship() ambas no lado “muitos”. O tipo de
relacionamento mais complexo, muitos para muitos, requer uma tabela adicional chamada tabela de
associação. Você aprenderá sobre relacionamentos muitos-para-muitos no Capítulo 12.

Operações de banco de dados

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.

Operações de banco de dados | 57


Machine Translated by Google

Criando as tabelas A primeira

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:

(venv) $ python hello.py shell >>>


from hello import db >>>
db.create_all()

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:

>>> from hello import Role, User >>>


admin_role = Role(name='Admin') >>>
mod_role = Role(name='Moderator') >>>
user_role = Role(name='User') >> >
user_john = User(username='john', role=admin_role) >>> user_susan
= User(username='susan', role=user_role) >>> user_david =
User(username='david', role=user_role)

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)

Nenhum >>> print(user_role.id)


Nenhum

58 | Capítulo 5: Bancos de dados


Machine Translated by Google

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)

Ou, de forma mais concisa:

>>> db.session.add_all([admin_role, mod_role, user_role, user_john,


... user_susan, user_david])

Para gravar os objetos no banco de dados, a sessão precisa ser confirmada chamando seu método commit() :

>>> db.session.commit()

Verifique os atributos id novamente; agora estão definidos:

>>> print(admin_role.id) 1

>>> print(mod_role.id)
2
>>> print(user_role.id) 3

A sessão do banco de dados db.session não está relacionada ao objeto de sessão

Flask discutido no Capítulo 4. As sessões do banco de dados também são chamadas


de transações.

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.

Uma sessão de banco de dados também pode ser revertida. Se db.session.rollback()


for chamado, todos os objetos que foram adicionados à sessão do banco de dados
serão restaurados ao estado que tinham no banco de dados.

Operações de banco de dados | 59


Machine Translated by Google

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":

>>> admin_role.name = 'Administrador' >>>


db.session.add(admin_role) >>>
db.session.commit()

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:

>>> db.session.delete(mod_role) >>>


db.session.commit()

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":

60 | Capítulo 5: Bancos de dados


Machine Translated by Google

>>> user_role = Role.query.filter_by(name='User').first()

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

lista está na documentação do SQLAlchemy.

Tabela 5-5. Filtros de consulta SQLAlchemy comuns

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.

Tabela 5-6. Executores de consulta SQLAlchemy mais comuns

Opção Descrição

todos() Retorna todos os resultados de uma consulta como uma lista

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

contagem() Retorna a contagem de resultados da consulta

paginar() Retorna um objeto Pagination que contém o intervalo especificado de resultados

Os relacionamentos funcionam de maneira semelhante às consultas. O exemplo a seguir consulta o relacionamento


um-para-muitos entre funções e usuários de ambas as extremidades:

>>> usuários = user_role.users


>>> usuários

[<Usuário u'susan'>, <Usuário u'david'>]


>>> usuários[0].role
<Função u'Usuário'>

Operações de banco de dados | 61


Machine Translated by Google

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.

Exemplo 5-4. app/ models.py: relacionamentos dinâmicos

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

Uso de banco de dados em funções de exibição

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.

Exemplo 5-5. hello.py: Uso do banco de dados em funções de visualização

@app.route('/', method=['GET', 'POST']) def


index(): form
= NameForm() if
form.validate_on_submit():
user = User.query.filter_by(username=form.name.data).first() se o usuário
for None: user =
User(username = form.name.data)
db.session.add(user)
session['known' ] = False
else:
session['known'] = True
session['name'] = form.name.data
form.name.data = ''
return redirect(url_for('index'))
return render_template('index.html',
formulário = formulário, nome = sessão.get('nome'),
conhecido = sessão.get('conhecido', Falso))

62 | Capítulo 5: Bancos de dados


Machine Translated by Google

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.

Exemplo 5-6. templates/ index.html

{% extends "base.html" %} {%
import "bootstrap/wtf.html" como wtf %}

{% block title %}Flasky{% endblock %}

{% 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 %}

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 5b para verificar esta versão do aplicativo.

Integração com Python Shell


Ter que importar a instância do banco de dados e os modelos toda vez que uma sessão de shell é iniciada é
um trabalho tedioso. Para evitar a repetição constante dessas importações, o comando shell do Flask-Script
pode ser configurado para importar automaticamente determinados objetos.

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.

Exemplo 5-7. hello.py: Adicionando um contexto de shell

de flask.ext.script import Shell

def make_shell_context():

Integração com Python Shell | 63


Machine Translated by Google

return dict(app=app, db=db, User=User, Role=Role)


manager.add_command("shell", Shell(make_context=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:

$ python hello.py shell >>>


app
<Flask 'app'>
>>> db
<SQLAlchemy engine='sqlite:////home/flask/flasky/data.sqlite'> >>> Usuário

<class 'app.User'>

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 5c para verificar esta versão do aplicativo.

Migrações de banco de dados com Flask-Migrate


À medida que avança no desenvolvimento de um aplicativo, você descobrirá que seus modelos de banco de dados
precisam ser alterados e, quando isso acontecer, o banco de dados também precisará ser atualizado.

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.

Criando um repositório de migração


Para começar, o Flask-Migrate deve ser instalado no ambiente virtual:

(venv) $ pip install flask-migrate

O Exemplo 5-8 mostra como a extensão é inicializada.

64 | Capítulo 5: Bancos de dados


Machine Translated by Google

Exemplo 5-8. hello.py: Configuração do Flask-Migrate

from flask.ext.migrate import Migrate, MigrateCommand

# ...

Migrate = Migrate(app, db)


manager.add_command('db', MigrateCommand)

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 :

(venv) $ python hello.py db init Criando


diretório /home/flask/flasky/migrations...done Criando diretório /home/
flask/flasky/migrations/versions...done Gerando /home/flask/flasky/migrations/
alembic.ini...done Gerando /home/flask/flasky/migrations/env.py...done
Gerando /home/flask/flasky/migrations/env.pyc...done Gerando /home/
flask/flasky/migrations /README...done Generating /home/flask/flasky/
migrations/script.py.mako...done Edite as configurações/conexões/
logging em '/home/flask/flasky/migrations/alembic.ini' antes de prosseguir .

Este comando cria uma pasta migrations , onde todos os scripts de migração serão armazenados.

Os arquivos em um repositório de migração de banco de dados sempre devem ser


adicionados ao controle de versão junto com o restante do aplicativo.

Criando um Script de Migração No

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.

As migrações Alembic podem ser criadas manual ou automaticamente usando os comandos


revision e migration , respectivamente. Uma migração manual cria um esqueleto de script de
migração com as funções upgrade() e downgrade() vazias que precisam ser implementadas pelo
desenvolvedor usando diretivas expostas pelo objeto Operations do Alembic . Uma migração
automática, por outro lado, gera o código para o upgrade() e

Migrações de banco de dados com Flask-Migrate | 65


Machine Translated by Google

downgrade() procurando diferenças entre as definições do modelo e o


estado atual do banco de dados.

As migrações automáticas nem sempre são precisas e podem perder alguns


detalhes. Os scripts de migração gerados automaticamente devem sempre
ser revisados.

O subcomando de migração cria um script de migração automática:

(venv) $ python hello.py db migration -m "migração inicial"


INFO [alembic.migration] Contexto impl SQLiteImpl.
INFO [alembic.migration] Irá assumir DDL não transacional.
INFO [alembic.autogenerate] Detectadas 'funções' adicionadas à tabela
INFO [alembic.autogenerate] Tabela adicionada 'users' detectada
INFO [alembic.autogenerate.compare] Detectado índice adicionado
'ix_users_username' em '['username']'
Gerando /home/flask/flasky/migrations/versions/1bc
594146bb5_initial_migration.py...concluído

Se você clonou o repositório Git do aplicativo no GitHub, pode executar git


checkout 5d para verificar esta versão do aplicativo.
Observe que você não precisa gerar as migrações para este aplicativo, pois
todos os scripts de migração estão incluídos no repositório.

Atualizando o banco de dados


Depois que um script de migração for revisado e aceito, ele poderá ser aplicado ao banco de
dados usando o comando db upgrade :

(venv) $ python hello.py atualização do


banco de dados INFO [alembic.migration] Contexto impl SQLiteImpl.
INFO [alembic.migration] Irá assumir DDL não transacional.
INFO [alembic.migration] Atualização em execução Nenhum -> 1bc594146bb5, migração inicial

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.

Se você clonou o repositório Git do aplicativo no GitHub, exclua seu arquivo de


banco de dados data.sqlite e execute o comando de atualização do Flask-
Migrate para regenerar o banco de dados por meio da estrutura de migração.

66 | Capítulo 5: Bancos de dados


Machine Translated by Google

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.

Migrações de banco de dados com Flask-Migrate | 67


Machine Translated by Google
Machine Translated by Google

CAPÍTULO 6

E-mail

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.

Suporte por e-mail com Flask-Mail


Flask-Mail é instalado com pip:

(venv) $ pip install flask-mail

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.

Tabela 6-1. Chaves de configuração do servidor SMTP Flask-Mail

Chave Descrição padrão

MAIL_HOSTNAME localhost Nome do host ou endereço IP do servidor de e-mail

25 Porta do servidor de e-mail


MAIL_PORT
MAIL_USE_TLS False Habilita a segurança da Transport Layer Security (TLS)

MAIL_USE_SSL False Ativa a segurança Secure Sockets Layer (SSL)

MAIL_USERNAME Nenhum Nome de usuário da conta de e-mail

MAIL_PASSWORD Nenhum Senha da conta de e-mail

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

Exemplo 6-1. hello.py: Configuração Flask-Mail para Gmail

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')

Nunca escreva as credenciais da conta diretamente em seus scripts,


especialmente se você planeja liberar seu trabalho como código aberto. Para
proteger as informações da sua conta, faça com que seu script importe
informações confidenciais do ambiente.

Flask-Mail é inicializado conforme mostrado no Exemplo 6-2.

Exemplo 6-2. hello.py: Inicialização Flask-Mail

from flask.ext.mail import Mail mail


= Mail(app)

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:

(venv) $ export MAIL_USERNAME=< nome de usuário do


Gmail> (venv) $ export MAIL_PASSWORD=< senha do Gmail>
Para usuários do Microsoft Windows, as variáveis de ambiente são definidas da seguinte forma:

(venv) $ set MAIL_USERNAME=< nome de usuário do


Gmail> (venv) $ set MAIL_PASSWORD=< senha do Gmail>

Enviando e-mail do Python Shell


Para testar a configuração, você pode iniciar uma sessão de shell e enviar um e-mail de teste:

(venv) $ python hello.py shell >>>


from flask.ext.mail import Message >>> from
hello import mail >>> msg =
Message('test subject', sender='[email protected]', destinatários
... =['você@exemplo.com'])
>>> msg.body = 'text body' >>>
msg.html = '<b>HTML</b> body' >>>
with app.app_context():
... mail.send(msg)
...

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.

Integrando e-mails com o aplicativo Para evitar ter que

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.

Exemplo 6-3. hello.py: suporte por e-mail

de flask.ext.mail mensagem de importação

app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin <[email protected]>'

def send_email(to, subject, template, **kwargs): msg =


Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject,
sender=app.config['FLASKY_MAIL_SENDER'], destinatários=[para])
msg.body = render_template(template + '.txt', **kwargs) msg.html =
render_template(template + '.html', **kwargs ) mail.send(msg)

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.

Exemplo 6-4. hello.py: Exemplo de e-mail


# ...
app.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMIN') #
...
@app.route('/', method=['GET', 'POST']) def
index(): form
= NameForm( ) se
form.validate_on_submit():
user = User.query.filter_by(username=form.name.data).first() se o usuário
for None: user =
User(username=form.name.data)
db.session.add(user)
session['known' ] = Falso se
app.config['FLASKY_ADMIN']:

Suporte por e-mail com Flask-Mail | 71


Machine Translated by Google

send_email(app.config['FLASKY_ADMIN'], 'Novo usuário', 'mail/


new_user', usuário=usuário)
outro:
session['known'] = True
session['name'] = form.name.data
form.name.data = ''

return redirect(url_for('index')) return


render_template('index.html', form=formulário, nome =sessão.get('nome'),
conhecido=sessão.get('conhecido', Falso))

O destinatário do e-mail é fornecido na variável de ambiente FLASKY_ADMIN carregada em uma variável de


configuração de mesmo nome durante a inicialização. Dois arquivos de modelo precisam ser criados para as
versões de texto e HTML do e-mail. Esses arquivos são armazenados em um

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.

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 6a para verificar esta versão do aplicativo.

Além das variáveis de ambiente MAIL_USERNAME e MAIL_PASSWORD descritas anteriormente, esta


versão do aplicativo precisa da variável de ambiente FLASKY_ADMIN .
Para usuários de Linux e Mac OS X, o comando para iniciar o aplicativo é:

(venv) $ export FLASKY_ADMIN=<seu-email-endereço>

Para usuários do Microsoft Windows, este é o comando equivalente:

(venv) $ set FLASKY_ADMIN=< nome de usuário do Gmail>

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.

Envio de e-mail assíncrono


Se você enviou alguns e-mails de teste, provavelmente notou que a função mail.send() bloqueia por alguns
segundos enquanto o e-mail é enviado, fazendo com que o navegador pareça não responder durante esse
tempo. Para evitar atrasos desnecessários durante o processamento da solicitação, a função de envio de e-
mail pode ser movida para um thread em segundo plano. O Exemplo 6-5 mostra essa mudança.

Exemplo 6-5. hello.py: suporte a e-mail assíncrono

de threading import Thread

def send_async_email(app, msg): com


app.app_context():

72 | Capítulo 6: E-mail
Machine Translated by Google

mail.send(msg)

def send_email(to, subject, template, **kwargs): msg =


Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject,
sender=app.config['FLASKY_MAIL_SENDER'], destinatários=[para])
msg.body = render_template(template + '.txt', **kwargs) msg.html =
render_template(template + '.html', **kwargs ) thr =
Thread(target=send_async_email, args=[app, msg]) thr.start()
return thr

Essa implementação destaca um problema interessante. Muitas extensões do Flask operam


sob a suposição de que há aplicativos ativos e contextos de solicitação. A função send() do
Flask-Mail usa current_app, então requer que o contexto do aplicativo esteja ativo.
Mas quando a função mail.send() é executada em um thread diferente, o contexto do aplicativo
precisa ser criado artificialmente usando app.app_context().

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 6b para verificar esta versão do aplicativo.

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.

Suporte por e-mail com Flask-Mail | 73


Machine Translated by Google
Machine Translated by Google

CAPÍTULO 7

Grande estrutura de aplicativos

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.

Exemplo 7-1. Estrutura básica do aplicativo Flask de vários arquivos

|-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

Essa estrutura possui quatro pastas de nível superior:

• O aplicativo Flask reside dentro de um pacote denominado genericamente app. • A pasta

migrations contém os scripts de migração do banco de dados, como antes. • Os testes de unidade

são escritos em um pacote de testes . • A pasta venv

contém o ambiente virtual Python, como antes.

Há também alguns novos arquivos:

• requirements.txt lista as dependências do pacote para que seja fácil gerar novamente um ambiente virtual
idêntico em um computador diferente.

• config.py armazena as definições de configuração. •

manage.py inicia o aplicativo e outras tarefas do aplicativo.

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 .

Exemplo 7-2. config.py: Configuração do aplicativo

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

76 | Capítulo 7: Estrutura de aplicativo grande


Machine Translated by Google

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 .

Na parte inferior do script de configuração, as diferentes configurações são registradas em um dicionário


de configuração . Uma das configurações (a de desenvolvimento neste caso) também é registrada como
padrão.

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.

Usando uma fábrica de aplicativos A forma

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.

Exemplo 7-3. app/ __init__.py: Construtor do pacote de aplicativos

from flask import Flask, render_template from


flask.ext.bootstrap import Bootstrap from
flask.ext.mail import Mail from
flask.ext.moment import Moment from
flask.ext.sqlalchemy import SQLAlchemy from
config import config

bootstrap = Bootstrap() mail


= Mail() moment
= Moment() db =
SQLAlchemy()

78 | Capítulo 7: Estrutura de aplicativo grande


Machine Translated by Google

def create_app(config_name): app


= Flask(__name__)
app.config.from_object(config[config_name])
config[config_name].init_app(app)

bootstrap.init_app(app)
mail.init_app(app)
moment.init_app(app)
db.init_app(app)

# anexe rotas e páginas de erro personalizadas aqui

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.

Implementando a Funcionalidade do Aplicativo em um Blueprint A

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.

Exemplo 7-4. app/ main/ __init__.py: Criação do projeto


do balão de importação Blueprint

principal = Projeto('principal', __nome__)

de . importar exibições, erros

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.

Exemplo 7-5. app/ _init_.py: Registro do Blueprint

def create_app(config_name):
# ...

from main import main as main_blueprint


app.register_blueprint(main_blueprint)

aplicativo de retorno

O Exemplo 7-6 mostra os manipuladores de erro.

Exemplo 7-6. app/ main/ errors.py: Blueprint com manipuladores de erros

de flask import render_template de .


importar principal

@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.

O Exemplo 7-7 mostra as rotas do aplicativo atualizadas para estarem no blueprint.

Exemplo 7-7. app/ main/ views.py: Blueprint com rotas de aplicativos

from datetime import datetime


from flask import render_template, session, redirect, url_for

80 | Capítulo 7: Estrutura de aplicativo grande


Machine Translated by Google

de . import main
from .forms import NameForm
from .. import db
from ..models import User

@main.route('/', method=['GET', 'POST']) def


index(): form
= NameForm() if
form.validate_on_submit():
...
# return redirecionamento(url_for('.index'))
return render_template('index.html',
form=form, name=session.get('name'),
known=session.get('known', False),
current_time=datetime.utcnow())

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

Exemplo 7-8. manage.py: Iniciar script

#!/ usr/ bin/ env python


import os
from app import create_app, db from
app.models import User, Role from
flask.ext.script import Manager, Shell from
flask.ext.migrate import Migrate, MigrateCommand

app = create_app(os.getenv('FLASK_CONFIG') or 'default') manager


= Manager(app) migration
= Migrate(app, db)

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()

O script começa criando um aplicativo. A configuração utilizada é retirada da variável de ambiente


FLASK_CONFIG se estiver definida; caso contrário, a configuração padrão será usada. Flask-Script,
Flask-Migrate e o contexto personalizado para o shell Python são então inicializados.

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:

(venv) $ pip freeze >requirements.txt

É 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

82 | Capítulo 7: Estrutura de aplicativo grande


Machine Translated by Google

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:

(venv) $ pip install -r requisitos.txt

Os números de versão no arquivo requirements.txt de exemplo provavelmente estarão desatualizados quando


você ler isto. Você pode tentar usar versões mais recentes dos pacotes, se quiser. Se você tiver algum
problema, pode sempre voltar para as versões especificadas no arquivo de requisitos, pois elas são compatíveis
com o aplicativo.

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.

Exemplo 7-9. testes/ test_basics.py: testes de unidade

import unittest
from flask import current_app from
app import create_app, db

class BasicsTestCase(unittest.TestCase): def


setUp(self): self.app
= create_app('testing') self.app_context
= self.app.app_context() self.app_context.push()
db.create_all()

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.

Se você clonou o repositório Git do aplicativo no GitHub, pode executar git


checkout 7a para verificar a versão convertida do aplicativo. Para garantir
que você tenha todas as dependências instaladas, execute também pip
install -r requirements.txt.

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 .

Exemplo 7-10. manage.py: comando do iniciador de teste de unidade

@manager.command
def test():
"""Execute os testes de unidade."""
importar testes
unittest = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)

O decorador manager.command simplifica a implementação de comandos personalizados. O nome da


função decorada é usado como o nome do comando e a docstring da função é exibida nas mensagens de
ajuda. A implementação da função test() invoca o executor de teste do pacote unittest .

Os testes de unidade podem ser executados da seguinte forma:

84 | Capítulo 7: Estrutura de aplicativo grande


Machine Translated by Google

(venv) $ python manage.py teste


test_app_exists (test_basics.BasicsTestCase) ... test_app_is_testing OK
(test_basics.BasicsTestCase) ... OK

.------------------------------------------------- ---------------------
Realizou 2 testes em 0,001s

OK

Configuração do banco de dados

O aplicativo reestruturado usa um banco de dados diferente da versão de script único.

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:

(venv) $ python manage.py atualização do banco de dados

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.

Configuração do banco de dados | 85


Machine Translated by Google
Machine Translated by Google

PARTE II

Exemplo: Uma Rede Social

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.

Extensões de autenticação para Flask


Existem muitos pacotes de autenticação Python excelentes, mas nenhum deles faz tudo. A solução de
autenticação de usuário apresentada neste capítulo usa vários pacotes e fornece a cola que os faz funcionar
bem juntos. Esta é a lista de pacotes que serão usados:

• Flask-Login: gerenciamento de sessões de usuário para usuários logados •

Werkzeug: hashing e verificação de senha • itsdangerous:

geração e verificação de token criptograficamente segura

Além dos pacotes específicos de autenticação, as seguintes extensões de uso geral serão usadas:

• Flask-Mail: Envio de e-mails relacionados à autenticação • Flask-

Bootstrap: templates HTML


• Flask-WTF: formulários da Web

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 hash de senha é uma tarefa complexa e difícil de acertar. É


recomendável que você não implemente sua própria solução, mas confie
em bibliotecas respeitáveis que foram revisadas pela comunidade. Se
você estiver interessado em aprender o que está envolvido na geração
de hashes de senha segura, consulte o artigo Salted Password Hashing -
Doing it Right é uma leitura que vale a pena.

Hashing de senhas com Werkzeug O módulo de

segurança de Werkzeug implementa convenientemente o hash de senha segura. Essa funcionalidade é


exposta com apenas duas funções, utilizadas nas fases de cadastro e verificação, respectivamente:

• generate_password_hash(password, method=pbkdf2:sha1, salt_length=8): Esta função


pega uma senha de texto simples e retorna o hash da senha como uma string que pode
ser armazenada no banco de dados do usuário. Os valores padrão para method e
salt_length são suficientes para a maioria dos casos de uso.

• check_password_hash(hash, password): Esta função usa um hash de senha recuperado do banco de


dados e a senha digitada pelo usuário. Um valor de retorno True indica que a senha está correta.

O Exemplo 8-1 mostra as mudanças no modelo User criado no Capítulo 5 para acomodar o hash de senha.

Exemplo 8-1. app/ models.py: hashing de senha no modelo de usuário


de werkzeug.security importar generate_password_hash, check_password_hash

90 | Capítulo 8: Autenticação do usuário


Machine Translated by Google

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)

def Verify_password(self, password): return


check_password_hash(self.password_hash, password)

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.

O método Verify_password pega uma senha e a passa para a função check_password_hash()


de Werkzeug para verificação em relação à versão hash armazenada no modelo User . Se
este método retornar True, então a senha está correta.

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 8a para verificar esta versão do aplicativo.

A funcionalidade de hash de senha agora está completa e pode ser testada no shell:

(venv) $ python manage.py shell >>> u


= User() >>>
u.password = 'cat' >>>
u.password_hash
'pbkdf2:sha1:1000$duxMk0OF$4735b293e397d6eeaf650aaf490fd9091f928bed' >>>
u.verify_password ('gato')
Verdadeiro

>>> u.verify_password('cachorro')
Falso
>>> u2 = User()
>>> u2.password = 'cat' >>>
u2.password_hash
'pbkdf2:sha1:1000$UjvnGeTP$875e28eb0874f44101d6b332442218f66975ee89'

Observe como os usuários u e u2 têm hashes de senha completamente diferentes, embora


ambos usem a mesma senha. Para garantir que essa funcionalidade continue a funcionar no

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 .

Exemplo 8-2. testes/ test_user_model.py: testes de hash de senha

import unittest
from app.models import User

class UserModelTestCase(unittest.TestCase): def


test_password_setter(self): u =
User(password = 'cat')
self.assertTrue(u.password_hash is not None)

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)

Criando um Blueprint de Autenticação


Blueprints foram introduzidos no Capítulo 7 como uma forma de definir rotas no escopo global depois
que a criação do aplicativo foi movida para uma função de fábrica. As rotas relacionadas ao sistema
de autenticação do usuário podem ser adicionadas a um blueprint de autenticação . Usar esquemas
diferentes para diferentes conjuntos de funcionalidade do aplicativo é uma ótima maneira de manter o
código bem organizado.

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.

Exemplo 8-3. app/ auth/ __init__.py: Criação do projeto

do balão de importação Blueprint

auth = Blueprint('auth', __name__)

de . importar visualizações

92 | Capítulo 8: Autenticação do usuário


Machine Translated by Google

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.

Exemplo 8-4. app/ auth/ views.py: Rotas Blueprint e funções de visualização

de flask import render_template de .


autorização de importação

@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.

Os blueprints também podem ser configurados para ter sua própria


pasta independente para modelos. Quando várias pastas de modelo
foram configuradas, a função render_template() pesquisa primeiro a
pasta de modelos configurada para o aplicativo e depois pesquisa as
pastas de modelo definidas por blueprints.

O esquema de autenticação precisa ser anexado ao aplicativo na função de fábrica create_app()


conforme mostrado no Exemplo 8-5.

Exemplo 8-5. app/ __init__.py: Anexo do projeto

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.

Criando um plano de autenticação | 93


Machine Translated by Google

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 8b para verificar esta versão do aplicativo.

Autenticação do usuário com Flask-Login


Quando os usuários fazem login no aplicativo, seu estado autenticado deve ser registrado para que seja lembrado
enquanto navegam por diferentes páginas. Flask-Login é uma extensão pequena, mas extremamente útil,
especializada em gerenciar esse aspecto específico de um sistema de autenticação de usuário, sem estar
vinculado a um mecanismo de autenticação específico.

Para começar, a extensão precisa ser instalada no ambiente virtual:

(venv) $ pip install flask-login

Preparando o modelo de usuário para logins Para

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.

Tabela 8-1. Métodos de usuário Flask-Login

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.

is_anonymous() Deve sempre retornar False para usuários regulares.

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

de flask.ext.login import UserMixin

class User(UserMixin, db.Model):


__tablename__ = 'users' id =
db.Column(db.Integer, primary_key = True) email =
db.Column(db.String(64), unique=True, index=True) username =
db.Column(db.String(64), unique=True, index=True) password_hash =
db.Column(db.String(128)) role_id =
db.Column(db.Integer, db.ForeignKey('roles .eu ia'))

94 | Capítulo 8: Autenticação do usuário


Machine Translated by Google

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.

O Flask-Login é inicializado na função de fábrica do aplicativo, conforme mostrado no Exemplo 8-7.

Exemplo 8-7. app/ __init__.py: Inicialização Flask-Login

from flask.ext.login import LoginManager

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.

Exemplo 8-8. app/ models.py: função de retorno de chamada do carregador do usuário

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.

Protegendo Rotas Para

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:

de flask.ext.login import login_required

@app.route('/secret')
@login_required
def secret():
return 'Apenas usuários autenticados são permitidos!'

Autenticação de usuário com Flask-Login | 95


Machine Translated by Google

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.

Exemplo 8-9. app/ auth/ forms.py: Formulário de login

from flask.ext.wtf import Form from


wtforms import StringField, PasswordField, BooleanField, SubmitField from
wtforms.validators import Required, Email

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

<ul class="nav navbar-nav navbar-right">


{% if current_user.is_authenticated() %} <li> <a
href="{{ url_for('auth.logout') }}"> Sair</a></li> {% else %} <li> <a

href="{{ url_for('auth.login') }}"> Entrar </a></li> {% endif %} </ul>

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.

96 | Capítulo 8: Autenticação do usuário


Machine Translated by Google

Figura 8-1. Forma de login

Conectando usuários

A implementação da função de visualização login() é mostrada no Exemplo 8-11.

Exemplo 8-11. app/ auth/ views.py: rota de login


from flask import render_template, redirect, request, url_for, flash from flask.ext.login
import login_user from . import auth
from ..models import
User from .forms import
LoginForm

@auth.route('/login', method=['GET', 'POST']) def login():


form =
LoginForm() if
form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first() se o usuário
não for None e user.verify_password(form.password.data): login_user(user,
form.remember_me.data) return
redirect( request.args.get('next') or url_for('main.index')) flash('Nome de usuário ou
senha inválido.') return render_template('auth/
login.html', form=form)

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

Autenticação de usuário com Flask-Login | 97


Machine Translated by Google

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.

Em um servidor de produção, a rota de login deve ser disponibilizada em


HTTP seguro para que os dados do formulário transmitidos ao servidor sejam enÿ
criptografado. Sem HTTP seguro, as credenciais de login podem ser
interceptadas durante o trânsito, anulando qualquer esforço para proteger
senhas no servidor.

O modelo de login precisa ser atualizado para renderizar o formulário. Essas mudanças são mostradas no
Exemplo 8-12.

Exemplo 8-12. app/ templates/ auth/ login.html: Renderizar formulário de login

{% extends "base.html" %} {%
import "bootstrap/wtf.html" como wtf %}

{% block title %}Flasky - Login{% endblock %}

{% block page_content %}
<div class="page-header">
<h1>Login</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}

98 | Capítulo 8: Autenticação do usuário


Machine Translated by Google

</div>
{% endblock %}

Desconectando usuários

A implementação da rota de logoff é mostrada no Exemplo 8-13.

Exemplo 8-13. app/ auth/ views.py: rota de saída

de flask.ext.login importar logout_user, login_required

@auth.route('/logout')
@login_required
def logout():
logout_user()
flash('Você foi desconectado.') return
redirect(url_for('main.index'))

Para desconectar um usuário, a função logout_user() do Flask-Login é chamada para remover e


redefinir a sessão do usuário. O logout é concluído com uma mensagem flash que confirma a ação e
um redirecionamento para a página inicial.

Se você clonou o repositório Git do aplicativo no GitHub, pode executar git


checkout 8c para verificar esta versão do aplicativo.
Esta atualização contém uma migração de banco de dados, então lembre-
se de executar python manage.py db upgrade depois de verificar o código.
Para garantir que você tenha todas as dependências instaladas, execute
também pip install -r requirements.txt.

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.

Exemplo 8-14. app/ templates/ index.html: Cumprimente o usuário logado


Olá,
{% if current_user.is_authenticated() %}
{{ current_user.username }} {%
else %}
Estranho
{% endif %}!

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:

Autenticação de usuário com Flask-Login | 99


Machine Translated by Google

(venv) $ python manage.py shell >>> u


= User(email='[email protected]', username='john', password='cat') >>> db.session.add(u)
> >> db.session.commit()

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.

Figura 8-2. Página inicial após login bem-sucedido

Registo de novo utilizador


Quando novos usuários desejam se tornar membros do aplicativo, eles devem se registrar para que
sejam conhecidos e possam fazer login. Um link na página de login os encaminhará para uma página
de registro, onde poderão inserir seu endereço de e-mail, nome de usuário, e senha.

Adicionando um formulário de registro de

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.

Exemplo 8-15. app/ auth/ forms.py: Formulário de registro do usuário

from flask.ext.wtf import Form from


wtforms import StringField, PasswordField, BooleanField, SubmitField from
wtforms.validators import Required, Length, Email, Regexp, EqualTo

100 | Capítulo 8: Autenticação do usuário


Machine Translated by Google

de wtforms import ValidationError


de ..models import User

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')

def valid_email(self, field): if


User.query.filter_by(email=field.data).first():
raise ValidationError('E-mail já cadastrado.')

def valid_username(self, field): if


User.query.filter_by(username=field.data).first(): raise
ValidationError('Nome de usuário já em uso.')

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.

Cadastro de Novo Usuário | 101


Machine Translated by Google

Figura 8-3. Formulário de cadastro de novo usuário

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>

Registar Novos Utilizadores A

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.

102 | Capítulo 8: Autenticação do usuário


Machine Translated by Google

Exemplo 8-17. app/ auth/ views.py: rota de registro do usuário

@auth.route('/register', method=['GET', 'POST']) def register():


form =
RegistrationForm() if
form.validate_on_submit(): user =
User(email=form.email.data , nome de
usuário=form.username.data,
senha=form.password.data)
db.session.add(user)
flash(' Agora você pode fazer
login.') return redirect(url_for('auth.login'))
return render_template( 'auth/register.html', formulário=formulário)

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 8d para verificar esta versão do aplicativo.

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.

Gerando tokens de confirmação com seus perigos O link 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

Confirmação de Conta | 103


Machine Translated by Google

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:

(venv) $ python manage.py shell >>>


from manage import app >>>
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer >>> s =
Serializer(app.config['SECRET_KEY'], expires_in = 3600) >>> token =
s.dumps({ 'confirm': 23 }) >>> token

'eyJhbGciOiJIUzI1NiIsImV4cCI6MTM4MTcxODU1OCwiaWF0IjoxMzgxNzE0OTU4fQ.ey ...' >>>


data = s.loads(token) >>> data
{u'confirm':
23}

Itsdangerous fornece vários tipos de geradores de token. Dentre elas, a classe


TimedJSONWebSignatureSerializer gera JSON Web Signatures (JWS) com tempo de expiração. O
construtor dessa classe usa uma chave de criptografia como argumento, que em um aplicativo Flask
pode ser a SECRET_KEY configurada.

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.

Exemplo 8-18. app/ models.py: confirmação da conta do usuário

from itsdangerous import TimedJSONWebSignatureSerializer as Serializer from flask


import current_app from . banco de
dados de importação

classe User(UserMixin, db.Model):


...
# confirmado = db.Column(db.Boolean, default=False)

def generate_confirmation_token(auto, expiração=3600):


s = Serializer(current_app.config['SECRET_KEY'], expiração) return
s.dumps({'confirm': self.id})

def confirm(self, token):


s = Serializer(current_app.config['SECRET_KEY']) tente:

104 | Capítulo 8: Autenticação do usuário


Machine Translated by Google

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.

Enviando e-mails de confirmação A rota

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

de ..email importar send_email

@auth.route('/register', method = ['GET', 'POST']) def register():


form =
RegistrationForm() if
form.validate_on_submit():
# ...
db.session.add(user)
db.session.commit()
token = user.generate_confirmation_token()
send_email(user.email, 'Confirme sua conta', 'auth/email/
confirm', user=usuário, token=token) flash('Um e -mail
de confirmação foi enviado para você por e-mail.')

Confirmação de Conta | 105


Machine Translated by Google

return redirect(url_for('main.index')) return


render_template('auth/register.html', form=form)

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

Prezado {{ user.username }},

Bem-vindo a Flasky!

Para confirmar sua conta, clique no seguinte link:

{{ url_for('auth.confirm', token=token, _external=True) }}

Sinceramente,

A Equipe Flasky

Observação: as respostas a este endereço de e-mail não são monitoradas.

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.

A função view que confirma as contas é mostrada no Exemplo 8-21.

Exemplo 8-21. app/ auth/ views.py: Confirme uma conta de usuário


de flask.ext.login import current_user

@auth.route('/confirm/<token>')
@login_required

106 | Capítulo 8: Autenticação do usuário


Machine Translated by Google

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

confirmação é inválido ou expirou.') return redirect(url_for('main.index'))

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')

Confirmação de Conta | 107


Machine Translated by Google

O manipulador before_app_request interceptará uma solicitação quando três condições forem


verdadeiro:

1. Um usuário está logado (current_user.is_authenticated() deve retornar True).


2. A conta do usuário não está confirmada.

3. O terminal solicitado (acessível como request.endpoint) está fora do esquema de autenticação.


O acesso às rotas de autenticação precisa ser concedido, pois são as rotas que permitirão ao
usuário confirmar a conta ou executar outras funções de gerenciamento de conta.

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.

Quando um retorno de chamada before_request ou before_app_request retorna


uma resposta ou um redirecionamento, o Flask envia isso para o cliente sem
invocar a função de exibição associada à solicitação. Isso efetivamente permite
que esses retornos de chamada interceptem uma solicitação quando necessário.

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.

Exemplo 8-23. app/ auth/ views.py: Reenviar e-mail de confirmação da conta


@auth.route('/confirm')
@login_required
def resend_confirmation():
token = current_user.generate_confirmation_token()
send_email('auth/email/confirm',
'Confirme sua conta', usuário, token=token) flash('Um
novo e-mail de confirmação foi enviado para você por e-mail.') return
redirect(url_for ('main.index'))

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.

Se você clonou o repositório Git do aplicativo no GitHub, pode executar git


checkout 8e para verificar esta versão do aplicativo.
Esta atualização contém uma migração de banco de dados, então lembre-se de
executar python manage.py db upgrade depois de verificar o código.

108 | Capítulo 8: Autenticação do usuário


Machine Translated by Google

Figura 8-4. Página de conta não confirmada

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

Gestão de contas | 109


Machine Translated by Google

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.

110 | Capítulo 8: Autenticação do usuário


Machine Translated by Google

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.

Há várias maneiras de implementar funções em um aplicativo. O método apropriado depende em grande


parte de quantas funções precisam ser suportadas e de quão elaboradas elas são.
Por exemplo, um aplicativo simples pode precisar de apenas duas funções, uma para usuários comuns e
outra para administradores. Neste caso, ter um campo booleano is_administrator no modelo User pode
ser tudo o que é necessário. Um aplicativo mais complexo pode precisar de funções adicionais com
níveis variados de poder entre usuários regulares e administradores.
Em algumas aplicações, pode nem fazer sentido falar sobre funções discretas; em vez disso, fornecer
aos usuários uma combinação de permissões pode ser a abordagem correta.

A implementação da função do usuário apresentada neste capítulo é um híbrido entre funções e


permissões discretas. Os usuários recebem uma função discreta, mas as funções são definidas em
termos de permissões.

Representação de funções do banco de dados

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.

Exemplo 9-1. app/ models.py: permissões de função

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 segunda adição ao modelo é o campo de permissões , que é um número inteiro que


ser usados como sinalizadores de bits. A cada tarefa será atribuída uma posição de bit e, para cada função, as tarefas
que são permitidos para essa função terão seus bits definidos como 1.

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.

Tabela 9-1. Permissões do aplicativo

Nome da tarefa valor de bits Descrição

Seguir usuários 0b00000001 (0x01) Siga outros usuários

Comente sobre postagens feitas por outros 0b00000010 (0x02) Comente sobre artigos escritos por outros

Escrever artigos 0b00000100 (0x04) Escrever artigos originais

Moderar comentários feitos por outros 0b00001000 (0x08) Suprimir comentários ofensivos feitos por outros

Acesso administrativo 0b10000000 (0x80) Acesso administrativo ao site

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.

A representação de código da Tabela 9-1 é mostrada no Exemplo 9-2.

Exemplo 9-2. app/ models.py: Constantes de permissão

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.

Tabela 9-2. Funções do usuário

Papel do usuário Permissões Descrição

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.

112 | Capítulo 9: Funções do usuário


Machine Translated by Google

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.

Exemplo 9-3. app/ models.py: Cria funções no banco de dados

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:

(venv) $ python manage.py shell >>>


Role.insert_roles()
>>> Role.query.all()
[<Função u'Administrador'>, <Função u'Usuário'>, <Função u'Moderador'>]

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

Atribuição de Função | 113


Machine Translated by Google

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

classe User(UserMixin, db.Model):


# ...
def __init__(self, **kwargs):
super(User, self).__init__(**kwargs) if
self.role for None:
if self.email == current_app.config['FLASKY_ADMIN']:
self.role = Role.query.filter_by(permissions=0xff).first()
se self.role for Nenhum:
self.role = Role.query.filter_by(padrão=True).first()
# ...

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

de flask.ext.login import UserMixin, AnonymousUserMixin

classe User(UserMixin, db.Model):


# ...

def can(self, permissions): return


self.role não é None e \ (self.role.permissions
& permissions) == permissions

def is_administrator(self): return


self.can(Permission.ADMINISTER)

class AnonymousUser(AnonymousUserMixin):
def can(self, permissions): return
False

def is_administrator(self): return


False

114 | Capítulo 9: Funções do usuário


Machine Translated by Google

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

from functools import wraps


from flask import abort
from flask.ext.login import current_user

def permission_required(permission): def


decorador(f):
@wraps(f)
def decorado_function(*args, **kwargs): se não
current_user.can(permission): abort(403)
return
f(*args, **kwargs )
função de retorno decorada_função
de retorno do decorador

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.

A seguir estão dois exemplos que demonstram o uso desses decoradores:

Verificação de função | 115


Machine Translated by Google

de decoradores import admin_required, permission_required

@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.

Exemplo 9-8. testes/ test_user_model.py: testes de unidade para funções e permissões

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))

Se você clonou o repositório Git do aplicativo no GitHub, pode executar git


checkout 9a para verificar esta versão do aplicativo.
Esta atualização contém uma migração de banco de dados, então lembre-se
de executar python manage.py db upgrade depois de verificar o código.

116 | Capítulo 9: Funções do usuário


Machine Translated by Google

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.

Verificação de função | 117


Machine Translated by Google
Machine Translated by Google

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.

Exemplo 10-1. app/ models.py: campos de informações do usuário

classe User(UserMixin, db.Model):


# ...
name = db.Column(db.String(64))
location = db.Column(db.String(64))
about_me = db.Column(db.Text())
member_since = db.Column(db.DateTime(), default=datetime.utcnow) last_seen
= db.Column(db.DateTime(), default=datetime.utcnow)

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 é

mostrado no Exemplo 10-2.

Exemplo 10-2. app/ models.py: atualize o horário da última visita de um usuário

classe User(UserMixin, db.Model):


# ...

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.

Exemplo 10-3. app/ auth/ views.py: Ping usuário logado

@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'))

Página de perfil do usuário

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.

Exemplo 10-4. app/ main/ views.py: rota da página de perfil

@main.route('/user/<username>') def
user(username): user
= User.query.filter_by(username=username).first() if user is None:
abort(404) return

render_template('user .html', usuário=usuário)

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.

120 | Capítulo 10: Perfis de Usuário


Machine Translated by Google

Exemplo 10-5. app/ templates/ user.html: modelo de perfil de usuário

{% 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 %}

Este modelo tem alguns detalhes de implementação interessantes:

• 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.

Exemplo 10-6. app/ templates/ base.html

{% if current_user.is_authenticated() %} <li> <a

href="{{ url_for('main.user', username=current_user.username) }}">


Perfil </
a> </
li> {%
endif %}

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.

Página de perfil do usuário | 121


Machine Translated by Google

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.

Se você clonou o repositório Git do aplicativo no GitHub, pode executar git


checkout 10a para verificar esta versão do aplicativo.
Esta atualização contém uma migração de banco de dados, então lembre-se
de executar python manage.py db upgrade depois de verificar o código.

Figura 10-1. Página de perfil do usuário

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.

Editor de perfil de nível de usuário

O formulário de edição de perfil para usuários regulares é mostrado no Exemplo 10-7.

122 | Capítulo 10: Perfis de Usuário


Machine Translated by Google

Exemplo 10-7. app/ main/ forms.py: Formulário de edição de perfil

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.

Exemplo 10-8. app/ main/ views.py: rota de edição de perfil

@main.route('/edit-profile', method=['GET', 'POST']) @login_required


def edit_profile():
form = EditProfileForm()
if form.validate_on_submit():
current_user.name = form.name .data
current_user.location = form.location.data
current_user.about_me = form.about_me.data
db.session.add(user) flash('Seu perfil foi atualizado.')
return
redirect(url_for('.user', username
=current_user.username))
form.name.data = current_user.name
form.location.data = current_user.location
form.about_me.data = current_user.about_me return
render_template('edit_profile.html', form=form)

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.

Exemplo 10-9. app/ templates/ user.html: Link de edição do perfil

{% if user == current_user %} <a


class="btn btn-default" href="{{ url_for('.edit_profile') }}"> Editar perfil </a> {% endif
%}

A condicional que inclui o link fará com que o link apareça apenas quando os usuários estiverem visualizando
seus próprios perfis.

Editor de perfil | 123


Machine Translated by Google

Figura 10-2. Editor de perfil

Editor de perfil de nível de administrador

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')

124 | Capítulo 10: Perfis de Usuário


Machine Translated by Google

def __init__(self, user, *args, **kwargs):


super(EditProfileAdminForm, self).__init__(*args, **kwargs) self.role.choices
= [(role.id, role.name) para função em
Role.query.order_by(Role.name).all()]
self.usuario = usuario

def valid_email(self, field): if field.data !


= self.user.email e \
User.query.filter_by(email=field.data).first(): raise
ValidationError('Email já registrado.')

def valid_username(self, field): if field.data !


= self.user.username e \
User.query.filter_by(username=field.data).first():
raise ValidationError('Nome de usuário já em uso.')

O SelectField é o wrapper do WTForm para o controle de formulário HTML <select> , que


implementa uma lista suspensa, usada neste formulário para selecionar uma função de usuário.
Uma instância de SelectField deve ter os itens definidos em seu atributo choice . Eles devem ser
fornecidos como uma lista de tuplas, com cada tupla consistindo de dois valores: um identificador
para o item e o texto a ser mostrado no controle como uma string. A lista de opções é definida no
construtor do formulário, com valores obtidos do modelo Role com uma consulta que classifica
todas as funções alfabeticamente por nome. O identificador para cada tupla é definido como o id
de cada função e, como são inteiros, um argumento coerce=int é adicionado ao construtor
SelectField para que os valores do campo sejam armazenados como inteiros em vez do padrão, que são strings.

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.

A definição de rota para o editor de perfil do administrador é mostrada no Exemplo 10-11.

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

Editor de perfil | 125


Machine Translated by Google

user.role = Role.query.get(form.role.data) user.name


= form.name.data user.location
= form.location.data
user.about_me = form.about_me.data
db.session.add(user)
flash('O perfil foi atualizado.') return
redirect(url_for('.user', username=user.username))
form.email.data = usuario.email
form.username.data = user.username
form.confirmed.data = user.confirmed
form.role.data = user.role_id
form.name.data = user.name
form.location.data = user.location
form.about_me.data = user.about_me
return render_template('edit_profile.html', form=form, user=user)

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.

126 | Capítulo 10: Perfis de Usuário


Machine Translated by Google

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 10b para verificar esta versão do aplicativo.

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:

(venv) $ python >>>


import hashlib >>>
hashlib.md5('[email protected]'.encode('utf-8')).hexdigest()
'd4c74594d841139328695756648b6bd6'

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.

Tabela 10-1. Argumentos da string de consulta do Gravatar

Nome do argumento Descrição

s Tamanho da imagem, em pixels.

r Classificação da imagem. As opções são "g", "pg", "r" e "x".

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.

Exemplo 10-13. app/ models.py: Geração de URL do Gravatar

importar hashlib
da solicitação de importação do balão

classe User(UserMixin, db.Model):

Avatares de usuário | 127


Machine Translated by Google

# ...
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:

(venv) $ python manage.py shell >>> u


= User(email='[email protected]') >>>
u.gravatar() 'http://
www.gravatar.com/avatar/d4c74594d84113932869575bd6?s =100&d=identicon&r=g' >>>
u.gravatar(size=256) 'http://
www.gravatar.com/avatar/d4c74594d84113932869575bd6?s=256&d=identicon&r=g'

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.

Exemplo 10-14. app/ tempaltes/ user.html: Avatar na 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.

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 10c para verificar esta versão do aplicativo.

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.

128 | Capítulo 10: Perfis de Usuário


Machine Translated by Google

Figura 10-3. Página de perfil do usuário com avatar

Exemplo 10-15. app/ models.py: Geração de URL do Gravatar com cache de hashes MD5

classe User(UserMixin, db.Model):


# ...
avatar_hash = db.Column(db.String(32))

def __init__(self, **kwargs):


# ...
se self.email não for None e self.avatar_hash for None:
self.avatar_hash =
hashlib.md5( self.email.encode('utf-8')).hexdigest()

def change_email(self, token):


# ...
self.email = new_email
self.avatar_hash =
hashlib.md5( self.email.encode('utf-8')).hexdigest()
db.session.add(self)
return True

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 = self.avatar_hash ou hashlib.md5(

Avatares de usuários | 129


Machine Translated by Google

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)

Durante a inicialização do modelo, o hash é calculado a partir do e-mail e armazenado


e, caso o usuário atualize o endereço de e-mail, o hash é recalculado. O método
gravatar() usa o hash do modelo, se disponível; caso contrário, funciona como antes e
gera o hash a partir do endereço de e-mail.

Se você clonou o repositório Git do aplicativo no GitHub, pode executar git


checkout 10d para verificar esta versão do aplicativo.
Esta atualização contém uma migração de banco de dados, então lembre-se
de executar python manage.py db upgrade depois de verificar o código.

No próximo capítulo, será criado o mecanismo de blog que alimenta esse aplicativo.

130 | Capítulo 10: Perfis de Usuário


Machine Translated by Google

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.

Envio e exibição de postagem de blog


Para dar suporte às postagens do blog, é necessário um novo modelo de banco de dados que as represente.
Esse modelo é mostrado no Exemplo 11-1.

Exemplo 11-1. app/ models.py: Postar modelo

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'))

classe User(UserMixin, db.Model):


# ...
posts = db.relationship('Post', backref='autor', lazy='dynamic')

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

Exemplo 11-2. app/ main/ forms.py: Formulário de postagem no blog

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

@main.route('/', method=['GET', 'POST']) def


index(): form
= PostForm() if
current_user.can(Permission.WRITE_ARTICLES) and \
form.validate_on_submit(): post
= Post(body=form.body.data,
author=current_user._get_current_object())
db.session.add(post)
return redirect(url_for('.index')) posts =
Post.query.order_by(Post.timestamp.desc()).all() return
render_template('index.html', form= formulário, posts=postagens)

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 %}

132 | Capítulo 11: Postagens de blog


Machine Translated by Google

</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.

Se você clonou o repositório Git do aplicativo no GitHub, pode executar git


checkout 11a para verificar esta versão do aplicativo.
Esta atualização contém uma migração de banco de dados, então lembre-se
de executar python manage.py db upgrade depois de verificar o código.

Envio e Exibição de Postagem de Blog | 133


Machine Translated by Google

Figura 11-1. Página inicial com formulário de envio de blog e lista de postagens de blog

Postagens de blog em páginas de perfil

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 =

user.posts .order_by(Post.timestamp.desc()).all() return


render_template('user.html', user=user, posts=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.

134 | Capítulo 11: Postagens de blog


Machine Translated by Google

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.

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 11b para verificar esta versão do aplicativo.

Paginação de listas de postagens de blog longas

À 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.

Criando dados falsos de postagens de

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:

(venv) $ pip install forgerypy

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 .

Paginação de listas de postagens de blog longas | 135


Machine Translated by Google

Exemplo 11-7. requisitos/ dev.txt: arquivo de requisitos de desenvolvimento


-r common.txt
ForgeryPy==0.1

O Exemplo 11-8 mostra métodos de classe adicionados aos modelos User e Post que podem gerar
dados falsos.

Exemplo 11-8. app/ models.py: Gera usuários falsos e postagens de blog


classe User(UserMixin, db.Model):
# ...
@staticmethod
def generate_fake(count=100): from
sqlalchemy.exc import IntegrityError from random
import seed import forgery_py

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(),

member_since=forgery_py.date.date(True)) db.session.add(u) tente: db.session.commit() exceto IntegrityError:


db.session.rollback()

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()

136 | Capítulo 11: Postagens de blog


Machine Translated by Google

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.

Se você clonou o repositório Git do aplicativo no GitHub, pode executar git


checkout 11c para verificar esta versão do aplicativo.
Para garantir que você tenha todas as dependências instaladas, execute
também pip install -r requirements/dev.txt.

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:

(venv) $ python manage.py shell >>>


User.generate_fake(100)
>>> Post.generate_fake(100)

Se você executar o aplicativo agora, verá uma longa lista de postagens de blog aleatórias na página
inicial.

Renderizando dados em páginas

O Exemplo 11-9 mostra as mudanças na rota da página inicial para suportar a paginação.

Exemplo 11-9. app/ main/ views.py: paginar a lista de postagens do blog


@main.route('/', method=['GET', 'POST']) def index():
# page =
...
request.args.get('page', 1, type=int) pagination =
Post.query .order_by(Post.timestamp.desc()).paginate( page,
per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],
error_out=False)
posts = pagination.items return
render_template('index.html', form=form, posts =postagens,
paginação=paginação)

Paginação de listas de postagens de blog longas | 137


Machine Translated by Google

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.

Adicionando um widget de paginação

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.

Tabela 11-1. Atributos de objeto de paginação Flask-SQLAlchemy

Atributo Descrição

Unid Os registros na página atual

A consulta de origem que foi paginada


consulta
O número da página atual
página
O número da página anterior
núm_anterior

next_num O próximo número da página

has_next True se houver uma próxima página

has_prev True se houver uma página anterior


O número total de páginas para a consulta
Páginas
O número de itens por página
por página

total O número total de itens retornados pela consulta

O objeto de paginação também possui alguns métodos, listados na Tabela 11-2.

138 | Capítulo 11: Postagens de blog


Machine Translated by Google

Tabela 11-2. Atributos de objeto de paginação Flask-SQLAlchemy


Método 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

left_current=2, de paginação. A lista terá páginas left_edge no lado esquerdo, páginas

right_current=5, left_current à esquerda da página atual, páginas right_current à

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

valor na sequência indica uma lacuna na sequência de páginas.

anterior() Um objeto de paginação para a página anterior.

seguinte() Um objeto de paginação para a próxima página.

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.

Exemplo 11-10. app/ templates/ _macros.html: macro de modelo de paginação


{% macro pagination_widget(pagination, endpoint) %} <ul
class="pagination">
<li{% if not pagination.has_prev %} class="disabled"{% endif %}> <a href="{%
if pagination.has_prev %}{{ url_for(endpoint,
page = pagination.page - 1, **kwargs) }}{% else %}#{% endif %}"> & laquo; </
a> </li>
{%
for p
in pagination.iter_pages() %} { % if p %} {% if
p ==
pagination.page %} <li class="active">

<a href="{{ url_for(endpoint, page = p, **kwargs) }}"> {{ p }}</a> </li> {%


else
%} <li> <a

href="{ { url_for(endpoint, page = p, **kwargs) }}">{{ p }}</a> </li> {% endif %} {% else


%} <li

class="disabled"><a href="#">&hellip;</a></li> {% endif %}


{% endfor %}
<li{% if not
pagination.has_next %} class="disabled"{% endif %}> <a href="{% if
pagination.has_next %}{{ url_for(endpoint,
page = pagination.page + 1, **kwargs) }}{%else %}#{%endif %}"> & raquo;

Paginação de listas de postagens de blog longas | 139


Machine Translated by Google

</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
%}

A Figura 11-2 mostra como os links de paginação aparecem na página.

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 11d para verificar esta versão do aplicativo.

140 | Capítulo 11: Postagens de blog


Machine Translated by Google

Figura 11-2. Paginação da postagem do blog

Postagens Rich-Text com Markdown e Flask-PageDown


Postagens de texto simples são suficientes para mensagens curtas e atualizações de status, mas os usuários que
desejam escrever artigos mais longos acharão a falta de formatação muito limitante. Nesta seção, o campo da área
de texto onde as postagens são inseridas será atualizado para oferecer suporte ao Markdown sintaxe e apresentar
uma visualização em rich text da postagem.

A implementação desse recurso requer alguns novos pacotes:

• PageDown, um conversor Markdown-to-HTML do lado do cliente implementado em Javaÿ


Roteiro.

• 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

higienizador de HTML implementado em Python.

Todos os pacotes Python podem ser instalados com pip:

(venv) $ pip install flask-pagedown markdown lixívia

Usando Flask-PageDown A extensão

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.

Exemplo 11-12. app/ __init__.py: Inicialização Flask-PageDown

from flask.ext.pagedown import PageDown #


...

Postagens Rich-Text com Markdown e Flask-PageDown | 141


Machine Translated by Google

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

de flask.ext.pagedown.fields importar PageDownField

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.

Exemplo 11-14. app/ index.html: Declaração de modelo Flask-PageDown

{% scripts de bloco %}
{{ super() }}
{{ pagedown.include_pagedown() }} {%
endblock %}

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 11e para verificar esta versão do aplicativo.
Para garantir que você tenha todas as dependências instaladas,
execute pip install -r requirements/dev.txt.

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.

142 | Capítulo 11: Postagens de blog


Machine Translated by Google

Figura 11-3. Formulário de postagem de blog em rich text

Manipulando Rich Text no Servidor Quando

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

de remarcação de importação remarcação


de importação alvejante

classe Post(db.Model): #
...
body_html = db.Column(db.Text)

Postagens Rich-Text com Markdown e Flask-PageDown | 143


Machine Translated by Google

...
# @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']

target.body_html = bleach.linkify(bleach.clean( markdown(value,


output_format='html'), tags=allowed_tags,
strip=True))

db.event.listen(Post.body, 'set', Post.on_changed_body)

A função on_changed_body é registrada como um ouvinte do evento “set” do SQLAlchemy para


o corpo, o que significa que ela será invocada automaticamente sempre que o campo do corpo
em qualquer instância da classe for definido com um novo valor. A função renderiza a versão
HTML do corpo e a armazena em body_html, tornando a conversão do texto Markdown em HTML
totalmente automática.

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.

A última alteração é substituir post.body por post.body_html no modelo quando disponível,


conforme mostrado no Exemplo 11-16.

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.

144 | Capítulo 11: Postagens de blog


Machine Translated by Google

Se você clonou o repositório Git do aplicativo no GitHub, pode executar git


checkout 11f para verificar esta versão do aplicativo.
Esta atualização também contém uma migração de banco de dados, então
lembre-se de executar python manage.py db upgrade depois de verificar o
código. Para garantir que você tenha todas as dependências instaladas,
execute pip install -r requirements/dev.txt.

Links permanentes para postagens de blog

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.

Exemplo 11-17. app/ main/ views.py: links permanentes para postagens

@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.

Exemplo 11-18. app/ templates/ _posts.html: Links permanentes para posts

<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>

Links permanentes para postagens de blog | 145


Machine Translated by Google

</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.

Exemplo 11-19. app/ templates/ post.html: modelo de link permanente

{% estende "base.html" %}

{% block title %}Flasky - Post{% endblock %}

{% block page_content %}
{% include '_posts.html' %} {%
endblock %}

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 11g para verificar esta versão do aplicativo.

Editor de postagens do blog

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.

Exemplo 11-20. app/ templates/ edit_post.html: Editar modelo de postagem de blog

{% extends "base.html" %} {%
import "bootstrap/wtf.html" como wtf %}

{% block title %}Flasky - Editar postagem{% endblock %}

{% block page_content %}
<div class="page-header">
<h1>Editar postagem</
h1> </
div>
<div> {{ wtf.quick_form(form) }}
</div>
{% endblock % }

146 | Capítulo 11: Postagens de blog


Machine Translated by Google

{% scripts de bloco %}
{{ super() }}
{{ pagedown.include_pagedown() }} {%
endblock %}

A rota que suporta o editor de postagem do blog é mostrada no Exemplo 11-21.

Exemplo 11-21. app/ main/ views.py: Edite a rota da postagem do blog

@main.route('/edit/<int:id>', methods=['GET', 'POST'])


@login_required
def edit(id):
post = Post.query.get_or_404(id) if
current_user != post.author e \ not
current_user.can(Permission.ADMINISTER): abort(403)
form =
PostForm() if
form.validate_on_submit():
post.body = form.body.data
db.session.add(post)
flash('A postagem foi atualizada.') return
redirect(url_for('post', id=post.id)) form.body.data =
post.body return
render_template('edit_post.html', form=form)

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.

Exemplo 11-22. app/ templates/ _posts.html: Editar links de postagens de blog

<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
%}

Editor de postagens de blog | 147


Machine Translated by Google

</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.

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 11h para verificar esta versão do aplicativo.

Figura 11-4. Edite e permalink links em postagens de blog.

148 | Capítulo 11: Postagens de blog


Machine Translated by Google

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.

Relacionamentos de banco de dados revisitados

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?

Considere o exemplo clássico de um relacionamento muitos-para-muitos: um banco de dados de alunos e as aulas


que eles estão cursando. Claramente, você não pode adicionar uma chave estrangeira a uma classe na tabela de
alunos, porque um aluno faz muitas aulas - uma chave estrangeira não é suficiente.
Da mesma forma, não é possível adicionar uma chave estrangeira ao aluno na tabela de turmas, pois as turmas
possuem mais de um aluno. Ambos os lados precisam de uma lista de chaves estrangeiras.

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.

Figura 12-1. Exemplo de relacionamento muitos-para-muitos

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'))
)

150 | Capítulo 12: Seguidores


Machine Translated by Google

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 é definido com a mesma construção db.relationship() que é usada para


relacionamentos um-para-muitos, mas no caso de um relacionamento muitos-para-muitos, o
argumento secundário adicional deve ser definido para a tabela de associação . O relacionamento
pode ser definido em qualquer uma das duas classes, com o argumento backref cuidando de expor
o relacionamento do outro lado também. A tabela de associação é definida como uma tabela
simples, não como um modelo, pois o SQLAlchemy gerencia esta tabela internamente.

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:

>>> s.classes.all() >>>


c.students.all()

A relação alunos disponível no modelo Classe é aquela definida no argumento db.backref() .


Observe que nesse relacionamento o argumento backref foi expandido para também ter um
atributo lazy = 'dynamic' , então ambos os lados retornam uma consulta que pode aceitar filtros
adicionais.

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

Relacionamentos de banco de dados revisitados | 151


Machine Translated by Google

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.

Figura 12-2. Seguidores, relacionamento muitos-para-muitos

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.

Relacionamentos muitos-para-muitos avançados Com

um relacionamento muitos-para-muitos autorreferencial configurado conforme indicado no exemplo


anterior, o banco de dados pode representar seguidores, mas há uma limitação. Uma necessidade
comum ao trabalhar com relacionamentos muitos-para-muitos é armazenar dados adicionais que se
aplicam ao link entre duas entidades. Para a relação de seguidores, pode ser útil armazenar a data em
que um usuário começou a seguir outro usuário, pois isso permitirá que as listas de seguidores sejam
apresentadas em ordem cronológica. O único local onde essas informações podem ser armazenadas é
na tabela de associação, mas em uma implementação semelhante à dos alunos e turmas mostradas
anteriormente, a tabela de associação é uma tabela interna totalmente gerenciada pelo SQLAlchemy.

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 .

152 | Capítulo 12: Seguidores


Machine Translated by Google

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

classe User(UserMixin, db.Model):


# ...
seguido = db.relationship('Follow',
Foreign_keys=[Follow.follower_id],
backref=db.backref('follower', lazy='ingressado'),
lazy='dynamic',
cascade='all, delete-orphan' )
seguidores = db.relationship('Follow',
Foreign_keys=[Follow.followed_id],
backref=db.backref('followed', lazy='joined'), lazy='dynamic',
cascade='all,
delete-orphan ')

Aqui, os relacionamentos seguidos e seguidores são definidos como relacionamentos um-para-muitos


individuais. Observe que é necessário eliminar qualquer ambiguidade entre chaves estrangeiras
especificando em cada relacionamento qual chave estrangeira usar por meio do argumento opcional
Foreign_keys . Os argumentos db.backref() nesses relacionamentos não se aplicam uns aos outros; as
referências anteriores são aplicadas ao modelo Follow .

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 .

Relacionamentos de banco de dados revisitados | 153


Machine Translated by Google

O argumento preguiçoso no lado do usuário de ambos os relacionamentos tem necessidades


diferentes. Estes estão do lado “um” e retornam do lado “muitos”; aqui é usado um modo dinâmico ,
de modo que os atributos de relacionamento retornam objetos de consulta em vez de retornar os
itens diretamente, para que filtros adicionais possam ser adicionados à consulta antes de ser executada.

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 valor atribuído a cascata é uma lista separada por vírgulas de opções


de cascata. Isso é um pouco confuso, mas a opção denominada all
representa todas as opções em cascata, exceto delete-orphan. Usando
o valor all, delete-orphan deixa as opções de cascata padrão habilitadas
e adiciona o comportamento delete para órfãos.

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.

Exemplo 12-3. app/ models/ user.py: métodos auxiliares de seguidores


classe Usuário(db.Model):
# ...
def seguir(auto, usuário):
se não for self.is_following(usuário): f
= Seguir(seguidor=auto, seguido=usuário)
db.session.add(f)

def deixar de seguir(auto,


usuário): f = self.followed.filter_by(followed_id=user.id).first() if f:

db.session.delete(f)

def is_following(auto, usuário):


return
self.followed.filter_by( seguir_id=user.id).first() não é nenhum

def is_followed_by(self, user):

154 | Capítulo 12: Seguidores


Machine Translated by Google

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.

Se você clonou o repositório Git do aplicativo no GitHub, pode executar git


checkout 12a para verificar esta versão do aplicativo.
Esta atualização contém uma migração de banco de dados, então lembre-se
de executar python manage.py db upgrade depois de verificar o código.

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.

Seguidores na página de perfil


A página de perfil de um usuário precisa apresentar um botão “Seguir” se o usuário que a visualiza não for um
seguidor, ou um botão “Deixar de seguir” se o usuário for um seguidor. Também é uma boa adição para mostrar
as contagens de seguidores e seguidos, exibir as listas de seguidores e usuários seguidos e mostrar um sinal
de “Segue você” quando apropriado. As alterações no modelo de perfil do usuário são mostradas no Exemplo
12-4. A Figura 12-3 mostra a aparência das adições na página de perfil.

Exemplo 12-4. app/ templates/ user.html: Melhorias do seguidor no cabeçalho do perfil do usuário

{% if current_user.can(Permission.FOLLOW) e usuário != current_user %}


{% if not current_user.is_following(user) %} <a
href="{{ url_for('.follow', username=user.username) }}" class="btn btn-
primary"> Seguir</a> { % else %} <a

href="{{ url_for('.unfollow', username=user.username) }}" class="btn


btn-default"> Parar de seguir</a>
{% fim se %}
{% endif %}
<a href="{{ url_for('.followers', username=user.username) }}">

Seguidores na Página de Perfil | 155


Machine Translated by Google

Seguidores: <span class="badge">{{ user.followers.count() }} </a> <a

href="{{ url_for('.followed_by', username=user.username) } }">


Seguindo: <span class="badge">{{ user.followed.count() }} </a> {% if

current_user.is_authenticated() and user != current_user and


user.is_following(current_user) %} | <span
class="label label-default">Segue você {% endif %}

Figura 12-3. Seguidores na página de perfil

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.

Exemplo 12-5. app/ main/ views.py: Siga a rota e visualize a função


@main.route('/follow/<username>')
@login_required
@permission_required(Permission.FOLLOW)
def follow(username):
user = User.query.filter_by(username=username).first() if user is
None: flash('Usuário
inválido.') return
redirect(url_for('.index'))
if current_user.is_following(user): flash('Você
já está seguindo este usuário.')

156 | Capítulo 12: Seguidores


Machine Translated by Google

return redirecionamento(url_for('.user', nome de usuário=nome de usuário))


current_user.follow(user) flash('
Agora você está seguindo %s.' % username) return
redirect(url_for('.user', username=username))

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.

A rota /followers/ <username> é invocada quando um usuário clica na contagem de seguidores de


outro usuário na página de perfil. A implementação é mostrada no Exemplo 12-6.

Exemplo 12-6. app/ main/ views.py: rota de seguidores e função de visualização


@main.route('/followers/<nome de usuário>')
def seguidores(nome de
usuário): user = User.query.filter_by(username=username).first() if
user is None:
flash('Invalid user.') return
redirecionar(url_for('.index'))
page = request.args.get('page', 1, type=int) pagination
= user.followers.paginate(
page, per_page=current_app.config['FLASKY_FOLLOWERS_PER_PAGE'],
error_out=False)
followings = [{'user': item.follower, 'timestamp': item.timestamp} for item in
pagination.items] return
render_template('followers. html', user=user, title="Seguidores de",
endpoint='.followers', paginação=paginação,
segue=followers)

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.

Seguidores na Página de Perfil | 157


Machine Translated by Google

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 12b para verificar esta versão do aplicativo.

Consultar postagens seguidas usando uma junção de banco de dados

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

usuários que eles seguem.

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

em uma tabela temporária que é o resultado da junção.

A melhor maneira de explicar como as junções funcionam é por meio de um exemplo.

A Tabela 12-1 mostra um exemplo de tabela de usuários com três usuários.

Tabela 12-1. tabela de usuários

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.

Tabela 12-2. Tabela de postagens

id autor_id corpo
12 Postagem no blog por susan

21 Postagem no blog por john

33 Postagem no blog de david

41 Segunda postagem no blog por john

Finalmente, a Tabela 12-3 mostra quem está seguindo quem. Nesta tabela, você pode ver que john está seguindo david, susan está

seguindo john e david não está seguindo ninguém.

158 | Capítulo 12: Seguidores


Machine Translated by Google

Tabela 12-3. Segue tabela

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.

Tabela 12-4. Tabela unida

id autor_id* corpo seguidor_id seguido_id*

21 Postagem no blog por john 2 1

33 Postagem no blog de david 2 3

41 Segunda postagem no blog por john 2 1

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.

• select_from(Follow) diz que a consulta começa com o modelo Follow .

• filter_by(follower_id=self.id) realiza a filtragem da seguinte tabela por


o usuário seguidor.

Consultar postagens seguidas usando uma junção de banco de dados | 159


Machine Translated by Google

• join(Post, Follow.followed_id == Post.author_id) junta os resultados de filter_by() com os objetos Post .

A consulta pode ser simplificada trocando a ordem do filtro e da junção:

return Post.query.join(Follow, Follow.followed_id == Post.author_id)\


.filter(Follow.follower_id == self.id)

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.

Exemplo 12-7. app/ models/ user.py: Obter postagens seguidas

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.

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 12c para verificar esta versão do aplicativo.

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.

Mostrar postagens seguidas na página inicial


A página inicial agora pode oferecer aos usuários a opção de visualizar todas as postagens do blog ou apenas
as dos usuários seguidos. O Exemplo 12-8 mostra como essa escolha é implementada.

Exemplo 12-8. app/ main/ views.py: Mostra todos os posts seguidos

@app.route('/', method = ['GET', 'POST']) def index():

# ...
show_followed = Falso

160 | Capítulo 12: Seguidores


Machine Translated by Google

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)

A opção de mostrar todas as postagens ou as seguidas é armazenada em um cookie chamado show_followed


que, quando definido como uma string não vazia, indica que apenas as postagens seguidas devem ser mostradas.
Os cookies são armazenados no objeto de solicitação como um dicionário request.cookies . O valor de string do
cookie é convertido em um Booleano e, com base em seu valor, uma variável local de consulta é definida para a
consulta que obtém as listas completas ou filtradas de postagens de blog. Para mostrar todas as postagens, a
consulta de nível superior Post.query é usada e a propriedade User.followed_posts adicionada recentemente é
usada quando a lista deve ser restrita a seguidores.
A consulta armazenada na variável local de consulta é então paginada e os resultados enviados para o modelo
como antes.

O cookie show_followed é definido em duas novas rotas, mostradas no Exemplo 12-9.

Exemplo 12-9. app/ main/ views.py: Seleção de todas as postagens ou seguidas

@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

Mostrar postagens seguidas na página inicial | 161


Machine Translated by Google

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.

Figura 12-4. Publicações seguidas na página inicial

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 12d para verificar esta versão do aplicativo.

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.

162 | Capítulo 12: Seguidores


Machine Translated by Google

Exemplo 12-10. app/ models/ user.py: Tornar os usuários seus próprios seguidores na construção

classe User(UserMixin, db.Model):


...
# def __init__(self, **kwargs):
# ...
self.follow(self)

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

classe User(UserMixin, db.Model):


...
# @staticmethod
def add_self_follows(): para
o usuário em User.query.all():
se não user.is_following(user):
user.follow(user)
db.session.add(user)
db.session.commit()
# ...

Agora o banco de dados pode ser atualizado executando a função de exemplo anterior no shell:

(venv) $ python manage.py shell >>>


User.add_self_follows()

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.

Mostrar postagens seguidas na página inicial | 163


Machine Translated by Google

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 12e para verificar esta versão do aplicativo.

No próximo capítulo, o subsistema de comentários do usuário será implementado - outro recurso


muito importante de aplicativos socialmente conscientes.

164 | Capítulo 12: Seguidores


Machine Translated by Google

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.

Representação de banco de dados de comentários


Os comentários não são muito diferentes das postagens do blog. Ambos têm um corpo, um autor e um registro de data e
hora e, nessa implementação específica, ambos são escritos com a sintaxe Markdown. A Figura 13-1 mostra um diagrama

da tabela de comentários e seus relacionamentos com outras tabelas no banco de dados.

Figura 13-1. Representação de banco de dados de comentários de postagem de blog

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

A tabela de comentários também está em um relacionamento um-para-muitos com a tabela de usuários .


Essa relação dá acesso a todos os comentários feitos por um usuário e, indiretamente, quantos
comentários ele escreveu, uma informação que pode ser interessante mostrar nas páginas de perfil do
usuário. A definição do modelo Comment é mostrada no Exemplo 13-1.

Exemplo 13-1. app/ models.py: modelo de comentário

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']

target.body_html = lixívia.linkify(bleach.clean( markdown(value ,


output_format='html'), tags=allowed_tags,
strip=True))

db.event.listen(Comment.body, 'set', Comment.on_changed_body)

Os atributos do modelo Comment são quase os mesmos do Post. Uma adição


é o campo desabilitado , um Booleano que será utilizado pelos moderadores para suprimir comentários
inapropriados ou ofensivos. Como foi feito para as postagens do blog, os comentários definem um evento
que é acionado sempre que o campo do corpo é alterado, automatizando a renderização do texto
Markdown para HTML. O processo é idêntico ao que foi feito para postagens de blog no Capítulo 11, mas
como os comentários tendem a ser curtos, a lista de tags HTML permitidas na conversão do Markdown é
mais restritiva, as tags relacionadas a parágrafos foram removidas e apenas as tags de formatação de
caracteres são deixadas.

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')

166 | Capítulo 13: Comentários do usuário


Machine Translated by Google

Envio e exibição de comentários


Neste aplicativo, os comentários são exibidos nas páginas de postagem de blog individuais que foram
adicionadas como links permanentes no Capítulo 11. Um formulário de envio também está incluído nesta
página. O Exemplo 13-3 mostra o formulário da web que será usado para inserir comentários – um formulário
extremamente simples que possui apenas um campo de texto e um botão de envio.

Exemplo 13-3. app/ main/ forms.py: Formulário de entrada de comentários

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.

Exemplo 13-4. app/ main/ views.py: suporte a comentários de postagens de blog

@main.route('/post/<int:id>', methods=['GET', 'POST']) def post(id):


post =
Post.query.get_or_404(id) form =
CommentForm() if
form.validate_on_submit():
comentário = Comentário(corpo=form.body.data,
post=post,
author=current_user._get_current_object())
db.session.add(comentário)
flash('Seu comentário foi publicado.') return
redirect(url_for('.post', id=post.id, page=- 1)) page =
request.args.get('page', 1, type=int) if page == -1: page
=
(post.comments.count() - 1) / \
current_app.config['FLASKY_COMMENTS_PER_PAGE' ] + 1
paginação = post.comments.order_by(Comment.timestamp.asc()).paginate( página,
per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],
error_out=False)
comentários = pagination.items
return render_template('post. html', postagens=[postagem], form=formulário,
comentários=comentários, paginação=paginação)

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

Envio e exibição de comentários | 167


Machine Translated by Google

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.

A lista de comentários associados à postagem é obtida por meio do relacionamento um-para-muitos


post.comments , classificados por carimbo de data/hora do comentário e paginados com as mesmas técnicas
usadas para postagens de blog. Os comentários e o objeto de paginação são enviados ao template para
renderização. A variável de configuração FLASKY_COMMENTS_PER_PAGE é adicionada ao config.py para
controlar o tamanho de cada página de comentários.

A renderização do comentário é definida em um novo modelo _comments.html que é semelhante a _posts.html ,


mas usa um conjunto diferente de classes CSS. Este modelo é incluído por _post.html abaixo do corpo do
post, seguido por uma chamada para a macro de paginação. Você pode revisar as alterações nos modelos no
repositório GitHub do aplicativo.

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

<a href="{{ url_for('.post', id=post.id) }}#comments"> <span


class="label label-primary">
{{ post.comments.count() }} Comentários </
a>

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 .

Se você clonou o repositório Git do aplicativo no GitHub, pode executar git


checkout 13a para verificar esta versão do aplicativo.
Esta atualização contém uma migração de banco de dados, então lembre-se
de executar python manage.py db upgrade depois de verificar o código.

168 | Capítulo 13: Comentários do usuário


Machine Translated by Google

Figura 13-2. Comentários da postagem do blog

Moderação dos comentários

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 %}
...

Moderação de Comentário | 169


Machine Translated by Google

A página de moderação mostra os comentários de todas as postagens na mesma lista, com os


comentários mais recentes exibidos primeiro. Abaixo de cada comentário há um botão que pode
alternar o atributo desabilitado . A rota /moderate é mostrada no Exemplo 13-7.

Exemplo 13-7. app/ main/ views.py: rota de moderação de comentários


@main.route('/moderate')
@login_required
@permission_required(Permission.MODERATE_COMMENTS)
def moderada():
page = request.args.get('page', 1, type=int) pagination
= Comment.query.order_by( Comment.timestamp.desc()).paginate( página,
per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],
error_out=False)
comments = pagination.items
return render_template('moderate.html', comments=comentários,
paginação=paginação, página=página)

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.

O modelo modered.html , mostrado no Exemplo 13-8, também é simples porque depende do


submodelo _comments.html criado anteriormente para a renderização dos comentários.

Exemplo 13-8. app/ templates/ moderate.html: modelo de moderação de comentários


{% extends "base.html" %} {%
import "_macros.html" como macros %}

{% block title %}Flasky - Moderação de comentários{% endblock %}

{% 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.

170 | Capítulo 13: Comentários do usuário


Machine Translated by Google

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.

Exemplo 13-10. app/ main/ views.py: Rotas de moderação de comentários


@main.route('/moderate/enable/<int:id>')
@login_required
@permission_required(Permission.MODERATE_COMMENTS)
def modere_enable(id):
comment = Comment.query.get_or_404(id)
comment.disabled = False db.
session.add(comentário)
return redirect(url_for('.moderate',
page=request.args.get('page', 1, type=int)))

Moderação de Comentário | 171


Machine Translated by Google

@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 )))

As rotas de ativação e desativação de comentários carregam o objeto de comentário, definem o campo


desativado com o valor adequado e o gravam de volta no banco de dados. No final, eles redirecionam de volta
para a página de moderação de comentários (mostrada na Figura 13-3) e, se um argumento de página foi
fornecido na string de consulta, eles o incluem no redirecionamento. Os botões no modelo _comments.html
foram renderizados com o argumento page para que o redirecionamento traga o usuário de volta à mesma
página.

Figura 13-3. Página de moderação de comentários

172 | Capítulo 13: Comentários do usuário


Machine Translated by Google

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 13b para verificar esta versão do aplicativo.

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.

Moderação de Comentário | 173


Machine Translated by Google
Machine Translated by Google

CAPÍTULO 14

Interfaces de Programação de Aplicativos

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 são tudo O conceito de

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.

176 | Capítulo 14: Interfaces de Programação de Aplicativos


Machine Translated by Google

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.

Tabela 14-1. Métodos de solicitação HTTP em APIs RESTful

Método Alvo Descrição HTTP

de solicitação código de status

PEGAR recurso individual Obtenha o recurso. 200

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

URL o URL do novo recurso e o retorna em um cabeçalho Location

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.

EXCLUIR recurso individual Excluir um recurso. 200

URL

EXCLUIR Coleta de recursos Exclua todos os recursos da coleção. 200

URL

A arquitetura REST não requer que todos os métodos sejam


implementados para um recurso. Se o cliente invocar um método que
não é suportado por um determinado recurso, uma resposta com o
código de status 405 para “Método não permitido” deve ser retornada.
O Flask lida com esse erro automaticamente.

Corpos de solicitação e resposta Os

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.

Introdução ao REST | 177


Machine Translated by Google

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

178 | Capítulo 14: Interfaces de Programação de Aplicativos


Machine Translated by Google

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.

Serviços Web RESTful com Flask


O Flask facilita muito a criação de serviços Web RESTful. O familiar decorador route() junto com seu
argumento opcional de métodos pode ser usado para declarar as rotas que lidam com as URLs de
recursos expostas pelo serviço. Trabalhar com dados JSON também é simples, pois os dados JSON
incluídos em uma solicitação são expostos automaticamente como um dicionário request.json Python e
uma resposta que precisa conter JSON pode ser facilmente gerada a partir de um dicionário Python
usando a função auxiliar jsonify() do Flask.

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.

Criando um esquema de API As

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.

Exemplo 14-1. Estrutura do projeto da API

|-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.

Serviços Web RESTful com Flask | 179


Machine Translated by Google

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.

Exemplo 14-2. app/ api_1_0/ __init__.py: Construtor de projeto de API

do balão de importação Blueprint

api = Blueprint('api', __name__)

de . autenticação de importação , postagens, usuários, comentários, erros

O registro do blueprint da API é mostrado no Exemplo 14-3.

Exemplo 14-3. app/ _init_.py: registro do projeto da API

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

status HTTP Nome Descrição


código

200 OK A solicitação foi concluída com sucesso.

201 Criada A solicitação foi concluída com sucesso e um novo recurso foi criado como resultado.

400 Pedido ruim A solicitação é inválida ou inconsistente.

401 Não autorizado A solicitação não inclui informações de autenticação.

403 Proibido As credenciais de autenticação enviadas com a solicitação são insuficientes para a solicitação.

404 Não encontrado O recurso referenciado na URL não foi encontrado.

405 Método não permitido O método de solicitação solicitado não é compatível com o recurso fornecido.

500 Erro interno do servidor Ocorreu um erro inesperado ao processar a solicitação.

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.

180 | Capítulo 14: Interfaces de Programação de Aplicativos


Machine Translated by Google

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.

Autenticação do usuário com Flask-HTTPAuth

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.

Serviços Web RESTful com Flask | 181


Machine Translated by Google

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 .

Flask-HTTPAuth é instalado com pip:

(venv) $ pip install flask-httpauth

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.

Exemplo 14-6. app/ api_1_0/ authentication.py: inicialização Flask-HTTPAuth

de flask.ext.httpauth import HTTPBasicAuth auth =


HTTPBasicAuth()

@auth.verify_password
def verify_password(email, password): if email
== '': g.current_user
= AnonymousUser() return True

user = User.query.filter_by(email = email).first() se não for


usuário:
retornar falso
g.current_user = usuário
return user.verify_password(senha)

182 | Capítulo 14: Interfaces de Programação de Aplicativos


Machine Translated by Google

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.

O retorno de chamada de autenticação salva o usuário autenticado no objeto g global do Flask


para que a função view possa acessá-lo posteriormente. Observe que quando um login
anônimo é recebido, a função retorna True e salva uma instância da classe AnonymousUser
usada com Flask-Login em g.current_user.

Como as credenciais do usuário estão sendo trocadas a cada solicitação, é


extremamente importante que as rotas da API sejam expostas em HTTP
seguro para que todas as solicitações e respostas sejam criptografadas.

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.

Exemplo 14-7. _app/ api_1_0/ authentication.py: manipulador de erros Flask-HTTPAuth


@auth.error_handler
def auth_error():
return não autorizado(' Credenciais inválidas')

Para proteger uma rota, o decorador auth.login_required é usado:

@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.

Exemplo 14-8. app/ api_1_0/ authentication.py: manipulador before_request com autenticação

de .errors importar proibido_error

@api.before_request
@auth.login_required
def before_request():

Serviços Web RESTful com Flask | 183


Machine Translated by Google

se não g.current_user.is_anonymous e \ não


g.current_user.confirmed: retorne
proibido('Conta não confirmada')

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.

Autenticação baseada em token

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.

Exemplo 14-9. app/ models.py: suporte à autenticação baseada em token


classe User(db.Model): #
...
def generate_auth_token(auto, expiração):
s = Serializer(current_app.config['SECRET_KEY'],
expires_in=expiração)
return s.dumps({'id': self.id})

@staticmethod
def verify_auth_token(token): s =
Serializer(current_app.config['SECRET_KEY']) try: data =

s.loads(token) exceto: return


None

return User.query.get(data['id'])

O método generate_auth_token() retorna um token assinado que codifica o campo id do usuário .


Um tempo de expiração dado em segundos também é usado. O método Verify_auth_token() pega
um token e, se for válido, retorna o usuário armazenado nele. Este é um método estático, pois o
usuário será conhecido somente após a decodificação do token.

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.

184 | Capítulo 14: Interfaces de Programação de Aplicativos


Machine Translated by Google

Exemplo 14-10. app/ api_1_0/ authentication.py: verificação de autenticação aprimorada com


suporte a token
@auth.verify_password
def verify_password(email_or_token, senha):
if email_or_token == '':
g.current_user = AnonymousUser()
return True
if password == '':
g.current_user = User.verify_auth_token(email_or_token) g.token_used
= True return
g.current_user is not None user =
User.query.filter_by(email=email_or_token).first() if not user:

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-11. app/ api_1_0/ authentication.py: Geração de token de autenticação


@api.route('/token') def
get_token(): se
g.current_user.is_anonymous() ou g.token_used: return não
autorizado(' Credenciais inválidas') return
jsonify({'token': g.current_user.generate_auth_token( vencimento=3600),
'expiração': 3600})

Como essa rota está no blueprint, os mecanismos de autenticação adicionados ao manipulador


before_request também se aplicam a ela. Para evitar que os clientes usem um token antigo para
solicitar um novo, a variável g.token_used é verificada e, dessa forma, as solicitações autenticadas
com um token podem ser rejeitadas. A função retorna um token na resposta JSON com um
período de validade de uma hora. O período também está incluído no JSON
resposta.

Serviços Web RESTful com Flask | 185


Machine Translated by Google

Serializando recursos de e para JSON Uma

necessidade frequente ao escrever um serviço da Web é converter a representação interna de


recursos de e para JSON, que é o formato de transporte usado em solicitações e respostas HTTP.
O exemplo 14-12 mostra um novo método to_json() adicionado à classe Post .

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),

'comentários': url_for('api.get_post_comments', id=self.id,


_external=True)
'comment_count': self.comments.count()

} 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.

Exemplo 14-13. app/ models.py: converte um usuário em um dicionário JSON serializável


classe User(UserMixin, db.Model):
# ...
def to_json(self):
json_user = {
'url': url_for('api.get_post', id=self.id, _external=True), 'username':
self.username, 'member_since':
self.member_since, 'last_seen':
self.last_seen, 'posts' :
url_for('api.get_user_posts', id=self.id, _external=True), 'followed_posts':
url_for('api.get_user_followed_posts',
id=self.id, _external=True),

186 | Capítulo 14: Interfaces de Programação de Aplicativos


Machine Translated by Google

'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

from app.exceptions import ValidationError

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.

Serviços Web RESTful com Flask | 187


Machine Translated by Google

Exemplo 14-15. app/ exceptions.py: exceção ValidationError

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:

@api.route('/posts/', method=['POST']) 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())

Implementando Endpoints de Recursos O que

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

188 | Capítulo 14: Interfaces de Programação de Aplicativos


Machine Translated by Google

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 de erros 404 está no nível do aplicativo, mas fornecerá


uma resposta JSON se o cliente solicitar esse formato. Se uma resposta
personalizada para o serviço da web for desejada, o manipulador de erro
404 pode ser substituído no blueprint.

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)}

Essa função de exibição é agrupada em um decorator permission_required (mostrado em um próximo exemplo)


que garante que o usuário autenticado tenha permissão para escrever postagens de blog. A criação real da
postagem do blog é direta devido ao suporte de tratamento de erros que foi implementado anteriormente. Uma
postagem de blog é criada a partir dos dados JSON e seu autor é atribuído explicitamente como o usuário
autenticado. Depois que o modelo é gravado no banco de dados, um código de status 201 é retornado e um
cabeçalho Location é adicionado com a URL do recurso recém-criado.

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.

Serviços Web RESTful com Flask | 189


Machine Translated by Google

Exemplo 14-19. app/ api_1_0/ decorators.py: permission_required decorador

def permission_required(permission): def


decorador(f):
@wraps(f)
def decorado_function(*args, **kwargs):
se não for g.current_user.can(permission):
return proibido('Permissões insuficientes')
return f(*args, **kwargs)
função de retorno decorada_função
de retorno do decorador

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.

Os manipuladores de recursos para usuários e comentários são implementados de maneira semelhante.


A Tabela 14-3 lista o conjunto de recursos implementados para este aplicativo. A implementação completa
está disponível para você estudar no repositório GitHub.

Tabela 14-3. Recursos da API Flasky


URL do recurso Métodos Descrição

/users/<int:id> Um usuário
PEGAR

/users/<int:id>/posts/ GET As postagens de blog escritas por um usuário

/users/<int:id>/timeline/ GET As postagens do blog seguidas por um usuário

/Postagens/
GET, POST Todas as postagens do blog

190 | Capítulo 14: Interfaces de Programação de Aplicativos


Machine Translated by Google

URL do recurso Métodos Descrição

/posts/<int:id> GET, COLOQUE uma postagem no blog

/posts/<int:id/>comments/ GET, POST Os comentários em uma postagem de blog

/comentários/ PEGAR todos os comentários

/comentários/<int:id> PEGAR Um comentário

Observe que os recursos implementados permitem que um cliente ofereça um subconjunto da


funcionalidade disponível por meio do aplicativo da web. A lista de recursos suportados pode ser
expandida se necessário, como expor seguidores, permitir moderação de comentários e implementar
quaisquer outros recursos que um cliente possa precisar.

Paginação de grandes coleções de recursos


As solicitações GET que retornam uma coleção de recursos podem ser extremamente caras e difíceis
de gerenciar para coleções muito grandes. Assim como os aplicativos da Web, os serviços da Web
podem optar por paginar as coleções.

O Exemplo 14-21 mostra uma possível implementação de paginação para a lista de postagens de blog.

Exemplo 14-21. app/ api_1_0/ posts.py: Paginação do post

@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.

Serviços Web RESTful com Flask | 191


Machine Translated by Google

Se você clonou o repositório Git do aplicativo no GitHub, pode executar git


checkout 14a para verificar esta versão do aplicativo.
Para garantir que você tenha todas as dependências instaladas, execute
também pip install -r requirements/dev.txt.

Testando serviços da Web com HTTPie Para testar

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.

HTTPie é instalado com pip:

(venv) $ pip install httpie

Uma solicitação GET pode ser emitida da seguinte maneira:

(venv) $ http --json --auth <email>:<senha> GET \ > http://


127.0.0.1:5000/api/v1.0/posts HTTP/1.0 200 OK
Content-Length:
7018 Content- Tipo:
application/json Data: Dom, 22 de
dezembro de 2013 08:11:24 GMT Servidor:
Werkzeug/0.9.4 Python/2.7.3

{
"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:

(venv) $ http --json --auth : GET http://127.0.0.1:5000/api/v1.0/posts/

O comando a seguir envia uma solicitação POST para adicionar uma nova postagem de blog:

(venv) $ http --auth <email>:<senha> --json POST \ > http://


127.0.0.1:5000/api/v1.0/posts/ \ > "body=Estou
adicionando uma postagem da *linha de comando*."
HTTP/1.0 201 CREATED
Comprimento do
conteúdo: 360 Tipo de conteúdo:
application/json Data: Dom, 22 de dezembro
de 2013 08:30:27 GMT Localização: http://127.0.0.1:5000/api/
v1.0/posts/ 111 Servidor: Werkzeug/0.9.4 Python/2.7.3

192 | Capítulo 14: Interfaces de Programação de Aplicativos


Machine Translated by Google

{
"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:

(venv) $ http --auth <email>:<senha> --json GET \ > http://


127.0.0.1:5000/api/v1.0/token HTTP/1.0 200 OK
Content-Length:
162 Content- Tipo:
application/json Data: Sáb, 04 de
janeiro de 2014 08:38:47 GMT Servidor:
Werkzeug/0.9.4 Python/3.3.3

{
"expiration": 3600,
"token": "eyJpYXQiOjEzODg4MjQ3MjcsImV4cCI6MTM4ODgyODMyNywiYWxnIjoiSFMy..."
}
E agora o token retornado pode ser usado para fazer chamadas na API pela próxima hora, passando-o junto

com um campo de senha vazio:

(venv) $ http --json --auth eyJpYXQ...: GET http://127.0.0.1:5000/api/v1.0/posts/

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.

Serviços Web RESTful com Flask | 193


Machine Translated by Google
Machine Translated by Google

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.

Este capítulo discute maneiras de melhorar e estender o teste de unidade.

Obtendo relatórios de cobertura de código


Ter um conjunto de testes é importante, mas é igualmente importante saber o quão bom ou ruim ele é. As ferramentas
de cobertura de código medem quanto do aplicativo é exercitado por testes de unidade e podem fornecer um relatório
detalhado que indica quais partes do código do aplicativo não estão sendo testadas. Essas informações são valiosas,
pois podem ser usadas para direcionar o esforço de escrever novos testes para as áreas que mais precisam.

Python tem uma excelente ferramenta de cobertura de código apropriadamente chamada de cobertura. Você pode
instalá-lo com pip:

(venv) cobertura de instalação $ 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.

Exemplo 15-1. manage.py: métricas de cobertura


#!/ usr/ bin/ env python
import os
COV = None
if os.environ.get('FLASK_COVERAGE'):
importar cobertura
COV = coverage.coverage(branch=True, include='app/*')
COV.start()

# ...

@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

198 | Capítulo 15: Teste


Machine Translated by Google

executado. Para obter métricas precisas, o script é reiniciado após definir o


Variável de ambiente FLASK_COVERAGE . Na segunda execução, a parte superior do script encontra
que a variável de ambiente está definida e ativa a cobertura desde o início.

A função coverage.coverage() inicia o mecanismo de cobertura. O ramo = Verdadeiro opÿ


ção permite a análise de cobertura de ramificação, que, além de rastrear quais linhas de código
executar, verifica se, para cada condicional, os casos Verdadeiro e Falso foram exÿ
ecutado. A opção include é usada para limitar a análise de cobertura aos arquivos que estão dentro
o pacote do aplicativo, que é o único código que precisa ser medido. Sem o
incluir , todas as extensões instaladas no ambiente virtual e o código para
os próprios testes seriam incluídos nos relatórios de cobertura - e isso acrescentaria muito
ruído ao relatório.

Após a execução de todos os testes, a função text() escreve um relatório no console e


também grava um relatório HTML mais agradável no disco. A versão HTML é muito boa para exibir
cobertura visualmente porque mostra as linhas do código-fonte codificadas por cores de acordo com sua
usar.

Se você tiver clonado o repositório Git do aplicativo no GitHub, poderá


execute git checkout 15a para verificar esta versão do aplicativo.
Para garantir que você tenha todas as dependências instaladas, execute também o pip
instale -r requisitos/dev.txt.

Segue um exemplo do relatório baseado em texto:

(venv) $ python manage.py test --coverage


...
.------------------------------------------------- ---------------------
Realizou 19 testes em 50,609s

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%

Obtendo relatórios de cobertura de código | 199


Machine Translated by Google

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.

Observe que o conteúdo da coluna Ausente foi omitido no relatório de exemplo


para melhorar a formatação. Esta coluna indica as linhas do código-fonte que foram perdidas
pelos testes como uma longa lista de intervalos de números de linha.

O cliente de teste Flask


Algumas partes do código do aplicativo dependem muito do ambiente criado
por um aplicativo em execução. Por exemplo, você não pode simplesmente invocar o código em uma função de exibição

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.

Testando aplicativos da Web O exemplo

15-2 mostra uma estrutura de teste de unidade que usa o cliente de teste.

200 | Capítulo 15: Teste


Machine Translated by Google

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

class FlaskClientTestCase(unittest.TestCase): def


setUp(self): self.app
= create_app('testing') self.app_context
= self.app.app_context() self.app_context.push()
db.create_all()

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

O cliente de teste Flask | 201


Machine Translated by Google

com tokens CSRF em testes, é melhor desabilitar a proteção CSRF na configuração de teste. Isso é
mostrado no Exemplo 15-3.

Exemplo 15-3. config.py: Desativa a proteção CSRF na configuração de teste

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)

# faça login com a nova conta


response = self.client.post(url_for('auth.login'), data={
'email': '[email protected]',
'password': 'cat' },
follow_redirects=True) data =
response.get_data(as_text=True)
self.assertTrue(re.search('Olá,\s+john !', dados))
self.assertTrue('Você ainda não confirmou sua conta' em dados)

# envie um token de confirmação


user = User.query.filter_by(email='[email protected]').first() token =
user.generate_confirmation_token() response =
self.client.get(url_for('auth.confirm', token=token), follow_redirects=True) data =
response.get_data(as_text=True)
self.assertTrue('Você confirmou sua conta' em
dados)

# 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)

202 | Capítulo 15: Teste


Machine Translated by Google

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.

O cliente de teste Flask | 203


Machine Translated by Google

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 15b para verificar esta versão do aplicativo.

Testando serviços da Web O

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

{ 'Autorização': 'Básico '


+ b64encode( (nome de
usuário + ':' + senha).encode('utf-8')).decode('utf-8') , 'Aceitar': 'aplicativo/
json', 'Tipo de conteúdo': 'aplicativo/
json'
}

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.client.get( url, headers=self.get_auth_header('[email protected]', 'cat'))

204 | Capítulo 15: Teste


Machine Translated by Google

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.

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 15c para verificar esta versão do aplicativo.

Teste de ponta a ponta com Selenium


O cliente de teste Flask não pode emular totalmente o ambiente de um aplicativo em execução. Por
exemplo, qualquer aplicativo que dependa do código JavaScript em execução no navegador do cliente
não funcionará, pois o código JavaScript incluído nas respostas retornadas ao teste não será executado
como seria em um cliente de navegador da Web real.

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.

A interface Python para Selenium é instalada com pip:

(venv) $ pip instalar selênio

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

Teste de ponta a ponta com Selenium | 205


Machine Translated by Google

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.

Exemplo 15-6. _app/ main/ views.py: rota de desligamento do servidor


@main.route('/shutdown') def
server_shutdown(): se não
current_app.testing: abort(404)
shutdown =
request.environ.get('werkzeug.server.shutdown') se não for desligamento:

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.

Exemplo 15-7. tests/ test_selenium.py: Framework para testes usando Selenium


do selenium import webdriver

class SeleniumTestCase(unittest.TestCase): cliente


= Nenhum

@classmethod
def setUpClass(cls): #
inicia o Firefox try:

cls.client = webdriver.Firefox() except: pass

# pule esses testes se o navegador não puder ser iniciado se


cls.client: # crie
o aplicativo cls.app =
create_app('testing') cls.app_context =
cls.app.app_context() cls.app_context.push()

206 | Capítulo 15: Teste


Machine Translated by Google

# suprima o log para manter a saída do teste de unidade limpa


import logging
logger = logging.getLogger('werkzeug')
logger.setLevel("ERROR")

# cria o banco de dados e preenche com alguns dados falsos


db.create_all()
Role.insert_roles()
User.generate_fake(10)
Post.generate_fake(10)

# adicione um usuário administrador


admin_role = Role.query.filter_by(permissions=0xff).first() admin =
User(email='[email protected]', username='john',
password='cat', role=admin_role,
confirmado=True) db.session.add(admin)
db.session.commit()

# inicia o servidor Flask em uma thread


threading.Thread(target=cls.app.run).start()

@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()

# remove o contexto do aplicativo


cls.app_context.pop()

def setUp(self): se
não for self.client:
self.skipTest(' Navegador da Web não disponível')

def tearDown(self): passe

Os métodos de classe setUpClass() e tearDownClass() são invocados antes e depois da


execução dos testes nesta classe. A configuração envolve iniciar uma instância do Firefox por
meio da API do webdriver do Selenium e criar um aplicativo e um banco de dados com alguns
dados iniciais para uso nos testes. O aplicativo é iniciado em um thread usando o método
app.run() padrão . No final, o aplicativo recebe uma solicitação para /shutdown, que faz com que
o thread em segundo plano seja encerrado. O navegador é então fechado e o banco de dados de teste removido.

Teste de ponta a ponta com Selenium | 207


Machine Translated by Google

O Selenium suporta muitos outros navegadores além do Firefox.


Consulte a documentação do Selenium se desejar usar um navegador
da web diferente.

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.

Exemplo 15-8. testes/ test_selenium.py: Exemplo de teste de unidade 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))

# navegue até a página de


login self.client.find_element_by_link_text('Log In').click()
self.assertTrue('<h1>Login</h1>' in 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))

# navegue até a página de perfil do usuário


self.client.find_element_by_link_text('Profile').click()
self.assertTrue('<h1>john</h1>' in 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

208 | Capítulo 15: Teste


Machine Translated by Google

o navegador. O Selenium fornece vários métodos de conveniência find_element_by...() que


podem procurar por elementos de maneiras diferentes.

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.

Se você clonou o repositório Git do aplicativo no GitHub, pode executar git


checkout 15d para verificar esta versão do aplicativo.
Esta atualização contém uma migração de banco de dados, então lembre-
se de executar python manage.py db upgrade depois de verificar o código.
Para garantir que você tenha todas as dependências instaladas, execute
também pip install -r requirements/dev.txt.

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.

Vale a pena? | 209


Machine Translated by Google
Machine Translated by Google

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.

Registrando desempenho lento do banco de dados

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

de flask.ext.sqlalchemy import get_debug_queries

@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

Essa funcionalidade é anexada a um manipulador after_app_request , que funciona de maneira semelhante


ao manipulador before_app_request , mas é invocado após a função view que lida com o retorno da solicitação.
Flask passa o objeto de resposta para o manipulador after_app_request caso precise ser modificado.

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.

Tabela 16-1. Estatísticas de consulta registradas por Flask-SQLAlchemy

Nome Descrição

instrução A instrução SQL

parâmetros Os parâmetros usados com a instrução SQL

start_time A hora em que a consulta foi

emitida end_time A hora em que a consulta retornou

duração A duração da consulta em segundos

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.

A função get_debug_queries() é habilitada apenas no modo de depuração por padrão. Infelizmente, os


problemas de desempenho do banco de dados raramente aparecem durante o desenvolvimento porque
bancos de dados muito menores são usados. Por esse motivo, é útil usar essa opção na produção. O Exemplo
16-2 mostra as alterações de configuração necessárias para habilitar o desempenho da consulta do banco de
dados no modo de produção.

Exemplo 16-2. config.py: Configuração para relatórios de consulta lentos

configuração de classe :
# ...
SQLALCHEMY_RECORD_QUERIES =
Verdadeiro FLASKY_DB_QUERY_TIMEOUT
...#
= 0,5

SQLALCHEMY_RECORD_QUERIES informa ao Flask-SQLAlchemy para habilitar o registro de estatísticas


de consulta. O limite de consulta lenta é definido como meio segundo. Ambas as variáveis de configuração
foram incluídas na classe Config base , portanto, serão habilitadas para todas as configurações.

212 | Capítulo 16: Desempenho


Machine Translated by Google

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.

Criação de perfil de código-fonte

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.

A criação de perfil geralmente é feita em um ambiente de desenvolvimento. Um


criador de perfil de código-fonte torna o aplicativo mais lento porque ele precisa
observar e anotar tudo o que está acontecendo. A criação de perfil em um sistema
de produção não é recomendada, a menos que seja usado um criador de perfil
leve projetado especificamente para ser executado em um ambiente de produção.

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.

Exemplo 16-3. manage.py: Execute o aplicativo no criador de perfil de solicitação

@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.

Criação de perfil de código-fonte | 213


Machine Translated by Google

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.

214 | Capítulo 16: Desempenho


Machine Translated by Google

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.

Fluxo de trabalho de implantação

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.

Exemplo 17-1. manage.py: comando de implantação

@manager.command
def deploy():
"""Executar tarefas de implantação."""
from flask.ext.migrate import upgrade from app.models
import Role, User

# migra o banco de dados para a revisão mais recente


upgrade()

# cria funções de usuário


Role.insert_roles()

# crie seguidores próprios para todos os usuários


User.add_self_follows()

215
Machine Translated by Google

As funções invocadas por este comando foram todas criadas antes; eles são apenas invocados todos juntos.

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 17a para verificar esta versão do aplicativo.

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.

Registro de erros durante a produção


Quando o aplicativo está sendo executado no modo de depuração, o depurador interativo do Werkzeug
aparece sempre que ocorre um erro. O rastreamento de pilha do erro é exibido na página da Web e é possível
examinar o código-fonte e até mesmo avaliar expressões no contexto de cada quadro de pilha usando o
depurador interativo baseado na Web do Flask.

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 .

Exemplo 17-2. config.py: Enviar e-mail para erros de aplicativo

classe ProduçãoConfig(Config):
# ...
@classmethod
def init_app(cls, app):
Config.init_app(aplicativo)

# erros de e-mail para os administradores


importar registro
de logging.handlers importar credenciais SMTPHandler
= Nenhuma
seguro = Nenhum

if getattr(cls, 'MAIL_USERNAME', None) não for None:


credenciais = (cls.MAIL_USERNAME, cls.MAIL_PASSWORD)

216 | Capítulo 17: Implantação


Machine Translated by Google

if getattr(cls, 'MAIL_USE_TLS', Nenhum):


secure = ()
mail_handler = SMTPHandler(
mailhost=(cls.MAIL_SERVER, cls.MAIL_PORT),
fromaddr=cls.FLASKY_MAIL_SENDER,
toaddrs=[cls.FLASKY_ADMIN],
subject=cls.FLASKY_MAIL_SUBJECT_PREFIX ' Erro de aplicação',
+ credenciais=credenciais,
seguro=seguro)
mail_handler.setLevel(logging.ERROR) app.
logger.addHandler(mail_handler)

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.

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 17b para verificar esta versão do aplicativo.

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.

Implementação em Nuvem | 217


Machine Translated by Google

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.

Preparando o aplicativo Para trabalhar

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.

Se você planeja hospedar seu aplicativo no Heroku, é uma boa ideia


começar a usar o Git desde o início. O GitHub possui guias de instalação
e configuração para os três principais sistemas operacionais em seu guia
de ajuda .

Criando uma conta no

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.

Instalando o Heroku Toolbelt A

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:

218 | Capítulo 17: Implantação


Machine Translated by Google

• heroku: O cliente Heroku, usado para criar e gerenciar aplicativos • capataz: Uma

ferramenta que pode simular o ambiente Heroku em seu próprio computador


para testar

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:

$ heroku create <appname>


Criando <appname>... pronto, pilha é cedro http://
<appname>.herokuapp.com/ | [email protected]:<appname>.git Git remote heroku
adicionado

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.

Como parte da criação do aplicativo, o Heroku aloca um servidor Git: [email protected]:<nome do


aplicativo>.git. O comando create adiciona este servidor ao seu repositório Git local como um git remote
com o nome heroku.

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:

A Plataforma Heroku | 219


Machine Translated by Google

$ addons heroku:add heroku-postgresql:dev Adicionando


heroku-postgresql:dev em <appname>... pronto, v3 (grátis)
Anexado como HEROKU_POSTGRESQL_BROWN_URL
O banco de dados foi criado e está disponível
! Este banco de dados está vazio. Se atualizar, você pode transferir! dados de
outro banco de dados com pgbackups:restore.
Use `heroku addons:docs heroku-postgresql:dev` para ver a documentação.

A referência HEROKU_POSTGRESQL_BROWN_URL é para o nome da variável de ambiente que


possui a URL do banco de dados. Observe que, ao tentar isso, você pode obter uma cor diferente de
marrom. O Heroku oferece suporte a vários bancos de dados por aplicativo, cada um obtendo uma
cor diferente na URL. Um banco de dados pode ser promovido e expõe sua URL em uma variável de
ambiente DATABASE_URL . O seguinte comando promove o banco de dados brown criado
anteriormente para primário:

$ heroku pg:promote HEROKU_POSTGRESQL_BROWN_URL


Promovendo HEROKU_POSTGRESQL_BROWN_URL para DATABASE_URL... concluído

O formato da variável de ambiente DATABASE_URL é exatamente o que o SQLAlchemy espera.


Lembre-se de que o script config.py usa o valor de DATABASE_URL se estiver definido, portanto a
conexão com o banco de dados Postgres agora funcionará automaticamente.

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.

Exemplo 17-3. config.py: Configuração do Heroku


class HerokuConfig(ProductionConfig): @classmethod
def init_app(cls,
app): ProductionConfig.init_app(app)

# log to stderr import


logging from
logging import StreamHandler file_handler =
StreamHandler()
file_handler.setLevel(logging.WARNING)
app.logger.addHandler(file_handler)

220 | Capítulo 17: Implantação


Machine Translated by Google

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 :

$ heroku config:set FLASK_CONFIG=heroku


Configurando vars de configuração e reiniciando <appname>...
concluído, v4 FLASK_CONFIG: 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:

$ heroku config:set MAIL_USERNAME=<your-gmail-username> $


heroku config:set MAIL_PASSWORD=<your-gmail-password>

A execução de um servidor da web

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:

(venv) $ pip install gunicorn

Para executar o aplicativo no Gunicorn, use o seguinte comando:

(venv) $ gunicorn manage:app


2013-12-03 09:52:10 [14363] [INFO] Iniciando gunicorn 18.0 2013-12-03
09:52:10 [14363] [INFO] Ouvindo em: http:// 127.0.0.1:8000 (14363)
2013-12-03 09:52:10 [14363] [INFO] Using worker: sync 2013-12-03
09:52:10 [14368] [INFO] Booting worker com pid: 14368

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.

A Plataforma Heroku | 221


Machine Translated by Google

Adicionando um arquivo

de requisitos O Heroku carrega as dependências do pacote de um arquivo requirements.txt armazenado


na pasta de nível superior. Todas as dependências neste arquivo serão importadas para um ambiente
virtual criado pelo Heroku como parte da implantação.

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.

O Exemplo 17-4 mostra um exemplo de arquivo de requisitos.

Exemplo 17-4. requirements.txt: arquivo de requisitos do Heroku

-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 Exemplo 17-5 mostra o conteúdo desse arquivo.

Exemplo 17-5. Procfile: Heroku Procfile

web: gunicorn gerenciar: 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.

Os aplicativos podem declarar tarefas adicionais com nomes diferentes


de web no Procfile. Estes podem ser outros serviços necessários para
o aplicativo. O Heroku inicia todas as tarefas listadas no Procfile quando
o aplicativo é implantado.

Testando com o Foreman 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

222 | Capítulo 17: Implantação


Machine Translated by Google

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>

Como o arquivo .env contém senhas e outras informações de conta


confidenciais, ele nunca deve ser adicionado ao repositório Git.

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:

(venv) $ capataz executar python manage.py implantar

O comando start lê o Procfile e executa todas as tarefas nele:

(venv) $ capataz start


22:55:08 web.1 | começou com pid 4246 22:55:08
web.1 | 2013-12-03 22:55:08 [4249] [INFO] Iniciando gunicorn 18.0 22:55:08 web.1 | 2013-12-03
22:55:08 [4249] [INFO] Ouvindo em: http://... 22:55:08 web.1 | 2013-12-03 22:55:08 [4249] [INFO]
Using worker: sync 22:55:08 web.1 | 2013-12-03 22:55:08 [4254] [INFO] Inicializando
trabalhador com pid: 4254

O Foreman consolida a saída de registro de todas as tarefas iniciadas e as despeja no console,


com cada linha prefixada com um carimbo de data/hora e o nome da tarefa.

É 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

Ativando HTTP seguro com Flask-SSLify Quando

o usuário efetua login no aplicativo enviando um nome de usuário e uma senha em um


formulário da Web, esses valores podem ser interceptados durante a viagem por terceiros,
conforme discutido várias vezes antes. Para evitar que as credenciais do usuário sejam
roubadas dessa forma, é necessário usar HTTP seguro, que criptografa todas as comunicações
entre clientes e o servidor usando criptografia de chave pública.

O Heroku disponibiliza todos os aplicativos acessados no domínio herokuapp.com em http:// e


https:// sem qualquer configuração usando o próprio certificado SSL do Heroku

A Plataforma Heroku | 223


Machine Translated by Google

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.

Exemplo 17-7. config.py: Configurar o uso de SSL

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.

224 | Capítulo 17: Implantação


Machine Translated by Google

Um exemplo de quando isso se torna um problema é a geração de URLs de avatar. Se você se


lembra do Capítulo 10, o método gravatar() do modelo User que gera as URLs do Gravatar
verifica request.is_secure para gerar a versão segura ou não segura da URL. Gerar um avatar
não seguro quando a página foi solicitada por SSL faria com que alguns navegadores exibissem
um aviso de segurança para o usuário, portanto, todos os componentes de uma página devem
ter segurança correspondente.

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.

Exemplo 17-8. config.py: Suporte para servidores proxy

classe HerokuConfig(ProductionConfig):
...
# @classmethod
def init_app(cls, app):
# ...

# manipular cabeçalhos de servidor


proxy de werkzeug.contrib.fixers import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app)

O middleware é adicionado no método de inicialização para a configuração do Heroku.


Os middlewares WSGI, como o ProxyFix , são adicionados agrupando o aplicativo WSGI.
Quando chega uma solicitação, os middlewares têm a chance de inspecionar o ambiente e fazer
alterações antes que a solicitação seja processada. O middleware ProxyFix é necessário não
apenas para Heroku, mas em qualquer implantação que use um servidor proxy reverso.

Se você clonou o repositório Git do aplicativo no GitHub, pode executar git


checkout 17c para verificar esta versão do aplicativo.
Para garantir que você tenha todas as dependências instaladas, execute
também pip install -r requirements.txt.

Implantando com git push A

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 :

A Plataforma Heroku | 225


Machine Translated by Google

$ git push heroku master


Contando objetos: 645, concluído.
Compressão Delta usando até 8 threads.
Comprimindo objetos: 100% (315/315), concluído.
Objetos de escrita: 100% (645/645), 95,52 KiB, concluído.
Total 645 (delta 369), reutilizado 457 (delta 288)

.---> Aplicativo Python


detectado .----> Nenhum runtime.txt fornecido; assumindo python-2.7.4.
.----> Preparando o tempo de execução do Python (python-2.7.4)
...
-----> Tamanho do slug compilado: 32,8
MB -----> Lançamento... concluído,
v8 http://<appname>.herokuapp.com implantado no 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:

$ heroku run python manage.py deploy


Executando `python manage.py predeploy` anexado ao terminal... up, run.8449 INFO
[alembic.migration] Contexto impl PostgresqlImpl.
INFO [alembic.migration] Irá assumir DDL transacional.
...

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

O aplicativo agora deve ser totalmente implantado e online em https:// <appname>.heroÿkuapp.com.

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

226 | Capítulo 17: Implantação


Machine Translated by Google

Durante o teste, também pode ser conveniente seguir o arquivo de log, o que pode ser feito da seguinte maneira:

$ heroku logs -t

Implantando uma atualização

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.

Hospedagem Tradicional | 227


Machine Translated by Google

• 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.

Importando variáveis de ambiente Da mesma

forma que o Heroku, um aplicativo executado em um servidor autônomo depende de determinadas


configurações, como URL do banco de dados, credenciais do servidor de e-mail e nome da
configuração. Estes são armazenados em variáveis de ambiente que devem ser importadas antes do início do aplicativo.

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.

Exemplo 17-9. manage.py: Importe o ambiente do arquivo .env

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.

Exemplo 17-10. config.py: exemplo de configuração do Unix

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)

228 | Capítulo 17: Implantação


Machine Translated by Google

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.

Se você clonou o repositório Git do aplicativo no GitHub, pode


executar git checkout 17d para verificar esta versão do aplicativo.

Hospedagem Tradicional | 229


Machine Translated by Google
Machine Translated by Google

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.

Usando um Ambiente de Desenvolvimento Integrado (IDE)


O desenvolvimento de aplicativos Flask em um ambiente de desenvolvimento integrado (IDE) pode
ser muito conveniente, pois recursos como conclusão de código e um depurador interativo podem
acelerar consideravelmente o processo de codificação. Alguns dos IDEs que funcionam bem com o
Flask estão listados aqui:

• 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

Ao configurar um aplicativo Flask para iniciar em um depurador, adicione


as opções --passthrough-errors --no-reload ao comando runserver . A
primeira opção desativa a captura de erros pelo Flask para que as
exceções lançadas durante o tratamento de uma solicitação sejam
enviadas até o depurador. A segunda desativa o módulo recarregador,
o que confunde alguns depuradores.

Encontrando Extensões Flask


Os exemplos neste livro dependem de várias extensões e pacotes, mas há muitos outros que também
são úteis e não foram discutidos. A seguir está uma pequena lista de alguns pacotes adicionais que
valem a pena explorar:

• Flask-Babel: Suporte à internacionalização e localização • Flask-

RESTful: Ferramentas para construir APIs RESTful •

Celery: Fila de tarefas para processamento de trabalhos em

segundo plano • Frozen-Flask: Conversão de um aplicativo Flask em um site

estático • Flask-DebugToolbar: Ferramentas de depuração

no navegador • Flask-Assets: Mesclar, minificar e compilar ativos CSS e JavaScript • Flask-

OAuth: Autenticação contra provedores OAuth • Flask-OpenID:

Autenticação contra provedores de OpenID • Flask-WhooshAlchemy:

Pesquisa de texto completo para modelos Flask-SQLAlchemy com base em


Whoosh

• 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.

Envolva-se com o Flask


O Flask não seria tão incrível sem o trabalho feito por sua comunidade de desenvolvedores.
Como agora você está se tornando parte desta comunidade e se beneficiando do trabalho de tantos
voluntários, considere encontrar uma maneira de retribuir. Aqui estão algumas ideias para ajudá-lo a
começar:

• Revise a documentação do Flask ou de seu projeto relacionado favorito e envie


correções ou melhorias.

232 | Capítulo 18: Recursos Adicionais


Machine Translated by Google

• Traduzir a documentação para um novo idioma. • Responda a

perguntas em sites de perguntas e respostas, como o Stack Overflow. •

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

como código-fonte aberto.

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!

Envolvendo-se com o Flask | 233


Machine Translated by Google
Machine Translated by Google

Índice

decoradores, 115
Símbolos
arquivo .env, 222, 228
E

e-mail, 221

tratamento de erros, 180


Recursos de interfaces de programação de aplicativos
(APIs), 188 controle

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

NoSQL, 50 registro de extensão,


232 função flash, 46 classe Flask, 7 flask.ext
desempenho, 211
modelo relacional, 49 namespace, 17, 26

relacionamentos, 56, 61, 149, 166


SQL, 49

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

variável de contexto g, 13, 15 delete método de sessão, 60

função de modelo get_flashed_messages, 47 função método drop_all, 58 filtro


jsonify, 179 função de consulta filter_by, 63 função
make_response, 16, 161 argumentos de get_debug_queries, 211 modelos, 54
métodos, 42 função de
redirecionamento, 16, 45 função Configuração do MySQL, método
render_template, 22, 46 variável de contexto de consulta de paginação 52 , 191
de solicitação, 12, 13 Configuração do Postgres, 52
Classe de resposta, 16 executores de consulta,
decorador de rota, 8, 14, 79 61 filtros de consulta,
método de execução, 9 61 objetos de consulta, 60
Configuração SECRET_KEY, 76 SQLALCHEMY_COMMIT_ON_TEAR-
desligamento do servidor, Configuração PARA BAIXO, 53
206 variável de contexto de sessão, 13, Configuração SQLALCHEMY_DATABASE_URI, 53, 76
45 método set_cookie, 16
arquivos estáticos, Configuração do SQLite, 52
32 pastas estáticas, Flask-SSLify, 223
33 pastas de modelos, 22 Frasco-WTF, 37
clientes de teste, 200 Classe BooleanField, 96
Mapa de URL, 14 Falsificação de solicitação entre sites (CSRF), 37
função url_for, 32, 45, 81, 106 argumento validadores personalizados, 101

url_prefix, 93 Validador de e-mail, 96


Flask-Bootstrap, 26 Validador EqualTo, 101
blocos, 28 Classe de formulário, 38

macros de forma rápida, 40 campos de formulário, 39

Flask-HTTPauth, 181 Classe PasswordField, 96

Flask-Login, 94 Validador Regexp,


Classe AnonymousUserMixin, 115 variável renderização 101 , 40
de contexto current_user, 96 Validador necessário, 38
Classe LoginManager, 95 Classe StringField, 38
decorador login_required, 95, 107 função Classe SubmitField, 38

login_user, 98 função função valid_on_submit, 98 método


logout_user, 99 valid_on_submit, 42 validadores, 38, 39
Classe UserMixin, 94
decorador user_loader, 95 Capataz, 218, 222
Flask-Mail, 69

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

JavaScript Object Notation (JSON), 177 serialização, R


186
Representational State Transfer (REST), arquivo de
Jinja2, 3, 22
requisitos 175 , 76, 82, 222
diretiva de bloco, 25, 27
Aplicações de Internet Ricas (RIAs), 175
diretiva de extensão, 25, 27
filtros, 23
para diretiva , 24 se S

diretiva, 24 , 41 diretiva HTTP seguro, 223


de importação , 24, 41 diretiva Selenium, 205
de inclusão, 25 diretiva de criador de perfil de código-fonte,
macro, 24 filtro seguro, 213 syslog, 228
24 diretiva de
conjunto , 170 super
macro, 25 herança
Teste T , 192, 197
de modelo, 25 variáveis, 23
testes de unidade, 83, 92,
116 aplicativos da web, 200
serviços da web, 204
L
Twitter Bootstrap, 26
registrando, 211, 211, 216, 220, 226, 228

EM

M
teste de unidade, 83

manage.py, 76, 81, 228 Fragmento de URL, 168


comando de cobertura, 197 funções de usuário,
comando db, 64 111 uWSGI, 221
comando de implantação, 215, 222
comando de perfil, 213
EM
comando runserver, 18 comando
virtualenv, 4
shell, 18, 63 comando de
ativar comando, 5 desativar
teste, 84
comando, 6

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.

Você também pode gostar