Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Python-generated files
__pycache__/


# Virtual environments
venvchallenge/

# Logs
*.log
logs/

# Database
db.sqlite3

.env
23 changes: 23 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
FROM python:3.11-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

RUN mkdir -p /app/logs /app/db

RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser

EXPOSE 8000

CMD ["gunicorn", "todo_challenge.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3", "--timeout", "60"]
249 changes: 249 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,252 @@
# 📝 Invera ToDo-List Challenge

API REST en **Django + DRF** para gestionar una lista de tareas con autenticación JWT, filtros, logs, tests y despliegue con Docker.

## 📌 Funcionalidades
- Registro y autenticación de usuarios vía **JWT**.
- CRUD de tareas:
- Crear, listar, actualizar y eliminar.
- Marcar como completadas.
- Filtros:
- Por contenido (`?search=...`)
- Por fecha de creación (`?created_after=YYYY-MM-DD`)
- **Logs**:
- Access logs vía middleware (método, path, status, usuario, duración).
- Business logs vía signals (creación, actualización, completado, borrado).
- **Tests** de integración (DRF APIClient).
- Despliegue en **Docker** con Gunicorn y SQLite persistente.

---

## Instalación y ejecución

### Local (sin Docker)
```bash
# Crear entorno y activar
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate

# Instalar dependencias
pip install -r requirements.txt

# Migraciones y superusuario
python manage.py migrate
python manage.py createsuperuser

# Levantar servidor
python manage.py runserver
```

---

### Con Docker
Requiere Docker y Docker Compose.

1. Crear archivo `.env`:
```env
SECRET_KEY=changeme-super-secret
DEBUG=0
ALLOWED_HOSTS=*
ACCESS_TOKEN_DAYS=7
REFRESH_TOKEN_DAYS=30
```

2. Construir y levantar:
```bash
docker compose up --build
```

3. Crear superusuario (opcional):
```bash
docker compose run --rm web python manage.py createsuperuser
```

---

## Endpoints principales

### Autenticación
- `POST /api/register/` → Registro de usuario
- `POST /api/login/` → Obtener `access` y `refresh` tokens
- `POST /api/refresh/` → Renovar token `access`

### Tareas
- `GET /api/tasks/` → Listar tareas propias
- Filtros: `?search=...` y/o `?created_after=YYYY-MM-DD`
- `POST /api/tasks/` → Crear tarea
- `GET /api/tasks/{id}/` → Detalle de tarea
- `PUT /api/tasks/{id}/` → Actualizar tarea
- `DELETE /api/tasks/{id}/` → Eliminar tarea
- `PUT /api/tasks/{id}/complete/` → Marcar como completada

---

## Ejemplos con `curl`

### Registro
```bash
curl -X POST http://localhost:8000/api/register/ -H "Content-Type: application/json" -d '{"username":"lucas","email":"lucas@mail.com","password":"12345678"}'
```

### Login
```bash
curl -X POST http://localhost:8000/api/login/ -H "Content-Type: application/json" -d '{"username":"lucas","password":"12345678"}'
```

### Crear tarea
```bash
curl -X POST http://localhost:8000/api/tasks/ -H "Authorization: Bearer <ACCESS_TOKEN>" -H "Content-Type: application/json" -d '{"title":"Estudiar Django","description":"Challenge Invera"}'
```

### Marcar como completada
```bash
curl -X PUT http://localhost:8000/api/tasks/1/complete/ -H "Authorization: Bearer <ACCESS_TOKEN>"
```

### Filtrar
```bash
# Por texto
curl -X GET "http://localhost:8000/api/tasks/?search=estudiar" -H "Authorization: Bearer <ACCESS_TOKEN>"

# Por fecha
curl -X GET "http://localhost:8000/api/tasks/?created_after=2025-08-01" -H "Authorization: Bearer <ACCESS_TOKEN>"
```

---

## Logs

- **Access logs**: `logs/access.log`
Middleware `AccessLogMiddleware` registra método, path, status, usuario y duración.

- **Business logs**: `logs/app.log`
Signals (`tasks/signals.py`) registran:
- `task created`
- `task updated`
- `task completed`
- `task uncompleted`
- `task deleted`

**Ejemplo access log**
```
ts=2025-08-06T12:34:57-0300 level=INFO logger=http.access method=POST path=/api/tasks/ status=201 user=lucas duration_ms=32 msg=request completed
```

**Ejemplo business log**
```
12:35:10 [INFO] tasks: task completed by lucas
```

---

## Tests

Para correr los tests:
```bash
# Local
python manage.py test -v 2

# Docker
docker compose run --rm web python manage.py test -v 2
```

