The SASCOP BME SubTec application provides a rich set of utility functions for common operations including email sending, PDF generation, chart creation, and database queries. These utilities are centralized in core/utils.py.
Execute raw SQL queries with parameter binding.Location:core/utils.py:57-64
core/utils.py
def ejecutar_query_sql(query, params=None, retornar_dict=True): with connection.cursor() as cursor: cursor.execute(query, params or ()) if retornar_dict: columns = [col[0] for col in cursor.description] return [dict(zip(columns, row)) for row in cursor.fetchall()] else: return cursor.fetchall()
Usage:
from core.utils import ejecutar_query_sql# Query with parameterssql = """ SELECT * FROM pte_header WHERE fecha >= %s AND fecha <= %s"""resultados = ejecutar_query_sql(sql, ['2024-01-01', '2024-12-31'])# Returns list of dictionariesfor row in resultados: print(row['id'], row['descripcion'])
Execute queries and fetch results in batches to avoid memory issues with large datasets.Location:core/utils.py:639-656
core/utils.py
def fn_ejecutar_query_sql_lotes(sql, parametros=None, tamano_lote=2000): """ Ejecuta una consulta SQL y devuelve un iterador (generador) por lotes. Diseñada exclusivamente para exportaciones masivas sin saturar la RAM. Extrae de 2000 en 2000 registros de la base de datos. """ with connection.cursor() as cursor: cursor.execute(sql, parametros) columnas = [columna[0] for columna in cursor.description] while True: resultados_lote = cursor.fetchmany(tamano_lote) if not resultados_lote: break for fila in resultados_lote: yield dict(zip(columnas, fila))
Usage:
from core.utils import fn_ejecutar_query_sql_lotes# Process large dataset in batchessql = "SELECT * FROM pte_detalle WHERE estatus_paso_id != 14"for registro in fn_ejecutar_query_sql_lotes(sql): # Process each record without loading everything into memory process_record(registro)
Use this function for large exports (Excel, CSV) to prevent memory exhaustion.
Get activity summary by user for a date range.Location:core/utils.py:65-123
core/utils.py
def fn_obtener_resumen_actividad_por_usuario(fecha_inicio, fecha_fin): sql = """ WITH resumen_totales AS ( SELECT usuario_id_id, COUNT(*) AS actividad_total FROM registro_actividad WHERE fecha >= %s AND fecha <= %s GROUP BY usuario_id_id ORDER BY actividad_total DESC LIMIT 10 ) SELECT ra.usuario_id_id, COUNT(*) AS total_por_modulo, ra.tabla_log, CONCAT_WS(' ', au.first_name, au.last_name) AS nombre_usuario, CASE WHEN ra.tabla_log = 0 THEN 'Cabecera PTE' WHEN ra.tabla_log = 1 THEN 'Pasos PTE' WHEN ra.tabla_log = 4 THEN 'Cabecera OT' WHEN ra.tabla_log = 5 THEN 'Pasos OT' ELSE 'OTRO' END AS nombre_modulo FROM registro_actividad ra INNER JOIN auth_user au ON ra.usuario_id_id = au.id WHERE ra.usuario_id_id IN ( SELECT usuario_id_id FROM resumen_totales ) AND ra.fecha >= %s AND ra.fecha <= %s GROUP BY ra.usuario_id_id, ra.tabla_log, au.first_name, au.last_name ORDER BY ( SELECT actividad_total FROM resumen_totales WHERE resumen_totales.usuario_id_id = ra.usuario_id_id ) DESC, total_por_modulo DESC; """ params = [fecha_inicio, fecha_fin, fecha_inicio, fecha_fin] return ejecutar_query_sql(sql, params)
Get summary of loaded PTE step files.Location:core/utils.py:125-146
core/utils.py
def fn_obtener_resumen_pasos_cargados(): sql = """ SELECT COUNT(pte_detalle.archivo) AS archivos_cargados, COUNT(*) FILTER (WHERE pte_detalle.archivo IS NULL) AS archivos_nulos, COUNT(*) AS total_registros, pte_header.id_responsable_proyecto_id, CONCAT_WS(' ', responsable_proyecto.descripcion) AS nombre_usuario FROM pte_detalle INNER JOIN pte_header ON pte_header.id = id_pte_header_id INNER JOIN responsable_proyecto ON pte_header.id_responsable_proyecto_id = responsable_proyecto.id WHERE pte_detalle.estatus_paso_id != 14 AND pte_header.estatus !=0 GROUP BY responsable_proyecto.id, pte_header.id_responsable_proyecto_id, nombre_usuario ORDER BY archivos_cargados DESC; """ return ejecutar_query_sql(sql)
Get summary of loaded OT step files.Location:core/utils.py:148-169
core/utils.py
def fn_obtener_resumen_ot_pasos_cargados(): sql = """ SELECT COUNT(ot_detalle.archivo) AS archivos_cargados, COUNT(*) FILTER (WHERE ot_detalle.archivo IS NULL) AS archivos_nulos, COUNT(*) AS total_registros, ot.id_responsable_proyecto_id, CONCAT_WS(' ', responsable_proyecto.descripcion) AS nombre_usuario FROM ot_detalle INNER JOIN ot ON ot.id = id_ot_id INNER JOIN responsable_proyecto ON ot.id_responsable_proyecto_id = responsable_proyecto.id WHERE ot_detalle.estatus_paso_id != 14 AND ot.estatus !=0 GROUP BY responsable_proyecto.id, ot.id_responsable_proyecto_id, nombre_usuario ORDER BY archivos_cargados DESC; """ return ejecutar_query_sql(sql)
Generate stacked bar chart for user activity.Location:core/utils.py:172-228 and core/utils.py:399-455
core/utils.py
def fn_generar_grafica_buffer(datos_queryset): """Genera la gráfica de Resumen de Actividades por usuario.""" datos_organizados = {} tipos_registro = set() mapeo_colores = { "Pasos PTE": COLOR_FUERZA, "Pasos OT": COLOR_SOLIDEZ, "Cabecera PTE": COLOR_CONFIANZA, "Cabecera OT": COLOR_DINAMISMO } for fila in datos_queryset: usuario = fila["nombre_usuario"] tipo = fila["nombre_modulo"] cantidad = fila["total_por_modulo"] if usuario not in datos_organizados: datos_organizados[usuario] = {} datos_organizados[usuario][tipo] = cantidad tipos_registro.add(tipo) lista_usuarios = list(datos_organizados.keys()) lista_tipos = sorted(list(tipos_registro)) nombres_ajustados = [textwrap.fill(nombre, width=15) for nombre in lista_usuarios] fig, ax = plt.subplots(figsize=(11, 5)) acumulado_altura = [0] * len(lista_usuarios) for tipo in lista_tipos: valores = [datos_organizados[u].get(tipo, 0) for u in lista_usuarios] color_segmento = mapeo_colores.get(tipo, COLOR_SOLIDEZ) ax.bar(range(len(lista_usuarios)), valores, bottom=acumulado_altura, label=tipo, color=color_segmento, width=0.6) acumulado_altura = [s + v for s, v in zip(acumulado_altura, valores)] max_y = max(acumulado_altura) if acumulado_altura else 100 ax.get_yaxis().set_major_formatter(ticker.StrMethodFormatter("{x:,.0f}")) ax.set_title("Resumen de Actividad por Usuario", pad=25, fontsize=12, color=COLOR_SERIEDAD, fontweight="bold") ax.legend(loc="upper right", fontsize=8, frameon=False) ax.set_xticks(range(len(lista_usuarios))) ax.set_xticklabels(nombres_ajustados, fontsize=8) for s in ["top", "right"]: ax.spines[s].set_visible(False) ax.yaxis.grid(True, linestyle="--", alpha=0.3) plt.tight_layout() buffer = io.BytesIO() plt.savefig(buffer, format="png", dpi=120) buffer.seek(0) plt.close(fig) return buffer
Usage:
from core.utils import fn_obtener_resumen_actividad_por_usuario, fn_generar_grafica_buffer# Get datadatos = fn_obtener_resumen_actividad_por_usuario('2024-01-01', '2024-01-31')# Generate chartbuffer = fn_generar_grafica_buffer(datos)# Save to filewith open('activity_chart.png', 'wb') as f: f.write(buffer.getvalue())