333 lines
11 KiB
Python
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
|