Arch/utils/validators.py

333 lines
11 KiB
Python

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