Aula 3: SQL e Django Models

Conteúdos

Introdução

SQL

SQL, ou Structured Query Language, é uma linguagem de programação que nos permite atualizar e consultar bancos de dados.

Logo SQL

Bancos de Dados

Antes de entrarmos em como usar a linguagem SQL, devemos discutir como nossos dados são armazenados. Ao usar SQL, trabalharemos com um banco de dados relacional onde todos os nossos dados são armazenados em várias tabelas. Cada uma dessas tabelas é composta por um número fixo de colunas e um número variável de linhas.

Para ilustrar como trabalhar com SQL, usaremos o exemplo de um site de cursos, que armazena dados do curso, usuários, entre outras coisas. Um exemplo de tabela que iremos ver está a seguir:

usuários

Existem vários sistemas de gerenciamento de banco de dados relacionais diferentes que são comumente usados para armazenar informações e que podem interagir facilmente com comandos SQL:

Os dois primeiros, MySQL e PostgreSQL, são sistemas de gerenciamento de banco de dados mais robustos que normalmente são executados em servidores separados daqueles que executam um site. SQLite, por outro lado, é um sistema mais leve que pode armazenar todos os seus dados em um único arquivo. Usaremos SQLite ao longo deste curso, pois é o sistema padrão usado pelo Django.

Tipos de Coluna

Assim como trabalhamos com vários tipos de variáveis em Python, SQLite tem tipos que representam diferentes formas de informação. Outros sistemas de gerenciamento podem ter tipos de dados diferentes, mas todos são bastante semelhantes aos do SQLite:

Tabelas

Agora, para realmente começar a usar SQL para interagir com um banco de dados, vamos começar criando uma nova tabela. O comando para criar uma nova tabela é mais ou menos assim:

CREATE TABLE users(
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    username TEXT UNIQUE NOT NULL,
    email TEXT UNIQUE NOT NULL,
    pass_hash TEXT NOT NULL
);

No comando acima, estamos criando uma nova tabela que decidimos chamar de users, e adicionamos quatro colunas a esta tabela:

  1. id: Muitas vezes é útil ter um número que nos permita identificar exclusivamente cada linha em uma tabela. Aqui especificamos que id é um inteiro e também que é nossa chave primária, significando que é nosso identificador único. Além disso, especificamos que ele AUTOINCREMENT, o que significa que não precisaremos fornecer um id toda vez que adicionarmos à tabela, pois isso será feito automaticamente.
  2. username: Aqui especificamos que este será um campo de texto, e ao escrever NOT NULL exigimos que ele tenha um valor. Ademais, ao escrever UNIQUE, exigimos que este valor seja único naquela coluna.
  3. email: Novamente especificamos que este será um campo de texto e impedimos que seja nulo.
  4. password_hash: Por último, especificamos que temos uma senha armazenada como texto, e que não seja nula. Em databases, não devemos guardar uma senha em texto puro! Como exemplo aqui, usamos uma senha com hash. Mas há diferentes maneiras de tratar senhas.

Acabamos de ver as restrições NOT NULL, PRIMARY KEY e UNIQUE ao criar uma coluna, mas existem várias outras restrições disponíveis para nós:

Agora que vimos como criar uma tabela, vamos ver como podemos adicionar linhas a ela. Em SQL, fazemos isso usando o comando INSERT:

INSERT INTO users
(username, email, password_hash)
VALUES ("fulano_dev", "fulano@codelab.br", "2b$12");

No comando acima, especificamos o nome da tabela em que desejamos inserir, depois fornecemos uma lista dos nomes das colunas sobre as quais forneceremos informações e, em seguida, especificamos os VALUES que gostaríamos de preencher nessa linha da tabela, garantindo que os VALUES venham na mesma ordem que nossa lista correspondente de colunas. Observe que não precisamos fornecer um valor para id porque ele é incrementado automaticamente.

SELECT

Depois que uma tabela é preenchida com algumas linhas, provavelmente queremos uma maneira de acessar os dados dentro dessa tabela. Fazemos isso usando a consulta SELECT do SQL. A consulta SELECT mais simples em nossa tabela de usuários pode ser mais ou menos assim:

SELECT * FROM users;

O comando acima (*) recupera todos os dados da nossa tabela de usuários

