Django, incrementando o Projeto

Sofisticando o modelo com chaves externas e datas

Vamos aprimorar o aplicativo classificando nossas notas por categorias, permitindo diversos cadernos separados. Para efeito didático usaremos apenas um nível de categoria. Idealmente essas poderiam ser subdivididas em subcategorias de vários níveis.

Para isso criaremos modelos para as seguintes tabelas:

Caderno
id pk, automatico
caderno texto
Categoria
id pk, automatico
categoria texto
Notas
id pk, automatico
caderno fk –> caderno
categoria fk –> categoria
titulo texto
texto texto
slug texto

Vamos fazer alterações nos modelos, que serão aplicadas no banco de dados com migrations.
app_notas/notas/models.py

from django.db import models
from django.utils.text import slugify

class Caderno(models.Model):
    titulo = models.CharField(default='', max_length=150)
    def __str__(self):
        return self.titulo

class Categoria(models.Model):
    categoria = models.CharField(default='', max_length=150)
    def __str__(self):
        return self.categoria

class Nota(models.Model):
    titulo = models.CharField(default='', max_length=150, help_text="Título da Nota")
    texto = models.TextField(default='', blank=True)
    slug = models.SlugField(default='', blank=True, max_length=255)
    data_criada = models.DateField(auto_now_add=True, help_text="Data de Criação")
    data_editada = models.DateField(auto_now=True, help_text="Data de Edição")
    caderno = models.ForeignKey(Caderno, on_delete=models.CASCADE)
    categoria = models.ForeignKey(Categoria, on_delete=models.CASCADE)

    def __str__(self):
        return self.titulo

    def save(self,*args,**kwargs):
        self.slug = slugify(self.titulo)
        super().save(*args,**kwargs)

As partes alteradas estão em negrito. Inserimos duas novas tabelas: Caderno e Categoria. Cada uma delas tem seu id criado automaticamente pelo django. Na tabela Nota acrescentamos caderno e categoria. Definidos como ForeignKey eles ficam associados às novas tabelas da seguinte forma:

nota.caderno (fk) ⟼ caderno.id
nota.categoria (fk) ⟼ categoria.id

O argumento help_text permite a especificação de texto descritivo que será exibido juntos com forms, como veremos.

Os campos do tipo DateField armazenam datas. (DateTimeField armazenam datas e horas). O argumento auto_now_add=True ajusta a campo para a hora do momento de criação do valor do campo enquanto
auto_now=True ajusta a campo na hora da última edição, quando o objeto é gravado.

A chave externa (fk) define uma relação de muitos-para-um. Por ex., cada nota tem apenas uma categoria mas podem haver diversas outras notas na mesma categoria.

O argumento on_delete informa como registros ligados por chaves externas devem ser tratados em caso de apagamento do registro principal. Ele pode receber diversos valores:

CASCADE força o apagamento de dados conectados pela chave externa.
PROTECT impede o apagamento de dados conectados pela chave externa. Levanta exceção ProtectedError.
RESTRICT similar à PROTECT, mas levanta erro RestrictedError.
SET_NULL mantém dados conectados pela chave externa, substituindo seu valor por NULL. O parâmetro deve ser ajustado na definição do modelo.
SET_DEFAULT similar à SET_NULL mas substitui o valor da chave por seu valor default. O parâmetro deve ser ajustado na definição do modelo.
SET( ) permite a construção de funções customizadas para atribuir valores ao campo fk apagado.
DO_NOTHING nenhuma atitude é tomada e os dados ligados por fk permanecem inalterados.
Uma descrição mais detalhadas por ser vista em Zerotobyte: On delete explained.

Além disso inserimos dois campos de data, data_criada e data_editada, que são preenchidas por default com a data do momento da operação.

Com essas alterações o banco de dados atual fica inconsistente, pois existem campos que demandariam fk e que estão vazios. Como estamos em desenvolvimento e os dados que temos são apenas experimentais, uma boa tática é a de resetar o banco de dados. Essa é também uma boa oportunidade para mencionar como isso é feito.

Resetando o banco de dados

Podemos ver todas as migrations feitas até agora com o comando:

$ python manage.py showmigrations

