What The Flask - 3/6
Extending FlaskFinalmente!!! A terceira parte da série What The Flask, mas ainda não acabou, serão 6 artigos para se tornar um Flasker, neste capítulo falaremos sobre como instalar e configurar as principais extensões do Flask para torna-lo uma solução full-stack com bootstrap no front-end, ORM para banco de dados, admin parecido com o Django Admin, Cache, Sistema de filas (celery/huey), Controle de Acesso, Envio de email, API REST e Login Social.
- Hello Flask: Introdução ao desenvolvimento web com Flask
- Flask patterns: Estruturando aplicações Flask
- Plug & Use: extensões essenciais para iniciar seu projeto. <-- Você está aqui
- DRY: Criando aplicativos reusáveis com Blueprints
- from flask.ext import magic: Criando extensões para o Flask e para o Jinja2
- Run Flask Run: "deploiando" seu app nos principais web servers e na nuvem.
Micro framework? Bom, o Flask foi criado com a premissa de ser um micro-framework, o que significa que ele não tem a intenção de entregar de bandeja para você todas as coisas que você precisa em único pacotinho e nem comandos mágicos que você roda e instantaneamente tem todo o seu projeto pronto. A idéia do Flask é ser pequeno e te dar o controle de tudo o que acontece no seu aplicativo, mas ao mesmo tempo o Flask se preocupa em ser facilmente extensivel, para isso os desenvolvedores pensaram em padrões que permitem que as extensões sejam instaladas de modo que não haja conflitos (lembra dos BluePrints do capítulo anterior?), além dos BluePrints tem também os patterns para desenvolvimento de extensions que ajuda a tornar a nossa vida mais fácil, nesta parte dessa série vamos instalar e configurar algumas das principais extensões do Flask (todas testadas por mim em projetos reais).
CMS de notícias
Nesta série estamos desenvolvendo um mini CMS para publicação de notícias, o código está disponível no github e para cada fase da evolução do projeto tem uma branch diferente. Esse aplicativo de notícias tem os seguintes requisitos:
- Front-end usando o Bootstrap
- Banco de dados MongoDB
- Controle de acesso para que apenas editores autorizados publiquem notícias
- Interface administrativa para as notícias e usuários
- Cache das notícias para minimizar o acesso ao banco de dados
NOTE: Existem várias extensões para Flask, algumas são aprovadas pelos desenvolvedores e entram para a lista disponível no site oficial, algumas entram para a listagem do metaflask (projeto em desenvolvimento), e uma grande parte está apenas no github. Como existem várias extensões que fazem a mesma coisa, as vezes é dificil escolher qual delas utilizar, eu irei mostrar aqui apenas as que eu utilizo e que já tenho experiência, mas isso não quer dizer que sejam as melhores, sinta-se a vontade para tentar com outras e incluir sua sugestão nos comentários.
- Flask Bootstrap - Para deixar as coisas bonitinhas
- Flask MongoEngine - Para armazenar os dados em um banco que é fácil fácil!
- Flask Security - Controle de acesso
- Flask-Admin - Um admin tão poderoso quanto o Django Admin
- Flask Cache - Para deixar o MongoDB "de boas"
- Bônus: Utilizaremos a Flask-DebugToolbar
TL;DR: A versão final do app deste artigo esta no github, os apressados podem querer executar o app e explorar o seu código antes de ler o artigo completo.
Deixando as coisas bonitinhas com o Bootstrap!
Atualmente a versão do nosso CMS está funcional porém bem feinha, não trabalhamos no design das páginas pois obviamente este não é o nosso objetivo, mas mesmo assim podemos deixar as coisas mais bonitinhas.
Atual Layout do nosso CMSCom a ajuda do Bootstrap e apenas uns ajustes básicos no front end podemos transformar o layout em algo muito mais apresentável.
Usaremos a extensão Flask-Bootstrap que traz alguns templates de base e utilidades para uso do Bootstrap no Flask.
Comece editando o arquivo de requirements adicionando Flask-Bootstrap
Arquivo requirements.txt
https://github.com/mitsuhiko/flask/tarball/master
dataset
nose
Flask-Bootstrap
Agora instale as dependencias em sua virtualenv.
pip install -r requirements.txt --upgrade
Agora com o Flask-Bootstrap instalado basta iniciarmos a extensão durante a criação de nosso app.
Editando o arquivo news_app.py incluiremos:
...fromflask_bootstrapimportBootstrapdefcreate_app(mode):......Bootstrap(app)returnapp
Sendo que o arquivo completo ficaria:
# coding: utf-8fromosimportpathfromflaskimportFlaskfrom.blueprints.noticiasimportnoticias_blueprintfromflask_bootstrapimportBootstrapdefcreate_app(mode):instance_path=path.join(path.abspath(path.dirname(__file__)),"%s_instance"%mode)app=Flask("wtf",instance_path=instance_path,instance_relative_config=True)app.config.from_object('wtf.default_settings')app.config.from_pyfile('config.cfg')app.config['MEDIA_ROOT']=path.join(app.config.get('PROJECT_ROOT'),app.instance_path,app.config.get('MEDIA_FOLDER'))app.register_blueprint(noticias_blueprint)Bootstrap(app)returnapp
As extensões Flask seguem dois padrões de inicialização: Imediato e Lazy, é recomendado que toda extensão siga este protocolo.
Inicialização imediata:
fromflask_nome_da_extensaoimportExtensaoapp=Flask(__name__)Extensao(app)
Da forma acima sempre importamos uma classe com o nome da extensao e então passamos o nosso app como parametro na inicialiação da extensão. Assim durante o init da extensão ela poderá injetar templates, modificar rotas e adicionar configs no app que foi passado como parametro.
Inicialização Lazy:
fromflask_nome_da_extensaoimportExtensaoapp=Flask(__name__)extensao=Extensao()# note que não é passado nada como parametro!# em qualquer momento no seu códigoExtensao.init_app(app)
Geralmente o primeiro modo inicio imediatoé o mais utilizado, o carregamento Lazy é útil em situações mais complexas como por exemplo se o seu sistema estiver esperando a conexão com um banco de dados.
NOTE: Toda extensão do Flask deve começar com o nome flask_ para ser considerada uma extensão dentro dos padrões.
No nosso caso utilizamos Bootstrap(app) e agora o bootstrap já está disponível para ser utilizado em nossos templates!
Customizando os templates com o BootStrap.
Precisaremos efetuar algumas mudanças nos templates para que eles utilizem os estilos do Bootstrap 3.x
Não entrarei em detalhes a respeito da estensão Flask-Bootstrap pois temos mais uma série de extensões para instalar, mas você pode consultar a documentação oficial para saber mais a respeito dos blocos de template e utilidades disponíveis.
Comece alterando o template base.html para:
{%- extends "bootstrap/base.html" %}
{% import "bootstrap/utils.html" as utils %}
{% block title %} {{title or "Notícias"}} {% endblock %}
{% block navbar -%}
<navclass="navbar navbar-default"><aclass="navbar-brand"href="#"><imgsrc="{{url_for('static', filename='generic_logo.gif')}}"style="height:30px;"></a><ulclass="nav navbar-nav"><li><ahref="{{url_for('noticias.index')}}">HOME</a></li><li><ahref="{{url_for('noticias.cadastro')}}">CADASTRO</a></li></ul></nav>
{%- endblock navbar %}
{% block content %}
<divclass="container"><divclass="jumbotron">
{% block news %}
{% endblock %}
</div>
{%- endblock content%}
Algumas coisas continuaram iguais ao template antigo, porém agora estamos utilizando blocos navbar e content definidos pelo Flask-Bootstrap e criamos um novo bloco news que usaremos para mostras as nossas notícias.
Altere o template index.html
{% extends "base.html" %}
{% block news %}
<h1>Todas as notícias</h1><ul>
{% for noticia in noticias %}
<li><ahref="{{url_for('noticias.noticia', noticia_id=noticia.id)}}">
{{noticia.titulo}}
</a></li>
{% endfor %}
</ul>
{% endblock %}
apenas mudamos o nome do bloco utilizado de content para news e adicionamos um título.
Altere o template noticia.html
{% extends "base.html" %}
{% block title%}
{{noticia.titulo}}
{% endblock%}
{% block news %}
<h1>{{noticia.titulo}}</h1>
{% if noticia.imagem %}
<imgsrc="{{ url_for('noticias.media', filename=noticia.imagem) }}"width="300"/>
{% endif %}
<hr/><p>
{{noticia.texto|safe}}
</p>
{% endblock %}
Novamente mudamos o bloco principal de content para news
Altere os templates cadastro.html e cadastro_sucesso.html
Altere o bloco utilizado de content para news e o restante deixe como está por enquanto.
{% extends "base.html" %}
{% block news %}
<formmethod="post"action="{{ url_for('noticias.cadastro') }}"enctype="multipart/form-data"><label>Titulo:<br/><inputtype="text"name="titulo"id="titulo"/></label><br/><label>Texto:<br/><textareaname="texto"id="texto"></textarea></label><br/><label> Imagem:<br/><inputtype="file"name="imagem"id="imagem"/></label><inputtype="submit"value="Postar"/></form>
{% endblock %}
Resultado Final!
Antes do bootstrap
e depois:
O diff com as mudanças que foram feitas pode ser acessado neste link
Agora que o app já está com uma cara mais bonita, vamos passar para o próximo requisito: Banco de Dados.
Até agora usamos o dataset que é uma mão na roda! integrando facilmente o nosso projeto com bancos como MySQL, Postgres ou SQLite.
Porém nosso site de notícias precisa utilizar MongoDB e para isso vamos recorrer ao Flask-MongoEngine
Utilizando MongoDB com Flask
Vamos migrar do Dataset + SQlite para MongoDB e obviamente você irá precisar do MongoDb Server rodando, você pode preferir instalar o Mongo localmente se estiver em uma máquina Linux/Debian: sudo apt-get install mongodb
ou siga instruções no site oficial do Mongo de acordo com seu sistema operacional.
Uma opção melhor é utilizar o docker para executar o MongoDB, desta forma você não precisa instalar o servidor de banco de dados em seu computador, precisa apenas do Docker e da imagem oficial do Mongo.
Vou mostrar os preocedimentos para instalação e execução no Linux/Debian, mas você pode tranquilamente utilizar outro S.O bastando seguir as instruções encontradas no site do docker.
Instalando o docker
No linux a maneira mais fácil de instalar o Docker é rodando o seguinte comando
wget -qO- https://get.docker.com/ | sh
Se você precisar de ajuda ou estiver usando um sistema operacional alternativo pode seguir as instruções do site oficial
Executando um container MongoDB
No DockerHub está disponível a imagem oficial do Mongo, basta executar o comando abaixo para ter o Mongo rodando em um container.
docker run -d -v $PWD/mongodata:/data/db -p 27017:27017 mongo
A parte do $PWD/mongodata:
pode ser substituida pelo caminho de sua preferencia, este é o local onde o Mongo irá salvar os dados.
Se preferir executar no modo efemero (perdendo todos os dados ao reiniciar o container) execute apenas
docker run -d -p 27017:27017 mongo
Instalando o MongoDB localmente (não recomendado, use o docker!)
Você pode preferir não usar o Docker e instalar o Mongo localmente, baixe o mongo descompacte, abra um console separado e execute: ./bin/mongod --dbpath /tmp/
lembrando de trocar o /tmp
por um diretório onde queira salvar seus dados.
Se preferir utilize os pacotes oficiais do seu sistema operacional para instalar o Mongo.
IMPORTANTE: Para continuar você precisa ter uma instância do MongoDB rodando localmente, no Docker ou até mesmo em um servidor remoto se preferir.
Flask-Mongoengine
Adicione a extensão Flask-Mongoengine ao seu arquivo requirements.txt
https://github.com/mitsuhiko/flask/tarball/master
flask-mongoengine
nose
Flask-Bootstrap
Agora execute pip install -r requirements.txt --upgrade
estando na virtualenv de seu projeto.
Substituindo o Dataset pelo MongoEngine
Agora vamos substituir o dataset pelo MongoEngine, por padrão o MongoEngine tentará conectar no localhost na porta 27017 e utilizar o banco de dados test. Mas no nosso caso é essencial informarmos exatamente as configurações desejadas.
No arquivo de configuração em development_instance/config.cfg
adicione as seguintes linhas:
MONGODB_DB="noticias"MONGODB_HOST="localhost"# substitua se utilizar um server remotoMONGODB_PORT=27017
Agora vamos ao arquivo db.py
vamos definir a conexão com o banco de dados Mongo, apague todo o conteúdo do arquivo e substitua por:
db.py
# coding: utf-8fromflask_mongoengineimportMongoEnginedb=MongoEngine()
Crie um novo arquivo chamado models.py, é nesse arquivo que definiremos o esquema de dados nas nossas notícias. Note que o Mongo é um banco schemaless, poderiamos apenas criar um objeto Noticia(db.DynamicDocument) usando herança do DynamicDocument e isso tiraria a necessidade da definição do schema, porém, na maioria dos casos definir um schema básico ajuda a construir formulários e validar os dados.
models.py
# coding: utf-8from.dbimportdbclassNoticia(db.Document):titulo=db.StringField()texto=db.StringField()imagem=db.StringField()
Nosso próximo passo é alterar as views para que o armazenamento seja feito no MongoDB ao invés do SQLite.
No MongoEngine algumas operações serão um pouco diferente, alguns exemplos:
Criar um novo registro de Noticia
Noticia.objects.create(titulo='Hello',texto='World',imagem='caminho/imagem.png')
Buscar todas as Noticias
Noticia.objects.all()
Buscar uma noticia pelo id
Noticia.objects.get(id='xyz')
Altere blueprints/noticias.py
para:
# coding: utf-8importosfromwerkzeugimportsecure_filenamefromflaskimport(Blueprint,request,current_app,send_from_directory,render_template)from..modelsimportNoticianoticias_blueprint=Blueprint('noticias',__name__)@noticias_blueprint.route("/noticias/cadastro",methods=["GET","POST"])defcadastro():ifrequest.method=="POST":dados_do_formulario=request.form.to_dict()imagem=request.files.get('imagem')ifimagem:filename=secure_filename(imagem.filename)path=os.path.join(current_app.config['MEDIA_ROOT'],filename)imagem.save(path)dados_do_formulario['imagem']=filenamenova_noticia=Noticia.objects.create(**dados_do_formulario)returnrender_template('cadastro_sucesso.html',id_nova_noticia=nova_noticia.id)returnrender_template('cadastro.html',title=u"Inserir nova noticia")@noticias_blueprint.route("/")defindex():todas_as_noticias=Noticia.objects.all()returnrender_template('index.html',noticias=todas_as_noticias,title=u"Todas as notícias")@noticias_blueprint.route("/noticia/<noticia_id>")defnoticia(noticia_id):noticia=Noticia.objects.get(id=noticia_id)returnrender_template('noticia.html',noticia=noticia)@noticias_blueprint.route('/media/<path:filename>')defmedia(filename):returnsend_from_directory(current_app.config.get('MEDIA_ROOT'),filename)
Lembre-se que nós ainda não conectamos ao Mongo Server apenas definimos como será a conexão, então precisaremos agora usar o método lazy de inicialização de extensões chamando o init_app()
do MongoEngine.
No arquivo news_app.py
adicione as seguintes linhas.
...fromdbimportdbdefcreate_app(mode):...db.init_app(app)returnapp
Sendo que o arquivo final será:
# coding: utf-8fromosimportpathfromflaskimportFlaskfrom.blueprints.noticiasimportnoticias_blueprintfromflask_bootstrapimportBootstrapfromdbimportdbdefcreate_app(mode):instance_path=path.join(path.abspath(path.dirname(__file__)),"%s_instance"%mode)app=Flask("wtf",instance_path=instance_path,instance_relative_config=True)app.config.from_object('wtf.default_settings')app.config.from_pyfile('config.cfg')app.config['MEDIA_ROOT']=path.join(app.config.get('PROJECT_ROOT'),app.instance_path,app.config.get('MEDIA_FOLDER'))app.register_blueprint(noticias_blueprint)Bootstrap(app)db.init_app(app)returnapp
Execute o programa
pythonrun.py
E veja se consegue inserir algumas noticias acessando http://localhost:5000
Para explorar os dados do MongoDB visualmente você pode utilizar o RoboMongo.
O Diff das alterações que fizemos relativas ao Flask-MongoEngine podem ser comparadas nos seguintes commits 88effa01b5ffd11f3fd7d5530f90591e421dd109 e 189f4d4d2c8af845ccc0b181e4f6a1831578fbfa
Controle de acesso com o Flask Security
Nosso CMS de notícias está inseguro, ou seja, qualquer um que acessar a url http://localhost:5000/noticias/cadastro vai conseguir adicionar uma nova notícia sem precisar efetuar login.
Para resolver este tipo de problema existe a extensão Flask-Login que oferece métodos auxiliares para autenticar usuários e também a Flask-Security é um pacote feito em cima do Flask-Login (controle de autenticação), Flask-Principal (Controle de Permissões) e Flask-Mail (envio de email).
A vantagem de usar o Flask-Security é que ele já se integra com o MongoEngine e oferece templates prontos para login, alterar senha, envio de email de confirmação etc...
Começaremos adicionando a dependencia ao arquivo de requirements.
requirements.txt
https://github.com/mitsuhiko/flask/tarball/master
flask-mongoengine
nose
Flask-Bootstrap
Flask-Security
E então instalamos com pip install -r requirements.txt --upgrade
Secret Key
Para encriptar os passwords dos usuários o Flask-Login irá utilizar a chave secret key do settings de seu projeto. É muito importante que esta chave seja segura e gerada de maneira randomica (utilize uuid4 ou outro método de geração de chaves).
Para testes e desenvolvimento você pode utilizar texto puro. mas em produção escolha uma chave segura!
Além disso o Flask-Security precisa que seja especificado qual tipo de hash usar nos passwords.
Adicione ao development_instance/config.cfg
SECRET_KEY='super-secret'SECURITY_PASSWORD_HASH='pbkdf2_sha512'SECURITY_PASSWORD_SALT=SECRET_KEY
Importante se esta chave for perdida todas as senhas armazenadas serão invalidadas.
Definindo o schema dos usuários e grupos
O Flask-Security permite o controle de acesso utilizando RBAC (Role Based Access Control), ou seja, usuários pertencem a grupos e os acessos são concedidos aos grupos.
Para isso precisamos armazenar (no nosso caso no MongoDB) os usuários e seus grupos.
Crie um novo arquivo security_models.py e criaremos duas classes User e Role
# coding: utf-8from.dbimportdbfromflask_securityimportUserMixin,RoleMixinfromflask_security.utilsimportencrypt_passwordclassRole(db.Document,RoleMixin):name=db.StringField(max_length=80,unique=True)description=db.StringField(max_length=255)@classmethoddefcreaterole(cls,name,description=None):returncls.objects.create(name=name,description=description)classUser(db.Document,UserMixin):name=db.StringField(max_length=255)email=db.EmailField(max_length=255,unique=True)password=db.StringField(max_length=255)active=db.BooleanField(default=True)confirmed_at=db.DateTimeField()roles=db.ListField(db.ReferenceField(Role,reverse_delete_rule=db.DENY),default=[])last_login_at=db.DateTimeField()current_login_at=db.DateTimeField()last_login_ip=db.StringField(max_length=255)current_login_ip=db.StringField(max_length=255)login_count=db.IntField()@classmethoddefcreateuser(cls,name,email,password,active=True,roles=None,username=None,*args,**kwargs):returncls.objects.create(name=name,email=email,password=encrypt_password(password),active=active,roles=roles,username=username,*args,**kwargs)
O arquivo acima define os models com todas as propriedades necessárias para que o Flask-Security funcione com o MongoEngine, não entrerei em detalhes de cada campo pois usaremos somente o básico neste tutorial, acesse a documentação do Flask-Security se desejar saber mais a respeitod e cada atributo.
Inicializando o Flask Security em seu projeto
Da mesma forma que fizemos com as outras extensões iremos fazer como security, alterando o arquivo news_app.py e inicializando a extensão utilizando o método default.
Importaremos o Security e o MongoEngineUserDatastore e inicializaremos a extensão passando nossos models de User e Role.
...fromflask_securityimportSecurity,MongoEngineUserDatastorefrom.dbimportdbfrom.security_modelsimportUser,Roledefcreate_app(mode):...Security(app=app,datastore=MongoEngineUserDatastore(db,User,Role))returnapp
news_app.py
# coding: utf-8fromosimportpathfromflaskimportFlaskfromflask_bootstrapimportBootstrapfromflask_securityimportSecurity,MongoEngineUserDatastorefrom.blueprints.noticiasimportnoticias_blueprintfrom.dbimportdbfrom.security_modelsimportUser,Roledefcreate_app(mode):instance_path=path.join(path.abspath(path.dirname(__file__)),"%s_instance"%mode)app=Flask("wtf",instance_path=instance_path,instance_relative_config=True)app.config.from_object('wtf.default_settings')app.config.from_pyfile('config.cfg')app.config['MEDIA_ROOT']=path.join(app.config.get('PROJECT_ROOT'),app.instance_path,app.config.get('MEDIA_FOLDER'))app.register_blueprint(noticias_blueprint)Bootstrap(app)db.init_app(app)Security(app=app,datastore=MongoEngineUserDatastore(db,User,Role))returnapp
Pronto, agora temos nossa base de usuários e grupos definida e o Security irá iniciar em nosso app todo o restante necessário para o controle de login (session, cookies, formulários etc..)
Exigindo login para cadastro de notícia
Altere a view de cadastro em blueprints/noticias.py
e utilize o decorator login_required
que é disponibilizado pelo Flask-Security, sendo que o inicio do arquivo ficará assim:
# coding: utf-8importosfromwerkzeugimportsecure_filenamefromflaskimport(Blueprint,request,current_app,send_from_directory,render_template)from..modelsimportNoticiafromflask_securityimportlogin_required# decoratornoticias_blueprint=Blueprint('noticias',__name__)@noticias_blueprint.route("/noticias/cadastro",methods=["GET","POST"])@login_required# aqui o login será verificadodefcadastro():...
Execute python run.py
acesse http://localhost:5000/noticias/cadastro e verifique que o login será exigido para continuar.
NOTE: Se por acaso ocorrer um erro TypeError: 'bool' object is not callable execute o seguinte comando
pip install Flask-Login==0.2.11
e adicioneFlask-Login==0.2.11
no arquivo requirements.txt. Este erro ocorre por causa de um recente bug na nova versão do Flask-Login.
Se tudo ocorrer como esperado agora você será encaminhado para a página de login.
O único problema é que você ainda não possui um usuário para efetuar o login. Em nosso model de User definimos um método create_user que pode ser utilizado diretamente em um terminal iPython. Porém o Flask-Security facilita bastante fornecendo também um formulário de registro de usuários.
Adicione as seguintes configurações no arquivo development_instance/config.cfg
para habilitar o formulário de registro de usuários.
SECURITY_REGISTERABLE=TrueSECURITY_TRACKABLE=True# para armazenar data, IP, ultimo login dos users.# as opções abaixo devem ser removidas em ambiente de produçãoSECURITY_SEND_REGISTER_EMAIL=FalseSECURITY_LOGIN_WITHOUT_CONFIRMATION=TrueSECURITY_CHANGEABLE=True
Agora acesse http://localhost:5000/register e você poderá registar um novo usuário e depois efetuar login.
NOTE: É recomendado que a opção de registro de usuário seja desabilidata em ambiente de produção, que seja utilizado outros meios como o Flask-Admin que veremos adiante para registrar novos usuários ou que seja habilitado o Captcha para os formulários de registro e login e também o envio de email de confirmação de cadastro.
Todas as opções de configuração do Flsk-Security estão disponíveis em https://pythonhosted.org/Flask-Security/configuration.html
Agora será interessante mostrar opções de Login, Logout, Alterar senha na barra de navegação. Para isso altere o template base.html
adicionando o bloco de access control.
{%- extends "bootstrap/base.html" %}
{% import "bootstrap/utils.html" as utils %}
{% block title %} {{title or "Notícias"}} {% endblock %}
{% block navbar -%}
<navclass="navbar navbar-default"><aclass="navbar-brand"href="#"><imgsrc="{{url_for('static', filename='generic_logo.gif')}}"style="height:30px;"></a><ulclass="nav navbar-nav"><li><ahref="{{url_for('noticias.index')}}">HOME</a></li><li><ahref="{{url_for('noticias.cadastro')}}">CADASTRO</a></li>
{% block access_control %}
<liclass="divider-vertical"></li>
{% if current_user.is_authenticated() %}
<liclass="dropdown"><ahref="#"class="dropdown-toggle"data-toggle="dropdown">
{{current_user.email}} <bclass="caret"></b></a><ulclass="dropdown-menu"><li><ahref="{{url_for_security('change_password')}}"><iclass="icon-user"></i> Change password</a></li><li><ahref="{{url_for_security('logout')}}"><iclass="icon-off"></i> Logout</a></li></ul></li>
{% else %}
<li><ahref="{{url_for_security('login')}}"><iclass="icon-off"></i> Login</a></li>
{% endif %}
{% endblock %}
</ul></nav>
{%- endblock navbar %}
{% block content %}
<divclass="container">
{%- with messages = get_flashed_messages(with_categories=True) %}
{%- if messages %}
<divclass="row"><divclass="col-md-12">
{{utils.flashed_messages(messages)}}
</div></div>
{%- endif %}
{%- endwith %}
<divclass="jumbotron">
{% block news %}
{% endblock %}
</div>
{%- endblock content%}
O resultado final será:
As opções de customização e instruções de como alterar os formulários e templates do Flask-Security encontram-se na documentação oficial.
O diff com todas as alterações feitas com o Flask-Security pode ser consultado neste link
Flask Admin - Um admin tão poderoso quanto o Django Admin!
Todos sabemos que uma das grandes vantagens de um framework full-stack como Django ou Web2py é a presença de um Admin para o banco de dados. Mesmo sendo Micro-Framework o Flask conta com a extensão Flask-Admin que o transforma em uma solução tão completa quanto o Django-Admin.
O Flask-Admin é uma das mais importantes extensões para o Flask e é frequentemente atualizada e tem uma comunidade muito ativa! O AirBnb recentemente lançou o AirFlow que utiliza o Flask-Admin
E o QuokkaCMS, principal CMS desenvolvido com Flask e MongoDB é baseado também no Flask-Admin.
Para começar vamos colocar os requisitos no arquivo de requirements!!
requirements.txt
https://github.com/mitsuhiko/flask/tarball/master
flask-mongoengine
nose
Flask-Bootstrap
Flask-Security
Flask-Login==0.2.11
Flask-Admin
Instalar com pip install -r requirements.txt --upgrade
Admin para o seu banco de dados MongoDB!!!
O Flask-Admin é um painel administrativo para bancos de dados de seus projetos Flask e ele tem suporte a diversos ORMs e Tecnologias como MySQL, PostGres, SQLServer e ORMs SQLAlchemy, Peewee, PyMongo e MongoEngine.
O Flask Admin utiliza o Bootstrap por padrão para a camada visual do admin, mas é possivel customizar com o uso de temas.
A primeira coisa a ser feita depois de ter o Flask-Admin instalado é inicializar o admin da mesma maneira que fizemos com as outras extensões.
Vamos adicionar as seguintes linhas ao arquivo news_app.py
fromflask_adminimportAdmin...defcreate_app(mode):...admin=Admin(app,name='Noticias',template_mode='bootstrap3')returnapp
Ficando o arquivo completo.
# coding: utf-8fromosimportpathfromflaskimportFlaskfromflask_bootstrapimportBootstrapfromflask_securityimportSecurity,MongoEngineUserDatastorefromflask_adminimportAdminfrom.blueprints.noticiasimportnoticias_blueprintfrom.dbimportdbfrom.security_modelsimportUser,Roledefcreate_app(mode):instance_path=path.join(path.abspath(path.dirname(__file__)),"%s_instance"%mode)app=Flask("wtf",instance_path=instance_path,instance_relative_config=True)app.config.from_object('wtf.default_settings')app.config.from_pyfile('config.cfg')app.config['MEDIA_ROOT']=path.join(app.config.get('PROJECT_ROOT'),app.instance_path,app.config.get('MEDIA_FOLDER'))app.register_blueprint(noticias_blueprint)Bootstrap(app)db.init_app(app)Security(app=app,datastore=MongoEngineUserDatastore(db,User,Role))admin=Admin(app,name='Noticias',template_mode='bootstrap3')returnapp
Agora basta executar python run.py
e acessar http://localhost:5000/admin/ e você verá a tela index do Flask-Admin.
Se você conseguir acessar a tela acima então o Flask-Admin está inicializado corretamente, perceba que não tem nada além de uma tela em branco e um botão "home".
Precisamos agora registrar nossas ModelViews que são as telas de administração para cada coleção ou tabela do banco de dados e também implementar a integração com o Flask-Security para garantir que somente pessoas autorizadas acessem o admin.
Menu de controle de acesso
Em nosso front-end incluimos na barra de menus os links de controle de acesso login, alterar senha e logout, precisamos agora incluir os mesmos itens na barra de menus do Flask-Admin.
Para começar crie um template novo em templates/admin_base.html com o seguinte conteúdo.
{% extends 'admin/base.html' %}
{% block access_control %}
<divclass="navbar-text btn-group pull-right"><ahref="#"class="dropdown-toggle"data-toggle="dropdown"role="button"aria-expanded="false"><iclass="glyphicon glyphicon-user"></i>
{% if current_user.is_authenticated() %}
{% if current_user.name -%}
{{ current_user.name }}
{% else -%}
{{ current_user.email }}
{%- endif %}
<spanclass="caret"></span></a><ulclass="dropdown-menu"role="menu"><li><ahref="{{url_for_security('logout')}}">Log out</a></li></ul>
{% else %}
Access
<spanclass="caret"></span></a><ulclass="dropdown-menu"role="menu"><li><ahref="{{url_for_security('login')}}">Login</a></li></ul>
{% endif %}
</div>
{% endblock %}
Agora altere o template base do Flask-Admin incluindo o parametro base_template='admin_base.html'
no news_app.py
def create_app(mode):
...
admin = Admin(app, name='Noticias', template_mode='bootstrap3',
base_template='admin_base.html')
return app
O Flask-Admin não possui uma forma automática de integração com o Flask-Security, porém podemos facilmente sobrescrever a classe de ModelView incluindo o controle de acesso necessário.
Para que isso fique mais fácil vamos centralizar a configuração do Flask-Admin em um único arquivo.
Crie um arquivo chamado wtf/admin.py na raiz do projeto, iremos extender a ModelView do Flask-Admin tornando-a segura e exigindo login e também iremos registrar os models Noticia, User e Role em nosso painel de adminsitração.
NOTE: Se desejar que apenas usuários que pertençam ao grupo admin tenham acesso descomente as linhas do método is_acessible
wtf/admin.py
# coding: utf-8fromflaskimportabort,redirect,request,url_forfromflask_securityimportcurrent_userfromflask_admin.contrib.mongoengineimportModelViewfromflask_adminimportAdminfrom.modelsimportNoticiafrom.security_modelsimportUser,Roleadmin=Admin(name='Noticias',template_mode='bootstrap3',base_template='admin_base.html')# Create customized model view classclassSafeModelView(ModelView):defis_accessible(self):ifnotcurrent_user.is_authenticated():returnFalse# if not current_user.has_role('admin'):# return FalsereturnTruedef_handle_view(self,name,**kwargs):""" Redireciona o usuário para página de login ou de acesso negado"""ifnotself.is_accessible():ifcurrent_user.is_authenticated():abort(403)# negado, caso não pertença ao grupo admin.else:returnredirect(url_for('security.login',next=request.url))defconfigure_admin(app):admin.init_app(app)admin.add_view(SafeModelView(Noticia))admin.add_view(SafeModelView(User,category='accounts'))admin.add_view(SafeModelView(Role,category='accounts'))
Agora só precisamos substituir a maneira como inicializamos o admin pela chamada a função configure_admin.
No arquivo news_app.py troque a parte
fromflask_adminimportAdmin...defcreate_app(mode):...admin=Admin(name='Noticias',template_mode='bootstrap3',base_template='admin_base.html')returnapp
Pelo uso da função configure_admin
from.adminimportconfigure_admin...defcreate_app(mode):...configure_admin(app)returnapp
Desta forma ao executar python run.py
e acessar http://localhost:5000/admin/ estando logado você irá os menus referentes aos nossos models e poderá editar/apagar/adicionar novos usuários e notícias.
O Flask-admin possui diversas opções de customização de template, formulários, permissões. Você pode limitar quais campos exibir, alterar o comportamento dos formulários e até mesmo incluir views e formulários que não estejam no banco de dados.
Limitando os campos a serem exibidos.
Atualmente clicando no menu accounts/user a lista exibe vários campos referentes ao cadastro de usuários (incluindo a senha encriptada).
Altere nosso arquivo admin.py e crie uma nova classe UserModelView onde limitaremos os campos que serão listados no admin.
wtf/admin.py
...classUserModelView(SafeModelView):column_list=("name","email","active","last_login_at","login_count")...
E agora no final do arquivo utilize esta classe ao adicionar a view.
...admin.add_view(UserModelView(User,category='accounts'))...
NOTE: Não iremos nos aprofundar em todas as opções de customização do Flask-admin, portanto consulte a documentação para saber mais.
O diff com todas as alterações referentes ao Flask-Admin estão no commit 1359c4cf9a31a0d471a3226a1bec2a672f9ffbbb
Flask Cache - Deixando o MongoDB "de boas"
A cada vez que acessamos a home do nosso site uma consulta é feita ao MongoDB, e isso está definido em blueprints/noticias.py
@noticias_blueprint.route("/")defindex():todas_as_noticias=Noticia.objects.all()returnrender_template('index.html',noticias=todas_as_noticias,title=u"Todas as notícias")
Na linha todas_as_noticias = Noticia.objects.all()
fazemos uma query ao MongoDB, se 10.000 usuários acessarem a nossa página 10.000 acessos serão feitos ao MongoDB.
O mesmo acontece na view de acesso a uma notícia noticia = Noticia.objects.get(id=noticia_id)
vai até o MongoDB e faz a query procurando a notícia pelo id e teremos novamente problemas se por exemplo muitos usuários acessarem a mesma notícia ao mesmo tempo.
Podemos otimizar as queries no Mongo utilizando indices porém o mais indicado é o uso de cache.
Em ambiente de alta disponibilidade é altamente recomendado usar um servidor de cache como o Varnish para servir de camada intermediária ou até mesmo gerar páginas HTML estáticas de cada uma das notícias.
Mas em caso de sites menores podermos contar com um sistema de cache mais simples utilizando MemCached, Redis ou até mesmo sistema de arquivos para armazenar o cache.
Em nosso projeto usaremos o Flask-Cache que poderá ser usado com Redis ou da maneira simples utilizando o sistema de arquivos.
Debug Toolbar
Antes de mais nada precisamos de uma ajuda para enxergarmos o problema, vamos utilizar a Flask-DebugToolbar para nos mostrar o custo de nossas idas ao banco de dados e outras coisas uteis para o desenvolvimento em Flask.
Adicione ao arquivo requirements.txt a flask-debugtoolbar e utilize o flask-mongoengine diretamente do github.
https://github.com/mitsuhiko/flask/tarball/master
https://github.com/MongoEngine/flask-mongoengine/tarball/master
nose
Flask-Bootstrap
Flask-Security
Flask-Login==0.2.11
Flask-Admin
flask-debugtoolbar
flask-cache
NOTE: usaremos o flask-mongoengine diretamente do github https://github.com/MongoEngine/flask-mongoengine/tarball/master pois a versão do PyPI ainda não está compatível com a debug-toolbar
E atualize sua envpip install -r requirements.txt --upgrade
Agora edite o arquivo development_instance/config.cfg adicionando as seguintes entradas.
development_instance/config.cfg
DEBUG=TrueDEBUG_TOOLBAR_ENABLED=TrueDEBUG_TB_INTERCEPT_REDIRECTS=FalseDEBUG_TB_PROFILER_ENABLED=TrueDEBUG_TB_TEMPLATE_EDITOR_ENABLED=TrueDEBUG_TB_PANELS=('flask_debugtoolbar.panels.versions.VersionDebugPanel','flask_debugtoolbar.panels.timer.TimerDebugPanel','flask_debugtoolbar.panels.headers.HeaderDebugPanel','flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel','flask_debugtoolbar.panels.template.TemplateDebugPanel','flask_mongoengine.panels.MongoDebugPanel','flask_debugtoolbar.panels.logger.LoggingPanel','flask_debugtoolbar.panels.profiler.ProfilerDebugPanel','flask_debugtoolbar.panels.config_vars.ConfigVarsDebugPanel',)
Inicialize a extensão no arquivo news_app.py
...fromflask_debugtoolbarimportDebugToolbarExtension...defcreate_app(mode):...DebugToolbarExtension(app)returnapp
Agora execute o projeto python run.py
e navegue pelo site e pelo admin e analise os painéis do Flask-DebugToolbar que aparecerão na lateral direita.
Note que temos vários painéis de DEBUG incluindo o painel MongoDB exibindo o tempo consumido para o acesso ao banco de dados, clicando neste botão da toolbar você poderá visualizar as queries que foram feitas ao MongoDB.
E também é interessante analisar o botão Profiler que exibe e o consumo de memória e CPU em cada parte de nosso app.
No nosso caso como estamos em ambiente de desenvolvimento e com apenas um usuário fazendo requests os números serão insignificantes, porém basta colocar em produção e ter um slashdot effect que as coisas começarão a complicar.
Portanto vamos utilizar o Flask-Cache para minimizar o acesso ao MongoDB.
O Flask-Cache possui integração com alguns sistemas de cache e são eles:
Built-in cache types:
- null: NullCache (default)
- simple: SimpleCache
- memcached: MemcachedCache (pylibmc or memcache required)
- gaememcached: GAEMemcachedCache
- redis: RedisCache (Werkzeug 0.7 required)
- filesystem: FileSystemCache
- saslmemcached: SASLMemcachedCache (pylibmc required)
NOTE: O mais recomendado é o uso de Redis ou memcached mas como isso exige a instalação de libs adicionais utilizaremos o FileSystemCache em nosso exemplo, porém o uso de cache em file system pode ser até mais lento que o acesso direto ao banco, portanto vamos utiliza-lo somente como exemplo.
Crie um arquivo cache.py na raiz do projeto:
fromflask_cacheimportCachecache=Cache(config={'CACHE_TYPE':'filesystem','CACHE_DIR':'/tmp'})
Vamos inicializar a extensão da mesma forma que fizemos com as outras e neste ponto nosso arquivo news_app.py deverá estar assim:
# coding: utf-8fromosimportpathfromflaskimportFlaskfromflask_bootstrapimportBootstrapfromflask_securityimportSecurity,MongoEngineUserDatastorefromflask_debugtoolbarimportDebugToolbarExtensionfrom.adminimportconfigure_adminfrom.blueprints.noticiasimportnoticias_blueprintfrom.dbimportdbfrom.security_modelsimportUser,Rolefrom.cacheimportcachedefcreate_app(mode):instance_path=path.join(path.abspath(path.dirname(__file__)),"%s_instance"%mode)app=Flask("wtf",instance_path=instance_path,instance_relative_config=True)app.config.from_object('wtf.default_settings')app.config.from_pyfile('config.cfg')app.config['MEDIA_ROOT']=path.join(app.config.get('PROJECT_ROOT'),app.instance_path,app.config.get('MEDIA_FOLDER'))app.register_blueprint(noticias_blueprint)Bootstrap(app)db.init_app(app)Security(app=app,datastore=MongoEngineUserDatastore(db,User,Role))configure_admin(app)DebugToolbarExtension(app)cache.init_app(app)returnapp
Pronto, agora podemos começar a utilizar o cache nas views e templates.
NOTE: Escrevi um artigo explicando o Flask-Cache com mais detalhes que está disponível em meu blog
Vamos colocar cache nas views de acesso ao MongoDB importaremos o cache que criamos no arquivo cache.py e utilizaremos diretamente ou como forma de decorator.
blueprints/noticias.py
...from..cacheimportcache...
Agora vamos utilizar o cache como decorator para cachear a view index durante 5 minutos utilizando @cache.cached(timeout=300)
para decorar a view.
@noticias_blueprint.route("/")@cache.cached(timeout=300)defindex():todas_as_noticias=Noticia.objects.all()returnrender_template('index.html',noticias=todas_as_noticias,title=u"Todas as notícias")
Agora acesse localhost:5000 e repare que no primeiro acesso o MongoDB será acessado mas quando der refresh (f5) agora o acesso não será mais feito durante 5 minutos.
Uma outra maneira de cachear é chamando o cache diretamente, vamos fazer isso na view noticia usando cache.get e cache.set
@noticias_blueprint.route("/noticia/<noticia_id>")defnoticia(noticia_id):noticia_cacheada=cache.get(noticia_id)ifnoticia_cacheada:noticia=noticia_cacheadaelse:noticia=Noticia.objects.get(id=noticia_id)cache.set(noticia_id,noticia,timeout=300)returnrender_template('noticia.html',noticia=noticia)
Além das dessas duas maneiras também é possível cachear blocos de template e memoizar funções que recebem argumentos.
BEWARE: Utilizar cache e controle de acesso é algo que deve ser feito com cuidado em nosso exemplo se um usuário autenticado acessar uma notícia com acesso controlado provavelmente o cache irá armazenar esta versão e todos os outros usuários terão acesso. Portanto se este for o seu caso, utilize o nome do usuário ou grupo como chave do cache.
Se quiser saber mais detalhes sobre o Flask-Cache consulte a postagem que fiz em meu blog e a documentação oficial.
O diff com as alterações realizadas com o Flask-Cache encontra-se em 27bacd25a788ffc041de332403a2426cd199b828
Algumas outras extensões recomendadas que não foram abordadas neste artigo
- [Flask Email] Para avisar os autores que tem novo comentário
- [Flask Queue/Celery] Pare enviar o email assincronamente e não bloquear o request
- [Flask Classy] Um jeito fácil de criar API REST e Views
- [Flask Oauth e OauthLib] Login com o Feicibuque e tuinter
A versão final do app está no github
END: Sim chegamos ao fim desta terceira parte da série What The Flask. Eu espero que você tenha aproveitado as dicas aqui mencionadas. Nas próximas 3 partes iremos desenvolver nossas próprias extensões e blueprints e também questṍes relacionados a deploy de aplicativos Flask. Acompanhe o PythonClub, o meu site e meu twitter para ficar sabendo quando a próxima parte for publicada.
PUBLICIDADE: Estou iniciando um curso online de Python e Flask, para iniciantes abordando com muito mais detalhes e exemplos práticos os temas desta série de artigos e muitas outras coisas envolvendo Python e Flask, o curso será oferecido no CursoDePython.com.br, ainda não tenho detalhes especificos sobre o valor do curso, mas garanto que será um preço justo e acessível. Caso você tenha interesse por favor preencha este formulário pois dependendo da quantidade de pessoas interessadas o curso sairá mais rapidamente.
PUBLICIDADE 2: Também estou escrevendo um livro de receitas Flask CookBook através da plataforma LeanPub, caso tenha interesse por favor preenche o formulário na página do livro
Muito obrigado e aguardo seu feedback com dúvidas, sugestões, correções etc na caixa de comentários abaixo.
Abraço! "Python é vida!"