todos

Pode ser o caso, porém, que não precisamos realmente de todas as colunas do banco de dados, apenas username e email. Para acessar apenas essas colunas, podemos substituir o * pelos nomes das colunas que gostaríamos de acessar. A consulta a seguir retorna todos os usernames e emails.

SELECT username, email FROM users;

Apenas duas colunas

À medida que nossas tabelas ficam cada vez maiores, também queremos reduzir quais linhas nossa consulta retorna. Fazemos isso adicionando um WHERE seguido por alguma condição. Por exemplo, o comando a seguir seleciona apenas a linha com um id de 3:

SELECT * FROM users WHERE id = 3;

apenas uma linha

Trabalhando com SQL no Terminal

Agora que conhecemos alguns comandos básicos de SQL, vamos testá-los no terminal! Para trabalhar com SQLite em seu computador, você deve primeiro baixar o SQLite. (Não usaremos em aula, mas você também pode baixar o DB Browser para uma maneira mais amigável de executar consultas SQL.)

Podemos começar criando um arquivo para nosso banco de dados criando manualmente um novo arquivo ou executando touch users.sql no terminal. Agora, se executarmos sqlite3 users.sql no terminal, seremos levados a um prompt SQLite onde podemos executar comandos SQL:

# Entrando no Prompt SQLite
(base) % sqlite3 users.sql
SQLite version 3.49.1 2025-02-18 13:38:58
Enter ".help" for usage hints.

# Criando uma nova Tabela
sqlite> CREATE TABLE users(
...>     id INTEGER PRIMARY KEY AUTOINCREMENT,
...>     username TEXT UNIQUE NOT NULL,
...>     email TEXT NOT NULL,
...>     password_hash TEXT NOT NULL
...> );

# Listando todas as tabelas atuais (Apenas users por enquanto)
sqlite> .tables
users

# Consultando tudo dentro de users (Que está vazio agora)
sqlite> SELECT * FROM users;

# Adicionando um usuário
sqlite> INSERT INTO users
...>     (username, email, password_hash)
...>     VALUES ("fulano_dev", "fulano@codelab.br", "2b$12");

# Verificando por novas informações, que agora podemos ver
sqlite> SELECT * FROM users;
1|fulano_dev|fulano@codelab.br|2b$12

# Adicionando mais alguns voos
sqlite> INSERT INTO users (username, email, password_hash) VALUES ("ciclano_dev", "ciclano@codelab.br", "a31lW");
sqlite> INSERT INTO users (username, email, password_hash) VALUES ("beltrano_dev", "beltrano@codelab.br", "Wxn96p3");
sqlite> INSERT INTO users (username, email, password_hash) VALUES ("alano_dev", "alano@codelab.br", "3OXePa");

# Consultando essas novas informações
sqlite> SELECT * FROM users;
1|fulano_dev|fulano@codelab.br|2b$12
2|beltrano_dev|beltrano@codelab.br|a31lW
3|ciclano_dev|ciclano@codelab.br|Wxn96p3
4|fulano_dev|fulano@codelab.br|3OXePa

# Alterando as configurações para tornar a saída mais legível
sqlite> .mode columns
sqlite> .headers yes

# Consultando todas as informações novamente
sqlite> SELECT * FROM users;
id  username      email                password_hash
--  ------------  -------------------  -------------
1   fulano_dev    fulano@codelab.br    2b$12        
2   ciclano_dev   ciclano@codelab.br   a31lW        
3   beltrano_dev  beltrano@codelab.br  Wxn96p3      
4   alano_dev     alano@codelab.br     3OXePa       

# Procurando apenas por um usuário com email ciclano@codelab.br
sqlite> SELECT * FROM users WHERE email = "ciclano@codelab.br";
id  username     email               password_hash
--  -----------  ------------------  -------------
2   ciclano_dev  ciclano@codelab.br  a31lW      

Também podemos usar mais do que apenas igualdade para filtrar usuários. Para valores inteiros e reais, podemos usar maior que ou menor que:

SELECT * FROM users WHERE id > 1;

> 500

E também podemos usar outra lógica (AND, OR) como em Python:

SELECT * FROM users WHERE id > 1 AND email = "alano@codelab.br";

> 1 e email

