Se agregaron partidas y modelos
This commit is contained in:
184
MEDIA_CLEANUP_GUIDE.md
Normal file
184
MEDIA_CLEANUP_GUIDE.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# Comando de Limpieza de Archivos Media
|
||||
|
||||
Este documento explica cómo utilizar los comandos de limpieza de archivos media huérfanos en el proyecto Django.
|
||||
|
||||
## ¿Qué son los archivos huérfanos?
|
||||
|
||||
Los archivos huérfanos son archivos que existen físicamente en el directorio `/media/` pero que no están referenciados en ningún campo de archivo (`FileField` o `ImageField`) de la base de datos. Esto puede ocurrir por:
|
||||
|
||||
- Eliminación de registros sin eliminar sus archivos asociados
|
||||
- Errores en procesos de carga de archivos
|
||||
- Archivos temporales que nunca se limpiaron
|
||||
- Migraciones de datos incorrectas
|
||||
|
||||
## Comandos Disponibles
|
||||
|
||||
### 1. cleanup_media (Comando Original)
|
||||
```bash
|
||||
python manage.py cleanup_media [opciones]
|
||||
```
|
||||
|
||||
### 2. cleanup_media_fast (Comando Optimizado)
|
||||
```bash
|
||||
python manage.py cleanup_media_fast [opciones]
|
||||
```
|
||||
|
||||
## Opciones Disponibles
|
||||
|
||||
### Opciones Comunes (ambos comandos):
|
||||
- `--dry-run`: Muestra qué archivos se eliminarían sin eliminarlos realmente
|
||||
- `--verbose`: Muestra información detallada del proceso
|
||||
- `--help`: Muestra ayuda del comando
|
||||
|
||||
### Opciones del cleanup_media:
|
||||
- `--batch-size N`: Tamaño del lote para procesar archivos (default: 10000)
|
||||
|
||||
### Opciones del cleanup_media_fast:
|
||||
- `--limit N`: Limitar el número de archivos huérfanos a procesar
|
||||
- `--sample-size N`: Tamaño de muestra para análisis rápido (default: 10000)
|
||||
- `--quick-scan`: Hacer un escaneo rápido con muestra limitada
|
||||
|
||||
## Uso Recomendado
|
||||
|
||||
### 1. Análisis Inicial (Siempre hacer primero)
|
||||
```bash
|
||||
# Escaneo rápido para obtener una estimación
|
||||
docker exec -it EFC_backend_dev bash -c "cd /app && python manage.py cleanup_media_fast --quick-scan --dry-run --verbose"
|
||||
|
||||
# O escaneo completo para números exactos (puede tomar tiempo)
|
||||
docker exec -it EFC_backend_dev bash -c "cd /app && python manage.py cleanup_media_fast --dry-run --verbose"
|
||||
```
|
||||
|
||||
### 2. Limpieza por Lotes (Recomendado para grandes volúmenes)
|
||||
```bash
|
||||
# Limpiar muestra de 10,000 archivos
|
||||
docker exec -it EFC_backend_dev bash -c "cd /app && python manage.py cleanup_media_fast --quick-scan --verbose"
|
||||
|
||||
# Limpiar hasta 5,000 archivos específicos
|
||||
docker exec -it EFC_backend_dev bash -c "cd /app && python manage.py cleanup_media_fast --limit 5000 --verbose"
|
||||
```
|
||||
|
||||
### 3. Limpieza Completa
|
||||
```bash
|
||||
# Eliminar todos los archivos huérfanos (CUIDADO: puede tomar mucho tiempo)
|
||||
docker exec -it EFC_backend_dev bash -c "cd /app && python manage.py cleanup_media_fast --verbose"
|
||||
```
|
||||
|
||||
## Ejemplos de Salida
|
||||
|
||||
### Dry Run (Modo Prueba):
|
||||
```
|
||||
Iniciando análisis en: /app/media
|
||||
Modo escaneo rápido - muestra de 10000 archivos
|
||||
Obteniendo muestra de archivos...
|
||||
Muestra obtenida: 10000 archivos
|
||||
Encontrados 5 modelos con campos de archivo
|
||||
Procesando Document...
|
||||
Document: 2059405 registros procesados
|
||||
Archivos huérfanos encontrados: 2763 (72.57 MB) - Tiempo: 7.81s
|
||||
|
||||
--- MODO PRUEBA: Los siguientes archivos se eliminarían ---
|
||||
- documents/vu_AC_0101_230_1703_3004804_4.pdf
|
||||
- documents/vu_AC_0101_300_3172_4001419_2.pdf
|
||||
... y 2743 archivos más
|
||||
```
|
||||
|
||||
### Eliminación Real:
|
||||
```
|
||||
¿Estás seguro de que quieres eliminar 2763 archivos? (s/N): s
|
||||
Progreso: 36.2% (1000 eliminados)
|
||||
Progreso: 72.4% (2000 eliminados)
|
||||
Eliminados 2763 archivos huérfanos (51.32 MB)
|
||||
```
|
||||
|
||||
## Casos de Uso
|
||||
|
||||
### Caso 1: Primera vez ejecutando el comando
|
||||
```bash
|
||||
# 1. Hacer análisis inicial
|
||||
docker exec -it EFC_backend_dev bash -c "cd /app && python manage.py cleanup_media_fast --quick-scan --dry-run"
|
||||
|
||||
# 2. Si hay pocos archivos (<5000), eliminar todos
|
||||
docker exec -it EFC_backend_dev bash -c "cd /app && python manage.py cleanup_media_fast --quick-scan"
|
||||
|
||||
# 3. Si hay muchos archivos, eliminar por lotes
|
||||
docker exec -it EFC_backend_dev bash -c "cd /app && python manage.py cleanup_media_fast --limit 10000"
|
||||
```
|
||||
|
||||
### Caso 2: Mantenimiento regular
|
||||
```bash
|
||||
# Escaneo rápido y limpieza
|
||||
docker exec -it EFC_backend_dev bash -c "cd /app && python manage.py cleanup_media_fast --quick-scan"
|
||||
```
|
||||
|
||||
### Caso 3: Limpieza masiva después de migración de datos
|
||||
```bash
|
||||
# 1. Análisis completo primero
|
||||
docker exec -it EFC_backend_dev bash -c "cd /app && python manage.py cleanup_media_fast --dry-run"
|
||||
|
||||
# 2. Limpieza por lotes grandes
|
||||
docker exec -it EFC_backend_dev bash -c "cd /app && python manage.py cleanup_media_fast --limit 50000"
|
||||
```
|
||||
|
||||
## Consideraciones de Seguridad
|
||||
|
||||
### ⚠️ IMPORTANTE - Hacer Backup
|
||||
Siempre haz un backup de tu directorio `/media/` antes de ejecutar eliminaciones masivas:
|
||||
```bash
|
||||
# Backup del directorio media
|
||||
docker exec EFC_backend_dev tar -czf /tmp/media_backup.tar.gz /app/media/
|
||||
|
||||
# Copiar backup al host
|
||||
docker cp EFC_backend_dev:/tmp/media_backup.tar.gz ./media_backup_$(date +%Y%m%d_%H%M%S).tar.gz
|
||||
```
|
||||
|
||||
### ✅ Buenas Prácticas
|
||||
1. **Siempre usar `--dry-run` primero** para ver qué se va a eliminar
|
||||
2. **Empezar con lotes pequeños** (`--limit`) en lugar de eliminar todo de una vez
|
||||
3. **Verificar la aplicación** después de cada limpieza para asegurar que no se rompió nada
|
||||
4. **Ejecutar en horarios de bajo tráfico** para grandes volúmenes
|
||||
5. **Monitorear el espacio en disco** antes y después
|
||||
|
||||
### 🚨 Advertencias
|
||||
- Los archivos eliminados **NO SE PUEDEN RECUPERAR** fácilmente
|
||||
- El comando solo verifica referencias en la BD, no enlaces desde código o templates
|
||||
- Archivos muy recientes podrían estar en proceso de ser referenciados
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "Too many open files"
|
||||
```bash
|
||||
# Aumentar límite de archivos abiertos
|
||||
docker exec -it EFC_backend_dev bash -c "ulimit -n 65536 && cd /app && python manage.py cleanup_media_fast --quick-scan"
|
||||
```
|
||||
|
||||
### Proceso muy lento
|
||||
```bash
|
||||
# Usar modo quick-scan en lugar de escaneo completo
|
||||
docker exec -it EFC_backend_dev bash -c "cd /app && python manage.py cleanup_media_fast --quick-scan --dry-run"
|
||||
```
|
||||
|
||||
### Verificar que el comando existe
|
||||
```bash
|
||||
# Listar comandos disponibles
|
||||
docker exec -it EFC_backend_dev bash -c "cd /app && python manage.py help"
|
||||
```
|
||||
|
||||
## Automatización
|
||||
|
||||
Para automatizar la limpieza regular, puedes crear un cron job:
|
||||
```bash
|
||||
# Ejecutar limpieza semanal los domingos a las 2 AM
|
||||
0 2 * * 0 docker exec EFC_backend_dev bash -c "cd /app && python manage.py cleanup_media_fast --quick-scan" >> /var/log/media_cleanup.log 2>&1
|
||||
```
|
||||
|
||||
## Monitoreo
|
||||
|
||||
Para monitorear el efecto de la limpieza:
|
||||
```bash
|
||||
# Antes de limpiar
|
||||
docker exec EFC_backend_dev du -sh /app/media/
|
||||
|
||||
# Después de limpiar (verificar el cambio)
|
||||
docker exec EFC_backend_dev du -sh /app/media/
|
||||
```
|
||||
0
api/customs/management/__init__.py
Normal file
0
api/customs/management/__init__.py
Normal file
0
api/customs/management/commands/__init__.py
Normal file
0
api/customs/management/commands/__init__.py
Normal file
243
api/customs/management/commands/cleanup_media.py
Normal file
243
api/customs/management/commands/cleanup_media.py
Normal file
@@ -0,0 +1,243 @@
|
||||
import os
|
||||
import gc
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.conf import settings
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from django.db import connection
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Elimina archivos de media que no están referenciados en la base de datos'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help='Muestra qué archivos se eliminarían sin eliminarlos',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--verbose',
|
||||
action='store_true',
|
||||
help='Muestra información detallada del proceso',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--batch-size',
|
||||
type=int,
|
||||
default=10000,
|
||||
help='Tamaño del lote para procesar archivos (default: 10000)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--limit',
|
||||
type=int,
|
||||
help='Limitar el número de archivos huérfanos a procesar',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
media_root = settings.MEDIA_ROOT
|
||||
|
||||
if not os.path.exists(media_root):
|
||||
self.stdout.write(
|
||||
self.style.ERROR(f'El directorio MEDIA_ROOT no existe: {media_root}')
|
||||
)
|
||||
return
|
||||
|
||||
if options['verbose']:
|
||||
self.stdout.write(f'Analizando archivos en: {media_root}')
|
||||
|
||||
# Obtener archivos de BD de forma más eficiente
|
||||
files_in_db = self._get_db_files_optimized(options)
|
||||
|
||||
if options['verbose']:
|
||||
self.stdout.write(f'Archivos referenciados en BD: {len(files_in_db)}')
|
||||
|
||||
# Procesar archivos en lotes para evitar problemas de memoria
|
||||
orphaned_files = self._find_orphaned_files_batch(media_root, files_in_db, options)
|
||||
|
||||
if not orphaned_files:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS('No se encontraron archivos huérfanos')
|
||||
)
|
||||
return
|
||||
|
||||
# Aplicar límite si se especifica
|
||||
if options['limit'] and len(orphaned_files) > options['limit']:
|
||||
orphaned_files = list(orphaned_files)[:options['limit']]
|
||||
self.stdout.write(f'Limitando a {options["limit"]} archivos')
|
||||
|
||||
# Calcula el tamaño total de archivos huérfanos
|
||||
total_size = self._calculate_total_size(orphaned_files, options)
|
||||
size_mb = total_size / (1024 * 1024)
|
||||
|
||||
self.stdout.write(
|
||||
f'Archivos huérfanos encontrados: {len(orphaned_files)} '
|
||||
f'({size_mb:.2f} MB)'
|
||||
)
|
||||
|
||||
if options['dry_run']:
|
||||
self.stdout.write('\n--- MODO PRUEBA: Los siguientes archivos se eliminarían ---')
|
||||
for i, file_path in enumerate(sorted(orphaned_files)):
|
||||
if i >= 50: # Limitar salida en dry-run
|
||||
remaining = len(orphaned_files) - 50
|
||||
self.stdout.write(f' ... y {remaining} archivos más')
|
||||
break
|
||||
relative_path = os.path.relpath(file_path, media_root)
|
||||
self.stdout.write(f' - {relative_path}')
|
||||
else:
|
||||
# Pide confirmación antes de eliminar
|
||||
confirm = input(
|
||||
f'\n¿Estás seguro de que quieres eliminar {len(orphaned_files)} archivos? (s/N): '
|
||||
)
|
||||
|
||||
if confirm.lower() not in ['s', 'si', 'sí', 'y', 'yes']:
|
||||
self.stdout.write('Operación cancelada')
|
||||
return
|
||||
|
||||
self._delete_files_batch(orphaned_files, media_root, options)
|
||||
|
||||
def _get_db_files_optimized(self, options):
|
||||
"""Obtiene archivos de BD de forma optimizada"""
|
||||
files_in_db = set()
|
||||
media_root = settings.MEDIA_ROOT
|
||||
|
||||
for model in apps.get_models():
|
||||
file_fields = [
|
||||
field for field in model._meta.get_fields()
|
||||
if isinstance(field, (models.FileField, models.ImageField))
|
||||
]
|
||||
|
||||
if not file_fields:
|
||||
continue
|
||||
|
||||
if options['verbose']:
|
||||
self.stdout.write(f'Procesando modelo {model.__name__}...')
|
||||
|
||||
# Procesar en lotes para evitar cargar todo en memoria
|
||||
batch_size = options['batch_size']
|
||||
offset = 0
|
||||
|
||||
while True:
|
||||
field_names = [field.name for field in file_fields]
|
||||
queryset = model.objects.values_list(*field_names)[offset:offset + batch_size]
|
||||
batch = list(queryset)
|
||||
|
||||
if not batch:
|
||||
break
|
||||
|
||||
for row in batch:
|
||||
for file_path in row:
|
||||
if file_path:
|
||||
full_path = os.path.join(media_root, str(file_path))
|
||||
files_in_db.add(full_path)
|
||||
|
||||
offset += batch_size
|
||||
|
||||
if options['verbose'] and offset % (batch_size * 10) == 0:
|
||||
self.stdout.write(f' Procesados {offset} registros...')
|
||||
|
||||
# Forzar liberación de memoria
|
||||
gc.collect()
|
||||
|
||||
return files_in_db
|
||||
|
||||
def _find_orphaned_files_batch(self, media_root, files_in_db, options):
|
||||
"""Encuentra archivos huérfanos procesando en lotes"""
|
||||
orphaned_files = []
|
||||
processed_count = 0
|
||||
|
||||
self.stdout.write('Buscando archivos huérfanos...')
|
||||
|
||||
for root, dirs, files in os.walk(media_root):
|
||||
for file in files:
|
||||
if file.startswith('.'):
|
||||
continue
|
||||
|
||||
full_path = os.path.join(root, file)
|
||||
|
||||
if full_path not in files_in_db:
|
||||
orphaned_files.append(full_path)
|
||||
|
||||
processed_count += 1
|
||||
|
||||
if options['verbose'] and processed_count % 50000 == 0:
|
||||
self.stdout.write(f'Procesados {processed_count} archivos, encontrados {len(orphaned_files)} huérfanos...')
|
||||
|
||||
# Liberar memoria periódicamente
|
||||
if processed_count % 100000 == 0:
|
||||
gc.collect()
|
||||
|
||||
return orphaned_files
|
||||
|
||||
def _calculate_total_size(self, orphaned_files, options):
|
||||
"""Calcula el tamaño total de archivos huérfanos"""
|
||||
total_size = 0
|
||||
count = 0
|
||||
|
||||
for file_path in orphaned_files:
|
||||
try:
|
||||
total_size += os.path.getsize(file_path)
|
||||
count += 1
|
||||
|
||||
if options['verbose'] and count % 10000 == 0:
|
||||
self.stdout.write(f'Calculando tamaño... {count}/{len(orphaned_files)}')
|
||||
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return total_size
|
||||
|
||||
def _delete_files_batch(self, orphaned_files, media_root, options):
|
||||
"""Elimina archivos en lotes"""
|
||||
deleted_count = 0
|
||||
deleted_size = 0
|
||||
batch_size = 1000
|
||||
|
||||
total_files = len(orphaned_files)
|
||||
|
||||
for i in range(0, total_files, batch_size):
|
||||
batch = orphaned_files[i:i + batch_size]
|
||||
|
||||
for file_path in batch:
|
||||
try:
|
||||
file_size = os.path.getsize(file_path)
|
||||
os.remove(file_path)
|
||||
deleted_count += 1
|
||||
deleted_size += file_size
|
||||
|
||||
if options['verbose'] and deleted_count % 5000 == 0:
|
||||
relative_path = os.path.relpath(file_path, media_root)
|
||||
self.stdout.write(f'Progreso: {deleted_count}/{total_files} - Eliminado: {relative_path}')
|
||||
|
||||
except OSError as e:
|
||||
relative_path = os.path.relpath(file_path, media_root)
|
||||
self.stdout.write(
|
||||
self.style.ERROR(f'Error eliminando {relative_path}: {e}')
|
||||
)
|
||||
|
||||
# Mostrar progreso por lotes
|
||||
progress = (i + batch_size) / total_files * 100
|
||||
self.stdout.write(f'Progreso: {min(progress, 100):.1f}% ({deleted_count} eliminados)')
|
||||
|
||||
# Forzar liberación de memoria
|
||||
gc.collect()
|
||||
|
||||
# Elimina directorios vacíos
|
||||
self._remove_empty_dirs(media_root)
|
||||
|
||||
deleted_mb = deleted_size / (1024 * 1024)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f'Eliminados {deleted_count} archivos huérfanos ({deleted_mb:.2f} MB)'
|
||||
)
|
||||
)
|
||||
|
||||
def _remove_empty_dirs(self, path):
|
||||
"""Elimina directorios vacíos recursivamente"""
|
||||
for root, dirs, files in os.walk(path, topdown=False):
|
||||
for dir_name in dirs:
|
||||
dir_path = os.path.join(root, dir_name)
|
||||
try:
|
||||
if not os.listdir(dir_path):
|
||||
os.rmdir(dir_path)
|
||||
except OSError:
|
||||
pass
|
||||
284
api/customs/management/commands/cleanup_media_fast.py
Normal file
284
api/customs/management/commands/cleanup_media_fast.py
Normal file
@@ -0,0 +1,284 @@
|
||||
import os
|
||||
import gc
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.conf import settings
|
||||
from django.apps import apps
|
||||
from django.db import models, connection
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Elimina archivos de media que no están referenciados en la base de datos (versión optimizada)'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help='Muestra qué archivos se eliminarían sin eliminarlos',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--verbose',
|
||||
action='store_true',
|
||||
help='Muestra información detallada del proceso',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--limit',
|
||||
type=int,
|
||||
help='Limitar el número de archivos huérfanos a procesar',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--sample-size',
|
||||
type=int,
|
||||
default=10000,
|
||||
help='Tamaño de muestra para análisis rápido (default: 10000)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--quick-scan',
|
||||
action='store_true',
|
||||
help='Hacer un escaneo rápido con muestra limitada',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
media_root = settings.MEDIA_ROOT
|
||||
|
||||
if not os.path.exists(media_root):
|
||||
self.stdout.write(
|
||||
self.style.ERROR(f'El directorio MEDIA_ROOT no existe: {media_root}')
|
||||
)
|
||||
return
|
||||
|
||||
start_time = time.time()
|
||||
self.stdout.write(f'Iniciando análisis en: {media_root}')
|
||||
|
||||
if options['quick_scan']:
|
||||
self.stdout.write(f'Modo escaneo rápido - muestra de {options["sample_size"]} archivos')
|
||||
orphaned_files = self._quick_scan(media_root, options)
|
||||
else:
|
||||
# Obtener archivos de BD usando SQL directo
|
||||
files_in_db = self._get_db_files_sql(options)
|
||||
|
||||
if options['verbose']:
|
||||
self.stdout.write(f'Archivos referenciados en BD: {len(files_in_db)}')
|
||||
|
||||
# Encontrar archivos huérfanos
|
||||
orphaned_files = self._find_orphaned_files(media_root, files_in_db, options)
|
||||
|
||||
if not orphaned_files:
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS('No se encontraron archivos huérfanos')
|
||||
)
|
||||
return
|
||||
|
||||
# Aplicar límite si se especifica
|
||||
if options['limit'] and len(orphaned_files) > options['limit']:
|
||||
orphaned_files = list(orphaned_files)[:options['limit']]
|
||||
self.stdout.write(f'Limitando a {options["limit"]} archivos')
|
||||
|
||||
# Calcula el tamaño total
|
||||
total_size = self._calculate_size_sample(orphaned_files, options)
|
||||
size_mb = total_size / (1024 * 1024)
|
||||
|
||||
elapsed_time = time.time() - start_time
|
||||
self.stdout.write(
|
||||
f'Archivos huérfanos encontrados: {len(orphaned_files)} '
|
||||
f'({size_mb:.2f} MB) - Tiempo: {elapsed_time:.2f}s'
|
||||
)
|
||||
|
||||
if options['dry_run']:
|
||||
self._show_dry_run_results(orphaned_files, media_root)
|
||||
else:
|
||||
self._delete_files(orphaned_files, media_root, options)
|
||||
|
||||
def _get_db_files_sql(self, options):
|
||||
"""Obtiene archivos usando consultas SQL directas"""
|
||||
files_in_db = set()
|
||||
media_root = settings.MEDIA_ROOT
|
||||
|
||||
# Mapear modelos a sus campos de archivo
|
||||
file_field_map = {}
|
||||
for model in apps.get_models():
|
||||
file_fields = [
|
||||
field for field in model._meta.get_fields()
|
||||
if isinstance(field, (models.FileField, models.ImageField))
|
||||
]
|
||||
if file_fields:
|
||||
file_field_map[model] = file_fields
|
||||
|
||||
if options['verbose']:
|
||||
self.stdout.write(f'Encontrados {len(file_field_map)} modelos con campos de archivo')
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
for model, fields in file_field_map.items():
|
||||
if options['verbose']:
|
||||
self.stdout.write(f'Procesando {model.__name__}...')
|
||||
|
||||
table_name = model._meta.db_table
|
||||
field_names = [field.column for field in fields]
|
||||
|
||||
# Construir query SQL
|
||||
field_selects = ', '.join(field_names)
|
||||
where_conditions = []
|
||||
for field in field_names:
|
||||
where_conditions.append(f'{field} IS NOT NULL AND {field} != \'\'')
|
||||
where_clause = ' OR '.join(where_conditions)
|
||||
|
||||
query = f"""
|
||||
SELECT {field_selects}
|
||||
FROM {table_name}
|
||||
WHERE {where_clause}
|
||||
"""
|
||||
|
||||
cursor.execute(query)
|
||||
rows = cursor.fetchall()
|
||||
|
||||
for row in rows:
|
||||
for file_path in row:
|
||||
if file_path:
|
||||
full_path = os.path.join(media_root, str(file_path))
|
||||
files_in_db.add(full_path)
|
||||
|
||||
if options['verbose']:
|
||||
self.stdout.write(f' {model.__name__}: {len(rows)} registros procesados')
|
||||
|
||||
return files_in_db
|
||||
|
||||
def _quick_scan(self, media_root, options):
|
||||
"""Escaneo rápido con muestra limitada"""
|
||||
sample_files = []
|
||||
count = 0
|
||||
target_size = options['sample_size']
|
||||
|
||||
self.stdout.write('Obteniendo muestra de archivos...')
|
||||
|
||||
for root, dirs, files in os.walk(media_root):
|
||||
for file in files:
|
||||
if file.startswith('.'):
|
||||
continue
|
||||
|
||||
sample_files.append(os.path.join(root, file))
|
||||
count += 1
|
||||
|
||||
if count >= target_size:
|
||||
break
|
||||
|
||||
if count >= target_size:
|
||||
break
|
||||
|
||||
self.stdout.write(f'Muestra obtenida: {len(sample_files)} archivos')
|
||||
|
||||
# Obtener archivos de BD para comparar
|
||||
files_in_db = self._get_db_files_sql(options)
|
||||
|
||||
# Encontrar huérfanos en la muestra
|
||||
orphaned_files = [f for f in sample_files if f not in files_in_db]
|
||||
|
||||
return orphaned_files
|
||||
|
||||
def _find_orphaned_files(self, media_root, files_in_db, options):
|
||||
"""Encuentra archivos huérfanos optimizado"""
|
||||
orphaned_files = []
|
||||
processed_count = 0
|
||||
|
||||
self.stdout.write('Buscando archivos huérfanos...')
|
||||
|
||||
for root, dirs, files in os.walk(media_root):
|
||||
for file in files:
|
||||
if file.startswith('.'):
|
||||
continue
|
||||
|
||||
full_path = os.path.join(root, file)
|
||||
|
||||
if full_path not in files_in_db:
|
||||
orphaned_files.append(full_path)
|
||||
|
||||
processed_count += 1
|
||||
|
||||
if options['verbose'] and processed_count % 100000 == 0:
|
||||
self.stdout.write(f'Procesados {processed_count} archivos, encontrados {len(orphaned_files)} huérfanos...')
|
||||
|
||||
# Liberar memoria cada 500k archivos
|
||||
if processed_count % 500000 == 0:
|
||||
gc.collect()
|
||||
|
||||
return orphaned_files
|
||||
|
||||
def _calculate_size_sample(self, orphaned_files, options):
|
||||
"""Calcula tamaño usando muestra si hay muchos archivos"""
|
||||
if len(orphaned_files) <= 1000:
|
||||
# Si hay pocos archivos, calcular tamaño exacto
|
||||
total_size = 0
|
||||
for file_path in orphaned_files:
|
||||
try:
|
||||
total_size += os.path.getsize(file_path)
|
||||
except OSError:
|
||||
pass
|
||||
return total_size
|
||||
else:
|
||||
# Si hay muchos archivos, usar muestra para estimar
|
||||
sample_size = min(1000, len(orphaned_files))
|
||||
sample_files = orphaned_files[:sample_size]
|
||||
sample_total = 0
|
||||
|
||||
for file_path in sample_files:
|
||||
try:
|
||||
sample_total += os.path.getsize(file_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Extrapolar al total
|
||||
avg_size = sample_total / sample_size if sample_size > 0 else 0
|
||||
estimated_total = avg_size * len(orphaned_files)
|
||||
|
||||
self.stdout.write(f'Tamaño estimado basado en muestra de {sample_size} archivos')
|
||||
return estimated_total
|
||||
|
||||
def _show_dry_run_results(self, orphaned_files, media_root):
|
||||
"""Muestra resultados del dry run"""
|
||||
self.stdout.write('\n--- MODO PRUEBA: Los siguientes archivos se eliminarían ---')
|
||||
|
||||
show_limit = 20
|
||||
for i, file_path in enumerate(sorted(orphaned_files)):
|
||||
if i >= show_limit:
|
||||
remaining = len(orphaned_files) - show_limit
|
||||
self.stdout.write(f' ... y {remaining} archivos más')
|
||||
break
|
||||
relative_path = os.path.relpath(file_path, media_root)
|
||||
self.stdout.write(f' - {relative_path}')
|
||||
|
||||
def _delete_files(self, orphaned_files, media_root, options):
|
||||
"""Elimina archivos con confirmación"""
|
||||
confirm = input(
|
||||
f'\n¿Estás seguro de que quieres eliminar {len(orphaned_files)} archivos? (s/N): '
|
||||
)
|
||||
|
||||
if confirm.lower() not in ['s', 'si', 'sí', 'y', 'yes']:
|
||||
self.stdout.write('Operación cancelada')
|
||||
return
|
||||
|
||||
deleted_count = 0
|
||||
deleted_size = 0
|
||||
|
||||
for i, file_path in enumerate(orphaned_files):
|
||||
try:
|
||||
file_size = os.path.getsize(file_path)
|
||||
os.remove(file_path)
|
||||
deleted_count += 1
|
||||
deleted_size += file_size
|
||||
|
||||
if options['verbose'] and deleted_count % 1000 == 0:
|
||||
progress = (i + 1) / len(orphaned_files) * 100
|
||||
self.stdout.write(f'Progreso: {progress:.1f}% ({deleted_count} eliminados)')
|
||||
|
||||
except OSError as e:
|
||||
relative_path = os.path.relpath(file_path, media_root)
|
||||
self.stdout.write(
|
||||
self.style.ERROR(f'Error eliminando {relative_path}: {e}')
|
||||
)
|
||||
|
||||
deleted_mb = deleted_size / (1024 * 1024)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f'Eliminados {deleted_count} archivos huérfanos ({deleted_mb:.2f} MB)'
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.2.3 on 2025-10-02 00:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('customs', '0010_alter_pedimento_contribuyente'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cove',
|
||||
name='acuse_cove_descargado',
|
||||
field=models.BooleanField(default=False, help_text='Indica si el acuse de la cove ha sido descargado'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cove',
|
||||
name='cove_descargado',
|
||||
field=models.BooleanField(default=False, help_text='Indica si la cove ha sido descargada'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='edocument',
|
||||
name='acuse_descargado',
|
||||
field=models.BooleanField(default=False, help_text='Indica si el acuse del e-documento ha sido descargado'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='edocument',
|
||||
name='edocument_descargado',
|
||||
field=models.BooleanField(default=False, help_text='Indica si el e-documento ha sido descargado'),
|
||||
),
|
||||
]
|
||||
31
api/customs/migrations/0012_partida.py
Normal file
31
api/customs/migrations/0012_partida.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 5.2.3 on 2025-10-03 01:18
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('customs', '0011_cove_acuse_cove_descargado_cove_cove_descargado_and_more'),
|
||||
('organization', '0002_remove_organizacion_membretado_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Partida',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('numero_partida', models.PositiveIntegerField(help_text='Número de la partida dentro del pedimento')),
|
||||
('descargado', models.BooleanField(default=False, help_text='Indica si la partida ha sido descargada')),
|
||||
('organizacion', models.ForeignKey(help_text='Organización a la que pertenece la partida', on_delete=django.db.models.deletion.CASCADE, related_name='partidas', to='organization.organizacion')),
|
||||
('pedimento', models.ForeignKey(help_text='Pedimento asociado a la partida', on_delete=django.db.models.deletion.CASCADE, related_name='partidas', to='customs.pedimento')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Partida',
|
||||
'verbose_name_plural': 'Partidas',
|
||||
'db_table': 'partida',
|
||||
'ordering': ['pedimento', 'numero_partida'],
|
||||
},
|
||||
),
|
||||
]
|
||||
17
api/customs/migrations/0013_alter_partida_unique_together.py
Normal file
17
api/customs/migrations/0013_alter_partida_unique_together.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.3 on 2025-10-03 02:12
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('customs', '0012_partida'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='partida',
|
||||
unique_together={('pedimento', 'numero_partida')},
|
||||
),
|
||||
]
|
||||
20
api/customs/migrations/0014_partida_created_at.py
Normal file
20
api/customs/migrations/0014_partida_created_at.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.2.3 on 2025-10-03 03:20
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('customs', '0013_alter_partida_unique_together'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='partida',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, help_text='Fecha de creación del registro'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
18
api/customs/migrations/0015_partida_updated_at.py
Normal file
18
api/customs/migrations/0015_partida_updated_at.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.3 on 2025-10-03 03:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('customs', '0014_partida_created_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='partida',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True, help_text='Fecha de última actualización del registro'),
|
||||
),
|
||||
]
|
||||
@@ -61,6 +61,24 @@ class Pedimento(models.Model):
|
||||
db_table = 'pedimento'
|
||||
ordering = ['pedimento']
|
||||
|
||||
class Partida(models.Model):
|
||||
pedimento = models.ForeignKey(Pedimento, on_delete=models.CASCADE, related_name='partidas', help_text="Pedimento asociado a la partida")
|
||||
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='partidas', help_text="Organización a la que pertenece la partida")
|
||||
numero_partida = models.PositiveIntegerField(help_text="Número de la partida dentro del pedimento")
|
||||
descargado = models.BooleanField(default=False, help_text="Indica si la partida ha sido descargada")
|
||||
created_at = models.DateTimeField(auto_now_add=True, help_text="Fecha de creación del registro")
|
||||
updated_at = models.DateTimeField(auto_now=True, help_text="Fecha de última actualización del registro")
|
||||
|
||||
def __str__(self):
|
||||
return f"Partida {self.numero_partida} del Pedimento {self.pedimento.pedimento}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Partida"
|
||||
verbose_name_plural = "Partidas"
|
||||
db_table = 'partida'
|
||||
ordering = ['pedimento', 'numero_partida']
|
||||
unique_together = ['pedimento', 'numero_partida'] # No puede existir el mismo número de partida para un pedimento
|
||||
|
||||
class EDocument(models.Model):
|
||||
pedimento = models.ForeignKey(Pedimento, on_delete=models.CASCADE, related_name='documentos', help_text="Pedimento asociado al documento")
|
||||
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='edocuments', help_text="Organización a la que pertenece el EDocument")
|
||||
@@ -71,6 +89,8 @@ class EDocument(models.Model):
|
||||
descripcion = models.CharField(max_length=200, blank=True, null=True, help_text="Descripción del documento")
|
||||
created_at = models.DateTimeField(auto_now_add=True, help_text="Fecha de creación del documento")
|
||||
updated_at = models.DateTimeField(auto_now=True, help_text="Fecha de última actualización del documento")
|
||||
edocument_descargado = models.BooleanField(default=False, help_text="Indica si el e-documento ha sido descargado")
|
||||
acuse_descargado = models.BooleanField(default=False, help_text="Indica si el acuse del e-documento ha sido descargado")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.descripcion} - {self.pedimento.pedimento}"
|
||||
@@ -87,6 +107,8 @@ class Cove(models.Model):
|
||||
numero_cove = models.CharField(max_length=20, unique=True, help_text="Número único de la cove")
|
||||
created_at = models.DateTimeField(auto_now_add=True, help_text="Fecha de creación de la cove")
|
||||
updated_at = models.DateTimeField(auto_now=True, help_text="Fecha de última actualización de la cove")
|
||||
cove_descargado = models.BooleanField(default=False, help_text="Indica si la cove ha sido descargada")
|
||||
acuse_cove_descargado = models.BooleanField(default=False, help_text="Indica si el acuse de la cove ha sido descargado")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.numero_cove} - {self.pedimento.pedimento}"
|
||||
|
||||
@@ -5,7 +5,8 @@ from api.customs.models import (
|
||||
ProcesamientoPedimento,
|
||||
EDocument,
|
||||
Cove,
|
||||
Importador
|
||||
Importador,
|
||||
Partida
|
||||
)
|
||||
from django.db import models
|
||||
from api.record.models import Document # Asegúrate de importar el modelo Documento
|
||||
@@ -41,7 +42,10 @@ class PedimentoSerializer(serializers.ModelSerializer):
|
||||
rep['documentos_peso_total'] = self.get_documentos_peso_total(instance)
|
||||
return rep
|
||||
|
||||
|
||||
class PartidaSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Partida
|
||||
fields = '__all__'
|
||||
|
||||
class TipoOperacionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
|
||||
@@ -71,8 +71,39 @@ def auditar_acuse_coves(organizacion_id):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@shared_task
|
||||
def auditar_acuse_edocuments(organizacion_id):
|
||||
# crear servicio individual para cada acuse de edocument faltante en microservicios
|
||||
pass
|
||||
pedimentos = obtener_pedimentos(organizacion_id)
|
||||
|
||||
for pedimento in pedimentos:
|
||||
acuses_descargados = pedimento.documents.filter(document_type=4)
|
||||
edocs = pedimento.documentos.all()
|
||||
|
||||
|
||||
|
||||
@shared_task
|
||||
def crear_partidas(organizacion_id):
|
||||
pedimentos = obtener_pedimentos(organizacion_id)
|
||||
|
||||
for pedimento in pedimentos:
|
||||
|
||||
crear_partidas_por_pedimento(pedimento.id)
|
||||
|
||||
@shared_task
|
||||
def crear_partidas_por_pedimento(pedimento_id):
|
||||
try:
|
||||
pedimento = Pedimento.objects.get(id=pedimento_id)
|
||||
except Pedimento.DoesNotExist:
|
||||
return
|
||||
|
||||
if pedimento.numero_partidas > pedimento.partidas.count():
|
||||
for i in range(1, pedimento.numero_partidas + 1):
|
||||
from api.customs.models import Partida
|
||||
Partida.objects.get_or_create(
|
||||
pedimento=pedimento,
|
||||
numero_partida=i,
|
||||
organizacion_id=pedimento.organizacion_id
|
||||
)
|
||||
@@ -185,8 +185,7 @@ def auditar_pedimento(organizacion_id):
|
||||
pedimento.remesas = xml_data.get('remesas')
|
||||
pedimento.tipo_operacion__id = xml_data.get('tipo_operacion')
|
||||
pedimento.fecha_pago = xml_data.get('fecha_pago')
|
||||
pedimento.pedimento_app = xml_data.get('fecha_pago')[2:4] + "-" + pedimento.aduana[:2] + "-" + pedimento.patente + "-" + pedimento.pedimento
|
||||
pedimento.save()
|
||||
pedimento.pedimento_app = xml_data.get('fecha_pago')[2:4] + "-" + pedimento.aduana[:2] + "-" + pedimento.patente + "-" + pedimento.pedimentodd
|
||||
|
||||
for edoc in xml_data.get('edocuments', []):
|
||||
EDocument.objects.get_or_create(
|
||||
|
||||
@@ -9,7 +9,8 @@ from .views import (
|
||||
ViewSetProcesamientoPedimento,
|
||||
ViewSetEDocument,
|
||||
ViewSetCove,
|
||||
ImportadorViewSet
|
||||
ImportadorViewSet,
|
||||
PartidaViewSet
|
||||
)
|
||||
# from .views import YourViewSet # Import your viewsets here
|
||||
|
||||
@@ -26,6 +27,7 @@ router.register(r'procesamientopedimentos', ViewSetProcesamientoPedimento, basen
|
||||
router.register(r'edocuments', ViewSetEDocument, basename='EDocument')
|
||||
router.register(r'coves', ViewSetCove, basename='Cove')
|
||||
router.register(r'importadores', ImportadorViewSet, basename='Importador')
|
||||
router.register(r'partidas', PartidaViewSet, basename='Partida')
|
||||
|
||||
# Import your viewsets here
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ from api.customs.models import (
|
||||
ProcesamientoPedimento,
|
||||
EDocument,
|
||||
Cove,
|
||||
Importador
|
||||
Importador,
|
||||
Partida
|
||||
)
|
||||
from api.customs.serializers import (
|
||||
PedimentoSerializer,
|
||||
@@ -29,7 +30,8 @@ from api.customs.serializers import (
|
||||
ProcesamientoPedimentoSerializer,
|
||||
EDocumentSerializer,
|
||||
CoveSerializer,
|
||||
ImportadorSerializer
|
||||
ImportadorSerializer,
|
||||
PartidaSerializer
|
||||
|
||||
)
|
||||
from api.logger.mixins import LoggingMixin
|
||||
@@ -202,6 +204,36 @@ class ViewSetPedimento(LoggingMixin, viewsets.ModelViewSet, OrganizacionFiltrada
|
||||
|
||||
my_tags = ['Pedimentos']
|
||||
|
||||
class PartidaViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
ViewSet for Partida model.
|
||||
Permite filtrar por:
|
||||
- pedimento: UUID del pedimento (query parameter principal)
|
||||
- pedimento__id: UUID del pedimento (alternativo)
|
||||
|
||||
Ejemplo: GET /api/partidas/?pedimento=6782d22e-5e97-4efc-87c9-bd8497c8ac7e
|
||||
"""
|
||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
||||
queryset = Partida.objects.all()
|
||||
serializer_class = PartidaSerializer
|
||||
pagination_class = CustomPagination
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = {
|
||||
'pedimento': ['exact'], # Filtro directo por UUID del pedimento
|
||||
'pedimento__id': ['exact'], # Filtro alternativo
|
||||
'numero_partida': ['exact', 'gte', 'lte'], # Filtros por número de partida
|
||||
'descargado': ['exact'], # Filtro por estado de descarga
|
||||
'created_at': ['exact', 'gte', 'lte'], # Filtros por fecha de creación
|
||||
'updated_at': ['exact', 'gte', 'lte'] # Filtros por fecha de actualización
|
||||
}
|
||||
search_fields = ['pedimento__pedimento', 'pedimento__pedimento_app']
|
||||
ordering_fields = ['numero_partida', 'pedimento__pedimento', 'id', 'created_at', 'updated_at']
|
||||
ordering = ['numero_partida'] # Ordenar por número de partida por defecto
|
||||
|
||||
my_tags = ['Partidas']
|
||||
|
||||
|
||||
|
||||
class ViewSetTipoOperacion(LoggingMixin, viewsets.ModelViewSet):
|
||||
"""
|
||||
ViewSet for TipoOperacion model.
|
||||
|
||||
0
api/tasks/__init__.py
Normal file
0
api/tasks/__init__.py
Normal file
3
api/tasks/admin.py
Normal file
3
api/tasks/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
api/tasks/apps.py
Normal file
6
api/tasks/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TasksConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'api.tasks'
|
||||
28
api/tasks/migrations/0001_initial.py
Normal file
28
api/tasks/migrations/0001_initial.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.2.3 on 2025-10-02 15:04
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.db.models.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('customs', '0011_cove_acuse_cove_descargado_cove_cove_descargado_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Task',
|
||||
fields=[
|
||||
('task_id', models.UUIDField(default=django.db.models.fields.UUIDField, primary_key=True, serialize=False)),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('message', models.TextField()),
|
||||
('status', models.CharField(max_length=50)),
|
||||
('result', models.TextField(blank=True, null=True)),
|
||||
('pedimento', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='customs.pedimento')),
|
||||
],
|
||||
),
|
||||
]
|
||||
21
api/tasks/migrations/0002_task_organizacion.py
Normal file
21
api/tasks/migrations/0002_task_organizacion.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.2.3 on 2025-10-02 15:09
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('organization', '0002_remove_organizacion_membretado_and_more'),
|
||||
('tasks', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='task',
|
||||
name='organizacion',
|
||||
field=models.ForeignKey(default='', on_delete=django.db.models.deletion.CASCADE, to='organization.organizacion'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
0
api/tasks/migrations/__init__.py
Normal file
0
api/tasks/migrations/__init__.py
Normal file
11
api/tasks/models.py
Normal file
11
api/tasks/models.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
class Task(models.Model):
|
||||
task_id = models.UUIDField(primary_key=True, default=models.UUIDField)
|
||||
pedimento = models.ForeignKey('customs.Pedimento', on_delete=models.CASCADE)
|
||||
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE)
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
message = models.TextField()
|
||||
status = models.CharField(max_length=50)
|
||||
result = models.TextField(null=True, blank=True)
|
||||
7
api/tasks/serializers.py
Normal file
7
api/tasks/serializers.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from rest_framework import serializers
|
||||
from .models import Task
|
||||
|
||||
class TaskSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Task
|
||||
fields = '__all__'
|
||||
3
api/tasks/tests.py
Normal file
3
api/tasks/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
10
api/tasks/urls.py
Normal file
10
api/tasks/urls.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import TaskViewSet
|
||||
from django.urls import path, include
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'tasks', TaskViewSet)
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
10
api/tasks/views.py
Normal file
10
api/tasks/views.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.shortcuts import render
|
||||
from rest_framework import viewsets
|
||||
from .models import Task
|
||||
from .serializers import TaskSerializer
|
||||
# Create your views here.
|
||||
|
||||
|
||||
class TaskViewSet(viewsets.ModelViewSet):
|
||||
queryset = Task.objects.all()
|
||||
serializer_class = TaskSerializer
|
||||
61
cleanup_all_media.sh
Normal file
61
cleanup_all_media.sh
Normal file
@@ -0,0 +1,61 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Iniciando limpieza completa de archivos media huérfanos..."
|
||||
|
||||
# Función para ejecutar limpieza
|
||||
cleanup_batch() {
|
||||
echo "Ejecutando limpieza de lote..."
|
||||
docker exec -it EFC_backend_dev bash -c "cd /app && python manage.py cleanup_media_fast --quick-scan --verbose" 2>/dev/null
|
||||
return $?
|
||||
}
|
||||
|
||||
# Contador de iteraciones
|
||||
iteration=1
|
||||
total_cleaned=0
|
||||
|
||||
while true; do
|
||||
echo "=== Iteración $iteration ==="
|
||||
|
||||
# Ejecutar dry-run para ver si hay archivos huérfanos
|
||||
result=$(docker exec -it EFC_backend_dev bash -c "cd /app && python manage.py cleanup_media_fast --quick-scan --dry-run" 2>/dev/null)
|
||||
|
||||
# Extraer número de archivos huérfanos
|
||||
orphaned_count=$(echo "$result" | grep -o "Archivos huérfanos encontrados: [0-9]*" | grep -o "[0-9]*")
|
||||
|
||||
if [ -z "$orphaned_count" ] || [ "$orphaned_count" -eq 0 ]; then
|
||||
echo "✅ No se encontraron más archivos huérfanos."
|
||||
echo "🎉 Limpieza completa finalizada después de $iteration iteraciones."
|
||||
echo "📊 Total estimado de archivos eliminados: $total_cleaned"
|
||||
break
|
||||
fi
|
||||
|
||||
echo "📁 Encontrados $orphaned_count archivos huérfanos en esta iteración"
|
||||
|
||||
# Preguntar confirmación en la primera iteración
|
||||
if [ $iteration -eq 1 ]; then
|
||||
echo "¿Continuar con la limpieza automática? (s/N):"
|
||||
read -r response
|
||||
if [[ ! "$response" =~ ^[sS]([iI]|í)?$ ]]; then
|
||||
echo "❌ Limpieza cancelada por el usuario."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ejecutar limpieza
|
||||
cleanup_result=$(docker exec -it EFC_backend_dev bash -c "cd /app && echo 's' | python manage.py cleanup_media_fast --quick-scan --verbose" 2>/dev/null)
|
||||
|
||||
# Extraer archivos eliminados
|
||||
deleted_count=$(echo "$cleanup_result" | grep -o "Eliminados [0-9]* archivos" | grep -o "[0-9]*")
|
||||
|
||||
if [ -n "$deleted_count" ]; then
|
||||
total_cleaned=$((total_cleaned + deleted_count))
|
||||
echo "✅ Eliminados $deleted_count archivos en esta iteración"
|
||||
fi
|
||||
|
||||
iteration=$((iteration + 1))
|
||||
|
||||
# Pausa pequeña para evitar sobrecarga
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "🏁 Proceso completado exitosamente."
|
||||
@@ -133,6 +133,7 @@ OWN_APPS = [
|
||||
'api.notificaciones',
|
||||
'api.reports',
|
||||
'api.cards',
|
||||
'api.tasks',
|
||||
]
|
||||
|
||||
INSTALLED_APPS = BASE_APPS + THIRD_APPS + OWN_APPS
|
||||
|
||||
@@ -50,6 +50,7 @@ urlpatterns = [
|
||||
path('api/v1/notificaciones/', include('api.notificaciones.urls')), # Notificaciones app
|
||||
path('api/v1/cards/', include('api.cards.urls')), # Cards app
|
||||
path('api/v1/reports/', include('api.reports.urls')), # Reports app
|
||||
path('api/v1/tasks/', include('api.tasks.urls')), # Tasks app
|
||||
]
|
||||
# En producción, los archivos media son servidos por Nginx
|
||||
if settings.DEBUG:
|
||||
|
||||
Reference in New Issue
Block a user