Para remover as alterações no BD e as migrations do django, removemos o conteudo do BD. No nosso caso, como estamos usando o SQLite, basta apagar o arquivo db.sqlite3 que fica na pasta do projeto app_notas/app_notas. Em seguido removemos todo o conteúdo da pasta migrations, na pasta de cada aplicativo (app_notas/notas no caso), exceto o arquivo __init__.py.

Podemos agora reconstruir o BD e refazer a conta de superuser:

$ python manage.py makemigrations
$ python manage.py migrate
$ python manage.py createsuperuser


Observações: Se você está usando outro BD, como PostGRE ou MySQL você deve apagar os bancos usando a sintaxe própria de seu gerenciador. Outras informações podem ser encontradas em Techiediaries: Resetting Django Migrations, inclusive sobre como zerar as migrations em produção, manualmente ou usando um app para isso.

Usando o app ampliado

Depois de termos ampliado as definições de nossos modelos, devemos incluir nos novos modelos em admin.py para que eles possam ser gerenciados pelo admin.
app-notas/notas/admin.py

from django.contrib import admin
from notas.models import Caderno, Categoria, Nota

admin.site.register(Caderno)
admin.site.register(Categoria)
admin.site.register(Nota)

Agora é possível entrar dados visitando o site de administração do django. Notamos que, se definirmos antes categorias e cadernos o admin apresenta uma caixa dropdown contendo as escolhas possíveis desses dados sempre que inserimos ou editamos notas.

Para finalizar essa etapa vamos exibir os novos campos na página inicial e nos detalhes exibidos. Ajustamos o arquivo inicial de templates para receber os dados adicionais.
app_notas/templates/home.html

{% extends "base.html" %}
{% block title %}
  <h1>Aplicativo de Notas</h1>
{% endblock %}
{% block content %}
  <h1>Página inicial</h1>
  <p>Essas são as minhas notas (clique para visualizar).</p>
  <table class="dados_notas">
    <tr><th>Caderno</th><th>Categoria</th>
    <th>Nota</th><th>Criada em</th><th>Editada em</th>
    <th>clique para editar</th></tr>
    {% for nota in notas %}
      <tr>
        <td>{{ nota.caderno }}</td><td>{{ nota.categoria }}</td>
        <td>{{ nota }}</td>
        <td>{{ nota.data_criada|date:"d/m/Y" }}</td><td>
        {{ nota.data_editada|date:"d/m/Y" }}</td>
        <td><a href="/nota/{{ nota.pk }}/">{{ nota.pk }}</a></td>
      </tr>
    {%endfor%}
  </table>
{% endblock %}

E acrescentamos formatação css para essa tabela, definida sob a classe dados_notas (mostrando só os acréscimos):
app_notas/static/css/estilo.css