SELECT * FROM users WHERE id > 3 OR email = "fulano@codelab.br";

> 3 ou email

Também podemos usar a palavra-chave IN para ver se um pedaço de dados é uma de várias opções:

SELECT * FROM users WHERE username IN ("fulano_dev", "ciclano_dev");

em

Podemos até usar expressões regulares para pesquisar palavras de forma mais ampla usando a palavra-chave LIKE. A consulta abaixo encontra todos os resultados com um f no username, usando % como caractere curinga.

SELECT * FROM users WHERE username LIKE "%f%";

username tem um 'f'

Funções

Também há várias funções SQL que podemos aplicar aos resultados de uma consulta. Elas podem ser úteis se não precisarmos de todos os dados retornados por uma consulta, mas apenas de algumas estatísticas resumidas dos dados.

UPDATE

Agora vimos como adicionar e pesquisar tabelas, mas também podemos querer atualizar linhas de uma tabela que já existem. Fazemos isso usando o comando UPDATE como mostrado abaixo. Como você pode ter adivinhado ao ler isso em voz alta, o comando encontra o usuário "alano_dev" e define seu email como alano2@codelab.br .

UPDATE users
SET email = "alano2@codelab.br"
WHERE username = "alano_dev";

DELETE

Também podemos querer a capacidade de excluir linhas do nosso banco de dados, e podemos fazer isso usando o comando DELETE.

DELETE FROM users WHERE username = "alano_dev";

Outras Cláusulas

Existem várias cláusulas adicionais que podemos usar para controlar as consultas que nos retornam

Unindo Tabelas

Até agora, trabalhamos apenas com uma tabela por vez, mas muitos bancos de dados na prática são preenchidos por várias tabelas que se relacionam de alguma forma. Como exemplo, no CodeClass, além da tabela de usuários, podemos ter uma tabela de cursos, mostrando cada curso disponível. Então, poderiamos relacionar as duas tabelas.

A tabela de cursos pode ser mais ou menos assim

Tabela Cursos

Dessa forma, podemos relacionar os usuários com cursos. Entretanto, um usuário pode estar inscrito em vários cursos, então se tivéssemos que relacionar os usuários com seus cursos, teríamos que criar várias colunas na tabela users, o que não é intuitivo. Assim, podemos criar mais uma tabela enrollments, que armazena as matrículas dos usuários. Ela se parece como:

table enrollments

Agora temos uma tabela enrollments, onde cada matrícula está relacionada a um usuário e a um curso. Como estamos usando a coluna id da tabela de usuários para preencher user_id e id da tabela de cursos para preencher course_id, chamamos esses valores de Chaves Estrangeiras.

A relação entre users e courses é conhecida como Muitos para muitos.

A tabela de matrículas é conhecida como uma tabela de associação, pois associa as tabelas users e courses.

Dessa forma, criamos diferentes tabelas que irão nos proporcionar uma melhor organização.

Consulta JOIN

Embora nossos dados agora estejam armazenados de forma mais eficiente, parece que pode ser mais difícil consultar nossos dados. Felizmente, SQL tem uma consulta JOIN onde podemos combinar duas tabelas para os propósitos de outra consulta.

Por exemplo, digamos que queremos listar todos os cursos de um usuário, então:

SELECT courses.title
FROM courses
JOIN enrollments ON courses.id = enrollments.course_id
WHERE enrollments.user_id = 1;

Aqui, o que fizemos foi selecionar os títulos dos cursos em que o usuário de id 1 está inscrito.

Para juntar as tabelas de curso e matrículas, procuramos linhas em enrollments onde courses.id = enrollments.course_id, o que conecta os cursos às matrículas.

Acabamos de usar algo chamado INNER JOIN, o que significa que estamos ignorando linhas que não têm correspondências entre as tabelas, mas existem outros tipos de junções, incluindo LEFT JOIN, RIGHT JOIN e FULL OUTER JOIN, que não discutiremos aqui em detalhes.

Indexação

Uma maneira de tornar nossas consultas mais eficientes ao lidar com tabelas grandes é criar um índice semelhante ao índice que você pode ver no final de um livro didático. Por exemplo, se sabemos que frequentemente procuraremos usuários por username, poderíamos criar um índice de username usando o comando:

