|
||
---|---|---|
.github/workflows | ||
app | ||
imgs_readme | ||
.dockerignore | ||
.gitignore | ||
docker-compose.yml | ||
Dockerfile | ||
README2.md | ||
README3.md | ||
README.md | ||
requirements.dev.txt | ||
requirements.txt |
REST API Django
Tecnologias
- Python 3.12.0
- Django 4.2.5
- Django REST Framework 3.14
- Django REST Swagger
- Docker 24.0.6 y (Docker-compose incluido con docker cli)
- PostgreSQL
- Git
- GitHub Actions
%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'basis'}}}%%
flowchart
subgraph " "
direction TB
SW{Swagger-UI}
subgraph APP["App Container"]
RF("REST Framework")
DJ("Django")
PY("Python")
end
subgraph DBC["DB Container"]
DB[(PostgreSQL)]
end
RF <--> SW
RF <--> DJ <--> PY
DB <--> DJ
end
Estructura del proyecto
app
Django projectapp/core/
código compartido entre multiples appsapp/user/
código relativo al usuarioapp/recipe/
código relativo a las recetas
TDD
Test Driven Develoment
%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'natural'}}}%%
flowchart
subgraph " "
direction LR
WT[Write Test]
RTF["Run Test
(Fails)"]
AF[Add Feature]
RTP["Run Test
(Passes)"]
RF[Refactor]
end
WT --> RTF --> AF --> RTP --> RF
RF --> RTP
- Esto proporciona un mejor entendimiento del código
- Permite realizar cambios con confianza
- Reduco bugs
Unitests
- Código que prueba código
- Establecer condiciones/entradas
- Correr fragmentos de código
- Verificar salidas con
assertions
- Beneficios
- Asegurar que el código corre como se espera
- Atrapar bugs
- Mejorar fiabilidad
- Proporciona confianza
Docker
¿Por qué usar Docker?
- Consistencia entre ambientes de desarrollo y producción
- Facilita la colaboración entre desarrolladores
- Todas las dependencias como código
- Requerimientos de Python
- Dependencias del S.O.
- Facilidad para limpiar el sistema (post-dev)
- Ahorro de tiempo
¿Como usar Docker?
- Crear dockerfile
- Crear docker compose configuration
- Correr todos los comandos usando Docker compose
Docker con GitHub Actions
- Docker Hub tiene un limite de acceso:
- 100 pulls/6 hr para usuarios sin authentificación
- 200 pulls/6 hr para usuarios con authentificación
- GitHub Actions es un servicio compartido
- 100 pulls/6 hr considera TODOS los usuarios
- Autenticación con Docker Hub
- Crear cuenta
- Configurar credenciales
- Login antes de correr un trabajo (job)
- Obtener 200 pulls/6 hr gratis
Configurar Docker
- Creación dockerfile
- Lista de pasos para crear imagen
- Escoger una imagen basada en python
- Instalar dependencias
- Establecer usuarios
Docker Compose
- Como se debe utlizar la imagen de docker
- Definir servicios
- Nombre (ej. app)
- Mapeo de puertos
- Mapeo de volumenes
- Correr todos los comandos a travez de Docker Compose
ej.docker compose run --rm app sh -c "python manage.py collectstatic"
docker compose
Ejecuta un comando de Docker Composerun
comienza un contenedor específico definido en la configuración--rm
remueve el contenedorapp
es el nombre del servicio/aplicaciónsh -c
pasa una orden a la shell del container"python manage.py ..."
comando a correr dentro del contenedor
docker build .
docker compose build
Linting
- Instalar
flake8
- requirements.dev.txt
- Configuración flake8
- Correr a travez de docker compose
docker compose run --rm app sh -c "flake8"
Testing
- Django test suite
- Configurar test por cada aplicación Django
- Correr a travez de docker compose
docker compose run --rm app sh -c "python manage.py test"
Creación del proyecto Django
docker compose run -rm app sh -c "django-admin startproject app ."
Iniciar el servidor
docker compose up
GitHub Actions
- Herramienta de automatización
- Similar a Travis-CI, GitLab CI/CD, Jenkins
- Ejecuta tareaas cunado el código cambia
- Tareas automatizadas comunes:
- Despliege/implementación
- Code Linting
- Tests Unitarios
Funciona con Trigger ej. push
to GitHub
¿Como funciona?
%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'natural'}}}%%
flowchart
subgraph " "
direction LR
TG["<b>Trigger</b>
Push to GitHub"]
JB["<b>Job</b>
Run unit tests"]
RS["<b>Result</b>
Success/fail"]
end
TG ==> JB ==> RS
Costo
- Se cobra por minutos de uso
- 2.000 minutos gratis
Configuranción GitHub Actions
- Creación de archivo
checks.yml
- Set Trigger
- Añadir passos para correr pruebas y linting
- Configurar DockerHub auth
- Necesitado para jalar imagenes base
- Limites:
- Anónimos: 100/6h
- Atentificado 200/6h
- GitHub Actions usan IP compartida, la limitación aplica para todos los usuarios al autenticar con DockerHub se obtienen 200/6h de uso exclusivo
Django Tests
- Basado en la biblioteca unittest
- Caracteristicas añadidas de Django
- Cliente del pruebas dummy web browser
- Simula autenticación
- Base de datos temporal
- Caracteristicas añadidas de REST Framework
- Cliente de pruebas de la API
¿Donde van los tests?
test.py
por aplicación- O crear un subdirectorio
tests/
para dividir las pruebas - Recordar
- Solo usar
tests.py
o directoriotests/
, no ambos - Los moduloes de prueba deben comenzar con
test_
- Los directorios de pruebas deben contener el archivo
__init__.py
- Solo usar
demo_code
├── app_one
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── models.py
│ ├── tests.py
│ ├── views.py
│ └── admin.py
└── app_two
├── tests
│ ├── __init__.py
│ ├── test_module_one.py
│ └── test_module_two.py
├── __init__.py
├── admin.py
├── apps.py
├── models.py
├── views.py
└── admin.py
Test DB
- Codigo de pruebas que usa la base de datos
- Base de datos específica para pruebas
%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'natural'}}}%%
flowchart
subgraph " "
direction LR
RT["<b>Runs Tests</b>"]
CD["<b>Clears data</b>"]
end
RT ==> CD ==> RT
- Por defecto, esto ocurre para cada test
Clases de Test
SimpleTestCase
- Sin integración con BD
- Util si no se require una BD para la lógica a probar
- Ahorra tiempo de ejecución
TestCase
- Integración con BD
- Util para probar código que usa la BD
Ej. test
""""
Unit test for views
""""
from django.test import SimpleTestCase
form app_two import views
class ViewsTests(SimpleTestCase):
def test_make_list_unique(self):
""" Test making a list unique. """
sample_items = [1, 1, 2, 2, 3, 4, 5, 5]
res = views.remove_duplicates(sample_items)
self.assertEqual(res, [1, 2, 3, 4, 5])
python manage.py test
Creación del primer test
Modulo a testear calc.py
con tests.py
"""
Sample tests
"""
from django.test import SimpleTestCase
from app import calc
class CalcTests(SimpleTestCase):
""" Test the calc module. """
def test_add_numbers(self):
""" Test adding numbers together. """
res = calc.add(5, 6)
self.assertEqual(res, 11)
Correr el test
docker compose run --rm app sh -c "python manage.py test"
Found 1 test(s).
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Usando TDD
- Crear la prueba para el comportamiento esperado tests.py
- La prueba debe fallar
- Crear el código para que el test pase (añadir la funcionalidad) calc.py
Mocking
- Evita depender de servicios externos, pues estos
- No garantizan disponibilidad
- Pueden hacer que los tests sean impredecibles e incositentes
- Evita consecuencias no intecionadas, por ejm.
- Enviar mails accidentalmente
- Sobrecargar servicios externos
ej.
%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'natural'}}}%%
flowchart
subgraph " "
direction LR
RU["<b>register_user()</b>"]
CIDB["<b>create_in_db()</b>"]
SWE["<b>send_welcome_email()</b>"]
MS[e-mail sent]
end
RU --> CIDB --> SWE --x MS
- Previene el envio del correo electónico
- Asegura que
send_welcome_email()
es llamado correctamente
Otro beneficio de Mocking
- Acelera las pruebas
%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'natural'}}}%%
flowchart
subgraph " "
direction LR
CDB["<b>check_db()</b>"]
SLP["<b>sleep()</b>"]
end
CDB --x SLP --> CDB
Como usar mock
- Se usa
unittest.mock
MagicMock/Mock
reemplaza objetos realespatch
sobreescribe el código de las pruebas
Testing Web Request
Probando la API
- Hacer peticiones reales
- Comprobar resultados
Django REST Framework provee un cliente para la API basado en Django TestClient
,
este realiza los requests y permite verificar resultados. Incluso permite
sobreescribir la autenticación, para probar la funcionalidad de la API, haciendo
que esta asuma que se esta autentificado.
from django.test import SimpleTestCase
from rest_framework.test import APIClient
class TestViews(SimpleTestCase):
def test_get_greetings(self):
""" Test getting greetings. """
client = APIClient()
res = client.get('/greetings/')
self.assertEqual(res.status_code, 200)
self.assertEqual(
res.data,
["Hello!", "Bonjour!", "Hola!"],
)
Problemas comunes en las pruebas
El test no se ejecuta
System check identified no issues (0 silenced).
----------------------------------------------------------------------
Ran 0 test in 0.000s
OK
- Se ejecutan menos tests que la cantidad creada
Razones posibles
- Falta el archivo
__init__.py
en el directoriotests.py
- Indentación de los casos de prueba
- Prefijo
test
faltante en los metodostest_some_function(self):
ImportError
al correr las pruebas
raise ImportError(
ImportError: 'tests' module incorrectly imported from ....
Expected ....Is this module globally installed?
)
Razon posible
- Existencia de
tests/
ytests.py
en la misma aplicación
Configurando la base de datos
PostgreSQL se integra bien con django, es oficialmente soportada.
Se utiliza Docker compose, se define la configuración dentro del proyecto para que sea reutilizable. Datos persistentes utilizando volumes. Maneja la configuración de la red. Configuranción usando variables de entorno.
Arquitectura
%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'basis'}}}%%
flowchart
direction TB
subgraph "<b>Docker Compose</b>"
DB[(PostgreSQL)]
direction LR
APP("<b>App</b>
Django")
DB <-..-> APP
end
Conectividad de red
services:
app:
depends_on:
- db
db:
image: postgres:16-alpine
- Establecer
depends_on
para iniciardb
primero - El servicio
app
puede usar el hostname dedb
Volumes
- Persistencia de datos
- Mapea el directorio del contenedeor con la máquina local
db:
image: postgres:16-alphine
volumes:
- dev-db-data:/var/lib/postgresql/data
volumes:
dev-db-data:
dev-static-data:
Agregando el servicio base de datos
services:
app:
build:
context: .
args:
- DEV=true
ports:
- "8000:8000"
volumes:
- ./app:/app
command: >
sh -c "python manage.py runserver 0.0.0.0:8000"
environment:
- DB_HOST=db
- DB_NAME=devdb
- DB_USER=devuser
- DB_PASS=changeme
depends_on:
- db
db:
image: postgres:16-alpine
volumes:
- dev-db-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=devdb
- POSTGRES_USER=devuser
- POSTGRES_PASSWORD=changeme
volumes:
dev-db-data:
Probar que todo funcione correctamente docker compose up
Configuración de la BD en Django
- Especificar como django conecta con la BD
settings.py
- Engine (tipo de BD)
- Hostname (DB IP o dominio)
- Port
- DB name
- DB user
- DB password
- Instalar las dependencias del conector
- Actualizar los requerimientos
DATABASES = {
'default': {
'ENGINE' django.db.backends.postgresql',
'HOST': os.environ.get('DB_HOST'),
'NAME': os.environ.get('DB_NAME'),
'USER': os.environ.get('DB_USER'),
'PASSWORD': os.environ.get('DB_PASSWORD'),
}
}
Psycopg2
Packete necesario para conectar Django con la base de datos PostgreSQL, es el conector mas popular en Python y es soportado por Django. Se puede instalar de las siguientes maneras:
psycopg2-binary
(Ok para desarrollo, no para producción)psycopg2
(se compila desde el código fuente, hay que satisfacer dependencias para esto)- Fácil de instalar con Docker
Lista de dependencias
- Compilador C
- python3-dev
- libpq-dev
Equivalentes para Apine
- postgresql-client
- build-base
- postgresql-dev
- musl-dev
Equivalentes para Debian (python:3.12.0-slim-bookworm)
- postgresql-client
- python3-dev
- libpq-dev
- gcc confimar
- python3-psycopg2 confirmar
Una buena practica es limpiar las dependencias que ya no serán necesarias
Previniendo Race Condition con docker compose
Usar depends_on
asegura que el service
comience (no asegura que la app
este corriendo)
Docker services timeline
La solución es hace que Django espere a la base de datos db
. Este chequea
la disponibilidad de la base de datos y continua cuando esta disponible
Comando personalizado de administración de Django en app core
Creación de aplicación core
docker compose run --rm app sh -c "python manage.py startapp core"
Se eliminal el archvo tests.py
para crear y usar el directorio app/core/tests/
donde se agrega el correspondiente archivo __init__.py
Se crean los directorios app/core/management/commands/
con los archivos
__init__.py
y commands.py
"""
Django command to wait for the DB to be available.
"""
import time
from psycopg2 import OperationalError as Psycopg2OpError
from django.db.utils import OperationalError
from django.core.management.base import BaseCommand
class Command(BaseCommand):
""" Django commando to wait for database. """
def handle(self, *args, **options):
"""Entrypoint for command."""
self.stdout.write('Waiting for database...')
db_up = False
while db_up is False:
try:
self.check(databases=['default'])
db_up = True
except (Psycopg2OpError, OperationalError):
self.stdout.write('Database unavailable, waiting 1 second...')
time.sleep(1)
self.stdout.write(self.style.SUCCESS('Database available!'))
Correr tests
docker compose run --rm app sh -c "python manage.py test"
[+] Creating 1/0
✔ Container django_rest_api_udemy-db-1 Running 0.0s
Found 4 test(s).
System check identified no issues (0 silenced).
..Waiting for database...
Database unavailable, waiting 1 second...
Database unavailable, waiting 1 second...
Database unavailable, waiting 1 second...
Database unavailable, waiting 1 second...
Database unavailable, waiting 1 second...
Database available!
.Waiting for database...
Database available!
.
----------------------------------------------------------------------
Ran 4 tests in 0.005s
OK
Correr command
docker compose run --rm app sh -c "python manage.py wait_for_db"
[+] Creating 1/0
✔ Container django_rest_api_udemy-db-1 Running 0.0s
Waiting for database...
Database available!
Correr linter docker compose run --rm app sh -c "flake8"
y corregir
Migraciones de la base de datos
Django ORM
- Object Reational Mapper (ORM)
- Capa de abstracción para los datos
- Django maneja la estructura y cambios de la BD
- Ayuda a enfocarse en el código de Python
- Permite usar otras bases de datos
%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'basis'}}}%%
flowchart
subgraph "ORM"
DM["Define models"]
GMF["Generate
migration files"]
SD["Setup database"]
STD["Store data"]
DM -.-> GMF -.-> SD -.-> STD
end
Models
- Cada modelo mapea a una tabla
- Los modelos contienen
- Nombre
- Campos
- Otra metadata
- Lógica en Python personalizada
Modelo ejemplo
class Ingredient(models.Model):
"""Ingredient for recipies."""
name = models.CharField(max_length=255)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
)
Creación de las migraciones
- Asegura que la app esta activa en
settings.py
- Se utiliza el CLI de Django
python manage.py makemigrations
- Aplicar migraciones
python manage.py makemigrations
- Correr despues de esperar por la base de datos
Actualización de Docker Compose y CI/CD
...
command: >
sh -c "python manage.py wait_for_db &&
python manage.py migrate &&
python manage.py runserver 0.0.0.0:8000"
...
docker compose down
[+] Running 3/3
✔ Container django_rest_api_udemy-app-1 Removed 0.0s
✔ Container django_rest_api_udemy-db-1 Removed 0.1s
✔ Network django_rest_api_udemy_default Removed 0.1s
docker compose up
Corriendo wait_for_db
antes de los tests en GitHub Actions
...
- name: Test
run: >
docker compose run --rm app sh -c "python manage.py wait_for_db &&
python manage.py test"
...
User Model
Autenticación Django
- Sistema de autenticación built-in
- Framework para características básicas
- Registro
- Login
- Autorización
- Se integran con el panel de administración de Django
Django user model
- Es la fundación del sistema de autenticación de Django
- Django incorpora por defecto un modelo de usuario
- Utiliza un nombre de usuario en vez de un email
- No es facil de personalizar
- Creación de modelo de usuario personalizado para nuevos proyectos
- Permite el uso de email en vz de nombre de usuario
- Asegura compatibilidad del proyecto con posibles cambios del modelo usuario en versiones futuras
Creación del modelo
- Basado en la clase
AbstractBaseUser
yPermissionsMixin
- Creación de administrador personalizado
- Se establece
AUTH_USER_MODEL
ensettings.py
para utlizar este modelo - Creación y ejecución de las migraciones
AbstractBaseUser
- Proporciona las características de autenticación
- No incluye campos
PermissionsMixin
- Soporte para el sistema de permisos de Django
- Incuye todos los campos y métodos necesarios
Problemas comunes
- Correr migraciones antes de crear el modelo personalizado
- Crear el modelo personalizado primero
- Tipeo
- Indentación
User Model personalizado
Campos de usuario
- email
EmailField
- name
CharField
- is_active
BooleanField
- is_staff
BooleanField
User Model administrador
- Usado para administar objetos
- Lógica personalizada para crear objetos
- Hash passwords
- Metodos para el CLI de Django
- Create superuser
BaseUserManager
- Clase base para administrar usuarios
- Métodos útliles de ayuda
normalize_email
para almacenar emails de forma consistente
- Métodos a definir
create_user
llamado al crear usuarioscreate_superuser
usado por el CLI para crear un super usuario (admin)
Agregando Unitetst para el modelo usuario personalizado
from django.test import TestCase
from django.contrib.auth import get_user_model
class ModelTests(TestCase):
def test_create_user_with_email_sucessfull(self):
email = 'test@example.com'
password = 'testpass123'
user = get_user_model().objects.create_user(
email=email,
password=password,
)
self.assertEqual(user.email, email)
self.assertTrue(user.check_password(password))
Agregar usuario personalizado al proyecto
from django.db import models
from django.contrib.auth.models import (
AbstractBaseUser,
BaseUserManager,
PermissionsMixin,
)
class UserManager(BaseUserManager):
"""Manager for users."""
def create_user(self, email, password=None, **extra_fields):
"""Create, save and return a new user."""
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
class User(AbstractBaseUser, PermissionsMixin):
"""User in the system."""
email = models.EmailField(max_length=255, unique=True)
name = models.CharField(max_length=255)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
# Asignar el UserManager a esta clase User
objects = UserManager()
USERNAME_FIELD = 'email'
Actualizar settings.py
para que Django utilize este modelo de autenticación
agregando al final del archivo lo sgte.
...
AUTH_USER_MODEL = 'core.User'
Crear migraciones
docker compose run --rm app sh -c "python manage.py makemigrations"
[+] Creating 1/0
✔ Container recipes_api_django-db-1 Running 0.0s
Migrations for 'core':
core/migrations/0001_initial.py
- Create model User
Codigo autogenerado 0001_initial.py
para app core
Aplicar migraciones
docker compose run --rm app sh -c "python manage.py wait_for_db && python manage.py migrate"
[+] Creating 1/0
✔ Container recipes_api_django-db-1 Running 0.0s
Waiting for database...
Database available!
Operations to perform:
Apply all migrations: admin, auth, contenttypes, core, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0001_initial... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying core.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying sessions.0001_initial... OK
En caso arrojar error por haber aplicado alguna migración previa se puede correr
docker rm <db_volume>
, si "esta en uso" primero correr docker compose down
.
Los nombres se pueden ver con docker volume ls
Al correr los tests nuevamente docker compose run --rm app sh -c "python manage.py test"
[+] Creating 1/0
✔ Container recipes_api_django-db-1 Running 0.0s
Found 5 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...Waiting for database...
Database unavailable, waiting 1 second...
Database unavailable, waiting 1 second...
Database unavailable, waiting 1 second...
Database unavailable, waiting 1 second...
Database unavailable, waiting 1 second...
Database available!
.Waiting for database...
Database available!
.
----------------------------------------------------------------------
Ran 5 tests in 0.675s
OK
Destroying test database for alias 'default'...
Normalización de direcciones de email
Test normalize email addresses
def test_new_user_email_normalized(self):
"""Test email is normalized for new users."""
sample_emails = [
['test1@EXAMPLE.com', 'test1@example.com'],
['test2@Example.com', 'test2@example.com'],
['TEST3@EXAMPLE.COM', 'TEST3@example.com'],
['test4@example.COM', 'test4@example.com'],
]
for email, expected in sample_emails:
user = get_user_model().objects.create_user(email, 'sample123')
self.assertEqual(user.email, expected)
Modificar el ceate_user
de app/core/models.py
para utilizar el método normalize_email
que provee la clase BaseUserManager
- user = self.model(email=email, **extra_fields)
+ user = self.model(email=self.normalize_email(email), **extra_fields)
Requerir email
Test requerir email
def test_new_user_withouth_email_raises_error(self):
"""Test that creating a user withouth an email raises a ValueError."""
with self.assertRaises(ValueError):
get_user_model().objects.create_user('', 'test123')
Modificar el ceate_user
de app/core/models.py
y levantar excepción ValueError
si usuario no ingresa un email
def create_user(self, email, password=None, **extra_fields):
"""Create, save and return a new user."""
if not email:
raise ValueError('User must have an email address.')
user = self.model(email=self.normalize_email(email), **extra_fields)
user.set_password(password)
user.save(using=self._db)
Funcionalidad super usuario
Test creación de super usuario
def test_create_superuser(self):
"""Test creating a superuser."""
user = get_user_model().objects.create_superuser(
'test@example.com',
'test123',
)
self.assertTrue(user.is_superuser)
self.assertTrue(user.is_staff)
Creación del método create_superuser
para la clase UserManager
en
app/core/models.py
def create_superuser(self, email, password):
"""Create and return a new superuser."""
user = self.create_user(email, password)
user.is_staff = True
user.is_superuser = True
user.save(using=self._db)
return user
Probando el modelo de usuario
Correr docker compose up
y en otra terminal
docker compose run --rm app sh -c "python manage.py createsuperuser"
[+] Creating 1/0
✔ Container recipes_api_django-db-1 Running 0.0s
Email: admin@example.com
Password:
Password (again):
Superuser created successfully
Django Admin
Es la interfáz gráfica para los modelos creados en el proyecto, permite la administración basica C.R.U.D. Requiere muy poco cóidgo para ser usado
Se activa por modelo, en admin.py
Personalización del administrador
Se crea una clase basada en ModelAdmin
o UserAdmin
donde se sobrescribe o
establecen variables de clase
ejemplo
class UserAdmin(BaseUserAdmin):
"""Define de admin pages for users."""
ordering = ['id']
list_display = ['emial', 'name']
fieldsets = (
(None, {'fields': ('email', 'password')}),
)
readonly_files = ['last_login']
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': (
'email',
),
})
)
Creando test para el administrador
class AdminSiteTests(TestCase):
"""Tests for Django admin."""
def setUp(self):
"""Create user and client."""
self.client = Client()
self.admin_user = get_user_model().objects.create_superuser(
email='admin@example.com',
password='testpass123',
)
self.client.force_login(self.admin_user)
self.user = get_user_model().objects.create_user(
email='user@example.com',
password='testpass123',
name='Test User'
)
def test_users_list(self):
"""Test that users are listed on page."""
url = reverse('admin:core_user_changelist')
res = self.client.get(url)
self.assertContains(res, self.user.name)
self.assertContains(res, self.user.email)
- Django docs reversing admin urls
- Django docs testing tools
Correr test docker compose run --rm app sh -c "python manage.py test"
Activar admin para core app
En admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from core import models
class UserAdmin(BaseUserAdmin):
"""Define the admin pages for users."""
ordering = ['id']
list_display = ['email', 'name']
admin.site.register(models.User, UserAdmin)
Modificar admin para que use los campos de usuario personalizado
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.translation import gettext_lazy as _
from core import models
class UserAdmin(BaseUserAdmin):
"""Define the admin pages for users."""
ordering = ['id']
list_display = ['email', 'name']
filedsets = (
(None, {'fields': ('email', 'password')}),
(
_('Permissions'),
{
'fields': (
'is_active',
'is_staff',
'is_superuser',
)
}
),
(_('Important dates', {'fields': ('last_login',)})),
)
readonly_fields = ['last_login']
admin.site.register(models.User, UserAdmin)
-
Requiere modificar pues espera campos que el modelo no tiene
Test página de creación de usuario
def test_create_user_page(self):
"""Test the create user page works."""
url = reverse('admin:core_user_add')
res = self.client.get(url)
self.assertEqual(res.status_code, 200)
Actualizar clase UserAdmin
para que use los campos personalizados
class UserAdmin(BaseUserAdmin):
...
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': (
'email',
'password1',
'password2',
'name',
'is_active',
'is_staff',
'is_superuser',
)
}),
)
...
Documentación de la API
Es necesario tener acceso a una buena documentación para que los desarrolladores puedan saber como usarla. Se documenta todo lo que sea necesario para usar la API
- Endopoints disponibles (paths)
- Métodos soportados
GET
,POST
,PUT
,PATCH
,DELETE
... - Formateo de payloads (inputs). Parametros, Post en formato JSON
- Formateo de respuestas (outputs). Respuesta en formato JSON
- Proceso Autenticación
Opiones de documentación
- Manual
- Documento de texto
- Markdown
- Automatizada
- Usa la metadata del código (comments)
- Genera páginas de documentación
Autodocs de DRF
- Documentación Autogenerada (3rd party library)
drf-spectacular
- Genera el "schema"
- Interfaz web navegable
- Test requests
- Maneja la autenticación
Como funciona
- Creación del archivo
schema
- Pasa el schema al GUI
Open API Schema
- Estandar para describir APIs
- Popular en la industria
- Soportada por la mayoría de herramientas de documentación de API
Ejemplo Schema
fragmento
/api/recipe/ingredients/:
get:
oprationId: recipe_ingredients_list
description: Manage ingredients in the database.
parameteres:
- in: query
name: assigned_only
schema:
type: integer
enum:
- 0
- 1
description: Filter by item assigned to recipies
tags:
- recipe
security:
- tokenAuth: []
responses:
'200':
content:
application/json
schema:
type: array
items:
$ref: '#/components/schemas/Ingredient'
description: ''
...
Implementación DRF
Se agrega dependencia en drf-spectacular>=0.16
en requirements.txt
Instalar app, en settings.py
INSTALLED_APPS = [
...
'core',
'rest_framework',
'drf_spectacular',
]
...
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
Activar las URLS
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
path('api/schema/', SpectacularAPIView.as_view(), name='api-schema'),
path(
'api/docs',
SpectacularSwaggerView.as_view(url_name='api-schema'),
name='api-docs'),
]
docker compose run
127.0.0.1:8000/api/docs
User API
Diseño
- Registro de usario
- Creación de token de autenticación
- Consultar y actualizar perfil
Endpoins
Endpoint | Method | Descripción |
---|---|---|
user/create |
POST |
Registrar nuevo usuario |
user/token |
POST |
Crea un nuevo token |
user/me/ |
PUT/PATCH |
Actualizar perfíl |
Creación user app
docker compose run --rm app sh -c "python manage.py startapp user"
[+] Creating 1/0
✔ Container recipes_api_django-db-1 Created 0.0s
[+] Running 1/1
✔ Container recipes_api_django-db-1 Started 0.2s
Activar user
app en settings.py
Test User API
app/user/tests/test_user_api.py
CREATE_USER_URL = reverse('user:create')
def create_user(**params):
"""Create and return a new user."""
return get_user_model().objects.crate_user(**params)
class PublicUserApiTest(TestCase):
"""Test the public features of the user API."""
def setUp(self):
self.client = APIClient()
def test_create_user_success(self):
"""Tests creating a user is successful."""
payload = {
'email': 'test@example.com',
'password':'testpass123',
'name': 'TestName',
}
res = self.client.post(CREATE_USER_URL, payload)
self.assertEqual(res.status_code, status.HTTP_201_CREATED)
user = get_user_model().objects.get(email=payload['email'])
self.assertTrue(user.check_password(payload['password']))
self.assertNotIn('password', res.data)
def test_user_with_email_exists_error(self):
"""Test error returned if user with email exists."""
payload = {
'email': 'test@example.com',
'password':'testpass123',
'name': 'Test Name',
}
create_user(**payload)
res = self.client.post(CREATE_USER_URL, payload)
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
def test_password_too_short_error(self):
"""Test an error is returned if password less than 5 chars."""
payload = {
'email': 'test@example.com',
'password':'pw',
'name': 'Test Name',
}
res = self.client.post(CREATE_USER_URL, payload)
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
user_exists = get_user_model().objects.filter(
email=payload['email']
).exists()
self.assertFalse(user_exists)
Creando funcionalidad de User API
-
from django.contrib.auth import get_user_model from rest_framework import serializers class UserSerializer(serializers.ModelSerializer): """Seralizer for the model object.""" class Meta: model = get_user_model() fields = ['email', 'password', 'name'] extra_kwargs = {'password': {'write_only': True, 'min_length': 5}} def create(self, validated_data): """Create and return a user with encrypted password""" return get_user_model().objects.create_user(**validated_data)
-
from rest_framework import generics from user.serializers import UserSerializer class CreateUserView(generics.CreateAPIView): """Create a new user in the system.""" serializer_class = UserSerializer
-
from django.urls import path from user import views app_name='user' urlpatterns = [ path('create/', views.CreateUserView.as_view(), name='create') ]
-
... from django.urls import include, path urlpatterns = [ ... path('api/user/', include('user.urls')), ]
Autenticación
Tipo de autenticación | Descripción |
---|---|
Básica | Envía usuario y password en cada request |
Token | Usa un token en el encabezado HTTP |
JSON Web Token (JWT) | Usa un token de acceso |
Sesión | Usa cookies |
En esta app se utilza Token por:
- Balance entre simplicidad y seguridad
- Soporte por defecto por DRF
- Bién soportada por la mayoria de clientes
%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'basis'}}}%%
flowchart
subgraph " "
CT["<b>Create token</b>
(Post username/password)"]
STOC["<b>Store token on client</b>"]
ITIH["<b>Include token in HTTP headers</b>"]
CT .-> STOC .-> ITIH
end
Pros del uso de Token
- Soporte por defecto
- Simple de usar
- Soportada por todos los clientes
- Evita enviar datos de usuario/password en cada request
Cons del uso de Token
- El token debe ser seguro
- Requiere hacer peticiones a la base de datos
Login out
- Sucede en el lado del cliente
- Borra el token
Test token API
Agregar tests en
app/user/tests/test_user_api.py
...
TOKEN_URL = reverse('user:token')
...
class PublicUserApiTest(TestCase):
"""Test the public features of the user API."""
...
def test_create_token_for_user(self):
"""Test generate token for valid credentials."""
user_details = {
'name': 'Test Name',
'email': 'test@example.com',
'password':'test-user-password123',
}
create_user(**user_details)
payload = {
'email': user_details['email'],
'password': user_details['password'],
}
res = self.client.post(TOKEN_URL, payload)
self.assertIn('token', res.data)
self.assertEqual(res.status_code, status.HTTP_200_OK)
def test_create_token_bad_credentials(self):
"""Test returns error if credentials invalid."""
create_user(email='test@example.com', password='goodpass')
payload = {'email': 'test@example.com' ,'password': 'badpass'}
res = self.client.post(TOKEN_URL, payload)
self.assertNotIn('token', res.data)
self.assertNotIn(res.status_code, status.HTTP_400_BAD_REQUEST)
def test_create_token_blank_password(self):
"""Test posting a blank password returns an error."""
payload = {'email': 'test@example.com' , 'password': ''}
res = self.client.post(TOKEN_URL, payload)
self.assertNotIn('token', res.data)
self.assertNotIn(res.status_code, status.HTTP_400_BAD_REQUEST)
Implementar Token API
-
Añadir app
rest_framework.authtoken
en settings.pyINSTALLED_APPS = [ ... 'rest_framework', 'rest_framework.authtoken', # <--- 'drf_spectacular', 'user', ]
Creación del serlizador para token api
-
... class AuthTokenSerializer(serializers.Serializer): """Serializer for the user auth token.""" email = serializer.EmailField() password = serializer.CharField( style={'input_type': 'password'}, trim_whitespace=False, ) def validate(self, attrs): """Validate and authenticate the user.""" email = attrs.get('email') password = attrs.get('password') user = authenticate( request=self.context.get('request'), username=email, password=password, ) if not user: msg = _('Unable to authenticate with provided credentials.') raise serializers.ValidationError(msg, code='authorization') attrs['user'] = user return attrs
-
vista user/views.py
... class CreateTokenView(ObtainAuthToken): """Create a new auth token for user.""" serializer_class = AuthTokenSerializer renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
-
urls user/urls.py
urlpatterns = [ ... path('token/', views.CreateTokenView.as_view(), name='token'), ]
Test administrar usuario
-
... ME_URL = reverse('user:me') ... class PrivateUserApiTests(TestCase): """Test API requests that require authentication.""" def setUp(self): self.user = create_user( email='test@example.com', password='testpass123', name='Test Name', ) self.client = APIClient() self.client.force_authenticate(user=self.user) def test_retrive_profile_success(self): """Test retrieving profile for logged in user.""" res = self.client.get(ME_URL) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertEqual( res.data, { 'name': self.user.name, 'email': self.user.email, }) def test_post_me_not_allowed(self): """Test POST is not allowed for the 'me' endpoint.""" res = self.client.post(ME_URL, {}) self.assertAlmostEqual( res.status_code, status.HTTP_405_METHOD_NOT_ALLOWED ) def test_update_user_profile(self): """Test updating the user profile for the autenticated user.""" payload = { 'name': 'Updated Name', 'password': 'newpassword123' } res = self.client.patch(ME_URL, payload) self.user.refresh_from_db() self.assertEqual(self.user.name, payload['name']) self.assertTrue(self.user.check_password(payload['password'])) self.assertEqual(res.status_code, status.HTTP_200_OK)
Implementación API actualizar usuario
me
endpoint
-
creación (sobrescritura) del método update serializer.py
... class UserSerializer(serializers.ModelSerializer): ... def create(self, validated_data): """Create and return a user with encrypted password""" return get_user_model().objects.create_user(**validated_data) def update(self, instance, validated_data): """Update and return user.""" password = validated_data.pop('password', None) user = super().update(instance, validated_data) if password: user.set_password(password) user.save() return user ...
-
vistas views.py
from rest_framework import generics, authentication, permissions ... class ManageUserView(generics.RetrieveUpdateAPIView): """Manage the autenticated user.""" serializer_class = UserSerializer authentication_classes = [authentication.TokenAuthentication] permission_classes = [permissions.IsAuthenticated] def get_object(self): """Retrieve and return the authenticated user.""" return self.request.user
-
urls urls.py
urlpatterns = [ ... path('me/', views.ManageUserView.as_view(), name='me'), ]
Pruebas en navegador
Ruta localhost:8000/api/docs