Repository: Proyecto-ISPP/FISIOFIND
Pull request #427: Feat/subida imagenes y documentos
Author: login: pacomateos10
Labels: name: hu
description: Historia de usuario que describe una funcionalidad específica
name: HIGH
Created: 2025-04-02T06:14:22Z | Merged: 2025-04-05T11:16:31Z

**Descripción del cambio:**
Realizar la subida y eliminación de archivos en la nube
- Implementar una nueva función para poder subir varios archivos en un mismo id.
- Poder eliminar los archivos de la base de datos y del storage externo en la misma petición.

**Motivación e impacto:**
- Se ha completado la mitad del objetivo esperado, ya que falta obtener los archivos y poder actualizarlos.

**Instrucciones**
- Se deben hacer tests unitarios
- Se ha comprobado el correcto funcionamiento de la funcionalidad en hoppscotch.


Diff:
diff --git a/backend/fisio_find/appointment/exampledata.json b/backend/fisio_find/appointment/exampledata.json
index e4883237..ddb81a2b 100644
--- a/backend/fisio_find/appointment/exampledata.json
+++ b/backend/fisio_find/appointment/exampledata.json
@@ -1,4 +1,13 @@
 [
+    {
+        "model": "users.pricing",
+        "pk": 1,
+        "fields": {
+            "name": "Plan Básico",
+            "price": 50,
+            "video_limit": 15
+        }
+    },
     {
         "model": "users.specialization",
         "pk": 1,
@@ -170,7 +179,8 @@
             "specializations": [
                 1,
                 2
-            ]
+            ],
+            "plan": 1
         }
     },
     {
@@ -586,8 +596,22 @@
             },
             "patient": 5,
             "physiotherapist": 1,
-            "status": "booked",
+            "status": "finished",
             "alternatives": null
         }
+    },
+    {
+        "model": "treatments.treatment",
+        "pk": 1,
+        "fields":{
+            "physiotherapist": 1,
+            "patient": 5,
+            "start_time": "2025-03-12T10:00:00Z",
+            "end_time":"2025-03-12T11:00:00Z",
+            "homework": "Realizar ejercicios de estiramiento durante 15 minutos al día.",
+            "is_active": true,
+            "created_at": "2025-03-12T10:00:00Z",
+            "updated_at": "2025-03-12T10:00:00Z"
+        }
     }
 ]
\ No newline at end of file
diff --git a/backend/fisio_find/appointment/views.py b/backend/fisio_find/appointment/views.py
index eb015dc5..a4dc3a14 100644
--- a/backend/fisio_find/appointment/views.py
+++ b/backend/fisio_find/appointment/views.py
@@ -1,22 +1,18 @@
 from collections import defaultdict
-from datetime import datetime, timedelta
-from rest_framework import generics
+from datetime import datetime, timedelta, timezone
 from rest_framework.filters import SearchFilter, OrderingFilter
 from appointment.models import Appointment, Physiotherapist, Patient
 from appointment.serializers import AppointmentSerializer
 from rest_framework.permissions import IsAuthenticated
 from payment.views import cancel_payment_patient, cancel_payment_pyshio, create_payment_setup
 from users.permissions import IsPhysiotherapist, IsPatient, IsPhysioOrPatient
-from users.permissions import IsAdmin
 from rest_framework.decorators import api_view, permission_classes
 from rest_framework.response import Response
 from rest_framework import status
 from rest_framework.pagination import PageNumberPagination
 from django.core.exceptions import ValidationError
-from rest_framework.views import APIView
 from django.utils.timezone import make_aware, is_aware
 from django.utils.dateparse import parse_datetime
-from datetime import datetime, timezone, timedelta
 from appointment.emailUtils import send_appointment_email
 from django.core import signing
 from django.core.signing import BadSignature, SignatureExpired
@@ -33,6 +29,7 @@ class StandardResultsSetPagination(PageNumberPagination):
     page_size_query_param = 'page_size'
     max_page_size = 100
 
+
 def update_schedule(data):
     if isinstance(data, Appointment):
         physio_id = data.physiotherapist.id
@@ -40,11 +37,11 @@ def update_schedule(data):
         physio_id = data.get('physiotherapist')
     if not physio_id:
         return Response({"error": "Debes proporcionar un ID de fisioterapeuta"}, status=status.HTTP_400_BAD_REQUEST)
-    
+
     physiotherapist = Physiotherapist.objects.get(id=physio_id)
     if not physiotherapist:
         return Response({"error": "Fisioterapeuta no encontrado"}, status=status.HTTP_404_NOT_FOUND)
-    
+
     current_schedule = physiotherapist.schedule
     if not current_schedule:
         return Response({"error": "No se ha definido un horario para este fisioterapeuta"}, status=status.HTTP_404_NOT_FOUND)
@@ -70,7 +67,7 @@ def update_schedule(data):
 @permission_classes([IsAuthenticated, IsPatient])
 def create_appointment_patient(request):
     patient = request.user.patient
-    
+
     weekly_days = {
         "monday": "lunes",
         "tuesday": "martes",
@@ -89,21 +86,21 @@ def create_appointment_patient(request):
         physiotherapist = Physiotherapist.objects.get(id=data['physiotherapist'])
     except Physiotherapist.DoesNotExist:
         return Response({"error": "Fisioterapeuta no encontrado"}, status=status.HTTP_404_NOT_FOUND)
-    
+
     schedule = physiotherapist.schedule
     weekly_schedule = schedule.get('weekly_schedule', {})
     exceptions = schedule.get('exceptions', {})
-    
+
     selected_service = data.get('service',{})
     if selected_service == None or selected_service == {}:
         return Response({"error": "Debes de seleccionar un servicio"}, status=status.HTTP_400_BAD_REQUEST)
-    
+
     physio_services = physiotherapist.services
     try:
         selected_service = validate_service_with_questionary(selected_service, physio_services)
     except Exception:
         return Response({"error": "Debes de enviar un cuestionario válido"}, status=status.HTTP_400_BAD_REQUEST)
-    
+
     # Parsear fechas de la solicitud
     start_time = parse_datetime(data['start_time'])
     if not is_aware(start_time):
@@ -156,7 +153,6 @@ def create_appointment_patient(request):
             patient=appointment.patient,
             appointment=appointment
         )
-        
 
         return Response({'appointment_data': serializer.data, 'payment_data': payment_data}, status=status.HTTP_201_CREATED)
 
@@ -189,7 +185,7 @@ def create_appointment_physio(request):
             patient=appointment.patient,
             appointment=appointment
         )
-       
+
         return Response(AppointmentSerializer(appointment).data, status=status.HTTP_201_CREATED)
 
     return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -311,11 +307,10 @@ def edit_weekly_schedule(request):
                 if not _is_valid_time_range(start, end):
                     raise ValidationError(
                         f"El rango {start}-{end} no es válido. La hora de inicio debe ser anterior a la de fin.")
-                
+
         # Actualizar solo el weekly_schedule en el schedule existente
         current_schedule['weekly_schedule'] = weekly_schedule
 
-
         # Validar la estructura de exceptions
         if not isinstance(data, dict) or 'exceptions' not in data:
             raise ValidationError(
@@ -340,7 +335,7 @@ def edit_weekly_schedule(request):
                 date_obj = datetime.strptime(date_str, "%Y-%m-%d")
             except ValueError:
                 raise ValidationError(f"La fecha {date_str} no tiene el formato válido YYYY-MM-DD.")
-            
+
             if not isinstance(blocks, list):
                 raise ValidationError(f"Los bloques para la fecha {date_str} deben ser una lista.")
 
@@ -482,10 +477,10 @@ def update_appointment(request, appointment_id):
             slot_end = slot["end"]
             if slot_start == appointment_start_time and date == appointment.start_time.strftime("%Y-%m-%d"):
                 return Response({"error": f"No puedes agregar la fecha actual de la cita ({appointment_start_time}) en 'alternatives'"}, status=status.HTTP_400_BAD_REQUEST)
-            
+
             if slot_start >= slot_end:
                 return Response({"error": f"En {date}, la hora de inicio debe ser menor que la de fin"}, status=status.HTTP_400_BAD_REQUEST)
-            
+
             for physio_appointment in physio_appointments:
                 if date == physio_appointment["start_time"].split("T")[0]:
                     if physio_appointment["end_time"].split("T")[1][:5] > slot_start >= physio_appointment["start_time"].split("T")[1][:5] or physio_appointment["start_time"].split("T")[1][:5] < slot_end <= physio_appointment["end_time"].split("T")[1][:5]:
@@ -511,10 +506,6 @@ def update_appointment(request, appointment_id):
 
     if serializer.is_valid():
         serializer.save()
-        # if serializer.data['alternatives']:
-            # send_appointment_email(appointment.id, 'modified')
-        # elif serializer.data['status'] == "confirmed":
-            # send_appointment_email(appointment.id, 'modified-accepted')
         update_schedule(data)
         return Response(serializer.data, status=status.HTTP_200_OK)
 
@@ -533,7 +524,7 @@ def accept_alternative(request, appointment_id):
     alternatives = appointment.alternatives
     if alternatives == "":
         return Response({"error": "No puedes aceptar una alternativa si la cita no tiene alternativas"}, status=status.HTTP_400_BAD_REQUEST)
-    
+
     # Verificar que el usuario autenticado sea el paciente correspondiente
     if user.patient != appointment.patient:
         return Response({"error": "No autorizado para aceptar una alternativa de esta cita"}, status=status.HTTP_403_FORBIDDEN)
@@ -542,7 +533,7 @@ def accept_alternative(request, appointment_id):
 
     if not selected_start_date or not selected_end_date:
         return Response({"error": "Debes proporcionar un 'start_time' y un 'end_time' válidos"}, status=status.HTTP_400_BAD_REQUEST)
-    
+
     # Extraer la fecha y la hora de las fechas
     selected_date = selected_start_date.split('T')[0]  # Ejemplo: "2025-04-07"
     selected_start_time = selected_start_date.split('T')[1][:5]  # Ejemplo: "10:00"
@@ -575,6 +566,7 @@ def accept_alternative(request, appointment_id):
 
     return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 
+
 @api_view(['PUT'])
 @permission_classes([IsAuthenticated])
 def confirm_appointment(request, appointment_id):
@@ -600,7 +592,7 @@ def confirm_appointment(request, appointment_id):
     appointment.status = "confirmed"
     appointment.save()
     # send_appointment_email(appointment.id, 'confirmed')
-    
+
     # Serializar la cita actualizada
     serializer = AppointmentSerializer(appointment)
 
@@ -717,7 +709,7 @@ def delete_appointment(request, appointment_id):
     except Exception as e:
         print(e)
         return Response({'error': 'An internal error has occurred. Please try again later.'}, status=status.HTTP_400_BAD_REQUEST)
-    
+
     # Enviar el correo con el rol del usuario
     send_appointment_email(appointment.id, 'canceled', role)
 
@@ -756,6 +748,7 @@ def get_appointment_by_id(request, appointmentId):
     except Exception as e:
         return Response({"error": "Bad request"}, status=status.HTTP_400_BAD_REQUEST)
 
+
 @api_view(['GET'])
 @permission_classes([IsAuthenticated])
 def confirm_appointment_using_token(request, token):
@@ -784,9 +777,9 @@ def confirm_appointment_using_token(request, token):
     appointment.save()
     send_appointment_email(appointment.id, 'confirmed')
 
-
     return Response({"message": "¡Cita aceptada con éxito!"}, status=status.HTTP_200_OK)
 
+
 @api_view(['GET'])
 @permission_classes([IsAuthenticated, IsPhysiotherapist])
 def list_finished_appointments_physio(request):
@@ -799,14 +792,14 @@ def list_finished_appointments_physio(request):
             {'detail': 'Debe ser fisioterapeuta para ver las citas'}, 
             status=status.HTTP_403_FORBIDDEN
         )
-    
+
     # Filtrar por estado finalizado
     status_filter = request.query_params.get('status', 'finished')
-    
+
     appointments = Appointment.objects.filter(
         physiotherapist=physiotherapist,
         status=status_filter
     ).order_by('-end_time')
-    
+
     serializer = AppointmentSerializer(appointments, many=True)
     return Response(serializer.data)