.dados_notas {text-align:left; border-collapse:collapse;}
.dados_notas tr, th, td {border:1px solid #aaa; padding:10px 30px;}    

Visitando a página incial veremos, no navegador:

Para visualizar detalhes da nota alteramos:
app_notas/templates/detalhe.html

{% extends "base.html" %}
{% block title %}
  <h1>Aplicativo de Notas: Detalhes</h1>
{% endblock %}

{% block content %}
  {% if secao == "erro" %}
    <h1>Nenhuma nota foi encontrada!</h1>
    <h2>Nota: {{ nota }}</h2>
  {% else %}
    <h1>Detalhe de uma nota</h1>
    <p><b>Caderno:</b> {{ nota.caderno }}</p>
    <p><b>Categoria:</b> {{ nota.categoria }}</p>
    <h2>Nota: {{ nota.titulo }}</h2>
    <p>{{ nota.texto|safe }}</p>
    <p>Postada em: {{nota.data_criada|date:"d/m/Y"}}</p>
    <p>Editada em: {{nota.data_editada|date:"d/m/Y"}}</p>
    <p><small>Id: {{ nota.pk }} Slug: {{ nota.slug }}</small></p>
  {% endif %}
{% endblock %}

Visitando uma página de detalhes (por exemplo clicando no link da página home) veremos:

Django Admin

A página de administração do django funciona como um app pre-desenvolvido. Ela é voltada para administradores e não deve ser usada para a interação de usuários com o site. Mesmo assim vale considerar algumas formas possíveis de aprimorar esse uso.

A referência para todo o controle do admin está em settings.py do aplicativo notas, enquanto a url que direciona para esse aplicativo está em urls.py do projeto:

# Em notas/settings.py:
INSTALLED_APPS = ['django.contrib.admin',  ... ]

# Em app_notas/urls.py
urlpatterns = [path('admin/', admin.site.urls), ...]

Para acessar o admin precisamos criar um usuário com poderes de administrador, (superuser)

$ python manage.py createsuperuser

Já vimos que, ao listar objetos de qualquer tabela, o admin usa a representação de string, definida com o modelo. Foi o que fizemos em, por ex. em models.py:
app_notas/notas/models.py

class Nota(models.Model):
    titulo = models.CharField(default='', max_length=150)
    ...
    def __str__(self):
        return self.titulo

Para customizar o texto exibido na página do admin acrescentamos as linhas à urls.py:
apps_notas/urls.py

urlpatterns = [ ... ]
admin.site.site_header  =  "Admin: Anotações"
admin.site.index_title  =  "Controle de Tabelas:  Anotações"

Vimos também quem nossos modelos são registrados em notas/admin.py. Podemos também ajustar a forma de exibição dos campos, acrescentar um campo de pesquisa sobre campos escolhidos da tabela e inserir filtros que limitam a exibição dos dados. Para isso criamos classes que herdam de admin.ModelAdmin e as registramos junto com os modelos.
apps_notas/notas/admin.py

from django.contrib import admin
from notas.models import Caderno, Categoria, Nota
 
class CadernoAdmin(admin.ModelAdmin):
    search_fields = ('titulo',)

class CategoriaAdmin(admin.ModelAdmin):
    list_filter = ('categoria',)
    search_fields = ('categoria',)

class NotaAdmin(admin.ModelAdmin):
    list_display = ('titulo', 'caderno', 'categoria', 'data_criada', 'data_editada',)
    list_filter = ('categoria', 'data_criada',)
    search_fields = ('titulo',)

admin.site.register(Caderno, CadernoAdmin)
admin.site.register(Categoria, CategoriaAdmin)
admin.site.register(Nota, NotaAdmin)

Feito isso visitamos a página ao admin e clicamos em Notas. A seguinte página é exibida:

O registro em admin.site disponibiliza o modelo para o app admin. Caso não se queira exibir um dos modelos no admin basta omitir esse registro.

Duas outras possibilidades são as de excluir um campo na página de edição das tabelas do admin e agrupar a exibição de dados sob um campo tal como datas.
apps_notas/notas/admin.py

class NotaAdmin(admin.ModelAdmin):
    ....
    date_hierarchy = 'data_criada'
    exclude = ( 'data_criada', 'data_editada',)

O primeiro filtro cria uma seleção acima da lista, por data de edição. O segundo exclue, no formulário do admin para edição de notas, os campos data_criada e data_editada. Isso é possível uma vez que ambas as datas são inseridas automaticamente com o parâmetro auto_now=True.

Formulários html no Django

Um formulário HTML é uma página construída com marcação html usada para o preenchimento de dados no navegador e habilitada para enviar esses dados para o servidor. Ela é formada por widgets que são campos de textos, caixas de seleção, botões, botões de rádio e checkboxes.


Formulários são uma das formas mais comuns de interação entre usuários e um aplicativo Web. O django traz mecanismos para facilitar a criação de formulários e integrá-los com o aplicativo. Para experimentar com os formulários vamos criar um formulário de inserção e outro de edição de notas existente.

Inserindo uma nota: Primeiro inserir uma url para requisitar uma páginas de inserção de dados. Podemos fazer isso como (mostrando só alterações):
notas/urls.py

urlpatterns = [
    ...
    path('inserir/', views.inserir, name='inserir'),
]

Claro que temos que criar uma view para receber essa solicitação. Mas antes criaremos uma classe derivada de django.forms.ModelForm, responsável pela geração de um formulário. Gravamos o seguinte arquivo em
app_notas/notas/forms.py

from django.forms import ModelForm
from .models import Nota

class NotaForm(ModelForm):
    class Meta:
        model=Nota
        fields=['titulo', 'texto', 'caderno', 'categoria']    

A classe ModelForm descreve um formulário. Ela pode acessar as informações sobre os campos do modelo e os representar em um formulário. A class Meta é usada para modificar as propriedades da classe externa onde foi definida. Dentro dela fazemos a associação com o modelo sendo exibido e os campos que se pretende editar.

Os campos data_criada, data_editada e slug são criados automaticamente e não precisam estar no formulário de inserção de notas. Para inserir todos os campos, caso isso seja o desejado, podemos usar fields = ‘__all__’. A ordem dos campos declarados será respeitada na exibição do formulário.

Inserimos uma view para processar a requisição:
app_notas/notas/views.py

from django.shortcuts import render, get_object_or_404, redirect
from .models import Nota
from .forms import NotaForm
...

def inserir(request):
    form=NotaForm()
    if request.method=='POST':
        form=NotaForm(request.POST)
        if form.is_valid():
            nota=form.save(commit=False)
            nota.save()
            return detalhePK(request, nota.pk)

    contexto = {'section':'inserir','form': form,}    
    return render(request, 'inserir.html', contexto)    

O parâmetro request recebe os dados inseridos na requisição. Se um formulário foi preenchido e enviado o método de requisição foi POST. Um objeto form é carregado com esses dados. Se os dados estão inseridos corretamente eles são gravados. Se não for válida (se algum campo necessário não foi preenchido, ou um email está em formato incorreto, por ex.) um formulário vazio é reexibido.

O uso de nota=form.save(commit=False) armazena na variável um objeto ainda não gravado. Neste intervalo uma validação ou manipulação manual de dados pode ser feita. Se considerado correto o objeto é gravado com nota.save(). Se a nota for gravada corretamente a função view detalhePK() é chamada para exibir a nota recém gravada. O template inserir.html deve conter os comandos para a exibição do formulário.
app_notas/templates/inserir.html

{% extends "base.html" %}
{% block title %}<h1>Inserindo uma nota</h1>{% endblock %}
{% block content %}
<form action="{% url 'inserir' %}" method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button class="button" type="submit">Inserir</button>
</form>
{% endblock %}

A ação disparada pelo clique no botão de inserir está descrita na tag action = {% url 'inserir' %}, um acesso à view descrita acima, com método POST. O token {% csrf_token %} acrescenta uma proteção contra ataques do tipo CSRF (Cross Site Request Forgeries), e {{ form.as_p }} faz a listagem HTML dos campos definidos em notas/forms.py, dispostos em parágrafos sucessivos. Também podemos dispor as campos do formulário como {{ form.as_ul }}, dispostos em uma lista não ordenada, ou {{ form.as_table }}, com os campos dentro de uma tabela.

Para acessar a página de inserção sem necessidade de escrever a URL completa, vamos colocar no cabeçalho de base.html um link com esse endereço. Alterando o código logo abaixo de <body> em (exibindo só trecho modificado):
app_notas/templates/base.htlm

<ul class="menu">
    <li><a class="{% if secao == 'home' %}
    active {% endif %}" href="{% url 'index' %}">Início</a></li>
    <li><a href="{% url 'inserir' %}">Inserir Nova Nota</a></li>
    <li><a href="/admin/">Admin</a></li>
  </ul>

Ao clicar em Inserir Nova Nota o seguinte formulário é exibido. Quando preenchido, se clicamos no botão Inserir a nota é gravada e exibida. Resta-nos agora escrever o código para editar ou apagar uma nota já inserida.

Editando uma nota: de forma similar, vamos alterar o projeto para que possamos editar notas existente e apagá-las. A url será recebida por:
app_notas/notas/urls.py

urlpatterns = [
    ...
    path('inserir/', views.inserir, name='inserir'),
    ...
    path('editar/<int:pk>/', views.editar, name='editar'),
]

Podemos usar o mesmo objeto NotaForm já criado, desde que aceitemos que os campos data_criada, data_editada e slug continuem sendo geridos automaticamente.

Uma nova view será incluída para a edição:
notas/views.py

...

def editar(request, pk):
    nota = get_object_or_404(Nota, pk=pk)
    form=NotaForm()
    if request.method=='POST':
        form=NotaForm(request.POST, instance=nota)
        if form.is_valid():
            nota=form.save(commit=False)
            nota.save()
            return detalhePK(request, nota.pk)

    contexto = {'section':'editar','form': form,}
    return render(request, 'editar.html', contexto)    

Como antes, um formulário preenchido corretamente é gravado e a nota editada é exibida. O template editar.html deve conter os comandos para a exibição do formulário.

app_notas/templates/editar.html

{% extends "base.html" %}
{% block title %}<h1>Editando uma nota</h1>{% endblock %}
{% block content %}
<form action="{% url 'editar' %}" method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button class="button" type="submit">Gravar</button>
</form>
{% endblock %}

Para dar acesso à página de edição, tendo escolhido uma nota, inserimos uma coluna na página que lista todas as notas em home.html (exibindo só a tabela que contém a lista de notas):
app_notas/templates/home.html

<table class="dados_notas">
    <tr><th>Caderno</th><th>Categoria</th><th>Nota</th>
    <th>Criada em</th><th>Editada em</th><th>visualizar</th>
    <th>editar</th></tr>
    {% for nota in notas %}
      <tr>
        <td>{{ nota.caderno }}</td>
        <td>{{ nota.categoria }}</td>
        <td>{{ nota }}</td>
        <td>{{ nota.data_criada|date:"d/m/Y" }}</td>
        <td>{{ nota.data_editada|date:"d/m/Y" }}</td>
        <td><a href="/nota/{{ nota.pk }}/">Índice = <b>{{ nota.pk }}</b></a></td>
        <td><a href="/editar/{{ nota.pk }}/">Editar: <b>{{ nota.pk }}</b></a></td>
      </tr>
    {%endfor%}
  </table>

Ao clicar em Editar um formulário idêntico ao formulário de edição aparece com os campos já preenchidos, permitindo sua edição. Novamente, ao serem gravados a tela de exibição de notas é mostrada.

Em app_notas/notas/views.py, na função editar(), uma nota específica é carregada e usada para instanciar um objeto NotaForm. Uma NotaForm(instance=nota) é exibida para edição no primeiro carregamento do formulário. Se o botão de Gravar foi clicado a view é acionada com request.method=='POST'. Isso faz com que os dados do formulário sejam gravados, se o formulário é válido.

Apagar uma nota já inserida: Para apagar uma nota já inserida, em urls.py inserimos
app_notas/notas/urls.py

urlpatterns = [
    ...
    path('apagar//', views.apagar, name='apagar'),
]

Em forms.py inserimos a definição de um formulário sem a exibição de campos:
notas/forms.py

class ApagarNotaForm(ModelForm):
    class Meta:
        model=Nota
        fields=[]

O próximo passo é acrescentar uma view para o apagamento em views.py
notas/views.py

...
from .forms import NotaForm, ApagarNotaForm
...
def apagar(request, pk=None):
    nota=get_object_or_404(Nota, pk=pk)
    if request.method=="POST":
        form=ApagarNotaForm(request.POST, instance=nota)
        if form.is_valid():
            nota.delete()
            return redirect('home')
    else:
        form=ApagarNotaForm(instance=nota)
        contexto={'section':'apagar', 'form': form, 'nota': nota,}
        return render(request,'blog/delete.html', contexto)

Um template para confirmar a escolha do usuário em apagar uma nota pode ter o seguinte conteúdo:
app_notas/templates/apagar.html

{% extends "base.html" %}
{% block title %}<h1>Apagando uma Nota</h1>{% endblock %}
{% block content %}
<form action="{% url 'apagar' nota.pk%}" method="post">
    {% csrf_token %}
    {{ form }}
    <h2>Título da Nota: {{ nota.titulo }}</h2>
    <p style="margin-left:10em;">{{ nota.texto | safe }}</p>
    <p>Tem certeza de que deseja apagar essa nota:? <button class="button" type="submit">Apagar</button>
    Cancelar</p>
</form>
{%endblock%}

Agora, na lista geral de notas, ao clicar Apagar somos levados à página com a opção de apagamento, que lista título e conteúdo da nota. Confirmando o apagamento a view.apagar é acionada com method=="POST". Clicando Cancelar a página inicial, com a lista de notas é exibida.

É claro que muitas otimizações podem ser feitas nesse código. Por exemplo, poderíamos ter apenas uma view para inserir | editar | apagar, passando parâmetros que executem cada uma das opções. Também podemos fazer muitos aperfeiçoamentos na interface visível para o usuário, melhorando os templates e usando CSS. Para ilustrar alguns possíveis aprimoramentos vamos melhorar a aparência da página inicial, que contém uma lista de notas, com as várias possibilidades de edição.

Primeiro alteramos views.py (mostrando só alterações):
app_notas/notas/views.py

from . import funcoes

def index(request):
    #notas = Nota.objects.all() (removemos essa linha)
    notas = Nota.objects.order_by('caderno','categoria', 'data_editada')
    notas = funcoes.transforma_notas(notas)
    contexto = {'secao':'home', 'mensagem':'Item de Menu', 'notas': notas,}
    return render(request, 'home.html', contexto )

Fazendo notas = Nota.objects.order_by('caderno','categoria', 'data_editada') o objeto notas será uma coleção de notas, agora ordenadas por ‘caderno’,’categoria’, ‘data_editada’, nessa ordem de prioridade.

Suponha agora que queremos montar uma lista onde nomes de cadernos e categorias repetidas não apareçam, na lista. Isto é: em cada linha cadernos e categorias, agora ordenados nessa ordem, não são exibidos repetidamente. Para isso acrescentamos um arquivo funcoes.py:
app_notas/notas/funcoes.py

def transforma_notas(notas):
    cat = ""
    cad = ""
    for nota in notas:
        if cat == nota.categoria.categoria:
            nota.categoria.categoria = ""
        else:
            cat = nota.categoria.categoria

        if cad == nota.caderno.titulo:
            nota.caderno.titulo = ""
        else:
            cad = nota.caderno.titulo
    return notas    

Essa função transforma em strings vazias os cadernos e categorias repetidos, de forma a que a lista só contenha essa informação quando ela é alterada. Por fim alteramos home.html:
templates/home.html

{% extends "base.html" %}
{% block title %}<h1 style="margin-top:80px;" >Aplicativo de Notas</h1>{% endblock %}

{% block content %}
<table class="dados_notas">
  <tr><th>Caderno</th><th>Categoria</th><th>Nota</th><th>Criada</th><th>Editada</th><th>Ver</th><th>Editar</th><th>Apagar</th></tr>
  <tr><td colspan="8" style="border-bottom: 1px solid #999;"></td></tr>
  <tr><td colspan="8"></td></tr>
  {% for nota in notas %}
  <tr>
    <td style="font-size:2em; font-weight: bold;">{{ nota.caderno }}</td>
    <td style="font-size:1.5em; font-weight: bold;">{{ nota.categoria }}</td>
    <td>{{ nota.titulo }}</td>
    <td>{{ nota.data_criada|date:"d/m/Y" }}</td>
    <td>{{ nota.data_editada|date:"d/m/Y" }}</td>
    <td><a href="/nota/{{ nota.pk }}/">📖</a></td>
    <td><a href="/editar/{{ nota.pk }}/">📝</a></td>
    <td><a href="/apagar/{{ nota.pk }}/">🚮</a></td>
  </tr>
  {%endfor%}
{% endblock %}

Os caracteres 📖, 📝 e 🚮 podem ser copiados e colados diretamente nos templates (ou qualquer outra página html). Alteramos ainda o arquivo estilo.css (mostrando só as alterações):
app_notas/static/estilo.css

.menu > li > a {
    background-color: rgb(21, 59, 141);
    border: 1px solid rgb(6, 6, 39);
    color: #fff;
    padding: 10px 20px;
    text-align: center;
    font-size: 16px;
    margin: 4px 2px;
    cursor: pointer;
}
.menu > li > a.ativa { font-weight:bold; }

Acessando nossa homepage temos agora:

Na imagem acima supomos que temos notas inseridas com esses cadernos, categorias, e títulos.

Muitos outras aperfeiçoamentos podem ser pensados para esse aplicativo de notas. Podemos, por exemplo, fazer as categorias serem dependentes dos cadernos, abrir um conjunto novo de notas para cada usuário, permitir a entrada de sub-categorias em vários níveis, criar um conjunto de tags para aplicar a cada nota, criando uma estrutura de relacionamentos entre tags de modo que o usuário possa navegar entre notas associadas a uma mesma ideia ou conceito.

Importante lembrar que, como estamos dentro de uma estrutura de programação do python, qualquer módulo importado para finalidades diversas pode ser facilmente incorporados no site ou aplicativo. Isso inclue pacotes de análise de texto, tratamento e exibição de dados, recursos de geo-referência, etc.

Artigos Django

1- Django, Websites com Python
2- Django, um Projeto
3- Django, incrementando o Projeto (esse artigo)

Bibliografia

Consulte a bibliografia no artigo inicial.

Leave a Reply

Your email address will not be published. Required fields are marked *