import re import os from datetime import datetime import pytz from werkzeug.utils import secure_filename from flask import current_app def validate_email(email): """ Validar formato de dirección de correo electrónico. Args: email (str): Dirección de correo a validar Returns: bool: True si es válido, False en caso contrario """ # Patrón básico para validar emails pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' return bool(re.match(pattern, email)) def validate_username(username): """ Validar formato de nombre de usuario. Args: username (str): Nombre de usuario a validar Returns: bool: True si es válido, False en caso contrario """ # Solo letras, números, guiones y guiones bajos, longitud entre 3 y 20 pattern = r'^[a-zA-Z0-9_-]{3,20}$' return bool(re.match(pattern, username)) def validate_password_strength(password): """ Validar fortaleza de contraseña. Args: password (str): Contraseña a validar Returns: tuple: (is_valid, message) - is_valid (bool): True si es válida - message (str): Mensaje descriptivo si no es válida """ # Verificar longitud mínima if len(password) < 8: return False, "La contraseña debe tener al menos 8 caracteres." # Verificar presencia de letras if not re.search(r'[a-zA-Z]', password): return False, "La contraseña debe contener al menos una letra." # Verificar presencia de números if not re.search(r'\d', password): return False, "La contraseña debe contener al menos un número." # Verificar presencia de caracteres especiales (opcional) if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password): return False, "La contraseña debe contener al menos un carácter especial." return True, "Contraseña válida." def validate_date_format(date_str, format='%Y-%m-%d'): """ Validar formato de fecha. Args: date_str (str): Fecha en formato string format (str, optional): Formato esperado. Por defecto '%Y-%m-%d'. Returns: bool: True si es válido, False en caso contrario """ try: datetime.strptime(date_str, format) return True except ValueError: return False def validate_iso_date(date_str): """ Validar fecha en formato ISO 8601. Args: date_str (str): Fecha en formato ISO 8601 Returns: bool: True si es válido, False en caso contrario """ try: datetime.fromisoformat(date_str.replace('Z', '+00:00')) return True except (ValueError, AttributeError): return False def validate_file_extension(filename, allowed_extensions): """ Validar extensión de archivo. Args: filename (str): Nombre del archivo allowed_extensions (list): Lista de extensiones permitidas Returns: bool: True si es válido, False en caso contrario """ return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in allowed_extensions def validate_file_size(file, max_size_bytes): """ Validar tamaño de archivo. Args: file: Objeto de archivo (de Flask) max_size_bytes (int): Tamaño máximo en bytes Returns: bool: True si es válido, False en caso contrario """ # Guardar posición actual current_position = file.tell() # Ir al final para obtener el tamaño file.seek(0, os.SEEK_END) size = file.tell() # Restaurar posición file.seek(current_position) return size <= max_size_bytes def validate_project_data(data): """ Validar datos de proyecto. Args: data (dict): Datos del proyecto a validar Returns: tuple: (is_valid, errors) - is_valid (bool): True si es válido - errors (dict): Diccionario con errores por campo """ errors = {} # Validar campos obligatorios required_fields = ['descripcion', 'cliente', 'esquema'] for field in required_fields: if field not in data or not data[field]: errors[field] = f"El campo {field} es obligatorio." # Validar longitud de descripción if 'descripcion' in data and data['descripcion']: if len(data['descripcion']) < 5: errors['descripcion'] = "La descripción debe tener al menos 5 caracteres." elif len(data['descripcion']) > 100: errors['descripcion'] = "La descripción no puede exceder los 100 caracteres." # Validar esquema if 'esquema' in data and data['esquema']: from services.schema_service import get_schema schema = get_schema(data['esquema']) if not schema: errors['esquema'] = "El esquema seleccionado no existe." # Validar proyecto padre si se especifica if 'proyecto_padre' in data and data['proyecto_padre']: from services.project_service import get_project parent_id = data['proyecto_padre'].replace('PROJ', '') try: parent_id = int(parent_id) parent = get_project(parent_id) if not parent: errors['proyecto_padre'] = "El proyecto padre seleccionado no existe." except ValueError: errors['proyecto_padre'] = "Formato de ID de proyecto padre inválido." return len(errors) == 0, errors def validate_document_data(data, file): """ Validar datos de documento. Args: data (dict): Datos del documento a validar file: Objeto de archivo (de Flask) Returns: tuple: (is_valid, errors) - is_valid (bool): True si es válido - errors (dict): Diccionario con errores por campo """ errors = {} # Validar campos obligatorios if 'nombre' not in data or not data['nombre']: errors['nombre'] = "El nombre del documento es obligatorio." # Validar archivo if not file: errors['file'] = "Debe seleccionar un archivo." else: # Validar extensión filename = secure_filename(file.filename) extension = filename.rsplit('.', 1)[1].lower() if '.' in filename else '' from services.document_service import get_allowed_filetypes allowed_filetypes = get_allowed_filetypes() if extension not in allowed_filetypes: errors['file'] = f"Tipo de archivo no permitido: {extension}" else: # Validar tamaño max_size = allowed_filetypes[extension].get('tamano_maximo', 10485760) # 10MB por defecto if not validate_file_size(file, max_size): errors['file'] = f"El archivo excede el tamaño máximo permitido ({max_size // 1048576} MB)." return len(errors) == 0, errors def validate_schema_data(data): """ Validar datos de esquema. Args: data (dict): Datos del esquema a validar Returns: tuple: (is_valid, errors) - is_valid (bool): True si es válido - errors (dict): Diccionario con errores por campo """ errors = {} # Validar campos obligatorios if 'descripcion' not in data or not data['descripcion']: errors['descripcion'] = "La descripción del esquema es obligatoria." if 'documentos' not in data or not data['documentos']: errors['documentos'] = "Se requiere al menos un tipo de documento en el esquema." # Validar documentos if 'documentos' in data and data['documentos']: for i, doc in enumerate(data['documentos']): # Validar campos obligatorios de cada documento if 'tipo' not in doc or not doc['tipo']: errors[f'documentos[{i}].tipo'] = "El tipo de documento es obligatorio." if 'nombre' not in doc or not doc['nombre']: errors[f'documentos[{i}].nombre'] = "El nombre del documento es obligatorio." # Validar tipo de documento if 'tipo' in doc and doc['tipo']: from services.document_service import get_allowed_filetypes allowed_filetypes = get_allowed_filetypes() if doc['tipo'] not in allowed_filetypes: errors[f'documentos[{i}].tipo'] = f"Tipo de documento no permitido: {doc['tipo']}" # Validar niveles if 'nivel_ver' in doc and not isinstance(doc['nivel_ver'], int): errors[f'documentos[{i}].nivel_ver'] = "El nivel de visualización debe ser un número entero." if 'nivel_editar' in doc and not isinstance(doc['nivel_editar'], int): errors[f'documentos[{i}].nivel_editar'] = "El nivel de edición debe ser un número entero." return len(errors) == 0, errors def validate_user_data(data, is_new_user=True): """ Validar datos de usuario. Args: data (dict): Datos del usuario a validar is_new_user (bool, optional): Indica si es un nuevo usuario. Por defecto es True. Returns: tuple: (is_valid, errors) - is_valid (bool): True si es válido - errors (dict): Diccionario con errores por campo """ errors = {} # Validar campos obligatorios para nuevos usuarios if is_new_user: required_fields = ['nombre', 'username', 'email', 'password'] for field in required_fields: if field not in data or not data[field]: errors[field] = f"El campo {field} es obligatorio." # Validar username if 'username' in data and data['username']: if not validate_username(data['username']): errors['username'] = "El nombre de usuario debe contener solo letras, números, guiones y guiones bajos, y tener entre 3 y 20 caracteres." elif is_new_user: # Verificar disponibilidad solo para nuevos usuarios from services.user_service import check_username_availability if not check_username_availability(data['username']): errors['username'] = "El nombre de usuario ya está en uso." # Validar email if 'email' in data and data['email']: if not validate_email(data['email']): errors['email'] = "El formato de correo electrónico no es válido." # Validar contraseña para nuevos usuarios o si se proporciona if is_new_user or ('password' in data and data['password']): if 'password' in data and data['password']: is_valid, message = validate_password_strength(data['password']) if not is_valid: errors['password'] = message # Validar nivel if 'nivel' in data: try: nivel = int(data['nivel']) if nivel < 0 or nivel > 9999: errors['nivel'] = "El nivel debe estar entre 0 y 9999." except (ValueError, TypeError): errors['nivel'] = "El nivel debe ser un número entero." # Validar fecha de caducidad if 'fecha_caducidad' in data and data['fecha_caducidad']: if not validate_iso_date(data['fecha_caducidad']): errors['fecha_caducidad'] = "El formato de fecha de caducidad no es válido. Use formato ISO 8601 (YYYY-MM-DDTHH:MM:SSZ)." return len(errors) == 0, errors