Los tests (`tasks/test.py`) cubren:
- Registro y login
- Auth obligatoria
- Crear tarea
- Listar (solo propias)
- Filtros por texto y fecha
- Update
- Acción complete
- Delete
- Aislamiento de usuarios

---

## Diagramas

### Secuencia: Request → Middleware → ViewSet/DRF → Signals → Loggers
```mermaid
sequenceDiagram
autonumber
participant C as Client
participant M as AccessLogMiddleware
participant V as DRF ViewSet
participant ORM as Django ORM
participant S as Signals
participant L1 as Logger http.access
participant L2 as Logger tasks

C->>M: HTTP Request (JWT)
M->>V: get_response(request)
V->>ORM: create/update/delete Task
ORM-->>S: emitir signals
S->>L2: business logs
V-->>M: Response
M->>L1: access log
M-->>C: HTTP Response
```

### Componentes
```mermaid
flowchart LR
subgraph Django App
A[settings.py]
B[tasks/apps.py]
C[tasks/middleware.py]
D[tasks/models.py]
E[tasks/signals.py]
F[tasks/views.py]
G[tasks/serializers.py]
end

subgraph Logging
H[(logs/access.log)]
I[(logs/app.log)]
L1[[http.access]]
L2[[tasks]]
end

Client --> C --> F --> D
D --> E --> L2 --> I
C --> L1 --> H
A -.-> C
A -.-> L1
A -.-> L2
B --> E
```

---

## Estructura del proyecto
```
todo_challenge/
├── todo_challenge/ # Configuración del proyecto
├── tasks/ # App de tareas
│ ├── migrations/
│ ├── models.py
│ ├── views.py
│ ├── serializers.py
│ ├── urls.py
│ ├── middleware.py
│ ├── signals.py
│ └── test.py
├── logs/ # Logs persistentes
├── db/ # Base SQLite persistente (Docker)
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── README.md
```

---

## Notas finales
- **JWT** configurable vía `.env` (`ACCESS_TOKEN_DAYS`, `REFRESH_TOKEN_DAYS`).
- **SQLite** para simplicidad, pero fácilmente migrable a PostgreSQL/MySQL.
- **Logs** preparados para ser enviados a Loki/Grafana si se requiere.
- **Tests** listos para CI/CD (puede agregarse GitHub Actions fácilmente).

# Invera ToDo-List Challenge (Python/Django Jr-SSr)

El propósito de esta prueba es conocer tu capacidad para crear una pequeña aplicación funcional en un límite de tiempo. A continuación, encontrarás las funciones, los requisitos y los puntos clave que debés tener en cuenta durante el desarrollo.
Expand Down
19 changes: 19 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: "3.9"

services:
web:
build: .
ports:
- "8000:8000"
env_file: .env
volumes:
- ./logs:/app/logs
- ./db:/app/db
command: >
sh -c "python manage.py migrate &&
gunicorn todo_challenge.wsgi:application --bind 0.0.0.0:8000 --workers 3 --timeout 60"
healthcheck:
test: ["CMD", "python", "manage.py", "check"]
interval: 30s
timeout: 10s
retries: 3
22 changes: 22 additions & 0 deletions manage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'todo_challenge.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)


if __name__ == '__main__':
main()
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Django==4.2.23
djangorestframework==3.15.2
djangorestframework-simplejwt==5.3.1
gunicorn==21.2.0
Empty file added tasks/__init__.py
Empty file.
26 changes: 26 additions & 0 deletions tasks/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from django.contrib import admin
from .models import Task
# Register your models here.


@admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
list_display = ("id", "title", "user", "completed", "created_at")
list_filter = ("completed", "created_at", "user")
search_fields = ("title", "description", "user__username", "user__email")
date_hierarchy = "created_at"
ordering = ("-created_at",)
readonly_fields = ("created_at",)
autocomplete_fields = ("user",)

actions = ["mark_completed", "mark_uncompleted"]

def mark_completed(self, request, queryset):
updated = queryset.update(completed=True)
self.message_user(request, f"{updated} tasks marked as completed.")
mark_completed.short_description = "Mark selected tasks as completed"

def mark_uncompleted(self, request, queryset):
updated = queryset.update(completed=False)
self.message_user(request, f"{updated} tasks marked as uncompleted.")
mark_uncompleted.short_description = "Mark selected tasks as uncompleted"
9 changes: 9 additions & 0 deletions tasks/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.apps import AppConfig


class TasksConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'tasks'

def ready(self):
import tasks.signals
Loading