Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
tests:
name: Pruebas (capa de modelos)
runs-on: ubuntu-latest

steps:
- name: Clonar el repositorio
uses: actions/checkout@v4

- name: Configurar Python 3.13
uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: pip
Comment on lines +18 to +22

- name: Instalar dependencias de desarrollo
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt

- name: Ejecutar la suite de pruebas
run: pytest
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,25 @@ margoth/
- [x] Verificación de creación de `data/` y `media/` junto al binario
- [x] Validación de ejecución del ejecutable compilado (`Margoth.exe`)

### Fase 9: Medios etiquetados ✅
- [x] Columna `label` en `patient_media` (con migración idempotente)
- [x] El ejercicio semántico usa etiquetas legibles, no el nombre de archivo
- [x] Campo de etiqueta al subir un medio

### Fase 10: Reportes y gestión de pacientes ✅
- [x] Vista de reportes de progreso (aciertos, tiempos, tendencia diaria)
- [x] Editar y eliminar pacientes (borrado en cascada de medios y métricas)

### Fase 11: Tableros CAA múltiples ✅
- [x] Varios tableros por paciente (upsert por id, sin sobrescribir)
- [x] Tamaño de grilla configurable (1..4 × 1..4)
- [x] Selector de tablero en el constructor y en el visor

### Calidad ✅
- [x] Suite de pruebas de la capa de modelos (`pytest`, 44 tests)
- [x] Integración continua en GitHub Actions
- [x] Ícono propio de la aplicación (`assets/icon.ico`)

## Verificación del Ejecutable Compilado

Tras ejecutar `Margoth.exe` en `dist/Margoth/`:
Expand All @@ -98,6 +117,19 @@ pip install -r requirements.txt
python src/main.py
```

### Pruebas
```bash
pip install -r requirements-dev.txt
pytest
```
La capa de modelos se prueba headless (sin PyQt6). El mismo comando corre en
CI (GitHub Actions) en cada push y pull request.

### Regenerar el ícono
```bash
python tools/generate_icon.py # -> assets/icon.png y assets/icon.ico
```

### Compilación (Windows)
```bash
pip install pyinstaller
Expand All @@ -114,9 +146,23 @@ El ejecutable se generará en `dist/Margoth/`.

- **Cero sobrecarga cognitiva**: Interfaces minimalistas para pacientes (Teoría de Mayer)
- **Accesibilidad**: Alto contraste, tipografías escalables (Segoe UI 12pt+)
- **Privacidad**: 100% offline, datos locales cifrados
- **Privacidad**: 100% offline. Los datos clínicos nunca salen del equipo (ver *Seguridad y privacidad*)
- **Personalización**: Soporte para fotos y audios del entorno del paciente

## Seguridad y privacidad

