Mudanza de repo

This commit is contained in:
2025-09-22 18:43:29 -06:00
parent 26fe36ca52
commit d11d543bdc
193 changed files with 10998 additions and 0 deletions

0
api/record/__init__.py Normal file
View File

29
api/record/admin.py Normal file
View 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
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class RecordConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api.record'

View 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'],
},
),
]

View 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'),
),
]

View File

97
api/record/models.py Normal file
View 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
View 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
View 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
View 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
View 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)