This commit is contained in:
8
.env
8
.env
@@ -3,8 +3,8 @@ 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_NAME=django_test
|
||||
DB_USER=django_user
|
||||
DB_PASSWORD=django_password
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,3 +10,4 @@ deployments/docker-compose.yml
|
||||
postgres_data/
|
||||
local_postgres_data/
|
||||
*.pyc
|
||||
apps/backend_admin/migrations/0001_initial.py
|
||||
|
||||
67
README.md
67
README.md
@@ -2,5 +2,72 @@
|
||||
|
||||
// V-Encore Lab: Sistema Automatizado v1.0.4
|
||||
|
||||
## 🚀 Inicio Rápido (Desarrollo Local)
|
||||
|
||||
### 1. Clonar y Configurar
|
||||
```bash
|
||||
git clone https://git.v-encore-lab.com/Proyecto-SaaS/django-core-base.git
|
||||
cd django-core-base
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
### 2. Instalar Dependencias
|
||||
```bash
|
||||
pip install -r deployments/requirements.txt
|
||||
```
|
||||
|
||||
### 3. Migraciones de Base de Datos
|
||||
```bash
|
||||
# Crear y aplicar migraciones para todos los modelos
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
|
||||
# Opcional: Crear superusuario
|
||||
python manage.py createsuperuser
|
||||
```
|
||||
|
||||
### 4. Correr el Servidor
|
||||
```bash
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
Abrir http://localhost:8000
|
||||
|
||||
## 🐳 Docker (Producción/Desarrollo)
|
||||
|
||||
```bash
|
||||
docker-compose up --build
|
||||
```
|
||||
|
||||
Acceder a:
|
||||
- App: http://localhost:8000
|
||||
- Admin: http://localhost:8000/admin/
|
||||
- DB: localhost:5432 (Postgres)
|
||||
|
||||
## 📋 Comandos Django Comunes
|
||||
|
||||
```bash
|
||||
# Verificar configuración
|
||||
python manage.py check
|
||||
|
||||
# Recopilar static files
|
||||
python manage.py collectstatic --noinput
|
||||
|
||||
# Test
|
||||
python manage.py test
|
||||
```
|
||||
|
||||
## 🔧 Estructura del Proyecto
|
||||
|
||||
```
|
||||
├── apps/ # Aplicaciones Django
|
||||
│ ├── backend_admin/
|
||||
│ ├── common/
|
||||
│ └── promociones/
|
||||
├── core/ # Configuración principal
|
||||
├── deployments/ # Docker, requirements prod
|
||||
└── manage.py
|
||||
```
|
||||
|
||||
## .env Variables
|
||||
Ver `.env.example` para configuración.
|
||||
@@ -1,13 +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):
|
||||
"""
|
||||
Lógica para comprobar la salud del sistema.
|
||||
Devuelve el estado básico del entorno.
|
||||
Capa Action: Valida credenciales y genera un par de tokens JWT.
|
||||
"""
|
||||
# En el futuro, podrías usar get_parameterized aquí si quisieras
|
||||
# consultar estados en la base de datos.
|
||||
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 {
|
||||
"status": "ok",
|
||||
"message": "V-Encore API System is active",
|
||||
"environment": "dev" # Esto podría venir de una variable de entorno
|
||||
'refresh': str(refresh),
|
||||
'access': str(refresh.access_token),
|
||||
'user': user.username,
|
||||
'status': 'success'
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
0
apps/backend_admin/migrations/__init__.py
Normal file
0
apps/backend_admin/migrations/__init__.py
Normal file
25
apps/backend_admin/models.py
Normal file
25
apps/backend_admin/models.py
Normal file
@@ -0,0 +1,25 @@
|
||||
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 = False # Al estar la tabla ya creada, Django no intentará modificarla
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.method} {self.path} ({self.status_code})"
|
||||
@@ -1,6 +1,12 @@
|
||||
from django.http import JsonResponse
|
||||
from .actions 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__)
|
||||
|
||||
@@ -26,3 +32,64 @@ def status_view(request):
|
||||
# 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 = 'apps.common' # <--- ESTO ES LO QUE DEBE PONER
|
||||
@@ -1,8 +1,6 @@
|
||||
from django.urls import path
|
||||
from .views import get_promocion_view, status_view
|
||||
from .views import PromocionObtener
|
||||
|
||||
urlpatterns = [
|
||||
# Capa 1: Definición del endpoint
|
||||
path('obtener/', get_promocion_view, name='get_promocion'),
|
||||
|
||||
path('obtener/', PromocionObtener.as_view(), name='obtener_promocion'),
|
||||
]
|
||||
@@ -1,73 +1,46 @@
|
||||
import logging
|
||||
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 apps.backend_admin.models import Log
|
||||
from .actions import getData
|
||||
from django.utils import timezone # Esta es la forma correcta
|
||||
class PromocionObtener(APIView):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
from django.http import JsonResponse
|
||||
from .actions import get_status_action
|
||||
def post(self, request):
|
||||
# --- BLOQUE 1: LOG INITIATION ---
|
||||
log_entry = Log.objects.create(
|
||||
user=request.user.username,
|
||||
path='promociones/obtener/',
|
||||
method='POST',
|
||||
status_code='0'
|
||||
)
|
||||
|
||||
|
||||
# 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")
|
||||
print('llega a despues de log entry')
|
||||
|
||||
try:
|
||||
# ---------------------------------------------------------
|
||||
# BLOQUE 2: Limpieza de datos (Data Cleaning)
|
||||
# ---------------------------------------------------------
|
||||
# Extraemos los parámetros del request y preparamos el diccionario
|
||||
raw_data = request.GET.dict()
|
||||
# --- BLOQUE 2: DATA CLEANING ---
|
||||
data = request.data
|
||||
log_entry.request = data
|
||||
log_entry.save()
|
||||
|
||||
# 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
|
||||
}
|
||||
params = {'id': data.get('id'), 'activo': data.get('activo')}
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 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 3: ACTION CALL ---
|
||||
# Aquí ya recibimos las fechas como strings gracias al paso 1
|
||||
resultado = getData(params)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# BLOQUE 4: Log de cierre y respuesta (Closure)
|
||||
# ---------------------------------------------------------
|
||||
logger.info(f"[SUCCESS] get_promocion_view finalizada. Registros encontrados: {len(resultado_db)}")
|
||||
# --- BLOQUE 4: LOG CLOSURE & RESPONSE ---
|
||||
log_entry.response = {"count": len(resultado)} # No guardes todo el JSON si es muy grande
|
||||
log_entry.status_code = '200'
|
||||
log_entry.save()
|
||||
|
||||
return JsonResponse({
|
||||
'status': 'success',
|
||||
'data': resultado_db
|
||||
}, status=200)
|
||||
return JsonResponse(resultado, safe=False, 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)
|
||||
|
||||
# En views.py
|
||||
def status_view(request):
|
||||
# BLOQUE 1: Log de iniciación
|
||||
logger.info("Iniciando petición de status...")
|
||||
|
||||
# BLOQUE 2: Limpieza y validación de datos (En este caso no hay datos de entrada)
|
||||
data_cleaned = {}
|
||||
|
||||
# BLOQUE 3: Llamada a la acción
|
||||
response_data = get_status_action()
|
||||
|
||||
# BLOQUE 4: Log de cierre y retorno
|
||||
logger.info("Status enviado correctamente.")
|
||||
return JsonResponse(response_data, status=200)
|
||||
log_entry.status_code = '500'
|
||||
log_entry.response = {'error': str(e)}
|
||||
log_entry.save()
|
||||
return JsonResponse({'error': str(e)}, status=500)
|
||||
@@ -1,6 +1,7 @@
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
from pathlib import Path
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from datetime import timedelta
|
||||
|
||||
# Cargar variables desde el archivo .env
|
||||
load_dotenv()
|
||||
@@ -8,9 +9,19 @@ load_dotenv()
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-change-me-for-production')
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = os.getenv('DEBUG', 'True').lower() == 'true'
|
||||
|
||||
# Constants from .env
|
||||
DB_NAME = os.getenv('DB_NAME')
|
||||
DB_USER = os.getenv('DB_USER')
|
||||
DB_PASSWORD = os.getenv('DB_PASSWORD')
|
||||
DB_HOST = os.getenv('DB_HOST')
|
||||
DB_PORT = os.getenv('DB_PORT')
|
||||
|
||||
ALLOWED_HOSTS = [
|
||||
'v-encore-lab.com',
|
||||
'dev.v-encore-lab.com',
|
||||
@@ -18,9 +29,12 @@ ALLOWED_HOSTS = [
|
||||
'localhost',
|
||||
'127.0.0.1',
|
||||
'django_app_dev',
|
||||
'django_app_master'
|
||||
'django_app_master',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken'
|
||||
]
|
||||
|
||||
# Application definition
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
@@ -28,11 +42,16 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'apps.promociones',
|
||||
'apps.backend_admin',
|
||||
'apps.common',
|
||||
'corsheaders',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
@@ -62,26 +81,17 @@ TEMPLATES = [
|
||||
|
||||
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 = {
|
||||
# Database
|
||||
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'),
|
||||
}
|
||||
'NAME': DB_NAME,
|
||||
'USER': DB_USER,
|
||||
'PASSWORD': DB_PASSWORD,
|
||||
'HOST': DB_HOST,
|
||||
'PORT': DB_PORT,
|
||||
}
|
||||
}
|
||||
|
||||
# Internationalization
|
||||
LANGUAGE_CODE = 'es-es'
|
||||
@@ -89,13 +99,36 @@ TIME_ZONE = 'Europe/Madrid'
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Static files
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# Logging básico
|
||||
# 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
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
'chrome-extension://amknoiejhlmhancpahfcfcfhllgkpbld',
|
||||
'http://localhost:8000',
|
||||
'http://127.0.0.1:8000',
|
||||
]
|
||||
|
||||
# Logging
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
from django.urls import path, include
|
||||
from django.urls import path, include
|
||||
from apps.backend_admin import views as admin_views
|
||||
|
||||
urlpatterns = [
|
||||
# Redirigimos todas las peticiones de /api/promociones/ a nuestra app
|
||||
path('api/promociones/', include('apps.promociones.urls')),
|
||||
path('admin/', include('apps.backend_admin.urls')), # El tuyo
|
||||
path('admin/', include('apps.backend_admin.urls')),
|
||||
# AÑADE ESTA LÍNEA AQUÍ PARA QUE SEA UNA RUTA PRINCIPAL:
|
||||
path('promociones/', include('apps.promociones.urls')),
|
||||
path('api/token/', admin_views.api_token, name='token_obtain_pair'),
|
||||
]
|
||||
@@ -4,7 +4,8 @@ FROM python:3.12-slim
|
||||
# Evitar que Python genere archivos .pyc y que el buffer se sature
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
# Definimos la zona horaria como variable de entorno
|
||||
ENV TZ=Europe/Madrid
|
||||
# Directorio de trabajo
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -1,25 +1,36 @@
|
||||
services:
|
||||
web:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deployments/Dockerfile
|
||||
container_name: ${CONTAINER_NAME}
|
||||
restart: always
|
||||
working_dir: /app
|
||||
db:
|
||||
image: postgres:15
|
||||
container_name: django_db_local
|
||||
# Cargamos el archivo directamente
|
||||
env_file:
|
||||
- ../.env
|
||||
environment:
|
||||
- DEBUG=${DEBUG_MODE}
|
||||
- PYTHONPATH=/app
|
||||
- DB_NAME=gitea
|
||||
- DB_USER=gitea
|
||||
- DB_PASSWORD=gitea # <-- Asegúrate de que esta sea la que pusiste en la web de Gitea
|
||||
- DB_HOST=gitea-db # <-- IMPORTANTE: Nombre del servicio, sin el "-1"
|
||||
- DB_PORT=5432
|
||||
networks:
|
||||
- gitea_bridge # <-- Conectamos el servicio a nuestro puente
|
||||
# POSTGRES_DB espera estas variables exactas,
|
||||
# así que las mapeamos a lo que tienes en tu .env
|
||||
- POSTGRES_DB=${DB_NAME}
|
||||
- POSTGRES_USER=${DB_USER}
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
ports:
|
||||
- "${PORT}:8000"
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- local_postgres_data:/var/lib/postgresql/data
|
||||
|
||||
networks:
|
||||
gitea_bridge: # <-- Definimos el puente
|
||||
external: true
|
||||
name: root_gitea_gitea # <-- ESTE es el nombre real que sale en tu 'docker network ls'
|
||||
web:
|
||||
build: .
|
||||
container_name: django_app_dev
|
||||
volumes:
|
||||
- ..:/app
|
||||
# Cargamos el archivo directamente aquí también
|
||||
env_file:
|
||||
- ../.env
|
||||
environment:
|
||||
- DB_HOST=db
|
||||
- DB_PORT=5432
|
||||
ports:
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
volumes:
|
||||
local_postgres_data:
|
||||
@@ -1,2 +1,7 @@
|
||||
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
|
||||
@@ -1,4 +0,0 @@
|
||||
Django==5.0.3
|
||||
psycopg2-binary==2.9.9
|
||||
gunicorn==21.2.0
|
||||
python-dotenv==1.0.1
|
||||
Reference in New Issue
Block a user