- **Sin nube**: la aplicación no consume ni expone ninguna API de red. Todos
los datos (BD SQLite y medios) viven en el equipo del terapeuta.
- **Aislamiento por paciente**: los medios se guardan en carpetas separadas
identificadas por UUID.
- **Cifrado en reposo (pendiente)**: hoy la base de datos SQLite **no** está
cifrada. La migración a [SQLCipher](https://www.zetetic.net/sqlcipher/)
(vía `pysqlcipher3`) está planificada como mejora futura; se difirió porque
requiere binarios nativos que complican el empaquetado con PyInstaller en
Windows. Mientras tanto, la protección recae en el control de acceso del
sistema operativo. **No usar en un equipo compartido sin cuenta de usuario
protegida.**

## Contribuidores

- **Carlos G** - Creador y desarrollador principal
Expand Down
16 changes: 0 additions & 16 deletions apps/api/manage.py

This file was deleted.

Binary file added assets/icon.ico
Binary file not shown.
Binary file added assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 11 additions & 3 deletions build_exe.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ def main():
project_root = os.path.dirname(os.path.abspath(__file__))
entry = os.path.join(project_root, "src", "main.py")
src_path = os.path.join(project_root, "src")
styles_src = os.path.join(project_root, "assets", "styles")
add_data = f"{styles_src}{os.pathsep}assets/styles"
assets_src = os.path.join(project_root, "assets")
icon_ico = os.path.join(assets_src, "icon.ico")

sep = os.pathsep
# Empaqueta toda la carpeta assets (estilos + ícono) junto al binario
add_data = f"{assets_src}{sep}assets"

cmd = [
sys.executable,
Expand All @@ -24,9 +28,13 @@ def main():
src_path,
"--add-data",
add_data,
entry,
]

if os.path.isfile(icon_ico):
cmd += ["--icon", icon_ico]

cmd.append(entry)

subprocess.run(cmd, check=True)


Expand Down
57 changes: 54 additions & 3 deletions docs/mvp.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,56 @@
# MVP Documentation
# Documentación funcional — Margoth

## Exercises and API
Margoth es una aplicación de escritorio **100 % offline** para intervención y
rehabilitación cognitiva y del lenguaje. No expone ni consume ninguna API en la
nube: toda la lógica corre localmente y los datos clínicos nunca salen del
equipo. La persistencia es SQLite local y los medios se guardan en el sistema
de archivos, por paciente.

This document describes the exercises associated with the project and the API endpoints available in the Django application. Further details will be provided as development progresses.
> Nota histórica: versiones muy tempranas incluían un esqueleto de API en
> Django. Se descartó por completo para cumplir el requisito de privacidad
> "100 % offline". Ya no existe backend de red.

## Módulos clínicos

### 1. Gestión de pacientes (Dashboard)
El terapeuta da de alta, edita y elimina pacientes, y sube material multimedia
personalizado (fotos y audios del entorno del paciente). Cada paciente tiene una
carpeta de medios aislada, identificada por UUID.

### 2. Tableros CAA (Comunicación Aumentativa y Alternativa)
Grillas de pictogramas/fotos con audio asociado. Al pulsar una casilla se
reproduce el audio vinculado. Pensados para pacientes con dificultades del
lenguaje expresivo.

### 3. Constructor visual de tableros CAA
Interfaz de arrastrar-asignar para montar los tableros: el terapeuta selecciona
un medio de la galería del paciente y lo coloca en una casilla de la grilla. La
configuración se guarda como `caa_boards.json` dentro de la carpeta del paciente.

### 4. Ejercicio semántico con métricas
Se muestra un estímulo visual central y tres opciones de respuesta (una correcta
y dos distractores). Se mide el tiempo de reacción con `time.perf_counter()` y se
registra cada intento (acierto/fallo + tiempo) en la tabla `exercise_metrics`.

### 5. Reportes de progreso
Vista de seguimiento por paciente que agrega `exercise_metrics`: porcentaje de
aciertos, tiempo de reacción promedio e histórico de sesiones. Es el insumo
clínico para evaluar la evolución del paciente.

## Modelo de datos (SQLite local)

| Tabla | Propósito |
|-------|-----------|
| `patients` | Datos del paciente y su carpeta de medios (UUID). |
| `patient_media` | Fotos/audios importados, con etiqueta (`label`) y tipo. |
| `exercise_metrics` | Un registro por intento de ejercicio (acierto + tiempo). |

Los tableros CAA no viven en la BD: se serializan como JSON en la carpeta de
medios de cada paciente (`caa_boards.json`).

## Principios de diseño

- **Cero sobrecarga cognitiva**: interfaces minimalistas para el paciente.
- **Cero modales**: errores y estados vacíos se renderizan inline en la vista.
- **Privacidad**: 100 % offline, datos locales.
- **Accesibilidad**: alto contraste, tipografías escalables, modo claro/oscuro.
6 changes: 5 additions & 1 deletion margoth_installer.iss
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
[Setup]
AppName=Margoth
AppVersion=1.0.0
AppVersion=1.1.0
AppVerName=Margoth 1.1.0
AppPublisher=Carlos G
AppComments=Rehabilitación cognitiva y del lenguaje, 100% offline
DefaultDirName={localappdata}\Margoth
DefaultGroupName=Margoth
OutputBaseFilename=Margoth_Setup
OutputDir=dist
Compression=lzma
SolidCompression=yes
PrivilegesRequired=lowest
SetupIconFile=assets\icon.ico
UninstallDisplayIcon={app}\Margoth.exe
ArchitecturesInstallIn64BitMode=x64

Expand Down
4 changes: 4 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[pytest]
testpaths = tests
python_files = test_*.py
addopts = -q
2 changes: 2 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-r requirements.txt
pytest>=8.0.0
21 changes: 21 additions & 0 deletions src/controllers/app_controller.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
from models.caa_board_model import CAABoardModel
from models.semantic_exercise_model import SemanticExerciseModel
from models.metrics_model import MetricsModel
from views.main_window import MainWindow
from views.dashboard_view import DashboardView
from views.builder_view import BuilderView
from views.semantic_exercise_view import SemanticExerciseView
from views.reports_view import ReportsView
from controllers.dashboard_controller import DashboardController
from controllers.caa_controller import CAAController
from controllers.builder_controller import BuilderController
from controllers.semantic_controller import SemanticController
from controllers.reports_controller import ReportsController


class AppController:
Expand All @@ -34,12 +37,20 @@ def __init__(self, theme_manager, db_manager=None, patient_model=None):
self.semantic_view = SemanticExerciseView()
self.semantic_model = SemanticExerciseModel(self.db_manager, self.patient_model)
self.semantic_controller = SemanticController(self.semantic_view, self.semantic_model)

self.reports_view = ReportsView()
self.reports_controller = ReportsController(
self.reports_view, MetricsModel(self.db_manager)
)

self.dashboard_view.caa_requested.connect(self._on_caa_requested)
self.dashboard_view.builder_requested.connect(self._on_builder_requested)
self.dashboard_view.exercise_requested.connect(self._on_exercise_requested)
self.dashboard_view.reports_requested.connect(self._on_reports_requested)
self.caa_controller.view.back_requested.connect(self._on_caa_back)
self.builder_view.back_requested.connect(self._on_builder_back)
self.semantic_view.exit_requested.connect(self._on_exercise_exit)
self.reports_view.back_requested.connect(self._on_reports_back)
self.main_window.set_main_view(self.dashboard_view)
self.dashboard_controller.initialize()

Expand Down Expand Up @@ -79,3 +90,13 @@ def _on_exercise_requested(self, patient_ref):

def _on_exercise_exit(self):
self.main_window.set_main_view(self.dashboard_view)

def _on_reports_requested(self, patient_ref):
patient_id = patient_ref.get("id")
patient_dict = self.patient_model.get_patient_by_id(patient_id)
if patient_dict:
self.reports_controller.load_report(patient_dict)
self.main_window.set_main_view(self.reports_view)

def _on_reports_back(self):
self.main_window.set_main_view(self.dashboard_view)
25 changes: 24 additions & 1 deletion src/controllers/builder_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ def __init__(self, view, patient_model, caa_model):
self._current_patient = None

self._view.board_save_requested.connect(self._on_save_requested)
self._view.board_selected.connect(self._on_board_selected)
self._view.new_board_requested.connect(self._on_new_board)

@property
def view(self):
Expand All @@ -17,7 +19,24 @@ def load_patient(self, patient_dict):
images = self._patient_model.get_patient_media(patient_id, "image")
audios = self._patient_model.get_patient_media(patient_id, "audio")
self._view.set_gallery(images, audios)
self._view.reset_board()
self._refresh_boards()

def _refresh_boards(self, active_id=None):
boards = self._caa_model.list_boards(self._current_patient)
self._view.set_boards(boards, active_id)
if boards:
target = active_id or boards[0]["id"]
board, _ = self._caa_model.load_board(self._current_patient, target)
self._view.load_board_data(board)
else:
self._view.new_board()

def _on_board_selected(self, board_id):
board, _ = self._caa_model.load_board(self._current_patient, board_id)
self._view.load_board_data(board)

def _on_new_board(self):
self._view.new_board()

def _on_save_requested(self, board_data):
if not self._current_patient:
Expand All @@ -27,5 +46,9 @@ def _on_save_requested(self, board_data):
saved = self._caa_model.save_board(self._current_patient, board_data)
if saved:
self._view.show_message("Tablero guardado correctamente")
# Refresca el selector conservando el tablero recién guardado activo
boards = self._caa_model.list_boards(self._current_patient)
active = board_data.get("id") or (boards[-1]["id"] if boards else None)
self._view.set_boards(boards, active)
else:
self._view.show_message("Error guardando tablero", is_error=True)
12 changes: 11 additions & 1 deletion src/controllers/caa_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,24 @@ def __init__(self, db_manager):

self._view.item_clicked.connect(self._on_item_clicked)
self._view.generate_test_requested.connect(self._on_generate_test)
self._view.board_change_requested.connect(self._on_board_changed)

@property
def view(self):
return self._view

def load_patient_board(self, patient_dict):
self._current_patient = patient_dict
board, items = self._model.load_first_board(patient_dict)
boards = self._model.list_boards(patient_dict)
active_id = boards[0]["id"] if boards else None
self._view.set_boards(boards, active_id)
board, items = self._model.load_board(patient_dict, active_id)
self._view.render_board(board, items)

def _on_board_changed(self, board_id):
if not self._current_patient:
return
board, items = self._model.load_board(self._current_patient, board_id)
self._view.render_board(board, items)

def _on_item_clicked(self, audio_path):
Expand Down
Loading
Loading