diff --git a/backend/fisio_find/files/__init__.py b/backend/fisio_find/files/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/backend/fisio_find/files/admin.py b/backend/fisio_find/files/admin.py
new file mode 100644
index 00000000..c9070fcb
--- /dev/null
+++ b/backend/fisio_find/files/admin.py
@@ -0,0 +1,124 @@
+from django.contrib import admin
+from django import forms
+from .models import PatientFile, Video
+import boto3, uuid
+from django.conf import settings
+from django.utils.html import format_html
+
+
+class PatientFileAdminForm(forms.ModelForm):
+    file_upload = forms.FileField(required=False, label="Subir archivo")  # 👈 Agregamos el campo de subida
+
+    class Meta:
+        model = PatientFile
+        fields = '__all__'
+
+    def save(self, commit=True):
+        instance = super().save(commit=False)
+        uploaded_file = self.cleaned_data.get('file_upload')
+
+        treatment = self.cleaned_data.get('treatment')
+        treatment_patient = treatment.patient.user.username
+
+        if uploaded_file:
+            # Configurar conexión con DigitalOcean Spaces
+            s3_client = boto3.client(
+                "s3",
+                aws_access_key_id=settings.DIGITALOCEAN_ACCESS_KEY_ID,
+                aws_secret_access_key=settings.DIGITALOCEAN_SECRET_ACCESS_KEY,
+                endpoint_url=settings.DIGITALOCEAN_ENDPOINT_URL
+            )
+
+            # Nombre del archivo en Spaces
+            file_extension = uploaded_file.name.split(".")[-1]
+            file_key = f"patient_files/{treatment_patient}/{uuid.uuid4()}.{file_extension}"
+
+            # Subir archivo a DigitalOcean
+            s3_client.upload_fileobj(
+                uploaded_file,
+                settings.DIGITALOCEAN_SPACE_NAME,
+                file_key,
+                ExtraArgs={'ACL': 'public-read'}
+            )
+
+            # Guardar la referencia al archivo
+            instance.file_key = file_key
+
+        if commit:
+            instance.save()
+        return instance
+
+
+@admin.register(PatientFile)
+class PatientFileAdmin(admin.ModelAdmin):
+    form = PatientFileAdminForm  # 👈 Usamos el nuevo formulario
+    list_display = ('id', 'title', 'uploaded_at', 'file_link')
+
+    def file_link(self, obj):
+        """Muestra un link al archivo en DigitalOcean"""
+        if obj.file_key:
+            url = f"{settings.DIGITALOCEAN_ENDPOINT_URL}/{settings.DIGITALOCEAN_SPACE_NAME}/{obj.file_key}"
+            return format_html(f'<a href="{url}" target="_blank">Ver archivo</a>')
+        return "No hay archivo"
+
+    file_link.allow_tags = True
+    file_link.short_description = "Archivo"
+
+    def delete_model(self, request, obj):
+        obj.delete_from_storage()  # 👈 Llama a tu método personalizado
+        super().delete_model(request, obj)
+
+
+class VideoAdminForm(forms.ModelForm):
+    video_upload = forms.FileField(required=False, label="Subir video")  # Campo para subir el archivo de video
+
+    class Meta:
+        model = Video
+        fields = '__all__'
+
+    def save(self, commit=True):
+        instance = super().save(commit=False)
+        uploaded_video = self.cleaned_data.get('video_upload')
+
+        treatment = self.cleaned_data.get('treatment')
+        treatment_patient = treatment.patient.user.username  # Suponiendo que puedes acceder al paciente desde el tratamiento
+
+        if uploaded_video:
+            # Configurar conexión con DigitalOcean Spaces
+            s3_client = boto3.client(
+                "s3",
+                aws_access_key_id=settings.DIGITALOCEAN_ACCESS_KEY_ID,
+                aws_secret_access_key=settings.DIGITALOCEAN_SECRET_ACCESS_KEY,
+                endpoint_url=settings.DIGITALOCEAN_ENDPOINT_URL
+            )
+
+            # Nombre del archivo en Spaces
+            file_extension = uploaded_video.name.split(".")[-1]
+            file_key = f"videos/{treatment_patient}/{uuid.uuid4()}.{file_extension}"
+
+            # Subir archivo a DigitalOcean
+            s3_client.upload_fileobj(
+                uploaded_video,
+                settings.DIGITALOCEAN_SPACE_NAME,
+                file_key,
+                ExtraArgs={'ACL': 'public-read'}
+            )
+
+            # Guardar la referencia al archivo
+            instance.file_key = file_key
+
+        if commit:
+            instance.save()
+        return instance
+
+
+class VideoAdmin(admin.ModelAdmin):
+    form = VideoAdminForm
+    list_display = ('title', 'description', 'uploaded_at', 'file_url')
+    search_fields = ('title', 'description')
+
+    def delete_model(self, request, obj):
+        obj.delete_from_storage()  # 👈 Llama a tu método personalizado
+        super().delete_model(request, obj)
+
+admin.site.register(Video, VideoAdmin)
diff --git a/backend/fisio_find/files/apps.py b/backend/fisio_find/files/apps.py
new file mode 100644
index 00000000..a5b57121
--- /dev/null
+++ b/backend/fisio_find/files/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class FilesConfig(AppConfig):
+    default_auto_field = "django.db.models.BigAutoField"
+    name = "files"
diff --git a/backend/fisio_find/files/migrations/__init__.py b/backend/fisio_find/files/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/backend/fisio_find/files/models.py b/backend/fisio_find/files/models.py
new file mode 100644
index 00000000..1c7de717
--- /dev/null
+++ b/backend/fisio_find/files/models.py
@@ -0,0 +1,71 @@
+from django.db import models
+from django.conf import settings
+import boto3
+from treatments.models import Treatment
+
+
+class PatientFile(models.Model):
+    treatment = models.ForeignKey(Treatment, on_delete=models.CASCADE, related_name='treatment_files')
+    title = models.CharField(max_length=255)
+    description = models.TextField(blank=True, null=True)
+    uploaded_at = models.DateTimeField(auto_now_add=True)
+    file_key = models.CharField(max_length=500, unique=True)
+    file_type = models.CharField(max_length=50, blank=True, null=True)
+
+    def __str__(self):
+        return self.title
+    
+    def delete_from_storage(self):
+        """Elimina el archivo de DigitalOcean Spaces"""
+        s3_client = boto3.client(
+            "s3",
+            aws_access_key_id=settings.DIGITALOCEAN_ACCESS_KEY_ID,
+            aws_secret_access_key=settings.DIGITALOCEAN_SECRET_ACCESS_KEY,
+            endpoint_url=settings.DIGITALOCEAN_ENDPOINT_URL
+        )
+
+        try:
+            # Intentar borrar el archivo de DigitalOcean
+            print(f"Eliminando archivo de DigitalOcean: {self.file_key}")  # Log para depuración
+            s3_client.delete_object(Bucket=settings.DIGITALOCEAN_SPACE_NAME, Key=self.file_key)
+            print(f"Archivo eliminado correctamente de DigitalOcean: {self.file_key}")  # Log para depuración
+        except Exception as e:
+            print(f"Error al eliminar el archivo de DigitalOcean: {e}")
+            raise  # Lanzamos la excepción para que se capture si hay un error
+
+    @property
+    def file_url(self):
+        """Devuelve la URL del archivo en DigitalOcean Spaces"""
+        return f"{settings.DIGITALOCEAN_ENDPOINT_URL}/{settings.DIGITALOCEAN_SPACE_NAME}/{self.file_key}"
+
+
+class Video(models.Model):
+    treatment = models.ForeignKey(Treatment, on_delete=models.CASCADE, related_name='treatment_videos')
+    title = models.CharField(max_length=255)
+    description = models.TextField(blank=True, null=True)
+    uploaded_at = models.DateTimeField(auto_now_add=True)
+    file_key = models.CharField(
+        max_length=500, unique=True
+    )
+
+    def __str__(self): 
+        return self.title
+
+    def delete_from_storage(self):
+        """Elimina el archivo de DigitalOcean Spaces"""
+        s3_client = boto3.client(
+            "s3",
+            aws_access_key_id=settings.DIGITALOCEAN_ACCESS_KEY_ID,
+            aws_secret_access_key=settings.DIGITALOCEAN_SECRET_ACCESS_KEY,
+            endpoint_url=settings.DIGITALOCEAN_ENDPOINT_URL
+        )
+
+        try:
+            s3_client.delete_object(Bucket=settings.DIGITALOCEAN_SPACE_NAME, Key=self.file_key)
+        except Exception as e:
+            print(f"Error al eliminar el archivo de Spaces: {e}")
+
+    @property
+    def file_url(self):
+        """Devuelve la URL pública del archivo almacenado en DigitalOcean"""
+        return f"https://fisiofind-repo.fra1.digitaloceanspaces.com/{self.file_key}"
\ No newline at end of file
diff --git a/backend/fisio_find/files/serializers.py b/backend/fisio_find/files/serializers.py
new file mode 100644
index 00000000..de009049
--- /dev/null
+++ b/backend/fisio_find/files/serializers.py
@@ -0,0 +1,184 @@
+from treatments.models import Treatment
+from django.conf import settings
+import boto3
+import uuid
+from rest_framework import serializers
+from .models import PatientFile, Video
+import logging
+
+
+class PatientFileSerializer(serializers.ModelSerializer):
+    files = serializers.ListField(
+        child=serializers.FileField(),  # Para manejar múltiples archivos
+        write_only=True
+    )
+    file_urls = serializers.SerializerMethodField()
+
+    class Meta:
+        model = PatientFile
+        fields = ["id", "title", "description", "uploaded_at", "file_key", "file_urls", "files"]
+        extra_kwargs = {
+            "file_key": {"read_only": True},  # La clave del archivo es solo de lectura
+        }
+
+    def validate_files(self, files):
+        """Valida que todos los archivos tengan una extensión permitida."""
+        allowed_extensions = (".pdf", ".jpg", ".jpeg", ".png", ".dicom")
+        for file in files:
+            if not file.name.lower().endswith(allowed_extensions):
+                raise serializers.ValidationError(f"Solo se permiten archivos con extensiones {', '.join(allowed_extensions)}.")
+        return files
+
+    def create(self, validated_data):
+        # Obtener la solicitud para acceder a request.data y request.FILES
+        request = self.context["request"]
+
+        # Obtener los campos del formulario directamente desde request.data
+        treatment = request.data.get('treatment')  # Recibimos treatment desde request.data
+        title = request.data.get('title')
+        description = request.data.get('description')
+
+        if not treatment:
+            raise serializers.ValidationError("El tratamiento es obligatorio.")
+
+        # Obtener los archivos
+        files = request.FILES.getlist("files")
+
+        # Usar get_or_create para asegurar que el archivo esté asociado con el tratamiento
+        treatment_file, created = PatientFile.objects.get_or_create(
+            treatment_id=treatment,  # Usamos el ID directamente
+            title=title,
+            defaults={"description": description, "file_key": ""}
+        )
+
+        # Obtener las claves de archivo existentes
+        file_keys = treatment_file.file_key.split(",") if treatment_file.file_key else []
+
+        # Crear el cliente de S3
+        s3_client = boto3.client(
+            "s3",
+            aws_access_key_id=settings.DIGITALOCEAN_ACCESS_KEY_ID,
+            aws_secret_access_key=settings.DIGITALOCEAN_SECRET_ACCESS_KEY,
+            endpoint_url=settings.DIGITALOCEAN_ENDPOINT_URL
+        )
+        try:
+            treatment_instance = Treatment.objects.get(id=treatment)
+            treatment_patient = treatment_instance.patient.user.username
+        except Treatment.DoesNotExist:
+            raise serializers.ValidationError("El tratamiento especificado no existe.")
+
+        # Subir cada archivo a DigitalOcean
+        for file in files:
+            file_extension = file.name.split(".")[-1]
+            file_key = f"patient_files/{treatment_patient}/{uuid.uuid4()}.{file_extension}"
+
+            try:
+                # Subir archivo a S3
+                s3_client.upload_fileobj(
+                    file,
+                    settings.DIGITALOCEAN_SPACE_NAME,
+                    file_key,
+                    ExtraArgs={"ACL": "private"},
+                )
+                file_keys.append(file_key)  # Agregar el archivo a la lista de claves
+            except Exception as e:
+                logger = logging.getLogger(__name__)
+                logger.error(f"Error al subir archivo: {str(e)}")
+                raise serializers.ValidationError("Error al subir archivo. Por favor, inténtelo de nuevo más tarde.")
+
+        # Guardar las claves de los archivos en el tratamiento
+        treatment_file.file_key = ",".join(file_keys)
+        treatment_file.save()
+
+        return treatment_file
+
+    def update(self, instance, validated_data):
+        """Actualiza un archivo en DigitalOcean Spaces y la BD."""
+
+        # Actualizar otros campos si están en los datos
+        instance.title = validated_data.get("title", instance.title)
+        instance.description = validated_data.get("description", instance.description)
+
+        instance.save()
+        return instance
+
+    def get_file_urls(self, obj):
+        """Devuelve la URL completa del archivo."""
+        return f"{settings.DIGITALOCEAN_ENDPOINT_URL}/{obj.file_key}"
+
+
+class VideoSerializer(serializers.ModelSerializer):
+    file = serializers.FileField(write_only=True)  # Para recibir el archivo en el request
+
+    class Meta:
+        model = Video
+        fields = ["id", "treatment", "title", "description", "file", "file_key", "file_url", "uploaded_at"]
+        extra_kwargs = {
+            "file_key": {"read_only": True},  # El usuario no debe enviar esto
+        }
+
+    def validate_file(self, file):
+        """Valida que el archivo sea un video permitido."""
+        allowed_extensions = (".mp4", ".avi", ".mov", ".mkv", ".webm")
+        if not file.name.lower().endswith(allowed_extensions):
+            raise serializers.ValidationError(f"Solo se permiten archivos con extensiones {', '.join(allowed_extensions)}.")
+        return file
+
+    def create(self, validated_data):
+        """Sube el archivo a DigitalOcean Spaces y guarda el Video en la BD."""
+        request = self.context["request"]
+        file = validated_data.pop("file")  
+        treatment = request.data.get('treatment')
+
+        try:
+            treatment_instance = Treatment.objects.get(id=treatment)
+            treatment_physio = treatment_instance.physiotherapist.user.username
+        except Treatment.DoesNotExist:
+            raise serializers.ValidationError("El tratamiento especificado no existe.")
+
+        # Generar un nombre único para evitar sobrescribir archivos
+        file_extension = file.name.split(".")[-1]
+        file_key = f"videos/{treatment_physio}/{uuid.uuid4()}.{file_extension}"
+
+        # Conectar a DigitalOcean Spaces
+        s3_client = boto3.client(
+            "s3",
+            region_name=settings.DIGITALOCEAN_REGION,
+            endpoint_url=settings.DIGITALOCEAN_ENDPOINT_URL,
+            aws_access_key_id=settings.DIGITALOCEAN_ACCESS_KEY_ID,
+            aws_secret_access_key=settings.DIGITALOCEAN_SECRET_ACCESS_KEY,
+        )
+
+        # Intentar subir el archivo
+        try:
+            s3_client.upload_fileobj(
+                file,
+                settings.DIGITALOCEAN_SPACE_NAME,
+                file_key,
+                ExtraArgs={"ACL": "public-read"},  # Permitir acceso público
+            )
+        except Exception as e:
+            logging.error(f"Error al subir archivo: {str(e)}")
+            raise serializers.ValidationError("Error al subir archivo. Por favor, inténtelo de nuevo más tarde.")
+
+        # Guardar en la BD
+        video_file, created = Video.objects.get_or_create(
+            file_key=file_key,
+            treatment_id=treatment,
+            **validated_data
+        )
+        return video_file
+
+    def update(self, instance, validated_data):
+        """Actualiza un video en DigitalOcean Spaces y la BD."""
+
+        # Actualizar título y descripción si están en los datos
+        instance.title = validated_data.get("title", instance.title)
+        instance.description = validated_data.get("description", instance.description)
+
+        instance.save()
+        return instance
+
+    def get_file_url(self, obj):
+        """Devuelve la URL completa del archivo."""
+        return f"{settings.DIGITALOCEAN_ENDPOINT_URL}/{obj.file_key}"
diff --git a/backend/fisio_find/files/tests.py b/backend/fisio_find/files/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/backend/fisio_find/files/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/backend/fisio_find/files/urls.py b/backend/fisio_find/files/urls.py
new file mode 100644
index 00000000..14e01a24
--- /dev/null
+++ b/backend/fisio_find/files/urls.py
@@ -0,0 +1,18 @@
+from django.urls import path
+from .views import *
+from django.conf import settings
+
+urlpatterns = [
+    path('files/create-files/', create_file, name='create_file'),
+    path('files/delete-file/<int:file_id>/', delete_patient_file, name='delete_patient_file'),
+    path('files/update-file/<int:file_id>/', update_patient_file, name='update_patient_file'),
+    path('files/list-file/<int:file_id>/', get_patient_file_by_id, name='get_patient_file_by_id'),
+    path('files/list-files/', get_patient_files, name='get_patient_files'),
+
+    path('videos/create-video/', create_video, name='create_video'),
+    path('videos/delete-video/<int:video_id>/', delete_video, name='delete_video'),
+    path('videos/list-video/<int:video_id>/', list_video_by_id, name='list_video_by_id'),
+    path('videos/list-videos/', list_my_videos, name='list_my_videos'),
+    path('videos/update-video/<int:video_id>/', update_video, name='update_video'),
+    path('videos/stream-video/<int:video_id>/', stream_video, name='stream_video'),
+]
\ No newline at end of file
diff --git a/backend/fisio_find/files/views.py b/backend/fisio_find/files/views.py
new file mode 100644
index 00000000..f29b9166
--- /dev/null
+++ b/backend/fisio_find/files/views.py
@@ -0,0 +1,341 @@
+from treatments.models import Treatment
+from rest_framework.decorators import api_view, permission_classes
+from rest_framework.response import Response
+from .serializers import PatientFileSerializer, VideoSerializer
+from django.http import StreamingHttpResponse, HttpResponse
+from django.conf import settings
+import boto3
+from rest_framework import status
+from users.permissions import IsPatient, IsPhysioOrPatient, IsPhysiotherapist
+from .models import PatientFile, Video
+
+
+@api_view(['POST'])
+@permission_classes([IsPatient])
+def create_file(request):
+    treatment_id = request.data.get('treatment')
+
+    if not treatment_id:
+        return Response(
+            {"message": "El ID del tratamiento es requerido"},
+            status=status.HTTP_400_BAD_REQUEST
+        )
+
+    # Aquí asumo que Treatment es el nombre del modelo y tiene una relación con el paciente
+    try:
+        treatment = Treatment.objects.get(id=treatment_id)
+    except Treatment.DoesNotExist:
+        return Response(
+            {"message": "Tratamiento no encontrado"},
+            status=status.HTTP_404_NOT_FOUND
+        )
+
+    # Verificar si el paciente asociado al tratamiento es el mismo que el que hace la solicitud
+    if treatment.patient != request.user.patient:
+        return Response(
+            {"message": "No tienes permiso para crear archivos para este tratamiento"},
+            status=status.HTTP_403_FORBIDDEN
+        )
+    mutable_data = request.data.copy()
+    serializer = PatientFileSerializer(data=mutable_data, context={'request': request})
+
+    if serializer.is_valid():
+        file = serializer.save()
+        return Response(
+            {
+                "mesaage": "Archivo creado correctamente",
+                "file": PatientFileSerializer(file).data
+            },
+            status=status.HTTP_201_CREATED
+        )
+    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+
+@api_view(['DELETE'])
+@permission_classes([IsPatient])
+def delete_patient_file(request, file_id):
+    user = request.user
+
+    try:
+        file = PatientFile.objects.get(id=file_id)
+
+        if not hasattr(user, 'patient') or file.treatment.patient.id != user.patient.id:
+            return Response({"error": "No tienes permiso para eliminar este archivo"}, status=status.HTTP_403_FORBIDDEN)
+
+        file.delete_from_storage()
+        file.delete()
+
+        return Response({"message": "Archivo eliminado correctamente"}, status=status.HTTP_200_OK)
+
+    except PatientFile.DoesNotExist:
+        return Response({"error": "Archivo no encontrado"}, status=status.HTTP_404_NOT_FOUND)
+
+
+@api_view(['PUT'])
+@permission_classes([IsPatient])
+def update_patient_file(request, file_id):
+    try:
+        file_instance = PatientFile.objects.get(id=file_id)
+    except PatientFile.DoesNotExist:
+        return Response({"error": "Archivo no encontrado"}, status=status.HTTP_404_NOT_FOUND)
+
+    # Verificar que el usuario autenticado es el dueño del archivo
+    if file_instance.treatment.patient.id != request.user.patient.id:
+        return Response({"error": "No tienes permiso para actualizar este archivo"}, status=status.HTTP_403_FORBIDDEN)
+
+    mutable_data = request.data.copy()
+
+    # Si el archivo es nuevo, lo agregamos al diccionario mutable
+    new_file = request.FILES.get("files")
+    if new_file:
+        mutable_data["files"] = new_file
+
+    serializer = PatientFileSerializer(file_instance, data=mutable_data, partial=True, context={"request": request})
+
+    if serializer.is_valid():
+        serializer.save()
+
+        return Response({
+            "message": "Archivo actualizado correctamente",
+            "file": serializer.data
+        }, status=status.HTTP_200_OK)
+
+    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+
+@api_view(['GET'])
+@permission_classes([IsPhysioOrPatient])
+def get_patient_file_by_id(request, file_id):
+    user = request.user
+    try:
+        file = PatientFile.objects.get(id=file_id)
+        serializer = PatientFileSerializer(file)
+
+        if hasattr(user, 'patient') and file.treatment.patient.id == user.patient.id:
+            return Response(serializer.data, status=status.HTTP_200_OK)
+        elif hasattr(user, 'physio') and file.treatment.physiotherapist.id == user.physio.id:
+            return Response(serializer.data, status=status.HTTP_200_OK)
+        else:
+            return Response({"error": "No tienes permiso para ver este archivo"}, status=status.HTTP_403_FORBIDDEN)
+    except PatientFile.DoesNotExist:
+        return Response({"error": "Archivo no encontrado"}, status=status.HTTP_404_NOT_FOUND)
+
+
+@api_view(['GET'])
+@permission_classes([IsPhysioOrPatient])
+def get_patient_files(request):
+    user = request.user
+    patient_files = []
+
+    # Filtrar los archivos que el usuario puede acceder
+    if hasattr(user, 'patient'):
+        files = PatientFile.objects.filter(treatment__patient=user.patient)
+    elif hasattr(user, 'physio'):
+        files = PatientFile.objects.filter(treatment__physiotherapist=user.physio)
+    else:
+        files = []
+
+    for file in files:
+        serializer = PatientFileSerializer(file)
+        patient_files.append(serializer.data)
+
+    if not patient_files:
+        return Response({"error": "No se han encontrado archivos a los que puedas acceder"},
+                        status=status.HTTP_404_NOT_FOUND)
+
+    return Response(patient_files, status=status.HTTP_200_OK)
+
+
+@api_view(['POST'])
+@permission_classes([IsPhysiotherapist])
+def create_video(request):
+    treatment_id = request.data.get('treatment')
+
+    if not treatment_id:
+        return Response(
+            {"message": "El ID del tratamiento es requerido"},
+            status=status.HTTP_400_BAD_REQUEST
+        )
+
+    # Aquí asumo que Treatment es el nombre del modelo y tiene una relación con el paciente
+    try:
+        treatment = Treatment.objects.get(id=treatment_id)
+    except Treatment.DoesNotExist:
+        return Response(
+            {"message": "Tratamiento no encontrado"},
+            status=status.HTTP_404_NOT_FOUND
+        )
+
+    # Verificar si el fisioterapeuta asociado al tratamiento es el mismo que el que hace la solicitud
+    if treatment.physiotherapist != request.user.physio:
+        return Response(
+            {"message": "No tienes permiso para crear archivos para este tratamiento"},
+            status=status.HTTP_403_FORBIDDEN
+        )
+
+    mutable_data = request.data.copy()
+    serializer = VideoSerializer(data=mutable_data, context={"request": request})
+
+    if serializer.is_valid():
+        video = serializer.save()
+        return Response(
+            {
+                "message": "Archivo creado correctamente",
+                "video": VideoSerializer(video).data
+            },
+            status=status.HTTP_201_CREATED
+        )
+
+    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+
+@api_view(['DELETE'])
+@permission_classes([IsPhysiotherapist])
+def delete_video(request, video_id):
+    user = request.user
+    try:
+        video = Video.objects.get(id=video_id)
+
+        if not hasattr(user, 'physio') or video.treatment.physiotherapist != user.physio:
+            return Response({"error": "No tienes permiso para eliminar este video"}, status=status.HTTP_403_FORBIDDEN)
+
+        video.delete_from_storage()
+        video.delete()
+
+        return Response({"message": "Video eliminado correctamente"}, status=status.HTTP_200_OK)
+
+    except Video.DoesNotExist:
+        return Response({"error": "Video no encontrado"}, status=status.HTTP_404_NOT_FOUND)
+
+
+@api_view(['GET'])
+@permission_classes([IsPhysioOrPatient])
+def list_video_by_id(request, video_id):
+    user = request.user
+    try:
+        video = Video.objects.get(id=video_id)
+        serializer = VideoSerializer(video)
+
+        if hasattr(user, 'patient') and video.treatment.patient == user.patient:
+            return Response(serializer.data, status=status.HTTP_200_OK)
+        elif hasattr(user, 'physio') and video.treatment.physiotherapist == user.physio:
+            return Response(serializer.data, status=status.HTTP_200_OK)
+        else:
+            return Response({"error": "No tienes permiso para ver este video"}, status=status.HTTP_403_FORBIDDEN)
+    except Video.DoesNotExist:
+        return Response({"error": "Video no encontrado"}, status=status.HTTP_404_NOT_FOUND)
+
+
+@api_view(['GET'])
+@permission_classes([IsPhysioOrPatient])
+def list_my_videos(request):
+    user = request.user
+    videos_owner = []
+
+    if hasattr(user, 'patient'):
+        videos = Video.objects.filter(treatment__patient=user.patient)
+    elif hasattr(user, 'physio'):
+        videos = Video.objects.filter(treatment__physiotherapist=user.physio)
+    else:
+        videos = []
+
+    for video in videos:
+        serializer = VideoSerializer(video)
+        videos_owner.append(serializer.data)
+
+    if not videos_owner:
+        return Response({"error": "No se han encontrado videos a los que puedas acceder"},
+                        status=status.HTTP_404_NOT_FOUND)
+
+    return Response(videos_owner, status=status.HTTP_200_OK)
+
+
+@api_view(['PUT'])
+@permission_classes([IsPhysiotherapist])
+def update_video(request, video_id):
+    try:
+        video = Video.objects.get(id=video_id)
+
+        # Verificar que el usuario autenticado es el fisioterapeuta propietario del video
+        if request.user.physio != video.treatment.physiotherapist:
+            return Response({'error': 'No tienes permiso para actualizar este video'}, status=status.HTTP_403_FORBIDDEN)
+
+    except Video.DoesNotExist:
+        return Response({'error': 'El video no existe'}, status=status.HTTP_404_NOT_FOUND)
+
+    # Convertir request.data en un diccionario mutable
+    mutable_data = request.data.copy()
+
+    # Serializar con los datos nuevos
+    serializer = VideoSerializer(video, data=mutable_data, partial=True, context={'request': request})
+
+    if serializer.is_valid():
+        serializer.save()
+        return Response({"message": "Video actualizado correctamente", "video": serializer.data},
+                        status=status.HTTP_200_OK)
+
+    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+
+@api_view(['GET'])
+@permission_classes([IsPatient])
+def stream_video(request, video_id):
+    try:
+        video = Video.objects.get(id=video_id)
+        if not hasattr(request.user, "patient") or video.treatment.patient != request.user.patient:
+            return Response({'error': 'No tienes permisos para reproducir este vídeo'}, status=403)
+
+        s3_client = boto3.client(
+            "s3",
+            region_name=settings.DIGITALOCEAN_REGION,
+            endpoint_url=settings.DIGITALOCEAN_ENDPOINT_URL,
+            aws_access_key_id=settings.DIGITALOCEAN_ACCESS_KEY_ID,
+            aws_secret_access_key=settings.DIGITALOCEAN_SECRET_ACCESS_KEY,
+        )
+
+        video_object = s3_client.get_object(
+            Bucket=settings.DIGITALOCEAN_SPACE_NAME,
+            Key=video.file_key
+        )
+
+        video_size = video_object["ContentLength"]
+        video_body = video_object["Body"]
+
+        # Manejar streaming por fragmentos (range requests)
+        range_header = request.headers.get("Range", None)
+        if range_header:
+            range_value = range_header.replace("bytes=", "").split("-")
+            start = int(range_value[0]) if range_value[0] else 0
+            end = int(range_value[1]) if len(range_value) > 1 and range_value[1] else video_size - 1
+            chunk_size = end - start + 1
+
+            # Leer solo el fragmento necesario
+            video_body.seek(start)
+            video_chunk = video_body.read(chunk_size)
+
+            # Responder con `206 Partial Content`
+            response = HttpResponse(video_chunk, content_type="video/mp4")
+            response["Content-Range"] = f"bytes {start}-{end}/{video_size}"
+            response["Accept-Ranges"] = "bytes"
+            response["Content-Length"] = str(chunk_size)
+            response["Cache-Control"] = "no-cache"
+            response["Connection"] = "keep-alive"
+            response.status_code = 206
+        else:
+            # Streaming completo en fragmentos
+            def stream_file():
+                for chunk in video_body.iter_chunks():
+                    yield chunk
+
+            response = StreamingHttpResponse(stream_file(), content_type="video/mp4")
+            response["Content-Length"] = str(video_size)
+            response["Accept-Ranges"] = "bytes"
+            response["Cache-Control"] = "no-cache"
+            response["Connection"] = "keep-alive"
+
+        return response
+
+    except Video.DoesNotExist:
+        return Response({"error": "El video no existe"}, status=404)
+    except Exception as e:
+        return Response({"error": str(e)}, status=500)
\ No newline at end of file
diff --git a/backend/fisio_find/fisio_find/settings.py b/backend/fisio_find/fisio_find/settings.py
index 54701883..abd97eac 100644
--- a/backend/fisio_find/fisio_find/settings.py
+++ b/backend/fisio_find/fisio_find/settings.py
@@ -98,6 +98,7 @@
     'treatments',
     'gestion_survey',
     'payment',
