Compare commits
113 Commits
264c66c7c0
...
pre-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a53cd3918 | ||
|
|
35e1d38859 | ||
|
|
a064895689 | ||
|
|
05e95f8880 | ||
|
|
af38d88876 | ||
|
|
02fa6247f1 | ||
|
|
428b745700 | ||
|
|
f6892b2166 | ||
|
|
d2c91d5196 | ||
|
|
d735d73322 | ||
|
|
197cff011f | ||
|
|
590e18e994 | ||
|
|
c692ce2a61 | ||
|
|
152e9c14ef | ||
|
|
99de5f06b5 | ||
|
|
d7a84a4dfa | ||
|
|
fbc5f0f6c4 | ||
|
|
cac00a4f8c | ||
|
|
a17f00bad2 | ||
|
|
b8c3e03348 | ||
|
|
252e176e9d | ||
|
|
41fc2a0aa0 | ||
|
|
001bf13d26 | ||
|
|
47f23a73e1 | ||
|
|
04c37f669c | ||
|
|
92a00ec75f | ||
|
|
30c540b6f9 | ||
|
|
778753afd6 | ||
| b91f5d09e5 | |||
| aee34d797d | |||
|
|
0fc5392bd2 | ||
| 1e1348bc1a | |||
| f68029edc1 | |||
|
|
384f47df5e | ||
|
|
a8dbb62b09 | ||
|
|
2684e251f7 | ||
|
|
94faedecae | ||
| 3f95e92318 | |||
| aed8661331 | |||
|
|
e908125d31 | ||
|
|
03663aacb4 | ||
|
|
0c18ffc2f9 | ||
| 77b722d739 | |||
| 3c7d47782b | |||
|
|
156b5ad77d | ||
|
|
5d2a6469aa | ||
|
|
2f6564d9a6 | ||
| e2ae400889 | |||
| e597a05f08 | |||
|
|
4425141cb3 | ||
|
|
29db0eb0a2 | ||
|
|
91fc6900eb | ||
| 0a73b91e12 | |||
| d444021a00 | |||
|
|
9fba8938ee | ||
| 508f3f028d | |||
| 7a151a4768 | |||
|
|
299428741b | ||
| bc82249a29 | |||
| 56e7d77d63 | |||
|
|
27ccce862d | ||
| f3514d399e | |||
| 1e72aa3e44 | |||
|
|
84baf8fbda | ||
| b08e74f459 | |||
|
|
5d69f89028 | ||
| 0ac0a859b9 | |||
| 02aca6a2a6 | |||
|
|
4a4d0940b3 | ||
| 7a9a1686fa | |||
| b4ab653c95 | |||
|
|
c32092d216 | ||
| 049764b79c | |||
|
|
7ba79c9739 | ||
| c60c140a97 | |||
|
|
bf5a38d425 | ||
| f610eb24d4 | |||
|
|
039349b5b1 | ||
| 816cc276f8 | |||
|
|
9dd97b34f2 | ||
| 5a7209badb | |||
| d39e078fb6 | |||
|
|
3da81a9495 | ||
| ac1c024bee | |||
|
|
e5908b1880 | ||
| 147a1d49cc | |||
|
|
1bf3337616 | ||
|
|
6fb3afa472 | ||
| 8579af3f21 | |||
|
|
102d7c6bfa | ||
| 32e3184b59 | |||
| 5e04933708 | |||
|
|
ac6e336772 | ||
| 083375c5f0 | |||
| d9bba25437 | |||
|
|
e486a0f556 | ||
| 6f84db00cd | |||
| b37b581a78 | |||
|
|
0971134702 | ||
|
|
02bc67c5fd | ||
| c54e1e9b9e | |||
|
|
02feaf445d | ||
| 15f2a21a2c | |||
|
|
c8aa23b564 | ||
| b60ecccad4 | |||
| c4e8675fe8 | |||
| 04fb83447f | |||
| bdda074fa1 | |||
| 64afc3aedb | |||
| 156e1aa27c | |||
| 16b7f956b3 | |||
| 34faf2157e | |||
| 2a6723aedf |
30
.claude/settings.local.json
Normal file
30
.claude/settings.local.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(gh auth:*)",
|
||||
"Bash(git config:*)",
|
||||
"Read(//c/Users/juanm/**)",
|
||||
"Bash(cmdkey /list)",
|
||||
"Bash(curl -s -X POST https://git.v-encore-lab.com/api/v1/repos/Proyecto-SaaS/django-core-base/pulls -H 'Authorization: token 3b78f0a988a74fcc251d4b5476dd54c7d98c26d2' -H 'Content-Type: application/json' -d '{\"title\":\"Merge pre-dev into dev\",\"head\":\"pre-dev\",\"base\":\"dev\",\"body\":\"Merge de pre-dev a dev\"}')",
|
||||
"Bash(curl -s -X POST https://git.v-encore-lab.com/api/v1/repos/Proyecto-SaaS/django-core-base/pulls -H 'Authorization: token 3b78f0a988a74fcc251d4b5476dd54c7d98c26d2' -H 'Content-Type: application/json' -d '{\"title\":\"Merge dev into master\",\"head\":\"dev\",\"base\":\"master\",\"body\":\"Merge de dev a master\"}')",
|
||||
"Bash(curl -s -X POST https://git.v-encore-lab.com/api/v1/repos/Proyecto-SaaS/django-core-base/pulls/28/merge -H 'Authorization: token 3b78f0a988a74fcc251d4b5476dd54c7d98c26d2' -H 'Content-Type: application/json' -d '{\"Do\":\"merge\",\"merge_message_field\":\"Merge pre-dev into dev\"}')",
|
||||
"Bash(curl -s https://git.v-encore-lab.com/api/v1/repos/Proyecto-SaaS/django-core-base/pulls/28 -H 'Authorization: token 3b78f0a988a74fcc251d4b5476dd54c7d98c26d2')",
|
||||
"Bash(python -c \"import sys,json; p=json.load\\(sys.stdin\\); print\\('merged:', p['merged'], '| state:', p['state']\\)\")",
|
||||
"Bash(curl -s -X POST https://git.v-encore-lab.com/api/v1/repos/Proyecto-SaaS/django-core-base/pulls/29/merge -H 'Authorization: token 3b78f0a988a74fcc251d4b5476dd54c7d98c26d2' -H 'Content-Type: application/json' -d '{\"Do\":\"merge\",\"merge_message_field\":\"Merge dev into master\"}')",
|
||||
"Bash(curl -s https://git.v-encore-lab.com/api/v1/repos/Proyecto-SaaS/django-core-base/pulls/29 -H 'Authorization: token 3b78f0a988a74fcc251d4b5476dd54c7d98c26d2')",
|
||||
"Bash(mkdir -p app)",
|
||||
"Bash(mv core:*)",
|
||||
"Bash(mv apps/backend_admin app/backend_admin)",
|
||||
"Bash(mv apps/common app/common)",
|
||||
"Bash(mv apps/promociones app/promociones)",
|
||||
"WebSearch",
|
||||
"Bash(npm install:*)",
|
||||
"Bash(engram init:*)",
|
||||
"Bash(dir:*)",
|
||||
"Bash(engram --help:*)",
|
||||
"Bash(engram remember:*)",
|
||||
"Bash(engram stats:*)",
|
||||
"Bash(engram recall:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
10
.env
10
.env
@@ -1,10 +0,0 @@
|
||||
# Seguridad
|
||||
DEBUG=True
|
||||
SECRET_KEY=una-clave-muy-secreta-y-larga-123456
|
||||
|
||||
# Base de Datos (Conectando al PostgreSQL que instalamos)
|
||||
DB_NAME=gitea
|
||||
DB_USER=gitea
|
||||
DB_PASSWORD=gitea
|
||||
DB_HOST=gitea-db
|
||||
DB_PORT=5432
|
||||
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -1,2 +1,5 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
|
||||
# Shell scripts: forzar LF siempre (evita CRLF en Windows que rompe Docker)
|
||||
*.sh text eol=lf
|
||||
|
||||
21
.gitignore
vendored
21
.gitignore
vendored
@@ -1 +1,22 @@
|
||||
# Configuración personal y secretos
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# Docker local (si decides no subirlo)
|
||||
deployments/docker-compose.override.yml
|
||||
deployments/docker-compose.yml
|
||||
|
||||
# Archivos de datos de la DB local
|
||||
postgres_data/
|
||||
local_postgres_data/
|
||||
|
||||
# Carpeta de datos (BD SQLite y similares), pero se mantiene la carpeta
|
||||
data/*
|
||||
!data/.gitkeep
|
||||
app/data/*
|
||||
!app/data/.gitkeep
|
||||
*.pyc
|
||||
# Bloquear todos los .env en cualquier carpeta
|
||||
.env
|
||||
**/core/.env
|
||||
**/deployments/.env
|
||||
110
CLAUDE.md
Normal file
110
CLAUDE.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Convenciones del Ecosistema V-Encore Lab
|
||||
|
||||
## Ecosistema de microservicios
|
||||
|
||||
| Repo | Rol | Puerto |
|
||||
|------|-----|--------|
|
||||
| `django-core-base` | Hub orquestador principal | 8000 |
|
||||
| `api_backoffice` | Consulta y gestión de BD | 8001 |
|
||||
| `api_comunicaciones` | Emails, notificaciones, webhooks | 8002 |
|
||||
| `api_documentacion` | Generación y gestión de documentos | 8003 |
|
||||
| `web_interno` | Panel de gestión (React + Ant Design) | 3000 |
|
||||
|
||||
## Stack Django
|
||||
|
||||
- Django 5.0 + DRF + SimpleJWT
|
||||
- Apps bajo `app/`. Prefijo `/api/` en todas las URLs salvo `admin/`.
|
||||
- Patrón 3 capas: **URL → View → Action**
|
||||
- SQL con `connection.cursor()` y placeholders (`%s`). Nunca concatenar strings.
|
||||
|
||||
## Patrón de vistas — 4 bloques obligatorios
|
||||
|
||||
```python
|
||||
from general.utilidades.acciones import LogService
|
||||
|
||||
class MiVista(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
path = '/mi-app/mi-endpoint/'
|
||||
|
||||
# Bloque 1 — Inicio Log
|
||||
log_id = LogService.gestionar_log(self, request, path=path)
|
||||
|
||||
try:
|
||||
# Bloque 2 — Data Cleaning
|
||||
data = request.data
|
||||
LogService.gestionar_log(self, request, log_id=log_id,
|
||||
path=path, body_request=data, status_code=100)
|
||||
params = {'campo': data.get('campo')}
|
||||
except Exception as error:
|
||||
response = {'error': str(error)}
|
||||
LogService.gestionar_log(self, request, log_id=log_id,
|
||||
path=path, body_response=response, status_code=400)
|
||||
return JsonResponse(response, status=400)
|
||||
|
||||
try:
|
||||
# Bloque 3 — Action Call
|
||||
resultado = mi_accion(params)
|
||||
# Bloque 4 — Cierre Log
|
||||
LogService.gestionar_log(self, request, log_id=log_id,
|
||||
path=path, body_response=resultado, status_code=200)
|
||||
return JsonResponse(resultado, status=200)
|
||||
except Exception as error:
|
||||
response = {'error': str(error)}
|
||||
LogService.gestionar_log(self, request, log_id=log_id,
|
||||
path=path, body_response=response, status_code=500)
|
||||
return JsonResponse(response, status=500)
|
||||
```
|
||||
|
||||
## LogService — reglas
|
||||
|
||||
- Siempre llamar como `LogService.gestionar_log(self, request, ...)` — pasar `self` de la vista.
|
||||
- Primera llamada (sin `log_id`): crea el registro, devuelve el `log_id`.
|
||||
- Llamadas siguientes: actualizar con `log_id=log_id`.
|
||||
- `body_request` y `body_response` se serializan con `DjangoJSONEncoder` — soporta `datetime.date`, `Decimal`, etc.
|
||||
- `status_code` como entero. Usar `is not None` para comprobar (0 es válido).
|
||||
|
||||
## Base de datos
|
||||
|
||||
- `DB_HOST` definido → PostgreSQL
|
||||
- `DB_HOST` no definido → SQLite en `app/data/db.sqlite3`
|
||||
- Migraciones siempre desde `app/`: `cd app && python manage.py migrate`
|
||||
- SQL INSERT con psycopg2: usar `INSERT ... RETURNING id` + `cursor.fetchone()`. **Nunca `cursor.lastrowid`**.
|
||||
- Booleans en psycopg2: pasar `True`/`False`, no `1`/`0`.
|
||||
|
||||
## Flujo de ramas
|
||||
|
||||
```
|
||||
pre-dev → dev → master
|
||||
```
|
||||
|
||||
- `pre-dev`: desarrollo activo — aquí entran todos los cambios
|
||||
- `dev`: validación previa a producción
|
||||
- `master`: producción estable
|
||||
- Merges siempre con `--no-ff`
|
||||
- **NUNCA** propagar en sentido inverso (master → dev o dev → pre-dev)
|
||||
- Secuencia correcta:
|
||||
```bash
|
||||
git checkout pre-dev && git merge <mi-cambio> --no-ff
|
||||
git checkout dev && git merge pre-dev --no-ff && git push origin dev
|
||||
git checkout master && git merge dev --no-ff && git push origin master
|
||||
```
|
||||
|
||||
## Estructura de app Django
|
||||
|
||||
```
|
||||
app/
|
||||
├── api_config/ # settings.py, urls.py, wsgi.py
|
||||
├── general/ # LogService, utils — NO tiene modelos propios
|
||||
├── backend_admin/ # Modelo Log (audit_logs), admin panel
|
||||
├── common/ # Modelos y utilidades compartidas entre apps
|
||||
├── <feature_app>/ # Una app por dominio de negocio
|
||||
├── data/ # SQLite (gitignored, solo .gitkeep versionado)
|
||||
└── manage.py
|
||||
```
|
||||
|
||||
## Fixtures
|
||||
|
||||
- `loaddata` ignora `auto_now_add=True` — incluir fechas explícitas en el JSON o falla con NOT NULL en PostgreSQL.
|
||||
322
README.md
322
README.md
@@ -1,6 +1,324 @@
|
||||
# django-core-base
|
||||
# django-core-base · API Hub Orquestador
|
||||
|
||||
// V-Encore Lab: Sistema Automatizado v1.0.4
|
||||
> V-Encore Lab — Microservicio principal. Actúa como orquestador y punto de entrada central del ecosistema SaaS.
|
||||
> Puerto por defecto: **8000**
|
||||
|
||||
---
|
||||
|
||||
## Tabla de contenidos
|
||||
|
||||
1. [Requisitos previos](#requisitos-previos)
|
||||
2. [Inicio rápido — Desarrollo local](#inicio-rápido--desarrollo-local)
|
||||
3. [Variables de entorno](#variables-de-entorno)
|
||||
4. [Base de datos](#base-de-datos)
|
||||
5. [Docker — Producción](#docker--producción)
|
||||
6. [Estructura del proyecto](#estructura-del-proyecto)
|
||||
7. [Endpoints principales](#endpoints-principales)
|
||||
8. [Patrón LogService](#patrón-logservice)
|
||||
9. [Flujo de ramas](#flujo-de-ramas)
|
||||
10. [Mantenimiento](#mantenimiento)
|
||||
|
||||
---
|
||||
|
||||
## Requisitos previos
|
||||
|
||||
| Herramienta | Versión mínima |
|
||||
|-------------|---------------|
|
||||
| Python | 3.11 |
|
||||
| pip | 23+ |
|
||||
| Docker | 24+ |
|
||||
| Docker Compose | v2 |
|
||||
| Git | 2.40+ |
|
||||
|
||||
---
|
||||
|
||||
## Inicio rápido — Desarrollo local
|
||||
|
||||
```bash
|
||||
# 1. Clonar
|
||||
git clone https://git.v-encore-lab.com/Proyecto-SaaS/django-core-base.git
|
||||
cd django-core-base
|
||||
|
||||
# 2. Entorno virtual
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # Linux/Mac
|
||||
.venv\Scripts\activate # Windows
|
||||
|
||||
# 3. Dependencias
|
||||
pip install -r deployments/requirements.txt
|
||||
|
||||
# 4. Variables de entorno
|
||||
cp .env.example .env
|
||||
# Editar .env según el entorno (ver sección Variables de entorno)
|
||||
|
||||
# 5. Migraciones (SQLite en local)
|
||||
cd app
|
||||
python manage.py migrate
|
||||
|
||||
# 6. Superusuario (primera vez)
|
||||
python manage.py createsuperuser
|
||||
|
||||
# 7. Servidor de desarrollo
|
||||
python manage.py runserver 0.0.0.0:8000
|
||||
```
|
||||
|
||||
Accesos:
|
||||
- API: http://localhost:8000/api/
|
||||
- Admin: http://localhost:8000/admin/
|
||||
|
||||
---
|
||||
|
||||
## Variables de entorno
|
||||
|
||||
Copia `.env.example` a `.env` y ajusta los valores:
|
||||
|
||||
| Variable | Descripción | Ejemplo / Default |
|
||||
|----------------|--------------------------------------------------|----------------------------|
|
||||
| `SECRET_KEY` | Clave secreta Django | `django-insecure-...` |
|
||||
| `DEBUG` | Modo debug | `True` (dev) / `False` (prod) |
|
||||
| `ALLOWED_HOSTS`| Hosts permitidos (separados por coma) | `localhost,127.0.0.1` |
|
||||
| `DB_HOST` | Host PostgreSQL. **Si no se define → SQLite** | `localhost` / vacío |
|
||||
| `DB_PORT` | Puerto PostgreSQL | `5432` |
|
||||
| `DB_NAME` | Nombre de la base de datos | `vencorelab` |
|
||||
| `DB_USER` | Usuario PostgreSQL | `postgres` |
|
||||
| `DB_PASSWORD` | Contraseña PostgreSQL | `postgres` |
|
||||
|
||||
---
|
||||
|
||||
## Base de datos
|
||||
|
||||
### Desarrollo local — SQLite
|
||||
|
||||
Si `DB_HOST` **no** está definido en `.env`, Django usa automáticamente SQLite:
|
||||
|
||||
```
|
||||
app/data/db.sqlite3
|
||||
```
|
||||
|
||||
```bash
|
||||
cd app
|
||||
python manage.py migrate # crea app/data/db.sqlite3
|
||||
python manage.py createsuperuser
|
||||
```
|
||||
|
||||
La carpeta `app/data/` está en `.gitignore` (solo se versiona `.gitkeep`).
|
||||
|
||||
### Producción — PostgreSQL
|
||||
|
||||
Define `DB_HOST` en `.env` para activar el driver PostgreSQL:
|
||||
|
||||
```env
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=vencorelab
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=supersecret
|
||||
```
|
||||
|
||||
```bash
|
||||
cd app
|
||||
python manage.py migrate
|
||||
```
|
||||
|
||||
### Comandos útiles de migración
|
||||
|
||||
```bash
|
||||
cd app
|
||||
|
||||
# Ver migraciones pendientes
|
||||
python manage.py showmigrations
|
||||
|
||||
# Crear migraciones de una app
|
||||
python manage.py makemigrations <app_name>
|
||||
|
||||
# Aplicar todas las migraciones
|
||||
python manage.py migrate
|
||||
|
||||
# Revertir migraciones de una app al estado inicial
|
||||
python manage.py migrate <app_name> zero
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker — Local y Producción
|
||||
|
||||
```bash
|
||||
cd deployments
|
||||
|
||||
# Construir e iniciar
|
||||
docker-compose up --build -d
|
||||
|
||||
# Ver logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Parar
|
||||
docker-compose down
|
||||
|
||||
# Parar y eliminar volúmenes (¡cuidado en producción!)
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
El contenedor expone el puerto **8000**.
|
||||
|
||||
### Crear superusuario (primera vez con Docker)
|
||||
|
||||
Una vez los contenedores estén corriendo:
|
||||
|
||||
```bash
|
||||
docker-compose exec web sh -c "cd /app/app && python manage.py createsuperuser"
|
||||
```
|
||||
|
||||
Introduce usuario, email y contraseña cuando lo pida. Estas credenciales son las que usarás para entrar en el panel web (`web_interno`) y en `/admin/`.
|
||||
|
||||
Para producción con PostgreSQL, asegúrate de que `.env` tenga `DB_HOST` apuntando al host correcto (puede ser el nombre del servicio en la red Docker).
|
||||
|
||||
---
|
||||
|
||||
## Estructura del proyecto
|
||||
|
||||
```
|
||||
django-core-base/
|
||||
├── app/
|
||||
│ ├── api_config/ # Configuración Django (settings, urls, wsgi)
|
||||
│ ├── general/ # App transversal
|
||||
│ │ └── utilidades/
|
||||
│ │ ├── acciones.py # LogService — auditoría centralizada
|
||||
│ │ └── utils.py # Utilidades HTTP (get_client_ip, etc.)
|
||||
│ ├── backend_admin/ # App admin: modelo Log, endpoints de gestión
|
||||
│ ├── common/ # Modelos y utilidades compartidas
|
||||
│ ├── promociones/ # App de ejemplo
|
||||
│ ├── automatizados/ # Endpoints para Jenkins/automatizaciones
|
||||
│ ├── data/ # Directorio de la BD SQLite (gitignored)
|
||||
│ │ └── .gitkeep
|
||||
│ └── manage.py
|
||||
├── deployments/
|
||||
│ ├── requirements.txt
|
||||
│ └── Dockerfile
|
||||
├── docker-compose.yml
|
||||
├── .env.example
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Endpoints principales
|
||||
|
||||
| Método | Ruta | Descripción | Auth |
|
||||
|--------|-------------------------------|-----------------------------------|--------------|
|
||||
| POST | `/api/token/` | Obtener JWT (login) | No |
|
||||
| POST | `/api/token/refresh/` | Renovar access token | No |
|
||||
| GET | `/admin/` | Panel de administración Django | Session |
|
||||
| POST | `/api/promociones/obtener/` | Consultar promociones | JWT Bearer |
|
||||
| GET | `/api/general/health/` | Health check | No |
|
||||
|
||||
Autenticación: `Authorization: Bearer <access_token>`
|
||||
|
||||
---
|
||||
|
||||
## Patrón LogService
|
||||
|
||||
Todas las vistas deben usar `LogService` para auditoría:
|
||||
|
||||
```python
|
||||
from general.utilidades.acciones import LogService
|
||||
|
||||
class MiVista(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
path = '/mi-app/mi-endpoint/'
|
||||
|
||||
# Bloque 1 — Inicio log
|
||||
log_id = LogService.gestionar_log(self, request, path=path)
|
||||
|
||||
try:
|
||||
# Bloque 2 — Limpieza de datos
|
||||
data = request.data
|
||||
LogService.gestionar_log(self, request, log_id=log_id,
|
||||
path=path, body_request=data, status_code=100)
|
||||
params = {'campo': data.get('campo')}
|
||||
except Exception as error:
|
||||
response = {'error': str(error)}
|
||||
LogService.gestionar_log(self, request, log_id=log_id,
|
||||
path=path, body_response=response, status_code=400)
|
||||
return JsonResponse(response, status=400)
|
||||
|
||||
try:
|
||||
# Bloque 3 — Acción
|
||||
resultado = mi_accion(params)
|
||||
LogService.gestionar_log(self, request, log_id=log_id,
|
||||
path=path, body_response=resultado, status_code=200)
|
||||
return JsonResponse(resultado, status=200)
|
||||
except Exception as error:
|
||||
response = {'error': str(error)}
|
||||
LogService.gestionar_log(self, request, log_id=log_id,
|
||||
path=path, body_response=response, status_code=500)
|
||||
return JsonResponse(response, status=500)
|
||||
```
|
||||
|
||||
`LogService` registra automáticamente en la tabla `audit_logs` el usuario, IP, path, request/response y status code.
|
||||
|
||||
---
|
||||
|
||||
## Flujo de ramas
|
||||
|
||||
```
|
||||
pre-dev → dev → master
|
||||
```
|
||||
|
||||
- `pre-dev`: desarrollo activo, integración de features
|
||||
- `dev`: validación previa a producción
|
||||
- `master`: rama de producción estable
|
||||
|
||||
```bash
|
||||
# Crear feature
|
||||
git checkout pre-dev
|
||||
git checkout -b feature/mi-feature
|
||||
|
||||
# Mergear a pre-dev
|
||||
git checkout pre-dev
|
||||
git merge feature/mi-feature --no-ff
|
||||
|
||||
# Promover a dev
|
||||
git checkout dev
|
||||
git merge pre-dev --no-ff
|
||||
|
||||
# Promover a master
|
||||
git checkout master
|
||||
git merge dev --no-ff
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mantenimiento
|
||||
|
||||
```bash
|
||||
# Limpiar sesiones expiradas
|
||||
cd app && python manage.py clearsessions
|
||||
|
||||
# Ver estado de la BD
|
||||
cd app && python manage.py dbshell
|
||||
|
||||
# Backup SQLite (desarrollo)
|
||||
cp app/data/db.sqlite3 app/data/db.sqlite3.bak
|
||||
|
||||
# Backup PostgreSQL (producción)
|
||||
pg_dump -U postgres vencorelab > backup_$(date +%Y%m%d).sql
|
||||
|
||||
# Actualizar dependencias
|
||||
pip list --outdated
|
||||
pip install -U <paquete>
|
||||
pip freeze > deployments/requirements.txt
|
||||
|
||||
# Reiniciar contenedor Docker
|
||||
docker-compose restart web
|
||||
|
||||
# Ver logs del contenedor
|
||||
docker-compose logs -f web --tail=100
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **V-Encore Lab** — Sistema Automatizado v1.0
|
||||
> Repositorio: `Proyecto-SaaS/django-core-base`
|
||||
|
||||
3
app/api_config/.env.example
Normal file
3
app/api_config/.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
# core/.env
|
||||
APP_CUSTOM_SETTING="Este es un valor privado de la app"
|
||||
EXTERNAL_SERVICE_API_KEY="sk_test_12345"
|
||||
@@ -1,4 +1,4 @@
|
||||
import os
|
||||
from django.core.asgi import get_asgi_application
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api_config.settings')
|
||||
application = get_asgi_application()
|
||||
190
app/api_config/settings.py
Normal file
190
app/api_config/settings.py
Normal file
@@ -0,0 +1,190 @@
|
||||
from pathlib import Path
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from datetime import timedelta
|
||||
|
||||
SIMPLE_JWT = {
|
||||
# Cambiamos el tiempo de acceso a 3 horas
|
||||
'ACCESS_TOKEN_LIFETIME': timedelta(hours=3),
|
||||
|
||||
# El tiempo del refresh token suele ser mayor (por ejemplo, 1 día)
|
||||
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
|
||||
|
||||
# Otras configuraciones que ya tengas...
|
||||
'ALGORITHM': 'HS256',
|
||||
'AUTH_HEADER_TYPES': ('Bearer',),
|
||||
}
|
||||
|
||||
# 1. RUTA DEL SETTINGS Y CARGA DEL .ENV
|
||||
# Obtenemos la ruta de la carpeta donde está este archivo (core/)
|
||||
CURRENT_DIR = Path(__file__).resolve().parent
|
||||
BASE_DIR = CURRENT_DIR.parent
|
||||
|
||||
# Cargamos el .env específico de esta carpeta (core/.env)
|
||||
load_dotenv(dotenv_path=CURRENT_DIR / '.env')
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-default-key-change-it')
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
# Mejoramos el parseo de DEBUG para que no falle si viene como string
|
||||
DEBUG = os.getenv('DEBUG', 'True').lower() in ('true', '1', 't')
|
||||
|
||||
# 2. ALLOWED HOSTS
|
||||
# Limpiamos y centralizamos los hosts permitidos
|
||||
ALLOWED_HOSTS = [
|
||||
'v-encore-lab.com',
|
||||
'dev.v-encore-lab.com',
|
||||
'185.187.169.109',
|
||||
'localhost',
|
||||
'127.0.0.1',
|
||||
os.getenv('APP_CONTAINER_NAME', 'django_app_dev'), # Dinámico para Docker
|
||||
]
|
||||
|
||||
# Application definition
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
# Plugins
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'corsheaders',
|
||||
|
||||
# Tus Apps (Asegúrate de que el path sea correcto)
|
||||
'general',
|
||||
'promociones',
|
||||
'automatizados',
|
||||
'backend_admin',
|
||||
'common',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'api_config.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'api_config.wsgi.application'
|
||||
|
||||
# 3. DATABASE
|
||||
# En producción (cuando DB_HOST está definido) usa PostgreSQL.
|
||||
# En local/desarrollo sin configuración, cae a SQLite en data/
|
||||
if os.getenv('DB_HOST'):
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': os.getenv('DB_NAME', 'postgres'),
|
||||
'USER': os.getenv('DB_USER', 'postgres'),
|
||||
'PASSWORD': os.getenv('DB_PASSWORD', ''),
|
||||
'HOST': os.getenv('DB_HOST'),
|
||||
'PORT': os.getenv('DB_PORT', '5432'),
|
||||
}
|
||||
}
|
||||
else:
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'data' / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
# Internationalization
|
||||
LANGUAGE_CODE = 'es-es'
|
||||
TIME_ZONE = 'Europe/Madrid'
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Static files
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# REST Framework
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||
),
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
),
|
||||
}
|
||||
|
||||
SIMPLE_JWT = {
|
||||
"ACCESS_TOKEN_LIFETIME": timedelta(hours=1),
|
||||
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
|
||||
}
|
||||
|
||||
# CORS & CSRF
|
||||
CORS_ALLOW_ALL_ORIGINS = DEBUG # Solo permitir todo en modo DEBUG
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
'https://v-encore-lab.com',
|
||||
'http://localhost:8000',
|
||||
'http://127.0.0.1:8000',
|
||||
]
|
||||
|
||||
# Logging simplificado
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'formatters': {
|
||||
'verbose': {
|
||||
'format': '{levelname} {asctime} {module} {message}',
|
||||
'style': '{',
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'verbose',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'': {
|
||||
'handlers': ['console'],
|
||||
'level': os.getenv('LOG_LEVEL', 'INFO'),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# --- URLs de APIs internas (via red Docker saas_network) ---
|
||||
API_BACKOFFICE = os.getenv('API_BACKOFFICE', 'http://api_backoffice:8001')
|
||||
API_COMUNICACIONES = os.getenv('API_COMUNICACIONES', 'http://api_comunicaciones:8002')
|
||||
API_DOCUMENTACION = os.getenv('API_DOCUMENTACION', 'http://api_documentacion:8003')
|
||||
|
||||
# --- CONFIGURACIONES PERSONALIZADAS DE LA APP ---
|
||||
|
||||
# Leemos la variable del .env (cargado previamente con load_dotenv)
|
||||
# Ponemos un valor por defecto por si se nos olvida ponerlo en el .env
|
||||
APP_CUSTOM_SETTING = os.getenv('APP_CUSTOM_SETTING', 'valor_por_defecto_seguro')
|
||||
|
||||
# Ejemplo de otra variable de API
|
||||
EXTERNAL_SERVICE_API_KEY = os.getenv('EXTERNAL_SERVICE_API_KEY', None)
|
||||
10
app/api_config/urls.py
Normal file
10
app/api_config/urls.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.urls import path, include
|
||||
from backend_admin import views as admin_views
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', include('backend_admin.urls')),
|
||||
path('api/general/', include('general.urls')),
|
||||
path('api/promociones/', include('promociones.urls')),
|
||||
path('api/automatizados/', include('automatizados.urls')),
|
||||
path('api/token/', admin_views.api_token, name='token_obtain_pair'),
|
||||
]
|
||||
@@ -2,6 +2,6 @@ import os
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
# Este es el enlace con tus configuraciones
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api_config.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
220
app/automatizados/acciones.py
Normal file
220
app/automatizados/acciones.py
Normal file
@@ -0,0 +1,220 @@
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from datetime import datetime
|
||||
|
||||
from django.db import connection
|
||||
from django.utils import timezone
|
||||
from common.utils import clean_sql_string, clean_sql_int
|
||||
|
||||
|
||||
# =========================================================
|
||||
# Helper HTTP (stdlib) — evita dependencia extra en requirements
|
||||
# =========================================================
|
||||
def _http_get_json(url, timeout=8):
|
||||
"""GET a un servicio externo que responde JSON. Usa stdlib para no añadir deps."""
|
||||
req = urllib.request.Request(url, headers={'User-Agent': 'django-core-base/1.0'})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as response:
|
||||
raw = response.read().decode('utf-8')
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
return {'raw': raw}
|
||||
|
||||
|
||||
def _http_get_text(url, timeout=8):
|
||||
"""GET a un servicio externo que responde texto plano."""
|
||||
req = urllib.request.Request(url, headers={'User-Agent': 'django-core-base/1.0'})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as response:
|
||||
return response.read().decode('utf-8').strip()
|
||||
|
||||
|
||||
# =========================================================
|
||||
# ACCIÓN 1 — Base de datos (INSERT histórico de ejecución)
|
||||
# set_parameterized
|
||||
# =========================================================
|
||||
def setEjecucion(params):
|
||||
"""
|
||||
Inserta una fila en automatizacion_ejecuciones registrando una ejecución.
|
||||
params esperados: nombre, descripcion, estado, origen, resultado, error
|
||||
"""
|
||||
query = """
|
||||
INSERT INTO automatizacion_ejecuciones
|
||||
(nombre, descripcion, estado, origen, resultado, error, fecha_inicio, fecha_fin, activo)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
"""
|
||||
|
||||
ahora = timezone.now()
|
||||
resultado_json = json.dumps(params.get('resultado')) if params.get('resultado') is not None else None
|
||||
|
||||
parameter_dict = [
|
||||
clean_sql_string(params.get('nombre')),
|
||||
clean_sql_string(params.get('descripcion')),
|
||||
clean_sql_string(params.get('estado') or 'ok'),
|
||||
clean_sql_string(params.get('origen') or 'manual'),
|
||||
resultado_json,
|
||||
clean_sql_string(params.get('error')) if params.get('error') else None,
|
||||
ahora,
|
||||
ahora,
|
||||
bool(params.get('activo', True)),
|
||||
]
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(query, parameter_dict)
|
||||
row = cursor.fetchone()
|
||||
nuevo_id = row[0] if row else None
|
||||
|
||||
return {'id': nuevo_id, 'fecha': ahora.isoformat()}
|
||||
|
||||
|
||||
# =========================================================
|
||||
# ACCIÓN 2 — Servicio externo: Cat Fact API
|
||||
# https://catfact.ninja/fact (gratis, sin auth)
|
||||
# =========================================================
|
||||
def getCatFact():
|
||||
url = 'https://catfact.ninja/fact'
|
||||
data = _http_get_json(url)
|
||||
return {
|
||||
'servicio': 'catfact.ninja',
|
||||
'url': url,
|
||||
'fact': data.get('fact'),
|
||||
'length': data.get('length'),
|
||||
}
|
||||
|
||||
|
||||
# =========================================================
|
||||
# ACCIÓN 3 — Servicio externo: GitHub Zen
|
||||
# https://api.github.com/zen (gratis, sin auth)
|
||||
# =========================================================
|
||||
def getGithubZen():
|
||||
url = 'https://api.github.com/zen'
|
||||
texto = _http_get_text(url)
|
||||
return {
|
||||
'servicio': 'api.github.com/zen',
|
||||
'url': url,
|
||||
'zen': texto,
|
||||
}
|
||||
|
||||
|
||||
# =========================================================
|
||||
# Orquestador — ejecuta las 3 acciones en secuencia
|
||||
# =========================================================
|
||||
def ejecutarAutomatizaciones(params):
|
||||
"""
|
||||
Ejecuta las 3 acciones automatizadas:
|
||||
1) Llamada a servicio externo: catfact.ninja
|
||||
2) Llamada a servicio externo: api.github.com/zen
|
||||
3) Persistencia en BD: inserta la ejecución en automatizacion_ejecuciones
|
||||
"""
|
||||
resultados = {'acciones': []}
|
||||
errores = []
|
||||
|
||||
# Acción 2 — catfact
|
||||
try:
|
||||
resultados['acciones'].append({'step': 1, 'ok': True, 'data': getCatFact()})
|
||||
except Exception as err:
|
||||
errores.append(f'catfact: {err}')
|
||||
resultados['acciones'].append({'step': 1, 'ok': False, 'error': str(err)})
|
||||
|
||||
# Acción 3 — github zen
|
||||
try:
|
||||
resultados['acciones'].append({'step': 2, 'ok': True, 'data': getGithubZen()})
|
||||
except Exception as err:
|
||||
errores.append(f'github_zen: {err}')
|
||||
resultados['acciones'].append({'step': 2, 'ok': False, 'error': str(err)})
|
||||
|
||||
# Acción 1 — persistir en BD
|
||||
estado_final = 'ok' if not errores else 'error'
|
||||
nombre = clean_sql_string(params.get('nombre')) or f'Ejecucion {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}'
|
||||
origen = clean_sql_string(params.get('origen')) or 'manual'
|
||||
|
||||
registro = setEjecucion({
|
||||
'nombre': nombre,
|
||||
'descripcion': params.get('descripcion') or 'Ejecución orquestada por /api/automatizados/ejecutar/',
|
||||
'estado': estado_final,
|
||||
'origen': origen,
|
||||
'resultado': resultados,
|
||||
'error': '; '.join(errores) if errores else None,
|
||||
'activo': True,
|
||||
})
|
||||
|
||||
resultados['acciones'].append({'step': 3, 'ok': True, 'data': registro})
|
||||
|
||||
return {
|
||||
'ejecucion_id': registro.get('id'),
|
||||
'estado': estado_final,
|
||||
'total_acciones': len(resultados['acciones']),
|
||||
'resultados': resultados,
|
||||
'errores': errores,
|
||||
}
|
||||
|
||||
|
||||
# =========================================================
|
||||
# Lectura — historial (get_parameterized)
|
||||
# =========================================================
|
||||
def getHistorial(params):
|
||||
"""
|
||||
Devuelve el histórico de ejecuciones. Permite filtrar por estado y limit.
|
||||
"""
|
||||
query = """
|
||||
SELECT id, nombre, descripcion, estado, origen, resultado, error,
|
||||
fecha_inicio, fecha_fin, activo
|
||||
FROM automatizacion_ejecuciones
|
||||
WHERE (%s = '' OR estado = %s)
|
||||
ORDER BY fecha_inicio DESC
|
||||
LIMIT %s
|
||||
"""
|
||||
|
||||
estado = clean_sql_string(params.get('estado')) if params.get('estado') else ''
|
||||
limite = clean_sql_int(params.get('limit')) or 20
|
||||
|
||||
parameter_dict = [estado, estado, limite]
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(query, parameter_dict)
|
||||
columns = [col[0] for col in cursor.description]
|
||||
rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
|
||||
|
||||
# Normalizamos el campo resultado (JSON serializado en SQLite)
|
||||
for row in rows:
|
||||
if isinstance(row.get('resultado'), str):
|
||||
try:
|
||||
row['resultado'] = json.loads(row['resultado'])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return {'total': len(rows), 'data': rows}
|
||||
|
||||
|
||||
# =========================================================
|
||||
# Estado del módulo
|
||||
# =========================================================
|
||||
def getEstado():
|
||||
"""
|
||||
Devuelve el estado general del módulo de automatizaciones:
|
||||
cuántas ejecuciones hay, última ejecución, endpoints disponibles.
|
||||
"""
|
||||
query = """
|
||||
SELECT COUNT(*) AS total,
|
||||
SUM(CASE WHEN estado = 'ok' THEN 1 ELSE 0 END) AS ok,
|
||||
SUM(CASE WHEN estado = 'error' THEN 1 ELSE 0 END) AS errores,
|
||||
MAX(fecha_inicio) AS ultima_ejecucion
|
||||
FROM automatizacion_ejecuciones
|
||||
"""
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(query)
|
||||
columns = [col[0] for col in cursor.description]
|
||||
row = cursor.fetchone()
|
||||
resumen = dict(zip(columns, row)) if row else {}
|
||||
|
||||
return {
|
||||
'status': 'ok',
|
||||
'modulo': 'automatizados',
|
||||
'endpoints': [
|
||||
'POST /api/automatizados/ejecutar/',
|
||||
'POST /api/automatizados/historial/',
|
||||
'POST /api/automatizados/estado/',
|
||||
],
|
||||
'resumen': resumen,
|
||||
}
|
||||
6
app/automatizados/apps.py
Normal file
6
app/automatizados/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AutomatizadosConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'automatizados'
|
||||
47
app/automatizados/fixtures/semillas.json
Normal file
47
app/automatizados/fixtures/semillas.json
Normal file
@@ -0,0 +1,47 @@
|
||||
[
|
||||
{
|
||||
"model": "automatizados.automatizacionejecucion",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"nombre": "Ejecución inicial de prueba",
|
||||
"descripcion": "Seed de ejemplo para poblar la tabla de ejecuciones automatizadas",
|
||||
"estado": "ok",
|
||||
"origen": "seed",
|
||||
"resultado": {"acciones": 0, "detalle": "fixture de arranque"},
|
||||
"error": null,
|
||||
"fecha_inicio": "2026-04-16T10:00:00Z",
|
||||
"fecha_fin": "2026-04-16T10:00:05Z",
|
||||
"activo": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "automatizados.automatizacionejecucion",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"nombre": "Chequeo nocturno",
|
||||
"descripcion": "Ejemplo de ejecución recurrente programada",
|
||||
"estado": "ok",
|
||||
"origen": "jenkins",
|
||||
"resultado": {"acciones": 3, "detalle": "todas las llamadas OK"},
|
||||
"error": null,
|
||||
"fecha_inicio": "2026-04-16T03:00:00Z",
|
||||
"fecha_fin": "2026-04-16T03:00:12Z",
|
||||
"activo": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "automatizados.automatizacionejecucion",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"nombre": "Ejecución con error simulado",
|
||||
"descripcion": "Registro de referencia para estado=error",
|
||||
"estado": "error",
|
||||
"origen": "manual",
|
||||
"resultado": null,
|
||||
"error": "timeout al llamar servicio externo",
|
||||
"fecha_inicio": "2026-04-15T18:30:00Z",
|
||||
"fecha_fin": "2026-04-15T18:30:08Z",
|
||||
"activo": false
|
||||
}
|
||||
}
|
||||
]
|
||||
0
app/automatizados/migrations/__init__.py
Normal file
0
app/automatizados/migrations/__init__.py
Normal file
25
app/automatizados/models.py
Normal file
25
app/automatizados/models.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class AutomatizacionEjecucion(models.Model):
|
||||
"""
|
||||
Registro histórico de ejecuciones automatizadas.
|
||||
Cada vez que el endpoint `ejecutar/` corre, se guarda una fila con el
|
||||
resultado consolidado de las acciones ejecutadas.
|
||||
"""
|
||||
nombre = models.CharField(max_length=255)
|
||||
descripcion = models.TextField(null=True, blank=True)
|
||||
estado = models.CharField(max_length=50, default='pendiente') # pendiente | ok | error
|
||||
origen = models.CharField(max_length=100, default='manual') # manual | jenkins | cron
|
||||
resultado = models.JSONField(null=True, blank=True)
|
||||
error = models.TextField(null=True, blank=True)
|
||||
fecha_inicio = models.DateTimeField(auto_now_add=True)
|
||||
fecha_fin = models.DateTimeField(null=True, blank=True)
|
||||
activo = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'automatizacion_ejecuciones'
|
||||
ordering = ['-fecha_inicio']
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.nombre} [{self.estado}]'
|
||||
8
app/automatizados/urls.py
Normal file
8
app/automatizados/urls.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.urls import path
|
||||
from .views import AutomatizadosEjecutar, AutomatizadosHistorial, AutomatizadosEstado
|
||||
|
||||
urlpatterns = [
|
||||
path('ejecutar/', AutomatizadosEjecutar.as_view(), name='automatizados_ejecutar'),
|
||||
path('historial/', AutomatizadosHistorial.as_view(), name='automatizados_historial'),
|
||||
path('estado/', AutomatizadosEstado.as_view(), name='automatizados_estado'),
|
||||
]
|
||||
132
app/automatizados/views.py
Normal file
132
app/automatizados/views.py
Normal file
@@ -0,0 +1,132 @@
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
from django.http import JsonResponse
|
||||
|
||||
from general.utilidades.acciones import LogService
|
||||
from .acciones import ejecutarAutomatizaciones, getHistorial, getEstado
|
||||
|
||||
|
||||
class AutomatizadosEjecutar(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
path = '/automatizados/ejecutar/'
|
||||
|
||||
# --- BLOQUE 1: Inicio Log ---
|
||||
log_id = LogService.gestionar_log(self, request, path=path)
|
||||
|
||||
try:
|
||||
# --- BLOQUE 2: Data Cleaning ---
|
||||
data = request.data
|
||||
status = 100
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_request=data, status_code=status)
|
||||
|
||||
params = {
|
||||
'nombre': data.get('nombre'),
|
||||
'descripcion': data.get('descripcion'),
|
||||
'origen': data.get('origen') or 'manual',
|
||||
}
|
||||
|
||||
except Exception as error:
|
||||
response = {'body': {'data': [], 'error': str(error)}, 'mensaje': str(error)}
|
||||
status = 400
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||
return JsonResponse(response, status=status, safe=False)
|
||||
|
||||
try:
|
||||
# --- BLOQUE 3: Action Call ---
|
||||
resultado = ejecutarAutomatizaciones(params)
|
||||
response = resultado
|
||||
status = 200
|
||||
# --- BLOQUE 4: Cierre Log ---
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||
return JsonResponse(response, safe=False, status=status)
|
||||
|
||||
except Exception as error:
|
||||
response = {'body': {'data': [], 'error': str(error)}, 'error': str(error)}
|
||||
status = 500
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||
return JsonResponse(response, status=status, safe=False)
|
||||
|
||||
|
||||
class AutomatizadosHistorial(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
path = '/automatizados/historial/'
|
||||
|
||||
# --- BLOQUE 1: Inicio Log ---
|
||||
log_id = LogService.gestionar_log(self, request, path=path)
|
||||
|
||||
try:
|
||||
# --- BLOQUE 2: Data Cleaning ---
|
||||
data = request.data
|
||||
status = 100
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_request=data, status_code=status)
|
||||
|
||||
params = {
|
||||
'estado': data.get('estado'),
|
||||
'limit': data.get('limit') or 20,
|
||||
}
|
||||
|
||||
except Exception as error:
|
||||
response = {'body': {'data': [], 'error': str(error)}, 'mensaje': str(error)}
|
||||
status = 400
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||
return JsonResponse(response, status=status, safe=False)
|
||||
|
||||
try:
|
||||
# --- BLOQUE 3: Action Call ---
|
||||
resultado = getHistorial(params)
|
||||
response = resultado
|
||||
status = 200
|
||||
# --- BLOQUE 4: Cierre Log ---
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||
return JsonResponse(response, safe=False, status=status)
|
||||
|
||||
except Exception as error:
|
||||
response = {'body': {'data': [], 'error': str(error)}, 'error': str(error)}
|
||||
status = 500
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||
return JsonResponse(response, status=status, safe=False)
|
||||
|
||||
|
||||
class AutomatizadosEstado(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
path = '/automatizados/estado/'
|
||||
|
||||
# --- BLOQUE 1: Inicio Log ---
|
||||
log_id = LogService.gestionar_log(self, request, path=path)
|
||||
|
||||
try:
|
||||
# --- BLOQUE 2: Data Cleaning ---
|
||||
data = request.data
|
||||
status = 100
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_request=data, status_code=status)
|
||||
|
||||
except Exception as error:
|
||||
response = {'body': {'data': [], 'error': str(error)}, 'mensaje': str(error)}
|
||||
status = 400
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||
return JsonResponse(response, status=status, safe=False)
|
||||
|
||||
try:
|
||||
# --- BLOQUE 3: Action Call ---
|
||||
resultado = getEstado()
|
||||
response = resultado
|
||||
status = 200
|
||||
# --- BLOQUE 4: Cierre Log ---
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||
return JsonResponse(response, safe=False, status=status)
|
||||
|
||||
except Exception as error:
|
||||
response = {'body': {'data': [], 'error': str(error)}, 'error': str(error)}
|
||||
status = 500
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||
return JsonResponse(response, status=status, safe=False)
|
||||
34
app/backend_admin/acciones.py
Normal file
34
app/backend_admin/acciones.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from django.contrib.auth import authenticate
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
|
||||
class Admin:
|
||||
def get_status_action(self):
|
||||
# Tu lógica de status que ya tenías
|
||||
return {"status": "ok", "service": "Admin Infrastructure"}
|
||||
|
||||
|
||||
def obtener_token_action(self, params):
|
||||
"""
|
||||
Capa Action: Valida credenciales y genera un par de tokens JWT.
|
||||
"""
|
||||
username = params.get('username')
|
||||
password = params.get('password')
|
||||
|
||||
# 1. Autenticación
|
||||
user = authenticate(username=username, password=password)
|
||||
|
||||
if user is not None:
|
||||
# 2. Generación de JWT (Access & Refresh)
|
||||
refresh = RefreshToken.for_user(user)
|
||||
|
||||
return {
|
||||
'refresh': str(refresh),
|
||||
'access': str(refresh.access_token),
|
||||
'user': user.username,
|
||||
'status': 'success'
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
35
app/backend_admin/migrations/0001_initial.py
Normal file
35
app/backend_admin/migrations/0001_initial.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# Generated by Django 5.0.3 on 2026-04-14 23:15
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Log',
|
||||
fields=[
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||
('user_id', models.IntegerField(default=0)),
|
||||
('user', models.CharField(default='anonimo', max_length=255)),
|
||||
('app_id', models.IntegerField(default=0)),
|
||||
('remote_address', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('request', models.JSONField(blank=True, null=True)),
|
||||
('response', models.JSONField(blank=True, null=True)),
|
||||
('status_code', models.CharField(default='0', max_length=10)),
|
||||
('path', models.CharField(max_length=255)),
|
||||
('method', models.CharField(max_length=10)),
|
||||
('createdAt', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('updatedAt', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'audit_logs',
|
||||
},
|
||||
),
|
||||
]
|
||||
1
app/backend_admin/migrations/__init__.py
Normal file
1
app/backend_admin/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Archivo para marcar esta carpeta como paquete de migraciones
|
||||
26
app/backend_admin/models.py
Normal file
26
app/backend_admin/models.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
class Log(models.Model):
|
||||
# Usamos BigAutoField para el BIGINT id de tu tabla
|
||||
id = models.BigAutoField(primary_key=True)
|
||||
user_id = models.IntegerField(default=0)
|
||||
user = models.CharField(max_length=255, default='anonimo')
|
||||
app_id = models.IntegerField(default=0)
|
||||
# GenericIPAddressField para el tipo INET de Postgres
|
||||
remote_address = models.GenericIPAddressField(null=True, blank=True)
|
||||
request = models.JSONField(null=True, blank=True) # Para JSONB
|
||||
response = models.JSONField(null=True, blank=True) # Para JSONB
|
||||
status_code = models.CharField(max_length=10, default='0')
|
||||
path = models.CharField(max_length=255)
|
||||
method = models.CharField(max_length=10)
|
||||
createdAt = models.DateTimeField(default=timezone.now)
|
||||
updatedAt = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
class Meta:
|
||||
db_table = 'audit_logs'
|
||||
# managed = True <-- Asegúrate de que no esté en False
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.method} {self.path} ({self.status_code})"
|
||||
7
app/backend_admin/urls.py
Normal file
7
app/backend_admin/urls.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.urls import path
|
||||
from .views import status_view
|
||||
|
||||
urlpatterns = [
|
||||
# Ruta final: /admin/status/
|
||||
path('status/', status_view, name='admin_status'),
|
||||
]
|
||||
95
app/backend_admin/views.py
Normal file
95
app/backend_admin/views.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from django.http import JsonResponse
|
||||
from .acciones import Admin
|
||||
import logging
|
||||
import json
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from django.utils import timezone
|
||||
from .models import Log
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def status_view(request):
|
||||
# BLOQUE 1: Log de iniciación
|
||||
logger.info("INICIO - Ejecutando Health Check de Administración.")
|
||||
|
||||
# BLOQUE 2: Limpieza y validación de datos
|
||||
# Para un status simple, el diccionario de limpieza está vacío
|
||||
data_cleaned = {}
|
||||
|
||||
# BLOQUE 3: Llamada a la acción
|
||||
try:
|
||||
# Instanciamos la clase Admin y llamamos al método
|
||||
admin_logic = Admin()
|
||||
response_data = admin_logic.get_status_action()
|
||||
status_code = 200
|
||||
except Exception as e:
|
||||
logger.error(f"ERROR - Fallo en get_status_action: {str(e)}")
|
||||
response_data = {"status": "error", "message": "Internal Server Error"}
|
||||
status_code = 500
|
||||
|
||||
# BLOQUE 4: Log de cierre y retorno
|
||||
logger.info(f"FIN - Health Check completado. Status: {status_code}")
|
||||
return JsonResponse(response_data, status=status_code)
|
||||
|
||||
@csrf_exempt
|
||||
@staticmethod
|
||||
def api_token(request):
|
||||
"""
|
||||
Endpoint: api/token/
|
||||
Patrón: 4 bloques con persistencia en Log DB.
|
||||
"""
|
||||
# --- BLOQUE 1: LOG INITIATION ---
|
||||
logger.info("INICIO - Petición de JWT (api/token/)")
|
||||
|
||||
# Iniciamos el registro en la base de datos (Estándar compañeros)
|
||||
log_entry = Log.objects.create(
|
||||
user='anonimo',
|
||||
path='api/token/',
|
||||
method='POST',
|
||||
createdAt=timezone.now(),
|
||||
status_code='0'
|
||||
)
|
||||
|
||||
try:
|
||||
if request.method == 'POST':
|
||||
# --- BLOQUE 2: DATA CLEANING ---
|
||||
body_data = json.loads(request.body)
|
||||
log_entry.request = body_data # Guardamos lo que entró
|
||||
log_entry.save()
|
||||
|
||||
params = {
|
||||
'username': body_data.get('username'),
|
||||
'password': body_data.get('password')
|
||||
}
|
||||
|
||||
# --- BLOQUE 3: ACTION CALL ---
|
||||
admin_logic = Admin()
|
||||
resultado = admin_logic.obtener_token_action(params)
|
||||
|
||||
# --- BLOQUE 4: LOG CLOSURE & RESPONSE ---
|
||||
if resultado:
|
||||
status = 200
|
||||
log_entry.user = resultado['user']
|
||||
log_entry.response = resultado
|
||||
log_entry.status_code = str(status)
|
||||
log_entry.updatedAt = timezone.now()
|
||||
log_entry.save()
|
||||
|
||||
logger.info(f"FIN - JWT generado para: {log_entry.user}")
|
||||
return JsonResponse(resultado, status=status)
|
||||
else:
|
||||
status = 401
|
||||
response_error = {"error": "Credenciales inválidas"}
|
||||
log_entry.status_code = str(status)
|
||||
log_entry.response = response_error
|
||||
log_entry.save()
|
||||
return JsonResponse(response_error, status=status)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"ERROR CRÍTICO en api_token: {str(e)}")
|
||||
log_entry.status_code = '500'
|
||||
log_entry.response = {'error': str(e)}
|
||||
log_entry.save()
|
||||
return JsonResponse({'error': 'Error interno'}, status=500)
|
||||
@@ -1,5 +1,6 @@
|
||||
# apps/common/apps.py
|
||||
from django.apps import AppConfig
|
||||
|
||||
class CommonConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.common'
|
||||
name = 'common'
|
||||
60
app/common/http_client.py
Normal file
60
app/common/http_client.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import urllib.request
|
||||
import json
|
||||
import os
|
||||
|
||||
BACKOFFICE_URL = os.getenv('BACKOFFICE_URL', 'http://django_app_backoffice:8000')
|
||||
COMUNICACIONES_URL = os.getenv('COMUNICACIONES_URL', 'http://django_app_comunicaciones:8000')
|
||||
DOCUMENTACION_URL = os.getenv('DOCUMENTACION_URL', 'http://django_app_documentacion:8000')
|
||||
|
||||
|
||||
def _post(url, payload):
|
||||
data = json.dumps(payload).encode('utf-8')
|
||||
req = urllib.request.Request(
|
||||
url, data=data,
|
||||
headers={'Content-Type': 'application/json'},
|
||||
method='POST'
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
return json.loads(resp.read())
|
||||
|
||||
|
||||
# --- Backoffice ---
|
||||
def backoffice_obtener_usuario(params):
|
||||
return _post(f'{BACKOFFICE_URL}/api/usuarios/obtener/', params)
|
||||
|
||||
def backoffice_crear_usuario(params):
|
||||
return _post(f'{BACKOFFICE_URL}/api/usuarios/guardar/', params)
|
||||
|
||||
def backoffice_obtener_cliente(params):
|
||||
return _post(f'{BACKOFFICE_URL}/api/clientes/obtener/', params)
|
||||
|
||||
def backoffice_crear_cliente(params):
|
||||
return _post(f'{BACKOFFICE_URL}/api/clientes/guardar/', params)
|
||||
|
||||
def backoffice_obtener_contrato(params):
|
||||
return _post(f'{BACKOFFICE_URL}/api/contratos/obtener/', params)
|
||||
|
||||
def backoffice_crear_contrato(params):
|
||||
return _post(f'{BACKOFFICE_URL}/api/contratos/guardar/', params)
|
||||
|
||||
|
||||
# --- Comunicaciones ---
|
||||
def comunicaciones_enviar_email(params):
|
||||
return _post(f'{COMUNICACIONES_URL}/api/email/enviar/', params)
|
||||
|
||||
def comunicaciones_enviar_sms(params):
|
||||
return _post(f'{COMUNICACIONES_URL}/api/sms/enviar/', params)
|
||||
|
||||
def comunicaciones_registrar_webhook(params):
|
||||
return _post(f'{COMUNICACIONES_URL}/api/webhooks/registrar/', params)
|
||||
|
||||
|
||||
# --- Documentacion ---
|
||||
def documentacion_generar_pdf(params):
|
||||
return _post(f'{DOCUMENTACION_URL}/api/generation/pdf/', params)
|
||||
|
||||
def documentacion_obtener_template(params):
|
||||
return _post(f'{DOCUMENTACION_URL}/api/templates/obtener/', params)
|
||||
|
||||
def documentacion_guardar_documento(params):
|
||||
return _post(f'{DOCUMENTACION_URL}/api/storage/guardar/', params)
|
||||
1
app/common/migrations/__init__.py
Normal file
1
app/common/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Archivo para marcar esta carpeta como paquete de migraciones
|
||||
0
app/data/.gitkeep
Normal file
0
app/data/.gitkeep
Normal file
0
app/general/__init__.py
Normal file
0
app/general/__init__.py
Normal file
1
app/general/admin.py
Normal file
1
app/general/admin.py
Normal file
@@ -0,0 +1 @@
|
||||
from django.contrib import admin
|
||||
6
app/general/apps.py
Normal file
6
app/general/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class GeneralConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'general'
|
||||
21
app/general/exception.py
Normal file
21
app/general/exception.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from rest_framework.views import exception_handler
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
def custom_exception_handler(exc, context):
|
||||
"""
|
||||
Handler global de excepciones para DRF.
|
||||
Añadir en settings.py:
|
||||
REST_FRAMEWORK = {
|
||||
'EXCEPTION_HANDLER': 'general.exception.custom_exception_handler',
|
||||
}
|
||||
"""
|
||||
response = exception_handler(exc, context)
|
||||
|
||||
if response is not None:
|
||||
response.data = {
|
||||
'body': {'data': [], 'error': response.data},
|
||||
'mensaje': str(exc),
|
||||
}
|
||||
|
||||
return response
|
||||
0
app/general/migrations/__init__.py
Normal file
0
app/general/migrations/__init__.py
Normal file
5
app/general/models.py
Normal file
5
app/general/models.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.db import models
|
||||
|
||||
# La app general no define modelos propios.
|
||||
# El modelo Log vive en backend_admin.models y se accede desde
|
||||
# general.utilidades.acciones.LogService.
|
||||
24
app/general/request.py
Normal file
24
app/general/request.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import json
|
||||
from django.http import JsonResponse
|
||||
|
||||
|
||||
def parse_body(request):
|
||||
"""
|
||||
Parsea el body de la request como JSON.
|
||||
Lanza ValueError si el body está vacío o no es JSON válido.
|
||||
"""
|
||||
raw = request.body
|
||||
if not raw:
|
||||
raise ValueError('El body de la petición está vacío.')
|
||||
return json.loads(raw)
|
||||
|
||||
|
||||
def build_error_response(message, status=400, data=None):
|
||||
"""Construye una respuesta de error en el formato estándar del proyecto."""
|
||||
body = {'data': data if data is not None else [], 'error': message}
|
||||
return JsonResponse({'body': body, 'mensaje': message}, status=status, safe=False)
|
||||
|
||||
|
||||
def build_success_response(data, status=200):
|
||||
"""Construye una respuesta de éxito en el formato estándar del proyecto."""
|
||||
return JsonResponse(data, status=status, safe=False)
|
||||
100
app/general/request_api.py
Normal file
100
app/general/request_api.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import json
|
||||
import requests
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Request_API:
|
||||
"""Dispatcher de llamadas HTTP internas hacia las APIs especializadas."""
|
||||
|
||||
def _post(self, base_url, url_path, data):
|
||||
url = base_url.rstrip('/') + '/' + url_path.lstrip('/')
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
response = requests.post(url, data=json.dumps(data), headers=headers, timeout=30)
|
||||
return response.json(), response.status_code
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# api_backoffice #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def backoffice_get_parameterized(self, data):
|
||||
return self._post(settings.API_BACKOFFICE, 'api/general/get_parameterized/', data)
|
||||
|
||||
def backoffice_get_dataComplex(self, data):
|
||||
return self._post(settings.API_BACKOFFICE, 'api/general/get_dataComplex/', data)
|
||||
|
||||
def backoffice_set_parameterized(self, data):
|
||||
return self._post(settings.API_BACKOFFICE, 'api/general/set_parameterized/', data)
|
||||
|
||||
def backoffice_set_data2(self, data):
|
||||
return self._post(settings.API_BACKOFFICE, 'api/general/set_data2/', data)
|
||||
|
||||
def backoffice_set_data_batch(self, data):
|
||||
return self._post(settings.API_BACKOFFICE, 'api/general/set_data_batch/', data)
|
||||
|
||||
def backoffice_get_BBDD(self, data):
|
||||
return self._post(settings.API_BACKOFFICE, 'api/general/get_BBDD/', data)
|
||||
|
||||
def backoffice_set_BBDD(self, data):
|
||||
return self._post(settings.API_BACKOFFICE, 'api/general/set_BBDD/', data)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# api_comunicaciones #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def comunicaciones_get_parameterized(self, data):
|
||||
return self._post(settings.API_COMUNICACIONES, 'api/general/get_parameterized/', data)
|
||||
|
||||
def comunicaciones_get_dataComplex(self, data):
|
||||
return self._post(settings.API_COMUNICACIONES, 'api/general/get_dataComplex/', data)
|
||||
|
||||
def comunicaciones_set_parameterized(self, data):
|
||||
return self._post(settings.API_COMUNICACIONES, 'api/general/set_parameterized/', data)
|
||||
|
||||
def comunicaciones_set_data2(self, data):
|
||||
return self._post(settings.API_COMUNICACIONES, 'api/general/set_data2/', data)
|
||||
|
||||
def comunicaciones_set_data_batch(self, data):
|
||||
return self._post(settings.API_COMUNICACIONES, 'api/general/set_data_batch/', data)
|
||||
|
||||
def comunicaciones_get_BBDD(self, data):
|
||||
return self._post(settings.API_COMUNICACIONES, 'api/general/get_BBDD/', data)
|
||||
|
||||
def comunicaciones_set_BBDD(self, data):
|
||||
return self._post(settings.API_COMUNICACIONES, 'api/general/set_BBDD/', data)
|
||||
|
||||
def comunicaciones_enviar_email(self, data):
|
||||
return self._post(settings.API_COMUNICACIONES, 'api/email/enviar/', data)
|
||||
|
||||
def comunicaciones_enviar_sms(self, data):
|
||||
return self._post(settings.API_COMUNICACIONES, 'api/sms/enviar/', data)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# api_documentacion #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def documentacion_get_parameterized(self, data):
|
||||
return self._post(settings.API_DOCUMENTACION, 'api/general/get_parameterized/', data)
|
||||
|
||||
def documentacion_get_dataComplex(self, data):
|
||||
return self._post(settings.API_DOCUMENTACION, 'api/general/get_dataComplex/', data)
|
||||
|
||||
def documentacion_set_parameterized(self, data):
|
||||
return self._post(settings.API_DOCUMENTACION, 'api/general/set_parameterized/', data)
|
||||
|
||||
def documentacion_set_data2(self, data):
|
||||
return self._post(settings.API_DOCUMENTACION, 'api/general/set_data2/', data)
|
||||
|
||||
def documentacion_set_data_batch(self, data):
|
||||
return self._post(settings.API_DOCUMENTACION, 'api/general/set_data_batch/', data)
|
||||
|
||||
def documentacion_get_BBDD(self, data):
|
||||
return self._post(settings.API_DOCUMENTACION, 'api/general/get_BBDD/', data)
|
||||
|
||||
def documentacion_set_BBDD(self, data):
|
||||
return self._post(settings.API_DOCUMENTACION, 'api/general/set_BBDD/', data)
|
||||
|
||||
def documentacion_generar_pdf(self, data):
|
||||
return self._post(settings.API_DOCUMENTACION, 'api/generation/pdf/', data)
|
||||
|
||||
def documentacion_guardar(self, data):
|
||||
return self._post(settings.API_DOCUMENTACION, 'api/storage/guardar/', data)
|
||||
6
app/general/serializers.py
Normal file
6
app/general/serializers.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class EmptySerializer(serializers.Serializer):
|
||||
"""Serializer base vacío para endpoints sin body estructurado."""
|
||||
pass
|
||||
7
app/general/tests.py
Normal file
7
app/general/tests.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class LogServiceTest(TestCase):
|
||||
def test_placeholder(self):
|
||||
"""Placeholder: añadir tests de LogService aquí."""
|
||||
pass
|
||||
6
app/general/urls.py
Normal file
6
app/general/urls.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.urls import path
|
||||
from .views import status_view
|
||||
|
||||
urlpatterns = [
|
||||
path('status/', status_view, name='general_status'),
|
||||
]
|
||||
0
app/general/utilidades/__init__.py
Normal file
0
app/general/utilidades/__init__.py
Normal file
114
app/general/utilidades/acciones.py
Normal file
114
app/general/utilidades/acciones.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import json
|
||||
import logging
|
||||
from django.utils import timezone
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from . import utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _json_safe(value):
|
||||
"""
|
||||
Convierte cualquier valor a un tipo seguro para JSONField.
|
||||
Usa DjangoJSONEncoder para manejar date, datetime, Decimal, UUID, etc.
|
||||
"""
|
||||
if value is None or value == '':
|
||||
return value
|
||||
try:
|
||||
return json.loads(json.dumps(value, cls=DjangoJSONEncoder))
|
||||
except Exception:
|
||||
return str(value)
|
||||
|
||||
|
||||
class LogService:
|
||||
"""
|
||||
Servicio centralizado de gestión de logs.
|
||||
Única fuente de inserción en la tabla audit_logs.
|
||||
|
||||
Uso en vistas:
|
||||
log_id = LogService.gestionar_log(self, request, path='/mi/path/')
|
||||
LogService.gestionar_log(self, request, log_id=log_id, body_request=data, status_code=200)
|
||||
|
||||
Si la BD no está disponible, el log falla silenciosamente para no
|
||||
interrumpir la petición del usuario.
|
||||
"""
|
||||
|
||||
def gestionar_log(
|
||||
self,
|
||||
request,
|
||||
log_id=None,
|
||||
path=None,
|
||||
user=None,
|
||||
body_request=None,
|
||||
body_response=None,
|
||||
status_code=None,
|
||||
):
|
||||
try:
|
||||
return LogService._ejecutar_log(
|
||||
self, request, log_id, path, user, body_request, body_response, status_code
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning('LogService.gestionar_log falló (log omitido): %s', str(e))
|
||||
return log_id # Devuelve el log_id existente o None si era creación
|
||||
|
||||
@staticmethod
|
||||
def _ejecutar_log(caller, request, log_id, path, user, body_request, body_response, status_code):
|
||||
# Importación diferida para evitar problemas de arranque si la BD no está lista
|
||||
from backend_admin.models import Log
|
||||
|
||||
# Determinar la app llamante a partir del módulo de la vista
|
||||
modulo = caller.__class__.__module__
|
||||
app_nombre = modulo.split('.')[0]
|
||||
|
||||
tag_header = request.headers.get('tag')
|
||||
|
||||
if tag_header:
|
||||
usuario_final = tag_header
|
||||
elif app_nombre:
|
||||
apps_automatizadas = ['automatizados']
|
||||
if app_nombre.lower() in apps_automatizadas:
|
||||
usuario_final = 'Jenkins'
|
||||
else:
|
||||
usuario_final = app_nombre
|
||||
else:
|
||||
usuario_final = user if user else 'orquestador'
|
||||
|
||||
if log_id is None:
|
||||
# --- CREACIÓN: primer registro del ciclo de vida de la petición ---
|
||||
path_final = path if path else request.path
|
||||
|
||||
nuevo_log = Log.objects.create(
|
||||
user_id=0,
|
||||
user=usuario_final,
|
||||
app_id=0,
|
||||
remote_address=utils.get_client_ip(request),
|
||||
request='',
|
||||
response='',
|
||||
status_code=status_code if status_code else '0',
|
||||
path=path_final,
|
||||
method=request.method,
|
||||
createdAt=timezone.now(),
|
||||
)
|
||||
return nuevo_log.pk
|
||||
|
||||
|
||||
else:
|
||||
# --- ACTUALIZACIÓN: enriquece el log con datos del procesamiento ---
|
||||
datos_a_actualizar = {
|
||||
'updatedAt': timezone.now(),
|
||||
}
|
||||
|
||||
if user:
|
||||
datos_a_actualizar['user'] = user
|
||||
if path:
|
||||
datos_a_actualizar['path'] = path
|
||||
if body_request is not None:
|
||||
# _json_safe convierte dates, Decimals, etc. a tipos JSON válidos
|
||||
datos_a_actualizar['request'] = _json_safe(body_request)
|
||||
if body_response is not None:
|
||||
datos_a_actualizar['response'] = _json_safe(body_response)
|
||||
if status_code is not None:
|
||||
datos_a_actualizar['status_code'] = str(status_code)
|
||||
|
||||
Log.objects.filter(pk=log_id).update(**datos_a_actualizar)
|
||||
return log_id
|
||||
33
app/general/utilidades/custom_errors.py
Normal file
33
app/general/utilidades/custom_errors.py
Normal file
@@ -0,0 +1,33 @@
|
||||
class ValidationError(Exception):
|
||||
"""Error de validación de datos de entrada."""
|
||||
def __init__(self, message, field=None):
|
||||
self.message = message
|
||||
self.field = field
|
||||
super().__init__(message)
|
||||
|
||||
def to_response(self):
|
||||
detail = {'error': self.message}
|
||||
if self.field:
|
||||
detail['field'] = self.field
|
||||
return {'body': {'data': [], **detail}, 'mensaje': self.message}
|
||||
|
||||
|
||||
class ExternalServiceError(Exception):
|
||||
"""Error en la comunicación con un servicio externo."""
|
||||
def __init__(self, message, service=None):
|
||||
self.message = message
|
||||
self.service = service
|
||||
super().__init__(message)
|
||||
|
||||
def to_response(self):
|
||||
return {'body': {'data': [], 'error': self.message}, 'mensaje': self.message}
|
||||
|
||||
|
||||
class NotFoundError(Exception):
|
||||
"""Recurso no encontrado."""
|
||||
def __init__(self, message='Recurso no encontrado'):
|
||||
self.message = message
|
||||
super().__init__(message)
|
||||
|
||||
def to_response(self):
|
||||
return {'body': {'data': [], 'error': self.message}, 'mensaje': self.message}
|
||||
6
app/general/utilidades/utils.py
Normal file
6
app/general/utilidades/utils.py
Normal file
@@ -0,0 +1,6 @@
|
||||
def get_client_ip(request):
|
||||
"""Obtiene la IP real del cliente, considerando proxies."""
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if x_forwarded_for:
|
||||
return x_forwarded_for.split(',')[0].strip()
|
||||
return request.META.get('REMOTE_ADDR')
|
||||
0
app/general/validaciones/__init__.py
Normal file
0
app/general/validaciones/__init__.py
Normal file
5
app/general/views.py
Normal file
5
app/general/views.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.http import JsonResponse
|
||||
|
||||
|
||||
def status_view(request):
|
||||
return JsonResponse({'status': 'ok', 'service': 'general'})
|
||||
@@ -6,7 +6,7 @@ import sys
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
# Apuntamos a la configuración dentro de la carpeta 'core'
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api_config.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
@@ -1,5 +1,12 @@
|
||||
from django.db import connection
|
||||
from apps.common.utils import clean_sql_string, clean_sql_int
|
||||
from common.utils import clean_sql_string, clean_sql_int
|
||||
|
||||
|
||||
def get_status_action():
|
||||
"""
|
||||
Acción simple para comprobar que la lógica de la API responde.
|
||||
"""
|
||||
return {"status": "ok", "message": "API is running"}
|
||||
|
||||
def getData(params):
|
||||
"""
|
||||
@@ -2,4 +2,4 @@ from django.apps import AppConfig
|
||||
|
||||
class PromocionesConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.promociones'
|
||||
name = 'promociones'
|
||||
35
app/promociones/fixtures/semillas.json
Normal file
35
app/promociones/fixtures/semillas.json
Normal file
@@ -0,0 +1,35 @@
|
||||
[
|
||||
{
|
||||
"model": "promociones.promocion",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"nombre": "Oferta de Bienvenida",
|
||||
"fecha_inicio": "2026-04-01",
|
||||
"descripcion": "Descuento para nuevos usuarios",
|
||||
"activo": true,
|
||||
"categoria_id": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "promociones.promocion",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"nombre": "Promo Primavera",
|
||||
"fecha_inicio": "2026-05-01",
|
||||
"descripcion": "Todo al 20% de descuento",
|
||||
"activo": true,
|
||||
"categoria_id": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "promociones.promocion",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"nombre": "Liquidación Stock",
|
||||
"fecha_inicio": "2026-04-14",
|
||||
"descripcion": "Últimas unidades",
|
||||
"activo": false,
|
||||
"categoria_id": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
26
app/promociones/migrations/0001_initial.py
Normal file
26
app/promociones/migrations/0001_initial.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Promocion',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('nombre', models.CharField(max_length=255)),
|
||||
('fecha_inicio', models.DateField(blank=True, null=True)),
|
||||
('fecha_modificacion', models.DateField(blank=True, null=True)),
|
||||
('descripcion', models.TextField(blank=True, null=True)),
|
||||
('activo', models.BooleanField(default=True)),
|
||||
('categoria_id', models.IntegerField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'promociones',
|
||||
},
|
||||
),
|
||||
]
|
||||
1
app/promociones/migrations/__init__.py
Normal file
1
app/promociones/migrations/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Archivo para marcar esta carpeta como paquete de migraciones
|
||||
6
app/promociones/urls.py
Normal file
6
app/promociones/urls.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.urls import path
|
||||
from .views import PromocionObtener
|
||||
|
||||
urlpatterns = [
|
||||
path('obtener/', PromocionObtener.as_view(), name='obtener_promocion'),
|
||||
]
|
||||
46
app/promociones/views.py
Normal file
46
app/promociones/views.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
from django.http import JsonResponse
|
||||
|
||||
from general.utilidades.acciones import LogService
|
||||
from .acciones import getData
|
||||
|
||||
|
||||
class PromocionObtener(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
path = '/promociones/obtener/'
|
||||
|
||||
# --- LOG: inicio de la petición ---
|
||||
log_id = LogService.gestionar_log(self, request, path=path)
|
||||
|
||||
try:
|
||||
# --- BLOQUE 2: limpieza y validación del body ---
|
||||
data = request.data
|
||||
status = 100
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_request=data, status_code=status)
|
||||
|
||||
params = {'id': data.get('id'), 'activo': data.get('activo')}
|
||||
|
||||
except Exception as error:
|
||||
response = {'body': {'data': [], 'error': str(error)}, 'mensaje': str(error)}
|
||||
status = 400
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||
return JsonResponse(response, status=status, safe=False)
|
||||
|
||||
try:
|
||||
# --- BLOQUE 3: llamada a la acción ---
|
||||
resultado = getData(params)
|
||||
response = resultado
|
||||
status = 200
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||
return JsonResponse(response, safe=False, status=status)
|
||||
|
||||
except Exception as error:
|
||||
response = {'body': {'data': [], 'error': str(error)}, 'error': str(error)}
|
||||
status = 500
|
||||
LogService.gestionar_log(self, request, log_id=log_id, path=path, body_response=response, status_code=status)
|
||||
return JsonResponse(response, status=status, safe=False)
|
||||
8
app/requirements.txt
Normal file
8
app/requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Django==5.0.3
|
||||
psycopg2-binary==2.9.9
|
||||
gunicorn==21.2.0
|
||||
python-dotenv==1.0.1
|
||||
djangorestframework
|
||||
django-cors-headers
|
||||
djangorestframework-simplejwt
|
||||
requests==2.32.3
|
||||
@@ -1,7 +0,0 @@
|
||||
from django.urls import path
|
||||
from .views import get_promocion_view
|
||||
|
||||
urlpatterns = [
|
||||
# Capa 1: Definición del endpoint
|
||||
path('obtener/', get_promocion_view, name='get_promocion'),
|
||||
]
|
||||
@@ -1,54 +0,0 @@
|
||||
import logging
|
||||
from django.http import JsonResponse
|
||||
from .actions import getData
|
||||
|
||||
# Configuración del logger para rastrear la ejecución
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_promocion_view(request):
|
||||
"""
|
||||
Vista estandarizada para la obtención de una promoción.
|
||||
"""
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# BLOQUE 1: Log de iniciación
|
||||
# ---------------------------------------------------------
|
||||
logger.info("[START] Iniciando ejecución de get_promocion_view")
|
||||
|
||||
try:
|
||||
# ---------------------------------------------------------
|
||||
# BLOQUE 2: Limpieza de datos (Data Cleaning)
|
||||
# ---------------------------------------------------------
|
||||
# Extraemos los parámetros del request y preparamos el diccionario
|
||||
raw_data = request.GET.dict()
|
||||
|
||||
# Aquí es donde ella aplicaría validaciones adicionales si fuera necesario
|
||||
clean_params = {
|
||||
'id': raw_data.get('id'),
|
||||
'activo': raw_data.get('activo', True) # Valor por defecto
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# BLOQUE 3: Llamada a la Action (Execution)
|
||||
# ---------------------------------------------------------
|
||||
# La lógica de SQL y parametrización vive dentro de esta llamada
|
||||
resultado_db = getData(clean_params)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# BLOQUE 4: Log de cierre y respuesta (Closure)
|
||||
# ---------------------------------------------------------
|
||||
logger.info(f"[SUCCESS] get_promocion_view finalizada. Registros encontrados: {len(resultado_db)}")
|
||||
|
||||
return JsonResponse({
|
||||
'status': 'success',
|
||||
'data': resultado_db
|
||||
}, status=200)
|
||||
|
||||
except Exception as e:
|
||||
# Log de error detallado en caso de fallo
|
||||
logger.error(f"[ERROR] Fallo crítico en get_promocion_view: {str(e)}")
|
||||
|
||||
return JsonResponse({
|
||||
'status': 'error',
|
||||
'message': 'Error interno del servidor'
|
||||
}, status=500)
|
||||
113
core/settings.py
113
core/settings.py
@@ -1,113 +0,0 @@
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Cargar variables desde el archivo .env
|
||||
load_dotenv()
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-change-me-for-production')
|
||||
DEBUG = os.getenv('DEBUG', 'True').lower() == 'true'
|
||||
|
||||
ALLOWED_HOSTS = [
|
||||
'v-encore-lab.com',
|
||||
'dev.v-encore-lab.com',
|
||||
'185.187.169.109', # Añade la IP aquí
|
||||
'localhost',
|
||||
'127.0.0.1',
|
||||
'django_app_dev',
|
||||
'django_app_master'
|
||||
]
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'apps.promociones',
|
||||
'apps.common',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'core.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'core.wsgi.application'
|
||||
|
||||
# Para desarrollo local: SQLite
|
||||
if DEBUG:
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
else:
|
||||
# Producción: PostgreSQL desde .env
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': os.getenv('DB_NAME'),
|
||||
'USER': os.getenv('DB_USER'),
|
||||
'PASSWORD': os.getenv('DB_PASSWORD'),
|
||||
'HOST': os.getenv('DB_HOST'),
|
||||
'PORT': os.getenv('DB_PORT'),
|
||||
}
|
||||
}
|
||||
|
||||
# Internationalization
|
||||
LANGUAGE_CODE = 'es-es'
|
||||
TIME_ZONE = 'Europe/Madrid'
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Static files
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# Logging básico
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'': {
|
||||
'handlers': ['console'],
|
||||
'level': 'INFO',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
# Redirigimos todas las peticiones de /api/promociones/ a nuestra app
|
||||
path('api/promociones/', include('apps.promociones.urls')),
|
||||
]
|
||||
0
data/.gitkeep
Normal file
0
data/.gitkeep
Normal file
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
34
deployments/.env.example
Normal file
34
deployments/.env.example
Normal file
@@ -0,0 +1,34 @@
|
||||
# =================================================================
|
||||
# 🏗️ INFRAESTRUCTURA (Docker & Jenkins)
|
||||
# =================================================================
|
||||
# Nombres que tomarán los contenedores en Docker
|
||||
APP_CONTAINER_NAME=
|
||||
DB_CONTAINER_NAME=
|
||||
|
||||
# Nombre del proyecto para Docker Compose (usado en Jenkins)
|
||||
PROJECT_NAME=
|
||||
|
||||
# Puertos que se abrirán al exterior (Host)
|
||||
PORT=
|
||||
DATABASE_EXPOSE_PORT=
|
||||
|
||||
# =================================================================
|
||||
# 🐍 CONFIGURACIÓN DE DJANGO
|
||||
# =================================================================
|
||||
# True para desarrollo, False para producción
|
||||
DEBUG=
|
||||
# Genera una clave segura: https://djecrety.ir/
|
||||
SECRET_KEY=
|
||||
# Lista separada por comas: localhost,127.0.0.1,tudominio.com
|
||||
ALLOWED_HOSTS=
|
||||
|
||||
# =================================================================
|
||||
# 🗄️ BASE DE DATOS (PostgreSQL)
|
||||
# =================================================================
|
||||
DB_NAME=
|
||||
DB_USER=
|
||||
DB_PASSWORD=
|
||||
|
||||
# El HOST debe ser "db" cuando se usa Docker Compose
|
||||
DB_HOST=
|
||||
DB_PORT=
|
||||
@@ -1,29 +1,36 @@
|
||||
# Usamos una imagen ligera de Python
|
||||
# 1. Usamos una imagen ligera de Python
|
||||
FROM python:3.12-slim
|
||||
|
||||
# Evitar que Python genere archivos .pyc y que el buffer se sature
|
||||
# 2. Evitar archivos .pyc y saturación del buffer
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV TZ=Europe/Madrid
|
||||
|
||||
# Directorio de trabajo
|
||||
# 3. Directorio de trabajo interno del contenedor
|
||||
WORKDIR /app
|
||||
|
||||
# Instalar dependencias del sistema necesarias
|
||||
# 4. Instalar dependencias del sistema
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libpq-dev \
|
||||
gcc \
|
||||
gettext \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Instalar dependencias de Python
|
||||
COPY requirements.txt /app/
|
||||
# 5. COPIAR REQUISITOS
|
||||
# OJO: Ahora el contexto es la RAÍZ, así que el archivo está en deployments/
|
||||
COPY deployments/requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copiar el resto del código
|
||||
COPY . /app/
|
||||
# 6. COPIAR EL CÓDIGO
|
||||
COPY . .
|
||||
|
||||
# Exponer el puerto de Django
|
||||
# Cambiamos al directorio de la app donde está manage.py
|
||||
WORKDIR /app/app
|
||||
|
||||
# 7. EXPOSICIÓN Y SCRIPT DE ENTRADA
|
||||
EXPOSE 8000
|
||||
|
||||
# Comando por defecto para arrancar (usaremos manage.py en dev y gunicorn en prod)
|
||||
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
|
||||
COPY deployments/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
51
deployments/Jenkinsfile
vendored
51
deployments/Jenkinsfile
vendored
@@ -5,37 +5,58 @@ pipeline {
|
||||
stage('Configurar Entorno') {
|
||||
steps {
|
||||
script {
|
||||
// Selección de configuración según la rama
|
||||
if (env.BRANCH_NAME == 'master') {
|
||||
env.PROJECT_NAME = "django_master"
|
||||
env.CONTAINER_NAME = "django_app_master"
|
||||
env.APP_CONTAINER_NAME = "django_app_master"
|
||||
env.PORT = "8001"
|
||||
env.DEBUG_MODE = "0"
|
||||
} else {
|
||||
env.ENV_CREDENTIAL_ID = "2"
|
||||
} else if (env.BRANCH_NAME == 'dev') {
|
||||
env.PROJECT_NAME = "django_dev"
|
||||
env.CONTAINER_NAME = "django_app_dev"
|
||||
env.APP_CONTAINER_NAME = "django_app_dev"
|
||||
env.PORT = "8000"
|
||||
env.DEBUG_MODE = "1"
|
||||
env.ENV_CREDENTIAL_ID = "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Despliegue') {
|
||||
stage('Fase Final: Containerización') {
|
||||
when { anyOf { branch 'dev'; branch 'master' } }
|
||||
steps {
|
||||
echo "DESPLEGANDO: ${env.CONTAINER_NAME} en el puerto ${env.PORT}"
|
||||
|
||||
// Usamos docker-compose con guion para asegurar compatibilidad
|
||||
sh """
|
||||
CONTAINER_NAME=${env.CONTAINER_NAME} \
|
||||
PORT=${env.PORT} \
|
||||
DEBUG_MODE=${env.DEBUG_MODE} \
|
||||
docker-compose -p ${env.PROJECT_NAME} -f deployments/docker-compose.yml up -d --build web
|
||||
"""
|
||||
withCredentials([file(credentialsId: env.ENV_CREDENTIAL_ID, variable: 'SECRET_ENV')]) {
|
||||
sh """
|
||||
echo "--> Preparando configuración segura..."
|
||||
cp \$SECRET_ENV deployments/.env
|
||||
|
||||
echo "--> 🚀 DESPLEGANDO PROYECTO: ${env.PROJECT_NAME}"
|
||||
|
||||
# 1. Limpieza de contenedores previos para evitar conflictos de nombres
|
||||
docker compose -p ${env.PROJECT_NAME} -f deployments/docker-compose.yml down --remove-orphans
|
||||
|
||||
echo "Ejecutando migraciones en ${env.CONTAINER_NAME}..."
|
||||
sh "docker exec ${env.CONTAINER_NAME} python manage.py migrate --noinput"
|
||||
# 2. Despliegue forzando la lectura del archivo .env específico
|
||||
# CRÍTICO: --env-file asegura que DATABASE_EXPOSE_PORT se lea correctamente
|
||||
docker compose \
|
||||
-p ${env.PROJECT_NAME} \
|
||||
-f deployments/docker-compose.yml \
|
||||
--env-file deployments/.env \
|
||||
up -d --build web
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "✅ Despliegue de ${env.BRANCH_NAME} completado con éxito."
|
||||
sh "rm -f deployments/.env"
|
||||
}
|
||||
failure {
|
||||
echo "❌ Error en el despliegue de ${env.BRANCH_NAME}."
|
||||
sh "rm -f deployments/.env"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
deployments/Makefile
Normal file
15
deployments/Makefile
Normal file
@@ -0,0 +1,15 @@
|
||||
NETWORK = saas_network
|
||||
|
||||
.PHONY: network up down logs
|
||||
|
||||
network:
|
||||
docker network inspect $(NETWORK) >/dev/null 2>&1 || docker network create $(NETWORK)
|
||||
|
||||
up: network
|
||||
docker compose up --build -d
|
||||
|
||||
down:
|
||||
docker compose down
|
||||
|
||||
logs:
|
||||
docker compose logs -f
|
||||
@@ -1,25 +1,48 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
container_name: ${DB_CONTAINER_NAME:-django_core_db}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: ${DB_NAME:-django_core_db}
|
||||
POSTGRES_USER: ${DB_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "${DATABASE_EXPOSE_PORT:-5432}:5432"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-django_core_db}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
|
||||
web:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deployments/Dockerfile
|
||||
container_name: ${CONTAINER_NAME}
|
||||
restart: always
|
||||
working_dir: /app # <--- Vital para que encuentre 'core'
|
||||
container_name: ${APP_CONTAINER_NAME:-django_core_app}
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- DEBUG=${DEBUG_MODE}
|
||||
- PYTHONPATH=/app # <--- Asegura que Python vea las carpetas
|
||||
- DB_NAME=gitea
|
||||
- DB_USER=gitea
|
||||
- DB_PASSWORD=gitea
|
||||
- DB_HOST=gitea-db
|
||||
- DB_HOST=db
|
||||
- DB_PORT=5432
|
||||
networks:
|
||||
- gitea_default
|
||||
ports:
|
||||
- "${PORT}:8000"
|
||||
- "${PORT:-8000}:8000"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- default
|
||||
- saas_network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
networks:
|
||||
gitea_default:
|
||||
external: true
|
||||
name: frontend
|
||||
saas_network:
|
||||
name: saas_network
|
||||
|
||||
41
deployments/entrypoint.sh
Normal file
41
deployments/entrypoint.sh
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Salir inmediatamente si un comando falla
|
||||
set -e
|
||||
|
||||
# --- Esperar a PostgreSQL si estamos en modo BD remota ---
|
||||
if [ -n "$DB_HOST" ]; then
|
||||
echo "--> Esperando a PostgreSQL en $DB_HOST:${DB_PORT:-5432}..."
|
||||
until python -c "
|
||||
import sys, psycopg2, os
|
||||
try:
|
||||
psycopg2.connect(
|
||||
host=os.environ['DB_HOST'],
|
||||
port=os.environ.get('DB_PORT', 5432),
|
||||
user=os.environ['DB_USER'],
|
||||
password=os.environ['DB_PASSWORD'],
|
||||
dbname=os.environ['DB_NAME']
|
||||
)
|
||||
sys.exit(0)
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
" 2>/dev/null; do
|
||||
echo " PostgreSQL no disponible, reintentando en 2s..."
|
||||
sleep 2
|
||||
done
|
||||
echo "--> PostgreSQL listo."
|
||||
fi
|
||||
|
||||
echo "--> Ejecutando migraciones..."
|
||||
python manage.py migrate --noinput
|
||||
|
||||
echo "--> Cargando semillas (si existen)..."
|
||||
python manage.py loaddata semillas 2>/dev/null || echo " Sin semillas, continuando."
|
||||
|
||||
echo "--> Arrancando servidor con Gunicorn..."
|
||||
exec gunicorn api_config.wsgi:application \
|
||||
--bind 0.0.0.0:8000 \
|
||||
--workers 2 \
|
||||
--timeout 120 \
|
||||
--access-logfile - \
|
||||
--error-logfile -
|
||||
@@ -1,2 +1,8 @@
|
||||
Django==5.0.3
|
||||
psycopg2-binary==2.
|
||||
psycopg2-binary==2.9.9
|
||||
gunicorn==21.2.0
|
||||
python-dotenv==1.0.1
|
||||
djangorestframework
|
||||
django-cors-headers
|
||||
djangorestframework-simplejwt
|
||||
requests==2.32.3
|
||||
11
environments/DEV.postman_environment.json
Normal file
11
environments/DEV.postman_environment.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"id": "x8jh10qysfo",
|
||||
"name": "DEV",
|
||||
"values": [
|
||||
{ "key": "base_url", "value": "https://dev.v-encore-lab.com", "type": "default", "enabled": true },
|
||||
{ "key": "username", "value": "admin", "type": "default", "enabled": true },
|
||||
{ "key": "password", "value": "admin", "type": "default", "enabled": true },
|
||||
{ "key": "access_token", "value": "", "type": "default", "enabled": true }
|
||||
],
|
||||
"_postman_variable_scope": "environment"
|
||||
}
|
||||
11
environments/LOCAL.postman_environment.json
Normal file
11
environments/LOCAL.postman_environment.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"id": "ftt19d5vxuj",
|
||||
"name": "LOCAL",
|
||||
"values": [
|
||||
{ "key": "base_url", "value": "http://localhost:8000", "type": "default", "enabled": true },
|
||||
{ "key": "username", "value": "admin", "type": "default", "enabled": true },
|
||||
{ "key": "password", "value": "admin", "type": "default", "enabled": true },
|
||||
{ "key": "access_token", "value": "", "type": "default", "enabled": true }
|
||||
],
|
||||
"_postman_variable_scope": "environment"
|
||||
}
|
||||
11
environments/PROD.postman_environment.json
Normal file
11
environments/PROD.postman_environment.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"id": "3ckrklvkf27",
|
||||
"name": "PROD",
|
||||
"values": [
|
||||
{ "key": "base_url", "value": "https://v-encore-lab.com", "type": "default", "enabled": true },
|
||||
{ "key": "username", "value": "admin", "type": "default", "enabled": true },
|
||||
{ "key": "password", "value": "admin", "type": "default", "enabled": true },
|
||||
{ "key": "access_token", "value": "", "type": "default", "enabled": true }
|
||||
],
|
||||
"_postman_variable_scope": "environment"
|
||||
}
|
||||
12
frontend/index.html
Normal file
12
frontend/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Django Core Base</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
16
frontend/node_modules/.bin/nanoid
generated
vendored
Normal file
16
frontend/node_modules/.bin/nanoid
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../nanoid/bin/nanoid.cjs" "$@"
|
||||
else
|
||||
exec node "$basedir/../nanoid/bin/nanoid.cjs" "$@"
|
||||
fi
|
||||
17
frontend/node_modules/.bin/nanoid.cmd
generated
vendored
Normal file
17
frontend/node_modules/.bin/nanoid.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\nanoid\bin\nanoid.cjs" %*
|
||||
28
frontend/node_modules/.bin/nanoid.ps1
generated
vendored
Normal file
28
frontend/node_modules/.bin/nanoid.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
16
frontend/node_modules/.bin/rolldown
generated
vendored
Normal file
16
frontend/node_modules/.bin/rolldown
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../rolldown/bin/cli.mjs" "$@"
|
||||
else
|
||||
exec node "$basedir/../rolldown/bin/cli.mjs" "$@"
|
||||
fi
|
||||
17
frontend/node_modules/.bin/rolldown.cmd
generated
vendored
Normal file
17
frontend/node_modules/.bin/rolldown.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\rolldown\bin\cli.mjs" %*
|
||||
28
frontend/node_modules/.bin/rolldown.ps1
generated
vendored
Normal file
28
frontend/node_modules/.bin/rolldown.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../rolldown/bin/cli.mjs" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../rolldown/bin/cli.mjs" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../rolldown/bin/cli.mjs" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../rolldown/bin/cli.mjs" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
16
frontend/node_modules/.bin/tsc
generated
vendored
Normal file
16
frontend/node_modules/.bin/tsc
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
|
||||
else
|
||||
exec node "$basedir/../typescript/bin/tsc" "$@"
|
||||
fi
|
||||
17
frontend/node_modules/.bin/tsc.cmd
generated
vendored
Normal file
17
frontend/node_modules/.bin/tsc.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\typescript\bin\tsc" %*
|
||||
28
frontend/node_modules/.bin/tsc.ps1
generated
vendored
Normal file
28
frontend/node_modules/.bin/tsc.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../typescript/bin/tsc" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../typescript/bin/tsc" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../typescript/bin/tsc" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../typescript/bin/tsc" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
16
frontend/node_modules/.bin/tsserver
generated
vendored
Normal file
16
frontend/node_modules/.bin/tsserver
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
|
||||
else
|
||||
exec node "$basedir/../typescript/bin/tsserver" "$@"
|
||||
fi
|
||||
17
frontend/node_modules/.bin/tsserver.cmd
generated
vendored
Normal file
17
frontend/node_modules/.bin/tsserver.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\typescript\bin\tsserver" %*
|
||||
28
frontend/node_modules/.bin/tsserver.ps1
generated
vendored
Normal file
28
frontend/node_modules/.bin/tsserver.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../typescript/bin/tsserver" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../typescript/bin/tsserver" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../typescript/bin/tsserver" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../typescript/bin/tsserver" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
16
frontend/node_modules/.bin/vite
generated
vendored
Normal file
16
frontend/node_modules/.bin/vite
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../vite/bin/vite.js" "$@"
|
||||
else
|
||||
exec node "$basedir/../vite/bin/vite.js" "$@"
|
||||
fi
|
||||
17
frontend/node_modules/.bin/vite.cmd
generated
vendored
Normal file
17
frontend/node_modules/.bin/vite.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\vite\bin\vite.js" %*
|
||||
28
frontend/node_modules/.bin/vite.ps1
generated
vendored
Normal file
28
frontend/node_modules/.bin/vite.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../vite/bin/vite.js" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../vite/bin/vite.js" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../vite/bin/vite.js" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../vite/bin/vite.js" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
536
frontend/node_modules/.package-lock.json
generated
vendored
Normal file
536
frontend/node_modules/.package-lock.json
generated
vendored
Normal file
@@ -0,0 +1,536 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.124.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz",
|
||||
"integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||
"integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@standard-schema/utils": "^0.3.0",
|
||||
"immer": "^11.0.0",
|
||||
"redux": "^5.0.1",
|
||||
"redux-thunk": "^3.1.0",
|
||||
"reselect": "^5.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.7",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
|
||||
"integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@standard-schema/utils": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/use-sync-external-store": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
|
||||
"integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rolldown/pluginutils": "1.0.0-rc.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"vite": "^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@rolldown/plugin-babel": {
|
||||
"optional": true
|
||||
},
|
||||
"babel-plugin-react-compiler": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "11.1.4",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz",
|
||||
"integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-android-arm64": "1.32.0",
|
||||
"lightningcss-darwin-arm64": "1.32.0",
|
||||
"lightningcss-darwin-x64": "1.32.0",
|
||||
"lightningcss-freebsd-x64": "1.32.0",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.32.0",
|
||||
"lightningcss-linux-arm64-gnu": "1.32.0",
|
||||
"lightningcss-linux-arm64-musl": "1.32.0",
|
||||
"lightningcss-linux-x64-gnu": "1.32.0",
|
||||
"lightningcss-linux-x64-musl": "1.32.0",
|
||||
"lightningcss-win32-arm64-msvc": "1.32.0",
|
||||
"lightningcss-win32-x64-msvc": "1.32.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
||||
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.5",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
|
||||
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.2.5",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
|
||||
"integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.2.5"
|
||||
}
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.2.25 || ^19",
|
||||
"react": "^18.0 || ^19",
|
||||
"redux": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"redux": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reselect": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.124.0",
|
||||
"@rolldown/pluginutils": "1.0.0-rc.15"
|
||||
},
|
||||
"bin": {
|
||||
"rolldown": "bin/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.0.0-rc.15",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.15",
|
||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.15",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.15",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.15",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.15",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-rc.15",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
|
||||
"integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
|
||||
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.8",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
|
||||
"integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
"postcss": "^8.5.8",
|
||||
"rolldown": "1.0.0-rc.15",
|
||||
"tinyglobby": "^0.2.15"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"@vitejs/devtools": "^0.1.0",
|
||||
"esbuild": "^0.27.0 || ^0.28.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "^4.0.0",
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitejs/devtools": {
|
||||
"optional": true
|
||||
},
|
||||
"esbuild": {
|
||||
"optional": true
|
||||
},
|
||||
"jiti": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
},
|
||||
"tsx": {
|
||||
"optional": true
|
||||
},
|
||||
"yaml": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
frontend/node_modules/@oxc-project/types/LICENSE
generated
vendored
Normal file
22
frontend/node_modules/@oxc-project/types/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024-present VoidZero Inc. & Contributors
|
||||
Copyright (c) 2023 Boshen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
3
frontend/node_modules/@oxc-project/types/README.md
generated
vendored
Normal file
3
frontend/node_modules/@oxc-project/types/README.md
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Oxc Types
|
||||
|
||||
Typescript definitions for Oxc AST nodes.
|
||||
26
frontend/node_modules/@oxc-project/types/package.json
generated
vendored
Normal file
26
frontend/node_modules/@oxc-project/types/package.json
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@oxc-project/types",
|
||||
"version": "0.124.0",
|
||||
"description": "Types for Oxc AST nodes",
|
||||
"keywords": [
|
||||
"AST",
|
||||
"Parser"
|
||||
],
|
||||
"homepage": "https://oxc.rs",
|
||||
"bugs": "https://github.com/oxc-project/oxc/issues",
|
||||
"license": "MIT",
|
||||
"author": "Boshen and oxc contributors",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/oxc-project/oxc.git",
|
||||
"directory": "npm/oxc-types"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/Boshen"
|
||||
},
|
||||
"files": [
|
||||
"types.d.ts"
|
||||
],
|
||||
"type": "module",
|
||||
"types": "types.d.ts"
|
||||
}
|
||||
1912
frontend/node_modules/@oxc-project/types/types.d.ts
generated
vendored
Normal file
1912
frontend/node_modules/@oxc-project/types/types.d.ts
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
21
frontend/node_modules/@reduxjs/toolkit/LICENSE
generated
vendored
Normal file
21
frontend/node_modules/@reduxjs/toolkit/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Mark Erikson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
110
frontend/node_modules/@reduxjs/toolkit/README.md
generated
vendored
Normal file
110
frontend/node_modules/@reduxjs/toolkit/README.md
generated
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
# Redux Toolkit
|
||||
|
||||

|
||||
[](https://www.npmjs.com/package/@reduxjs/toolkit)
|
||||
[](https://www.npmjs.com/package/@reduxjs/toolkit)
|
||||
|
||||
**The official, opinionated, batteries-included toolset for efficient Redux development**
|
||||
|
||||
## Installation
|
||||
|
||||
### Create a React Redux App
|
||||
|
||||
The recommended way to start new apps with React and Redux Toolkit is by using [our official Redux Toolkit + TS template for Vite](https://github.com/reduxjs/redux-templates), or by creating a new Next.js project using [Next's `with-redux` template](https://github.com/vercel/next.js/tree/canary/examples/with-redux).
|
||||
|
||||
Both of these already have Redux Toolkit and React-Redux configured appropriately for that build tool, and come with a small example app that demonstrates how to use several of Redux Toolkit's features.
|
||||
|
||||
```bash
|
||||
# Vite with our Redux+TS template
|
||||
# (using the `degit` tool to clone and extract the template)
|
||||
npx degit reduxjs/redux-templates/packages/vite-template-redux my-app
|
||||
|
||||
# Next.js using the `with-redux` template
|
||||
npx create-next-app --example with-redux my-app
|
||||
```
|
||||
|
||||
We do not currently have official React Native templates, but recommend these templates for standard React Native and for Expo:
|
||||
|
||||
- https://github.com/rahsheen/react-native-template-redux-typescript
|
||||
- https://github.com/rahsheen/expo-template-redux-typescript
|
||||
|
||||
### An Existing App
|
||||
|
||||
Redux Toolkit is available as a package on NPM for use with a module bundler or in a Node application:
|
||||
|
||||
```bash
|
||||
# NPM
|
||||
npm install @reduxjs/toolkit
|
||||
|
||||
# Yarn
|
||||
yarn add @reduxjs/toolkit
|
||||
```
|
||||
|
||||
The package includes a precompiled ESM build that can be used as a [`<script type="module">` tag](https://unpkg.com/@reduxjs/toolkit/dist/redux-toolkit.browser.mjs) directly in the browser.
|
||||
|
||||
## Documentation
|
||||
|
||||
The Redux Toolkit docs are available at **https://redux-toolkit.js.org**, including API references and usage guides for all of the APIs included in Redux Toolkit.
|
||||
|
||||
The Redux core docs at https://redux.js.org includes the full Redux tutorials, as well usage guides on general Redux patterns.
|
||||
|
||||
## Purpose
|
||||
|
||||
The **Redux Toolkit** package is intended to be the standard way to write Redux logic. It was originally created to help address three common concerns about Redux:
|
||||
|
||||
- "Configuring a Redux store is too complicated"
|
||||
- "I have to add a lot of packages to get Redux to do anything useful"
|
||||
- "Redux requires too much boilerplate code"
|
||||
|
||||
We can't solve every use case, but in the spirit of [`create-react-app`](https://github.com/facebook/create-react-app), we can try to provide some tools that abstract over the setup process and handle the most common use cases, as well as include some useful utilities that will let the user simplify their application code.
|
||||
|
||||
Because of that, this package is deliberately limited in scope. It does _not_ address concepts like "reusable encapsulated Redux modules", folder or file structures, managing entity relationships in the store, and so on.
|
||||
|
||||
Redux Toolkit also includes a powerful data fetching and caching capability that we've dubbed "RTK Query". It's included in the package as a separate set of entry points. It's optional, but can eliminate the need to hand-write data fetching logic yourself.
|
||||
|
||||
## What's Included
|
||||
|
||||
Redux Toolkit includes these APIs:
|
||||
|
||||
- `configureStore()`: wraps `createStore` to provide simplified configuration options and good defaults. It can automatically combine your slice reducers, add whatever Redux middleware you supply, includes `redux-thunk` by default, and enables use of the Redux DevTools Extension.
|
||||
- `createReducer()`: lets you supply a lookup table of action types to case reducer functions, rather than writing switch statements. In addition, it automatically uses the [`immer` library](https://github.com/mweststrate/immer) to let you write simpler immutable updates with normal mutative code, like `state.todos[3].completed = true`.
|
||||
- `createAction()`: generates an action creator function for the given action type string. The function itself has `toString()` defined, so that it can be used in place of the type constant.
|
||||
- `createSlice()`: combines `createReducer()` + `createAction()`. Accepts an object of reducer functions, a slice name, and an initial state value, and automatically generates a slice reducer with corresponding action creators and action types.
|
||||
- `combineSlices()`: combines multiple slices into a single reducer, and allows "lazy loading" of slices after initialisation.
|
||||
- `createListenerMiddleware()`: lets you define "listener" entries that contain an "effect" callback with additional logic, and a way to specify when that callback should run based on dispatched actions or state changes. A lightweight alternative to Redux async middleware like sagas and observables.
|
||||
- `createAsyncThunk()`: accepts an action type string and a function that returns a promise, and generates a thunk that dispatches `pending/resolved/rejected` action types based on that promise
|
||||
- `createEntityAdapter()`: generates a set of reusable reducers and selectors to manage normalized data in the store
|
||||
- The `createSelector()` utility from the [Reselect](https://github.com/reduxjs/reselect) library, re-exported for ease of use.
|
||||
|
||||
For details, see [the Redux Toolkit API Reference section in the docs](https://redux-toolkit.js.org/api/configureStore).
|
||||
|
||||
## RTK Query
|
||||
|
||||
**RTK Query** is provided as an optional addon within the `@reduxjs/toolkit` package. It is purpose-built to solve the use case of data fetching and caching, supplying a compact, but powerful toolset to define an API interface layer for your app. It is intended to simplify common cases for loading data in a web application, eliminating the need to hand-write data fetching & caching logic yourself.
|
||||
|
||||
RTK Query is built on top of the Redux Toolkit core for its implementation, using [Redux](https://redux.js.org/) internally for its architecture. Although knowledge of Redux and RTK are not required to use RTK Query, you should explore all of the additional global store management capabilities they provide, as well as installing the [Redux DevTools browser extension](https://github.com/reduxjs/redux-devtools), which works flawlessly with RTK Query to traverse and replay a timeline of your request & cache behavior.
|
||||
|
||||
RTK Query is included within the installation of the core Redux Toolkit package. It is available via either of the two entry points below:
|
||||
|
||||
```ts no-transpile
|
||||
import { createApi } from '@reduxjs/toolkit/query'
|
||||
|
||||
/* React-specific entry point that automatically generates
|
||||
hooks corresponding to the defined endpoints */
|
||||
import { createApi } from '@reduxjs/toolkit/query/react'
|
||||
```
|
||||
|
||||
### What's included
|
||||
|
||||
RTK Query includes these APIs:
|
||||
|
||||
- `createApi()`: The core of RTK Query's functionality. It allows you to define a set of endpoints describe how to retrieve data from a series of endpoints, including configuration of how to fetch and transform that data. In most cases, you should use this once per app, with "one API slice per base URL" as a rule of thumb.
|
||||
- `fetchBaseQuery()`: A small wrapper around fetch that aims to simplify requests. Intended as the recommended baseQuery to be used in createApi for the majority of users.
|
||||
- `<ApiProvider />`: Can be used as a Provider if you do not already have a Redux store.
|
||||
- `setupListeners()`: A utility used to enable refetchOnMount and refetchOnReconnect behaviors.
|
||||
|
||||
See the [**RTK Query Overview**](https://redux-toolkit.js.org/rtk-query/overview) page for more details on what RTK Query is, what problems it solves, and how to use it.
|
||||
|
||||
## Contributing
|
||||
|
||||
Please refer to our [contributing guide](/CONTRIBUTING.md) to learn about our development process, how to propose bugfixes and improvements, and how to build and test your changes to Redux Toolkit.
|
||||
6
frontend/node_modules/@reduxjs/toolkit/dist/cjs/index.js
generated
vendored
Normal file
6
frontend/node_modules/@reduxjs/toolkit/dist/cjs/index.js
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
'use strict'
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./redux-toolkit.production.min.cjs')
|
||||
} else {
|
||||
module.exports = require('./redux-toolkit.development.cjs')
|
||||
}
|
||||
2387
frontend/node_modules/@reduxjs/toolkit/dist/cjs/redux-toolkit.development.cjs
generated
vendored
Normal file
2387
frontend/node_modules/@reduxjs/toolkit/dist/cjs/redux-toolkit.development.cjs
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
frontend/node_modules/@reduxjs/toolkit/dist/cjs/redux-toolkit.development.cjs.map
generated
vendored
Normal file
1
frontend/node_modules/@reduxjs/toolkit/dist/cjs/redux-toolkit.development.cjs.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user