CREATE INDEX user_username_index ON users (username);

Vulnerabilidades SQL

Agora que sabemos o básico de usar SQL para trabalhar com dados, é importante destacar as principais vulnerabilidades associadas ao uso de SQL. Começaremos com Injeção SQL.

Um ataque de injeção SQL é quando um usuário malicioso insere código SQL como entrada em um site para contornar as medidas de segurança do site. Por exemplo, digamos que nossa tabela de usuários seja mais simples (com username e password), e então um formulário de login na página inicial de um site. Podemos procurar o usuário usando uma consulta como:

SELECT * FROM users
WHERE username = username AND password = password;

Um usuário chamado Harry pode acessar este site e digitar harry como nome de usuário e 12345 como senha, caso em que a consulta seria assim:

SELECT * FROM users
WHERE username = "harry" AND password = "12345";

Um hacker, por outro lado, pode digitar harry" -- como nome de usuário e nada como senha. Acontece que -- representa um comentário em SQL, então a consulta ficaria assim:

SELECT * FROM users
WHERE username = "harry"--" AND password = "12345";

Porque nesta consulta a verificação de senha foi comentada, o hacker pode fazer login na conta de Harry sem saber sua senha. Para resolver este problema, podemos usar:

A outra principal vulnerabilidade quando se trata de SQL é conhecida como Condição de Corrida.

Uma condição de corrida é uma situação que ocorre quando várias consultas a um banco de dados ocorrem simultaneamente. Quando essas não são tratadas adequadamente, podem surgir problemas nos momentos precisos em que os bancos de dados são atualizados. Por exemplo, digamos que eu tenha $150 na minha conta bancária. Uma condição de corrida pode ocorrer se eu fizer login na minha conta bancária tanto no meu telefone quanto no meu laptop e tentar sacar $100 em cada dispositivo. Se os desenvolvedores de software do banco não lidaram com condições de corrida corretamente, então eu posso conseguir sacar $200 de uma conta com apenas $150. Uma solução potencial para este problema seria bloquear o banco de dados. Poderíamos não permitir nenhuma outra interação com o banco de dados até que uma transação seja concluída. No exemplo do banco, depois de navegar para a página "Fazer um Saque" no meu computador, o banco pode não me permitir navegar para essa página no meu telefone.

Django Models

Django Models são um nível de abstração sobre o SQL que nos permite trabalhar com bancos de dados usando classes e objetos Python em vez de consultas SQL diretas.

Vamos começar a usar models criando um projeto Django para o dev.learn() e criando um app dentro desse projeto.

>> django-admin startproject devlearn
>> cd devlearn
>> python manage.py startapp codeclass

Agora teremos que passar pelo processo de adicionar um app como de costume:

  1. Adicione codeclass à lista INSTALLED_APPS em settings.py
  2. Adicione uma rota para codeclass em urls.py:
    path("codeclass/", include("codeclass.urls")),
  3. Crie um arquivo urls.py dentro do aplicativo codeclass. E preencha-o com imports padrão de urls.py e listas.

Agora, em vez de criar caminhos reais e começar em views.py, criaremos alguns models no arquivo models.py. Neste arquivo, descreveremos quais dados queremos armazenar em nosso aplicativo. Então, Django determinará a sintaxe SQL necessária para armazenar informações em cada um de nossos models. Nesta etapa, trabalharemos inicialmente com os modelos de cursos, e veremos mais sobre os usuários à frente.

O modelo de cursos pode ser como:

class Course(models.Model):
    title = models.CharField(max_length=64)
    description = models.CharField(blank=True)

Vamos ver o que está acontecendo nesta definição de model:

Migrações

Agora, mesmo que tenhamos criado um model, ainda não temos um banco de dados para armazenar essas informações. Para criar um banco de dados a partir de nossos models, navegamos até o diretório principal de nosso projeto e executamos o comando.

>> python manage.py makemigrations

Este comando cria alguns arquivos Python que criarão ou editarão nosso banco de dados para poder armazenar o que temos em nossos models. Você deve obter uma saída que se parece mais ou menos com a abaixo, e se navegar até seu diretório migrations, notará que um novo arquivo foi criado para nós

saída de migrações 0

