Mudanza de repo
This commit is contained in:
0
api/record/__init__.py
Normal file
0
api/record/__init__.py
Normal file
29
api/record/admin.py
Normal file
29
api/record/admin.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from django.contrib import admin
|
||||
from .models import Document, DocumentType, Fuente
|
||||
|
||||
# Register your models here.
|
||||
|
||||
class DocumentAdmin(admin.ModelAdmin):
|
||||
model = Document
|
||||
list_display = ('id', 'pedimento', 'archivo', 'size', 'extension', 'created_at', 'updated_at')
|
||||
search_fields = ('pedimento.pedimento', 'archivo')
|
||||
list_filter = ('created_at', 'updated_at')
|
||||
|
||||
class DocumentTypeAdmin(admin.ModelAdmin):
|
||||
model = DocumentType
|
||||
list_display = ('id', 'nombre', 'descripcion')
|
||||
search_fields = ('nombre',)
|
||||
ordering = ('nombre',)
|
||||
|
||||
|
||||
class FuenteAdmin(admin.ModelAdmin):
|
||||
model = Fuente
|
||||
list_display = ('id', 'nombre', 'descripcion')
|
||||
search_fields = ('nombre',)
|
||||
ordering = ('nombre',)
|
||||
|
||||
|
||||
|
||||
admin.site.register(Document, DocumentAdmin)
|
||||
admin.site.register(DocumentType, DocumentTypeAdmin)
|
||||
admin.site.register(Fuente, FuenteAdmin)
|
||||
6
api/record/apps.py
Normal file
6
api/record/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class RecordConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'api.record'
|
||||
52
api/record/migrations/0001_initial.py
Normal file
52
api/record/migrations/0001_initial.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# Generated by Django 5.2.3 on 2025-07-14 16:14
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('customs', '0001_initial'),
|
||||
('organization', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DocumentType',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('nombre', models.CharField(max_length=100, unique=True)),
|
||||
('descripcion', models.TextField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Tipo de Documento',
|
||||
'verbose_name_plural': 'Tipos de Documento',
|
||||
'db_table': 'document_type',
|
||||
'ordering': ['nombre'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Document',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('archivo', models.FileField(max_length=400, upload_to='documents/')),
|
||||
('extension', models.CharField(blank=True, max_length=60, null=True)),
|
||||
('size', models.PositiveIntegerField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('organizacion', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='organization.organizacion')),
|
||||
('pedimento', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='customs.pedimento')),
|
||||
('document_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='record.documenttype')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Document',
|
||||
'verbose_name_plural': 'Documents',
|
||||
'db_table': 'document',
|
||||
'ordering': ['created_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
33
api/record/migrations/0002_fuente_document_fuente.py
Normal file
33
api/record/migrations/0002_fuente_document_fuente.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.2.3 on 2025-08-12 14:01
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('record', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Fuente',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('nombre', models.CharField(max_length=100, unique=True)),
|
||||
('descripcion', models.TextField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Fuente',
|
||||
'verbose_name_plural': 'Fuentes',
|
||||
'db_table': 'fuente',
|
||||
'ordering': ['nombre'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='document',
|
||||
name='fuente',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='record.fuente'),
|
||||
),
|
||||
]
|
||||
0
api/record/migrations/__init__.py
Normal file
0
api/record/migrations/__init__.py
Normal file
97
api/record/models.py
Normal file
97
api/record/models.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from django.db import models
|
||||
import uuid
|
||||
|
||||
from api.organization.models import UsoAlmacenamiento
|
||||
|
||||
# Create your models here.
|
||||
|
||||
class Document(models.Model):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
|
||||
organizacion = models.ForeignKey('organization.Organizacion', on_delete=models.CASCADE, related_name='documents')
|
||||
pedimento = models.ForeignKey('customs.Pedimento', on_delete=models.CASCADE, related_name='documents')
|
||||
archivo = models.FileField(upload_to='documents/', max_length=400)
|
||||
document_type = models.ForeignKey('DocumentType', on_delete=models.CASCADE, related_name='documents', blank=True, null=True)
|
||||
extension = models.CharField(max_length=60, blank=True, null=True)
|
||||
size = models.PositiveIntegerField()
|
||||
fuente = models.ForeignKey('Fuente', on_delete=models.CASCADE, related_name='documents', blank=True, null=True)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
is_new = self._state.adding
|
||||
|
||||
# Usar get_or_create en lugar de get para manejar el caso cuando no existe
|
||||
uso_almacenamiento, created = UsoAlmacenamiento.objects.get_or_create(
|
||||
organizacion=self.organizacion,
|
||||
defaults={'espacio_utilizado': 0}
|
||||
)
|
||||
|
||||
almacenamiento_licencia_bytes = self.organizacion.licencia.almacenamiento * 1024 ** 3
|
||||
|
||||
if is_new:
|
||||
if uso_almacenamiento.espacio_utilizado + self.size > almacenamiento_licencia_bytes:
|
||||
raise ValueError("La organización no tiene suficiente espacio de almacenamiento disponible")
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
uso_almacenamiento.espacio_utilizado += self.size
|
||||
uso_almacenamiento.save()
|
||||
else:
|
||||
old_file = Document.objects.get(pk=self.pk)
|
||||
if old_file.size != self.size:
|
||||
diferencia = self.size - old_file.size
|
||||
if uso_almacenamiento.espacio_utilizado + diferencia > almacenamiento_licencia_bytes:
|
||||
raise ValueError("No hay suficiente espacio para la actualización")
|
||||
|
||||
uso_almacenamiento.espacio_utilizado += diferencia
|
||||
uso_almacenamiento.save()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
def delete(self, *args, **kwargs):
|
||||
# Usar get_or_create aquí también por si acaso
|
||||
uso_almacenamiento, created = UsoAlmacenamiento.objects.get_or_create(
|
||||
organizacion=self.organizacion,
|
||||
defaults={'espacio_utilizado': 0}
|
||||
)
|
||||
|
||||
uso_almacenamiento.espacio_utilizado -= self.size
|
||||
uso_almacenamiento.save()
|
||||
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.archivo.name}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Document"
|
||||
verbose_name_plural = "Documents"
|
||||
db_table = 'document'
|
||||
ordering = ['created_at']
|
||||
|
||||
class DocumentType(models.Model):
|
||||
nombre = models.CharField(max_length=100, unique=True)
|
||||
descripcion = models.TextField(blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.nombre
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Tipo de Documento"
|
||||
verbose_name_plural = "Tipos de Documento"
|
||||
db_table = 'document_type'
|
||||
ordering = ['nombre']
|
||||
|
||||
class Fuente(models.Model):
|
||||
nombre = models.CharField(max_length=100, unique=True)
|
||||
descripcion = models.TextField(blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.nombre
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Fuente"
|
||||
verbose_name_plural = "Fuentes"
|
||||
db_table = 'fuente'
|
||||
ordering = ['nombre']
|
||||
39
api/record/serializers.py
Normal file
39
api/record/serializers.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import Document, Fuente, DocumentType
|
||||
|
||||
|
||||
|
||||
from api.customs.models import Pedimento
|
||||
|
||||
class DocumentSerializer(serializers.ModelSerializer):
|
||||
pedimento_numero = serializers.SerializerMethodField(read_only=True)
|
||||
pedimento = serializers.PrimaryKeyRelatedField(queryset=Pedimento.objects.all())
|
||||
|
||||
class Meta:
|
||||
model = Document
|
||||
fields = ('id', 'organizacion', 'pedimento', 'pedimento_numero', 'archivo', 'document_type', 'size', 'extension', 'fuente','created_at', 'updated_at')
|
||||
read_only_fields = ('id', 'size', 'extension', 'created_at', 'updated_at', 'pedimento_numero')
|
||||
|
||||
def get_pedimento_numero(self, obj):
|
||||
if obj.pedimento:
|
||||
return obj.pedimento.pedimento_app
|
||||
return None
|
||||
|
||||
def validate_archivo(self, value):
|
||||
"""Validar que se proporcione un archivo"""
|
||||
if not value:
|
||||
raise serializers.ValidationError("Se requiere un archivo para subir")
|
||||
return value
|
||||
|
||||
class FuenteSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Fuente
|
||||
fields = ('id', 'nombre', 'descripcion')
|
||||
read_only_fields = ('id','nombre', 'descripcion')
|
||||
|
||||
class DocumentTypeSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = DocumentType
|
||||
fields = ('id', 'nombre', 'descripcion')
|
||||
read_only_fields = ('id', 'nombre', 'descripcion')
|
||||
97
api/record/tests.py
Normal file
97
api/record/tests.py
Normal file
@@ -0,0 +1,97 @@
|
||||
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase, APIClient
|
||||
from rest_framework import status
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from api.organization.models import Organizacion, UsoAlmacenamiento
|
||||
from api.cuser.models import CustomUser
|
||||
from api.customs.models import Pedimento
|
||||
from .models import Document
|
||||
import io
|
||||
|
||||
class DocumentViewSetTests(APITestCase):
|
||||
def setUp(self):
|
||||
self.org = Organizacion.objects.create(nombre="OrgTest", is_active=True, is_verified=True)
|
||||
self.pedimento = Pedimento.objects.create(organizacion=self.org, numero="123456")
|
||||
self.admin = CustomUser.objects.create_user(username="admin", password="adminpass", organizacion=self.org)
|
||||
self.admin.groups.create(name="admin")
|
||||
self.superuser = CustomUser.objects.create_superuser(username="superuser", password="superpass")
|
||||
self.client = APIClient()
|
||||
|
||||
def test_list_documents_only_own_org(self):
|
||||
doc1 = Document.objects.create(organizacion=self.org, pedimento=self.pedimento, archivo="documents/test1.pdf", size=100, extension="pdf")
|
||||
org2 = Organizacion.objects.create(nombre="OrgTest2", is_active=True, is_verified=True)
|
||||
ped2 = Pedimento.objects.create(organizacion=org2, numero="654321")
|
||||
Document.objects.create(organizacion=org2, pedimento=ped2, archivo="documents/test2.pdf", size=200, extension="pdf")
|
||||
self.client.force_authenticate(user=self.admin)
|
||||
url = reverse('Document-list')
|
||||
response = self.client.get(url)
|
||||
ids = [d['id'] for d in response.data]
|
||||
self.assertIn(str(doc1.id), ids)
|
||||
self.assertEqual(len(ids), 1)
|
||||
|
||||
def test_create_document_success(self):
|
||||
self.client.force_authenticate(user=self.admin)
|
||||
file_content = b"dummy pdf content"
|
||||
archivo = SimpleUploadedFile("test.pdf", file_content, content_type="application/pdf")
|
||||
url = reverse('Document-list')
|
||||
data = {
|
||||
"pedimento": str(self.pedimento.id),
|
||||
"archivo": archivo,
|
||||
"size": len(file_content),
|
||||
"extension": "pdf"
|
||||
}
|
||||
response = self.client.post(url, data, format='multipart')
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
doc = Document.objects.get(id=response.data['id'])
|
||||
self.assertEqual(doc.organizacion, self.org)
|
||||
|
||||
def test_update_document_size(self):
|
||||
doc = Document.objects.create(organizacion=self.org, pedimento=self.pedimento, archivo="documents/test1.pdf", size=100, extension="pdf")
|
||||
self.client.force_authenticate(user=self.admin)
|
||||
url = reverse('Document-detail', args=[doc.id])
|
||||
file_content = b"new content"
|
||||
archivo = SimpleUploadedFile("test2.pdf", file_content, content_type="application/pdf")
|
||||
data = {
|
||||
"archivo": archivo,
|
||||
"size": len(file_content),
|
||||
"extension": "pdf"
|
||||
}
|
||||
response = self.client.patch(url, data, format='multipart')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
doc.refresh_from_db()
|
||||
self.assertEqual(doc.size, len(file_content))
|
||||
|
||||
def test_delete_document_frees_storage(self):
|
||||
doc = Document.objects.create(organizacion=self.org, pedimento=self.pedimento, archivo="documents/test1.pdf", size=100, extension="pdf")
|
||||
UsoAlmacenamiento.objects.create(organizacion=self.org, espacio_utilizado=100)
|
||||
self.client.force_authenticate(user=self.admin)
|
||||
url = reverse('Document-detail', args=[doc.id])
|
||||
response = self.client.delete(url)
|
||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||
uso = UsoAlmacenamiento.objects.get(organizacion=self.org)
|
||||
self.assertEqual(uso.espacio_utilizado, 0)
|
||||
|
||||
def test_permission_denied_for_other_org(self):
|
||||
org2 = Organizacion.objects.create(nombre="OrgTest2", is_active=True, is_verified=True)
|
||||
ped2 = Pedimento.objects.create(organizacion=org2, numero="654321")
|
||||
doc2 = Document.objects.create(organizacion=org2, pedimento=ped2, archivo="documents/test2.pdf", size=200, extension="pdf")
|
||||
self.client.force_authenticate(user=self.admin)
|
||||
url = reverse('Document-detail', args=[doc2.id])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_superuser_can_access_all(self):
|
||||
org2 = Organizacion.objects.create(nombre="OrgTest2", is_active=True, is_verified=True)
|
||||
ped2 = Pedimento.objects.create(organizacion=org2, numero="654321")
|
||||
doc2 = Document.objects.create(organizacion=org2, pedimento=ped2, archivo="documents/test2.pdf", size=200, extension="pdf")
|
||||
self.client.force_authenticate(user=self.superuser)
|
||||
url = reverse('Document-detail', args=[doc2.id])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_protected_download_requires_auth(self):
|
||||
doc = Document.objects.create(organizacion=self.org, pedimento=self.pedimento, archivo="documents/test1.pdf", size=100, extension="pdf")
|
||||
url = reverse('descargar-documento', args=[doc.id])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
27
api/record/urls.py
Normal file
27
api/record/urls.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# This file defines the URL patterns for the customs app in a Django project.
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
|
||||
# import necessary viewsets
|
||||
# from .views import YourViewSet # Import your viewsets here
|
||||
from .views import DocumentViewSet, ProtectedDocumentDownloadView, BulkDownloadZipView, GetFuenteView, DocumentTypeView
|
||||
# Create a router and register your viewsets with it
|
||||
|
||||
router = DefaultRouter()
|
||||
|
||||
# Register your viewsets with the router here
|
||||
# Example:
|
||||
# from .views import MyViewSet
|
||||
# router.register(r'myviewset', MyViewSet, basename='myviewset')
|
||||
router.register(r'documents', DocumentViewSet, basename='Document')
|
||||
|
||||
|
||||
# No registres ProtectedDocumentDownloadView en el router, solo como path individual
|
||||
|
||||
urlpatterns = [
|
||||
path('documents/bulk-download/', BulkDownloadZipView.as_view(), name='bulk-download-documents'),
|
||||
path('documents/descargar/<uuid:pk>/', ProtectedDocumentDownloadView.as_view(), name='descargar-documento'),
|
||||
path('fuente/', GetFuenteView.as_view(), name='get-fuente'),
|
||||
path('document-type/', DocumentTypeView.as_view(), name='document-type-list-create'),
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
266
api/record/views.py
Normal file
266
api/record/views.py
Normal file
@@ -0,0 +1,266 @@
|
||||
from django.shortcuts import render
|
||||
from django.http import FileResponse, Http404
|
||||
from django.db import transaction
|
||||
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from .serializers import DocumentSerializer, FuenteSerializer, DocumentTypeSerializer
|
||||
from .models import Document, Fuente, DocumentType
|
||||
from api.organization.models import UsoAlmacenamiento
|
||||
from io import BytesIO
|
||||
import zipfile
|
||||
from django.utils.text import slugify
|
||||
from django.http import HttpResponse
|
||||
from rest_framework.decorators import action
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
from core.permissions import (
|
||||
IsSameOrganization,
|
||||
IsSameOrganizationDeveloper,
|
||||
IsSameOrganizationAndAdmin,
|
||||
IsSuperUser
|
||||
)
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from mixins.filtrado_organizacion import DocumentosFiltradosMixin
|
||||
|
||||
class CustomPagination(PageNumberPagination):
|
||||
|
||||
"""
|
||||
Paginación personalizada con parámetros flexibles
|
||||
- Si no se especifica page_size, devuelve todos los resultados (sin paginación)
|
||||
- Si se especifica page_size, usa paginación normal
|
||||
"""
|
||||
page_size = None # Por defecto 10000 por página
|
||||
page_size_query_param = 'page_size'
|
||||
max_page_size = 10000 # Límite máximo de seguridad
|
||||
page_query_param = 'page'
|
||||
|
||||
# Usar la paginación estándar de DRF, pero con page_size=10000 por defecto y máximo 10000
|
||||
|
||||
# Create your views here.
|
||||
class DocumentViewSet(viewsets.ModelViewSet, DocumentosFiltradosMixin):
|
||||
"""
|
||||
ViewSet for Document model.
|
||||
"""
|
||||
permission_classes = [IsAuthenticated & (IsSuperUser | IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper )]
|
||||
model = Document
|
||||
|
||||
pagination_class = CustomPagination
|
||||
serializer_class = DocumentSerializer
|
||||
# Habilitar filtro por pedimento (UUID) y pedimento_numero (campo pedimento del modelo relacionado)
|
||||
filterset_fields = ['extension', 'size', 'document_type', 'pedimento', 'pedimento__pedimento']
|
||||
|
||||
# Puedes filtrar por pedimento usando: /api/record/documents/?pedimento=<id> o /api/record/documents/?pedimento__pedimento=<numero>
|
||||
# Ejemplo: /api/record/documents/?pedimento_numero=12345678
|
||||
my_tags = ['Documents']
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self.get_queryset_filtrado_por_organizacion()
|
||||
pedimento_numero = self.request.query_params.get('pedimento_numero')
|
||||
if pedimento_numero:
|
||||
queryset = queryset.filter(pedimento__pedimento_app=pedimento_numero)
|
||||
|
||||
return queryset
|
||||
|
||||
@transaction.atomic
|
||||
def perform_create(self, serializer):
|
||||
user = self.request.user
|
||||
if not user.is_authenticated or not hasattr(user, 'organizacion'):
|
||||
raise ValidationError({"error": "Usuario no autenticado o sin organización"})
|
||||
|
||||
archivo = self.request.FILES.get('archivo')
|
||||
if not archivo:
|
||||
raise ValidationError({"archivo": "Se requiere un archivo para subir"})
|
||||
|
||||
# Permitir que el superusuario especifique la organización
|
||||
organizacion = user.organizacion
|
||||
|
||||
if self.request.user.is_superuser:
|
||||
organizacion = serializer.validated_data.get('organizacion', organizacion)
|
||||
|
||||
uso = UsoAlmacenamiento.objects.select_for_update().get_or_create(
|
||||
organizacion=organizacion,
|
||||
defaults={'espacio_utilizado': 0}
|
||||
)[0]
|
||||
|
||||
# Calcular límites
|
||||
max_almacenamiento_bytes = organizacion.licencia.almacenamiento * 1024 ** 3
|
||||
nuevo_espacio_utilizado = uso.espacio_utilizado + archivo.size
|
||||
|
||||
# Validación estricta con raise ValidationError
|
||||
if nuevo_espacio_utilizado > max_almacenamiento_bytes:
|
||||
espacio_faltante = nuevo_espacio_utilizado - max_almacenamiento_bytes
|
||||
raise ValidationError({
|
||||
"error": "Espacio de almacenamiento insuficiente",
|
||||
"detalle": {
|
||||
"espacio_faltante_gb": round(espacio_faltante / (1024 ** 3), 2),
|
||||
"espacio_utilizado_gb": round(uso.espacio_utilizado / (1024 ** 3), 2),
|
||||
"limite_gb": organizacion.licencia.almacenamiento,
|
||||
"archivo_gb": round(archivo.size / (1024 ** 3), 4)
|
||||
},
|
||||
"codigo": "storage_limit_exceeded"
|
||||
}, code=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Guardar documento y actualizar espacio atómicamente
|
||||
documento = serializer.save(
|
||||
organizacion=organizacion,
|
||||
size=archivo.size,
|
||||
extension=archivo.name.split('.')[-1].lower()
|
||||
)
|
||||
|
||||
uso.espacio_utilizado = nuevo_espacio_utilizado
|
||||
uso.save()
|
||||
|
||||
@transaction.atomic
|
||||
def perform_update(self, serializer):
|
||||
instance = self.get_object()
|
||||
new_file = self.request.FILES.get('archivo')
|
||||
|
||||
if new_file:
|
||||
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
||||
raise ValidationError({"error": "Usuario no autenticado o sin organización"})
|
||||
|
||||
organizacion = self.request.user.organizacion
|
||||
uso = UsoAlmacenamiento.objects.select_for_update().get(organizacion=organizacion)
|
||||
|
||||
diferencia = new_file.size - instance.size
|
||||
max_almacenamiento_bytes = organizacion.licencia.almacenamiento * 1024 ** 3
|
||||
nuevo_espacio_utilizado = uso.espacio_utilizado + diferencia
|
||||
|
||||
if nuevo_espacio_utilizado > max_almacenamiento_bytes:
|
||||
espacio_faltante = nuevo_espacio_utilizado - max_almacenamiento_bytes
|
||||
raise ValidationError({
|
||||
"error": "Espacio insuficiente para actualizar el archivo",
|
||||
"detalle": {
|
||||
"espacio_faltante_bytes": espacio_faltante,
|
||||
"tamaño_nuevo_archivo": new_file.size,
|
||||
"tamaño_anterior_archivo": instance.size
|
||||
},
|
||||
"codigo": "update_storage_limit_exceeded"
|
||||
}, code=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Actualizar documento y espacio
|
||||
serializer.save(size=new_file.size)
|
||||
uso.espacio_utilizado = nuevo_espacio_utilizado
|
||||
uso.save()
|
||||
else:
|
||||
serializer.save()
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
# Restar el espacio al eliminar
|
||||
uso = UsoAlmacenamiento.objects.get(organizacion=instance.organizacion)
|
||||
uso.espacio_utilizado -= instance.size
|
||||
uso.save()
|
||||
instance.delete()
|
||||
|
||||
class ProtectedDocumentDownloadView(APIView, DocumentosFiltradosMixin):
|
||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
||||
serializer_class = DocumentSerializer
|
||||
model = Document
|
||||
my_tags = ['Documents']
|
||||
|
||||
def get_queryset(self):
|
||||
return self.get_queryset_filtrado_por_organizacion()
|
||||
|
||||
def get(self, request, pk):
|
||||
if not request.user.is_authenticated or not hasattr(request.user, 'organizacion'):
|
||||
raise Http404("Usuario no autenticado")
|
||||
|
||||
|
||||
try:
|
||||
doc = Document.objects.get(pk=pk)
|
||||
except Document.DoesNotExist:
|
||||
raise Http404("Documento no encontrado")
|
||||
|
||||
# Verifica que el usuario pertenece a la organización del documento
|
||||
|
||||
if self.request.user.is_superuser:
|
||||
return FileResponse(doc.archivo.open('rb'))
|
||||
|
||||
if doc.organizacion != request.user.organizacion:
|
||||
raise Http404("No autorizado")
|
||||
|
||||
return FileResponse(doc.archivo.open('rb'))
|
||||
|
||||
class BulkDownloadZipView(APIView):
|
||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
||||
my_tags = ['Documents']
|
||||
|
||||
def post(self, request):
|
||||
|
||||
if not request.user.is_authenticated or not hasattr(request.user, 'organizacion'):
|
||||
return Response({"error": "Usuario no autenticado o sin organización"}, status=401)
|
||||
|
||||
pks = request.data.get('document_ids', [])
|
||||
pedimento_nombre = request.data.get('pedimento_nombre', 'documentos')
|
||||
if not isinstance(pks, list) or not pks:
|
||||
return Response({"error": "Debe proporcionar una lista de IDs de documentos en 'document_ids'."}, status=400)
|
||||
|
||||
if self.request.user.is_superuser:
|
||||
docs = Document.objects.filter(pk__in=pks)
|
||||
else:
|
||||
docs = Document.objects.filter(pk__in=pks, organizacion=request.user.organizacion)
|
||||
|
||||
if docs.count() != len(pks):
|
||||
return Response({"error": "Uno o más documentos no existen o no pertenecen a su organización."}, status=404)
|
||||
|
||||
buffer = BytesIO()
|
||||
with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
||||
for doc in docs:
|
||||
# Usar solo el nombre del archivo sin descripcion
|
||||
file_name = slugify(doc.archivo.name.rsplit('/', 1)[-1].rsplit('.', 1)[0])
|
||||
ext = doc.archivo.name.split('.')[-1]
|
||||
zip_name = f"{file_name}.{ext}"
|
||||
doc.archivo.open('rb')
|
||||
zip_file.writestr(zip_name, doc.archivo.read())
|
||||
doc.archivo.close()
|
||||
|
||||
buffer.seek(0)
|
||||
safe_name = slugify(pedimento_nombre)
|
||||
response = HttpResponse(buffer, content_type='application/zip')
|
||||
response['Content-Disposition'] = f'attachment; filename={safe_name or "documentos"}.zip'
|
||||
|
||||
return response
|
||||
|
||||
class GetFuenteView(APIView):
|
||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
||||
serializer_class = FuenteSerializer
|
||||
my_tags = ['Fuente Documentos']
|
||||
|
||||
def get_queryset(self):
|
||||
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
||||
return Fuente.objects.none()
|
||||
return Fuente.objects.all()
|
||||
|
||||
def get(self, request):
|
||||
queryset = self.get_queryset()
|
||||
serializer = self.serializer_class(queryset, many=True)
|
||||
return Response(serializer.data, status=200)
|
||||
|
||||
class DocumentTypeView(APIView):
|
||||
permission_classes = [IsAuthenticated & (IsSameOrganization | IsSameOrganizationAndAdmin | IsSameOrganizationDeveloper | IsSuperUser)]
|
||||
serializer_class = DocumentTypeSerializer
|
||||
my_tags = ['Tipo de Documentos']
|
||||
|
||||
def get_queryset(self):
|
||||
if not self.request.user.is_authenticated or not hasattr(self.request.user, 'organizacion'):
|
||||
return DocumentType.objects.none()
|
||||
return DocumentType.objects.all()
|
||||
|
||||
def get(self, request):
|
||||
queryset = self.get_queryset()
|
||||
if not queryset.exists():
|
||||
return Response({"detail": "No hay tipos de documento disponibles."}, status=404)
|
||||
serializer = self.serializer_class(queryset, many=True)
|
||||
return Response(serializer.data, status=200)
|
||||
Reference in New Issue
Block a user