+    'files',
 ]
 
 INSTALLED_APPS += ['corsheaders', 'django_extensions', 'django_filters']
diff --git a/backend/fisio_find/fisio_find/urls.py b/backend/fisio_find/fisio_find/urls.py
index 94c6695a..3c6c4a98 100644
--- a/backend/fisio_find/fisio_find/urls.py
+++ b/backend/fisio_find/fisio_find/urls.py
@@ -22,6 +22,7 @@ def home(request):
     path('api/treatments/', include('treatments.urls')),
     path('api/guest_session/', include('guest_session.urls')),
     path('api/payments/', include('payment.urls')),
+    path('api/cloud/', include('files.urls')),
 ]
 
 urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
diff --git a/backend/fisio_find/treatments/models.py b/backend/fisio_find/treatments/models.py
index d810e239..cfcbb684 100644
--- a/backend/fisio_find/treatments/models.py
+++ b/backend/fisio_find/treatments/models.py
@@ -3,6 +3,7 @@
 from django.core.exceptions import ValidationError
 from users.models import Patient, Physiotherapist
 
+
 class Treatment(models.Model):
     physiotherapist = models.ForeignKey(Physiotherapist, on_delete=models.CASCADE, related_name='treatments')
     patient = models.ForeignKey(Patient, on_delete=models.CASCADE, related_name='treatments')
diff --git a/backend/fisio_find/users/admin.py b/backend/fisio_find/users/admin.py
index 4dcb3241..e3f605a4 100644
--- a/backend/fisio_find/users/admin.py
+++ b/backend/fisio_find/users/admin.py
@@ -1,14 +1,15 @@
 from django.contrib import admin
-from django.contrib.auth.admin import UserAdmin as BaseUserAdmin 
+from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
 from .models import AppUser, Patient, Physiotherapist, Admin, Specialization, PhysiotherapistSpecialization
 from .forms import AppUserCreationForm, AppUserChangeForm
 
-class AppUserAdmin(BaseUserAdmin): 
+
+class AppUserAdmin(BaseUserAdmin):
     add_form = AppUserCreationForm
     form = AppUserChangeForm
     model = AppUser
 
-    list_display = ['id','username', 'first_name', 'email', 'dni', 'account_status']
+    list_display = ['id', 'username', 'first_name', 'email', 'dni', 'account_status']
 
     fieldsets = BaseUserAdmin.fieldsets + (
         (None, {
@@ -25,20 +26,53 @@ class AppUserAdmin(BaseUserAdmin):
             ),
         }),
     )
-    
+
     search_fields = ('username', 'first_name', 'last_name', 'email', 'dni', 'account_status')
     list_filter = ('account_status', 'is_staff', 'is_active', 'groups')
 
+
 class PatientAdmin(admin.ModelAdmin):
-    search_fields = ['user__email','user__username','user__first_name','user__last_name','user__dni', 'gender']
+    search_fields = ['user__email', 'user__username', 'user__first_name', 'user__last_name', 'user__dni', 'gender']
     list_filter = ['gender']
-    list_display = ['id','user__username', 'user__first_name','user__last_name','user__email']
+    list_display = ['id', 'user__username', 'user__first_name', 'user__last_name', 'user__email']
+
 
 class PhysioAdmin(admin.ModelAdmin):