A seguir, para aplicar essas migrações ao nosso banco de dados, executamos o comando

>> python manage.py migrate

Agora, você verá que algumas migrações padrão foram aplicadas junto com as nossas, e também notará que agora temos um arquivo chamado db.sqlite3 no diretório do nosso projeto

saída de migração

Shell

Agora, para começar a adicionar informações e manipular este banco de dados, podemos entrar no shell do Django, onde podemos executar comandos Python dentro de nosso projeto.

python manage.py shell
Python 3.11.2 (main, Nov 30 2024, 21:22:50) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)

# Importar nosso model de curso
>>> from codeclass.models import Course

# Criar um novo curso
>>> c = Course(title="Web-Dev", description="Welcome to webdev course!")

# Inserir esse curso em nosso banco de dados
>>> c.save()

# Consultar todos os cursos armazenados no banco de dados
>>> Course.objects.all()
Out: <QuerySet [<Course: Course object (1)>]>

Quando consultamos nosso banco de dados, vemos que obtemos apenas um curso chamado Course object (1). Este não é um nome muito informativo, mas podemos consertar isso. Dentro de models.py, definiremos uma função __str__ que fornece instruções sobre como transformar um objeto Course em uma string:

class Course(models.Model):
    title = models.CharField(max_length=64)
    description = models.CharField(blank=True)

    def __str__(self):
        return self.title

Agora, quando voltamos ao shell, nossa saída é um pouco mais legível. Obs: talvez seja necessário sair e entrar do shell. Para recuperar o curso, podemos fazer c = Course.objects.get(title="Web-Dev")

# Criar uma variável chamada courses para armazenar os resultados de uma consulta
>>> courses = Course.objects.all()

# Exibindo todos os cursos
>>> courses
Out: <QuerySet [<Course: Web-Dev>]>

# Isolando apenas o primeiro curso
>>> course = courses.first()

# Imprimindo informações do curso
>>> course
Out: <Course: Web-Dev>

# Exibir id do curso
>>> course.id
Out: 1

# Exibir título do curso
>>> course.title
Out: 'Web-Dev'

# Exibir descrição do curso
>>> course.description
Out: 'Welcome to webdev course!''

Este é um bom começo. Agora, iremos criar um modelo para as matrículas. Devemos lembrar que a tabela de matrículas descreve uma relação Many-to-Many entre usuários e cursos. Para manter simplicidade, iremos também implementar uma tabela de estudantes (que substituirá, por enquanto, a tabela de usuários). Nosso models.py está assim:

from django.db import models

class Course(models.model):
    # Definições anteriores aqui

class Student(models.Model):
    username = models.CharField(max_length=50, unique=True)
    email = models.CharField(unique=True)
    bio = models.CharField(blank=True)
    
    def __str__(self):
        return self.username

class Enrollment(models.Model):
    student = models.ForeignKey(Student, on_delete=models.CASCADE, related_name='enrollments')
    course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='enrollments')
    enrolled_at = models.DateTimeField(auto_now_add=True) # Auto-definido na criação

    class Meta:
        unique_together = ('student', 'course') # Previne duplicatas

    def __str__(self):
        return f"{self.student.username} matriculado em {self.course.title}"

As mudanças nos campos student e course dentro da classe Enrollment são novas para nós:

Toda vez que fazemos alterações em models.py, temos que fazer migrações e depois migrar. Caso tivéssemos modificado algum campo do modelo Course, deveríamos remover as entradas antigas de cursos.

# Criar Novas Migrações
>> python manage.py makemigrations

# Migrar
>> python manage.py migrate

Agora, vamos testar esses novos models no shell do Django:

# Importar todos os models
>>> from codeclass.models import *

# Criar alguns novos estudantes
>>> ful = Student(username="fulano", email="fulano@codelab.br", bio="Hello")
>>> cic = Student(username="ciclano", email="ciclano@codelab.br", bio="Hi")


# Salvar os estudantes no banco de dados
>>> ful.save()
>>> cic.save()

# Recuperamos um dos cursos com o comando abaixo
>>> c = Course.objects.get(title="Web-Dev")

# Adicionar uma matrícula e salvá-la no banco de dados
>>> m = Enrollment(student=ful, course=c)
>>> m.save()

