2023-10-04 00:07:55 -03:00
|
|
|
# REST API Django
|
|
|
|
|
|
|
|
<style>div.mermaid{text-align: center;}</style>
|
|
|
|
|
|
|
|
|
|
|
|
## Tecnologias
|
|
|
|
|
|
|
|
- [Python](https://docs.python.org/3/) 3.12.0
|
|
|
|
- [Django](https://docs.djangoproject.com/en/4.2/) 4.2.5
|
|
|
|
- [Django REST Framework](https://www.django-rest-framework.org/) 3.14
|
|
|
|
- [Django REST Swagger](https://django-rest-swagger.readthedocs.io/en/latest/)
|
|
|
|
- [Docker](https://docs.docker.com/) 24.0.6 y
|
|
|
|
*([Docker-compose](https://docs.docker.com/compose/)* incluido con docker cli)
|
|
|
|
- [PostgreSQL](https://www.postgresql.org/about/)
|
|
|
|
- Git
|
|
|
|
- GitHub Actions
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
%%{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 project*
|
|
|
|
- `app/core/` *código compartido entre multiples apps*
|
|
|
|
- `app/user/` *código relativo al usuario*
|
|
|
|
- `app/recipe/` *código relativo a las recetas*
|
|
|
|
|
|
|
|
## TDD
|
|
|
|
|
|
|
|
**T**est **D**riven **D**eveloment
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
%%{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
|
2023-10-07 16:36:13 -03:00
|
|
|
ej. `docker compose run --rm app sh -c "python manage.py collectstatic"`
|
|
|
|
- `docker compose` Ejecuta un comando de Docker Compose
|
2023-10-04 00:07:55 -03:00
|
|
|
- `run` comienza un contenedor específico definido en la configuración
|
|
|
|
- `--rm` remueve el contenedor
|
2023-10-05 22:48:42 -03:00
|
|
|
- `app` es el nombre del servicio/aplicación
|
2023-10-04 00:07:55 -03:00
|
|
|
- `sh -c` pasa una orden a la shell del container
|
|
|
|
- `"python manage.py ..."` comando a correr dentro del contenedor
|
|
|
|
|
|
|
|
<br>
|
|
|
|
|
|
|
|
- [Dockerfile](./Dockerfile)
|
|
|
|
- [.dockerignore](./.dockerignore)
|
|
|
|
|
|
|
|
```sh
|
|
|
|
docker build .
|
|
|
|
```
|
|
|
|
|
|
|
|
- [docker-compose.yml](./docker-compose.yml)
|
|
|
|
|
|
|
|
```sh
|
2023-10-07 16:36:13 -03:00
|
|
|
docker compose build
|
2023-10-04 00:07:55 -03:00
|
|
|
```
|
|
|
|
|
|
|
|
### Linting
|
|
|
|
|
|
|
|
- Instalar `flake8`
|
|
|
|
- [requirements.dev.txt](./requirements.dev.txt)
|
|
|
|
- Configuración [flake8](./app/.flake8)
|
2023-10-07 16:36:13 -03:00
|
|
|
- Correr a travez de docker compose `docker compose run --rm app sh -c "flake8"`
|
2023-10-04 00:07:55 -03:00
|
|
|
|
|
|
|
### Testing
|
|
|
|
|
|
|
|
- Django test suite
|
2023-10-05 22:48:42 -03:00
|
|
|
- Configurar test por cada aplicación Django
|
2023-10-07 16:36:13 -03:00
|
|
|
- Correr a travez de docker compose `docker compose run --rm app sh -c "python
|
2023-10-04 00:07:55 -03:00
|
|
|
manage.py test"`
|
|
|
|
|
|
|
|
### Creación del proyecto Django
|
|
|
|
|
|
|
|
```sh
|
2023-10-07 16:36:13 -03:00
|
|
|
docker compose run -rm app sh -c "django-admin startproject app ."
|
2023-10-04 00:07:55 -03:00
|
|
|
```
|
|
|
|
|
|
|
|
### Iniciar el servidor
|
|
|
|
|
|
|
|
```sh
|
2023-10-07 16:36:13 -03:00
|
|
|
docker compose up
|
2023-10-04 00:07:55 -03:00
|
|
|
```
|
|
|
|
|
|
|
|
### 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?
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
%%{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*
|
|
|
|
|
2023-10-05 22:48:42 -03:00
|
|
|
#### Configuranción GitHub Actions
|
2023-10-04 00:07:55 -03:00
|
|
|
|
|
|
|
- Creación de archivo [`checks.yml`](./.github/workflows/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
|
|
|
|
|
2023-10-05 22:48:42 -03:00
|
|
|
## 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 directorio `tests/`, no ambos
|
|
|
|
- Los moduloes de prueba deben comenzar con `test_`
|
|
|
|
- Los directorios de pruebas deben contener el archivo `__init__.py`
|
|
|
|
|
|
|
|
```txt
|
|
|
|
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
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
%%{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
|
|
|
|
|
|
|
|
```py
|
|
|
|
""""
|
|
|
|
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`](./app/app/calc.py) con [`tests.py`](app/app/tests.py)
|
|
|
|
|
|
|
|
```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"`
|
|
|
|
|
|
|
|
```python
|
|
|
|
Found 1 test(s).
|
|
|
|
System check identified no issues (0 silenced).
|
|
|
|
.
|
|
|
|
----------------------------------------------------------------------
|
|
|
|
Ran 1 test in 0.000s
|
|
|
|
|
|
|
|
OK
|
|
|
|
```
|
|
|
|
|
|
|
|
### Usando TDD
|
|
|
|
|
|
|
|
1. Crear la prueba para el comportamiento esperado [tests.py](./app/app/tests.py)
|
|
|
|
2. La prueba debe fallar
|
|
|
|
3. Crear el código para que el test pase (añadir la funcionalidad)
|
|
|
|
[calc.py](./app/app/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.
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
%%{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
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
%%{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 reales
|
|
|
|
- `patch` 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.
|
|
|
|
|
|
|
|
```py
|
|
|
|
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
|
|
|
|
|
|
|
|
```py
|
|
|
|
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 directorio `tests.py`
|
|
|
|
- Indentación de los casos de prueba
|
|
|
|
- Prefijo `test` faltante en los metodos `test_some_function(self):`
|
|
|
|
|
|
|
|
#### `ImportError` al correr las pruebas
|
|
|
|
|
|
|
|
```py
|
|
|
|
raise ImportError(
|
|
|
|
ImportError: 'tests' module incorrectly imported from ....
|
|
|
|
Expected ....Is this module globally installed?
|
|
|
|
)
|
|
|
|
```
|
|
|
|
|
|
|
|
**Razon posible**
|
|
|
|
|
|
|
|
- Existencia de `tests/` y `tests.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
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
%%{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
|
|
|
|
|
|
|
|
```yml
|
|
|
|
services:
|
|
|
|
app:
|
|
|
|
depends_on:
|
|
|
|
- db
|
|
|
|
|
|
|
|
db:
|
|
|
|
image: postgres:16-alpine
|
|
|
|
```
|
|
|
|
|
|
|
|
- Establecer `depends_on` para iniciar `db` primero
|
|
|
|
- El servicio `app` puede usar el hostname de `db`
|
|
|
|
|
|
|
|
### Volumes
|
|
|
|
|
|
|
|
- Persistencia de datos
|
|
|
|
- Mapea el directorio del contenedeor con la máquina local
|
|
|
|
|
|
|
|
```yml
|
|
|
|
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
|
|
|
|
|
|
|
|
[docker-compose.yml](./docker-compose.yml)
|
|
|
|
|
|
|
|
```yml
|
|
|
|
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`](./app/app/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
|
|
|
|
|
|
|
|
```py
|
|
|
|
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`
|
|
|
|
|
|
|
|
```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"`
|
|
|
|
|
|
|
|
```sh
|
|
|
|
[+] 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"`
|
|
|
|
|
|
|
|
```sh
|
|
|
|
[+] 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
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
%%{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
|
|
|
|
|
|
|
|
```py
|
|
|
|
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`
|
2023-10-07 16:36:13 -03:00
|
|
|
- Se utiliza el CLI de Django `python manage.py makemigrations`
|
2023-10-05 22:48:42 -03:00
|
|
|
- Aplicar migraciones `python manage.py makemigrations`
|
|
|
|
- Correr despues de esperar por la base de datos
|
|
|
|
|
|
|
|
## Actualización de Docker Compose y CI/CD
|
|
|
|
|
|
|
|
```yml
|
|
|
|
...
|
|
|
|
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`
|
|
|
|
|
|
|
|
```sh
|
|
|
|
[+] 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
|
|
|
|
|
|
|
|
[checks.yml](./.github/workflows/checks.yml)
|
|
|
|
|
|
|
|
```yml
|
|
|
|
...
|
|
|
|
- name: Test
|
|
|
|
run: >
|
|
|
|
docker compose run --rm app sh -c "python manage.py wait_for_db &&
|
|
|
|
python manage.py test"
|
|
|
|
...
|
|
|
|
```
|
2023-10-07 16:36:13 -03:00
|
|
|
|
|
|
|
## 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` y `PermissionsMixin`
|
|
|
|
- Creación de administrador personalizado
|
|
|
|
- Se establece `AUTH_USER_MODEL` en `settings.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 usuarios
|
|
|
|
- `create_superuser` usado por el CLI para crear un super usuario (**admin**)
|
|
|
|
|
|
|
|
### Agregando Unitetst para el modelo usuario personalizado
|
|
|
|
|
|
|
|
[test_models.py](./app/core/tests/tests_models.py)
|
|
|
|
|
|
|
|
```py
|
|
|
|
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
|
|
|
|
|
|
|
|
[models.py](./app/core/models.py)
|
|
|
|
|
|
|
|
```py
|
|
|
|
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.
|
|
|
|
|
|
|
|
```py
|
|
|
|
...
|
|
|
|
|
|
|
|
AUTH_USER_MODEL = 'core.User'
|
|
|
|
```
|
|
|
|
|
|
|
|
Crear migraciones
|
|
|
|
|
|
|
|
`docker compose run --rm app sh -c "python manage.py makemigrations"`
|
|
|
|
|
|
|
|
```sh
|
|
|
|
[+] 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](./app/core/migrations/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"`
|
|
|
|
|
|
|
|
```sh
|
|
|
|
[+] 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"`
|
|
|
|
|
|
|
|
```sh
|
|
|
|
[+] 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
|
|
|
|
|
|
|
|
[test_models.py](./app/core/tests/test_models.py)
|
|
|
|
|
|
|
|
```py
|
|
|
|
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](./app/core/models.py)
|
|
|
|
para utilizar el método `normalize_email` que provee la clase **BaseUserManager**
|
|
|
|
|
|
|
|
```diff
|
|
|
|
- user = self.model(email=email, **extra_fields)
|
|
|
|
+ user = self.model(email=self.normalize_email(email), **extra_fields)
|
|
|
|
```
|
|
|
|
|
|
|
|
## Requerir email
|
|
|
|
|
|
|
|
### Test requerir email
|
|
|
|
|
|
|
|
[test_models.py](./app/core/tests/test_models.py)
|
|
|
|
|
|
|
|
```py
|
|
|
|
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](./app/core/models.py)
|
|
|
|
y levantar excepción `ValueError` si usuario no ingresa un email
|
|
|
|
|
|
|
|
```py
|
|
|
|
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
|
|
|
|
|
|
|
|
[test_models.py](./app/core/tests/test_models.py)
|
|
|
|
|
|
|
|
```py
|
|
|
|
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](./app/core/models.py)
|
|
|
|
|
|
|
|
|
|
|
|
```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"`
|
|
|
|
|
|
|
|
```sh
|
|
|
|
[+] 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`](./app/core/admin.py)
|
|
|
|
|
|
|
|
### Personalización del administrador
|
|
|
|
|
|
|
|
Se crea una clase basada en `ModelAdmin` o `UserAdmin` donde se sobrescribe o
|
|
|
|
establecen variables de clase
|
|
|
|
|
|
|
|
ejemplo
|
|
|
|
|
|
|
|
```py
|
|
|
|
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',
|
|
|
|
),
|
|
|
|
})
|
|
|
|
)
|
|
|
|
```
|
|
|
|
|
|
|
|
- `ordening` 
|
|
|
|
- `list_display` 
|
|
|
|
- `fieldsets` 
|
|
|
|
- `readonly_fields` 
|
|
|
|
- `add_fieldsets` 
|
|
|
|
|
|
|
|
### Creando test para el administrador
|
|
|
|
|
|
|
|
[`app/core/tests/test_models.py`](./app/core/tests/test_admin.py)
|
|
|
|
|
|
|
|
```py
|
|
|
|
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](https://docs.djangoproject.com/en/4.2/ref/contrib/admin/#reversing-admin-urls)
|
|
|
|
- Django docs
|
|
|
|
[testing tools](https://docs.djangoproject.com/en/4.2/topics/testing/tools/#overview-and-a-quick-example)
|
|
|
|
|
|
|
|
Correr test `docker compose run --rm app sh -c "python manage.py test"`
|
|
|
|
|
|
|
|
### Activar admin para core app
|
|
|
|
|
|
|
|
En [`admin.py`](./app/core/admin.py)
|
|
|
|
|
|
|
|
```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)
|
|
|
|
```
|
|
|
|
|
|
|
|
- **Admin**
|
|
|
|

|
|
|
|
- **CORE** Section
|
|
|
|

|
|
|
|
- **CORE** Usuarios, requiere modificar pues espera campos que el modelo no tiene
|
|
|
|

|
|
|
|
|
|
|
|
### Modificar admin para que use los campos de usuario personalizado
|
|
|
|
|
|
|
|
```py
|
|
|
|
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)
|
|
|
|
```
|
|
|
|
|
|
|
|
- Administrador de usuario personalizado
|
|
|
|

|
|
|
|
|
|
|
|
- Cambio de lenguaje y timezone, y traducción `gettext_lazy`
|
|
|
|

|
|
|
|
|
|
|
|
- Requiere modificar pues espera campos que el modelo no tiene
|
|
|
|
- 
|
|
|
|
|
|
|
|
### Test página de creación de usuario
|
|
|
|
|
|
|
|
```py
|
|
|
|
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
|
|
|
|
|
|
|
|
[app/core/admin.py](./app/core/admin.py)
|
|
|
|
|
|
|
|
```py
|
|
|
|
class UserAdmin(BaseUserAdmin):
|
|
|
|
...
|
|
|
|
add_fieldsets = (
|
|
|
|
(None, {
|
|
|
|
'classes': ('wide',),
|
|
|
|
'fields': (
|
|
|
|
'email',
|
|
|
|
'password1',
|
|
|
|
'password2',
|
|
|
|
'name',
|
|
|
|
'is_active',
|
|
|
|
'is_staff',
|
|
|
|
'is_superuser',
|
|
|
|
)
|
|
|
|
}),
|
|
|
|
)
|
|
|
|
...
|
|
|
|
```
|
|
|
|
|
|
|
|
- Los test pasan
|
|
|
|
|
|
|
|
- Página para crear usuarios 
|
|
|
|
- Panel de usuarios del administrador 
|