Neste post vamos definir a estrutura de pastas e ficheiros para o projecto do nosso blog.
Vamos lá então por a mão na massa!

Criar os seguintes packages:
  • config: É onde vamos colocar os módulos de configuração.
  • controllers: Aqui vamos colocar os módulos com as regras de negócio.
  • libs: É onde ficam os módulos com as classes e funções genéricas.
  • models: Este é o package onde vamos ter os módulos relacionados com a persistência de dados.

Criar as seguintes pastas:
  • static/css: Pasta onde vão ficar os ficheiros .css.
  • static/scripts: Pasta onde vão ficar os ficheiros de script .js.
  • static/images: Pasta onde vão ficar os ficheiros das imagens usadas no site.
  • templates: Pasta onde vão ficar os ficheiros html que servirão de templates para as páginas do blog.

Nota: Um package é uma forma de agrupar módulos em Python e é formado por uma pasta com um ficheiro com o nome __init__.py, este ficheiro é necessário para que o interpretador Python trate a pasta como sendo um package, e assim podemos aceder ao módulo usando o ponto . da seguinte forma nome_do_package.mome_do_modulo. O __init__.py pode ser usado para colocar instruções de inicialização do package mas na maioria das vezes é apenas um ficheiro vazio.
Para saber mais sobre módulos e packages em Python consulta a documentação do Python em Modules -- Python 3.3.3 documentation.


Instalar novas bibliotecas / frameworks
Com o ambiente virtual do nosso projecto activo, importar o Jinja2.
pip install jinja2

O Jinja2 é uma framework Python para desenhar templates de páginas html. Com esta framework podemos escrever blocos com instruções de código em Python nos ficheiros html.



Criar os seguintes ficheiros:
  • controller/handler.py: Ficheiro principal no processamento dos requests e responses.
  • controller/specific.py: Ficheiro com "alias" para os principais métodos da framework Web tornado.
  • templates/base.html: Ficheiro base para os nossos templates html.
  • templates/error.html: Template de uma página de erro genérica.

No ficheiro handler.py do package controller será implementada a classe Handler que será a classe principal no processamento dos requests e responses do nosso blog.

Como já tinha dito nos posts anteriores, não quero ficar muito dependente da framerowk web (neste caso o tornado), por isso vou criar no ficheiro specific.py do package controller a classe Specific que conterá "alias" para os principais métodos da framework Web utilizada no desenvolvimento do projecto, neste caso a framework tornado. Se mais tarde quiser mudar para outra framework Web, a maior parte do trabalho consistirá em alterar esta classe em vez de ter que procurar e alterar os métodos da framework espalhados por todo o projecto. A classe Specific vai herdar da classe tornado.web.RequestHandler e a classe Handler, por sua vez, vai herdar da classe Specific.

Nota: A classe tornado.web.RequestHandler é a classe da framework tornado responsável pelo processamento dos requests.


Código fonte dos ficheiros

O código fonte será acompanhado por comentários que ajudam, espero eu, a explicar o código.

- main.py (ficheiro actualizado)
import tornado.ioloop
import tornado.web
import logging
import traceback

from controllers.handler import Handler 

#Pagina de entrada da aplicação.

class Home(tornado.web.RequestHandler):
    """
    Classe da pagina principal do blog.
    Apenas para os primeiros testes, no futuro será removida.
    """
    def get(self):
        self.write("Página inicial do blog!")


class PageNotFound(Handler):
    """
    Classe usada quando não é possível mostrar
    a página solicitada pelo cliente.
    """
    def get(self):
        """Mostra uma página de erro genérica"""
        try:
            #código de erro 404: not found
            self.http_error(404)
        except Exception:
            logging.error("PageNotFound.get: {}".format(traceback.format_exc()))
            
#Protocolo wsgi.
#Escolhe a instrução a ser executada em função
#do url submetido pelo cliente.
application = tornado.web.Application([
    (r"/", Home),
    (r'/.*', PageNotFound)
])


if __name__ == "__main__":    
    #Inicio da aplicação.
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

- controller/handler.py
import jinja2
import os
import logging
import traceback

from controllers import specific
 