# Exibir algumas informações sobre a matrícula
>>> m
Out: <Enrollment: 1: fulano matriculado em Web-Dev>

# Exemplo de uso do related_name para buscar as matrículas de um estudante específico
>>> student = Student.objects.get(id=1)
>>> enrollments = student.course_enrollments.all()
>>> enrollments
Out: <QuerySet [<Enrollment: fulano matriculado em Web-Dev>]>

Iniciando nossa aplicação

Agora podemos começar a construir uma aplicação em torno deste processo de usar models para interagir com um banco de dados. Vamos começar criando uma rota de índice para nosso site. Dentro de urls.py:

urlpatterns = [
path('', views.index, name="index"),
]

Dentro de views.py:

from django.shortcuts import render
from .models import Student, Course, Enrollment

# Create your views here.
def index(request):
    return render(request, 'codeclass/index.html', {
        "courses": Course.objects.all(),
    })

Dentro de nosso novo arquivo layout.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Cursos</title>
</head>
<body>
    {% block body %}
    {% endblock %}
</body>
</html>

Dentro de um novo arquivo index.html:

{% extends "codeclass/layout.html" %}

{% block body %}
<h1>Cursos:</h1>
<ul>
    {% for course in courses %}
        <li>Curso {{ course.id }}: {{ course.title }} - {{ course.description }}</li>
    {% endfor %}
</ul>
{% endblock %}
    

O que fizemos aqui é criar uma página padrão onde temos uma lista de todos os cursos que criamos até agora. Quando abrimos a página agora, ela se parece com isso

Apenas um curso na lista

Agora, vamos adicionar mais alguns cursos à nossa aplicação voltando ao shell do Django:


# Criando e salvando um novo curso:
>>> p = Course(title="Python-Intro", description="Welcome to python course!")
>>> p.save()
        

Agora, nossa página se parece com isso:

2 cursos

Django Admin

Como é muito comum que os desenvolvedores precisem criar novos objetos como fizemos no shell, o Django vem com um admin interface padrão que nos permite fazer isso mais facilmente. Para começar a usar esta ferramenta, devemos primeiro criar um usuário administrativo:

>> python manage.py createsuperuser
Username: user_a
Email address: a@a.com
Password:
Password (again):
Superuser created successfully.

Agora, devemos adicionar nossos models ao aplicativo admin entrando no arquivo admin.py dentro de nosso app, e importando e registrando nossos models. Isso diz ao Django quais models gostaríamos de ter acesso no aplicativo admin.

from django.contrib import admin
from .models import Student, Course, Enrollment

# Registrar seus models aqui.
admin.site.register(Student)
admin.site.register(Course)
admin.site.register(Enrollment)

Agora, quando visitamos nosso site e adicionamos /admin à url, podemos fazer login em uma página que se parece com isso

login

Depois de fazer login, você será levado a uma página como a abaixo, onde pode criar, editar e excluir objetos armazenados no banco de dados

página admin

Agora, vamos adicionar mais algumas páginas ao nosso site. Começaremos adicionando a capacidade de clicar em um curso para obter mais informações sobre ele. Para fazer isso, vamos criar um caminho de URL que inclui o id de um curso:

path("<int:course_id>", views.course, name="course")

Então, em views.py criaremos uma função course que recebe um id de curso e renderiza um novo arquivo html:

def course(request, course_id):
    course = Course.objects.get(id=course_id)
    return render(request, "codeclass/course.html", {
        "course": course
    })

Agora criaremos um template para exibir essas informações de curso com um link de volta para a página inicial

{% extends "codeclass/layout.html" %}

{% block body %}
<h1>Curso {{ course.id }}</h1>
<ul>
    <li>Título: {{ course.title }}</li>
    <li>Descrição: {{ course.description }}</li>
</ul>
<a href="{% url 'index' %}">Todos os cursos</a>
{% endblock %}

Finalmente, precisamos adicionar a capacidade de vincular de uma página para outra, então modificaremos nossa página de índice para incluir links:

{% extends "codeclass/layout.html" %}

{% block body %}
<h1>Cursos:</h1>
<ul>
    {% for course in courses %}
        <li><a href="{% url 'course' course.id %}">Curso {{ course.id }}: {{ course.title }} - {{ course.description }}</li>
    {% endfor %}