-    search_fields = ['user__email','user__username','user__first_name','user__last_name','user__dni', 'gender',
+    search_fields = ['user__email', 'user__username', 'user__first_name', 'user__last_name', 'user__dni', 'gender',
                      'autonomic_community', 'collegiate_number']
     list_filter = ['gender', 'autonomic_community']
-    list_display = ['id','user__username', 'user__email', 'collegiate_number', 'autonomic_community']
+    list_display = ['id', 'user__username', 'user__email', 'collegiate_number', 'autonomic_community']
+
+
+# New admin class for Video model
+class VideoAdmin(admin.ModelAdmin):
+    search_fields = ['title', 'description', 'physiotherapist__user__username', 'physiotherapist__user__email']
+    list_filter = ['uploaded_at']
+    list_display = ['id', 'title', 'get_physiotherapist', 'uploaded_at']
+
+    def get_physiotherapist(self, obj):
+        return obj.physiotherapist.user.username
+    get_physiotherapist.short_description = 'Physiotherapist'
+    get_physiotherapist.admin_order_field = 'physiotherapist__user__username'
+
+
+# New admin class for PatientFile model
+class PatientFileAdmin(admin.ModelAdmin):
+    search_fields = ['title', 'description', 'patient__user__username', 'patient__user__email', 'file_type']
+    list_filter = ['uploaded_at', 'file_type']
+    list_display = ['id', 'title', 'get_patient', 'file_type', 'uploaded_at']
+
+    def get_patient(self, obj):
+        return obj.patient.user.username
+    get_patient.short_description = 'Patient'
+    get_patient.admin_order_field = 'patient__user__username'
+
+
+# Admin class for Specialization model
+class SpecializationAdmin(admin.ModelAdmin):
+    search_fields = ['name']
+    list_display = ['id', 'name']
+
 
 admin.site.register(AppUser, AppUserAdmin)
 admin.site.register(Patient, PatientAdmin)
diff --git a/backend/fisio_find/users/forms.py b/backend/fisio_find/users/forms.py
index ad53c354..28d853eb 100644
--- a/backend/fisio_find/users/forms.py
+++ b/backend/fisio_find/users/forms.py
@@ -1,7 +1,13 @@
 from django import forms
 from django.contrib.auth.forms import UserCreationForm, UserChangeForm
 from .models import AppUser
-from .util import *
+from .util import (
+    telefono_no_mide_9,
+    codigo_postal_no_mide_5,
+    validate_dni_structure,
+    validate_dni_match_letter
+)
+
 
 class AppUserCreationForm(UserCreationForm):
     class Meta:
@@ -16,6 +22,7 @@ def clean_username(self):
         if qs.exists():
             raise forms.ValidationError("El nombre de usuario ya está en uso.")
         return username
+
     def clean_email(self):
         email = self.cleaned_data.get('email')
         if not email:
@@ -61,6 +68,7 @@ class Meta:
         model = AppUser
         fields = ('username', 'email', 'dni', 'phone_number', 'postal_code', 'photo', 'account_status')
 
+
 class AppUserChangeForm(UserChangeForm):
 
     def clean_username(self):
@@ -113,7 +121,6 @@ def clean_dni(self):
             raise forms.ValidationError("El DNI ya está en uso.")
         return dni
 
-
     class Meta:
         model = AppUser
         fields = ('username', 'email', 'dni', 'phone_number', 'postal_code', 'photo', 'account_status')
diff --git a/backend/fisio_find/users/models.py b/backend/fisio_find/users/models.py
index 5b46dc76..3d34e6a1 100644
--- a/backend/fisio_find/users/models.py
+++ b/backend/fisio_find/users/models.py
@@ -1,7 +1,5 @@
 from django.db import models
 from django.contrib.auth.models import AbstractUser
-import boto3
-from django.conf import settings
 
 ACCOUNT_STATUS_CHOICES = [
     ('ACTIVE', 'Active'),
@@ -35,30 +33,35 @@
     ('COMUNIDAD VALENCIANA', 'Comunidad Valenciana')
 ]
 
+
 class AppUser(AbstractUser):
     photo = models.ImageField(null=True, blank=True, upload_to='profile_photos/')
     dni = models.CharField(max_length=9, unique=True)
     phone_number = models.CharField(max_length=9)
     postal_code = models.CharField(max_length=5)
     account_status = models.CharField(max_length=10, choices=ACCOUNT_STATUS_CHOICES, default='ACTIVE')
-    
+
     def __str__(self):
         return f"{self.username} - {self.email}"
 
+
 class Patient(models.Model):
     user = models.OneToOneField(AppUser, on_delete=models.CASCADE, related_name='patient')
     gender = models.CharField(max_length=1, choices=GENDER_CHOICES)
     stripe_customer_id = models.CharField(max_length=255, blank=True, null=True)
     birth_date = models.DateField()
-    
+
     def __str__(self):
         return f"{self.user.username} - {self.user.email}"
-    
+
+
 class Specialization(models.Model):
     name = models.CharField(max_length=50, unique=True)
 
     def __str__(self):
         return self.name
+
+
 class PhysiotherapistSpecialization(models.Model):
     physiotherapist = models.ForeignKey(
         'Physiotherapist', on_delete=models.CASCADE, related_name="physio_specializations"
@@ -66,7 +69,8 @@ class PhysiotherapistSpecialization(models.Model):
     specialization = models.ForeignKey(
         'Specialization', on_delete=models.SET_NULL, null=True, blank=True
     )
-    
+
+
 class Pricing(models.Model):
     name = models.CharField(max_length=50, unique=True)
     price = models.DecimalField(max_digits=5, decimal_places=2)
@@ -75,6 +79,7 @@ class Pricing(models.Model):
     def __str__(self):
         return self.name
 
+
 class Physiotherapist(models.Model):
     user = models.OneToOneField(AppUser, on_delete=models.CASCADE, related_name='physio')
     bio = models.TextField(null=True, blank=True)
@@ -100,41 +105,9 @@ class Physiotherapist(models.Model):
     def __str__(self):
         return f"{self.user.username} - {self.user.email}"
 
+
 class Admin(models.Model):
     user = models.OneToOneField(AppUser, on_delete=models.CASCADE, related_name='admin')
-    
+
     def __str__(self):
         return f"{self.user.username} - {self.user.email}"
-    
-
-class Video(models.Model):
-    physiotherapist = models.ForeignKey(Physiotherapist, on_delete=models.CASCADE, related_name='videos')
-    patients = models.ManyToManyField(Patient, related_name='videos', blank=True)
-    title = models.CharField(max_length=255)
-    description = models.TextField(blank=True, null=True)
-    uploaded_at = models.DateTimeField(auto_now_add=True)
-    file_key = models.CharField(
-        max_length=500, unique=True
-    )     
-
-    def __str__(self): 
-        return self.title
-    
-    def delete_from_storage(self):
-        """Elimina el archivo de DigitalOcean Spaces"""
-        s3_client = boto3.client(
-            "s3",
-            aws_access_key_id=settings.DIGITALOCEAN_ACCESS_KEY_ID,
-            aws_secret_access_key=settings.DIGITALOCEAN_SECRET_ACCESS_KEY,
-            endpoint_url=settings.DIGITALOCEAN_ENDPOINT_URL
-        )
-
-        try:
-            s3_client.delete_object(Bucket=settings.DIGITALOCEAN_SPACE_NAME, Key=self.file_key)
-        except Exception as e:
-            print(f"Error al eliminar el archivo de Spaces: {e}")
-
-    @property
-    def file_url(self):
-        """Devuelve la URL pública del archivo almacenado en DigitalOcean"""
-        return f"https://fisiofind-repo.fra1.digitaloceanspaces.com/{self.file_key}"
\ No newline at end of file
diff --git a/backend/fisio_find/users/permissions.py b/backend/fisio_find/users/permissions.py
index 10baed79..3ebc8406 100644
--- a/backend/fisio_find/users/permissions.py
+++ b/backend/fisio_find/users/permissions.py
@@ -1,12 +1,14 @@
 from rest_framework.permissions import BasePermission
 
+
 class IsPatient(BasePermission):
     """
     Permite el acceso solo a usuarios que tengan un perfil de paciente.
     """
     def has_permission(self, request, view):
         return hasattr(request.user, 'patient')
-    
+
+
 class IsPhysiotherapist(BasePermission):
     """
     Permite el acceso a usuarios que sean fisioterapeutas.
@@ -14,16 +16,18 @@ class IsPhysiotherapist(BasePermission):
     def has_permission(self, request, view):
         return hasattr(request.user, 'physio')
 
+
 class IsPhysioOrPatient(BasePermission):
     """
     Permite el acceso a usuarios que sean pacientes o fisioterapeutas.
     """
     def has_permission(self, request, view):
         return hasattr(request.user, 'patient') or hasattr(request.user, 'physio')
-    
+
+
 class IsAdmin(BasePermission):
     """
     Permite el acceso a usuarios que sean administrador.
     """
     def has_permission(self, request, view):
-        return hasattr(request.user, 'admin')
\ No newline at end of file
+        return hasattr(request.user, 'admin')
diff --git a/backend/fisio_find/users/serializers.py b/backend/fisio_find/users/serializers.py
index 21bac376..ece3c240 100644
--- a/backend/fisio_find/users/serializers.py
+++ b/backend/fisio_find/users/serializers.py
@@ -1,4 +1,3 @@
-import json
 from rest_framework import serializers
 from rest_framework.validators import UniqueValidator
 from django.contrib.auth.hashers import make_password
@@ -8,13 +7,9 @@
 from users.validacionFisios import validar_colegiacion
 from .models import AppUser, Patient, Physiotherapist, PhysiotherapistSpecialization, Specialization, Pricing
 from datetime import date, datetime
-from django.conf import settings
 from django.core.exceptions import ValidationError
 from django.core.validators import RegexValidator
-from datetime import date, datetime
-import boto3, uuid
 from users.util import validate_dni_match_letter, codigo_postal_no_mide_5, telefono_no_mide_9, validate_dni_structure
-from .models import AppUser, Patient, Physiotherapist, PhysiotherapistSpecialization, Specialization, Video
 
 
 class AppUserSerializer(serializers.ModelSerializer):
@@ -37,7 +32,7 @@ class AppUserSerializer(serializers.ModelSerializer):
     class Meta:
         model = AppUser
         fields = [
-            'user_id', 'username', 'first_name', 'last_name', 'email', 
+            'user_id', 'username', 'first_name', 'last_name', 'email',
             'photo', 'dni', 'phone_number', 'postal_code', 'account_status'
         ]
         extra_kwargs = {
@@ -96,7 +91,6 @@ class PhysioSerializer(serializers.ModelSerializer):
     class Meta:
         model = Physiotherapist
         fields = '__all__'
-        #exclude = ['user']
 
 
 class PatientSerializer(serializers.ModelSerializer):
@@ -117,7 +111,7 @@ def validate_birth_date(self, value):
 
         if value >= datetime.now().date():
             raise serializers.ValidationError("La fecha de nacimiento debe ser anterior a la fecha actual.")
-        elif value < date(1900,1,1):
+        elif value < date(1900, 1, 1):
             raise serializers.ValidationError("La fecha de nacimiento no puede ser tan atrás en el tiempo.")
         return value
 
@@ -146,11 +140,10 @@ def update(self, instance, validated_data):
 
         # Impedir la modificación del DNI
         if user_data and 'dni' in user_data:
-            user_data.pop('dni', None) 
-        
-        if 'birth_date' in validated_data:
-            validated_data.pop('birth_date', None)  
+            user_data.pop('dni', None)
 
+        if 'birth_date' in validated_data:
+            validated_data.pop('birth_date', None)
 
         # Si hay datos de usuario, actualizar solo los campos permitidos
         if user_data:
@@ -296,7 +289,7 @@ class PhysioRegisterSerializer(serializers.ModelSerializer):
             'invalid': 'Valor de plan inválido'
         }
     )
-    
+
     # Campos nuevos de Stripe (solo lectura)
     stripe_subscription_id = serializers.CharField(read_only=True)
     subscription_status = serializers.CharField(read_only=True)
@@ -339,8 +332,11 @@ def validate(self, data):
             collegiate_number=collegiate_number,
             autonomic_community=autonomic_community
         ).exists():
-            validation_errors["collegiate_number"] = "Ya existe un fisioterapeuta con este nombre, apellido, número de colegiado y comunidad autónoma."
-        
+            validation_errors["collegiate_number"] = (
+                "Ya existe un fisioterapeuta con este nombre, apellido, "
+                "número de colegiado y comunidad autónoma."
+            )
+
         full_name_uppercase = first_name.upper() + " " + last_name.upper()
         valid_physio = validar_colegiacion(full_name_uppercase, collegiate_number, autonomic_community)
         if not valid_physio:
@@ -413,8 +409,9 @@ def update(self, instance, validated_data):
                 return instance
 
         except IntegrityError:
-            raise serializers.ValidationError({"error": "Error de integridad en la base de datos. Posible duplicado de datos."})
-
+            raise serializers.ValidationError({
+                "error": "Error de integridad en la base de datos. Posible duplicado de datos."
+            })
 
 
 class PhysioUpdateSerializer(serializers.ModelSerializer):
@@ -441,7 +438,8 @@ class PhysioUpdateSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = Physiotherapist
-        fields = ['email', 'phone_number', 'postal_code', 'bio', 'photo', 'services', 'specializations', 'schedule', 'plan']
+        fields = ['email', 'phone_number', 'postal_code', 'bio', 'photo', 'services',
+                  'specializations', 'schedule', 'plan']
 
     def validate(self, data):
         """Validaciones solo para los campos proporcionados."""
@@ -470,7 +468,9 @@ def validate_services(self, value):
             raise serializers.ValidationError("Los servicios deben ser un objeto JSON.")
         for service_name, service_data in value.items():
             if not isinstance(service_data, dict):
-                raise serializers.ValidationError(f"El servicio '{service_name}' debe contener un objeto con sus propiedades.")
+                raise serializers.ValidationError(
+                    f"El servicio '{service_name}' debe contener un objeto con sus propiedades."
+                )
         return value
 
     def update(self, instance, validated_data):
@@ -500,7 +500,8 @@ def update(self, instance, validated_data):
                     instance.physiotherapistspecialization_set.all().delete()
                     for spec_name in specializations_data:
                         specialization, _ = Specialization.objects.get_or_create(name=spec_name)
-                        PhysiotherapistSpecialization.objects.create(physiotherapist=instance, specialization=specialization)
+                        PhysiotherapistSpecialization.objects.create(physiotherapist=instance,
+                                                                     specialization=specialization)
 
                 instance.save()
                 return instance
@@ -524,122 +525,3 @@ class PatientAdminViewSerializer(serializers.ModelSerializer):
     class Meta:
         model = Patient
         fields = '__all__'
-
-
-class VideoSerializer(serializers.ModelSerializer):
-    file = serializers.FileField(write_only=True)  # Para recibir el archivo en el request
-    file_url = serializers.SerializerMethodField()  # Para devolver la URL pública
-    patients = serializers.PrimaryKeyRelatedField(many=True, queryset=Patient.objects.all(), required=False)  
-
-    class Meta:
-        model = Video
-        fields = ["id", "patients", "title", "description", "file", "file_key", "file_url", "uploaded_at"]
-        extra_kwargs = {
-            "file_key": {"read_only": True},  # El usuario no debe enviar esto
-            "title": {"required": False}, # opcional en update
-        }
-
-    def validate_file(self, file):
-        """Valida que el archivo sea un video permitido."""
-        allowed_extensions = (".mp4")
-        if not file.name.lower().endswith(allowed_extensions):
-            raise serializers.ValidationError("Solo se permiten archivos .mp4 .")
-        return file
-
-    def create(self, validated_data):
-        """Sube el archivo a DigitalOcean Spaces y guarda el Video en la BD."""
-        file = validated_data.pop("file", None)  # Extraer archivo del request
-        if not file:
-            raise serializers.ValidationError("El archivo es requerido para crear un video.")
-        physiotherapist = self.context["request"].user.physio  # Usuario logueado
-        patients = validated_data.pop("patients", [])
-
-        # Generar un nombre único para evitar sobrescribir archivos
-        file_extension = file.name.split(".")[-1]
-        file_key = f"videos/{physiotherapist.id}/{uuid.uuid4()}.{file_extension}"
-
-        # Conectar a DigitalOcean Spaces
-        s3_client = boto3.client(
-            "s3",
-            region_name=settings.DIGITALOCEAN_REGION,
-            endpoint_url=settings.DIGITALOCEAN_ENDPOINT_URL,
-            aws_access_key_id=settings.DIGITALOCEAN_ACCESS_KEY_ID,
-            aws_secret_access_key=settings.DIGITALOCEAN_SECRET_ACCESS_KEY,
-        )
-
-        # Intentar subir el archivo
-        try:
-            s3_client.upload_fileobj(
-                file,
-                settings.DIGITALOCEAN_SPACE_NAME,
-                file_key,
-                ExtraArgs={"ACL": "public-read"},  # Permitir acceso público
-            )
-        except Exception as e:
-            raise serializers.ValidationError(f"Error al subir archivo: {str(e)}")
-
-        # Guardar en la BD
-        video = Video.objects.create(
-            file_key=file_key,
-            physiotherapist=physiotherapist,
-            **validated_data  # Agrega título, descripción, etc.
-        )
-        video.patients.set(patients)
-        return video
-
-    def update(self, instance, validated_data):
-        """Actualiza un video en DigitalOcean Spaces y la BD."""
-        file = validated_data.pop("file", None)  # Nuevo archivo (opcional)
-        patients = validated_data.pop("patients", None)  # Lista de pacientes (opcional)
-
-        # Actualizar título y descripción si están en los datos
-        instance.title = validated_data.get("title", instance.title)
-        instance.description = validated_data.get("description", instance.description)
-
-        if file:
-            # Conectar a DigitalOcean Spaces
-            s3_client = boto3.client(
-                "s3",
-                region_name=settings.DIGITALOCEAN_REGION,
-                endpoint_url=settings.DIGITALOCEAN_ENDPOINT_URL,
-                aws_access_key_id=settings.DIGITALOCEAN_ACCESS_KEY_ID,
-                aws_secret_access_key=settings.DIGITALOCEAN_SECRET_ACCESS_KEY,
-            )
-
-            # Borrar archivo anterior de DigitalOcean
-            try:
-                s3_client.delete_object(
-                    Bucket=settings.DIGITALOCEAN_SPACE_NAME,
-                    Key=instance.file_key,
-                )
-            except Exception as e:
-                raise serializers.ValidationError(f"Error al eliminar archivo anterior: {str(e)}")
-
-            # Generar nuevo nombre único
-            file_extension = file.name.split(".")[-1]
-            new_file_key = f"videos/{instance.physiotherapist.id}/{uuid.uuid4()}.{file_extension}"
-
-            # Subir nuevo archivo
-            try:
-                s3_client.upload_fileobj(
-                    file,
-                    settings.DIGITALOCEAN_SPACE_NAME,
-                    new_file_key,
-                    ExtraArgs={"ACL": "public-read"},
-                )
-            except Exception as e:
-                raise serializers.ValidationError(f"Error al subir nuevo archivo: {str(e)}")
-
-            # Actualizar referencia en la BD
-            instance.file_key = new_file_key
-
-        # Si se pasan pacientes, actualizarlos
-        if patients is not None:
-            instance.patients.set(patients)
-
-        instance.save()
-        return instance
-
-    def get_file_url(self, obj):
-        """Devuelve la URL completa del archivo."""
-        return f"https://fisiofind-repo.fra1.digitaloceanspaces.com/{obj.file_key}"
\ No newline at end of file
diff --git a/backend/fisio_find/users/tests.py b/backend/fisio_find/users/tests.py
index 9cde1a53..426ca799 100644
--- a/backend/fisio_find/users/tests.py
+++ b/backend/fisio_find/users/tests.py
@@ -1,25 +1,20 @@
 from rest_framework.test import APITestCase, APIRequestFactory, APIClient
 from django.test import TestCase
-from rest_framework import status
 from django.urls import reverse
-from users.validacionFisios import validar_colegiacion
 from django.core.files.uploadedfile import SimpleUploadedFile
 from io import BytesIO
 from PIL import Image
-from users.serializers import (PatientRegisterSerializer,PatientSerializer, 
-                               PhysioRegisterSerializer, AppUserSerializer, 
-                               Specialization, PhysiotherapistSpecialization,
-                               PhysioUpdateSerializer, VideoSerializer)
+from users.serializers import (PatientRegisterSerializer, PatientSerializer,
+                               PhysioRegisterSerializer, AppUserSerializer,
+                               PhysioUpdateSerializer)
 from users.models import AppUser, Patient, Physiotherapist, Pricing
 from datetime import date, timedelta
-from unittest.mock import patch, MagicMock
+from unittest.mock import patch, MagicMock, Mock
 import json
 from django.core.exceptions import ValidationError
-from users.models import AppUser, Physiotherapist, Patient, Pricing, Video
 from datetime import datetime
 from rest_framework import serializers
-import boto3
-import uuid
+import stripe
 
 
 def get_fake_image(name="photo.jpg"):
@@ -29,6 +24,7 @@ def get_fake_image(name="photo.jpg"):
     buffer.seek(0)
     return SimpleUploadedFile(name=name, content=buffer.read(), content_type="image/jpeg")
 
+
 class AppUserRequiredFieldsTests(APITestCase):
 
     def setUp(self):
@@ -356,9 +352,9 @@ def test_duplicate_dni(self):
         serializer = PatientRegisterSerializer(data=data)
         self.assertFalse(serializer.is_valid())
         self.assertIn("dni", serializer.errors)
-    
+
     # --------------- GENDER --------------
-    
+
     def test_invalid_gender_value(self):
         data = self.get_base_data()
         data["gender"] = "X"  # Valor no válido
@@ -462,7 +458,7 @@ def test_missing_user_fields(self):
             data = self.get_valid_data()
             data["user"].pop(field)
             serializer = PatientSerializer(instance=self.patient, data=data, context={'request': self.request})
-            print(field,serializer.is_valid())
+            print(field, serializer.is_valid())
             self.assertFalse(serializer.is_valid())
             self.assertIn(field, serializer.errors.get("user", serializer.errors))
 
@@ -720,7 +716,7 @@ def test_invalid_service_nested_structure(self):
         self.assertIn("services", serializer.errors)
 
     def test_update_specializations(self):
-        #Specialization.objects.create(name="Traumatología")
+        # Specialization.objects.create(name="Traumatología")
         data = {
             "specializations": ["Deportiva", "Traumatología"]
         }
@@ -755,6 +751,7 @@ def test_update_photo(self):
         physio = serializer.save()
         self.assertTrue(physio.user.photo.name.endswith("profile.jpg"))
 
+
 class PatientProfileViewTests(APITestCase):
 
     def setUp(self):
@@ -804,6 +801,7 @@ def test_patch_invalid_birth_date(self):
         self.assertEqual(response.status_code, 400)
         self.assertIn("birth_date", response.data)
 
+
 class CustomLoginViewTests(APITestCase):
 
     def setUp(self):
@@ -842,6 +840,7 @@ def test_login_missing_fields(self):
         self.assertIn("username", response.data)
         self.assertIn("password", response.data)
 
+
 class LogoutViewTests(APITestCase):
 
     def setUp(self):
@@ -1002,6 +1001,7 @@ def test_return_user_unauthenticated(self):
         response = self.client.get(self.url)
         self.assertEqual(response.status_code, 401)
 
+
 class ValidatePhysioRegistrationTests(APITestCase):
 
     def setUp(self):
@@ -1080,10 +1080,11 @@ def test_underage_physio(self, mock_validar):
         self.assertEqual(response.status_code, 400)
         self.assertIn("birth_date", response.data)
 
+
 class PhysioRegisterViewTests(APITestCase):
 
     def setUp(self):
-        self.url = reverse("physio_register") 
+        self.url = reverse("physio_register")
         self.plan, _ = Pricing.objects.get_or_create(
             name='blue',
             defaults={'price': 10, 'video_limit': 5}
@@ -1168,8 +1169,6 @@ def test_invalid_postal_code(self, mock_validar):
         self.assertEqual(response.status_code, 400)
         self.assertIn("postal_code", response.data)
 
-from unittest.mock import patch, Mock
-import stripe
 
 class ProcessPaymentViewTests(APITestCase):
 
@@ -1276,7 +1275,9 @@ def test_physio_update_success(self):
                     "id": 1,
                     "title": "Primera consulta",
                     "price": 30,
-                    "description": "Evaluaremos tu estado físico, hablaremos sobre tus molestias y realizaremos pruebas para diseñar un plan de tratamiento personalizado que se adapte a tus necesidades y estilo de vida.",
+                    "description": "Evaluaremos tu estado físico, hablaremos sobre tus molestias y realizaremos pruebas "
+                                   "para diseñar un plan de tratamiento personalizado que se adapte a tus necesidades y "
+                                   "estilo de vida.",
                     "duration": 45,
                     "tipo": "PRIMERA_CONSULTA",
                     "custom_questionnaire": {
@@ -1287,8 +1288,10 @@ def test_physio_update_success(self):
                                 {"type": "Number", "label": "Peso (kg)", "scope": "#/properties/peso"},
                                 {"type": "Number", "label": "Altura (cm)", "scope": "#/properties/altura"},
                                 {"type": "Number", "label": "Edad", "scope": "#/properties/edad"},
-                                {"type": "Control", "label": "Nivel de actividad física", "scope": "#/properties/actividad_fisica"},
-                                {"type": "Control", "label": "Motivo de la consulta", "scope": "#/properties/motivo_consulta"},
+                                {"type": "Control", "label": "Nivel de actividad física",
+                                         "scope": "#/properties/actividad_fisica"},
+                                {"type": "Control", "label": "Motivo de la consulta",
+                                         "scope": "#/properties/motivo_consulta"},
                                 {"type": "Control", "label": "¿Qué te duele?", "scope": "#/properties/q1"},
                                 {"type": "Control", "label": "¿Cómo describirías el dolor?", "scope": "#/properties/q2"}
                             ]
@@ -1386,6 +1389,7 @@ def test_invalid_services_missing_required_fields(self):
                 response.status_code, 400,
             )
             self.assertIn("error", response.data)
+
     def test_services_as_json_string(self):
         data = {
             "services": json.dumps({
@@ -1404,8 +1408,10 @@ def test_services_as_json_string(self):
                                 {"type": "Number", "label": "Peso (kg)", "scope": "#/properties/peso"},
                                 {"type": "Number", "label": "Altura (cm)", "scope": "#/properties/altura"},
                                 {"type": "Number", "label": "Edad", "scope": "#/properties/edad"},
-                                {"type": "Control", "label": "Nivel de actividad física", "scope": "#/properties/actividad_fisica"},
-                                {"type": "Control", "label": "Motivo de la consulta", "scope": "#/properties/motivo_consulta"},
+                                {"type": "Control", "label": "Nivel de actividad física",
+                                         "scope": "#/properties/actividad_fisica"},
+                                {"type": "Control", "label": "Motivo de la consulta",
+                                         "scope": "#/properties/motivo_consulta"},
                                 {"type": "Control", "label": "¿Qué te duele?", "scope": "#/properties/q1"},
                                 {"type": "Control", "label": "¿Cómo describirías el dolor?", "scope": "#/properties/q2"}
                             ]
@@ -1445,12 +1451,12 @@ def test_services_as_dict_direct(self):
 
     def test_services_invalid_type(self):
         data = {
-            "services": 12345 
+            "services": 12345
         }
         response = self.client.put(self.url, data, format="json")
         self.assertEqual(response.status_code, 400)
         self.assertIn("services", response.data)
-    
+
     def test_missing_required_custom_elements(self):
         elements = [  # Solo uno de los necesarios
             {'type': 'Number', 'label': 'Peso (kg)', 'scope': '#/properties/peso'}
@@ -1480,7 +1486,6 @@ def test_missing_required_custom_elements(self):
         self.assertEqual(response.status_code, 400)
         self.assertIn("error", response.data)
 
-
     def test_missing_ui_schema_in_questionnaire(self):
         service = {
             "id": 1,
@@ -1527,7 +1532,6 @@ def test_invalid_custom_questionnaire_element(self):
         self.assertEqual(response.status_code, 400)
         self.assertIn("error", response.data)
 
-
     def test_invalid_tipo_value(self):
         service = {
             "id": 1,
@@ -1739,7 +1743,6 @@ def test_questionnaire_type_not_group(self):
         self.assertIn("error", response.data)
 
 
-
 class PhysioCreateServiceViewTests(APITestCase):
 
     def setUp(self):
@@ -1785,7 +1788,8 @@ def setUp(self):
                         {"type": "Number", "label": "Peso (kg)", "scope": "#/properties/peso"},
                         {"type": "Number", "label": "Altura (cm)", "scope": "#/properties/altura"},
                         {"type": "Number", "label": "Edad", "scope": "#/properties/edad"},
-                        {"type": "Control", "label": "Nivel de actividad física", "scope": "#/properties/actividad_fisica"},
+                        {"type": "Control", "label": "Nivel de actividad física",
+                                 "scope": "#/properties/actividad_fisica"},
                         {"type": "Control", "label": "Motivo de la consulta", "scope": "#/properties/motivo_consulta"},
                         {"type": "Control", "label": "¿Qué te duele?", "scope": "#/properties/q1"},
                         {"type": "Control", "label": "¿Cómo describirías el dolor?", "scope": "#/properties/q2"}
@@ -1979,6 +1983,7 @@ def test_create_service_duplicate_elements(self):
         response = self.client.post(self.url, invalid, format="json")
         self.assertEqual(response.status_code, 400)
 
+
 class PhysioUpdateSpecificServiceTests(APITestCase):
 
     def setUp(self):
@@ -2031,7 +2036,8 @@ def setUp(self):
                         {"type": "Number", "label": "Peso (kg)", "scope": "#/properties/peso"},
                         {"type": "Number", "label": "Altura (cm)", "scope": "#/properties/altura"},
                         {"type": "Number", "label": "Edad", "scope": "#/properties/edad"},
-                        {"type": "Control", "label": "Nivel de actividad física", "scope": "#/properties/actividad_fisica"},
+                        {"type": "Control", "label": "Nivel de actividad física",
+                                 "scope": "#/properties/actividad_fisica"},
                         {"type": "Control", "label": "Motivo de la consulta", "scope": "#/properties/motivo_consulta"},
                         {"type": "Control", "label": "¿Qué te duele?", "scope": "#/properties/q1"},
                         {"type": "Control", "label": "¿Cómo describirías el dolor?", "scope": "#/properties/q2"}
@@ -2253,6 +2259,7 @@ def test_delete_unauthenticated(self):
         response = self.client.delete(self.url(1))
         self.assertIn(response.status_code, [401, 403])
 
+
 class PatientRegisterViewTests(APITestCase):
 
     def setUp(self):
@@ -2389,7 +2396,7 @@ def test_ANDALUCIA_valid_3(self):
         nombre_completo = "Mª DEL MAR GÁLVEZ CLEMENTE"
         num_colegiado = "20"
         self.assertTrue(validar_colegiacion(nombre_completo,num_colegiado, comunidad_autonoma))
-        
+
     def test_ANDALUCIA_valid_4(self):
         comunidad_autonoma = "ANDALUCIA"
         nombre_completo = "ESTEFANÍA RAMA MORENO"
@@ -2401,7 +2408,7 @@ def test_ANDALUCIA_INvalid_1(self):
         nombre_completo = "ALFONSO REINA MARTÍNEZ"
         num_colegiado = "13092"
         self.assertFalse(validar_colegiacion(nombre_completo,num_colegiado, comunidad_autonoma))
-        
+
     def test_ANDALUCIA_INvalid_2(self):
         comunidad_autonoma = "ANDALUCIA"
         nombre_completo = "PABLO CARTAS MARTÍNEZ"
@@ -2501,14 +2508,14 @@ def test_BALEARES_INvalid_2(self):
         nombre_completo = "Carlotta Elisabeth Gelabert Doran"
         num_colegiado = "1233"
         self.assertFalse(validar_colegiacion(nombre_completo,num_colegiado, comunidad_autonoma))
-        
+
     # TESTS CANARIAS
     def test_CANARIAS_valid_1(self):
         comunidad_autonoma = "CANARIAS"
         nombre_completo = "ANTONIO JESUS FERNANDEZ VILAR"
         num_colegiado = "7"
         self.assertTrue(validar_colegiacion(nombre_completo,num_colegiado, comunidad_autonoma))
-        
+
     def test_CANARIAS_valid_2(self):
         comunidad_autonoma = "CANARIAS"
         nombre_completo = "UTE ROSS"
@@ -2650,7 +2657,7 @@ def test_MURCIA_INvalid_1(self):
         nombre_completo = "JUAN MANUEL ACOSTA RODRÍGUEZ"
         num_colegiado = "277"
         self.assertFalse(validar_colegiacion(nombre_completo,num_colegiado, comunidad_autonoma))
-        
+
     # TESTS NAVARRA
     def test_NAVARRA_valid_1(self):
         comunidad_autonoma = "NAVARRA"
@@ -2689,7 +2696,7 @@ def test_COMUNIDAD_VALENCIANA_INvalid_1(self):
         nombre_completo = "VICENTA FORTUNY ALMUDÉVER"
         num_colegiado = "4"
         self.assertFalse(validar_colegiacion(nombre_completo,num_colegiado, comunidad_autonoma))
-"""
+
 
 class VideoModelTests(TestCase):
     def setUp(self):
@@ -2736,7 +2743,6 @@ def setUp(self):
 
     # --- Pruebas de campos y relaciones ---
     def test_video_creation(self):
-        """Verifica que un video se crea correctamente con los campos básicos."""
         self.assertEqual(self.video.title, "Video de prueba")
         self.assertEqual(self.video.description, "Descripción del video")
         self.assertEqual(self.video.file_key, "videos/test_video.mp4")
@@ -2744,7 +2750,6 @@ def test_video_creation(self):
         self.assertIsInstance(self.video.uploaded_at, datetime)
 
     def test_file_key_unique_constraint(self):
-        """Verifica que no se puedan crear dos videos con el mismo file_key."""
         with self.assertRaises(Exception):  # Puede ser IntegrityError o ValidationError según la DB
             Video.objects.create(
                 physiotherapist=self.physio,
@@ -2753,17 +2758,14 @@ def test_file_key_unique_constraint(self):
             )
 
     def test_many_to_many_patients(self):
-        """Verifica que se puedan asociar pacientes a un video."""
         self.video.patients.add(self.patient)
         self.assertIn(self.patient, self.video.patients.all())
         self.assertIn(self.video, self.patient.videos.all())
 
     def test_blank_patients(self):
-        """Verifica que el campo patients puede estar vacío."""
         self.assertEqual(self.video.patients.count(), 0)  # Por defecto está vacío
 
     def test_blank_description(self):
-        """Verifica que description puede ser blank o null."""
         video = Video.objects.create(
             physiotherapist=self.physio,
             title="Sin descripción",
@@ -2772,19 +2774,16 @@ def test_blank_description(self):
         self.assertIsNone(video.description)
 
     def test_on_delete_cascade(self):
-        """Verifica que eliminar un fisioterapeuta elimina sus videos."""
         video_id = self.video.id
         self.physio.delete()
         self.assertFalse(Video.objects.filter(id=video_id).exists())
 
     # --- Pruebas de métodos ---
     def test_str_method(self):
-        """Verifica que __str__ devuelve el título del video."""
         self.assertEqual(str(self.video), "Video de prueba")
 
     @patch('boto3.client')
     def test_delete_from_storage_success(self, mock_boto_client):
-        """Verifica que delete_from_storage elimina el archivo de S3."""
         mock_s3 = MagicMock()
         mock_boto_client.return_value = mock_s3
         self.video.delete_from_storage()
@@ -2795,7 +2794,6 @@ def test_delete_from_storage_success(self, mock_boto_client):
 
     @patch('boto3.client')
     def test_delete_from_storage_failure(self, mock_boto_client):
-        """Verifica que delete_from_storage maneja excepciones correctamente."""
         mock_s3 = MagicMock()
         mock_s3.delete_object.side_effect = Exception("Error de S3")
         mock_boto_client.return_value = mock_s3
@@ -2804,13 +2802,11 @@ def test_delete_from_storage_failure(self, mock_boto_client):
             mock_print.assert_called_once_with("Error al eliminar el archivo de Spaces: Error de S3")
 
     def test_file_url_property(self):
-        """Verifica que file_url genera la URL correcta."""
         expected_url = "https://fisiofind-repo.fra1.digitaloceanspaces.com/videos/test_video.mp4"
         self.assertEqual(self.video.file_url, expected_url)
 
     # --- Pruebas adicionales ---
     def test_max_length_title(self):
-        """Verifica que el título no exceda los 255 caracteres."""
         long_title = "a" * 256
         with self.assertRaises(ValidationError):
             video = Video(
@@ -2821,7 +2817,6 @@ def test_max_length_title(self):
             video.full_clean()  # Lanza ValidationError si excede max_length
 
     def test_max_length_file_key(self):
-        """Verifica que file_key no exceda los 500 caracteres."""
         long_key = "a" * 501
         with self.assertRaises(ValidationError):
             video = Video(
@@ -2863,7 +2858,6 @@ def setUp(self):
         )
 
     def get_valid_data(self):
-        """Datos válidos base para las pruebas."""
         video_file = SimpleUploadedFile("test.mp4", b"file_content", content_type="video/mp4")
         return {
             "title": "Test Video",
@@ -2874,7 +2868,6 @@ def get_valid_data(self):
 
     # --- Pruebas de campos y serialización ---
     def test_read_only_file_key(self):
-        """Verifica que file_key no se pueda modificar."""
         data = self.get_valid_data()
         data["file_key"] = "videos/hack.mp4"  # Intento de sobrescribir
         serializer = VideoSerializer(data=data, context={"request": self.request})
@@ -2883,20 +2876,17 @@ def test_read_only_file_key(self):
         self.assertNotEqual(video.file_key, "videos/hack.mp4")  # Debe ignorar el valor enviado
 
     def test_file_url_serialization(self):
-        """Verifica que file_url se serializa correctamente."""
         serializer = VideoSerializer(instance=self.video)
         expected_url = f"https://fisiofind-repo.fra1.digitaloceanspaces.com/videos/1/initial.mp4"  # Ajusta según settings
         self.assertEqual(serializer.data["file_url"], expected_url)
 
     # --- Pruebas de validación ---
     def test_validate_file_valid(self):
-        """Verifica que un archivo .mp4 pasa la validación."""
         data = self.get_valid_data()
         serializer = VideoSerializer(data=data, context={"request": self.request})
         self.assertTrue(serializer.is_valid(), serializer.errors)
 
     def test_validate_file_invalid_extension(self):
-        """Verifica que un archivo no .mp4 falla la validación."""
         invalid_file = SimpleUploadedFile("test.jpg", b"file_content", content_type="image/jpeg")
         data = self.get_valid_data()
         data["file"] = invalid_file
@@ -2908,7 +2898,6 @@ def test_validate_file_invalid_extension(self):
     # --- Pruebas de creación ---
     @patch('boto3.client')
     def test_create_success(self, mock_boto_client):
-        """Verifica que se crea un video correctamente."""
         mock_s3 = MagicMock()
         mock_boto_client.return_value = mock_s3
         data = self.get_valid_data()
@@ -2922,7 +2911,6 @@ def test_create_success(self, mock_boto_client):
 
     @patch('boto3.client')
     def test_create_upload_failure(self, mock_boto_client):
-        """Verifica que falla si la subida a S3 falla."""
         mock_s3 = MagicMock()
         mock_s3.upload_fileobj.side_effect = Exception("Error de red")
         mock_boto_client.return_value = mock_s3
@@ -2936,7 +2924,6 @@ def test_create_upload_failure(self, mock_boto_client):
     # --- Pruebas de actualización ---
     @patch('boto3.client')
     def test_update_metadata_only(self, mock_boto_client):
-        """Verifica que se pueden actualizar título y descripción sin cambiar el archivo."""
         data = {"title": "Nuevo título", "description": "Nueva descripción"}
         serializer = VideoSerializer(instance=self.video, data=data, context={"request": self.request}, partial=True)
         self.assertTrue(serializer.is_valid(), serializer.errors)
@@ -2948,7 +2935,6 @@ def test_update_metadata_only(self, mock_boto_client):
 
     @patch('boto3.client')
     def test_update_with_new_file(self, mock_boto_client):
-        """Verifica que se actualiza el archivo correctamente."""
         mock_s3 = MagicMock()
         mock_boto_client.return_value = mock_s3
         new_file = SimpleUploadedFile("new.mp4", b"new_content", content_type="video/mp4")
@@ -2966,7 +2952,6 @@ def test_update_with_new_file(self, mock_boto_client):
 
     @patch('boto3.client')
     def test_update_delete_failure(self, mock_boto_client):
-        """Verifica que falla si no se puede eliminar el archivo anterior."""
         mock_s3 = MagicMock()
         mock_s3.delete_object.side_effect = Exception("Error al borrar")
         mock_boto_client.return_value = mock_s3
@@ -2980,7 +2965,6 @@ def test_update_delete_failure(self, mock_boto_client):
 
     # --- Pruebas de casos límite ---
     def test_create_without_patients(self):
-        """Verifica que se puede crear un video sin pacientes."""
         data = self.get_valid_data()
         del data["patients"]
         with patch('boto3.client') as mock_boto_client:
@@ -2992,9 +2976,9 @@ def test_create_without_patients(self):
             self.assertEqual(video.patients.count(), 0)
 
     def test_invalid_patient_id(self):
-        """Verifica que falla si se pasa un ID de paciente inexistente."""
         data = self.get_valid_data()
         data["patients"] = [999]  # ID inexistente
         serializer = VideoSerializer(data=data, context={"request": self.request})
         self.assertFalse(serializer.is_valid())
         self.assertIn("patients", serializer.errors)
+"""
diff --git a/backend/fisio_find/users/urls.py b/backend/fisio_find/users/urls.py
index fc430a59..592135d8 100644
--- a/backend/fisio_find/users/urls.py
+++ b/backend/fisio_find/users/urls.py
@@ -1,5 +1,20 @@
 from django.urls import path
-from .views import *
+from .views import (
+    patient_register_view,
+    custom_token_obtain_view,
+    logout_view,
+    check_role_view,
+    physio_register_view,
+    validate_physio_registration,
+    process_payment,
+    physio_update_view,
+    physio_create_service_view,
+    physio_update_service_view,
+    physio_delete_service_view,
+    PatientProfileView,
+    return_user,
+    physio_get_services_view,
+)
 from django.conf import settings
 from django.conf.urls.static import static
 
@@ -18,13 +33,7 @@
     path('profile/', PatientProfileView.as_view(), name='profile'),
     path('current-user/', return_user, name='current_user'),
 
-    path('videos/upload/', create_file, name='upload_video'),
-    path('videos/delete/<int:video_id>/', delete_video, name='delete_video'),
-    path('videos/list-my-videos/', list_my_videos, name='list_my_videos'),
-    path('videos/stream-video/<int:video_id>/', stream_video, name='stream_video'),
-    path('videos/update-video/<int:video_id>/', update_video, name='update_video'),
-
     path('services/<int:physio_id>/', physio_get_services_view, name='physio_get_xservices'),
-    
+
 ]
-urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
\ No newline at end of file
+urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
diff --git a/backend/fisio_find/users/util.py b/backend/fisio_find/users/util.py
index 46d685b8..638302ac 100644
--- a/backend/fisio_find/users/util.py
+++ b/backend/fisio_find/users/util.py
@@ -1,29 +1,34 @@
 import re
 import json
 
+
 def validate_dni_structure(data_dni):
     dni_pattern = re.compile(r'^\d{8}[A-HJ-NP-TV-Z]$')
     return dni_pattern.match(data_dni)
 
+
 def validate_dni_match_letter(data_dni):
     # Validar que la letra del DNI es correcta
     try:
         dni_numbers = data_dni[:-1]
         dni_letter = data_dni[-1].upper()
-        
+
         letters = "TRWAGMYFPDXBNJZSQVHLCKE"
         return letters[int(dni_numbers) % 23] != dni_letter
     except ValueError:
         return False
 
+
 def codigo_postal_no_mide_5(postal_code):
     return len(postal_code) != 5
 
+
 def telefono_no_mide_9(phone_number):
     return len(phone_number) != 9
 
+
 def check_service_json(service_json):
-    
+
     # Comprueba si la entrada es str o dict, si es str intenta parsearlo
     # si es dict directamente lo coge, sino simplemente lanza error
     if isinstance(service_json, str):
@@ -42,34 +47,35 @@ def check_service_json(service_json):
 
     if not isinstance(new_service["title"], str):
         raise json.JSONDecodeError()
-    
+
     if not isinstance(new_service["description"], str):
         raise json.JSONDecodeError()
-    
-    if not isinstance(new_service["tipo"], str) or new_service["tipo"] not in ["PRIMERA_CONSULTA", "CONTINUAR_TRATAMIENTO", "OTRO"]:
+
+    if not isinstance(new_service["tipo"], str) or new_service["tipo"] not in ["PRIMERA_CONSULTA",
+                                                                               "CONTINUAR_TRATAMIENTO", "OTRO"]:
         raise json.JSONDecodeError()
-    
+
     if new_service["tipo"] == "PRIMERA_CONSULTA" and new_service["title"] != "Primera consulta":
         raise json.JSONDecodeError()
-    
+
     if new_service["tipo"] == "CONTINUAR_TRATAMIENTO" and new_service["title"] != "Continuación de tratamiento":
         raise json.JSONDecodeError()
-    
+
     if not isinstance(new_service["price"], int):
         raise json.JSONDecodeError()
-    
+
     if not isinstance(new_service["duration"], int):
         raise json.JSONDecodeError()
-    
+
     if new_service["custom_questionnaire"] not in [None, {}]:
-        
+
         if not isinstance(new_service["custom_questionnaire"],dict):
             raise json.JSONDecodeError()
         elif not "UI Schema" in new_service["custom_questionnaire"]:
             raise json.JSONDecodeError()
         elif not isinstance(new_service["custom_questionnaire"]["UI Schema"],dict):
             raise json.JSONDecodeError()
-            
+
         questionaries = new_service["custom_questionnaire"]["UI Schema"]
         if not "type" in questionaries or questionaries["type"] != "Group":
             raise json.JSONDecodeError()
@@ -77,12 +83,12 @@ def check_service_json(service_json):
             raise json.JSONDecodeError()
         elif not "elements" in questionaries or not isinstance(questionaries["elements"],list):
             raise json.JSONDecodeError()
-            
+
         questionaries = questionaries["elements"]
         necesary = [{'type': 'Number', 'label': 'Peso (kg)', 'scope': '#/properties/peso'}, {'type': 'Number', 'label': 'Altura (cm)', 'scope': '#/properties/altura'}, {'type': 'Number', 'label': 'Edad', 'scope': '#/properties/edad'}, {'type': 'Control', 'label': 'Nivel de actividad física', 'scope': '#/properties/actividad_fisica'}, {'type': 'Control', 'label': 'Motivo de la consulta', 'scope': '#/properties/motivo_consulta'}]
-       
+
         removed_duplicated = set()
-       
+
         for quest in questionaries:
             if quest in necesary:
                 necesary.remove(quest)
@@ -100,9 +106,10 @@ def check_service_json(service_json):
         elif len(questionaries) != len(removed_duplicated):
             # Hay duplicados
             raise json.JSONDecodeError()
-    
+
     return new_service
 
+
 def validate_service_with_questionary(selected_service, physio_services):
     try:
         selected_id = selected_service["id"]
diff --git a/backend/fisio_find/users/validacionFisios.py b/backend/fisio_find/users/validacionFisios.py
index c0a7d1f0..35b89de0 100644
--- a/backend/fisio_find/users/validacionFisios.py
+++ b/backend/fisio_find/users/validacionFisios.py
@@ -2,8 +2,6 @@
 from selenium.webdriver.chrome.service import Service
 from selenium.webdriver.common.by import By
 from selenium.webdriver.common.keys import Keys
-from selenium.webdriver.support.ui import WebDriverWait
-from selenium.webdriver.support import expected_conditions as EC
 from webdriver_manager.chrome import ChromeDriverManager
 from bs4 import BeautifulSoup
 import time
@@ -15,18 +13,19 @@
 # Cargar las variables de entorno desde el archivo .env
 load_dotenv()
 
+
 class SeleniumScraper:
-    
+
     def __init__(self):
-        
+
         if os.getenv("DEBUG") == "True":
             # Configurar Selenium (se requiere que JS esté habilitado)
             options = webdriver.ChromeOptions()
             options.add_argument("--headless")  # Ejecutar en segundo plano
-            options.add_argument("--no-sandbox")  
+            options.add_argument("--no-sandbox")
             options.add_argument("--enable-javascript")  # Asegurar que JS está habilitado
-            options.add_argument("--disable-dev-shm-usage")  
-            
+            options.add_argument("--disable-dev-shm-usage")
+
             # Inicializar WebDriver
             self.driver = webdriver.Chrome(
                 service=Service(executable_path=ChromeDriverManager().install()),
@@ -35,11 +34,11 @@ def __init__(self):
         else:
             options = webdriver.ChromeOptions()
             options.add_argument("--headless")  # Ejecutar en segundo plano
-            options.add_argument("--no-sandbox")  
+            options.add_argument("--no-sandbox")
             options.add_argument("--enable-javascript")  # Asegurar que JS está habilitado
-            options.add_argument("--disable-gpu")  
+            options.add_argument("--disable-gpu")
             options.add_argument("--window-size=1920x1080")
-            options.add_argument("--disable-dev-shm-usage") 
+            options.add_argument("--disable-dev-shm-usage")
             options.binary_location = "/usr/bin/chromium-browser"
 
             # Especificar ruta manual de ChromeDriver
@@ -50,39 +49,40 @@ def __init__(self):
                 service=Service(executable_path=chromedriver_path),
                 options=options
             )
-            
 
-    def obtener_colegiado(self, valorBusqueda: str, url: str, xpath: str, loadTime: int = 2, general: str = None) -> BeautifulSoup:
+    def obtener_colegiado(
+      self, valorBusqueda: str, url: str, xpath: str, loadTime: int = 2, general: str = None) -> BeautifulSoup:
         self.driver.get(url)
         time.sleep(loadTime)  # Esperar que cargue la página
-        
+
         if "murcia" in url:
             num_sort = self.driver.find_element(By.XPATH, '//*[@id="myTable"]/thead/tr/th[3]')
             num_sort.click()
-            
+
         if general == "navarra":
             select = self.driver.find_element(By.XPATH, '//*[@id="colegio"]/option[16]')
             select.click()
-        
+
         if general == "canarias":
             select = self.driver.find_element(By.XPATH, '//*[@id="colegio"]/option[6]')
             select.click()
-            
+
         if general == "castilla y leon":
             select = self.driver.find_element(By.XPATH, '//*[@id="colegio"]/option[9]')
             select.click()
-    
+
         search_box = self.driver.find_element(By.XPATH, xpath)
         search_box.send_keys(valorBusqueda)  # Ingresar valor de búsqueda (nombre o número)
         search_box.send_keys(Keys.RETURN)
-        
+
         # Para el caso de castilla y leon, hay que hacer click en el botón de buscar
         if "cpfcyl" in url:
-            search_div = self.driver.find_element(By.XPATH, '//*[@id="cdk-accordion-child-0"]/div/form/div/web-loading-button/button/span[1]/div')
+            xpath = '//*[@id="cdk-accordion-child-0"]/div/form/div/web-loading-button/button/span[1]/div'
+            search_div = self.driver.find_element(By.XPATH, xpath)
             search_div.click()
-    
+
         time.sleep(2)  # Esperar la carga de resultados
-    
+
         soup = BeautifulSoup(self.driver.page_source, "html.parser")
         return soup
 
@@ -124,7 +124,7 @@ def validar_colegiacion(nombre: str, numero: str, comunidad: str) -> bool:
                 except Exception as e:
                     print(f"⚠️ Error durante la validación en {comunidad}: {e}")
                     return False
-            
+
             case "aragon":  # Por número y nombre
                 url = "https://ventanilla.colfisioaragon.org/buscador-colegiados"
                 try:
@@ -138,7 +138,7 @@ def validar_colegiacion(nombre: str, numero: str, comunidad: str) -> bool:
                 except Exception as e:
                     print(f"⚠️ Error durante la validación en {comunidad}: {e}")
                     return False
-            
+
             case "asturias":  # Por número y nombre
                 url = "https://www.cofispa.org/censo-colegiados"
                 try:
@@ -147,7 +147,7 @@ def validar_colegiacion(nombre: str, numero: str, comunidad: str) -> bool:
                     if resultado:
                         datos = [td.text.strip() for td in resultado.find_all("td")]
                         datos = quitar_tildes(f"{datos[1]} {datos[2]} {datos[3]}")
-                        if("Mª" in datos):
+                        if ("Mª" in datos):
                             datos = datos.replace("Mª", "MARIA")
                         return datos == quitar_tildes(nombre)
                     else:
@@ -155,7 +155,7 @@ def validar_colegiacion(nombre: str, numero: str, comunidad: str) -> bool:
                 except Exception as e:
                     print(f"⚠️ Error durante la validación en {comunidad}: {e}")
                     return False
-            
+
             case "baleares":  # Por número y nombre
                 url = "http://www.colfisiobalear.org/es/area-social-y-ciudadana/profesionales-colegiados/"
                 try:
@@ -170,7 +170,7 @@ def validar_colegiacion(nombre: str, numero: str, comunidad: str) -> bool:
                 except Exception as e:
                     print(f"⚠️ Error durante la validación en {comunidad}: {e}")
                     return False
-            
+
             case "canarias":  # Por número y nombre (NO FUNCIONA CON HEADLESS)
                 url = "https://www.consejo-fisioterapia.org/vu_colegiados.html"
                 try:
@@ -184,7 +184,7 @@ def validar_colegiacion(nombre: str, numero: str, comunidad: str) -> bool:
                 except Exception as e:
                     print(f"⚠️ Error durante la validación en {comunidad}: {e}")
                     return False
-            
+
             case "cantabria":  # Por número y nombre
                 url = "https://colfisiocant.org/busqueda-profesionales/"
                 try:
@@ -200,7 +200,7 @@ def validar_colegiacion(nombre: str, numero: str, comunidad: str) -> bool:
                 except Exception as e:
                     print(f"⚠️ Error durante la validación en {comunidad}: {e}")
                     return False
-            
+
             case "castilla-la mancha":  # Por número y nombre
                 url = "https://www.coficam.org/ventanilla-unica/censo-colegial"
                 try:
@@ -216,7 +216,7 @@ def validar_colegiacion(nombre: str, numero: str, comunidad: str) -> bool:
                 except Exception as e:
                     print(f"⚠️ Error durante la validación en {comunidad}: {e}")
                     return False
-            
+
             case "castilla y leon":  # Por número y nombre (NO FUNCIONA CON HEADLESS)
                 url = "https://www.consejo-fisioterapia.org/vu_colegiados.html"
                 try:
@@ -230,7 +230,7 @@ def validar_colegiacion(nombre: str, numero: str, comunidad: str) -> bool:
                 except Exception as e:
                     print(f"⚠️ Error durante la validación en {comunidad}: {e}")
                     return False
-            
+
             case "cataluña":  # Por número y nombre
                 url = "https://www.fisioterapeutes.cat/es/ciudadanos/profesionales"
                 try:
@@ -246,11 +246,12 @@ def validar_colegiacion(nombre: str, numero: str, comunidad: str) -> bool:
                 except Exception as e:
                     print(f"⚠️ Error durante la validación en {comunidad}: {e}")
                     return False
-            
+
             case "extremadura":  # Por número y nombre
                 url = "https://cofext.org/cms/colegiados.php"
                 try:
-                    soup = scraper.obtener_colegiado(quitar_tildes(nombre), url, '//*[@id="example_filter"]/label/input')
+                    xpath = '//*[@id="example_filter"]/label/input'
+                    soup = scraper.obtener_colegiado(quitar_tildes(nombre), url, xpath)
                     resultado = soup.find("tr", class_="odd")
                     if resultado:
                         datos = [td.text.strip() for td in resultado.find_all("td")]
@@ -260,7 +261,7 @@ def validar_colegiacion(nombre: str, numero: str, comunidad: str) -> bool:
                 except Exception as e:
                     print(f"⚠️ Error durante la validación en {comunidad}: {e}")
                     return False
-            
+
             case "galicia":  # Por número y nombre
                 url = "https://www.cofiga.org/ciudadanos/colegiados"
                 try:
@@ -276,13 +277,15 @@ def validar_colegiacion(nombre: str, numero: str, comunidad: str) -> bool:
                 except Exception as e:
                     print(f"⚠️ Error durante la validación en {comunidad}: {e}")
                     return False
-            
-            case "la rioja":  # Por número y nombre (se haría por nombre, pero si se rellena el número con ceros, no debe haber problema)
+
+            # Por número y nombre (se haría por nombre, pero si se rellena el número con ceros, no debe haber problema)
+            case "la rioja":
                 url = "https://www.coflarioja.org/ciudadanos/listado-de-fisioterapeutas/buscar-colegiados"
                 try:
                     while len(numero) < 4:
                         numero = "0" + numero
-                    soup = scraper.obtener_colegiado(numero, url, '//*[@id="busqueda-colegiados-search-input"]/div/input')
+                    xpath = '//*[@id="busqueda-colegiados-search-input"]/div/input'
+                    soup = scraper.obtener_colegiado(numero, url, xpath)
                     resultado = soup.find("tbody").tr
                     if resultado:
                         datos = [td.text.strip() for td in resultado.find_all("td")]
@@ -295,11 +298,12 @@ def validar_colegiacion(nombre: str, numero: str, comunidad: str) -> bool:
                 except Exception as e:
                     print(f"⚠️ Error durante la validación en {comunidad}: {e}")
                     return False
-            
+
             case "madrid":  # Por nombre
                 url = "https://cfisiomad.com/#/ext/buscarcolegiado"
                 try:
-                    soup = scraper.obtener_colegiado(nombre, url, "/html/body/app-root/app-externos/section/div/app-search-collegiate/div/div/form/input[1]", 10)
+                    xpath = "/html/body/app-root/app-externos/section/div/app-search-collegiate/div/div/form/input[1]"
+                    soup = scraper.obtener_colegiado(nombre, url, xpath, 10)
                     resultado = soup.find("tbody").tr
                     if resultado:
                         datos = [td.text.strip() for td in resultado.find_all("td")]
@@ -309,7 +313,7 @@ def validar_colegiacion(nombre: str, numero: str, comunidad: str) -> bool:
                 except Exception as e:
                     print(f"⚠️ Error durante la validación en {comunidad}: {e}")
                     return False
-            
+
             case "murcia":  # Por número y nombre
                 url = "https://cfisiomurcia.com/buscador-de-colegiados/"
                 try:
@@ -326,7 +330,7 @@ def validar_colegiacion(nombre: str, numero: str, comunidad: str) -> bool:
                 except Exception as e:
                     print(f"⚠️ Error durante la validación en {comunidad}: {e}")
                     return False
-            
+
             case "navarra":  # Por nombre, se valida en la web general de colegiados
                 url = "https://www.consejo-fisioterapia.org/vu_colegiados.html"
                 try:
@@ -340,7 +344,7 @@ def validar_colegiacion(nombre: str, numero: str, comunidad: str) -> bool:
                 except Exception as e:
                     print(f"⚠️ Error durante la validación en {comunidad}: {e}")
                     return False
-            
+
             case "pais vasco":  # Por nombre (TARDA MUCHO)
                 url = "https://cofpv.org/es/colegiados.asp"
                 try:
@@ -354,11 +358,12 @@ def validar_colegiacion(nombre: str, numero: str, comunidad: str) -> bool:
                 except Exception as e:
                     print(f"⚠️ Error durante la validación en {comunidad}: {e}")
                     return False
-            
+
             case "comunidad valenciana":  # Por número y nombre
                 url = "https://app.colfisiocv.com/college/collegiatelist/"
                 try:
-                    soup = scraper.obtener_colegiado(numero, url, '//*[@id="root"]/div/div[2]/div[3]/div/div[2]/div/div[1]/div[1]/div[2]/input')
+                    xpath = '//*[@id="root"]/div/div[2]/div[3]/div/div[2]/div/div[1]/div[1]/div[2]/input'
+                    soup = scraper.obtener_colegiado(numero, url, xpath)
                     resultado = soup.find("tr", class_="bg-white border-b")
                     if resultado:
                         datos = [td.text.strip() for td in resultado.find_all("td")]
@@ -369,7 +374,7 @@ def validar_colegiacion(nombre: str, numero: str, comunidad: str) -> bool:
                 except Exception as e:
                     print(f"⚠️ Error durante la validación en {comunidad}: {e}")
                     return False
-            
+
             case _:
                 return False
     finally:
diff --git a/backend/fisio_find/users/views.py b/backend/fisio_find/users/views.py
index e9f5f3e5..10ee3c43 100644
--- a/backend/fisio_find/users/views.py
+++ b/backend/fisio_find/users/views.py
@@ -3,13 +3,8 @@
 from rest_framework.response import Response
 from rest_framework import status, generics
 from rest_framework.permissions import AllowAny, IsAuthenticated
-from rest_framework import generics
-from django.http import HttpResponse, StreamingHttpResponse
 from django.conf import settings
 from django.shortcuts import get_object_or_404
-from django.conf import settings
-
-import boto3
 import logging
 import stripe
 import json
@@ -20,25 +15,19 @@
     PhysioSerializer,
     PatientSerializer,
     AppUserSerializer,
-    VideoSerializer,
-    PhysioUpdateSerializer,
-)
+    PhysioUpdateSerializer
+    )
 
-from .models import (
-    Physiotherapist, 
-    Patient, 
-    AppUser, 
-    Video , 
-    Specialization)
+from .models import Physiotherapist, Patient, Specialization
 
 from .permissions import (
     IsPatient,
     IsPhysiotherapist,
-    IsPhysioOrPatient,
-    IsAdmin,
 )
+
 from users.util import check_service_json
 
+
 class PatientProfileView(generics.RetrieveAPIView):
     permission_classes = [IsPatient]
 
@@ -72,6 +61,7 @@ def patch(self, request, *args, **kwargs):
         except Patient.DoesNotExist:
             return Response({"error": "Perfil de paciente no encontrado"}, status=status.HTTP_404_NOT_FOUND)
 
+
 @api_view(['POST'])
 @permission_classes([AllowAny])
 def patient_register_view(request):
@@ -142,12 +132,13 @@ def validate_physio_registration(request):
         return Response({"valid": True}, status=status.HTTP_200_OK)
     return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 
+
 @api_view(['POST'])
 @permission_classes([AllowAny])
 def physio_register_view(request):
     serializer = PhysioRegisterSerializer(data=request.data)
     if serializer.is_valid():
-        
+
         serializer.save()
         return Response({"message": "Fisioteraputa registrado correctamente"}, status=status.HTTP_201_CREATED)
     return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -168,7 +159,6 @@ def process_payment(request):
     payment_method_id = request.data.get("payment_method_id")
     amount = request.data.get("amount")
     currency = request.data.get("currency", "eur")
-    
 
     if not payment_method_id or not amount:
         print("estamos aqui")
@@ -192,9 +182,7 @@ def process_payment(request):
             automatic_payment_methods={
                 'enabled': True,
                 'allow_redirects': 'never'  # Evita métodos de redirección
-},
-            
-            
+            },
         )
 
         if intent.status == "succeeded":
@@ -205,7 +193,8 @@ def process_payment(request):
                 "payment_intent_client_secret": intent.client_secret
             }, status=status.HTTP_200_OK)
         else:
-            return Response({"error": "El pago no fue exitoso.", "status": intent.status}, status=status.HTTP_400_BAD_REQUEST)
+            return Response({"error": "El pago no fue exitoso.", "status": intent.status},
+                            status=status.HTTP_400_BAD_REQUEST)
 
     except stripe.error.CardError as e:
         print("error de tarjeta")
@@ -219,10 +208,10 @@ def process_payment(request):
 @permission_classes([IsPhysiotherapist])
 def physio_update_view(request):
     physio = get_object_or_404(Physiotherapist, user=request.user)
-    
+
     # Extraer especializaciones primero para manejarlas por separado
     specializations_data = None
-    
+
     # Procesar el campo specializations
     if "specializations" in request.data:
         if isinstance(request.data["specializations"], str):
@@ -238,7 +227,7 @@ def physio_update_view(request):
                     specializations_data = [request.data["specializations"]]
         elif isinstance(request.data["specializations"], list):
             specializations_data = request.data["specializations"]
-    
+
     # Preparar datos para el serializador, excluyendo las especializaciones
     request_data = {}
     for key, value in request.data.items():
@@ -252,27 +241,26 @@ def physio_update_view(request):
     # Ensure services are parsed as JSON if provided
     if "services" in request_data and isinstance(request_data["services"], str):
         try:
-            # Comprueba estructura y parametros obligatorios de 
+            # Comprueba estructura y parametros obligatorios de
             if isinstance(request_data["services"], str):
                 request_data["services"] = json.loads(request_data["services"])
             elif isinstance(request_data["services"], dict):
                 request_data["services"] = request_data["services"]
             else:
                 raise json.JSONDecodeError()
-            
+
             lista_ids = set()
-            
+
             for key, service in request_data["services"].items():
                 service = check_service_json(service)
-                if "id" not in service or service["id"] == None or not isinstance(service["id"],int):
+                if "id" not in service or service["id"] == None or not isinstance(service["id"], int):
                     raise json.JSONDecodeError()
                 lista_ids.add(service["id"])
-                
+
             if len(lista_ids) != len(request_data["services"]):
                 # Hay ids repetidos y el id tiene que ser unico
                 raise json.JSONDecodeError()
 
-                
         except json.JSONDecodeError:
             return Response({"error": "Formato de servicios inválido."}, status=status.HTTP_400_BAD_REQUEST)
         except Exception:
@@ -284,7 +272,7 @@ def physio_update_view(request):
     if serializer.is_valid():
         # Guardar primero los datos principales
         updated_physio = serializer.save()
-        
+
         # Actualizar especializaciones
         if specializations_data is not None:
             try:
@@ -294,29 +282,29 @@ def physio_update_view(request):
                     # Buscamos por nombre en el modelo de Specialization
                     spec, created = Specialization.objects.get_or_create(name=spec_name)
                     specialization_ids.append(spec.id)
-                
+
                 # Ahora asignamos los IDs al campo ManyToMany
                 updated_physio.specializations.set(specialization_ids)
-                
+
                 # Guardar explícitamente después de modificar las relaciones M2M
                 updated_physio.save()
-                
+
                 # Refrescar objeto para tener los datos actualizados
                 updated_physio.refresh_from_db()
-                
+
             except Exception as e:
                 logging.error(f"Error al guardar especializaciones: {str(e)}")
-                return Response({"error": "Error al guardar especializaciones"},  
-                              status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+                return Response({"error": "Error al guardar especializaciones"},
+                                status=status.HTTP_500_INTERNAL_SERVER_ERROR)
 
         # Usar serializers para convertir los datos a JSON
         physio_serializer = PhysioSerializer(updated_physio)
         user_serializer = AppUserSerializer(updated_physio.user)
-        
+
         response_data = {
             "message": "Fisioterapeuta actualizado correctamente"
         }
-        
+
         return Response(response_data, status=status.HTTP_200_OK)
 
     return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -325,12 +313,12 @@ def physio_update_view(request):
 @api_view(['POST'])
 @permission_classes([IsPhysiotherapist])
 def physio_create_service_view(request):
-    
+
     """Crea un nuevo servicio para el fisioterapeuta autenticado o actualiza el existente"""
-    
+
     # Obtener el fisioterapeuta asociado al usuario autenticado
     physio = get_object_or_404(Physiotherapist, user=request.user)
-    
+
     # Obtener servicios existentes
     existing_services = physio.services or {}
 
@@ -341,8 +329,7 @@ def physio_create_service_view(request):
         return Response({"error": "Formato de servicios inválido."}, status=status.HTTP_400_BAD_REQUEST)
     except Exception:
         return Response({"error": "Formato de servicios inválido."}, status=status.HTTP_400_BAD_REQUEST)
-    
-    
+
     # Actualizar servicios existentes o añadir nuevos
     service_id = new_service.get('id')
     service_updated = False
@@ -359,32 +346,32 @@ def physio_create_service_view(request):
             new_id += 1
         new_service['id'] = new_id
         existing_services[str(new_id)] = new_service
-    
+
     # Preparar los datos para el serializador
     update_data = {'services': existing_services}
-    
+
     # Usar el serializador para actualización
     serializer = PhysioUpdateSerializer(physio, data=update_data, partial=True)
-    
+
     if serializer.is_valid():
         serializer.update(physio, serializer.validated_data)
-        return Response({"message": "Servicios actualizados correctamente", "services": existing_services}, status=status.HTTP_200_OK)
-    
-    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+        return Response({"message": "Servicios actualizados correctamente", "services": existing_services},
+                        status=status.HTTP_200_OK)
 
+    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 
 
 @api_view(['PUT'])
 @permission_classes([IsPhysiotherapist])
 def physio_update_service_view(request, service_id):
     """Actualiza un servicio existente para el fisioterapeuta autenticado"""
-    
+
     # Obtener el fisioterapeuta asociado al usuario autenticado
     physio = get_object_or_404(Physiotherapist, user=request.user)
-    
+
     # Obtener servicios existentes (asegurar que sea un diccionario)
     existing_services = physio.services or {}
-    
+
     # Obtener nuevos servicios del request
     try:
         # Comprueba la estructura y parametros necesarios del json de service
@@ -394,16 +381,16 @@ def physio_update_service_view(request, service_id):
         return Response({"error": "Formato de servicios inválido."}, status=status.HTTP_400_BAD_REQUEST)
     except Exception:
         return Response({"error": "Formato de servicios inválido."}, status=status.HTTP_400_BAD_REQUEST)
-    
+
     # Obtener datos del servicio a actualizar
     new_service_data = request.data
-    
+
     # Convertir service_id a string para asegurar comparación correcta
     service_id_str = str(service_id)
-    
+
     # Verificar si el servicio existe
     service_found = False
-    
+
     # Depuración
     logging.debug(f"Buscando servicio con ID: {service_id_str}")
     logging.debug(f"Servicios disponibles: {list(existing_services.keys())}")
@@ -413,7 +400,7 @@ def physio_update_service_view(request, service_id):
         # Actualizar el servicio existente
         existing_services[service_id_str].update(new_service_data)
         service_found = True
-    
+
     # Si no se encontró por ID directo, buscar por el campo 'id' dentro del objeto
     if not service_found:
         for existing_id, service_data in existing_services.items():
@@ -425,17 +412,17 @@ def physio_update_service_view(request, service_id):
                 existing_services[existing_id].update(new_service_data)
                 service_found = True
                 break
-    
+
     if not service_found:
-        return Response({"error": f"No se encontró ningún servicio con ID {service_id}"}, 
+        return Response({"error": f"No se encontró ningún servicio con ID {service_id}"},
                         status=status.HTTP_404_NOT_FOUND)
 
     # Preparar los datos para el serializador
     update_data = {'services': existing_services}
-    
+
     # Usar el serializador para actualización
     serializer = PhysioUpdateSerializer(physio, data=update_data, partial=True)
-    
+
     if serializer.is_valid():
         updated_physio = serializer.save()
         # Verificar que los cambios se guardaron
@@ -443,16 +430,17 @@ def physio_update_service_view(request, service_id):
             print("Servicios actualizados correctamente en la base de datos")
         else:
             print("ADVERTENCIA: Los servicios pueden no haberse actualizado correctamente")
-            
+
         return Response({
-            "message": "Servicio actualizado correctamente", 
+            "message": "Servicio actualizado correctamente",
             "services": existing_services,
             "updated_service_id": service_id
         }, status=status.HTTP_200_OK)
-    
+
     print("Errores en el serializador:", serializer.errors)
     return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
 
+
 @api_view(['GET'])
 @permission_classes([AllowAny])
 def physio_get_services_view(request, physio_id):
@@ -464,12 +452,13 @@ def physio_get_services_view(request, physio_id):
     }
     return Response(response_data)
 
+
 @api_view(['DELETE'])
 @permission_classes([IsAuthenticated, IsPhysiotherapist])
 def physio_delete_service_view(request, service_id):
     physio = get_object_or_404(Physiotherapist, user=request.user)
     services = physio.services or {}
-    
+
     # Buscar el servicio por su 'id' dentro del JSON anidado
     service_found = None
     for key, service in services.items():
@@ -479,236 +468,10 @@ def physio_delete_service_view(request, service_id):
 
     if not service_found:
         return Response({"error": "El servicio no existe"}, status=status.HTTP_404_NOT_FOUND)
-    
+
     # Eliminar el servicio
     del services[service_found]
     physio.services = services
     physio.save()
-    
-    return Response({"message": "Servicio eliminado correctamente", "services": services}, status=status.HTTP_200_OK)
-
-
-
-@api_view(['POST'])
-@permission_classes([IsPhysiotherapist])
-def create_file(request):
-    print("🔍 Datos recibidos:", request.data)
-
-    # Convertir request.data en un diccionario mutable (para modificar el QueryDict)
-    mutable_data = request.data.copy()
-
-    # Manejo de `patients` usando emails en lugar de IDs
-    patients_raw = mutable_data.get("patients")
-    if patients_raw:
-        if isinstance(patients_raw, str):
-            try:
-                # Se espera una cadena JSON con una lista de emails, por ejemplo: '["email1@example.com", "email2@example.com"]'
-                patients_list = json.loads(patients_raw)
-                if isinstance(patients_list, list) and all(isinstance(i, str) for i in patients_list):
-                    # Buscar usuarios que coincidan con los emails proporcionados
-                    users = AppUser.objects.filter(email__in=patients_list)
-                    found_emails = set(users.values_list("email", flat=True))
-                    missing_emails = set(patients_list) - found_emails
-                    if missing_emails:
-                        return Response(
-                            {"errorManaged": f"Usuarios no encontrados para emails: {', '.join(list(missing_emails))}"},
-                            status=status.HTTP_400_BAD_REQUEST
-                        )
-                    
-                    # Obtener los pacientes asociados a esos usuarios
-                    patients = Patient.objects.filter(user__in=users)
-                    patients_found_emails = set(patients.values_list("user__email", flat=True))
-                    missing_patients = found_emails - patients_found_emails
-                    if missing_patients:
-                        return Response(
-                            {"errorManaged": f"Paciente no encontrado para usuarios con emails: {', '.join(list(missing_patients))}"},
-                            status=status.HTTP_400_BAD_REQUEST
-                        )
-                    
-                    # Convertir el queryset a una lista de IDs de Patient
-                    patient_ids = list(patients.values_list("id", flat=True))
-                    mutable_data.setlist("patients", patient_ids)
-                else:
-                    return Response(
-                        {"errorManaged": "Formato de patients incorrecto, debe ser una lista de emails"},
-                        status=status.HTTP_400_BAD_REQUEST
-                    )
-            except json.JSONDecodeError:
-                return Response(
-                    {"errorManaged": "Formato de patients inválido"},
-                    status=status.HTTP_400_BAD_REQUEST
-                )
-
-    print("📌 Datos después de procesar:", mutable_data)  # Para depuración
-
-    # Pasamos mutable_data en lugar de request.data al serializer
-    serializer = VideoSerializer(data=mutable_data, context={"request": request})
-
-    if serializer.is_valid():
-        video = serializer.save()
-        return Response(
-            {
-                "message": "Archivo creado correctamente",
-                "video": VideoSerializer(video).data
-            },
-            status=status.HTTP_201_CREATED
-        )
-
-    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-
-
-@api_view(['DELETE'])
-@permission_classes([IsPhysiotherapist])
-def delete_video(request, video_id):
-    user = request.user
-    print(user.physio.id)  # Depuración
-    print(f"Usuario autenticado: {user}, Rol: {getattr(user, 'physiotherapist', None)}")  # Depuración
-    try:
-        video = Video.objects.get(id=video_id)
-        print(f"Video encontrado: {video}")  # Depuración
-    
-        if not hasattr(user, 'physio') or video.physiotherapist.id != user.physio.id:
-            return Response({"error": "No tienes permiso para eliminar este video"}, status=status.HTTP_403_FORBIDDEN)
-
-        video.delete_from_storage()
-        video.delete()
-
-        return Response({"message": "Video eliminado correctamente"}, status=status.HTTP_200_OK)
-
-    except Video.DoesNotExist:
-        return Response({"error": "Video no encontrado"}, status=status.HTTP_404_NOT_FOUND)
-    
-
-@api_view(['GET'])
-@permission_classes([IsPhysioOrPatient])  
-def list_my_videos(request):
-    user = request.user
-
-    try:
-        if hasattr(user, 'patient'):
-            print("Patient:", user.patient.id)
-            videos = Video.objects.filter(patients__id=user.patient.id)
-
-        elif hasattr(user, 'physio'):
-            print("Physio:", user.physio.id)
-            videos = Video.objects.filter(physiotherapist=user.physio.id)
-
-        else:
-            return Response({"error": "No tienes permisos para ver estos videos"}, status=status.HTTP_403_FORBIDDEN)
-
-        serializer = VideoSerializer(videos, many=True)
-        return Response(serializer.data, status=status.HTTP_200_OK)
-
-    except Exception as e:
-        print(f"Error al obtener los videos: {e}")
-        return Response({"error": "Hubo un problema al obtener los videos"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
 
-
-s3_client = boto3.client(
-    's3',
-    aws_access_key_id=settings.DIGITALOCEAN_ACCESS_KEY_ID,
-    aws_secret_access_key=settings.DIGITALOCEAN_SECRET_ACCESS_KEY,
-    region_name=settings.DIGITALOCEAN_REGION,
-    endpoint_url=settings.DIGITALOCEAN_ENDPOINT_URL,
-)
-
-
-@api_view(['GET'])
-@permission_classes([IsPatient])
-def stream_video(request, video_id):
-    try:
-        video = Video.objects.get(id=video_id)
-        if not hasattr(request.user, "patient"):
-            return Response({'error': 'No tienes un perfil de paciente'}, status=403)
-
-        patient_id = request.user.patient.id  # Obtener el ID del paciente
-
-        if patient_id not in video.patients.values_list('id', flat=True):
-            return Response({'error': 'No tienes acceso a este video'}, status=403)
-
-        video_object = s3_client.get_object(
-            Bucket=settings.DIGITALOCEAN_SPACE_NAME,
-            Key=video.file_key
-        )
-
-        video_size = video_object["ContentLength"]
-        video_body = video_object["Body"]
-
-        # Manejar streaming por fragmentos (range requests)
-        range_header = request.headers.get("Range", None)
-        if range_header:
-            range_value = range_header.replace("bytes=", "").split("-")
-            start = int(range_value[0]) if range_value[0] else 0
-            end = int(range_value[1]) if len(range_value) > 1 and range_value[1] else video_size - 1
-            chunk_size = end - start + 1
-
-            # Leer solo el fragmento necesario
-            video_body.seek(start)
-            video_chunk = video_body.read(chunk_size)
-
-            # Responder con `206 Partial Content`
-            response = HttpResponse(video_chunk, content_type="video/mp4")
-            response["Content-Range"] = f"bytes {start}-{end}/{video_size}"
-            response["Accept-Ranges"] = "bytes"
-            response["Content-Length"] = str(chunk_size)
-            response["Cache-Control"] = "no-cache"
-            response["Connection"] = "keep-alive"
-            response.status_code = 206
-        else:
-            # Streaming completo en fragmentos
-            def stream_file():
-                for chunk in video_body.iter_chunks():
-                    yield chunk
-
-            response = StreamingHttpResponse(stream_file(), content_type="video/mp4")
-            response["Content-Length"] = str(video_size)
-            response["Accept-Ranges"] = "bytes"
-            response["Cache-Control"] = "no-cache"
-            response["Connection"] = "keep-alive"
-        
-        return response
-
-    except Video.DoesNotExist:
-        return Response({"error": "El video no existe"}, status=404)
-    except Exception as e:
-        return Response({"error": str(e)}, status=500)
-
-
-@api_view(['PUT'])
-@permission_classes([IsPhysiotherapist])  # Solo usuarios autenticados pueden acceder
-def update_video(request, video_id):
-    try:
-        # Obtener el video desde la BD
-        video = Video.objects.get(id=video_id)
-
-        # Verificar que el usuario autenticado es el fisioterapeuta propietario del video
-        if request.user.physio != video.physiotherapist:
-            return Response({'error': 'No tienes permiso para actualizar este video'}, status=status.HTTP_403_FORBIDDEN)
-
-    except Video.DoesNotExist:
-        return Response({'error': 'El video no existe'}, status=status.HTTP_404_NOT_FOUND)
-
-    # Convertir request.data en un diccionario mutable
-    mutable_data = request.data.copy()
-
-    # Manejo de `patients`
-    patients_raw = mutable_data.get("patients")
-    if patients_raw:
-        if isinstance(patients_raw, str):
-            try:
-                patients_list = json.loads(patients_raw)  # Convierte "[1, 3]" a [1, 3]
-            except json.JSONDecodeError:
-                return Response({"error": "Formato de patients inválido"}, status=status.HTTP_400_BAD_REQUEST)
-        elif isinstance(patients_raw, list):
-            patients_list = patients_raw
-        else:
-            return Response({"error": "Formato de patients incorrecto, debe ser una lista de enteros"}, status=status.HTTP_400_BAD_REQUEST)
-        
-    # Serializar con los datos nuevos
-    serializer = VideoSerializer(video, data=mutable_data, partial=True, context={'request': request})
-
-    if serializer.is_valid():
-        serializer.save()
-        return Response({"message": "Video actualizado correctamente", "video": serializer.data}, status=status.HTTP_200_OK)
-    
-    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+    return Response({"message": "Servicio eliminado correctamente", "services": services}, status=status.HTTP_200_OK)


Reviews:
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T06:15:42Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T08:10:21Z
user: login: antoniommff
body: He corregido los conflictos y he incorporado algunas sugerencias de copilot
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T11:00:04Z

Review comments:
diff_hunk: @@ -632,5 +632,77 @@
         return instance
 
     def get_file_url(self, obj):
+        """Devuelve la URL completa del archivo."""
+        return f"{settings.DIGITALOCEAN_ENDPOINT_URL}/{obj.file_key}"
+
+
+class PatientFileSerializer(serializers.ModelSerializer):
+    files = serializers.ListField(
+        child=serializers.FileField(),  # Para manejar múltiples archivos
+        write_only=True
+    )
+    file_urls = serializers.SerializerMethodField()
+
+    class Meta:
+        model = PatientFile
+        fields = ["id", "title", "description", "uploaded_at", "file_key", "file_urls", "files"]
+        extra_kwargs = {
+            "file_key": {"read_only": True},  # La clave del archivo es solo de lectura
+        }
+
+    def validate_files(self, files):
+        """Valida que todos los archivos tengan una extensión permitida."""
+        allowed_extensions = (".pdf", ".jpg", ".jpeg", ".png", ".dicom")
+        for file in files:
+            if not file.name.lower().endswith(allowed_extensions):
+                raise serializers.ValidationError(f"Solo se permiten archivos con extensiones {', '.join(allowed_extensions)}.")
+        return files
+
+    def create(self, validated_data):
+        """Sube los archivos a DigitalOcean Spaces y actualiza la BD en una misma subida."""
+        request = self.context["request"]
+        patient = request.user.patient
+        files = validated_data.pop('files')
+
+        # 🔍 Intentamos recuperar una subida previa con el mismo título
+        patient_file, created = PatientFile.objects.get_or_create(
+            patient=patient,
+            title=validated_data.get("title"),
+            defaults={"description": validated_data.get("description"), "file_key": ""}
+        )
+
+        # Inicializamos la lista de claves de archivos
+        file_keys = patient_file.file_key.split(",") if patient_file.file_key else []
+
+        # 🔗 Conectar a DigitalOcean Spaces
+        s3_client = boto3.client(
+            "s3",
+            aws_access_key_id=settings.DIGITALOCEAN_ACCESS_KEY_ID,
+            aws_secret_access_key=settings.DIGITALOCEAN_SECRET_ACCESS_KEY,
+            endpoint_url=settings.DIGITALOCEAN_ENDPOINT_URL
+        )
+
+        for file in files:
+            file_extension = file.name.split(".")[-1]
+            file_key = f"patient_files/{patient.id}/{uuid.uuid4()}.{file_extension}"
+
+            try:
+                s3_client.upload_fileobj(
+                    file,
+                    settings.DIGITALOCEAN_SPACE_NAME,
+                    file_key,
+                    ExtraArgs={"ACL": "private"},  # Mantener privado
+                )
+                file_keys.append(file_key)  # 🔥 Agregamos el archivo a la lista
+            except Exception as e:
+                raise serializers.ValidationError(f"Error al subir archivo: {str(e)}")
path: backend/fisio_find/users/serializers.py
user: login: github-advanced-security[bot]
body: ## Information exposure through an exception

[Stack trace information](1) flows to this location and may be exposed to an external user.

[Show more details](https://github.com/Proyecto-ISPP/FISIOFIND/security/code-scanning/56)
created_at: 2025-04-02T06:15:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,181 @@
+from treatments.models import Treatment
+from django.conf import settings
+import boto3
+import uuid
+from rest_framework import serializers
+from .models import PatientFile, Video
+
+
+class PatientFileSerializer(serializers.ModelSerializer):
+    files = serializers.ListField(
+        child=serializers.FileField(),  # Para manejar múltiples archivos
+        write_only=True
+    )
+    file_urls = serializers.SerializerMethodField()
+
+    class Meta:
+        model = PatientFile
+        fields = ["id", "title", "description", "uploaded_at", "file_key", "file_urls", "files"]
+        extra_kwargs = {
+            "file_key": {"read_only": True},  # La clave del archivo es solo de lectura
+        }
+
+    def validate_files(self, files):
+        """Valida que todos los archivos tengan una extensión permitida."""
+        allowed_extensions = (".pdf", ".jpg", ".jpeg", ".png", ".dicom")
+        for file in files:
+            if not file.name.lower().endswith(allowed_extensions):
+                raise serializers.ValidationError(f"Solo se permiten archivos con extensiones {', '.join(allowed_extensions)}.")
+        return files
+
+    def create(self, validated_data):
+        # Obtener la solicitud para acceder a request.data y request.FILES
+        request = self.context["request"]
+        
+        # Obtener los campos del formulario directamente desde request.data
+        treatment = request.data.get('treatment')  # Recibimos treatment desde request.data
+        title = request.data.get('title')
+        description = request.data.get('description')
+
+        if not treatment:
+            raise serializers.ValidationError("El tratamiento es obligatorio.")
+        
+        # Obtener los archivos
+        files = request.FILES.getlist("files")
+
+        # Usar get_or_create para asegurar que el archivo esté asociado con el tratamiento
+        treatment_file, created = PatientFile.objects.get_or_create(
+            treatment_id=treatment,  # Usamos el ID directamente
+            title=title,
+            defaults={"description": description, "file_key": ""}
+        )
+
+        # Obtener las claves de archivo existentes
+        file_keys = treatment_file.file_key.split(",") if treatment_file.file_key else []
+
+        # Crear el cliente de S3
+        s3_client = boto3.client(
+            "s3",
+            aws_access_key_id=settings.DIGITALOCEAN_ACCESS_KEY_ID,
+            aws_secret_access_key=settings.DIGITALOCEAN_SECRET_ACCESS_KEY,
+            endpoint_url=settings.DIGITALOCEAN_ENDPOINT_URL
+        )
+        try:
+            treatment_instance = Treatment.objects.get(id=treatment)
+            treatment_patient = treatment_instance.patient.user.username
+        except Treatment.DoesNotExist:
+            raise serializers.ValidationError("El tratamiento especificado no existe.")
+        
+        # Subir cada archivo a DigitalOcean
+        for file in files:
+            file_extension = file.name.split(".")[-1]
+            file_key = f"patient_files/{treatment_patient}/{uuid.uuid4()}.{file_extension}"
+
+            try:
+                # Subir archivo a S3
+                s3_client.upload_fileobj(
+                    file,
+                    settings.DIGITALOCEAN_SPACE_NAME,
+                    file_key,
+                    ExtraArgs={"ACL": "private"},
+                )
+                file_keys.append(file_key)  # Agregar el archivo a la lista de claves
+            except Exception as e:
+                raise serializers.ValidationError(f"Error al subir archivo: {str(e)}")
path: backend/fisio_find/files/serializers.py
user: login: github-advanced-security[bot]
body: ## Information exposure through an exception

[Stack trace information](1) flows to this location and may be exposed to an external user.

[Show more details](https://github.com/Proyecto-ISPP/FISIOFIND/security/code-scanning/58)
created_at: 2025-04-05T08:10:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,181 @@
+from treatments.models import Treatment
+from django.conf import settings
+import boto3
+import uuid
+from rest_framework import serializers
+from .models import PatientFile, Video
+
+
+class PatientFileSerializer(serializers.ModelSerializer):
+    files = serializers.ListField(
+        child=serializers.FileField(),  # Para manejar múltiples archivos
+        write_only=True
+    )
+    file_urls = serializers.SerializerMethodField()
+
+    class Meta:
+        model = PatientFile
+        fields = ["id", "title", "description", "uploaded_at", "file_key", "file_urls", "files"]
+        extra_kwargs = {
+            "file_key": {"read_only": True},  # La clave del archivo es solo de lectura
+        }
+
+    def validate_files(self, files):
+        """Valida que todos los archivos tengan una extensión permitida."""
+        allowed_extensions = (".pdf", ".jpg", ".jpeg", ".png", ".dicom")
+        for file in files:
+            if not file.name.lower().endswith(allowed_extensions):
+                raise serializers.ValidationError(f"Solo se permiten archivos con extensiones {', '.join(allowed_extensions)}.")
+        return files
+
+    def create(self, validated_data):
+        # Obtener la solicitud para acceder a request.data y request.FILES
+        request = self.context["request"]
+        
+        # Obtener los campos del formulario directamente desde request.data
+        treatment = request.data.get('treatment')  # Recibimos treatment desde request.data
+        title = request.data.get('title')
+        description = request.data.get('description')
+
+        if not treatment:
+            raise serializers.ValidationError("El tratamiento es obligatorio.")
+        
+        # Obtener los archivos
+        files = request.FILES.getlist("files")
+
+        # Usar get_or_create para asegurar que el archivo esté asociado con el tratamiento
+        treatment_file, created = PatientFile.objects.get_or_create(
+            treatment_id=treatment,  # Usamos el ID directamente
+            title=title,
+            defaults={"description": description, "file_key": ""}
+        )
+
+        # Obtener las claves de archivo existentes
+        file_keys = treatment_file.file_key.split(",") if treatment_file.file_key else []
+
+        # Crear el cliente de S3
+        s3_client = boto3.client(
+            "s3",
+            aws_access_key_id=settings.DIGITALOCEAN_ACCESS_KEY_ID,
+            aws_secret_access_key=settings.DIGITALOCEAN_SECRET_ACCESS_KEY,
+            endpoint_url=settings.DIGITALOCEAN_ENDPOINT_URL
+        )
+        try:
+            treatment_instance = Treatment.objects.get(id=treatment)
+            treatment_patient = treatment_instance.patient.user.username
+        except Treatment.DoesNotExist:
+            raise serializers.ValidationError("El tratamiento especificado no existe.")
+        
+        # Subir cada archivo a DigitalOcean
+        for file in files:
+            file_extension = file.name.split(".")[-1]
+            file_key = f"patient_files/{treatment_patient}/{uuid.uuid4()}.{file_extension}"
+
+            try:
+                # Subir archivo a S3
+                s3_client.upload_fileobj(
+                    file,
+                    settings.DIGITALOCEAN_SPACE_NAME,
+                    file_key,
+                    ExtraArgs={"ACL": "private"},
+                )
+                file_keys.append(file_key)  # Agregar el archivo a la lista de claves
+            except Exception as e:
+                raise serializers.ValidationError(f"Error al subir archivo: {str(e)}")
+
+        # Guardar las claves de los archivos en el tratamiento
+        treatment_file.file_key = ",".join(file_keys)
+        treatment_file.save()
+
+        return treatment_file
+    
+    def update(self, instance, validated_data):
+        """Actualiza un archivo en DigitalOcean Spaces y la BD."""
+    
+        # Actualizar otros campos si están en los datos
+        instance.title = validated_data.get("title", instance.title)
+        instance.description = validated_data.get("description", instance.description)
+
+        instance.save()
+        return instance
+
+    def get_file_urls(self, obj):
+        """Devuelve la URL completa del archivo."""
+        return f"{settings.DIGITALOCEAN_ENDPOINT_URL}/{obj.file_key}"
+
+
+class VideoSerializer(serializers.ModelSerializer):
+    file = serializers.FileField(write_only=True)  # Para recibir el archivo en el request
+
+    class Meta:
+        model = Video
+        fields = ["id", "treatment", "title", "description", "file", "file_key", "file_url", "uploaded_at"]
+        extra_kwargs = {
+            "file_key": {"read_only": True},  # El usuario no debe enviar esto
+        }
+
+    def validate_file(self, file):
+        """Valida que el archivo sea un video permitido."""
+        allowed_extensions = (".mp4", ".avi", ".mov", ".mkv", ".webm")
+        if not file.name.lower().endswith(allowed_extensions):
+            raise serializers.ValidationError(f"Solo se permiten archivos con extensiones {', '.join(allowed_extensions)}.")
+        return file
+
+    def create(self, validated_data):
+        """Sube el archivo a DigitalOcean Spaces y guarda el Video en la BD."""
+        request = self.context["request"]
+        file = validated_data.pop("file")  
+        treatment = request.data.get('treatment')
+
+        try:
+            treatment_instance = Treatment.objects.get(id=treatment)
+            treatment_physio = treatment_instance.physiotherapist.user.username
+        except Treatment.DoesNotExist:
+            raise serializers.ValidationError("El tratamiento especificado no existe.")
+
+        # Generar un nombre único para evitar sobrescribir archivos
+        file_extension = file.name.split(".")[-1]
+        file_key = f"videos/{treatment_physio}/{uuid.uuid4()}.{file_extension}"
+
+        # Conectar a DigitalOcean Spaces
+        s3_client = boto3.client(
+            "s3",
+            region_name=settings.DIGITALOCEAN_REGION,
+            endpoint_url=settings.DIGITALOCEAN_ENDPOINT_URL,
+            aws_access_key_id=settings.DIGITALOCEAN_ACCESS_KEY_ID,
+            aws_secret_access_key=settings.DIGITALOCEAN_SECRET_ACCESS_KEY,
+        )
+
+        # Intentar subir el archivo
+        try:
+            s3_client.upload_fileobj(
+                file,
+                settings.DIGITALOCEAN_SPACE_NAME,
+                file_key,
+                ExtraArgs={"ACL": "public-read"},  # Permitir acceso público
+            )
+        except Exception as e:
+            raise serializers.ValidationError(f"Error al subir archivo: {str(e)}")
path: backend/fisio_find/files/serializers.py
user: login: github-advanced-security[bot]
body: ## Information exposure through an exception

[Stack trace information](1) flows to this location and may be exposed to an external user.

[Show more details](https://github.com/Proyecto-ISPP/FISIOFIND/security/code-scanning/59)
created_at: 2025-04-05T08:10:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,339 @@
+from django.shortcuts import render
+from treatments.models import Treatment
+from rest_framework.decorators import api_view, permission_classes
+from rest_framework.response import Response
+from .serializers import PatientFileSerializer, VideoSerializer
+from django.http import StreamingHttpResponse, HttpResponse
+from django.conf import settings
+import boto3
+import uuid
+from rest_framework import status
+from users.permissions import IsPatient, IsPhysioOrPatient, IsPhysiotherapist
+from .models import PatientFile, Video
+import json
+
+
+@api_view(['POST'])
+@permission_classes([IsPatient])
+def create_file(request):
+    treatment_id = request.data.get('treatment')
+
+    if not treatment_id:
+        return Response(
+            {"message": "El ID del tratamiento es requerido"},
+            status=status.HTTP_400_BAD_REQUEST
+        )
+    
+    # Aquí asumo que Treatment es el nombre del modelo y tiene una relación con el paciente
+    try:
+        treatment = Treatment.objects.get(id=treatment_id)
+    except Treatment.DoesNotExist:
+        return Response(
+            {"message": "Tratamiento no encontrado"},
+            status=status.HTTP_404_NOT_FOUND
+        )
+
+    # Verificar si el paciente asociado al tratamiento es el mismo que el que hace la solicitud
+    if treatment.patient != request.user.patient:
+        return Response(
+            {"message": "No tienes permiso para crear archivos para este tratamiento"},
+            status=status.HTTP_403_FORBIDDEN
+        )
+    mutable_data = request.data.copy()
+    serializer = PatientFileSerializer(data=mutable_data, context={'request': request})
+
+    if serializer.is_valid():
+        file = serializer.save()
+        return Response(
+            {
+                "mesaage": "Archivo creado correctamente",
+                "file": PatientFileSerializer(file).data
+            },
+            status=status.HTTP_201_CREATED
+        )
+    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+@api_view(['DELETE'])
+@permission_classes([IsPatient])
+def delete_patient_file(request, file_id):
+    user = request.user
+    
+    try:
+        file = PatientFile.objects.get(id=file_id)
+
+        if not hasattr(user, 'patient') or file.treatment.patient.id != user.patient.id:
+            return Response({"error": "No tienes permiso para eliminar este archivo"}, status=status.HTTP_403_FORBIDDEN)
+
+        file.delete_from_storage()
+        file.delete()
+
+        return Response({"message": "Archivo eliminado correctamente"}, status=status.HTTP_200_OK)
+
+    except PatientFile.DoesNotExist:
+        return Response({"error": "Archivo no encontrado"}, status=status.HTTP_404_NOT_FOUND)
+    
+
+@api_view(['PUT'])
+@permission_classes([IsPatient])
+def update_patient_file(request, file_id):
+    try:
+        file_instance = PatientFile.objects.get(id=file_id)
+    except PatientFile.DoesNotExist:
+        return Response({"error": "Archivo no encontrado"}, status=status.HTTP_404_NOT_FOUND)
+
+    # Verificar que el usuario autenticado es el dueño del archivo
+    if file_instance.treatment.patient.id != request.user.patient.id:
+        return Response({"error": "No tienes permiso para actualizar este archivo"}, status=status.HTTP_403_FORBIDDEN)
+
+    mutable_data = request.data.copy()
+
+    # Si el archivo es nuevo, lo agregamos al diccionario mutable
+    new_file = request.FILES.get("files")
+    if new_file:
+        mutable_data["files"] = new_file
+
+    serializer = PatientFileSerializer(file_instance, data=mutable_data, partial=True, context={"request": request})
+
+    if serializer.is_valid():
+        serializer.save()
+
+        return Response({
+            "message": "Archivo actualizado correctamente",
+            "file": serializer.data
+        }, status=status.HTTP_200_OK)
+
+    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+@api_view(['GET'])
+@permission_classes([IsPhysioOrPatient])
+def get_patient_file_by_id(request, file_id):
+    user = request.user
+    try:
+        file = PatientFile.objects.get(id=file_id)
+        serializer = PatientFileSerializer(file)
+
+        if hasattr(user, 'patient') and file.treatment.patient.id == user.patient.id:
+            return Response(serializer.data, status=status.HTTP_200_OK)
+        elif hasattr(user, 'physio') and file.treatment.physiotherapist.id == user.physio.id:
+            return Response(serializer.data, status=status.HTTP_200_OK)
+        else:
+            return Response({"error": "No tienes permiso para ver este archivo"}, status=status.HTTP_403_FORBIDDEN)
+    except PatientFile.DoesNotExist:
+        return Response({"error": "Archivo no encontrado"}, status=status.HTTP_404_NOT_FOUND)
+    
+
+@api_view(['GET'])
+@permission_classes([IsPhysioOrPatient])
+def get_patient_files(request):
+    user = request.user
+    patient_files = []
+
+    # Filtrar los archivos que el usuario puede acceder
+    if hasattr(user, 'patient'):
+        files = PatientFile.objects.filter(treatment__patient=user.patient)
+    elif hasattr(user, 'physio'):
+        files = PatientFile.objects.filter(treatment__physiotherapist=user.physio)
+    else:
+        files = []
+
+    for file in files:
+        serializer = PatientFileSerializer(file)
+        patient_files.append(serializer.data)
+
+    if not patient_files:
+        return Response({"error": "No se han encontrado archivos a los que puedas acceder"}, status=status.HTTP_404_NOT_FOUND)
+
+    return Response(patient_files, status=status.HTTP_200_OK)
+
+
+@api_view(['POST'])
+@permission_classes([IsPhysiotherapist])
+def create_video(request):
+    treatment_id = request.data.get('treatment')
+
+    if not treatment_id:
+        return Response(
+            {"message": "El ID del tratamiento es requerido"},
+            status=status.HTTP_400_BAD_REQUEST
+        )
+
+    # Aquí asumo que Treatment es el nombre del modelo y tiene una relación con el paciente
+    try:
+        treatment = Treatment.objects.get(id=treatment_id)
+    except Treatment.DoesNotExist:
+        return Response(
+            {"message": "Tratamiento no encontrado"},
+            status=status.HTTP_404_NOT_FOUND
+        )
+
+    # Verificar si el fisioterapeuta asociado al tratamiento es el mismo que el que hace la solicitud
+    if treatment.physiotherapist != request.user.physio:
+        return Response(
+            {"message": "No tienes permiso para crear archivos para este tratamiento"},
+            status=status.HTTP_403_FORBIDDEN
+        )
+
+    mutable_data = request.data.copy()
+    serializer = VideoSerializer(data=mutable_data, context={"request": request})
+
+    if serializer.is_valid():
+        video = serializer.save()
+        return Response(
+            {
+                "message": "Archivo creado correctamente",
+                "video": VideoSerializer(video).data
+            },
+            status=status.HTTP_201_CREATED
+        )
+
+    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+
+@api_view(['DELETE'])
+@permission_classes([IsPhysiotherapist])
+def delete_video(request, video_id):
+    user = request.user
+    try:
+        video = Video.objects.get(id=video_id)
+    
+        if not hasattr(user, 'physio') or video.treatment.physiotherapist != user.physio:
+            return Response({"error": "No tienes permiso para eliminar este video"}, status=status.HTTP_403_FORBIDDEN)
+
+        video.delete_from_storage()
+        video.delete()
+
+        return Response({"message": "Video eliminado correctamente"}, status=status.HTTP_200_OK)
+
+    except Video.DoesNotExist:
+        return Response({"error": "Video no encontrado"}, status=status.HTTP_404_NOT_FOUND)
+    
+
+@api_view(['GET'])
+@permission_classes([IsPhysioOrPatient])
+def list_video_by_id(request, video_id):
+    user = request.user
+    try:
+        video = Video.objects.get(id=video_id)
+        serializer = VideoSerializer(video)
+
+        if hasattr(user, 'patient') and video.treatment.patient == user.patient:
+            return Response(serializer.data, status=status.HTTP_200_OK)
+        elif hasattr(user, 'physio') and video.treatment.physiotherapist == user.physio:
+            return Response(serializer.data, status=status.HTTP_200_OK)
+        else:
+            return Response({"error": "No tienes permiso para ver este video"}, status=status.HTTP_403_FORBIDDEN)
+    except Video.DoesNotExist:
+        return Response({"error": "Video no encontrado"}, status=status.HTTP_404_NOT_FOUND)
+    
+
+@api_view(['GET'])
+@permission_classes([IsPhysioOrPatient])
+def list_my_videos(request):
+    user = request.user
+    videos_owner = []
+
+    if hasattr(user, 'patient'):
+        videos = Video.objects.filter(treatment__patient=user.patient)
+    elif hasattr(user, 'physio'):
+        videos = Video.objects.filter(treatment__physiotherapist=user.physio)
+    else:
+        videos = []
+    
+    for video in videos:
+        serializer = VideoSerializer(video)
+        videos_owner.append(serializer.data)
+
+    if not videos_owner:
+        return Response({"error": "No se han encontrado videos a los que puedas acceder"}, status=status.HTTP_404_NOT_FOUND)
+
+    return Response(videos_owner, status=status.HTTP_200_OK)
+
+
+@api_view(['PUT'])
+@permission_classes([IsPhysiotherapist])
+def update_video(request, video_id):
+    try:
+        video = Video.objects.get(id=video_id)
+
+        # Verificar que el usuario autenticado es el fisioterapeuta propietario del video
+        if request.user.physio != video.treatment.physiotherapist:
+            return Response({'error': 'No tienes permiso para actualizar este video'}, status=status.HTTP_403_FORBIDDEN)
+
+    except Video.DoesNotExist:
+        return Response({'error': 'El video no existe'}, status=status.HTTP_404_NOT_FOUND)
+
+    # Convertir request.data en un diccionario mutable
+    mutable_data = request.data.copy()
+        
+    # Serializar con los datos nuevos
+    serializer = VideoSerializer(video, data=mutable_data, partial=True, context={'request': request})
+
+    if serializer.is_valid():
+        serializer.save()
+        return Response({"message": "Video actualizado correctamente", "video": serializer.data}, status=status.HTTP_200_OK)
+    
+    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+
+
+@api_view(['GET'])
+@permission_classes([IsPatient])
+def stream_video(request, video_id):
+    try:
+        video = Video.objects.get(id=video_id)
+        if not hasattr(request.user, "patient") or video.treatment.patient != request.user.patient:
+            return Response({'error': 'No tienes permisos para reproducir este vídeo'}, status=403)
+
+        s3_client = boto3.client(
+            "s3",
+            region_name=settings.DIGITALOCEAN_REGION,
+            endpoint_url=settings.DIGITALOCEAN_ENDPOINT_URL,
+            aws_access_key_id=settings.DIGITALOCEAN_ACCESS_KEY_ID,
+            aws_secret_access_key=settings.DIGITALOCEAN_SECRET_ACCESS_KEY,
+        )
+
+        video_object = s3_client.get_object(
+            Bucket=settings.DIGITALOCEAN_SPACE_NAME,
+            Key=video.file_key
+        )
+
+        video_size = video_object["ContentLength"]
+        video_body = video_object["Body"]
+
+        # Manejar streaming por fragmentos (range requests)
+        range_header = request.headers.get("Range", None)
+        if range_header:
+            range_value = range_header.replace("bytes=", "").split("-")
+            start = int(range_value[0]) if range_value[0] else 0
+            end = int(range_value[1]) if len(range_value) > 1 and range_value[1] else video_size - 1
+            chunk_size = end - start + 1
+
+            # Leer solo el fragmento necesario
+            video_body.seek(start)
+            video_chunk = video_body.read(chunk_size)
+
+            # Responder con `206 Partial Content`
+            response = HttpResponse(video_chunk, content_type="video/mp4")
+            response["Content-Range"] = f"bytes {start}-{end}/{video_size}"
+            response["Accept-Ranges"] = "bytes"
+            response["Content-Length"] = str(chunk_size)
+            response["Cache-Control"] = "no-cache"
+            response["Connection"] = "keep-alive"
+            response.status_code = 206
+        else:
+            # Streaming completo en fragmentos
+            def stream_file():
+                for chunk in video_body.iter_chunks():
+                    yield chunk
+
+            response = StreamingHttpResponse(stream_file(), content_type="video/mp4")
+            response["Content-Length"] = str(video_size)
+            response["Accept-Ranges"] = "bytes"
+            response["Cache-Control"] = "no-cache"
+            response["Connection"] = "keep-alive"
+        
+        return response
+
+    except Video.DoesNotExist:
+        return Response({"error": "El video no existe"}, status=404)
+    except Exception as e:
+        return Response({"error": str(e)}, status=500)
path: backend/fisio_find/files/views.py
user: login: github-advanced-security[bot]
body: ## Information exposure through an exception

[Stack trace information](1) flows to this location and may be exposed to an external user.

[Show more details](https://github.com/Proyecto-ISPP/FISIOFIND/security/code-scanning/60)
created_at: 2025-04-05T08:10:21Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-05T11:03:46Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=Proyecto-ISPP_FISIOFIND&pullRequest=427) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [14 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=Proyecto-ISPP_FISIOFIND&pullRequest=427&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [35.9% Coverage on New Code](https://sonarcloud.io/component_measures?id=Proyecto-ISPP_FISIOFIND&pullRequest=427&metric=new_coverage&view=list) (required ≥ 80%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=Proyecto-ISPP_FISIOFIND&pullRequest=427)


author_association: NONE