-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement dashboard export helpers #257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,20 +2,16 @@ | |||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| from typing import Dict, List, Optional | ||||||||||||||||||||||||
| from typing import Dict, List, Optional, Union | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| import csv | ||||||||||||||||||||||||
| from io import BytesIO, StringIO | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| from django.contrib.auth import get_user_model | ||||||||||||||||||||||||
| from django.core.exceptions import PermissionDenied, ValidationError | ||||||||||||||||||||||||
| from django.utils import timezone | ||||||||||||||||||||||||
| from reportlab.lib.pagesizes import letter | ||||||||||||||||||||||||
| from reportlab.pdfgen import canvas | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| from callcentersite.apps.users.models_permisos_granular import AuditoriaPermiso | ||||||||||||||||||||||||
| from callcentersite.apps.users.service_helpers import ( | ||||||||||||||||||||||||
| auditar_accion_exitosa, | ||||||||||||||||||||||||
| validar_usuario_existe, | ||||||||||||||||||||||||
| verificar_permiso_y_auditar, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| from callcentersite.apps.users.services_permisos_granular import UserManagementService | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| from .models import DashboardConfiguracion | ||||||||||||||||||||||||
|
|
@@ -27,6 +23,27 @@ | |||||||||||||||||||||||
| class DashboardService: | ||||||||||||||||||||||||
| """Orquesta la construcción de respuestas para el dashboard.""" | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @staticmethod | ||||||||||||||||||||||||
| def ver_dashboard(usuario_id: int) -> Dict[str, object]: | ||||||||||||||||||||||||
| """Retorna la configuración del dashboard del usuario. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Si el usuario no tiene configuración personalizada se usan los widgets | ||||||||||||||||||||||||
| por defecto registrados en ``WIDGET_REGISTRY``. | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| configuracion = DashboardConfiguracion.objects.filter( | ||||||||||||||||||||||||
| usuario_id=usuario_id | ||||||||||||||||||||||||
| ).first() | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| widget_keys = configuracion.configuracion.get("widgets", []) if configuracion else list(WIDGET_REGISTRY.keys()) | ||||||||||||||||||||||||
| widgets = [WIDGET_REGISTRY[widget].__dict__ for widget in widget_keys if widget in WIDGET_REGISTRY] | ||||||||||||||||||||||||
2-Coatl marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # No fallback to defaults if user's config exists but is invalid; respect explicit selection. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||
| "widgets": widgets, | ||||||||||||||||||||||||
| "last_update": timezone.now().isoformat(), | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @staticmethod | ||||||||||||||||||||||||
| def overview() -> Dict[str, object]: | ||||||||||||||||||||||||
| """Retorna overview general del dashboard (legacy).""" | ||||||||||||||||||||||||
|
|
@@ -67,14 +84,12 @@ def exportar( | |||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Referencia: docs/PLAN_MAESTRO_PRIORIDAD_02.md (Tarea 26) | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| # Verificar permiso y auditar | ||||||||||||||||||||||||
| verificar_permiso_y_auditar( | ||||||||||||||||||||||||
| tiene_permiso = UserManagementService.usuario_tiene_permiso( | ||||||||||||||||||||||||
| usuario_id=usuario_id, | ||||||||||||||||||||||||
| capacidad_codigo='sistema.vistas.dashboards.exportar', | ||||||||||||||||||||||||
| recurso_tipo='dashboard', | ||||||||||||||||||||||||
| accion='exportar', | ||||||||||||||||||||||||
| mensaje_error='No tiene permiso para exportar dashboards', | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| if not tiene_permiso: | ||||||||||||||||||||||||
| raise PermissionDenied('No tiene permiso para exportar dashboards') | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # Validar formato | ||||||||||||||||||||||||
| if formato not in ['pdf', 'excel']: | ||||||||||||||||||||||||
|
|
@@ -84,12 +99,12 @@ def exportar( | |||||||||||||||||||||||
| # Por ahora retornamos un placeholder | ||||||||||||||||||||||||
| archivo = f'/tmp/dashboard_{usuario_id}_{timezone.now().timestamp()}.{formato}' | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # Auditar acción exitosa | ||||||||||||||||||||||||
| auditar_accion_exitosa( | ||||||||||||||||||||||||
| AuditoriaPermiso.objects.create( | ||||||||||||||||||||||||
| usuario_id=usuario_id, | ||||||||||||||||||||||||
| capacidad_codigo='sistema.vistas.dashboards.exportar', | ||||||||||||||||||||||||
| recurso_tipo='dashboard', | ||||||||||||||||||||||||
| accion='exportar', | ||||||||||||||||||||||||
| resultado='permitido', | ||||||||||||||||||||||||
| detalles=f'Dashboard exportado a {formato}', | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
@@ -99,6 +114,69 @@ def exportar( | |||||||||||||||||||||||
| 'timestamp': timezone.now().isoformat(), | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @staticmethod | ||||||||||||||||||||||||
| def personalizar_dashboard(usuario_id: int, widgets: List[str]) -> DashboardConfiguracion: | ||||||||||||||||||||||||
2-Coatl marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
| """Guarda la lista de widgets habilitados para el usuario. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||
| usuario_id: ID del usuario dueño de la configuración. | ||||||||||||||||||||||||
| widgets: Lista de identificadores de widgets. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Raises: | ||||||||||||||||||||||||
| ValidationError: Si la lista está vacía o contiene widgets inexistentes. | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| if not widgets: | ||||||||||||||||||||||||
| raise ValidationError('Debe proporcionar al menos un widget para personalizar el dashboard') | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| widgets_invalidos = [widget for widget in widgets if widget not in WIDGET_REGISTRY] | ||||||||||||||||||||||||
| if widgets_invalidos: | ||||||||||||||||||||||||
| raise ValidationError(f"Widget invalido: {', '.join(widgets_invalidos)}") | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
| raise ValidationError(f"Widget invalido: {', '.join(widgets_invalidos)}") | |
| raise ValidationError(f"Widget inválido: {', '.join(widgets_invalidos)}") |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new method personalizar_dashboard bypasses permission checks entirely, while the existing personalizar method (line 202-207) includes explicit permission validation. This creates a security vulnerability where users can personalize dashboards without proper authorization. Add the same permission check using UserManagementService.usuario_tiene_permiso with capability code 'sistema.vistas.dashboards.personalizar'.
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CSV export may fail if any widget dictionary doesn't contain all the expected fields ("type", "title", "value", "change", "period"). The writer.writerow(widget) call will raise a ValueError if keys are missing. Add validation to ensure widgets have all required fields, or use extrasaction='ignore' and handle missing keys gracefully.
| writer = csv.DictWriter(output, fieldnames=["type", "title", "value", "change", "period"]) | |
| writer.writeheader() | |
| for widget in widgets: | |
| writer.writerow(widget) | |
| fieldnames = ["type", "title", "value", "change", "period"] | |
| writer = csv.DictWriter(output, fieldnames=fieldnames, extrasaction='ignore') | |
| writer.writeheader() | |
| for widget in widgets: | |
| # Fill missing keys with empty string | |
| row = {key: widget.get(key, "") for key in fieldnames} | |
| writer.writerow(row) |
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The manually constructed PDF in lines 173-178 is incomplete and malformed:
- Missing proper xref table entries (incorrect format and incomplete offsets)
- The xref section has "trail\n" instead of "trailer"
- Missing proper EOF newline
- Object references and offsets don't match the actual byte positions
This will produce a corrupted PDF that most readers will fail to open. Consider using a lightweight PDF library like fpdf or weasyprint, or at minimum, construct a valid minimal PDF with correct xref offsets and trailer structure.
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new method exportar_dashboard bypasses permission checks entirely, while the existing exportar method (line 88-93) includes explicit permission validation. This creates a security vulnerability where users can export dashboards without proper authorization by calling the new method instead of the legacy one. Add the same permission check using UserManagementService.usuario_tiene_permiso with capability code 'sistema.vistas.dashboards.exportar'.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot open a new pull request to apply changes based on this feedback
Copilot
AI
Nov 18, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new methods personalizar_dashboard and exportar_dashboard lack audit logging, while the existing methods personalizar (line 219-227) and exportar (line 103-110) properly create AuditoriaPermiso records. This creates an inconsistent audit trail where some dashboard operations are logged and others aren't, making it difficult to track user actions and comply with security requirements. Add equivalent audit logging using AuditoriaPermiso.objects.create() for both new methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot open a new pull request to apply changes based on this feedback
Uh oh!
There was an error while loading. Please reload this page.