</ul>
{% endblock %}

Agora nossa página inicial se parece com isso

Nova home, com cursos

E quando clicamos no primeiro curso, por exemplo, somos levados a esta página

Curso 1, web dev

Relações Muitos-para-Muitos

Agora, iremos trabalhar com uma nova característica no Django. Para isso, adaptaremos nosso model Course para ter um outro campo:

class Course(models.Model):
    title = models.CharField(max_length=64)
    description = models.CharField(blank=True)
    students = models.ManyToManyField(Student, blank=True, related_name="courses")

    def __str__(self):
        return self.title

Para realmente fazer essas alterações, devemos fazer migrações e migrar. Como nosso modelo antigo Course não serve mais aqui, devemos tratar as remoções de cursos (o que remove as matrículas, por meio da propriedade CASCADE) Podemos então registrar o model Course em admin.py e visitar a página admin para criar alguns cursos! Obs: para remover os cursos que fizemos até agora, entre no shell e digite Course.objects.all().delete().

Agora que adicionamos os cursos de Web-Dev e Python-Intro, vamos atualizar nossa página de curso para que ela exiba todos os estudantes em um curso. Primeiro visitaremos views.py e atualizaremos nossa visualização de curso para fornecer uma lista de estudantes como contexto. Acessamos a lista usando o related_name que definimos anteriormente.

def course(request, course_id):
    course = Course.objects.get(id=course_id)
    students = course.students.all()
    return render(request, "codeclass/course.html", {
        "course": course,
        "students": students
    })

Agora, adicione uma lista de estudantes a course.html:

<h2>Estudantes:</h2>
<ul>
{% for student in students %}
    <li>{{ student.username }}</li>
{% empty %}
    <li>Nenhum estudante.</li>
{% endfor %}
</ul>

Neste ponto, quando clicamos no primeiro curso (que possui id=4 agora, pelas remoções), vemos

curso 4

Agora, vamos trabalhar em dar aos visitantes do nosso site a capacidade de adicionar uma matrícula (para fins didáticos, qualquer visitante pode matricular qualquer estudante disponível, o que não é o ideal em um cenário real). Faremos isso adicionando uma rota de matrícula em urls.py:

path("<int:course_id>/enroll", views.enroll, name="enroll")

Agora, adicionaremos uma função enroll a views.py que adiciona um estudante a um curso:

def enroll(request, course_id):

    # Para uma requisição post, adicionar uma
    if request.method == "POST":

        # Acessando o curso
        course = Course.objects.get(pk=course_id)

        # Encontrando o id do estudante a partir dos dados do formulário enviado
        student_id = int(request.POST["student"])

        # Encontrando o estudante com base no id
        student = Student.objects.get(pk=student_id)

        # Adicionar estudante no curso, mas relações de cursos e enrollments
        course.students.add(student)
        Enrollment.objects.create(student=student, course=course)

        # Redirecionar usuário para a página do voo
        return HttpResponseRedirect(reverse("course", args=(course.id,)))
        

A seguir, adicionaremos algum contexto ao nosso template de curso para que a página tenha acesso a todos que não são atualmente estudantes no curso usando a capacidade do Django de excluir certos objetos de uma consulta:

def course(request, course_id):
    course = Course.objects.get(id=course_id)
    students = course.students.all()
    non_students = Student.objects.exclude(id__in=students.values_list('id', flat=True)).all()
    return render(request, "codeclass/course.html", {
        "course": course,
        "students": students,
        "non_students": non_students
    })

Agora, adicionaremos um formulário à nossa página HTML de curso usando um campo de entrada select:

<form action="{% url 'enroll' course.id %}" method="post">
    {% csrf_token %}
    <select name="student" id="">
        {% for student in non_students %}
            <option value="{{ student.id }}">{{ student.username }}</option>
        {% endfor %}
    </select>
    <input type="submit">
</form>

Agora, vamos ver como o site fica quando vamos para uma página de curso e adicionamos estudantes:

formulário

Outra vantagem de usar o aplicativo admin do Django é que ele é personalizável. Por exemplo, se desejarmos ver todos os aspectos de um curso na interface admin, podemos criar uma nova classe dentro de admin.py e adicioná-la como um argumento ao registrar o model Course:

class CourseAdmin(admin.ModelAdmin):
    list_display = ("id", "title", "description", "students_list")
    
    # Lista de estudantes
    def students_list(self, obj):
        return ", ".join([student.username for student in obj.students.all()])
    students_list.short_description = "Students"
    
# Registrar seus models aqui.
admin.site.register(Course, CourseAdmin)

Uma coisa a se notar é que não passamos a lista de estudantes students diretamente. Como students do model é um campo Many-to-Many, não é suportado pelo list_display, visto que retorna uma QuerySet. Então, para mitigar isto, podemos criar uma função que retorna a lista de estudantes em uma string. Também há outras coisas que podemos definir, como limite de estudantes na string, etc.

Agora, quando visitamos a página admin para cursos, podemos ver as mudanças:

tabela admin

Confira a documentação do admin do Django para encontrar mais maneiras de personalizar o aplicativo admin.

Usuários

A última coisa que discutiremos na aula de hoje é a ideia de autenticação, ou permitir que os usuários façam login e logout de um site. Felizmente, o Django torna isso muito fácil para nós, então vamos passar por um exemplo de como faríamos isso. Começaremos criando um novo app chamado users. Aqui passaremos por todas as etapas normais de criação de um novo app, mas em nosso novo arquivo urls.py, adicionaremos mais algumas rotas:

urlpatterns = [
    path('', views.index, name="index"),
    path("login", views.login_view, name="login"),
    path("logout", views.logout_view, name="logout")
]

Vamos começar criando um formulário onde um usuário pode fazer login. Criaremos um arquivo layout.html como sempre, e então criaremos um arquivo login.html que contém um formulário e que exibe uma mensagem se existir.

{% extends "users/layout.html" %}

{% block body %}
{% if message %}
    
{{ message }}
{% endif %} <form action="{% url 'login' %}" method="post"> {% csrf_token %} <input type="text", name="username", placeholder="Nome de Usuário"> <input type="password", name="password", placeholder="Senha"> <input type="submit", value="Login"> </form> {% endblock %}

Agora, em views.py, adicionaremos três funções:

def index(request):
# Se nenhum usuário estiver logado, retornar à página de login:
    if not request.user.is_authenticated:
    return HttpResponseRedirect(reverse("login"))
return render(request, "users/user.html")

def login_view(request):
    return render(request, "users/login.html")

def logout_view(request):
    # Pass é uma maneira simples de dizer ao python para não fazer nada.
    pass

A seguir, podemos ir para o site admin e adicionar alguns usuários. Depois de fazer isso, voltaremos para views.py e atualizaremos nossa função login_view para lidar com uma requisição POST com nome de usuário e senha:

# Importações adicionais que precisaremos:
from django.contrib.auth import authenticate, login, logout

def login_view(request):
    if request.method == "POST":
        # Acessando nome de usuário e senha dos dados do formulário
        username = request.POST["username"]
        password = request.POST["password"]

        # Verificar se nome de usuário e senha estão corretos, retornando objeto User se sim
        user = authenticate(request, username=username, password=password)

        # Se um objeto user for retornado, fazer login e rotear para a página inicial:
        if user:
            login(request, user)
            return HttpResponseRedirect(reverse("index"))
        # Caso contrário, retornar à página de login novamente com novo contexto
        else:
            return render(request, "users/login.html", {
                "message": "Credenciais Inválidas"
            })
    return render(request, "users/login.html")

Agora, criaremos o arquivo user.html que a função index renderiza quando um usuário está autenticado:

{% extends "users/layout.html" %}

{% block body %}
<h1>Bem-vindo, {{ request.user.first_name }}</h1>
<ul>
    <li>Nome de Usuário: {{ request.user.username }}</li>
    <li>Email: {{ request.user.email }}</li>
</ul>

<a href="{% url 'logout' %}">Sair</a>
{% endblock %}

Finalmente, para permitir que o usuário saia, atualizaremos a função logout_view para que ela use a função logout integrada do Django:

def logout_view(request):
logout(request)
return render(request, "users/login.html", {
            "message": "Deslogado"
        })

Agora que terminamos, aqui está uma demonstração do site

Demo1 Demo2 Demo3 Demo4

Isso é tudo para esta aula!