|
|
|
@ -275,6 +275,388 @@
|
|
|
|
|
</section> |
|
|
|
|
</section> |
|
|
|
|
|
|
|
|
|
<section> |
|
|
|
|
<section> |
|
|
|
|
<h2><code>requirements.txt</code></h2> |
|
|
|
|
|
|
|
|
|
<pre><code>flask~=0.12.2 |
|
|
|
|
Markdown~=2.6.9</code></pre> |
|
|
|
|
|
|
|
|
|
<aside class="notes"> |
|
|
|
|
Nossas duas dependências: Flask e |
|
|
|
|
Markdown. |
|
|
|
|
|
|
|
|
|
"~=" é um validador de versões com |
|
|
|
|
a definição de "compatível". Ou seja |
|
|
|
|
estamos buscando o Flask compatível com |
|
|
|
|
a versão 0.12.2 e o Markdown compatível |
|
|
|
|
com a versão 2.6.9. |
|
|
|
|
|
|
|
|
|
Como o PIP sabe disso? Por vesionamento |
|
|
|
|
semantico. |
|
|
|
|
</aside> |
|
|
|
|
</section> |
|
|
|
|
</section> |
|
|
|
|
|
|
|
|
|
<section> |
|
|
|
|
<section> |
|
|
|
|
<h2><code>main.py</code></h2> |
|
|
|
|
<h3>Imports</h3> |
|
|
|
|
|
|
|
|
|
<pre><code>import os |
|
|
|
|
import os.path |
|
|
|
|
|
|
|
|
|
import markdown |
|
|
|
|
|
|
|
|
|
from flask import Flask |
|
|
|
|
from flask import render_template</code></pre> |
|
|
|
|
|
|
|
|
|
<aside class="notes"> |
|
|
|
|
Dos imports, a única coisa "diferente" |
|
|
|
|
aqui é o import do markdown -- uma biblioteca |
|
|
|
|
externa que vai estar no nosso requirements.txt |
|
|
|
|
-- e os imports do Flask. Aqui só vamos usar dois |
|
|
|
|
elementos: o Flask, que é o objeto principal |
|
|
|
|
e o render_template, para renderizar templates |
|
|
|
|
Jinja. |
|
|
|
|
</aside> |
|
|
|
|
</section> |
|
|
|
|
|
|
|
|
|
<section> |
|
|
|
|
<h2><code>main.py</code></h2> |
|
|
|
|
<h3>A App</h3> |
|
|
|
|
|
|
|
|
|
<pre><code>app = Flask(__name__)</code></pre> |
|
|
|
|
|
|
|
|
|
<aside class="notes"> |
|
|
|
|
Flask é o objeto principal da aplicação. |
|
|
|
|
Ele precisa de um namespace e o nome |
|
|
|
|
da própria classe serve. |
|
|
|
|
</aside> |
|
|
|
|
</section> |
|
|
|
|
|
|
|
|
|
<section> |
|
|
|
|
<h2><code>main.py</code></h2> |
|
|
|
|
<h3>Configuração</h3> |
|
|
|
|
|
|
|
|
|
<pre><code>app.config.from_object('ata.defaults') |
|
|
|
|
app.config.from_envvar('ATA_SETTINGS', silent=True)</code></pre> |
|
|
|
|
|
|
|
|
|
<aside class="notes"> |
|
|
|
|
O objeto da aplicação (ou simplesmente app) |
|
|
|
|
tem um objeto "config" com as configurações. |
|
|
|
|
|
|
|
|
|
As configurações podem ser carregadas de vários |
|
|
|
|
lugares e aqui temos duas: |
|
|
|
|
|
|
|
|
|
from_object vai tentar importar o objeto e usar |
|
|
|
|
as variáveis definidas no mesmo. |
|
|
|
|
|
|
|
|
|
from_envvar vai procurar o arquivo apontando |
|
|
|
|
pela variável de ambiente indicada (por exemplo, |
|
|
|
|
se alguém tivesse feito `export ATA_SETTINGS=/etc/ata.cfg` |
|
|
|
|
esse arquivo seria carregado). Normalmente, quando |
|
|
|
|
uma das configs indicadas não for encontrada (por |
|
|
|
|
exemplo, o arquivo não existe ou a variável de |
|
|
|
|
ambiente não foi definida) o Flask vai abortar |
|
|
|
|
a execução. Para não abortar, precisamos do |
|
|
|
|
`silent=True` -- fail silently. |
|
|
|
|
</aside> |
|
|
|
|
</section> |
|
|
|
|
|
|
|
|
|
<section> |
|
|
|
|
<h2><code>main.py</code></h2> |
|
|
|
|
<h3>A primeira rota.</h3> |
|
|
|
|
|
|
|
|
|
<pre><code>@app.route('/') |
|
|
|
|
def index():</code></pre> |
|
|
|
|
|
|
|
|
|
<aside class="notes"> |
|
|
|
|
Nossa primeira iteração com o sistema de |
|
|
|
|
rotas do Flask. Vale notar que, diferente do |
|
|
|
|
Django, que tem um arquivo com todas as rotas, |
|
|
|
|
Flask utiliza decorators para indicar qual |
|
|
|
|
função será chamada. É possível também indicar |
|
|
|
|
quais métodos serão aceitos (e assim é possível |
|
|
|
|
ter uma função para GET e outra para POST, na |
|
|
|
|
mesma rota). |
|
|
|
|
|
|
|
|
|
Assim como o Django, Flask trabalha com endpoints. |
|
|
|
|
O nome do endpoint, nesse caso, é o nome da |
|
|
|
|
própria função (então, neste caso, o endpoint |
|
|
|
|
é "index"). Mais pra frente veremos porque isso |
|
|
|
|
é importante. |
|
|
|
|
</aside> |
|
|
|
|
</section> |
|
|
|
|
|
|
|
|
|
<section> |
|
|
|
|
<h2><code>main.py</code></h2> |
|
|
|
|
<h3>O índice</h3> |
|
|
|
|
|
|
|
|
|
<pre><code> content = os.listdir(app.config['STORAGE']) |
|
|
|
|
data = [] |
|
|
|
|
for entry in sorted(content, reverse=True): |
|
|
|
|
app.logger.debug('Found %s', entry) |
|
|
|
|
if not entry.endswith('.md'): |
|
|
|
|
continue |
|
|
|
|
|
|
|
|
|
with open(os.path.join(app.config['STORAGE'], entry)) as origin: |
|
|
|
|
content = origin.read() |
|
|
|
|
output = markdown.markdown(content) |
|
|
|
|
try: |
|
|
|
|
first_paragraph_end = output.index('</p>') |
|
|
|
|
paragraph = output[:first_paragraph_end] |
|
|
|
|
except ValuError: |
|
|
|
|
paragraph = output |
|
|
|
|
|
|
|
|
|
entry_name = entry[:entry.find('.md')] |
|
|
|
|
data.append((entry_name, output)) |
|
|
|
|
|
|
|
|
|
return render_template('index.html', data=data)</code></pre> |
|
|
|
|
|
|
|
|
|
<aside class="notes"> |
|
|
|
|
Infelizmente, não tem como diminuir a quantidade |
|
|
|
|
de código a ser apresentado aqui. Mas basicamente |
|
|
|
|
temos o seguinte: |
|
|
|
|
|
|
|
|
|
Estamos usando o app.config para buscar uma |
|
|
|
|
configuração, "STORAGE". Nesse exemplo, é o |
|
|
|
|
diretório com o conteúdo e ele é configurado |
|
|
|
|
em "defaults.py" (que vamos ver na sequencia). |
|
|
|
|
|
|
|
|
|
Segundo, estamos usando a biblioteca Markdown para |
|
|
|
|
converter o arquivo MarkDown para HTML. De forma |
|
|
|
|
"boba", estamos pegando o primeiro parágrafo. |
|
|
|
|
|
|
|
|
|
Terceiro, estamos usando a função "render_template" |
|
|
|
|
para renderizar o arquivo "index.html", com uma |
|
|
|
|
variável chamada "data". Essa variável vai aparecer |
|
|
|
|
nos templates -- e a única forma de passar variáveis |
|
|
|
|
é através da passagem pelos parametros da função |
|
|
|
|
render_template. |
|
|
|
|
</aside> |
|
|
|
|
</section> |
|
|
|
|
|
|
|
|
|
<section> |
|
|
|
|
<h2><code>main.py</code></h2> |
|
|
|
|
<h3>Entradas específicas</h3> |
|
|
|
|
|
|
|
|
|
<pre><code>@app.route('/<entry_name>') |
|
|
|
|
def show_entry(entry_name): |
|
|
|
|
filename = os.path.join(app.config['STORAGE'], entry_name + '.md') |
|
|
|
|
with open(filename) as origin: |
|
|
|
|
content = origin.read() |
|
|
|
|
output = markdown.markdown(content) |
|
|
|
|
|
|
|
|
|
return render_template('entry.html', output=output, |
|
|
|
|
entry=entry_name)</code></pre> |
|
|
|
|
|
|
|
|
|
<aside class="notes"> |
|
|
|
|
Mais uma rota, com uma diferença: ela aceita |
|
|
|
|
um parametro a mais, "entry_name". Esse mesmo |
|
|
|
|
nome deve aparecer na função. |
|
|
|
|
|
|
|
|
|
Assim como visto anteriormente, render_template |
|
|
|
|
recebe um arquivo template a ser carregado e |
|
|
|
|
duas variáveis: "output" com o conteúdo renderizado |
|
|
|
|
do Markdown e "entry" com o nome da entrada. |
|
|
|
|
|
|
|
|
|
Ponto para quem achar o erro aqui (não é erro: é |
|
|
|
|
pra mostrar umas das facilidades do Flask). |
|
|
|
|
</aside> |
|
|
|
|
</section> |
|
|
|
|
|
|
|
|
|
<section> |
|
|
|
|
<h2><code>main.py</code></h2> |
|
|
|
|
<h3>Tratamento de erros.</h3> |
|
|
|
|
|
|
|
|
|
<pre><code>@app.errorhandler(404) |
|
|
|
|
def page_not_found(): |
|
|
|
|
return render_template('page_not_found.html')</code></pre> |
|
|
|
|
|
|
|
|
|
<aside class="notes"> |
|
|
|
|
Com "errorhandler" é possível indicar o que |
|
|
|
|
deve ser apresentado quando ocorre um erro. |
|
|
|
|
|
|
|
|
|
No caso, estamos definindo que quando ocorrer |
|
|
|
|
um 404, deve ser feito o render_template do |
|
|
|
|
arquivo "page_not_found.html" |
|
|
|
|
</aside> |
|
|
|
|
</section> |
|
|
|
|
|
|
|
|
|
<section> |
|
|
|
|
<h2><code>main.py</code></h2> |
|
|
|
|
<h3>Tratamento de erros</h3> |
|
|
|
|
|
|
|
|
|
<pre><code>@app.errorhandler(FileNotFoundError) |
|
|
|
|
def entry_not_found(_): |
|
|
|
|
return render_template('entry_not_found.html')</code></pre> |
|
|
|
|
|
|
|
|
|
<aside class="notes"> |
|
|
|
|
Além de tratar com códigos de erro, |
|
|
|
|
errorhandler também sabe lidar com |
|
|
|
|
exceções. No caso, se em qualquer |
|
|
|
|
lugar da aplicação ocorrer um |
|
|
|
|
"FileNotFoundError", esse será tratado |
|
|
|
|
aqui. |
|
|
|
|
</aside> |
|
|
|
|
</section> |
|
|
|
|
</section> |
|
|
|
|
|
|
|
|
|
<section> |
|
|
|
|
<section> |
|
|
|
|
<h2><code>defaults.py</code></h2> |
|
|
|
|
|
|
|
|
|
<pre><code>DEBUG=True |
|
|
|
|
STORAGE='/home/jbiason/src/ata/contents'</code></pre> |
|
|
|
|
|
|
|
|
|
<aside class="notes"> |
|
|
|
|
Contém as configurações do sistema. |
|
|
|
|
DEBUG é uma variável controlada pelo |
|
|
|
|
próprio Flask (assim como o DEBUG do Django). |
|
|
|
|
Existem várias variáveis que podem ser |
|
|
|
|
configuradas e todas estão listadas |
|
|
|
|
na documentação do Flask. |
|
|
|
|
|
|
|
|
|
Entre essas variáveis está o DEBUG, o diretório |
|
|
|
|
onde se encontram os templates, o diretório |
|
|
|
|
dos arquivos estáticos e assim por diante. |
|
|
|
|
|
|
|
|
|
No nosso caso, como não estamos substiuindo |
|
|
|
|
essas variáveis, elas estão com o valor padrão |
|
|
|
|
de estarem junto com os fontes (assim como |
|
|
|
|
criamos nossa estrutura de diretórios). |
|
|
|
|
</aside> |
|
|
|
|
</section> |
|
|
|
|
</section> |
|
|
|
|
|
|
|
|
|
<section> |
|
|
|
|
<section> |
|
|
|
|
<h3><code>style.css</code></h3> |
|
|
|
|
|
|
|
|
|
<pre><code>h1#header { |
|
|
|
|
background-color: black; |
|
|
|
|
color: white; |
|
|
|
|
padding: 10px; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.entry { |
|
|
|
|
margin-left: 30px; |
|
|
|
|
margin-right: 30px; |
|
|
|
|
margin-bottom: 15px; |
|
|
|
|
}</code></pre> |
|
|
|
|
|
|
|
|
|
<aside class="notes"> |
|
|
|
|
O arquivo de estilos é bem bobo, mas |
|
|
|
|
serve para vermos onde ele deve ficar |
|
|
|
|
quando colocarmos o Flask em produção. |
|
|
|
|
</aside> |
|
|
|
|
</section> |
|
|
|
|
</section> |
|
|
|
|
|
|
|
|
|
<section> |
|
|
|
|
<section> |
|
|
|
|
<h2><code>templates</code></h2> |
|
|
|
|
<h3><code>layout.html</code></h3> |
|
|
|
|
|
|
|
|
|
<pre><code><!doctype html> |
|
|
|
|
<html> |
|
|
|
|
<head> |
|
|
|
|
<title>Atas de Reunião</title> |
|
|
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> |
|
|
|
|
</head> |
|
|
|
|
|
|
|
|
|
<body> |
|
|
|
|
<h1 id='header'> |
|
|
|
|
Atas de reuniao |
|
|
|
|
</h1> |
|
|
|
|
{% block maincontent %}{% endblock %} |
|
|
|
|
</body> |
|
|
|
|
</html></code></pre> |
|
|
|
|
|
|
|
|
|
<aside class="notes"> |
|
|
|
|
Para templates Jinja, assim como Django, é |
|
|
|
|
possível ter um arquivo base do qual todos |
|
|
|
|
os demais decendem. |
|
|
|
|
|
|
|
|
|
Importante notar aqui o url_for(), que faz |
|
|
|
|
a conversão do endpoint para uma URL (no nosso |
|
|
|
|
caso, do static) e quando endpoint requer um |
|
|
|
|
parametro (como o nosso "entry_name") ele *tem* |
|
|
|
|
que estar no comando. static requer filename |
|
|
|
|
para saber qual arquivo estático deve ser |
|
|
|
|
carregado. |
|
|
|
|
</aside> |
|
|
|
|
</section> |
|
|
|
|
|
|
|
|
|
<section> |
|
|
|
|
<h2><code>templates</code></h2> |
|
|
|
|
<h3><code>index.html</code></h3> |
|
|
|
|
|
|
|
|
|
<pre><code>{% extends "layout.html" %} |
|
|
|
|
{% block maincontent %} |
|
|
|
|
<div class='entry'> |
|
|
|
|
{% for entry, text in data %} |
|
|
|
|
<a href="{{ url_for('show_entry', entry_name=entry) }}">{{ entry }}</a> |
|
|
|
|
{{ text|safe }} |
|
|
|
|
{% endfor %} |
|
|
|
|
</div> |
|
|
|
|
{% endblock %}</code></pre> |
|
|
|
|
|
|
|
|
|
<aside class="notes"> |
|
|
|
|
O arquivo apresentado na raíz da aplicação. |
|
|
|
|
|
|
|
|
|
Variáveis com {{ }}, url_for() usando a função |
|
|
|
|
"show_entry" com um parametro |
|
|
|
|
</aside> |
|
|
|
|
</section> |
|
|
|
|
|
|
|
|
|
<section> |
|
|
|
|
<h2><code>templates</code></h2> |
|
|
|
|
<h3><code>entry.html</code></h3> |
|
|
|
|
|
|
|
|
|
<pre><code>{% extends "layout.html" %} |
|
|
|
|
{% block maincontent %} |
|
|
|
|
<div class="entry"> |
|
|
|
|
<div class="title">{{ entry }}</div> |
|
|
|
|
{{ output|safe }} |
|
|
|
|
</div> |
|
|
|
|
{% endblock %}</code></pre> |
|
|
|
|
|
|
|
|
|
<aside class="notes"> |
|
|
|
|
Como apresentar somente uma entrada. |
|
|
|
|
</aside> |
|
|
|
|
</section> |
|
|
|
|
|
|
|
|
|
<section> |
|
|
|
|
<h2><code>templates</code></h2> |
|
|
|
|
<h3><code>page_not_found.html</code></h3> |
|
|
|
|
|
|
|
|
|
<pre><code>{% extends "layout.html" %} |
|
|
|
|
{% block maincontent %} |
|
|
|
|
<h2>Page not found</h2> |
|
|
|
|
{% endblock %}</code></pre> |
|
|
|
|
|
|
|
|
|
<aside class="notes"> |
|
|
|
|
A página quando ocorrer um 404... |
|
|
|
|
</aside> |
|
|
|
|
</section> |
|
|
|
|
|
|
|
|
|
<section> |
|
|
|
|
<h2><code>templates</code></h2> |
|
|
|
|
<h3><code>entry_not_found.html</code></h3> |
|
|
|
|
|
|
|
|
|
<pre><code>{% extends "layout.html" %} |
|
|
|
|
{% block maincontent %} |
|
|
|
|
<h2>Entry not found</h2> |
|
|
|
|
{% endblock %}</code></pre> |
|
|
|
|
|
|
|
|
|
<aside class="notes"> |
|
|
|
|
... e quando a entrada não existir |
|
|
|
|
(que é a captura do nosso FileNotFoundError) |
|
|
|
|
</aside> |
|
|
|
|
</section> |
|
|
|
|
|
|
|
|
|
<section data-background='_images/thats-all-folks.jpg'> |
|
|
|
|
<section> |
|
|
|
|
<h1 class="fragment semi-opaque">Perguntas?</h1> |
|
|
|
|