class Handler(specific.Specific):
    """
    Classe principal no processamento dos requests e responses do blog.
    """
    
    #definição da variável da pasta onde estão os templates
    template_dir = os.path.join(os.path.dirname(__file__), "../templates")
    
    #inicialização do jinja2
    jinja_env = jinja2.Environment(extensions=['jinja2.ext.loopcontrols', 'jinja2.ext.i18n'], 
                                   loader = jinja2.FileSystemLoader(template_dir),
                                   autoescape = True)

           
    def write(self, value, *a, **kw):
        """Envia a string de resposta (página web) para o cliente"""
        if not value:
            value = ''
        super(Handler, self).write(value, *a, **kw)
        
        
    def render(self, template, result_str=False, **kw):
        """
        Constrói a string de resposta (página web) usando o template indicado
        e envia-a para o cliente.
        """
        result = ''
        try:
            result = self.render_str(template, **kw)                
            self.write(result)
                
        except Exception:
            logging.error("Handler.render: {}".format(traceback.format_exc()))            
        finally:
            return result
    
    
    def render_str(self, template, **kw):
        """
        Constrói a string de resposta (página web).
        
        Todos os objectos que estão no dicionário 'kw',
        estarão disponíveis  para uso no 'template'.
        """
        
        try:     
            #envia todos as 'query strings' para o template
            args = self.i_get_all_arguments()       
            for k in args:
                kw[k] = self.i_get_argument(k)
                
            t = self.jinja_env.get_template(template)
        except Exception:
            logging.error("Handler.render_str: {}".format(traceback.format_exc()))
        finally:            
            return t.render(kw)
    

    def http_error(self, code=404, **params):
        """
        Altera o código de estado da resposta http
        e mostra a página de erro!
        """
        try:
            self.i_set_status(code=code)            
            self.i_set_header('Content-Type', 'text/html')            
            self.render(u"error.html", title = "Error", code = code)
            message = "Oops! Handler.http_error: ({}) - {}".format(code, self.request.path)
            if int(code) >= 500:
                logging.error(message)
            else:
                logging.warning(message)            
            
        except Exception:
            logging.error("Handler.http_error: {}".format(traceback.format_exc()))        

- controller/specific.py
import tornado.web
import urllib
 
class Specific(tornado.web.RequestHandler):
    """
    Contem 'alias' para os principais métodos da framework.
    
    Para distinguir melhor os métodos desta classe
    o nome de todos os metodos começam por 'i_'.
    """
    
    def i_get_cookie(self, name, **kw):
        """Obtem o valor de um cookie"""
        return self.get_cookie(name, **kw)
    
    def i_add_cookie(self, cookie_string, **kw):
        """Cria um novo cookie"""
        self.i_add_header('Set-Cookie', cookie_string, **kw)
        
    def i_get_argument(self, arg, **kw):
        """Obtem o valor de uma 'query string' enviada no request"""        
        value = ''
        try:
            value = self.get_argument(arg, **kw)
        finally:
            return value
        
    def i_get_all_arguments(self):
        """Obtem o valor de todas as 'query strings' enviadas no request"""        
        return self.request.arguments
    
    def i_get_header(self, header):
        """Obtem o valor de um header"""
        return self.request.headers.get(header)
        
    def i_add_header(self, header, value, **kw):
        """Adiciona um header"""
        self.add_header(header, value, **kw)        
    
    def i_set_header(self, header, value, **kw):
        """Altera o valor de um header"""
        self.set_header(header, value, **kw)
    
    def i_set_status(self, code, **kw):
        """Atribui o status code do response"""
        self.set_status(status_code=code, **kw)
        
    def i_get_method(self, **kw):
        """Obtem o nome metodo enviado no request"""
        return self.request.method.lower()
    
    def i_redirect(self, uri, **kw):
        """Faz o redirect do url"""
        self.redirect(uri)
    
    def i_redirect_params(self, uri, dict_arguments, **kw):
        """
        Faz o redirect do url com 'query strings'.
        
        :param uri:
            O endereço url para onde vai ser redireccionado.
        :param dict_arguments:
            Dicionário com os as query strings e respectivos valores.
        """
        
        self.i_redirect(uri + '?' + urllib.parse.urlencode(dict_arguments))
    
    def i_get_path(self, **kw):
        """Obtem a path do request"""
        return self.request.path
    
    def i_get_referer(self, **kw):
        """Obtem a referer do request"""
        return self.i_get_header("Referer")
    
    def i_get_referer_path(self, **kw):
        """Obtem a path do referer sem 'query strings'"""
        referer = self.i_get_referer()
        start_query_string_pos = referer.find('?')
        if start_query_string_pos >= 0:
            referer = referer[:start_query_string_pos]            
        return referer

- templates/base.html
<!DOCTYPE html>
<html lang="pt-pt">
<head>
    <title>{{title}}</title>
    <meta name="description" content="{{description}}"/>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
</head>    

<body>
    <div>Titulo do Blogue</div>
    
    <div id="main">
        {# bloco que será usado pelos templates que herdarem base.html #}
        {% block container %}
        {% endblock %}
    </div>
    
</body>
</html>


- templates/error.html
{# aqui herdo o template base.html #}
{% extends "base.html" %}

{# aqui vai o conteúdo para o bloco container do template base.html #}
{% block container %}    

Erro: {{code}}!

{% endblock %}


Executem a aplicação (com o ambiente virtual do nosso projecto activo):
python main.py

Agora no browser escrevam o endereço http://localhost:8888/, para ver a página inicial do blog e depois tentem aceder a uma página do nosso blog que não exista, por ex.: http://localhost:8888/teste/, para ver a página de erro.


No próximo post falarei um pouco sobre templates e sobre a framework para templating Jinja2. Até lá.

Este post faz parte da série de posts "Tutorial, Blog em Python":
  1. Tutorial, Blog em Python - Parte 1: Introdução
  2. Tutorial, Blog em Python - Parte 2: Ambiente de Desenvolvimento
  3. Tutorial, Blog em Python - Parte 3: Estrutura do projecto (post actual)
  4. Tutorial, Blog em Python - Parte 4: Templates
  5. Tutorial, Blog em Python - Parte 5: Base de Dados
  6. Tutorial, Blog em Python - Parte 6: Source code